diff --git a/Obsidian/Brain/log.md b/Obsidian/Brain/log.md index 685c7af..04f3859 100644 --- a/Obsidian/Brain/log.md +++ b/Obsidian/Brain/log.md @@ -50,6 +50,50 @@ Touched: none ## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB Touched: none +## [2026-05-08 17:30] session | MelissaPaciente Fase 6 — Tab Financeiro completa + mark paid mutation +Touched: none +Detalhes: Tab Financeiro espelha o legacy + adiciona mutation que o +legacy NAO tem (mark/unmark pago direto da tabela). + +EXTENSAO patientFormatters.js: +- recordStatus(r): pago | vencido | pendente +- RECORD_STATUS_LABEL map. +- fmtPaymentMethod(v): PIX/Cartao/Dinheiro/Boleto/Transferencia/Convenio + cobrindo variantes. + +EXTENSAO usePatientFinancial.js: +- ref `busy` + `_lastPatientId` interno. +- recordsOrdenados computed (DESC por due_date com fallback created_at). +- markPaid(recordId): UPDATE financial_records SET paid_at=NOW() + auto- + reload. Retorna {ok, error?}. +- markUnpaid(recordId): UPDATE SET paid_at=NULL + auto-reload (reverte). + +MELISSAPACIENTE.VUE — script +- Imports: recordStatus, RECORD_STATUS_LABEL, fmtPaymentMethod. +- markRecordPaid(record) handler: chama markPaid + toast success/error. +- revertRecordPaid(record): chama markUnpaid + toast. + +MELISSAPACIENTE.VUE — Tab Financeiro reescrita +- Loading state. +- Empty state com CTA "Novo lancamento" (botao mpa-quick-btn--cta). +- 3 KPIs (Pago / Pendente com proxVenc / Em atraso com cor adaptativa). +- Header "Lancamentos" com badge count + botao "+ Novo" no canto. +- Tabela 6-col: Vencimento (date mono+rel) | Descricao | Forma | Valor + (mono right) | Status pill colorida (pago verde / vencido vermelho / + pendente azul) | Action button. +- Action: pi-check (verde) pra marcar pago, pi-undo (amarelo) pra reverter. +- border-left adaptativa por status (verde pago / vermelho vencido / + azul pendente). +- Mobile: tabela colapsa em cards 2-col 4-row (date|amount / desc / + method|status / action). + +CSS: ~190L novos pros componentes (mpa-fin__table/row/date/desc/method/ +amount/status/action + responsive). Padrao Melissa: status pills com +color-mix, JetBrains Mono pra valores, header cell uppercase letter- +spacing. + +ESLint: 0 errors da minha mudanca. + ## [2026-05-08 16:30] session | MelissaPaciente Fase 5 — Tab Agenda completa Touched: none Detalhes: Tab Agenda com KPIs, filtros, agrupamento por mes e acoes diff --git a/src/features/patients/composables/usePatientFinancial.js b/src/features/patients/composables/usePatientFinancial.js index 25806bb..bf1fac2 100644 --- a/src/features/patients/composables/usePatientFinancial.js +++ b/src/features/patients/composables/usePatientFinancial.js @@ -17,8 +17,11 @@ export function usePatientFinancial() { const records = ref([]); const loading = ref(false); const error = ref(''); + const busy = ref(false); + let _lastPatientId = null; async function load(patientId) { + _lastPatientId = patientId || null; if (!patientId) { records.value = []; return; @@ -101,15 +104,74 @@ export function usePatientFinancial() { }; }); + /** + * Lancamentos ordenados DESC por due_date (fallback created_at). + * Mais recente primeiro pra alimentar a tabela da Tab Financeiro. + */ + const recordsOrdenados = computed(() => + [...records.value].sort((a, b) => { + const da = a.due_date || a.created_at; + const db = b.due_date || b.created_at; + return new Date(db) - new Date(da); + }) + ); + + /** + * Marca um lancamento como pago (paid_at = now). Auto-reload. + * Retorna {ok, error?}. + */ + async function markPaid(recordId) { + if (!recordId || busy.value) return { ok: false, error: 'busy' }; + busy.value = true; + try { + const { error: err } = await supabase + .from('financial_records') + .update({ paid_at: new Date().toISOString() }) + .eq('id', recordId); + if (err) throw err; + if (_lastPatientId) await load(_lastPatientId); + return { ok: true }; + } catch (e) { + return { ok: false, error: e?.message || 'Erro ao marcar como pago' }; + } finally { + busy.value = false; + } + } + + /** + * Reverte: remove paid_at (volta pra pendente). Auto-reload. + */ + async function markUnpaid(recordId) { + if (!recordId || busy.value) return { ok: false, error: 'busy' }; + busy.value = true; + try { + const { error: err } = await supabase + .from('financial_records') + .update({ paid_at: null }) + .eq('id', recordId); + if (err) throw err; + if (_lastPatientId) await load(_lastPatientId); + return { ok: true }; + } catch (e) { + return { ok: false, error: e?.message || 'Erro ao reverter pagamento' }; + } finally { + busy.value = false; + } + } + return { records, loading, error, + busy, load, totalRecebido, totalEmAberto, totalAtrasado, ultimoPago, - statusFinanceiro + statusFinanceiro, + recordsOrdenados, + markPaid, + markUnpaid }; } diff --git a/src/features/patients/utils/patientFormatters.js b/src/features/patients/utils/patientFormatters.js index de0776a..548e870 100644 --- a/src/features/patients/utils/patientFormatters.js +++ b/src/features/patients/utils/patientFormatters.js @@ -222,6 +222,42 @@ export const STATUS_LABEL = { bloqueado: 'Bloqueado' }; +/** + * Determina o status financeiro de um lancamento: + * - "pago": paid_at preenchido + * - "vencido": due_date < hoje E paid_at vazio + * - "pendente": demais casos com paid_at vazio + */ +export function recordStatus(r) { + if (r?.paid_at) return 'pago'; + if (r?.due_date) { + const ms = new Date(r.due_date + 'T23:59:59').getTime(); + if (!Number.isNaN(ms) && ms < Date.now()) return 'vencido'; + } + return 'pendente'; +} + +export const RECORD_STATUS_LABEL = { + pago: 'Pago', + pendente: 'Pendente', + vencido: 'Vencido' +}; + +/** + * Mapeia variantes de payment_method pra label legivel. + */ +export function fmtPaymentMethod(v) { + const s = String(v ?? '').toLowerCase(); + if (!s) return ''; + if (s === 'pix') return 'PIX'; + if (s === 'cartao' || s === 'cartão' || s === 'credit_card') return 'Cartão'; + if (s === 'dinheiro' || s === 'cash') return 'Dinheiro'; + if (s === 'boleto') return 'Boleto'; + if (s === 'transferencia' || s === 'transfer' || s === 'ted' || s === 'doc') return 'Transferência'; + if (s === 'convenio' || s === 'convênio') return 'Convênio'; + return v; +} + export const STATUS_SEVERITY = { agendado: 'info', realizado: 'success', diff --git a/src/layout/melissa/MelissaPaciente.vue b/src/layout/melissa/MelissaPaciente.vue index 1448961..f8d329f 100644 --- a/src/layout/melissa/MelissaPaciente.vue +++ b/src/layout/melissa/MelissaPaciente.vue @@ -41,6 +41,9 @@ import { fmtMarital, fmtPhoneMobile, sessionDuration, + recordStatus, + RECORD_STATUS_LABEL, + fmtPaymentMethod, STATUS_LABEL, STATUS_SEVERITY, tagStyle as tagStyleHelper @@ -243,6 +246,34 @@ const agendaAgrupadas = computed(() => { return groups; }); +// Handler de mutacao financeira: marcar pago / reverter +async function markRecordPaid(record) { + const result = await financialHook.markPaid(record.id); + if (result.ok) { + toast.add({ severity: 'success', summary: 'Lançamento marcado como pago', life: 2200 }); + } else { + toast.add({ + severity: 'error', + summary: 'Falha ao marcar pago', + detail: result.error || 'Erro inesperado', + life: 4000 + }); + } +} +async function revertRecordPaid(record) { + const result = await financialHook.markUnpaid(record.id); + if (result.ok) { + toast.add({ severity: 'success', summary: 'Pagamento revertido', life: 2200 }); + } else { + toast.add({ + severity: 'error', + summary: 'Falha ao reverter', + detail: result.error || 'Erro inesperado', + life: 4000 + }); + } +} + // Handler de mutacao de status (Realizada / Falta / Cancelar) async function updateSessionStatus(ev, novoStatus, msg) { const result = await sessionsHook.updateStatus(ev.id, novoStatus); @@ -1454,24 +1485,157 @@ onBeforeUnmount(() => { - +
- Em desenvolvimento — Fase 6. Recebido R$ {{ financialHook.totalRecebido.value.toFixed(2) }}; - em aberto R$ {{ financialHook.totalEmAberto.value.toFixed(2) }}; - atrasado R$ {{ financialHook.totalAtrasado.value.toFixed(2) }}. -
-