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:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user