agenda: C9 OK + rowGroup por paciente em /financeiro + bubble cobranca-atualizada
Cenário 9 (Per-session — Michael Balint 12 × R$ 150)
- Testado e passou. 1 rule + 12 agenda_eventos materializadas + 12
financial_records pending. Sem billing_contract. Badge $ em todas as
12 sessões. Conforme esperado.
/melissa/financeiro-lancamentos: agrupado por paciente
- DataTable com rowGroupMode='subheader' + groupRowsBy='patient_id'
- Header de grupo com avatar + nome + badge "N lançamento(s)"
- expandableRowGroups + v-model:expandedRowGroups; watcher popula
todos os grupos da página atual como expandidos (sempre que
recordsGrouped muda — refletindo paginação/filtros)
- Sort outer por nome do paciente, preserva inner order
(pai → filhos de multas/taxas via mesmo agenda_evento_id)
Bubble-up @cobranca-atualizada → M.refetch
- Antes: ao marcar como pago no dialog, o card no FC ficava stale
até trocar de view. AgendaEventoFinanceiroPanel emitia
cobranca-atualizada mas só o loadOccFinancialRecord do dialog
escutava; o _paymentStateMap da agenda nao re-rodava.
- Fix: AgendaEventDialog ganhou _onCobrancaAtualizada que dispara
loadOccFinancialRecord() E emit('cobranca-atualizada') pra cima.
MelissaLayout escuta nos 2 dialogs e chama M.refetch() +
refetchEventosHoje(). Card passa pra borda verde na hora.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+56
-31
@@ -1,46 +1,70 @@
|
||||
# HANDOFF — 2026-05-19 madrugada (C1-C8 ✅ + UI saldo + Usar/Revogar, próximo C9)
|
||||
# HANDOFF — 2026-05-20 madrugada (C1-C9 ✅, próximo C10)
|
||||
|
||||
Documento de continuidade. **Quando voltar, comece lendo esta página até o fim.**
|
||||
|
||||
> **🎯 SE A FORÇA CAIR / SESSÃO PERDER CONTEXTO:** estamos na rodada de
|
||||
> testes manuais dos 13 cenários do doc viva
|
||||
> `src/docs/agenda-compromisso-financeiro-cenarios.html`. **C1-C8 ✅**.
|
||||
> Próximo: **Cenário 9** (Michael Balint · per_session · 12 × R$ 150 —
|
||||
> materializa todas 12 sessões + cria 12 records pendentes upfront).
|
||||
> `src/docs/agenda-compromisso-financeiro-cenarios.html`. **C1-C9 ✅**.
|
||||
> Próximo: **Cenário 10** (status change avulsa — Joyce/Ana/Sándor:
|
||||
> marcar como realizado/faltou/cancelado e ver consequências no
|
||||
> financeiro via STATUS_TO_EXCEPTION + financial_exceptions).
|
||||
|
||||
> **🟢 WORKING TREE LIMPO** após commit/push de 19/05 madrugada. UI de
|
||||
> pacote saldo completa (info card violeta + botão Usar/Revogar atômico
|
||||
> que materializa+realiza+cobra ou desfaz). Backfill de
|
||||
> determined_commitment_id em revogar+usar (evita campo "Título" indevido
|
||||
> em sessões legadas).
|
||||
> **🟢 WORKING TREE LIMPO** após commit/push do checkpoint pós-C8/C9.
|
||||
> Per-session funcionando (12 events + 12 records gerados em batch).
|
||||
> Financeiro com rowGroup por paciente (expand/collapse). Bubble-up
|
||||
> @cobranca-atualizada → M.refetch faz o card da agenda atualizar
|
||||
> badge/borda imediatamente após pagar.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 PRÓXIMO PASSO IMEDIATO — Cenário 9 (Per-session)
|
||||
## 🔴 PRÓXIMO PASSO IMEDIATO — Cenário 10 (Status change AVULSA)
|
||||
|
||||
| Campo | Valor |
|
||||
|---|---|
|
||||
| Paciente | **Michael Balint** |
|
||||
| Frequência | **Semanal · 12 ocorrências** |
|
||||
| Serviço | Sessão R$ 150 |
|
||||
| Cobrança ao salvar | **1 por sessão** (chargeMode=per_session) |
|
||||
Doc HTML diz: testar status change numa sessão avulsa com cobrança pendente,
|
||||
mudando entre realizado / faltou / cancelado. As consequências financeiras
|
||||
seguem `financial_exceptions` (regras configuradas pelo terapeuta sobre o
|
||||
que acontece com a cobrança nesses casos).
|
||||
|
||||
**Esperado:**
|
||||
- 1 row em `recurrence_rules`
|
||||
- **12 rows em `agenda_eventos`** (TODAS materializadas — diferença chave vs C6-C8)
|
||||
- 0 rows em `billing_contracts` (per_session NÃO usa contrato)
|
||||
- **12 rows em `financial_records`** (1 por sessão, todos pending, amount=150 cada)
|
||||
- Agenda: 12 sessões com **badge $ amber** em todas (cada uma tem seu record próprio)
|
||||
- Popover de cada uma: linha amber "A receber R$ 150,00 (cobrança pendente)"
|
||||
Possíveis pacientes pra teste: usar Joyce, Sándor ou outro com cobrança
|
||||
avulsa pendente já criada.
|
||||
|
||||
Diferença chave vs cenários anteriores:
|
||||
- C6 (none): 1 row materializada + 0 records
|
||||
- C7 (upfront): 1 row + 1 record (R$ totalPacote)
|
||||
- C8 (saldo): 0 rows + 0 records (modelo Cliniko, geram on-the-fly via Usar)
|
||||
- **C9 (per_session): N rows + N records pré-criados**
|
||||
**Esperado** (depende das `financial_exceptions` configuradas no tenant):
|
||||
- Realizada: status muda; cobrança permanece (caminho default)
|
||||
- Faltou: pode ter regra → cobrança 100% (paciente paga falta) ou cancela
|
||||
- Cancelado: pode ter regra → cancelar cobrança ou cobrar parcial
|
||||
|
||||
Após C9: C10-C13 (status change + edit cobrada).
|
||||
Quando todos passarem, replicar em **Rail** (`AgendaTerapeutaPage.vue`) e **Clínica** (`AgendaClinicaPage.vue`).
|
||||
Conferir:
|
||||
- `STATUS_TO_EXCEPTION` mapping em `useAgendaFinanceiro.js`
|
||||
- `getFinancialExceptionRule(tenantId, exceptionType)` retorna a regra
|
||||
- `handleStatusChange` orquestra: agenda update + financial adjust
|
||||
|
||||
Após C10: C11 (status change pacote saldo — usar a infra do Usar/Revogar)
|
||||
→ C12 (antecipar pagamento) → C13 (edit cobrada).
|
||||
|
||||
Quando todos passarem, replicar em **Rail** (`AgendaTerapeutaPage.vue`) e
|
||||
**Clínica** (`AgendaClinicaPage.vue`).
|
||||
|
||||
---
|
||||
|
||||
## 📦 O que foi feito em 20/05 madrugada (C9 + rowGroup financeiro + bubble cobranca-atualizada)
|
||||
|
||||
### Cenário 9 ✅ (Per-session — Michael Balint 12 × R$ 150)
|
||||
Testado e passou. Criou-se 1 rule + 12 agenda_eventos materializadas + 12 financial_records pending. Sem billing_contract. Cada sessão com badge $ amber individual. **Sem nenhuma `linha de pacote`** no popover (não tem contract → não aparece). Conforme esperado.
|
||||
|
||||
### `/melissa/financeiro-lancamentos` agrupado por paciente
|
||||
- DataTable com `rowGroupMode='subheader'` + `groupRowsBy='patient_id'`
|
||||
- Default: todos os grupos da página expandidos (watcher popula `expandedGroups` com unique patient_ids quando `recordsGrouped` muda)
|
||||
- Header de grupo: avatar pequeno + nome + badge "N lançamento(s)"
|
||||
- Click no chevron contrai/expande (auto via PrimeVue `expandableRowGroups`)
|
||||
- Sort estável: ordena outer por nome do paciente, preserva inner order (pai → filhos de multas/taxas)
|
||||
|
||||
### Bubble-up `@cobranca-atualizada`
|
||||
Antes: `AgendaEventoFinanceiroPanel.@cobranca-atualizada` disparava só `loadOccFinancialRecord` (interno do dialog). O `_paymentStateMap` da agenda ficava stale → card no FC só atualizava ao trocar de view.
|
||||
|
||||
Agora: `AgendaEventDialog._onCobrancaAtualizada` faz duas coisas:
|
||||
1. `loadOccFinancialRecord()` — refresca estado interno do dialog
|
||||
2. `emit('cobranca-atualizada')` — bubble pra MelissaLayout
|
||||
|
||||
MelissaLayout escuta nos 2 dialogs (principal + occurrenceMode) e chama `onCobrancaAtualizada` que dispara `M.refetch() + refetchEventosHoje()`. Resultado: card na agenda passa pra borda verde imediatamente após marcar pago.
|
||||
|
||||
---
|
||||
|
||||
@@ -282,7 +306,8 @@ User tentou rodar C5 e bateu em 3 problemas seguidos. Cada um virou um fix:
|
||||
| 6 | Recorrente sem pacote (Maria Magali / Anna Freud) | ✅ |
|
||||
| 7 | Pacote upfront (Ana Souza Ferreira 4 × R$ 200) | ✅ |
|
||||
| 8 | Pacote saldo (Otávio 12 × R$ 50) | ✅ |
|
||||
| **9** | **1 por sessão (Michael Balint 12 × R$ 150)** | 🔴 **PRÓXIMO** |
|
||||
| 9 | 1 por sessão (Michael Balint 12 × R$ 150) | ✅ |
|
||||
| **10** | **Status change avulsa (realizado/faltou/cancelado)** | 🔴 **PRÓXIMO** |
|
||||
| 10 | Status change avulsa (realizado/faltou/cancelado) | ⏳ |
|
||||
| 11 | Status change pacote saldo | ⏳ |
|
||||
| 12 | Antecipar pagamento (Carl Jung) | ⏳ |
|
||||
|
||||
@@ -14,6 +14,38 @@ Chronological, append-only record of everything that's happened in this wiki.
|
||||
|
||||
---
|
||||
|
||||
## [2026-05-20 06:00] session | C9 OK + rowGroup por paciente + bubble cobranca-atualizada
|
||||
Touched: none (codigo + HANDOFF)
|
||||
Detalhes:
|
||||
|
||||
CENARIO 9 (per_session):
|
||||
- Michael Balint 12 sessoes R$ 150 modo "1 por sessao"
|
||||
- DB conforme esperado: 1 rule, 12 events materializadas, 0 contracts,
|
||||
12 records pending (R$ 150 cada)
|
||||
- Visual: 12 badges $ amber, todas pending. Sem linha de pacote no
|
||||
popover (per_session nao usa contract).
|
||||
|
||||
ROWGROUP NO /melissa/financeiro-lancamentos:
|
||||
- DataTable com rowGroupMode='subheader' + groupRowsBy='patient_id'
|
||||
- Default todos expandidos (watcher recalcula a cada recordsGrouped)
|
||||
- Header de grupo: avatar + nome + badge "N lançamento(s)"
|
||||
- Sort outer por nome de paciente, preserva ordem interna pai→filhos
|
||||
- Click no chevron contrai/expande
|
||||
|
||||
BUBBLE @cobranca-atualizada:
|
||||
- Bug: card na agenda nao atualizava badge/borda apos marcar como
|
||||
pago no dialog (so dava refresh ao trocar de view)
|
||||
- Root cause: AgendaEventoFinanceiroPanel.@cobranca-atualizada
|
||||
disparava so loadOccFinancialRecord (interno do dialog). O
|
||||
_paymentStateMap da agenda ficava stale.
|
||||
- Fix: AgendaEventDialog tem agora _onCobrancaAtualizada que faz
|
||||
loadOccFinancialRecord + emit('cobranca-atualizada'). MelissaLayout
|
||||
escuta nos 2 dialogs e chama M.refetch() + refetchEventosHoje().
|
||||
Card no FC passa pra borda verde imediatamente.
|
||||
|
||||
PROXIMO: Cenario 10 (status change avulsa — realizado/faltou/cancelado
|
||||
+ STATUS_TO_EXCEPTION em financial_exceptions).
|
||||
|
||||
## [2026-05-20 03:00] session | C8 OK + Usar/Revogar saldo + UI pacote + ajustes UX
|
||||
Touched: none (sem nova wiki page; mudancas em codigo + HANDOFF)
|
||||
Detalhes: noite longa cobrindo C8 (pacote saldo) e principalmente
|
||||
|
||||
@@ -147,7 +147,16 @@ const props = defineProps({
|
||||
blockOverlapWarning: { type: Object, default: null }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'delete', 'updateSeriesEvent', 'editSeriesOccurrence', 'updated', 'usar-sessao', 'revogar-sessao']);
|
||||
const emit = defineEmits(['update:modelValue', 'save', 'delete', 'updateSeriesEvent', 'editSeriesOccurrence', 'updated', 'usar-sessao', 'revogar-sessao', 'cobranca-atualizada']);
|
||||
|
||||
// Helper: chamado pelo AgendaEventoFinanceiroPanel quando a cobrança
|
||||
// muda (gerada, paga, cancelada). Refresha estado interno do dialog
|
||||
// E bubble pra MelissaLayout disparar refetch da agenda (sem isso, o
|
||||
// card do FC fica com paymentState stale até trocar de view).
|
||||
function _onCobrancaAtualizada() {
|
||||
loadOccFinancialRecord();
|
||||
emit('cobranca-atualizada');
|
||||
}
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
@@ -1823,7 +1832,7 @@ onBeforeUnmount(() => {
|
||||
Para alterar tipo ou serviços, ajuste a cobrança no Financeiro abaixo.
|
||||
</div>
|
||||
</Message>
|
||||
<AgendaEventoFinanceiroPanel v-if="!occFinancialRecord._synthesized" :evento="eventRow" class="m-3" @cobranca-atualizada="loadOccFinancialRecord" />
|
||||
<AgendaEventoFinanceiroPanel v-if="!occFinancialRecord._synthesized" :evento="eventRow" class="m-3" @cobranca-atualizada="_onCobrancaAtualizada" />
|
||||
</template>
|
||||
|
||||
<!-- LOADING -->
|
||||
@@ -2338,7 +2347,7 @@ onBeforeUnmount(() => {
|
||||
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" />
|
||||
<AgendaEventoFinanceiroPanel v-if="!occFinancialRecord._synthesized && isEdit && eventRow?.id" :evento="eventRow" class="m-3" @cobranca-atualizada="_onCobrancaAtualizada" />
|
||||
</template>
|
||||
<Transition v-else-if="!occFinancialLoading" name="aed-pay-expand">
|
||||
<div class="field-card__body aed-pay-body">
|
||||
@@ -2407,7 +2416,7 @@ onBeforeUnmount(() => {
|
||||
v-if="!occFinancialRecord && !occFinancialLoading && isEdit && eventRow?.id"
|
||||
:evento="eventRow"
|
||||
class="m-3"
|
||||
@cobranca-atualizada="loadOccFinancialRecord"
|
||||
@cobranca-atualizada="_onCobrancaAtualizada"
|
||||
/>
|
||||
|
||||
<!-- Botão "Ver lançamentos" (2026-05-14): abre dialog
|
||||
|
||||
@@ -161,9 +161,37 @@ const recordsGrouped = computed(() => {
|
||||
out.push({ ...r, _isChild: idx > 0 && group.length > 1, _hasChildren: idx === 0 && group.length > 1 });
|
||||
});
|
||||
}
|
||||
// Ordena por paciente pra rowGroupMode='subheader' do PrimeVue agrupar
|
||||
// corretamente. Sort estável preserva ordem interna (parent → children
|
||||
// do mesmo agenda_evento_id; séries em ordem cronológica).
|
||||
out.sort((a, b) => {
|
||||
const aName = a.patients?.nome_completo || '~'; // sem paciente vai pro final
|
||||
const bName = b.patients?.nome_completo || '~';
|
||||
return aName.localeCompare(bName, 'pt-BR');
|
||||
});
|
||||
return out;
|
||||
});
|
||||
|
||||
// Grupos expandidos (default: todos os pacientes da página atual).
|
||||
// Watcher recalcula sempre que recordsGrouped muda — quando user troca
|
||||
// pagina/filtro, todos os grupos da nova lista entram expandidos.
|
||||
const expandedGroups = ref([]);
|
||||
watch(
|
||||
recordsGrouped,
|
||||
(list) => {
|
||||
const ids = [...new Set((list || []).map((r) => r.patient_id).filter(Boolean))];
|
||||
expandedGroups.value = ids;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
// Conta lançamentos por paciente na lista atual — exibido no header
|
||||
// de grupo. Inclui só "pais" (não conta filhos de cobranças extras pra
|
||||
// não inflar o número).
|
||||
function _countForPatient(pid) {
|
||||
if (!pid) return 0;
|
||||
return (recordsGrouped.value || []).filter((r) => r.patient_id === pid && !r._isChild).length;
|
||||
}
|
||||
|
||||
// ── Paginação server-side ─────────────────────────────
|
||||
const pageFirst = ref(0);
|
||||
const pageRows = ref(20);
|
||||
@@ -588,8 +616,24 @@ onBeforeUnmount(() => {
|
||||
tableStyle="min-width: 880px"
|
||||
:rowClass="(r) => [r.status === 'overdue' ? 'mfl-row-overdue' : '', r._isChild ? 'mfl-row-child' : '', r._hasChildren ? 'mfl-row-parent' : ''].filter(Boolean).join(' ')"
|
||||
class="mfl-table"
|
||||
rowGroupMode="subheader"
|
||||
groupRowsBy="patient_id"
|
||||
:expandableRowGroups="true"
|
||||
v-model:expandedRowGroups="expandedGroups"
|
||||
@page="onPageChange"
|
||||
>
|
||||
<template #groupheader="{ data }">
|
||||
<div class="mfl-group-header">
|
||||
<span
|
||||
class="mfl-row__avatar"
|
||||
:style="data.patients?.identification_color ? { background: data.patients.identification_color } : null"
|
||||
>
|
||||
{{ data.patients?.nome_completo?.[0]?.toUpperCase() ?? '?' }}
|
||||
</span>
|
||||
<span class="mfl-group-header__name">{{ data.patients?.nome_completo ?? 'Sem paciente' }}</span>
|
||||
<span class="mfl-group-header__count">{{ _countForPatient(data.patient_id) }} lançamento(s)</span>
|
||||
</div>
|
||||
</template>
|
||||
<Column header="Paciente" style="min-width: 13rem">
|
||||
<template #body="{ data }">
|
||||
<!-- Em records "filhos" (multa, taxa) do mesmo agenda_evento_id,
|
||||
@@ -1471,6 +1515,27 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
/* Row content */
|
||||
/* Group header — paciente como subheader da DataTable, com avatar
|
||||
pequeno + nome + contagem. Click no chevron (auto via PrimeVue
|
||||
expandableRowGroups) expande/contrai o bloco do paciente. */
|
||||
.mfl-group-header {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.mfl-group-header__name {
|
||||
color: var(--text-color);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.mfl-group-header__count {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--surface-border), transparent 40%);
|
||||
}
|
||||
.mfl-row__patient {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1485,6 +1485,15 @@ async function onRevogarSessao(payload = null) {
|
||||
}
|
||||
}
|
||||
|
||||
// Bubble up do AgendaEventoFinanceiroPanel via AgendaEventDialog.
|
||||
// Disparado quando cobrança é gerada / paga / cancelada dentro do
|
||||
// dialog. Re-roda o bulk-load do useMelissaAgenda pra que o FC
|
||||
// atualize badge $ / borda verde sem precisar trocar de view.
|
||||
function onCobrancaAtualizada() {
|
||||
M.refetch();
|
||||
refetchEventosHoje();
|
||||
}
|
||||
|
||||
async function onWhatsapp() {
|
||||
const ev = eventoSelecionado.value;
|
||||
if (!ev?.patient_id) {
|
||||
@@ -2932,6 +2941,7 @@ function onKeydown(e) {
|
||||
@editSeriesOccurrence="M.onEditSeriesOccurrence"
|
||||
@usar-sessao="onUsarSessao"
|
||||
@revogar-sessao="onRevogarSessao"
|
||||
@cobranca-atualizada="onCobrancaAtualizada"
|
||||
/>
|
||||
|
||||
<!-- 2º AgendaEventDialog — empilhado por cima do principal pra editar
|
||||
@@ -2962,6 +2972,7 @@ function onKeydown(e) {
|
||||
@updateSeriesEvent="M.onUpdateSeriesEvent"
|
||||
@usar-sessao="onUsarSessao"
|
||||
@revogar-sessao="onRevogarSessao"
|
||||
@cobranca-atualizada="onCobrancaAtualizada"
|
||||
/>
|
||||
|
||||
<!-- BloqueioDialog — bloqueio de horário/período/dia/feriados.
|
||||
|
||||
Reference in New Issue
Block a user