From 272c8043358eeac39504e96e8ad37e321a951cbb Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 20 May 2026 14:28:04 -0300 Subject: [PATCH] agenda: revogar antecipacao de pagamento MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX gap descoberto durante teste C12: apos antecipar pagamento, nao havia caminho via popover pra desfazer caso user tenha errado (paciente nao pagou, errou o valor, etc). Implementacao: - Botao "Antecipar pagamento" agora alterna pra "Revogar pagamento" (vermelho, --danger) quando isAntecipacaoAtiva (status=agendado + paymentState=paid) - Handler onRevogarAntecipacao em MelissaLayout: ConfirmDialog vermelho + cancela record paid + nota de auditoria em notes ("[YYYY-MM-DD] Antecipacao revogada em ...") + refetch - Apos revogar, botao volta pra "Antecipar pagamento" — user pode antecipar de novo com valor/metodo corretos Limites: so disponivel em status='agendado'. Apos Realizada o paid representa pagamento real da sessao realizada, nao antecipacao — estorno deve ir pelo /financeiro. Sobre "Usar" desaparecer apos antecipar (questao do user): comportamento correto. "Usar" cria record+consome saldo — duplicaria com paid existente. Apos antecipar, fluxo correto e clicar Realizada (que detecta paid pre-existente via fix anterior 00c4168). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/layout/melissa/MelissaEventoPanel.vue | 19 ++++++ src/layout/melissa/MelissaLayout.vue | 75 +++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/src/layout/melissa/MelissaEventoPanel.vue b/src/layout/melissa/MelissaEventoPanel.vue index 0d50c58..4013aca 100644 --- a/src/layout/melissa/MelissaEventoPanel.vue +++ b/src/layout/melissa/MelissaEventoPanel.vue @@ -30,6 +30,7 @@ const emit = defineEmits([ 'faltou', 'cancelar', 'remarcar', + 'revogar-antecipacao', 'edit-sessao', // botão dedicado ao lado das horas → AgendaEventDialog 'edit-paciente', // botão "Editar" do grupo Outras opções → PatientCadastroDialog 'abrir-prontuario', @@ -142,6 +143,13 @@ const isSessaoComPaciente = computed( // caso tenha sido marcado por engano. const isSessaoEncerrada = computed(() => statusSlug.value === 'cancelado' || statusSlug.value === 'faltou'); +// "Antecipação ativa": sessão ainda agendada (não rolou) com cobrança paga. +// O paid não veio de Realizada — veio de "Antecipar pagamento" que adianta +// o pagamento. Nesse estado, o botão "Antecipar pagamento" vira "Revogar +// pagamento" pra desfazer caso o user tenha errado. Após Realizada, o paid +// vira pagamento normal da sessão (estorno via /financeiro). +const isAntecipacaoAtiva = computed(() => statusSlug.value === 'agendado' && ev.value.paymentState === 'paid'); + // Estado de pagamento — vem anotado pelo useMelissaAgenda via bulk-query // em financial_records. 'paid' | 'pending' | 'none'. Renderiza linha // curta abaixo do horário pra sessão com paciente (espelha os 3 canais @@ -490,6 +498,7 @@ function modalidadeIcon(mod) { Lançamentos + diff --git a/src/layout/melissa/MelissaLayout.vue b/src/layout/melissa/MelissaLayout.vue index 739c38c..029d610 100644 --- a/src/layout/melissa/MelissaLayout.vue +++ b/src/layout/melissa/MelissaLayout.vue @@ -879,6 +879,80 @@ async function confirmAnteciparPagamento() { } } +// Revogar antecipação de pagamento (C12): desfaz o `onAnteciparPagamento`. +// Cancela o record paid + nota de auditoria em notes. Só disponível pra +// sessão em status='agendado' (após Realizada o paid vira pagamento normal +// e estorno é via /financeiro). +async function onRevogarAntecipacao() { + const ev = eventoSelecionado.value; + if (!ev?.id || eventoBusy.value) return; + const isVirtualId = typeof ev.id === 'string' && ev.id.startsWith('rec::'); + if (isVirtualId) { + toast.add({ severity: 'warn', summary: 'Sessão virtual', detail: 'Sessão sem antecipação ativa.', life: 3000 }); + return; + } + // Confirma com user — paid é sensível + const ok = await new Promise((resolve) => { + confirm.require({ + message: 'Revogar o pagamento antecipado desta sessão? O lançamento financeiro será cancelado e poderá antecipar de novo.', + header: 'Revogar antecipação?', + icon: 'pi pi-exclamation-triangle', + acceptLabel: 'Revogar pagamento', + rejectLabel: 'Cancelar', + acceptClass: 'p-button-danger', + accept: () => resolve(true), + reject: () => resolve(false), + onHide: () => resolve(false) + }); + }); + if (!ok) return; + + eventoBusy.value = true; + try { + // Acha o paid record vinculado + const { data: paidRec, error: fetchErr } = await supabase + .from('financial_records') + .select('id, notes, payment_method, final_amount, amount') + .eq('agenda_evento_id', ev.id) + .eq('status', 'paid') + .order('paid_at', { ascending: false }) + .limit(1) + .maybeSingle(); + if (fetchErr) throw fetchErr; + if (!paidRec?.id) { + toast.add({ severity: 'info', summary: 'Nada a revogar', detail: 'Esta sessão não tem pagamento antecipado.', life: 3500 }); + return; + } + const today = new Date().toISOString().slice(0, 10); + const reason = `Antecipação revogada em ${today}`; + const noteEntry = `[${today}] ${reason}`; + const noteText = paidRec.notes ? `${paidRec.notes}\n${noteEntry}` : noteEntry; + const { error: cancelErr } = await supabase + .from('financial_records') + .update({ + status: 'cancelled', + notes: noteText, + updated_at: new Date().toISOString() + }) + .eq('id', paidRec.id); + if (cancelErr) throw cancelErr; + + toast.add({ + severity: 'success', + summary: 'Antecipação revogada', + detail: `Cobrança de R$ ${Number(paidRec.final_amount || paidRec.amount || 0).toFixed(2).replace('.', ',')} cancelada.`, + life: 4000 + }); + M.refetch(); + refetchEventosHoje(); + fecharEvento(); + } catch (e) { + toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao revogar antecipação.', life: 5000 }); + } finally { + eventoBusy.value = false; + } +} + async function onVerLancamentos() { const ev = eventoSelecionado.value; if (!ev?.id) return; @@ -2504,6 +2578,7 @@ function onKeydown(e) { @revogar-sessao="onRevogarSessao" @ver-lancamentos="onVerLancamentos" @antecipar-pagamento="onAnteciparPagamento" + @revogar-antecipacao="onRevogarAntecipacao" @edit-paciente="onEditPaciente" @abrir-prontuario="onAbrirProntuario" @whatsapp="onWhatsapp"