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:
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/composables/useAgendaBloqueios.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
// useAgendaBloqueios
|
||||
// Carrega e expõe rows de public.agenda_bloqueios + computed de events
|
||||
// background pra renderizar no FullCalendar (cinza). Usado pelos 3 layouts
|
||||
// da agenda (Melissa, Terapeuta/Rail, Clínica).
|
||||
//
|
||||
// Contrato:
|
||||
// - load(ownerId, rangeStart, rangeEnd)
|
||||
// carrega bloqueios cujo data_inicio esteja dentro do range OU que
|
||||
// sejam recorrentes (data_inicio pode estar em qualquer ponto, mas
|
||||
// o build de events filtra pra emitir só dentro do range visível)
|
||||
// - bloqueios ref(Array)
|
||||
// - loading ref(Bool)
|
||||
// - error ref(String)
|
||||
// - buildEventsForRange(rangeStart, rangeEnd)
|
||||
// wrapper sobre agendaMappers.buildBloqueioBackgroundEvents
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { buildBloqueioBackgroundEvents } from '@/features/agenda/services/agendaMappers';
|
||||
|
||||
export function useAgendaBloqueios() {
|
||||
const bloqueios = ref([]);
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
|
||||
// ownerIdOrIds: string (1 owner) ou Array<string> (multi-owner, Clínica).
|
||||
async function load(ownerIdOrIds, rangeStart, rangeEnd) {
|
||||
if (!ownerIdOrIds) return;
|
||||
const ids = Array.isArray(ownerIdOrIds)
|
||||
? ownerIdOrIds.filter(Boolean)
|
||||
: [ownerIdOrIds];
|
||||
if (!ids.length) return;
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
const isoStart = _toISODate(rangeStart);
|
||||
const isoEnd = _toISODate(rangeEnd);
|
||||
|
||||
// Query: recorrentes (qualquer data) OU não-recorrentes com
|
||||
// data_inicio <= isoEnd e (data_fim ?? data_inicio) >= isoStart.
|
||||
// 2 queries simples + merge pra evitar string-building frágil.
|
||||
const baseNonRec = supabase
|
||||
.from('agenda_bloqueios')
|
||||
.select('*')
|
||||
.eq('recorrente', false)
|
||||
.lte('data_inicio', isoEnd)
|
||||
.or(`data_fim.gte.${isoStart},and(data_fim.is.null,data_inicio.gte.${isoStart})`);
|
||||
const baseRec = supabase
|
||||
.from('agenda_bloqueios')
|
||||
.select('*')
|
||||
.eq('recorrente', true);
|
||||
|
||||
const [{ data: nonRec, error: e1 }, { data: rec, error: e2 }] = await Promise.all([
|
||||
ids.length === 1 ? baseNonRec.eq('owner_id', ids[0]) : baseNonRec.in('owner_id', ids),
|
||||
ids.length === 1 ? baseRec.eq('owner_id', ids[0]) : baseRec.in('owner_id', ids)
|
||||
]);
|
||||
if (e1) throw e1;
|
||||
if (e2) throw e2;
|
||||
bloqueios.value = [...(nonRec || []), ...(rec || [])];
|
||||
} catch (e) {
|
||||
error.value = e?.message || 'Falha ao carregar bloqueios.';
|
||||
bloqueios.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildEventsForRange(rangeStart, rangeEnd) {
|
||||
return buildBloqueioBackgroundEvents(bloqueios.value, rangeStart, rangeEnd);
|
||||
}
|
||||
|
||||
return { bloqueios, loading, error, load, buildEventsForRange };
|
||||
}
|
||||
|
||||
function _toISODate(d) {
|
||||
if (!d) return null;
|
||||
const dt = d instanceof Date ? d : new Date(d);
|
||||
const y = dt.getFullYear();
|
||||
const m = String(dt.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(dt.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
Reference in New Issue
Block a user