Agenda, Agendador, Configurações

This commit is contained in:
Leonardo
2026-03-12 08:58:36 -03:00
parent f733db8436
commit f4b185ae17
197 changed files with 33405 additions and 6507 deletions
+198 -128
View File
@@ -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}`
}