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
|
'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
|
'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)
|
'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
|
'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
|
'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
|
'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" />
|
<i class="pi pi-money-bill" />
|
||||||
<span class="evento-act__label">Antecipar pagamento</span>
|
<span class="evento-act__label">Antecipar pagamento</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
v-else
|
|
||||||
class="evento-act evento-act--danger"
|
class="evento-act evento-act--danger"
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
v-tooltip.top="'Desfazer o pagamento antecipado — cancela o lançamento e libera pra antecipar de novo'"
|
v-tooltip.top="'Desfazer o pagamento antecipado — cancela o lançamento'"
|
||||||
@click="emit('revogar-antecipacao')"
|
@click="emit('revogar-antecipacao')"
|
||||||
>
|
>
|
||||||
<i class="pi pi-times-circle" />
|
<i class="pi pi-times-circle" />
|
||||||
<span class="evento-act__label">Revogar pagamento</span>
|
<span class="evento-act__label">Revogar pagamento</span>
|
||||||
</button>
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -759,6 +759,9 @@ const anteciparDialogOpen = ref(false);
|
|||||||
const anteciparMethod = ref('pix');
|
const anteciparMethod = ref('pix');
|
||||||
const anteciparBusy = ref(false);
|
const anteciparBusy = ref(false);
|
||||||
const anteciparEventoRef = ref(null); // snapshot do evento no momento do click
|
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 = [
|
const anteciparMethodOptions = [
|
||||||
{ value: 'pix', label: 'Já recebi — PIX' },
|
{ value: 'pix', label: 'Já recebi — PIX' },
|
||||||
{ value: 'dinheiro', label: 'Já recebi — Dinheiro' },
|
{ value: 'dinheiro', label: 'Já recebi — Dinheiro' },
|
||||||
@@ -782,6 +785,39 @@ async function onAnteciparPagamento() {
|
|||||||
}
|
}
|
||||||
anteciparEventoRef.value = ev;
|
anteciparEventoRef.value = ev;
|
||||||
anteciparMethod.value = 'pix';
|
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;
|
anteciparDialogOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -790,6 +826,53 @@ async function confirmAnteciparPagamento() {
|
|||||||
if (!ev || anteciparBusy.value) return;
|
if (!ev || anteciparBusy.value) return;
|
||||||
anteciparBusy.value = true;
|
anteciparBusy.value = true;
|
||||||
try {
|
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;
|
const tenantId = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
|
||||||
// ownerId: ev.owner_id é prioridade. Fallback pra M.ownerId (composable
|
// ownerId: ev.owner_id é prioridade. Fallback pra M.ownerId (composable
|
||||||
// que conhece o user logado). Pra virtuais ou snapshots stale, ev pode
|
// que conhece o user logado). Pra virtuais ou snapshots stale, ev pode
|
||||||
@@ -2629,6 +2712,7 @@ function onKeydown(e) {
|
|||||||
@revogar-sessao="onRevogarSessao"
|
@revogar-sessao="onRevogarSessao"
|
||||||
@ver-lancamentos="onVerLancamentos"
|
@ver-lancamentos="onVerLancamentos"
|
||||||
@antecipar-pagamento="onAnteciparPagamento"
|
@antecipar-pagamento="onAnteciparPagamento"
|
||||||
|
@trocar-metodo-antecipacao="onTrocarMetodoAntecipacao"
|
||||||
@revogar-antecipacao="onRevogarAntecipacao"
|
@revogar-antecipacao="onRevogarAntecipacao"
|
||||||
@edit-paciente="onEditPaciente"
|
@edit-paciente="onEditPaciente"
|
||||||
@abrir-prontuario="onAbrirProntuario"
|
@abrir-prontuario="onAbrirProntuario"
|
||||||
@@ -3181,19 +3265,24 @@ function onKeydown(e) {
|
|||||||
v-model:visible="anteciparDialogOpen"
|
v-model:visible="anteciparDialogOpen"
|
||||||
modal
|
modal
|
||||||
:draggable="false"
|
:draggable="false"
|
||||||
header="Antecipar pagamento"
|
:header="anteciparMode === 'update' ? 'Trocar método de pagamento' : 'Antecipar pagamento'"
|
||||||
:style="{ width: '480px', maxWidth: '96vw' }"
|
:style="{ width: '480px', maxWidth: '96vw' }"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-3 pt-1">
|
<div class="flex flex-col gap-3 pt-1">
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
|
<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.
|
Receba antecipadamente o valor desta sessão.
|
||||||
|
</template>
|
||||||
</div>
|
</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 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-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 class="text-xs opacity-70">Valor: R$ {{ Number(anteciparEventoRef.price || 0).toFixed(2).replace('.', ',') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-1.5">
|
<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
|
<Select
|
||||||
v-model="anteciparMethod"
|
v-model="anteciparMethod"
|
||||||
:options="anteciparMethodOptions"
|
:options="anteciparMethodOptions"
|
||||||
@@ -3202,13 +3291,16 @@ function onKeydown(e) {
|
|||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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.
|
O saldo do pacote será decrementado quando você marcar a sessão como Realizada.
|
||||||
</small>
|
</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>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button label="Cancelar" severity="secondary" outlined :disabled="anteciparBusy" @click="anteciparDialogOpen = false" />
|
<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>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user