Agenda, Agendador, Configurações
This commit is contained in:
@@ -1,124 +1,189 @@
|
||||
// src/features/agenda/services/agendaMappers.js
|
||||
//
|
||||
// Suporta dois tipos de linha:
|
||||
// 1. Evento real (agenda_eventos do banco) — is_occurrence = false/undefined
|
||||
// 2. Ocorrência virtual (gerada por useRecurrence) — is_occurrence = true
|
||||
//
|
||||
// Em ambos os casos o shape de saída para o FullCalendar é idêntico.
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// mapAgendaEventosToCalendarEvents
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function mapAgendaEventosToCalendarEvents (rows) {
|
||||
return (rows || []).map((r) => {
|
||||
// 🔥 regra importante:
|
||||
// prioridade: owner_id
|
||||
// fallback: terapeuta_id
|
||||
const ownerId = normalizeId(r?.owner_id ?? r?.terapeuta_id ?? null)
|
||||
|
||||
const commitment = r.determined_commitments
|
||||
const bgColor = commitment?.bg_color ? `#${commitment.bg_color}` : undefined
|
||||
const txtColor = commitment?.text_color || undefined
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
title: r.titulo || tituloFallback(r.tipo),
|
||||
start: r.inicio_em,
|
||||
end: r.fim_em,
|
||||
...(bgColor && { backgroundColor: bgColor, borderColor: bgColor }),
|
||||
...(txtColor && { textColor: txtColor }),
|
||||
extendedProps: {
|
||||
// 🔥 ESSENCIAL PARA O MOSAICO
|
||||
owner_id: ownerId,
|
||||
|
||||
tipo: r.tipo ?? null,
|
||||
status: r.status ?? null,
|
||||
|
||||
paciente_id: r.paciente_id ?? null,
|
||||
paciente_nome: r.patients?.nome_completo ?? null,
|
||||
paciente_avatar: r.patients?.avatar_url ?? null,
|
||||
terapeuta_id: r.terapeuta_id ?? null,
|
||||
|
||||
observacoes: r.observacoes ?? null,
|
||||
|
||||
// ✅ usados na clínica p/ mascarar/privacidade
|
||||
visibility_scope: r.visibility_scope ?? null,
|
||||
masked: !!r.masked,
|
||||
|
||||
// ✅ compromisso determinístico
|
||||
determined_commitment_id: r.determined_commitment_id ?? null,
|
||||
commitment_bg_color: bgColor ?? null,
|
||||
commitment_text_color: txtColor ?? null,
|
||||
|
||||
// ✅ campos customizados
|
||||
titulo_custom: r.titulo_custom ?? null,
|
||||
extra_fields: r.extra_fields ?? null
|
||||
}
|
||||
}
|
||||
})
|
||||
return (rows || []).map(_mapRow).filter(Boolean)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// mapAgendaEventosToClinicResourceEvents
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function mapAgendaEventosToClinicResourceEvents (rows) {
|
||||
return (rows || []).map((r) => {
|
||||
const ev = _mapRow(r)
|
||||
if (!ev) return null
|
||||
ev.resourceId = normalizeId(r?.owner_id ?? r?.terapeuta_id ?? null)
|
||||
return ev
|
||||
}).filter(Boolean)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// mapper interno
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function _mapRow (r) {
|
||||
if (!r) return null
|
||||
|
||||
const isOccurrence = !!r.is_occurrence
|
||||
const isRealSession = !isOccurrence
|
||||
|
||||
const ownerId = normalizeId(r?.owner_id ?? r?.terapeuta_id ?? null)
|
||||
|
||||
// commitment / cores
|
||||
const commitment = r.determined_commitments ?? r.commitment ?? null
|
||||
const baseBg = commitment?.bg_color ? `#${commitment.bg_color}` : null
|
||||
const baseTxt = commitment?.text_color ? `#${commitment.text_color}` : null
|
||||
const statusBg = _statusBgColor(r.status)
|
||||
const bgColor = statusBg ?? baseBg ?? undefined
|
||||
const txtColor = baseTxt ?? (statusBg ? '#ffffff' : undefined)
|
||||
|
||||
// título
|
||||
const nomeP = r.patients?.nome_completo ?? r.paciente_nome ?? r.patient_name ?? ''
|
||||
const titleBase = r.titulo_custom || r.titulo || (nomeP ? nomeP : tituloFallback(r.tipo))
|
||||
const icon = _statusIcon(r.status, isOccurrence, !!r.recurrence_id)
|
||||
const title = `${icon}${titleBase}`
|
||||
|
||||
// recorrência — nova + fallback legada
|
||||
const recurrenceId = r.recurrence_id ?? null
|
||||
const originalDate = r.original_date ?? r.recurrence_date ?? null
|
||||
const exceptionType = r.exception_type ?? null
|
||||
|
||||
return {
|
||||
id: r.id ?? `occ::${recurrenceId}::${originalDate}`,
|
||||
title,
|
||||
start: r.inicio_em,
|
||||
end: r.fim_em,
|
||||
|
||||
...(bgColor && { backgroundColor: bgColor, borderColor: bgColor }),
|
||||
...(txtColor && { textColor: txtColor }),
|
||||
|
||||
extendedProps: {
|
||||
// identidade
|
||||
dbId: r.id ?? null,
|
||||
isOccurrence,
|
||||
isRealSession,
|
||||
|
||||
// owner
|
||||
owner_id: ownerId,
|
||||
terapeuta_id: normalizeId(r?.terapeuta_id ?? null),
|
||||
|
||||
// compromisso
|
||||
tipo: r.tipo ?? null,
|
||||
status: r.status ?? null,
|
||||
determined_commitment_id: r.determined_commitment_id ?? null,
|
||||
commitment_bg_color: bgColor ?? null,
|
||||
commitment_text_color: txtColor ?? null,
|
||||
|
||||
// paciente
|
||||
patient_id: r.patient_id ?? null,
|
||||
paciente_id: r.patient_id ?? null, // alias para compatibilidade com dialog/form
|
||||
paciente_nome: nomeP,
|
||||
paciente_avatar: r.patients?.avatar_url ?? r.paciente_avatar ?? null,
|
||||
|
||||
// campos
|
||||
observacoes: r.observacoes ?? null,
|
||||
titulo_custom: r.titulo_custom ?? null,
|
||||
extra_fields: r.extra_fields ?? null,
|
||||
modalidade: r.modalidade ?? null,
|
||||
|
||||
// privacidade (clínica)
|
||||
visibility_scope: r.visibility_scope ?? null,
|
||||
masked: !!r.masked,
|
||||
|
||||
// recorrência — NOVA arquitetura
|
||||
recurrence_id: recurrenceId,
|
||||
original_date: originalDate,
|
||||
exception_type: exceptionType,
|
||||
exception_id: r.exception_id ?? null,
|
||||
exception_reason: r.exception_reason ?? null,
|
||||
|
||||
// recorrência — fallback LEGADA (não quebra enquanto migra)
|
||||
serie_id: r.serie_id ?? recurrenceId ?? null,
|
||||
serie_dia_semana: r.agenda_series?.dia_semana ?? r.serie_dia_semana ?? null,
|
||||
serie_hora: r.agenda_series?.hora_inicio ?? r.serie_hora ?? null,
|
||||
serie_duracao: r.agenda_series?.duracao_min ?? r.serie_duracao ?? null,
|
||||
serie_status: r.agenda_series?.status ?? r.serie_status ?? null,
|
||||
is_exception: r.is_exception ?? (exceptionType != null),
|
||||
|
||||
// financeiro
|
||||
price: r.price ?? null,
|
||||
|
||||
// timestamps
|
||||
inicio_em: r.inicio_em,
|
||||
fim_em: r.fim_em,
|
||||
tenant_id: r.tenant_id ?? null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// buildNextSessions
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function buildNextSessions (rows, now = new Date()) {
|
||||
const nowMs = now.getTime()
|
||||
return (rows || [])
|
||||
.filter((r) => new Date(r.fim_em).getTime() >= nowMs)
|
||||
.filter(r => new Date(r.fim_em).getTime() >= nowMs)
|
||||
.slice(0, 6)
|
||||
.map((r) => ({
|
||||
id: r.id,
|
||||
title: r.titulo || tituloFallback(r.tipo),
|
||||
.map(r => ({
|
||||
id: r.id,
|
||||
title: r.titulo || tituloFallback(r.tipo),
|
||||
startISO: r.inicio_em,
|
||||
endISO: r.fim_em,
|
||||
tipo: r.tipo,
|
||||
status: r.status,
|
||||
pacienteId: r.paciente_id || null
|
||||
endISO: r.fim_em,
|
||||
tipo: r.tipo,
|
||||
status: r.status,
|
||||
pacienteId: r.patient_id || null
|
||||
}))
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// calcDefaultSlotDuration
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function calcDefaultSlotDuration (settings) {
|
||||
const min =
|
||||
((settings?.usar_granularidade_custom && settings?.granularidade_min) || 0) ||
|
||||
settings?.admin_slot_visual_minutos ||
|
||||
30
|
||||
|
||||
return minutesToDuration(min)
|
||||
}
|
||||
|
||||
export function minutesToDuration (min) {
|
||||
const h = Math.floor(min / 60)
|
||||
const m = min % 60
|
||||
const hh = String(h).padStart(2, '0')
|
||||
const mm = String(m).padStart(2, '0')
|
||||
return `${hh}:${mm}:00`
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// buildWeeklyBreakBackgroundEvents — código original preservado integralmente
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function tituloFallback (tipo) {
|
||||
const t = String(tipo || '').toLowerCase()
|
||||
if (t.includes('sess')) return 'Sessão'
|
||||
if (t.includes('block') || t.includes('bloq')) return 'Bloqueio'
|
||||
if (t.includes('pessoal')) return 'Pessoal'
|
||||
if (t.includes('clin')) return 'Clínica'
|
||||
return 'Compromisso'
|
||||
}
|
||||
|
||||
/**
|
||||
* Pausas semanais (jsonb) -> background events do FullCalendar.
|
||||
* Leitura flexível:
|
||||
* - esperado: [{ weekday: 1..7 ou 0..6, start:"HH:MM", end:"HH:MM", label }]
|
||||
*/
|
||||
export function buildWeeklyBreakBackgroundEvents (pausas, rangeStart, rangeEnd) {
|
||||
if (!Array.isArray(pausas) || pausas.length === 0) return []
|
||||
|
||||
const out = []
|
||||
const out = []
|
||||
const dayMs = 24 * 60 * 60 * 1000
|
||||
|
||||
for (let ts = startOfDay(rangeStart).getTime(); ts < rangeEnd.getTime(); ts += dayMs) {
|
||||
const d = new Date(ts)
|
||||
const dow = d.getDay() // 0..6
|
||||
const d = new Date(ts)
|
||||
const dow = d.getDay()
|
||||
|
||||
for (const p of pausas) {
|
||||
const wd = normalizeWeekday(p?.weekday)
|
||||
if (wd === null) continue
|
||||
if (wd !== dow) continue
|
||||
const wd = normalizeWeekday(p?.weekday ?? p?.dia_semana)
|
||||
if (wd === null || wd !== dow) continue
|
||||
|
||||
const start = asTime(p?.start ?? p?.inicio ?? p?.from)
|
||||
const end = asTime(p?.end ?? p?.fim ?? p?.to)
|
||||
const end = asTime(p?.end ?? p?.fim ?? p?.to)
|
||||
if (!start || !end) continue
|
||||
|
||||
out.push({
|
||||
id: `break-${ts}-${start}-${end}`,
|
||||
start: combineDateTimeISO(d, start),
|
||||
end: combineDateTimeISO(d, end),
|
||||
id: `break-${ts}-${start}-${end}`,
|
||||
start: combineDateTimeISO(d, start),
|
||||
end: combineDateTimeISO(d, end),
|
||||
display: 'background',
|
||||
overlap: false,
|
||||
extendedProps: { kind: 'break', label: p?.label ?? 'Pausa' }
|
||||
@@ -129,48 +194,53 @@ export function buildWeeklyBreakBackgroundEvents (pausas, rangeStart, rangeEnd)
|
||||
return out
|
||||
}
|
||||
|
||||
export function mapAgendaEventosToClinicResourceEvents (rows) {
|
||||
return (rows || []).map((r) => {
|
||||
const ownerId = normalizeId(r?.owner_id ?? r?.terapeuta_id ?? null)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// minutesToDuration / tituloFallback
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const commitment = r.determined_commitments
|
||||
const bgColor = commitment?.bg_color ? `#${commitment.bg_color}` : undefined
|
||||
const txtColor = commitment?.text_color || undefined
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
title: r.titulo || tituloFallback(r.tipo),
|
||||
start: r.inicio_em,
|
||||
end: r.fim_em,
|
||||
|
||||
// 🔥 resourceId também precisa ser confiável
|
||||
resourceId: ownerId,
|
||||
|
||||
...(bgColor && { backgroundColor: bgColor, borderColor: bgColor }),
|
||||
...(txtColor && { textColor: txtColor }),
|
||||
|
||||
extendedProps: {
|
||||
owner_id: ownerId,
|
||||
|
||||
tipo: r.tipo ?? null,
|
||||
status: r.status ?? null,
|
||||
|
||||
paciente_id: r.paciente_id ?? null,
|
||||
terapeuta_id: r.terapeuta_id ?? null,
|
||||
observacoes: r.observacoes ?? null,
|
||||
|
||||
visibility_scope: r.visibility_scope ?? null,
|
||||
masked: !!r.masked,
|
||||
|
||||
determined_commitment_id: r.determined_commitment_id ?? null,
|
||||
commitment_bg_color: bgColor ?? null,
|
||||
commitment_text_color: txtColor ?? null
|
||||
}
|
||||
}
|
||||
})
|
||||
export function minutesToDuration (min) {
|
||||
const h = Math.floor(min / 60)
|
||||
const m = min % 60
|
||||
return `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:00`
|
||||
}
|
||||
|
||||
// -------------------- helpers --------------------
|
||||
export function tituloFallback (tipo) {
|
||||
const t = String(tipo || '').toLowerCase()
|
||||
if (t.includes('sess')) return 'Sessão'
|
||||
if (t.includes('block') || t.includes('bloq')) return 'Bloqueio'
|
||||
if (t.includes('pessoal')) return 'Pessoal'
|
||||
if (t.includes('clin')) return 'Clínica'
|
||||
return 'Compromisso'
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// helpers de status
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function _statusBgColor (status) {
|
||||
const map = {
|
||||
realizado: '#6b7280',
|
||||
faltou: '#ef4444',
|
||||
cancelado: '#f97316',
|
||||
bloqueado: '#6b7280',
|
||||
remarcado: '#a855f7',
|
||||
}
|
||||
return map[status] ?? null
|
||||
}
|
||||
|
||||
function _statusIcon (status, isOccurrence, hasSerie) {
|
||||
if (status === 'realizado') return '✓ '
|
||||
if (status === 'faltou') return '✗ '
|
||||
if (status === 'cancelado') return '∅ '
|
||||
if (status === 'bloqueado') return '⊘ '
|
||||
if (status === 'remarcado') return '↺ '
|
||||
if (hasSerie || isOccurrence) return '↻ '
|
||||
return ''
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// helpers internos — originais preservados
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
function normalizeId (v) {
|
||||
if (v === null || v === undefined) return null
|
||||
@@ -190,7 +260,7 @@ function normalizeWeekday (value) {
|
||||
function asTime (v) {
|
||||
if (!v || typeof v !== 'string') return null
|
||||
const s = v.trim()
|
||||
if (/^\d{2}:\d{2}$/.test(s)) return `${s}:00`
|
||||
if (/^\d{2}:\d{2}$/.test(s)) return `${s}:00`
|
||||
if (/^\d{2}:\d{2}:\d{2}$/.test(s)) return s
|
||||
return null
|
||||
}
|
||||
@@ -203,7 +273,7 @@ function startOfDay (d) {
|
||||
|
||||
function combineDateTimeISO (date, timeHHMMSS) {
|
||||
const yyyy = date.getFullYear()
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(date.getDate()).padStart(2, '0')
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(date.getDate()).padStart(2, '0')
|
||||
return `${yyyy}-${mm}-${dd}T${timeHHMMSS}`
|
||||
}
|
||||
Reference in New Issue
Block a user