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(() => { - +
-
-
-
-
-
Financeiro — Fase 6
-
Recebíveis, atrasos, histórico
-
-
-
-

- 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) }}. -

-
+ +
+ Carregando…
+ + +
+
+
Sem lançamentos financeiros
+
+ Adicione o primeiro lançamento de cobrança ou recebimento deste paciente. +
+ +
+ +
@@ -2498,6 +2662,189 @@ onBeforeUnmount(() => { font-size: 0.66rem !important; } +/* ═══════ Tab Financeiro (Fase 6) ═══════ */ +.mpa-fin__head-actions { + display: flex; + align-items: center; + gap: 6px; +} +.mpa-icon-btn-sm { + width: 26px; + height: 26px; + display: grid; + place-items: center; + border-radius: 6px; + background: var(--m-bg-medium); + border: 1px solid var(--m-border); + color: var(--p-primary-color); + cursor: pointer; + font-family: inherit; + transition: background-color 120ms ease, border-color 120ms ease; +} +.mpa-icon-btn-sm:hover { + background: color-mix(in srgb, var(--p-primary-color) 14%, transparent); + border-color: color-mix(in srgb, var(--p-primary-color) 38%, transparent); +} +.mpa-icon-btn-sm > i { font-size: 0.78rem; } + +/* Quick btn variant CTA pra empty state */ +.mpa-quick-btn--cta { + margin: 12px auto 0; + padding: 10px 16px; + background: var(--m-bg-medium); + border-color: var(--m-border-strong); +} + +/* Tabela financeira */ +.mpa-fin__table { + display: flex; + flex-direction: column; +} +.mpa-fin__row { + display: grid; + grid-template-columns: 110px 1fr 90px 100px 100px 36px; + gap: 12px; + align-items: center; + padding: 10px 14px; + border-top: 1px solid var(--m-border); + font-size: 0.82rem; + border-left: 3px solid transparent; + transition: background-color 120ms ease; +} +.mpa-fin__row:first-child { border-top: none; } +.mpa-fin__row:hover:not(.mpa-fin__row--head) { background: var(--m-bg-medium); } +.mpa-fin__row[data-status="pago"] { border-left-color: rgb(34, 197, 94); } +.mpa-fin__row[data-status="vencido"] { border-left-color: rgb(239, 68, 68); } +.mpa-fin__row[data-status="pendente"] { border-left-color: rgb(96, 165, 250); } + +.mpa-fin__row--head { + background: var(--m-bg-medium); + font-size: 0.66rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--m-text-muted); + border-left-color: transparent !important; +} + +.mpa-fin__date { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} +.mpa-fin__date-main { + font-weight: 700; + color: var(--m-text); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.78rem; +} +.mpa-fin__date-rel { + font-size: 0.66rem; + color: var(--m-text-muted); +} + +.mpa-fin__desc { + color: var(--m-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} +.mpa-fin__method { + color: var(--m-text-muted); + font-size: 0.74rem; +} +.mpa-fin__amount { + font-weight: 800; + color: var(--m-text); + text-align: right; + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.82rem; +} + +.mpa-fin__col-amount { text-align: right; } +.mpa-fin__col-method, +.mpa-fin__col-status, +.mpa-fin__col-action { text-align: center; } + +.mpa-fin__status { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 10px; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 700; + background: var(--m-bg-medium); + border: 1px solid var(--m-border); +} +.mpa-fin__status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; +} +.mpa-fin__status[data-status="pago"] { + background: color-mix(in srgb, rgb(34, 197, 94) 12%, transparent); + color: rgb(34, 197, 94); + border-color: color-mix(in srgb, rgb(34, 197, 94) 35%, transparent); +} +.mpa-fin__status[data-status="vencido"] { + background: color-mix(in srgb, rgb(239, 68, 68) 12%, transparent); + color: rgb(239, 68, 68); + border-color: color-mix(in srgb, rgb(239, 68, 68) 35%, transparent); +} +.mpa-fin__status[data-status="pendente"] { + background: color-mix(in srgb, rgb(96, 165, 250) 12%, transparent); + color: rgb(96, 165, 250); + border-color: color-mix(in srgb, rgb(96, 165, 250) 35%, transparent); +} + +.mpa-fin__action { + display: flex; + justify-content: center; +} + +/* Mobile: stack tabela em cards 2-col */ +@media (max-width: 720px) { + .mpa-fin__row--head { display: none; } + .mpa-fin__row { + grid-template-columns: 1fr auto; + grid-template-rows: auto auto auto; + gap: 4px 10px; + padding: 12px 14px; + } + .mpa-fin__date { + grid-row: 1; + grid-column: 1; + } + .mpa-fin__amount { + grid-row: 1; + grid-column: 2; + text-align: right; + } + .mpa-fin__desc { + grid-row: 2; + grid-column: 1 / -1; + font-size: 0.78rem; + } + .mpa-fin__method { + grid-row: 3; + grid-column: 1; + } + .mpa-fin__status-cell { + grid-row: 3; + grid-column: 2; + text-align: right; + } + .mpa-fin__action { + grid-row: 4; + grid-column: 1 / -1; + justify-content: flex-end; + } +} + /* ═══════ Tab Agenda (Fase 5) ═══════ */ .mpa-ag__group + .mpa-ag__group { margin-top: 10px; } .mpa-ag__list {