agenda: C5+C6 testes OK + atalho Gerar fatura + RPC idempotência fix
DB - migration 20260519000001: create_financial_record_for_session passa a ignorar status='cancelled' na idempotência (era bug — cancelar e tentar regerar travava silencioso retornando o cancelado) Cenário 5 (convênio) — fixes pra save + visualização - Convênio: amount lia 'price' (null) → agora detecta via insurance_plan_id e usa insurance_value. payment_method forçado 'convenio' (era 'asaas') - Popover: ev.price era null em convênio → normalize expõe insurance_value e paymentLabel faz fallback. Linha mostra "A receber R$ X" corretamente - /financeiro: branch novo pra payment_method='convenio' → pill violeta com pi-id-card (antes ficava sem indicador, igual particular) Cenário 6 (recorrente sem pacote, Maria Magali) — materialização - chargeMode='none' não materializava a 1ª (todas viravam virtuais, sem badge $). Agora materializa a 1ª no fluxo de criação recorrente - Bug intermediário: usei 'paciente_id' (Portuguese) mas agendaRepository dropa esse campo. Corrigido pra 'patient_id' (English DB column) Atalho "Gerar fatura" no popover - Pill amber pequeno ao lado de "A cobrar R$ X" no popover (paymentVariant ='none' + sessão materializada) - Wire em MelissaLayout via emit gerar-cobranca + handler onGerarCobrancaQuick (chama gerarCobrancaManual, fecha popover pra impedir double-click) - Bulk-load do useMelissaAgenda e fetchRecord do AgendaEventoFinanceiroPanel agora filtram status='cancelled' (resolve badge $ residual + botão sumido) Header do popover: info de pacote/série - "Sessão · Pacote · N sessões" ou "Sessão X de Y" abaixo do tipo (computed seriesLabel lê do _raw da rule) Título do dialog "Sessão do Pacote · Sessão" - Quando commitment name é "Sessão" (default), drop pra evitar duplicação - Outros nomes (Avaliação, etc) permanecem com forma completa Excluir série inteira (popover) - Novo botão "Excluir série" no popover quando evento pertence a recorrência - Hard delete: financial_records pendentes → agenda_eventos materializados → recurrence_rules (CASCADE leva exceptions + rule_services) - Bloqueia se algum record tem status='paid' (estornar primeiro) cancel_session some da agenda - useRecurrence.expandRules agora pula occurrence com exception type= 'cancel_session' (era visível com status cancelado; doc dizia "some da agenda" mas código mantinha. Honra a promessa do diálogo) - patient_missed / therapist_canceled / holiday_block permanecem visíveis como histórico UX outros - "+ Novo convênio" toolbar em ConfiguracoesConveniosPage (botão faltava — empty state mandava clicar em botão inexistente) - InsurancePlanServiceQuickCreateDialog: cadastrar procedimento POR CIMA do AgendaEventDialog sem sair da agenda. Auto-seleciona quando nada estava selecionado antes - Hint contextual abaixo do card Sessão/Honorários: convênio = "Nº guia opcional"; gratuito = "sem cobrança". Particular sem hint - recurrence_exceptions cancel agora usa upsert com onConflict (idempotente, não quebra com unique violation em re-cancel) - goToConveniosConfig removida (dead code após quick-create inline) CSS - .aed-row-50 perdeu margin-bottom (user request) - .field-card.mb-4 ganhou margin-top: 1rem (scoped a composer wrappers) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,8 +35,10 @@ const emit = defineEmits([
|
||||
'whatsapp',
|
||||
'historico',
|
||||
'delete-sessao', // botão "Excluir sessão" — só pra sessões avulsas (sem recorrência)
|
||||
'delete-series', // botão "Excluir série inteira" — hard delete da regra + materializadas + records pendentes
|
||||
'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)
|
||||
'antecipar-pagamento', // botão "Antecipar pagamento" — paciente quer pagar antes da sessão (pacote saldo)
|
||||
'gerar-cobranca' // botão inline "Gerar fatura" ao lado de "A cobrar R$ X" — atalho sem precisar abrir dialog
|
||||
]);
|
||||
|
||||
// Regra atualizada (2026-05-14): botão Excluir aparece SEMPRE pra sessão.
|
||||
@@ -51,6 +53,27 @@ const canDelete = computed(() => {
|
||||
return true;
|
||||
});
|
||||
|
||||
// Mostra "Excluir série inteira" apenas pra eventos que pertencem a
|
||||
// uma regra de recorrência (materializados OU ocorrências virtuais).
|
||||
const isPartOfSeries = computed(() => {
|
||||
const e = ev.value;
|
||||
if (!e) return false;
|
||||
return !!(e.recurrence_id || e.serie_id);
|
||||
});
|
||||
|
||||
// Label "Pacote · X sessões" / "Série semanal" pro popover quando o
|
||||
// evento pertence a uma recorrência. Lê do _raw que carrega os campos
|
||||
// da regra (max_occurrences, frequency_type quando expandido).
|
||||
const seriesLabel = computed(() => {
|
||||
if (!isPartOfSeries.value) return null;
|
||||
const raw = ev.value._raw || {};
|
||||
const total = raw.max_occurrences || raw.qtd_sessoes || raw.series_total || null;
|
||||
const idx = raw.occurrence_index || raw.serie_index || null;
|
||||
if (total && idx) return `Sessão ${idx} de ${total}`;
|
||||
if (total) return `Pacote · ${total} sessões`;
|
||||
return 'Série recorrente';
|
||||
});
|
||||
|
||||
const ev = computed(() => props.evento || {});
|
||||
|
||||
const tipoLabel = computed(() => {
|
||||
@@ -105,7 +128,9 @@ const paymentIcon = computed(() => {
|
||||
});
|
||||
const paymentLabel = computed(() => {
|
||||
const state = ev.value.paymentState;
|
||||
const valor = ev.value.price;
|
||||
// Em sessão particular, valor mora em price. Em convênio, vai pra
|
||||
// insurance_value (price = null). Fallback cobre os dois casos.
|
||||
const valor = ev.value.price ?? ev.value.insurance_value;
|
||||
const valorFmt = (valor != null && !Number.isNaN(Number(valor)))
|
||||
? Number(valor).toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })
|
||||
: null;
|
||||
@@ -148,7 +173,10 @@ function modalidadeIcon(mod) {
|
||||
<div class="evento-head__main">
|
||||
<div class="evento-pill" :style="{ backgroundColor: ev.color }" />
|
||||
<div class="min-w-0">
|
||||
<div class="evento-tipo">{{ tipoLabel }}</div>
|
||||
<div class="evento-tipo">
|
||||
{{ tipoLabel }}
|
||||
<span v-if="seriesLabel" class="evento-tipo__series">· {{ seriesLabel }}</span>
|
||||
</div>
|
||||
<div class="evento-titulo">
|
||||
{{ isSessaoComPaciente ? ev.pacienteNome : (ev.label || ev.titulo || '—') }}
|
||||
</div>
|
||||
@@ -193,12 +221,38 @@ function modalidadeIcon(mod) {
|
||||
<i class="pi pi-trash" />
|
||||
<span>Excluir sessão</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isPartOfSeries"
|
||||
type="button"
|
||||
class="evento-row__edit evento-row__edit--danger"
|
||||
:disabled="busy"
|
||||
v-tooltip.top="'Excluir a série inteira (todas as sessões da recorrência)'"
|
||||
@click="emit('delete-series')"
|
||||
>
|
||||
<i class="pi pi-history" />
|
||||
<span>Excluir série</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>
|
||||
<!-- Atalho "Gerar fatura" — só pra sessão materializada
|
||||
com paymentState='none' (cobrança ainda não gerada).
|
||||
Pago/pendente já existe um record; nesses casos não
|
||||
cabe gerar de novo. -->
|
||||
<button
|
||||
v-if="paymentVariant === 'none' && !ev.is_occurrence"
|
||||
type="button"
|
||||
class="evento-row__pay-action"
|
||||
:disabled="busy"
|
||||
v-tooltip.top="'Gerar fatura agora'"
|
||||
@click="emit('gerar-cobranca')"
|
||||
>
|
||||
<i class="pi pi-plus" />
|
||||
<span>Gerar fatura</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="ev.modalidade" class="evento-row">
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user