Files
agenciapsilmno/src/features/agenda/composables/useAgendaBloqueios.js
T
Leonardo e95ed9b585 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>
2026-05-19 08:31:18 -03:00

99 lines
3.8 KiB
JavaScript

/*
|--------------------------------------------------------------------------
| 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}`;
}