/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Criado e desenvolvido por Leonardo Nohama | | Tecnologia aplicada à escuta. | Estrutura para o cuidado. | | Arquivo: src/features/agenda/services/agendaMappers.js | Data: 2026 | Local: São Carlos/SP — Brasil |-------------------------------------------------------------------------- | © 2026 — Todos os direitos reservados |-------------------------------------------------------------------------- */ // 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(_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, paciente_status: r.patients?.status ?? r.paciente_status ?? 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, billed: r.billed ?? false, billing_contract_id: r.billing_contract_id ?? null, insurance_plan_id: r.insurance_plan_id ?? null, insurance_guide_number: r.insurance_guide_number ?? null, insurance_value: r.insurance_value != null ? Number(r.insurance_value) : null, insurance_plan_service_id: r.insurance_plan_service_id ?? 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) .slice(0, 6) .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.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); } // ───────────────────────────────────────────────────────────────────────────── // buildWeeklyBreakBackgroundEvents — código original preservado integralmente // ───────────────────────────────────────────────────────────────────────────── export function buildWeeklyBreakBackgroundEvents(pausas, rangeStart, rangeEnd) { if (!Array.isArray(pausas) || pausas.length === 0) return []; 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(); for (const p of pausas) { 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); if (!start || !end) continue; out.push({ id: `break-${ts}-${start}-${end}`, start: combineDateTimeISO(d, start), end: combineDateTimeISO(d, end), display: 'background', overlap: false, extendedProps: { kind: 'break', label: p?.label ?? 'Pausa' } }); } } return out; } // ───────────────────────────────────────────────────────────────────────────── // minutesToDuration / tituloFallback // ───────────────────────────────────────────────────────────────────────────── 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`; } 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 = { agendado: '#3b82f6', // azul realizado: '#22c55e', // verde faltou: '#f97316', // laranja cancelado: '#ef4444', // vermelho remarcado: '#a855f7', // roxo bloqueado: '#6b7280' // cinza }; return map[status] ?? null; } function _statusIcon(status, isOccurrence, hasSerie) { if (status === 'realizado') return '✓ '; if (status === 'faltou') return '✗ '; if (status === 'cancelado') return '∅ '; if (status === 'remarcado') return '↺ '; if (status === 'bloqueado') return '⊘ '; if (hasSerie || isOccurrence) return '↻ '; return ''; } // ───────────────────────────────────────────────────────────────────────────── // helpers internos — originais preservados // ───────────────────────────────────────────────────────────────────────────── function normalizeId(v) { if (v === null || v === undefined) return null; const s = String(v).trim(); return s ? s : null; } function normalizeWeekday(value) { if (value === null || value === undefined) return null; const n = Number(value); if (Number.isNaN(n)) return null; if (n >= 0 && n <= 6) return n; if (n >= 1 && n <= 7) return n === 7 ? 0 : n; return null; } 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}:\d{2}$/.test(s)) return s; return null; } function startOfDay(d) { const x = new Date(d); x.setHours(0, 0, 0, 0); return x; } 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'); return `${yyyy}-${mm}-${dd}T${timeHHMMSS}`; }