agenda C12 UX: "Trocar metodo" em vez de Revogar+Antecipar

Iteracao UX do C12 (antecipar pagamento) — antes user que queria
trocar PIX por dinheiro precisava Revogar (cancela record) +
Antecipar de novo (cria record novo), acumulando lixo no audit
trail (memoria project_c12_antecipar_iterar: ciclos longos chegaram
a 5+ records cancelled num mesmo evento).

MelissaEventoPanel ganha 3 botoes quando isAntecipacaoAtiva:
  - "Trocar metodo"   (default, icone pi-sync)
  - "Revogar pagamento" (danger, icone pi-times-circle)
Antes mostrava so "Revogar".

MelissaLayout:
- anteciparMode ref ('create' | 'update') + onTrocarMetodoAntecipacao
  pre-seleciona o metodo atual lendo o paid record antes de abrir
  o dialog
- confirmAnteciparPagamento ramifica: mode='update' faz UPDATE no
  paid existente (payment_method + paid_at + notes audit "metodo
  trocado: X -> Y"). Sem cancel cycle, sem record novo.
- Dialog header/labels/CTA dinamicos por mode

Result: ciclo trocar metodo agora gera 0 records cancelled (so
update + nota auditoria). Revogar continua disponivel pra quando
realmente precisar cancelar o pagamento.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-21 05:29:02 -03:00
parent d7cd2541e4
commit 9c518a2b44
2 changed files with 118 additions and 15 deletions
+21 -10
View File
@@ -40,6 +40,7 @@ const emit = defineEmits([
'delete-series', // botão "Excluir série inteira" — hard delete da regra + materializadas + records pendentes
'ver-lancamentos', // botão "Lançamentos" — abre dialog com financial_records vinculados
'antecipar-pagamento', // botão "Antecipar pagamento" — paciente quer pagar antes da sessão (pacote saldo)
'trocar-metodo-antecipacao', // botão "Trocar método" — UPDATE no record paid sem cancel+criar novo (evita lixo cancelled)
'gerar-cobranca', // botão inline "Gerar fatura" ao lado de "A cobrar R$ X" — atalho sem precisar abrir dialog
'usar-sessao', // botão "Usar" no card de pacote saldo — materializa+realizada+gera cobrança individual
'revogar-sessao' // botão "Revogar" — desfaz Usar (cancela record + decrementa saldo). Bloqueado se já pago
@@ -507,16 +508,26 @@ function modalidadeIcon(mod) {
<i class="pi pi-money-bill" />
<span class="evento-act__label">Antecipar pagamento</span>
</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>
<template v-else>
<button
class="evento-act"
:disabled="busy"
v-tooltip.top="'Atualizar a forma de pagamento sem cancelar o registro (não gera lixo no histórico)'"
@click="emit('trocar-metodo-antecipacao')"
>
<i class="pi pi-sync" />
<span class="evento-act__label">Trocar método</span>
</button>
<button
class="evento-act evento-act--danger"
:disabled="busy"
v-tooltip.top="'Desfazer o pagamento antecipado cancela o lançamento'"
@click="emit('revogar-antecipacao')"
>
<i class="pi pi-times-circle" />
<span class="evento-act__label">Revogar pagamento</span>
</button>
</template>
</div>
</section>
+97 -5
View File
@@ -759,6 +759,9 @@ const anteciparDialogOpen = ref(false);
const anteciparMethod = ref('pix');
const anteciparBusy = ref(false);
const anteciparEventoRef = ref(null); // snapshot do evento no momento do click
// mode: 'create' (Antecipar pagamento — novo record) vs 'update' (Trocar método
// — UPDATE no record paid existente, sem cancel+criar lixo de cancelled).
const anteciparMode = ref('create');
const anteciparMethodOptions = [
{ value: 'pix', label: 'Já recebi — PIX' },
{ value: 'dinheiro', label: 'Já recebi — Dinheiro' },
@@ -782,6 +785,39 @@ async function onAnteciparPagamento() {
}
anteciparEventoRef.value = ev;
anteciparMethod.value = 'pix';
anteciparMode.value = 'create';
anteciparDialogOpen.value = true;
}
// Trocar método de pagamento de uma antecipação ATIVA (record paid existente).
// Mesmo dialog do Antecipar, mas no submit faz UPDATE no record existente
// em vez de cancel+criar novo — evita acumular records cancelled no audit
// trail. Default seleciona o método atual pra UX clara.
async function onTrocarMetodoAntecipacao() {
const ev = eventoSelecionado.value;
if (!ev?.id) 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;
}
// Busca método atual do record paid pra pré-selecionar no dialog
try {
const { data: paidRec } = await supabase
.from('financial_records')
.select('payment_method')
.eq('agenda_evento_id', ev.id)
.eq('status', 'paid')
.order('paid_at', { ascending: false })
.limit(1)
.maybeSingle();
const current = paidRec?.payment_method;
anteciparMethod.value = ['pix', 'dinheiro', 'deposito', 'cartao_maquininha'].includes(current) ? current : 'pix';
} catch {
anteciparMethod.value = 'pix';
}
anteciparEventoRef.value = ev;
anteciparMode.value = 'update';
anteciparDialogOpen.value = true;
}
@@ -790,6 +826,53 @@ async function confirmAnteciparPagamento() {
if (!ev || anteciparBusy.value) return;
anteciparBusy.value = true;
try {
// ─── MODE 'update': Trocar método ───────────────────────────
// Apenas UPDATE no record paid existente. Sem materializar (já é real),
// sem RPC, sem novo record. Evita lixo cancelled no audit trail.
if (anteciparMode.value === 'update') {
const settlement = anteciparMethod.value;
const today = new Date().toISOString();
const { data: paidRec, error: fetchErr } = await supabase
.from('financial_records')
.select('id, payment_method, notes')
.eq('agenda_evento_id', ev.id)
.eq('status', 'paid')
.order('paid_at', { ascending: false })
.limit(1)
.maybeSingle();
if (fetchErr) throw fetchErr;
if (!paidRec?.id) {
throw new Error('Antecipação não encontrada para troca de método.');
}
const oldMethod = paidRec.payment_method || '—';
const noteEntry = `[${today.slice(0, 10)}] Método trocado: ${oldMethod}${settlement}`;
const newNotes = paidRec.notes ? `${paidRec.notes}\n${noteEntry}` : noteEntry;
const patch = {
payment_method: settlement === 'link' ? 'asaas' : settlement,
status: settlement === 'link' ? 'pending' : 'paid',
paid_at: settlement === 'link' ? null : today,
notes: newNotes,
updated_at: today
};
const { error: upErr } = await supabase
.from('financial_records')
.update(patch)
.eq('id', paidRec.id);
if (upErr) throw upErr;
const methodLabel = anteciparMethodOptions.find((o) => o.value === settlement)?.label || settlement;
toast.add({
severity: 'success',
summary: 'Método atualizado',
detail: methodLabel,
life: 3500
});
anteciparDialogOpen.value = false;
await M.refetch();
refetchEventosHoje();
return;
}
// ─── MODE 'create' (Antecipar pagamento — fluxo original) ──
const tenantId = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
// ownerId: ev.owner_id é prioridade. Fallback pra M.ownerId (composable
// que conhece o user logado). Pra virtuais ou snapshots stale, ev pode
@@ -2629,6 +2712,7 @@ function onKeydown(e) {
@revogar-sessao="onRevogarSessao"
@ver-lancamentos="onVerLancamentos"
@antecipar-pagamento="onAnteciparPagamento"
@trocar-metodo-antecipacao="onTrocarMetodoAntecipacao"
@revogar-antecipacao="onRevogarAntecipacao"
@edit-paciente="onEditPaciente"
@abrir-prontuario="onAbrirProntuario"
@@ -3181,19 +3265,24 @@ function onKeydown(e) {
v-model:visible="anteciparDialogOpen"
modal
:draggable="false"
header="Antecipar pagamento"
:header="anteciparMode === 'update' ? 'Trocar método de pagamento' : 'Antecipar pagamento'"
:style="{ width: '480px', maxWidth: '96vw' }"
>
<div class="flex flex-col gap-3 pt-1">
<div class="text-sm">
Receba antecipadamente o valor desta sessão.
<template v-if="anteciparMode === 'update'">
Atualizar a forma de pagamento sem cancelar o registro atual (mais limpo no histórico).
</template>
<template v-else>
Receba antecipadamente o valor desta sessão.
</template>
</div>
<div v-if="anteciparEventoRef" class="flex flex-col gap-1 px-3 py-2 rounded-md bg-[var(--surface-section)] border border-[var(--surface-border)]">
<div class="text-sm font-semibold">{{ anteciparEventoRef.pacienteNome || 'Sessão' }}</div>
<div class="text-xs opacity-70">Valor: R$ {{ Number(anteciparEventoRef.price || 0).toFixed(2).replace('.', ',') }}</div>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-xs font-medium">Como o paciente pagou?</label>
<label class="text-xs font-medium">{{ anteciparMode === 'update' ? 'Novo método de pagamento' : 'Como o paciente pagou?' }}</label>
<Select
v-model="anteciparMethod"
:options="anteciparMethodOptions"
@@ -3202,13 +3291,16 @@ function onKeydown(e) {
size="small"
/>
</div>
<small class="text-xs opacity-60">
<small v-if="anteciparMode !== 'update'" class="text-xs opacity-60">
O saldo do pacote será decrementado quando você marcar a sessão como Realizada.
</small>
<small v-else class="text-xs opacity-60">
A troca é registrada no histórico do lançamento (auditoria), sem criar novo registro.
</small>
</div>
<template #footer>
<Button label="Cancelar" severity="secondary" outlined :disabled="anteciparBusy" @click="anteciparDialogOpen = false" />
<Button label="Confirmar" icon="pi pi-check" :loading="anteciparBusy" @click="confirmAnteciparPagamento" />
<Button :label="anteciparMode === 'update' ? 'Atualizar' : 'Confirmar'" icon="pi pi-check" :loading="anteciparBusy" @click="confirmAnteciparPagamento" />
</template>
</Dialog>