agenda: revogar antecipacao de pagamento
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) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ const emit = defineEmits([
|
|||||||
'faltou',
|
'faltou',
|
||||||
'cancelar',
|
'cancelar',
|
||||||
'remarcar',
|
'remarcar',
|
||||||
|
'revogar-antecipacao',
|
||||||
'edit-sessao', // botão dedicado ao lado das horas → AgendaEventDialog
|
'edit-sessao', // botão dedicado ao lado das horas → AgendaEventDialog
|
||||||
'edit-paciente', // botão "Editar" do grupo Outras opções → PatientCadastroDialog
|
'edit-paciente', // botão "Editar" do grupo Outras opções → PatientCadastroDialog
|
||||||
'abrir-prontuario',
|
'abrir-prontuario',
|
||||||
@@ -142,6 +143,13 @@ const isSessaoComPaciente = computed(
|
|||||||
// caso tenha sido marcado por engano.
|
// caso tenha sido marcado por engano.
|
||||||
const isSessaoEncerrada = computed(() => statusSlug.value === 'cancelado' || statusSlug.value === 'faltou');
|
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
|
// Estado de pagamento — vem anotado pelo useMelissaAgenda via bulk-query
|
||||||
// em financial_records. 'paid' | 'pending' | 'none'. Renderiza linha
|
// em financial_records. 'paid' | 'pending' | 'none'. Renderiza linha
|
||||||
// curta abaixo do horário pra sessão com paciente (espelha os 3 canais
|
// curta abaixo do horário pra sessão com paciente (espelha os 3 canais
|
||||||
@@ -490,6 +498,7 @@ function modalidadeIcon(mod) {
|
|||||||
<span class="evento-act__label">Lançamentos</span>
|
<span class="evento-act__label">Lançamentos</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
v-if="!isAntecipacaoAtiva"
|
||||||
class="evento-act"
|
class="evento-act"
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
v-tooltip.top="'Paciente quer pagar antes da sessão (pacote saldo)'"
|
v-tooltip.top="'Paciente quer pagar antes da sessão (pacote saldo)'"
|
||||||
@@ -498,6 +507,16 @@ function modalidadeIcon(mod) {
|
|||||||
<i class="pi pi-money-bill" />
|
<i class="pi pi-money-bill" />
|
||||||
<span class="evento-act__label">Antecipar pagamento</span>
|
<span class="evento-act__label">Antecipar pagamento</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="evento-act evento-act--danger"
|
||||||
|
:disabled="busy"
|
||||||
|
v-tooltip.top="'Desfazer o pagamento antecipado — cancela o lançamento e libera pra antecipar de novo'"
|
||||||
|
@click="emit('revogar-antecipacao')"
|
||||||
|
>
|
||||||
|
<i class="pi pi-times-circle" />
|
||||||
|
<span class="evento-act__label">Revogar pagamento</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
async function onVerLancamentos() {
|
||||||
const ev = eventoSelecionado.value;
|
const ev = eventoSelecionado.value;
|
||||||
if (!ev?.id) return;
|
if (!ev?.id) return;
|
||||||
@@ -2504,6 +2578,7 @@ function onKeydown(e) {
|
|||||||
@revogar-sessao="onRevogarSessao"
|
@revogar-sessao="onRevogarSessao"
|
||||||
@ver-lancamentos="onVerLancamentos"
|
@ver-lancamentos="onVerLancamentos"
|
||||||
@antecipar-pagamento="onAnteciparPagamento"
|
@antecipar-pagamento="onAnteciparPagamento"
|
||||||
|
@revogar-antecipacao="onRevogarAntecipacao"
|
||||||
@edit-paciente="onEditPaciente"
|
@edit-paciente="onEditPaciente"
|
||||||
@abrir-prontuario="onAbrirProntuario"
|
@abrir-prontuario="onAbrirProntuario"
|
||||||
@whatsapp="onWhatsapp"
|
@whatsapp="onWhatsapp"
|
||||||
|
|||||||
Reference in New Issue
Block a user