agenda: Fase 5 (status change/edit cobrada) + indicadores visuais + UX convenio
DB
- drop agenda_excecoes (substituida por financial_exceptions + lock-edit
baseado em financial_records)
- financial_records.payment_link (Asaas + link compartilhavel)
- financial_exceptions.consume_on_miss (rotular nao-show consome ou nao)
- billing_contracts.charging_style (upfront/saldo/per_session)
Payment refactor
- paymentSettlement -> paymentMethod (string) + markPaidNow (bool).
Handler aplica payment_method sempre; status='paid'+paid_at apenas
quando markPaidNow=true && method != 'link'. Asaas (link) sempre
liquida via webhook, nunca nasce paid.
- create_financial_record_for_session com pos-RPC patch pra payment_method
e (opcional) status='paid' quando user marca "ja recebi".
Indicadores visuais (3 canais distintos por estado)
- Paid: barra esquerda emerald-500 4px na agenda (MelissaAgenda),
pi-check-circle no popover/Resumo.
- Pending: badge \$ amber canto direito (mantido); linha amber no popover/
Resumo "A receber R\$ X (cobranca pendente)".
- Neutro: sem badge nem barra (compromisso pessoal, bloqueio, ou
ocorrencia virtual de pacote upfront/saldo).
- Bulk-load de paymentState em _reloadRange etapa 4 (1 query unica em
financial_records mapeada por agenda_evento_id).
- AgendaEventDialog Resumo lateral ganha linha entre pi-clock e
pi-map-marker via novo sessionPaymentRecord (sem guard de
occurrenceMode, contrario ao occFinancialRecord que continua so pra
Rail/Clinica). 5 estados: paid+paid_at, overdue+venceu, pending+vence,
sem cobranca c/ valor, sem cobranca s/ valor.
UX de convenio
- InsurancePlanServiceQuickCreateDialog novo: cadastra procedimento
POR CIMA do AgendaEventDialog sem sair da agenda. Auto-seleciona
novo procedimento so quando nada estava selecionado antes.
- Caixa cinza "Cadastrar procedimento" sempre visivel quando convenio
selecionado, com copy variavel (0 procedimentos: chamada urgente;
1+: "se quiser adicionar mais").
- "+ Novo convenio" toolbar em ConfiguracoesConveniosPage (botao
estava faltando, empty state mandava clicar em botao inexistente).
- Hint contextual abaixo do card Sessao/Honorarios: convenio = "N da
guia eh opcional", gratuito = "sem cobranca", particular = sem hint.
Label "N da Guia" tambem ganhou "(opcional)" no service-picker dialog.
Bug fixes
- pickDbFields whitelist faltando 'modalidade' (useMelissaAgenda.js:74)
— sessoes avulsas eram salvas como presencial independente da
escolha visual. Adicionado.
- goToConveniosConfig removida — fazia router.push("/therapist/
configuracoes/convenios") mas /configuracoes/* eh rota raiz, nao
filha. Substituida pelo quick-create inline (#1).
- bloqueioCobrindo + dialogBlockOverlap passados via deps em
_buildHandlers (refs do useMelissaAgenda nao sao acessiveis no
escopo de _buildHandlers).
Fase 5 (status change + edit cobrada)
- AgendaStatusChangeConfirmDialog: confirm dialog quando user muda
status pra realizada/faltou/cancelado, com opcoes de markPaid ou
gerar cobranca conforme o caso.
- useAgendaBloqueios novo composable: extrai logica de bloqueios
cinza (background events) do MelissaAgenda.
Doc viva
- src/docs/agenda-compromisso-financeiro-cenarios.html: 13 cenarios
de teste manual. C1-C4 ja validados. Cada teste validado vira parte
da doc final pra area de ajuda (pos-Fase 9).
Wiki/handoff
- agenda-compromisso-fluxo e agenda-billing-pesquisa-mercado (decisoes
arquiteturais sobre billing).
- HANDOFF.md atualizado.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,21 @@ export function useAgendaEventLifecycle({
|
||||
const sendingReminder = ref(false);
|
||||
const serviceQuickDlgOpen = ref(false);
|
||||
const insuranceQuickDlgOpen = ref(false);
|
||||
const planServiceQuickDlgOpen = ref(false);
|
||||
|
||||
// occurrenceMode: financial_record da ocorrencia atual (se existir).
|
||||
// Usado pra travar edicao de tipo/servicos quando ja ha cobranca emitida
|
||||
// (padrao SimplePractice — cobranca emitida e imutavel; ajustes via fluxo
|
||||
// do Financeiro, nao via dialog). 2026-05-12.
|
||||
const occFinancialRecord = ref(null);
|
||||
const occFinancialLoading = ref(false);
|
||||
|
||||
// sessionPaymentRecord (2026-05-18): financial_record da sessão (mesmo
|
||||
// shape do occFinancialRecord) mas SEM o guard de occurrenceMode.
|
||||
// Carregado em qualquer edit de sessão pra alimentar a linha "Cobrança"
|
||||
// do Resumo lateral do AgendaEventDialog. Não dispara lock — esse
|
||||
// continua via occFinancialRecord (território da Fase 6/C13).
|
||||
const sessionPaymentRecord = ref(null);
|
||||
|
||||
// ── computeds locais ───────────────────────────────────────
|
||||
const serieCountByStatus = computed(() => {
|
||||
@@ -192,6 +207,56 @@ export function useAgendaEventLifecycle({
|
||||
}
|
||||
}
|
||||
|
||||
// ── occurrence financial record loader ────────────────────
|
||||
async function loadOccFinancialRecord() {
|
||||
occFinancialRecord.value = null;
|
||||
if (!props.occurrenceMode) return;
|
||||
const evId = props.eventRow?.id;
|
||||
if (!evId) return;
|
||||
occFinancialLoading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
|
||||
.eq('agenda_evento_id', evId)
|
||||
.in('status', ['pending', 'paid', 'overdue'])
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
occFinancialRecord.value = data ?? null;
|
||||
} catch (e) {
|
||||
console.warn('[occurrence] erro ao carregar financial_record:', e?.message);
|
||||
occFinancialRecord.value = null;
|
||||
} finally {
|
||||
occFinancialLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// sessionPaymentRecord loader (2026-05-18): mesma query, sem guard
|
||||
// de occurrenceMode. Alimenta a linha "Cobrança" do Resumo do dialog
|
||||
// em qualquer edit de sessão (Melissa/Rail/Clínica) com eventRow.id.
|
||||
async function loadSessionPaymentRecord() {
|
||||
sessionPaymentRecord.value = null;
|
||||
const evId = props.eventRow?.id;
|
||||
if (!evId) return;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
|
||||
.eq('agenda_evento_id', evId)
|
||||
.in('status', ['pending', 'paid', 'overdue'])
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (error) throw error;
|
||||
sessionPaymentRecord.value = data ?? null;
|
||||
} catch (e) {
|
||||
console.warn('[session-payment] erro ao carregar financial_record:', e?.message);
|
||||
sessionPaymentRecord.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function onPillEditClick(ev) {
|
||||
emit('editSeriesOccurrence', {
|
||||
id: ev.id,
|
||||
@@ -278,6 +343,26 @@ export function useAgendaEventLifecycle({
|
||||
}
|
||||
}
|
||||
|
||||
// Quick-create de procedimento (insurance_plan_services) — inline,
|
||||
// sem sair do dialog. Trigger no card Sessao/Honorarios quando o
|
||||
// convenio selecionado nao tem procedimentos ou quando user quer
|
||||
// adicionar mais. Apos criar, recarrega os planos pra refletir no
|
||||
// computed planServices.
|
||||
function openPlanServiceQuickCreate() {
|
||||
if (!composer.form.value.insurance_plan_id) return;
|
||||
planServiceQuickDlgOpen.value = true;
|
||||
}
|
||||
async function onPlanServiceCreated(service) {
|
||||
await loadInsurancePlans(props.planOwnerId || props.ownerId);
|
||||
// Auto-seleciona o procedimento recem-criado se o user nao
|
||||
// tinha nenhum selecionado ainda (caso comum: convenio sem
|
||||
// procedimentos -> cadastra o primeiro -> ja entra selecionado).
|
||||
if (service?.id && !pickerBilling.selectedPlanService.value) {
|
||||
pickerBilling.selectedPlanService.value = service.id;
|
||||
pickerBilling.onProcedureSelect(service.id);
|
||||
}
|
||||
}
|
||||
|
||||
// ── lembrete WhatsApp manual (8.2) ─────────────────────────
|
||||
async function onSendManualReminder() {
|
||||
if (!composer.form.value?.id) return;
|
||||
@@ -349,7 +434,21 @@ export function useAgendaEventLifecycle({
|
||||
if (composer.hasSerie.value) loadSerieEvents();
|
||||
else serieEvents.value = [];
|
||||
|
||||
if (composer.isEdit.value) {
|
||||
// occurrenceMode: carrega financial_record desta ocorrencia
|
||||
// pra decidir se o card Sessao/Honorarios fica locked (cobranca
|
||||
// ja emitida) ou unlocked (sem cobranca, edicao livre).
|
||||
loadOccFinancialRecord();
|
||||
|
||||
// sessionPaymentRecord: carrega em qualquer edit (Melissa
|
||||
// tambem) pra alimentar a linha "Cobrança" do Resumo lateral.
|
||||
loadSessionPaymentRecord();
|
||||
|
||||
// occurrenceMode: editando UMA ocorrencia de serie ja existente —
|
||||
// tipo de compromisso ja foi escolhido (paciente + sessao). Pular
|
||||
// step 1 incondicionalmente. Defesa em camadas: useMelissaAgenda
|
||||
// ja seta is_occurrence=true na row (faz isEdit=true), mas se outro
|
||||
// call site esquecer essa flag o guard aqui salva.
|
||||
if (props.occurrenceMode || composer.isEdit.value) {
|
||||
composer.step.value = 2;
|
||||
} else {
|
||||
const preset = props.presetCommitmentId;
|
||||
@@ -452,11 +551,17 @@ export function useAgendaEventLifecycle({
|
||||
sendingReminder,
|
||||
serviceQuickDlgOpen,
|
||||
insuranceQuickDlgOpen,
|
||||
planServiceQuickDlgOpen,
|
||||
occFinancialRecord,
|
||||
occFinancialLoading,
|
||||
sessionPaymentRecord,
|
||||
// computeds
|
||||
serieCountByStatus,
|
||||
pillDeleteMenuItems,
|
||||
// series
|
||||
loadSerieEvents,
|
||||
loadOccFinancialRecord,
|
||||
loadSessionPaymentRecord,
|
||||
onPillEditClick,
|
||||
onPillStatusChange,
|
||||
onPillDeleteClick,
|
||||
@@ -468,6 +573,8 @@ export function useAgendaEventLifecycle({
|
||||
onServiceCreated,
|
||||
openInsuranceQuickCreate,
|
||||
onInsuranceCreated,
|
||||
openPlanServiceQuickCreate,
|
||||
onPlanServiceCreated,
|
||||
// reminder
|
||||
onSendManualReminder
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user