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:
@@ -33,9 +33,24 @@ const emit = defineEmits([
|
||||
'edit-paciente', // botão "Editar" do grupo Outras opções → PatientCadastroDialog
|
||||
'abrir-prontuario',
|
||||
'whatsapp',
|
||||
'historico'
|
||||
'historico',
|
||||
'delete-sessao', // botão "Excluir sessão" — só pra sessões avulsas (sem recorrência)
|
||||
'ver-lancamentos', // botão "Lançamentos" — abre dialog com financial_records vinculados
|
||||
'antecipar-pagamento' // botão "Antecipar pagamento" — paciente quer pagar antes da sessão (pacote saldo)
|
||||
]);
|
||||
|
||||
// Regra atualizada (2026-05-14): botão Excluir aparece SEMPRE pra sessão.
|
||||
// Handler no parent verifica:
|
||||
// - virtual sem materialização → cria recurrence_exception cancel_session
|
||||
// - real sem records pagos → DELETE (cobranças pendentes vão junto)
|
||||
// - real com record PAGO → bloqueia (estorno pelo Financeiro primeiro)
|
||||
const canDelete = computed(() => {
|
||||
const e = ev.value;
|
||||
if (!e) return false;
|
||||
// Pra MVP: oculta só em compromisso não-sessão sem id real.
|
||||
return true;
|
||||
});
|
||||
|
||||
const ev = computed(() => props.evento || {});
|
||||
|
||||
const tipoLabel = computed(() => {
|
||||
@@ -69,6 +84,41 @@ const isSessaoComPaciente = computed(
|
||||
() => ev.value.tipo === 'sessao' && (ev.value.patient_id || ev.value.pacienteNome)
|
||||
);
|
||||
|
||||
// Estado de pagamento — vem anotado pelo useMelissaAgenda via bulk-query
|
||||
// em financial_records. 'paid' | 'pending' | 'none'. Renderiza linha
|
||||
// curta abaixo do horário pra sessão com paciente (espelha os 3 canais
|
||||
// visuais da agenda). Ocorrências virtuais (sem id real) sempre 'none'
|
||||
// — não polui séries com pacote upfront.
|
||||
const showPaymentRow = computed(() => {
|
||||
if (!isSessaoComPaciente.value) return false;
|
||||
if (ev.value.is_occurrence) return false;
|
||||
return !!ev.value.paymentState;
|
||||
});
|
||||
const paymentVariant = computed(() => {
|
||||
const s = ev.value.paymentState;
|
||||
if (s === 'paid') return 'paid';
|
||||
if (s === 'pending') return 'pending';
|
||||
return 'none';
|
||||
});
|
||||
const paymentIcon = computed(() => {
|
||||
return paymentVariant.value === 'paid' ? 'pi pi-check-circle' : 'pi pi-dollar';
|
||||
});
|
||||
const paymentLabel = computed(() => {
|
||||
const state = ev.value.paymentState;
|
||||
const valor = ev.value.price;
|
||||
const valorFmt = (valor != null && !Number.isNaN(Number(valor)))
|
||||
? Number(valor).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
|
||||
: null;
|
||||
if (state === 'paid') {
|
||||
return valorFmt ? `Pago · ${valorFmt}` : 'Pago';
|
||||
}
|
||||
if (state === 'pending') {
|
||||
return valorFmt ? `A receber ${valorFmt} (cobrança pendente)` : 'Cobrança pendente';
|
||||
}
|
||||
// 'none' — sessão sem cobrança gerada ainda
|
||||
return valorFmt ? `A cobrar ${valorFmt}` : 'Cobrança ainda não gerada';
|
||||
});
|
||||
|
||||
function fmtHora(decimal) {
|
||||
if (decimal === null || decimal === undefined || Number.isNaN(decimal)) return '—';
|
||||
const h = Math.floor(decimal);
|
||||
@@ -121,16 +171,34 @@ function modalidadeIcon(mod) {
|
||||
{{ fmtHora(ev.startH) }} – {{ fmtHora(ev.endH) }}
|
||||
<span v-if="duracaoMin() !== null" class="evento-row__sub">· {{ duracaoMin() }}min</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="evento-row__edit"
|
||||
:disabled="busy"
|
||||
v-tooltip.top="'Editar sessão (data, hora, recorrência…)'"
|
||||
@click="emit('edit-sessao')"
|
||||
>
|
||||
<i class="pi pi-pencil" />
|
||||
<span>Editar sessão</span>
|
||||
</button>
|
||||
<div class="evento-row__edit-stack">
|
||||
<button
|
||||
type="button"
|
||||
class="evento-row__edit evento-row__edit--primary"
|
||||
:disabled="busy"
|
||||
v-tooltip.top="'Editar sessão (data, hora, recorrência…)'"
|
||||
@click="emit('edit-sessao')"
|
||||
>
|
||||
<i class="pi pi-pencil" />
|
||||
<span>Editar sessão</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
type="button"
|
||||
class="evento-row__edit evento-row__edit--danger"
|
||||
:disabled="busy"
|
||||
v-tooltip.top="'Excluir esta sessão (permanente)'"
|
||||
@click="emit('delete-sessao')"
|
||||
>
|
||||
<i class="pi pi-trash" />
|
||||
<span>Excluir sessão</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showPaymentRow" class="evento-row evento-row--pay" :class="`evento-row--pay-${paymentVariant}`">
|
||||
<i :class="paymentIcon" />
|
||||
<span>{{ paymentLabel }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="ev.modalidade" class="evento-row">
|
||||
@@ -239,6 +307,34 @@ function modalidadeIcon(mod) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Grupo Financeiro — abre dialog com todos os lançamentos
|
||||
vinculados a esta sessão (cobrança original + multas/taxas)
|
||||
+ antecipar pagamento (paciente paga antes da sessão).
|
||||
Adicionado 2026-05-14. Só pra sessão com paciente. -->
|
||||
<section v-if="isSessaoComPaciente" class="evento-actions__section">
|
||||
<div class="evento-actions__label">Financeiro:</div>
|
||||
<div class="evento-actions__group">
|
||||
<button
|
||||
class="evento-act"
|
||||
:disabled="busy"
|
||||
v-tooltip.top="'Ver lançamentos vinculados a esta sessão'"
|
||||
@click="emit('ver-lancamentos')"
|
||||
>
|
||||
<i class="pi pi-list" />
|
||||
<span class="evento-act__label">Lançamentos</span>
|
||||
</button>
|
||||
<button
|
||||
class="evento-act"
|
||||
:disabled="busy"
|
||||
v-tooltip.top="'Paciente quer pagar antes da sessão (pacote saldo)'"
|
||||
@click="emit('antecipar-pagamento')"
|
||||
>
|
||||
<i class="pi pi-money-bill" />
|
||||
<span class="evento-act__label">Antecipar pagamento</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Grupo Geral (não-sessão: bloqueio/compromisso/etc).
|
||||
Aqui "Editar" abre o evento em si (não tem paciente). -->
|
||||
<section v-else class="evento-actions__section">
|
||||
@@ -369,12 +465,56 @@ function modalidadeIcon(mod) {
|
||||
margin-left: 4px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
/* Botão "Editar sessão" inline na linha das horas. Discreto na largura
|
||||
padrão, ganha destaque no hover. Margin-left auto pra alinhar à direita. */
|
||||
.evento-row__edit {
|
||||
|
||||
/* Linha de cobrança — espelha os 3 canais visuais da agenda:
|
||||
- paid: verde (estado-alvo, sessão quitada)
|
||||
- pending: amber (cobrança gerada mas não paga)
|
||||
- none: amber leve (sem cobrança gerada ainda) */
|
||||
.evento-row--pay {
|
||||
font-weight: 500;
|
||||
}
|
||||
.evento-row--pay-paid {
|
||||
color: #047857; /* emerald-700 */
|
||||
}
|
||||
.evento-row--pay-paid > i {
|
||||
color: #10b981; /* emerald-500 */
|
||||
}
|
||||
html.app-dark .evento-row--pay-paid {
|
||||
color: #34d399; /* emerald-400 */
|
||||
}
|
||||
html.app-dark .evento-row--pay-paid > i {
|
||||
color: #34d399;
|
||||
}
|
||||
.evento-row--pay-pending,
|
||||
.evento-row--pay-none {
|
||||
color: #b45309;
|
||||
}
|
||||
.evento-row--pay-pending > i,
|
||||
.evento-row--pay-none > i {
|
||||
color: #f59e0b;
|
||||
}
|
||||
html.app-dark .evento-row--pay-pending,
|
||||
html.app-dark .evento-row--pay-none {
|
||||
color: #fbbf24;
|
||||
}
|
||||
html.app-dark .evento-row--pay-pending > i,
|
||||
html.app-dark .evento-row--pay-none > i {
|
||||
color: #fbbf24;
|
||||
}
|
||||
/* 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 {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: stretch;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.evento-row__edit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
padding: 4px 10px;
|
||||
background: var(--m-bg-soft);
|
||||
@@ -397,6 +537,28 @@ function modalidadeIcon(mod) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.evento-row__edit i { font-size: 0.65rem; }
|
||||
/* Variant primary (Editar sessão — ação principal). */
|
||||
.evento-row__edit--primary {
|
||||
background: var(--primary-color, #7c6af7);
|
||||
border-color: var(--primary-color, #7c6af7);
|
||||
color: var(--primary-color-text, #fff);
|
||||
}
|
||||
.evento-row__edit--primary:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--primary-color, #7c6af7) 88%, black);
|
||||
border-color: color-mix(in srgb, var(--primary-color, #7c6af7) 88%, black);
|
||||
color: var(--primary-color-text, #fff);
|
||||
}
|
||||
/* Variant danger (Excluir sessão — destrutivo, outlined). */
|
||||
.evento-row__edit--danger {
|
||||
background: transparent;
|
||||
border-color: color-mix(in srgb, var(--red-500, #ef4444) 50%, var(--m-border));
|
||||
color: var(--red-400, #f87171);
|
||||
}
|
||||
.evento-row__edit--danger:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--red-500, #ef4444) 12%, transparent);
|
||||
border-color: var(--red-500, #ef4444);
|
||||
color: var(--red-300, #fca5a5);
|
||||
}
|
||||
.evento-status {
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
|
||||
Reference in New Issue
Block a user