MelissaPaciente Fase 6: Tab Financeiro completa + mark paid (mutation que legacy nao tem)
EXTENSAO src/features/patients/utils/patientFormatters.js
- recordStatus(r): pago / vencido (paid_at NULL && due_date < hoje) / pendente
- RECORD_STATUS_LABEL map
- fmtPaymentMethod(v): PIX/Cartao/Dinheiro/Boleto/Transferencia/Convenio
cobrindo variantes pt-br + camelCase
EXTENSAO src/features/patients/composables/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 via _lastPatientId. Retorna {ok, error?}
- markUnpaid(recordId): reverte (paid_at=NULL) + auto-reload
MELISSAPACIENTE.VUE — script
- Imports: recordStatus, RECORD_STATUS_LABEL, fmtPaymentMethod
- markRecordPaid(r): chama financialHook.markPaid + toast success/error
- revertRecordPaid(r): chama markUnpaid + toast
MELISSAPACIENTE.VUE — Tab Financeiro reescrita (substitui placeholder Fase 1)
- Loading state
- Empty state com CTA "Novo lancamento" (mpa-quick-btn--cta)
- 3 KPIs: Pago / Pendente com proxVenc / Em atraso (cor adaptativa
vermelho quando > 0, cinza quando 0)
- Header "Lancamentos" com badge count + botao "+ Novo" no canto
- Tabela 6-col responsiva:
- Vencimento (date mono + relative)
- Descricao
- Forma (PIX/Cartao/etc)
- Valor (mono right-aligned)
- Status pill colorida (verde pago / vermelho vencido / azul pendente)
- Action button (pi-check verde marca pago / pi-undo amarelo reverte)
- border-left adaptativa por status
- Mobile: tabela colapsa em cards 2-col 4-row
DIFERENCA DO LEGACY: o PatientProntuario.vue exibe a tabela mas NAO
permite marcar pago/reverter direto dela. MelissaPaciente adiciona essa
acao inline (mutation auto-reload).
CSS: ~190L novos. Padrao Melissa: status pills com color-mix, JetBrains
Mono pra valores, header cell uppercase letter-spacing.
ESLint: 0 errors da minha mudanca.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user