diff --git a/HANDOFF.md b/HANDOFF.md index aa25244..0c39f1f 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,38 +1,36 @@ -# HANDOFF — 2026-05-18 noite (C1-C4 OK, UX convênio refinado, C5 ainda não rodou save) +# HANDOFF — 2026-05-19 (C1-C6 ✅, próximo C7 — pacote upfront) 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-C4 ✅, **C5 -> ainda não rodou save** — a tarde do dia 18 foi consumida refinando UX -> de convênio (3 bugs/melhorias) e preparando o C5. Próximo passo: -> **executar de fato o save do C5** (Sándor + Unimed Nacional + R$ 95). +> `src/docs/agenda-compromisso-financeiro-cenarios.html`. **C1-C6 ✅**. +> Próximo passo: **Cenário 7** (Donald Winnicott · pacote UPFRONT · 4 × +> R$ 200 = cobrança única de R$ 800). -> **🟡 WORKING TREE BEM PESADO** — refactor de payment, indicadores -> visuais (barra verde + popover + Resumo do dialog), inline quick-create -> de procedimento, fix de rota convênios, botão "+ Novo convênio", -> hint contextual. Migrations da Fase 5 já rodadas em 14/05. **Considerar -> commitar antes de mais trabalho** — diff tá grande. +> **🟢 WORKING TREE LIMPO** após commit/push de 19/05 da manhã. Migration +> nova (20260519000001) já rodada no DB local. Pronto pra próximo bloco. --- -## 🔴 PRÓXIMO PASSO IMEDIATO — Cenário 5 (Convênio) - -Receita do doc HTML. Resumo: +## 🔴 PRÓXIMO PASSO IMEDIATO — Cenário 7 (Pacote UPFRONT) | Campo | Valor | |---|---| -| Paciente | **Sándor** | -| Convênio | **Unimed Nacional** (criar via `InsurancePlanQuickCreateDialog` se não existir) | -| Valor | **R$ 95** | +| Paciente | **Donald Winnicott** | +| Frequência | **Semanal · 4 ocorrências** | +| Serviço | qualquer (ex: Sessão particular R$ 200) | +| Cobrança ao salvar | **Gerar cobrança** + estilo **"Pacote único (upfront)"** | +| Total esperado | **R$ 800** (4 × 200) | **Esperado:** -- Record com `insurance_plan_id` preenchido + pill "convênio" visível -- Na agenda: badge $ amber (record pendente até fechamento mensal do convênio) -- Popover: linha amber "A receber R$ 95,00 (cobrança pendente)" +- 1 row em `recurrence_rules` + **1 row em `agenda_eventos`** (1ª materializada) +- 1 row em `billing_contracts` (type=package, charging_style=upfront, total_sessions=4, package_price=800) +- **1 row em `financial_records`** com amount=800, status=pending (cobrança ÚNICA do pacote, vencimento na 1ª sessão) +- Agenda: 1ª com badge $ (R$ 800 a cobrar) + 3 virtuais limpas (cobertas pelo pacote) -Após o 5 passar: 6-9 (recorrentes) → 10-13 (status change + edit cobrada). Quando todos passarem, replicar em **Rail** (`AgendaTerapeutaPage.vue`) e **Clínica** (`AgendaClinicaPage.vue`). +Após C7: C8 (saldo) → C9 (per_session) → C10-C13 (status change + edit cobrada). +Quando todos passarem, replicar em **Rail** (`AgendaTerapeutaPage.vue`) e **Clínica** (`AgendaClinicaPage.vue`). --- @@ -150,7 +148,9 @@ User tentou rodar C5 e bateu em 3 problemas seguidos. Cada um virou um fix: | 2 | Avulsa sem cobrança | ✅ | | 3 | Avulsa cobrar ao salvar | ✅ | | 4 | Avulsa "já recebi" no salvar | ✅ | -| **5** | **Avulsa convênio (Sándor + Unimed)** | 🔴 **PRÓXIMO** | +| 5 | Avulsa convênio (Sándor + Unimed) | ✅ | +| 6 | Recorrente sem pacote (Maria Magali / Anna Freud) | ✅ | +| **7** | **Pacote UPFRONT (Donald Winnicott 4 × R$ 200)** | 🔴 **PRÓXIMO** | | 6 | Recorrente sem pacote (Anna Freud 4 sem) | ⏳ | | 7 | Pacote upfront (Donald Winnicott 4× R$ 200) | ⏳ | | 8 | Pacote saldo (Carl Jung 4× R$ 40) | ⏳ | diff --git a/Obsidian/Brain/log.md b/Obsidian/Brain/log.md index 68e1edf..eae2218 100644 --- a/Obsidian/Brain/log.md +++ b/Obsidian/Brain/log.md @@ -14,6 +14,74 @@ Chronological, append-only record of everything that's happened in this wiki. --- +## [2026-05-19 14:00] session | C5+C6 OK + atalho gerar fatura + RPC idempotencia fix +Touched: project_rpc_idempotency_cancelled (nova) +Detalhes: sessao longa cobrindo C5 e C6 ate green checkmark, com varios +fixes intermediarios: + +CENARIO 5 (Sandor + Unimed convenio): +- 2 bugs no save: amount lia 'price' (null em convenio) e payment_method + caia em 'asaas' por fallback. Fix: detecta convenio via insurance_plan_id, + amount=insurance_value, payment_method='convenio'. markPaidNow ignorado + pra convenio (sempre nasce pending). +- 2 bugs na visualizacao: popover so mostrava "Cobranca pendente" sem + valor (lia ev.price) e /financeiro nao mostrava pill convenio. + Fix: normalize expoe insurance_value; popover fallback price ?? insurance_value; + /financeiro ganhou branch pra payment_method='convenio' (pill violeta). + +CENARIO 6 (Maria Magali, recorrente sem pacote): +- chargeMode='none' nao materializava 1a (todas viravam virtuais, sem badge). + Fix: adicionado bloco no fluxo de criacao recorrente que materializa 1a + quando chargeMode='none' && paciente_id. Package/per_session ja + materializam nos seus helpers; package saldo intencionalmente NAO. +- Primeira tentativa de fix usou paciente_id (Portuguese) mas + agendaRepository dropa esse campo (legado). Corrigido pra patient_id + (English DB column). Fallback `normalized.patient_id ?? normalized.paciente_id`. + +ATALHO "Gerar fatura" no popover: +- Pill amber "Gerar fatura" ao lado de "A cobrar R$ X" no popover. +- Wire em MelissaLayout via emit gerar-cobranca + handler onGerarCobrancaQuick. +- Fecha popover apos sucesso pra impedir click duplicado. +- Tooltip simplificado: "Gerar fatura agora". + +BUG RPC IDEMPOTENCIA (gravado em memoria): +- create_financial_record_for_session checava idempotencia por + agenda_evento_id + deleted_at IS NULL, MAS sem filtrar status='cancelled'. + User cancelava cobranca sem querer → todo regenerar retornava cancelado + e nada inseria. Toast verde mentindo. +- Fix: migration 20260519000001 adiciona AND status != 'cancelled'. +- Memoria salva em project_rpc_idempotency_cancelled.md (padrao a aplicar + em toda RPC futura com idempotencia por chave natural). + +INFO PACOTE NO POPOVER: +- Header do popover agora mostra "Sessao · Pacote · N sessoes" ou similar + (computed seriesLabel le do _raw). Visual: violeta sem caps lock. + +TITULO DIALOG "Sessao do Pacote · Sessao": +- Quando commitment name eh "Sessao" (default), drop pra evitar duplicacao. + Outros nomes (Avaliacao etc) permanecem com forma completa. + +CSS: +- .aed-row-50 perdeu margin-bottom (user request). +- .field-card.mb-4 ganhou margin-top: 1rem (scoped via composer wrappers). + +OUTROS BUGS RESOLVIDOS NO CAMINHO: +- AgendaEventoFinanceiroPanel.fetchRecord agora filtra cancelled. +- bulk-load do useMelissaAgenda agora filtra cancelled. +- recurrence_exceptions cancel agora usa upsert (idempotente, nao quebra + com unique constraint quando user cancela 2x ou tem exception zumbi). +- "Excluir serie inteira" botao novo no popover (hard delete: rule + + materializadas + financial_records pendentes; bloqueia se paid). +- cancel_session exception agora SOME da agenda (era visivel com status + cancelado, doc dizia "some da agenda" mas codigo mantinha). +- ConfiguracoesConveniosPage ganhou botao "+ Novo convenio" (faltava). +- goToConveniosConfig removida (dead code pos quick-create inline). +- Quick-create inline de procedimento (InsurancePlanServiceQuickCreateDialog). +- Hint contextual abaixo do card Sessao/Honorarios (convenio = "N guia + opcional"; gratuito = "sem cobranca"). + +PROXIMO: Cenario 7 (Donald Winnicott · pacote UPFRONT · 4 × R$ 200). + ## [2026-05-18 23:30] session | UX de convenio refinado (3 fixes) + hint contextual Touched: none (sem nova wiki page; tudo em codigo + HANDOFF) Detalhes: tarde inteira consumida em refinar UX de convenio antes do diff --git a/database-novo/migrations/20260519000001_create_financial_record_ignore_cancelled.sql b/database-novo/migrations/20260519000001_create_financial_record_ignore_cancelled.sql new file mode 100644 index 0000000..853dc98 --- /dev/null +++ b/database-novo/migrations/20260519000001_create_financial_record_ignore_cancelled.sql @@ -0,0 +1,86 @@ +-- ============================================================================ +-- create_financial_record_for_session: idempotência ignora cancelled +-- ---------------------------------------------------------------------------- +-- Bug: a função recusava criar um novo financial_record quando já existia +-- um record cancelled pro mesmo agenda_evento_id, porque a checagem de +-- idempotência só filtrava `deleted_at IS NULL` (e cancel preserva +-- deleted_at = NULL pra manter auditoria). +-- +-- Consequência: user cancelava cobrança sem querer e ficava preso — todo +-- "Gerar cobrança" subsequente retornava o registro cancelado sem inserir +-- nova linha (frontend recebia data, achava sucesso, mas DB ficava como +-- estava). +-- +-- Fix: adiciona `AND status != 'cancelled'` na checagem. Cancelled passa a +-- ser tratado como "sem cobrança ativa" pra idempotência. Audit history +-- continua preservado (rows cancelled permanecem na tabela). +-- +-- Idempotente: CREATE OR REPLACE substitui a função existente. +-- ============================================================================ + +BEGIN; + +CREATE OR REPLACE FUNCTION public.create_financial_record_for_session( + p_tenant_id uuid, + p_owner_id uuid, + p_patient_id uuid, + p_agenda_evento_id uuid, + p_amount numeric, + p_due_date date +) RETURNS SETOF public.financial_records + LANGUAGE plpgsql SECURITY DEFINER + SET search_path TO 'public' + AS $$ +DECLARE + v_existing public.financial_records%ROWTYPE; + v_new public.financial_records%ROWTYPE; +BEGIN + -- Idempotência: retorna o registro existente se já foi criado. + -- Ignora cancelled (treat as "no active record") pra permitir regenerar + -- cobrança após cancelamento. + SELECT * INTO v_existing + FROM public.financial_records + WHERE agenda_evento_id = p_agenda_evento_id + AND deleted_at IS NULL + AND status != 'cancelled' + LIMIT 1; + + IF FOUND THEN + RETURN NEXT v_existing; + RETURN; + END IF; + + -- Cria o novo registro + INSERT INTO public.financial_records ( + tenant_id, + owner_id, + patient_id, + agenda_evento_id, + amount, + discount_amount, + final_amount, + status, + due_date + ) VALUES ( + p_tenant_id, + p_owner_id, + p_patient_id, + p_agenda_evento_id, + p_amount, + 0, + p_amount, + 'pending', + p_due_date + ) + RETURNING * INTO v_new; + + -- Marca o evento da agenda como billed = true + UPDATE public.agenda_eventos + SET billed = TRUE + WHERE id = p_agenda_evento_id; + + RETURN NEXT v_new; +END; +$$; + +COMMIT; diff --git a/src/components/agenda/AgendaEventoFinanceiroPanel.vue b/src/components/agenda/AgendaEventoFinanceiroPanel.vue index cadc00a..bb23822 100644 --- a/src/components/agenda/AgendaEventoFinanceiroPanel.vue +++ b/src/components/agenda/AgendaEventoFinanceiroPanel.vue @@ -106,10 +106,15 @@ async function fetchRecord() { fetching.value = true; try { + // Ignora records cancelados — permite que o user gere nova cobrança + // após cancelar (caso comum: cancelou sem querer ou quer recobrar). + // Sem esse filtro, o scenario ficava em 'com-cobranca' mostrando + // o cancelado, e o botão "Gerar cobrança" sumia. const { data, error } = await supabase .from('financial_records') .select('id, amount, discount_amount, final_amount, status, due_date, paid_at, payment_method') .eq('agenda_evento_id', props.evento.id) + .neq('status', 'cancelled') .order('created_at', { ascending: false }) .limit(1) .maybeSingle(); diff --git a/src/features/agenda/components/AgendaEventDialog.vue b/src/features/agenda/components/AgendaEventDialog.vue index 0ee8fba..ace059b 100644 --- a/src/features/agenda/components/AgendaEventDialog.vue +++ b/src/features/agenda/components/AgendaEventDialog.vue @@ -924,7 +924,14 @@ const headerMainLabel = computed(() => { const name = selectedCommitment.value?.name || ''; if (!name) return headerTitle.value; if (isEdit.value) { - if (hasSerie.value) return `Sessão do Pacote · ${name}`; + if (hasSerie.value) { + // Evita "Sessão do Pacote · Sessão" (nome duplicado) — quando + // o commitment chama 'Sessão' (default), mostra só "Sessão do + // Pacote". Outros nomes (ex: Avaliação) entram normais. + const lower = name.toLowerCase(); + if (lower === 'sessão' || lower === 'sessao') return 'Sessão do Pacote'; + return `Sessão do Pacote · ${name}`; + } return `Editar ${name}`; } if (isPacote.value) { @@ -2303,20 +2310,6 @@ onBeforeUnmount(() => { - -
- - Nº da guia é opcional — você pode salvar a sessão e preencher depois, quando o convênio responder. -
-
- - Sessão gratuita — nenhum lançamento será gerado no Financeiro. -
-
@@ -2431,6 +2424,21 @@ onBeforeUnmount(() => {
+ +
+ + Nº da guia é opcional — você pode salvar a sessão e preencher depois, quando o convênio responder. +
+
+ + Sessão gratuita — nenhum lançamento será gerado no Financeiro. +
+ +
@@ -415,6 +469,13 @@ function modalidadeIcon(mod) { letter-spacing: 0.2em; font-weight: 600; } +.evento-tipo__series { + color: var(--p-primary-color); + text-transform: none; + letter-spacing: 0; + font-weight: 500; + margin-left: 4px; +} .evento-titulo { color: var(--m-text); font-size: 1.1rem; @@ -501,6 +562,43 @@ html.app-dark .evento-row--pay-pending > i, html.app-dark .evento-row--pay-none > i { color: #fbbf24; } +/* Atalho "Gerar fatura" — pill amber pequeno ao lado de "A cobrar R$ X". + Aparece só pra paymentVariant='none' (sem cobrança ainda). Click emite + 'gerar-cobranca' pro parent que chama gerarCobrancaManual sem abrir + o AgendaEventDialog. */ +.evento-row__pay-action { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 999px; + background: color-mix(in srgb, #f59e0b 16%, transparent); + border: 1px solid color-mix(in srgb, #f59e0b 40%, transparent); + color: #b45309; + font-size: 0.74rem; + font-weight: 600; + cursor: pointer; + flex-shrink: 0; + transition: background-color 120ms ease, border-color 120ms ease; +} +.evento-row__pay-action:hover:not(:disabled) { + background: color-mix(in srgb, #f59e0b 26%, transparent); + border-color: color-mix(in srgb, #f59e0b 60%, transparent); +} +.evento-row__pay-action:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.evento-row__pay-action > i { + font-size: 0.72rem; +} +html.app-dark .evento-row__pay-action { + color: #fbbf24; + background: color-mix(in srgb, #fbbf24 14%, transparent); + border-color: color-mix(in srgb, #fbbf24 30%, transparent); +} + /* Stack de botões "Editar sessão" + "Excluir sessão" (Fase 5, 2026-05-14). Empilhados verticalmente à direita da linha das horas. */ .evento-row__edit-stack { diff --git a/src/layout/melissa/MelissaFinanceiroLancamentos.vue b/src/layout/melissa/MelissaFinanceiroLancamentos.vue index 809b979..7a29405 100644 --- a/src/layout/melissa/MelissaFinanceiroLancamentos.vue +++ b/src/layout/melissa/MelissaFinanceiroLancamentos.vue @@ -688,6 +688,13 @@ onBeforeUnmount(() => { Ver boleto
+ +
+ + {{ paymentLabel(data.payment_method) }} +