agenda: C7 OK + Fase 6 lock-edit ativada em Melissa + cross-week payment propagation
Cenário 7 (Pacote UPFRONT — Ana Souza Ferreira 4×R$ 200 = R$ 800)
- Testado e passou. User criou Ana, pagou os R$ 800 em dinheiro pelo
Financeiro. Borda verde + popover "Pago R$ 800" funcionando.
Fase 6 (lock-edit cobrada) ativada em Melissa
- Removido guard `if (!props.occurrenceMode) return;` em
loadOccFinancialRecord (useAgendaEventLifecycle.js:217+). Agora ele
carrega em ambos modos (Rail/Clínica E Melissa)
- loadOccFinancialRecord SINTETIZA record paid/pending pra siblings de
contrato upfront ativo — assim TODAS as ocorrências da série mostram
"Cobrança paga R$ 800 do pacote" no AgendaEventDialog
- AgendaEventDialog card Sessão/Honorários (flow Melissa) ganhou lock
template: Tag em vez de Select billingType quando occFinancialRecord
existe; Message com cadeado "Cobrança de R$ X já emitida"
- AgendaEventoFinanceiroPanel só renderiza dentro do lock quando record
é REAL (não sintetizado) — evita "Gerar cobrança" indevido em sibling
- paymentSummary do Resumo lateral unificado pra usar occFinancialRecord
(em vez do sessionPaymentRecord paralelo de antes)
Cross-week propagation de pacote upfront
- BUG: ao navegar pra semana só com virtuais (sem reais), bulk-load
caía no else `_rulePaymentMap.value = {}` — virtuais perdiam estado
paid herdado
- FIX em useMelissaAgenda._reloadRange:
* Maps (payment/amount/rule) inicializados SEMPRE no início
* Propagação roda independente de realIds.length (depende só de
ruleIdsInView.size>0, considera reais E virtuais com recurrence_id)
* Query cross-week: pra cada rule em view, busca QUALQUER evento
sibling em qualquer semana + seus records pra determinar estado do
contrato. Encontra o record do pacote mesmo em outra semana
- Saldo NÃO propaga (filter: charging_style='upfront' || NULL); cada
sessão de saldo gera cobrança individual ao realizar
- Memória durável: memory/project_cross_week_propagation.md
Visualização de virtuais cobertas
- MelissaEventoPanel.showPaymentRow: virtuais só escondem quando state
='none'. Com paid/pending herdado, exibem linha colorida
- MelissaAgenda fcEvents: isPaidSession e badge $ pendente removeram
exigência de !is_occurrence. Virtuais herdadas via propagação mostram
borda verde / badge amber
Atalho "Gerar fatura" no popover
- Pill amber pequeno ao lado de "A cobrar R$ X" quando paymentVariant
='none' && !is_occurrence. Click → gerarCobrancaManual direto, fecha
popover pra impedir double-click. Tooltip: "Gerar fatura agora"
- Wire em MelissaLayout via novo emit gerar-cobranca + handler
onGerarCobrancaQuick
Info de pacote no popover
- Header agora mostra "Sessão · Pacote · N sessões" (computed
seriesLabel lê de _raw do rule)
Botão "Excluir série inteira"
- Novo emit delete-series em MelissaEventoPanel + botão ao lado de
"Excluir sessão" quando evento tem recurrence_id
- Handler onDeleteSeries em MelissaLayout: hard delete em 3 etapas
(financial_records pendentes → agenda_eventos materializados →
recurrence_rules CASCADE leva exceptions). Bloqueia se algum record
paid (estorno via Financeiro primeiro)
cancel_session some da agenda
- useRecurrence.expandRules agora pula occurrence com exception.type=
'cancel_session' (era visível com status cancelado; doc dizia que
some). patient_missed/therapist_canceled/holiday_block permanecem
como histórico
recurrence_exceptions cancel idempotente
- MelissaLayout onDeleteEvento usa upsert com onConflict pra exception
cancel — não quebra mais com unique violation em re-cancel
billing_contract_id na 1ª materializada
- _createPackageContract agora .select() o contrato após insert e seta
billing_contract_id no insert da 1ª agenda_eventos materializada
onVerLancamentos cobre virtual de upfront
- Antes virtual sempre toast "Sem lançamentos". Agora busca records via
siblings da série pra encontrar o do pacote. Saldo/sem pacote continua
com toast
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -965,12 +965,15 @@ const occBillingStatusSeverity = computed(() => {
|
||||
// da cobrança (Pago / Pendente / Atrasada / Sem cobrança). Espelha
|
||||
// os 3 canais visuais da agenda (verde pago / amber pendente /
|
||||
// neutro). Sources:
|
||||
// - sessionPaymentRecord (1 query em useAgendaEventLifecycle)
|
||||
// - occFinancialRecord (lifecycle, agora carrega em ambos modos —
|
||||
// guard de occurrenceMode foi removido em 2026-05-19 pra ativar
|
||||
// lock-edit em Melissa tambem). Cobre record direto OU sintetizado
|
||||
// a partir de contrato upfront ativo.
|
||||
// - eventRow.price (fallback pra "Sem cobrança · R$ X" quando
|
||||
// ainda nao ha record)
|
||||
const paymentSummary = computed(() => {
|
||||
if (!isSessionEvent.value) return null;
|
||||
const rec = sessionPaymentRecord.value;
|
||||
const rec = occFinancialRecord.value;
|
||||
const fmtDate = (d) => {
|
||||
if (!d) return '';
|
||||
try {
|
||||
@@ -1770,16 +1773,22 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</div>
|
||||
<div class="field-card__body">
|
||||
<!-- LOCKED: cobranca ja emitida -->
|
||||
<!-- LOCKED: cobranca ja emitida.
|
||||
AgendaEventoFinanceiroPanel só renderiza quando
|
||||
o record é REAL (não sintetizado de contrato
|
||||
upfront em sibling). Sintetizado: a sessão não
|
||||
tem record próprio, então o panel exibiria
|
||||
"Gerar cobrança" indevidamente. O lock Message
|
||||
já comunica o estado. -->
|
||||
<template v-if="occFinancialRecord">
|
||||
<Message severity="info" :closable="false" class="m-2">
|
||||
<div class="text-sm">
|
||||
<i class="pi pi-lock mr-1" />
|
||||
Cobrança de <b>{{ fmtBRL(occFinancialRecord.final_amount ?? occFinancialRecord.amount) }}</b> já emitida.
|
||||
Cobrança de <b>{{ fmtBRL(occFinancialRecord.final_amount ?? occFinancialRecord.amount) }}</b> {{ occFinancialRecord._synthesized ? 'do pacote' : 'já emitida' }}.
|
||||
Para alterar tipo ou serviços, ajuste a cobrança no Financeiro abaixo.
|
||||
</div>
|
||||
</Message>
|
||||
<AgendaEventoFinanceiroPanel :evento="eventRow" class="m-3" @cobranca-atualizada="loadOccFinancialRecord" />
|
||||
<AgendaEventoFinanceiroPanel v-if="!occFinancialRecord._synthesized" :evento="eventRow" class="m-3" @cobranca-atualizada="loadOccFinancialRecord" />
|
||||
</template>
|
||||
|
||||
<!-- LOADING -->
|
||||
@@ -2204,7 +2213,13 @@ onBeforeUnmount(() => {
|
||||
<!-- Mobile: label visível; Desktop: label some e Select
|
||||
ocupa o espaço. Mesmo padrão do Paciente e Data/Horário. -->
|
||||
<span class="md:hidden">Sessão / Honorários</span>
|
||||
<!-- Lock-edit (Fase 6, ativada em Melissa 2026-05-19):
|
||||
quando há cobrança paga/pendente OU contrato upfront
|
||||
ativo, mostra Tag em vez do Select pra impedir
|
||||
troca de billingType. -->
|
||||
<Tag v-if="occFinancialRecord" :value="occBillingStatusLabel" :severity="occBillingStatusSeverity" class="ml-auto" />
|
||||
<Select
|
||||
v-else-if="!occFinancialLoading"
|
||||
v-model="billingType"
|
||||
:options="billingTypeOptions"
|
||||
optionLabel="label"
|
||||
@@ -2213,7 +2228,20 @@ onBeforeUnmount(() => {
|
||||
class="aed-pay-mod-select ml-auto md:ml-0"
|
||||
/>
|
||||
</div>
|
||||
<Transition name="aed-pay-expand">
|
||||
<!-- LOCKED: cobrança emitida (real OU sintetizada via
|
||||
contrato upfront pago/pendente). Trava edição de
|
||||
tipo/serviços/preço. Ajustes só via Financeiro. -->
|
||||
<template v-if="occFinancialRecord">
|
||||
<Message severity="info" :closable="false" class="m-2">
|
||||
<div class="text-sm">
|
||||
<i class="pi pi-lock mr-1" />
|
||||
Cobrança de <b>{{ fmtBRL(occFinancialRecord.final_amount ?? occFinancialRecord.amount) }}</b> {{ occFinancialRecord._synthesized ? 'do pacote' : 'já emitida' }}.
|
||||
Para alterar tipo ou serviços, ajuste a cobrança no Financeiro abaixo.
|
||||
</div>
|
||||
</Message>
|
||||
<AgendaEventoFinanceiroPanel v-if="!occFinancialRecord._synthesized && isEdit && eventRow?.id" :evento="eventRow" class="m-3" @cobranca-atualizada="loadOccFinancialRecord" />
|
||||
</template>
|
||||
<Transition v-else-if="!occFinancialLoading" name="aed-pay-expand">
|
||||
<div class="field-card__body aed-pay-body">
|
||||
<!-- Gratuito: aviso informativo, sem ações -->
|
||||
<div v-if="billingType === 'gratuito'" class="aed-pay-gratuito">
|
||||
@@ -2273,10 +2301,15 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<!-- Cobrança da sessão (financial_records ja criado) —
|
||||
só em edição. Movido pra dentro do Sessao/Honorarios
|
||||
em 2026-05-11 (antes ficava num side-card separado). -->
|
||||
<AgendaEventoFinanceiroPanel v-if="isSessionEvent && isEdit && eventRow?.id" :evento="eventRow" class="m-3" @cobranca-atualizada="loadSessionPaymentRecord" />
|
||||
<!-- UNLOCKED: panel pra gerar cobrança inline (atalho do
|
||||
popover é o caminho principal, mas mantido aqui pra
|
||||
paridade com flow anterior). Só aparece sem record. -->
|
||||
<AgendaEventoFinanceiroPanel
|
||||
v-if="!occFinancialRecord && !occFinancialLoading && isEdit && eventRow?.id"
|
||||
:evento="eventRow"
|
||||
class="m-3"
|
||||
@cobranca-atualizada="loadOccFinancialRecord"
|
||||
/>
|
||||
|
||||
<!-- Botão "Ver lançamentos" (2026-05-14): abre dialog
|
||||
listando todos os financial_records vinculados à
|
||||
|
||||
@@ -208,23 +208,89 @@ export function useAgendaEventLifecycle({
|
||||
}
|
||||
|
||||
// ── occurrence financial record loader ────────────────────
|
||||
// Tenta 3 caminhos pra encontrar um record relevante:
|
||||
// 1) Record direto por agenda_evento_id (materializada com cobrança própria)
|
||||
// 2) Se a sessão pertence a contrato upfront PAGO, sintetiza um record
|
||||
// 'paid' com o package_price → assim TODAS as ocorrências da série
|
||||
// mostram "Cobrança paga" no dialog (cobertas pelo pacote pago).
|
||||
// 3) Senão, null → card unlocked.
|
||||
async function loadOccFinancialRecord() {
|
||||
occFinancialRecord.value = null;
|
||||
if (!props.occurrenceMode) return;
|
||||
// Guard de occurrenceMode REMOVIDO em 2026-05-19 — necessario pra
|
||||
// que o lock de "edit cobrada" (Fase 6) tambem ative em Melissa,
|
||||
// nao so em Rail/Clinica. Padrao SimplePractice: cobranca emitida
|
||||
// eh imutavel pelo dialog; ajustes via estorno no Financeiro.
|
||||
const evId = props.eventRow?.id;
|
||||
if (!evId) return;
|
||||
const ruleId = props.eventRow?.recurrence_id;
|
||||
const patientId = props.eventRow?.paciente_id || props.eventRow?.patient_id;
|
||||
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;
|
||||
// 1) Record direto (materializada que tem agenda_evento_id real)
|
||||
const isVirtualId = typeof evId === 'string' && evId.startsWith('rec::');
|
||||
if (evId && !isVirtualId) {
|
||||
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;
|
||||
if (data) {
|
||||
occFinancialRecord.value = data;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 2) Sintetiza record 'paid' quando há contrato upfront pago.
|
||||
// A 1ª materializada tem o record real; siblings (virtuais ou
|
||||
// materializadas sem cobrança individual) herdam status do
|
||||
// contrato pra UI mostrar "Cobrança paga" coerentemente.
|
||||
if (ruleId && patientId) {
|
||||
const { data: contracts } = await supabase
|
||||
.from('billing_contracts')
|
||||
.select('id, package_price, charging_style, status')
|
||||
.eq('patient_id', patientId)
|
||||
.eq('type', 'package')
|
||||
.eq('status', 'active')
|
||||
.order('created_at', { ascending: false });
|
||||
const upfront = (contracts || []).find((c) => c.charging_style === 'upfront');
|
||||
if (upfront) {
|
||||
// Confere se há record PAGO ligado a qualquer evento do
|
||||
// mesmo recurrence_id (ou seja, contrato foi quitado).
|
||||
const { data: siblingEvents } = await supabase
|
||||
.from('agenda_eventos')
|
||||
.select('id')
|
||||
.eq('recurrence_id', ruleId);
|
||||
const ids = (siblingEvents || []).map((e) => e.id);
|
||||
if (ids.length) {
|
||||
// Pega o record mais recente NAO cancelado (paid OU
|
||||
// pending OU overdue). Pacote upfront tem 1 record
|
||||
// unico cobrindo toda a serie — qualquer status dele
|
||||
// trava as siblings (cobranca ja emitida, imutavel).
|
||||
const { data: anyRec } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, amount, final_amount, status, due_date, paid_at, payment_method')
|
||||
.in('agenda_evento_id', ids)
|
||||
.in('status', ['paid', 'pending', 'overdue'])
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
if (anyRec) {
|
||||
// Sintetiza usando o package_price (não o per-session
|
||||
// que pode ter mudado depois de editar).
|
||||
occFinancialRecord.value = {
|
||||
...anyRec,
|
||||
amount: upfront.package_price,
|
||||
final_amount: upfront.package_price,
|
||||
_synthesized: true
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
occFinancialRecord.value = null;
|
||||
} catch (e) {
|
||||
console.warn('[occurrence] erro ao carregar financial_record:', e?.message);
|
||||
occFinancialRecord.value = null;
|
||||
|
||||
Reference in New Issue
Block a user