00c4168393
Preparacao pra teste C12 (antecipar pagamento). Fluxo: 1. User clica "Antecipar pagamento" em virtual futura -> cria record paid R$ X sem consumir saldo 2. Depois marca a sessao como Realizada -> dialog deve detectar o paid + so consumir saldo (NAO criar record novo, evitar duplicidade) Sem esse fix, marcar Realizada apos antecipar abriria o dialog "Gerar cobranca?" com default true, gerando record novo duplicado. Implementacao: - _loadStatusChangeContext: carrega ctx.existingPaidRecord (qualquer paid linkado ao evento, n=1) - Dialog: nova prop existingPaidRecord + computed showAlreadyPaid (substitui showCobrancaPacote quando paid existe) - Template: bloco "Sessao ja paga via antecipacao" com info do pagamento + preview do consumo de saldo - _applyStatusDecisions: novo branch 4-pre roda ANTES do generatePackageCharge: se realizado+pacote saldo+paid existe, roda tasks pendentes (1b amarra) + incrementa saldo sem criar record. Return cedo. Backfill: Andre 10/06 voltou pra agendado + saldo 2/4 (estado limpo pra testar C12 com a sessao 10/06 antecipando). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
672 lines
30 KiB
Vue
672 lines
30 KiB
Vue
<!--
|
||
|--------------------------------------------------------------------------
|
||
| Agência PSI
|
||
|--------------------------------------------------------------------------
|
||
| Criado e desenvolvido por Leonardo Nohama
|
||
|
|
||
| Tecnologia aplicada à escuta.
|
||
| Estrutura para o cuidado.
|
||
|
|
||
| Arquivo: src/features/agenda/components/AgendaStatusChangeConfirmDialog.vue
|
||
| Data: 2026
|
||
| Local: São Carlos/SP — Brasil
|
||
|--------------------------------------------------------------------------
|
||
| © 2026 — Todos os direitos reservados
|
||
|--------------------------------------------------------------------------
|
||
-->
|
||
<script setup>
|
||
/*
|
||
* AgendaStatusChangeConfirmDialog — Dialog que aparece ao mudar status
|
||
* de uma sessão (realizado/faltou/cancelado). Mostra defaults vindos da
|
||
* config (financial_exceptions, billing_contracts) e permite override
|
||
* caso a caso pelo terapeuta/clínica antes de aplicar.
|
||
*
|
||
* 5 variantes de renderização baseadas em (novoStatus, eventoContext):
|
||
* - faltou/cancelado + avulsa → bloco multa
|
||
* - faltou/cancelado + pacote saldo → bloco saldo + bloco multa
|
||
* - faltou/cancelado + pacote upfront → bloco multa (saldo n/a)
|
||
* - realizado + avulsa pendente → bloco "registrar pagamento"
|
||
* - realizado + pacote saldo → bloco "gerar cobrança no pacote"
|
||
*
|
||
* Emit 'confirm' com objeto descrevendo o que o handler deve fazer:
|
||
* {
|
||
* consumeSaldo: bool, // só relevante em pacote saldo + faltou/cancelado
|
||
* applyFine: bool, // se vai cobrar multa
|
||
* fineAmount: number|null, // valor da multa (editavel)
|
||
* markPaid: bool, // realizado avulsa → marcar paga
|
||
* paymentMethod: string, // método se markPaid ou gerar cobrança
|
||
* generatePackageCharge: bool // realizado em pacote saldo → criar record
|
||
* }
|
||
*/
|
||
import { ref, computed, watch } from 'vue';
|
||
|
||
const props = defineProps({
|
||
modelValue: { type: Boolean, default: false },
|
||
evento: { type: Object, default: null },
|
||
novoStatus: { type: String, default: '' }, // 'realizado' | 'faltou' | 'cancelado' | 'agendado' (reverse)
|
||
regraExcecao: { type: Object, default: null }, // row de financial_exceptions ou null
|
||
billingContract: { type: Object, default: null }, // row de billing_contracts ou null
|
||
billingContractStyle: { type: String, default: null }, // 'upfront' | 'saldo' | null
|
||
// Quando avulsa+pendente e novoStatus='realizado': financial_record relacionado
|
||
pendingRecord: { type: Object, default: null },
|
||
// Quando pacote saldo + realizado + record paid pré-existente (C12 antecipado):
|
||
// dialog não oferece "Gerar cobrança" — só confirma consumo de saldo.
|
||
existingPaidRecord: { type: Object, default: null },
|
||
// Preço da sessão (pra calcular multa percentual e cobrança de pacote saldo)
|
||
sessionPrice: { type: Number, default: 0 },
|
||
// Reverse transition (novoStatus='agendado'): artefatos a desfazer.
|
||
// { previousStatus, activeRecords[], saldoConsumed }
|
||
reverseArtifacts: { type: Object, default: null }
|
||
});
|
||
|
||
const emit = defineEmits(['update:modelValue', 'confirm']);
|
||
|
||
// ─── State interno (refs editáveis) ─────────────────────────────────────
|
||
const consumeSaldo = ref(false);
|
||
const applyFine = ref(false);
|
||
const fineAmount = ref(0);
|
||
const markPaid = ref(true); // default em realizado: já recebeu
|
||
const paymentMethod = ref('pix'); // método em "como recebeu" ou "como cobrar"
|
||
const generatePackageCharge = ref(true); // realizado em pacote saldo: gera por padrão
|
||
// ─── Reverse transition state (novoStatus='agendado') ──────────────────
|
||
// Decisões pra desfazer ao reverter pra agendado.
|
||
const reverseCancelPending = ref(true); // cancela records pending/overdue
|
||
const reverseRestoreSaldo = ref(true); // devolve 1 ao saldo do pacote
|
||
// Records 'paid' não têm radio — só warning textual (estorno é manual).
|
||
|
||
// Reset/init ao abrir
|
||
watch(
|
||
() => props.modelValue,
|
||
(open) => {
|
||
if (!open) return;
|
||
// Defaults baseados em context
|
||
consumeSaldo.value = !!props.regraExcecao?.default_consume_on_miss;
|
||
applyFine.value = _calcInitialFineApply();
|
||
fineAmount.value = _calcInitialFineAmount();
|
||
// Default markPaid:
|
||
// - Avulsa realizada (showRegistrarPagto): default false (manter pendente)
|
||
// - Pacote saldo realizada (showCobrancaPacote): default false (gerar pendente)
|
||
// Em ambos casos o user precisa selecionar ativamente "Sim, já recebi"
|
||
// pra registrar paid — evita marcar paid sem querer.
|
||
markPaid.value = false;
|
||
// paymentMethod default depende do contexto. Inicia 'pending' (que cai
|
||
// no select de "Como vai cobrar?" quando markPaid=false). Quando user
|
||
// troca pra "Sim, já recebi", precisa escolher PIX/Dinheiro/etc.
|
||
paymentMethod.value = 'pending';
|
||
generatePackageCharge.value = true;
|
||
// Reverse transition: defaults safer = cancela records pendentes +
|
||
// devolve saldo (typical recovery flow). Records paid não têm radio.
|
||
reverseCancelPending.value = true;
|
||
reverseRestoreSaldo.value = true;
|
||
}
|
||
);
|
||
|
||
// ─── Computeds: o que renderizar ────────────────────────────────────────
|
||
const isFaltouOrCancelado = computed(() => props.novoStatus === 'faltou' || props.novoStatus === 'cancelado');
|
||
const isRealizado = computed(() => props.novoStatus === 'realizado');
|
||
const isReverseAgendado = computed(() => props.novoStatus === 'agendado');
|
||
const isAvulsa = computed(() => !props.billingContract);
|
||
const isPacoteSaldo = computed(() => props.billingContract?.type === 'package' && props.billingContractStyle === 'saldo');
|
||
const isPacoteUpfront = computed(() => props.billingContract?.type === 'package' && props.billingContractStyle === 'upfront');
|
||
|
||
// Reverse transition: derivados pra renderizar blocos
|
||
const reversePendingRecords = computed(() => {
|
||
const recs = props.reverseArtifacts?.activeRecords || [];
|
||
return recs.filter((r) => r.status === 'pending' || r.status === 'overdue');
|
||
});
|
||
const reversePaidRecords = computed(() => {
|
||
const recs = props.reverseArtifacts?.activeRecords || [];
|
||
return recs.filter((r) => r.status === 'paid');
|
||
});
|
||
const reverseHasPaid = computed(() => reversePaidRecords.value.length > 0);
|
||
const reverseHasPending = computed(() => reversePendingRecords.value.length > 0);
|
||
const reverseShowSaldo = computed(() => isReverseAgendado.value && !!props.reverseArtifacts?.saldoConsumed);
|
||
const reversePreviousStatusLabel = computed(() => {
|
||
const s = props.reverseArtifacts?.previousStatus;
|
||
if (s === 'realizado') return 'Realizada';
|
||
if (s === 'faltou') return 'Faltou';
|
||
if (s === 'cancelado') return 'Cancelado';
|
||
return s || 'estado anterior';
|
||
});
|
||
|
||
// Mostrar bloco multa: faltou/cancelado + regra existe + charge_mode != 'none'
|
||
const showFineBlock = computed(() => isFaltouOrCancelado.value && props.regraExcecao && props.regraExcecao.charge_mode !== 'none');
|
||
|
||
// Mostrar bloco saldo: faltou/cancelado + pacote saldo
|
||
const showSaldoBlock = computed(() => isFaltouOrCancelado.value && isPacoteSaldo.value);
|
||
|
||
// Mostrar bloco "registrar pagamento": realizado + avulsa pendente
|
||
const showRegistrarPagto = computed(() => isRealizado.value && isAvulsa.value && props.pendingRecord && props.pendingRecord.status === 'pending');
|
||
|
||
// Mostrar bloco "cobrança no pacote": realizado + pacote saldo + SEM paid pré-existente
|
||
// (se já tem paid via antecipação, mostra o bloco "já pago" em vez deste)
|
||
const showCobrancaPacote = computed(() => isRealizado.value && isPacoteSaldo.value && !props.existingPaidRecord);
|
||
// Mostrar bloco "já paga via antecipação": realizado + pacote saldo + paid pré-existente
|
||
const showAlreadyPaid = computed(() => isRealizado.value && isPacoteSaldo.value && !!props.existingPaidRecord);
|
||
|
||
// ─── Header ────────────────────────────────────────────────────────────
|
||
const headerTitle = computed(() => {
|
||
const labels = {
|
||
realizado: '✓ Marcar como Realizado',
|
||
faltou: '⚠ Marcar como Faltou',
|
||
cancelado: '✕ Marcar como Cancelado',
|
||
agendado: '↺ Reativar sessão (voltar pra Agendada)'
|
||
};
|
||
return labels[props.novoStatus] || 'Atualizar status';
|
||
});
|
||
|
||
// ─── Sub-info do evento ────────────────────────────────────────────────
|
||
const pacienteNome = computed(() => props.evento?.paciente_nome || props.evento?.patients?.nome_completo || 'Paciente');
|
||
const dataHora = computed(() => {
|
||
const ini = props.evento?.inicio_em;
|
||
if (!ini) return '';
|
||
try {
|
||
const d = new Date(ini);
|
||
return d.toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' });
|
||
} catch {
|
||
return '';
|
||
}
|
||
});
|
||
const isSerieSemPacote = computed(() => {
|
||
// Série recorrente que não criou billing_contract (chargeMode='none' no save).
|
||
// Detecta via recurrence_id/serie_id na row sem contract carregado.
|
||
if (props.billingContract) return false;
|
||
const e = props.evento;
|
||
return !!(e?.recurrence_id || e?.serie_id || e?.is_occurrence);
|
||
});
|
||
|
||
const tipoTag = computed(() => {
|
||
if (isPacoteSaldo.value) return `Pacote saldo: ${props.billingContract.sessions_used ?? 0} de ${props.billingContract.total_sessions ?? '?'} usadas`;
|
||
if (isPacoteUpfront.value) return `Pacote upfront: R$ ${_fmtBRL(props.billingContract.package_price)} pago`;
|
||
if (isSerieSemPacote.value) return 'Série recorrente (sem pacote)';
|
||
if (props.pendingRecord) return `Avulsa: R$ ${_fmtBRL(props.pendingRecord.final_amount || props.pendingRecord.amount)} pendente`;
|
||
return 'Avulsa';
|
||
});
|
||
|
||
// ─── Labels e options ──────────────────────────────────────────────────
|
||
const paymentMethodOptions = [
|
||
{ value: 'pix', label: 'PIX' },
|
||
{ value: 'dinheiro', label: 'Dinheiro' },
|
||
{ value: 'deposito', label: 'Depósito' },
|
||
{ value: 'cartao_maquininha', label: 'Cartão (maquininha)' }
|
||
];
|
||
// Opções pra "Como vai cobrar?" quando markPaid=false (sessão pendente
|
||
// no pacote saldo). 'pending' = só registra como pendente, terapeuta
|
||
// cobra depois pelo /financeiro. 'link' = gera link Asaas e marca
|
||
// payment_method='asaas' no record (pós-confirm o handler updata).
|
||
const paymentMethodOptionsPending = [
|
||
{ value: 'pending', label: 'Apenas registrar como pendente' },
|
||
{ value: 'link', label: 'Enviar link de pagamento (Asaas)' }
|
||
];
|
||
|
||
const regraResumo = computed(() => {
|
||
const r = props.regraExcecao;
|
||
if (!r) return '';
|
||
if (r.charge_mode === 'full') return `Sessão completa = R$ ${_fmtBRL(props.sessionPrice)}`;
|
||
if (r.charge_mode === 'fixed_fee') return `Taxa fixa: R$ ${_fmtBRL(r.charge_value)}`;
|
||
if (r.charge_mode === 'percentage') return `${r.charge_pct}% da sessão = R$ ${_fmtBRL((props.sessionPrice * (r.charge_pct ?? 0)) / 100)}`;
|
||
return 'Sem cobrança';
|
||
});
|
||
|
||
// ─── Helpers ───────────────────────────────────────────────────────────
|
||
function _fmtBRL(v) {
|
||
return Number(v ?? 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
}
|
||
|
||
function _calcInitialFineAmount() {
|
||
const r = props.regraExcecao;
|
||
if (!r) return 0;
|
||
if (r.charge_mode === 'full') return Number(props.sessionPrice) || 0;
|
||
if (r.charge_mode === 'fixed_fee') return Number(r.charge_value) || 0;
|
||
if (r.charge_mode === 'percentage') return parseFloat((((Number(props.sessionPrice) || 0) * (Number(r.charge_pct) || 0)) / 100).toFixed(2));
|
||
return 0;
|
||
}
|
||
|
||
function _calcInitialFineApply() {
|
||
// Se regra existe e charge_mode != 'none': default true
|
||
// Exceção: cancelado fora da janela min_hours_notice → default false (paciente cancelou dentro do prazo)
|
||
const r = props.regraExcecao;
|
||
if (!r || r.charge_mode === 'none') return false;
|
||
if (props.novoStatus === 'cancelado' && r.min_hours_notice != null && props.evento?.inicio_em) {
|
||
const horasAteSessao = (new Date(props.evento.inicio_em).getTime() - Date.now()) / (1000 * 60 * 60);
|
||
// Se cancelou COM MAIS antecedência que min → sem multa por padrão
|
||
if (horasAteSessao >= Number(r.min_hours_notice)) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// Texto explicativo de porquê a multa veio (des)marcada por padrão.
|
||
// Aparece abaixo do checkbox no bloco multa pra deixar a regra visível
|
||
// ao terapeuta no momento da decisão.
|
||
const fineDefaultReason = computed(() => {
|
||
const r = props.regraExcecao;
|
||
if (!r || r.charge_mode === 'none') return '';
|
||
if (props.novoStatus !== 'cancelado' || r.min_hours_notice == null || !props.evento?.inicio_em) return '';
|
||
const horasAteSessao = (new Date(props.evento.inicio_em).getTime() - Date.now()) / (1000 * 60 * 60);
|
||
const min = Number(r.min_hours_notice);
|
||
const horasFmt = horasAteSessao < 0
|
||
? `${Math.abs(horasAteSessao).toFixed(1)}h após o início`
|
||
: horasAteSessao < 1
|
||
? `${Math.round(horasAteSessao * 60)}min antes`
|
||
: `${horasAteSessao.toFixed(1)}h antes`;
|
||
if (horasAteSessao >= min) {
|
||
return `Cancelou ${horasFmt} da sessão. Regra: multa apenas quando cancelamento ocorre com menos de ${min}h de antecedência → sem multa por padrão.`;
|
||
}
|
||
return `Cancelou ${horasFmt} da sessão (menos que os ${min}h da regra) → multa aplicada por padrão.`;
|
||
});
|
||
|
||
// ─── Actions ───────────────────────────────────────────────────────────
|
||
function onConfirm() {
|
||
// markPaid agora é considerado em DOIS contextos:
|
||
// 1. Avulsa pendente (showRegistrarPagto): paciente já pagou a cobrança?
|
||
// 2. Pacote saldo realizado (showCobrancaPacote): já recebeu o valor da sessão?
|
||
// Em ambos casos: markPaid=true → record vira paid; false → fica pending.
|
||
const considerMarkPaid = showRegistrarPagto.value || (showCobrancaPacote.value && generatePackageCharge.value);
|
||
emit('confirm', {
|
||
consumeSaldo: showSaldoBlock.value ? consumeSaldo.value : false,
|
||
applyFine: showFineBlock.value ? applyFine.value : false,
|
||
fineAmount: showFineBlock.value && applyFine.value ? Number(fineAmount.value) || 0 : 0,
|
||
markPaid: considerMarkPaid ? markPaid.value : false,
|
||
paymentMethod: paymentMethod.value,
|
||
generatePackageCharge: showCobrancaPacote.value ? generatePackageCharge.value : false,
|
||
// Reverse transition: só relevante quando novoStatus='agendado'
|
||
reverseCancelPending: isReverseAgendado.value ? reverseCancelPending.value : false,
|
||
reverseRestoreSaldo: isReverseAgendado.value ? reverseRestoreSaldo.value : false
|
||
});
|
||
emit('update:modelValue', false);
|
||
}
|
||
|
||
function onCancel() {
|
||
emit('update:modelValue', false);
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<Dialog
|
||
:visible="modelValue"
|
||
modal
|
||
:draggable="false"
|
||
:style="{ width: '480px', maxWidth: '96vw' }"
|
||
:breakpoints="{ '640px': '98vw' }"
|
||
@update:visible="emit('update:modelValue', $event)"
|
||
>
|
||
<template #header>
|
||
<div class="asccd-head">
|
||
<div class="asccd-head__title">{{ headerTitle }}</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="flex flex-col gap-4">
|
||
<!-- Resumo do evento -->
|
||
<div class="asccd-summary">
|
||
<div class="asccd-summary__name">{{ pacienteNome }}</div>
|
||
<div class="asccd-summary__meta">
|
||
<span v-if="dataHora">{{ dataHora }}</span>
|
||
<span class="asccd-summary__tag">{{ tipoTag }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── Reverse transition: voltar pra Agendada (com artefatos) ── -->
|
||
<div v-if="isReverseAgendado" class="asccd-block">
|
||
<div class="asccd-block__title">
|
||
<i class="pi pi-info-circle" />
|
||
Reverter status de <b>{{ reversePreviousStatusLabel }}</b> pra <b>Agendada</b>
|
||
</div>
|
||
<small class="asccd-hint">Esta sessão tem ações financeiras vinculadas. Escolha o que fazer com cada uma antes de reverter:</small>
|
||
|
||
<!-- Records pendentes -->
|
||
<div v-if="reverseHasPending" class="asccd-block mt-3">
|
||
<div class="asccd-block__title">
|
||
<i class="pi pi-money-bill" />
|
||
Cobrança pendente vinculada
|
||
</div>
|
||
<ul class="asccd-list">
|
||
<li v-for="r in reversePendingRecords" :key="r.id">
|
||
<span class="font-medium">{{ r.description || 'Cobrança' }}</span>
|
||
<span> · R$ {{ _fmtBRL(r.final_amount || r.amount) }}</span>
|
||
</li>
|
||
</ul>
|
||
<div class="asccd-radio-group mt-2">
|
||
<label class="asccd-radio">
|
||
<input type="radio" :value="true" v-model="reverseCancelPending" />
|
||
<span><b>Cancelar</b> a(s) cobrança(s) — recomendado</span>
|
||
</label>
|
||
<label class="asccd-radio">
|
||
<input type="radio" :value="false" v-model="reverseCancelPending" />
|
||
<span><b>Manter</b> ativa(s) — sessão volta agendada mas cobrança continua aberta</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Records pagos: warning sem ação -->
|
||
<div v-if="reverseHasPaid" class="asccd-info mt-3">
|
||
<i class="pi pi-exclamation-triangle" />
|
||
<div>
|
||
<b>Atenção:</b> Esta sessão tem cobrança(s) já paga(s) ({{ reversePaidRecords.length }} record(s)).
|
||
Reverter o status NÃO estorna o pagamento automaticamente — pra estornar use o /financeiro.
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Saldo consumido em pacote -->
|
||
<div v-if="reverseShowSaldo" class="asccd-block mt-3">
|
||
<div class="asccd-block__title">
|
||
<i class="pi pi-wallet" />
|
||
Saldo do pacote consumido
|
||
</div>
|
||
<div class="asccd-fine-rule">Saldo atual: {{ billingContract?.sessions_used ?? '?' }} de {{ billingContract?.total_sessions ?? '?' }} usadas</div>
|
||
<div class="asccd-radio-group mt-2">
|
||
<label class="asccd-radio">
|
||
<input type="radio" :value="true" v-model="reverseRestoreSaldo" />
|
||
<span><b>Devolver</b> 1 sessão ao saldo — recomendado</span>
|
||
</label>
|
||
<label class="asccd-radio">
|
||
<input type="radio" :value="false" v-model="reverseRestoreSaldo" />
|
||
<span><b>Manter</b> saldo consumido — sessão volta agendada mas saldo continua decrementado</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── Bloco SALDO (pacote saldo + faltou/cancelado) ────────── -->
|
||
<div v-if="showSaldoBlock" class="asccd-block">
|
||
<div class="asccd-block__title">
|
||
<i class="pi pi-wallet" />
|
||
O que fazer com a vaga do pacote?
|
||
</div>
|
||
<div class="asccd-radio-group">
|
||
<label class="asccd-radio">
|
||
<input type="radio" :value="false" v-model="consumeSaldo" />
|
||
<span><b>Remarcar</b> — não consome saldo (paciente pode remarcar)</span>
|
||
</label>
|
||
<label class="asccd-radio">
|
||
<input type="radio" :value="true" v-model="consumeSaldo" />
|
||
<span><b>Descontar</b> — sessão perdida ({{ billingContract?.sessions_used ?? 0 }} → {{ (billingContract?.sessions_used ?? 0) + 1 }})</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── Bloco MULTA (faltou/cancelado com regra configurada) ── -->
|
||
<div v-if="showFineBlock" class="asccd-block">
|
||
<div class="asccd-block__title">
|
||
<i class="pi pi-money-bill" />
|
||
Aplicar multa?
|
||
</div>
|
||
<div class="asccd-fine-rule">Regra atual: {{ regraResumo }}</div>
|
||
<div class="asccd-fine-row">
|
||
<Checkbox v-model="applyFine" inputId="asccd-apply-fine" binary />
|
||
<label for="asccd-apply-fine" class="cursor-pointer">Aplicar multa</label>
|
||
<InputNumber
|
||
v-model="fineAmount"
|
||
:disabled="!applyFine"
|
||
mode="currency"
|
||
currency="BRL"
|
||
locale="pt-BR"
|
||
:min="0"
|
||
size="small"
|
||
class="asccd-fine-input"
|
||
/>
|
||
</div>
|
||
<small v-if="fineDefaultReason" class="asccd-hint">
|
||
<i class="pi pi-info-circle" /> {{ fineDefaultReason }}
|
||
</small>
|
||
<small v-if="isPacoteUpfront" class="asccd-hint">
|
||
ℹ Pacote já pago; multa entra como cobrança adicional avulsa.
|
||
</small>
|
||
</div>
|
||
|
||
<!-- Pacote upfront sem multa configurada: aviso -->
|
||
<div v-if="isFaltouOrCancelado && isPacoteUpfront && !showFineBlock" class="asccd-info">
|
||
<i class="pi pi-info-circle" />
|
||
Pacote já foi pago. Nenhuma multa configurada — só atualiza o status.
|
||
</div>
|
||
|
||
<!-- ─── Bloco REGISTRAR PAGAMENTO (realizado + avulsa pendente) ── -->
|
||
<div v-if="showRegistrarPagto" class="asccd-block">
|
||
<div class="asccd-block__title">
|
||
<i class="pi pi-check-circle" />
|
||
A sessão foi paga?
|
||
</div>
|
||
<div class="asccd-radio-group">
|
||
<label class="asccd-radio">
|
||
<input type="radio" :value="false" v-model="markPaid" />
|
||
<span>Não, manter cobrança pendente</span>
|
||
</label>
|
||
<label class="asccd-radio">
|
||
<input type="radio" :value="true" v-model="markPaid" />
|
||
<span>Sim, registrar pagamento</span>
|
||
</label>
|
||
</div>
|
||
<div v-if="markPaid" class="asccd-method-row">
|
||
<label class="asccd-method-label">Como recebeu?</label>
|
||
<Select
|
||
v-model="paymentMethod"
|
||
:options="paymentMethodOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
size="small"
|
||
class="asccd-method-select"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── Bloco "JÁ PAGA via antecipação" (C12 fluxo de retorno) ─── -->
|
||
<div v-if="showAlreadyPaid" class="asccd-block">
|
||
<div class="asccd-block__title">
|
||
<i class="pi pi-check-circle" />
|
||
Sessão já paga via antecipação
|
||
</div>
|
||
<div class="asccd-info">
|
||
<i class="pi pi-info-circle" />
|
||
<div>
|
||
Cobrança de <b>R$ {{ _fmtBRL(existingPaidRecord.final_amount || existingPaidRecord.amount) }}</b>
|
||
já foi registrada como paga ({{ existingPaidRecord.payment_method || 'método não definido' }}).
|
||
Marcar como Realizada vai <b>consumir 1 sessão do saldo</b>
|
||
({{ billingContract?.sessions_used ?? 0 }} → {{ (billingContract?.sessions_used ?? 0) + 1 }}/{{ billingContract?.total_sessions ?? '?' }}).
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ─── Bloco COBRANÇA NO PACOTE (realizado + pacote saldo) ──── -->
|
||
<div v-if="showCobrancaPacote" class="asccd-block">
|
||
<div class="asccd-block__title">
|
||
<i class="pi pi-money-bill" />
|
||
Gerar cobrança no pacote?
|
||
</div>
|
||
<div class="asccd-fine-rule">Valor da sessão: R$ {{ _fmtBRL(sessionPrice) }}</div>
|
||
<div class="asccd-fine-row">
|
||
<Checkbox v-model="generatePackageCharge" inputId="asccd-gen-charge" binary />
|
||
<label for="asccd-gen-charge" class="cursor-pointer">Gerar cobrança e consumir 1 sessão</label>
|
||
</div>
|
||
<!-- Sub-question 1: a sessão já foi paga? (espelha o padrão da avulsa) -->
|
||
<div v-if="generatePackageCharge" class="asccd-radio-group mt-2">
|
||
<label class="asccd-radio">
|
||
<input type="radio" :value="false" v-model="markPaid" />
|
||
<span>Não, gerar como cobrança pendente</span>
|
||
</label>
|
||
<label class="asccd-radio">
|
||
<input type="radio" :value="true" v-model="markPaid" />
|
||
<span>Sim, já recebi</span>
|
||
</label>
|
||
</div>
|
||
<!-- Sub-question 2a: se "Já recebi" → método de recebimento (sem prefixo) -->
|
||
<div v-if="generatePackageCharge && markPaid" class="asccd-method-row">
|
||
<label class="asccd-method-label">Como recebeu?</label>
|
||
<Select
|
||
v-model="paymentMethod"
|
||
:options="paymentMethodOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
size="small"
|
||
class="asccd-method-select"
|
||
/>
|
||
</div>
|
||
<!-- Sub-question 2b: se "Pendente" → forma de cobrança (link Asaas vs registrar simples) -->
|
||
<div v-if="generatePackageCharge && !markPaid" class="asccd-method-row">
|
||
<label class="asccd-method-label">Como vai cobrar?</label>
|
||
<Select
|
||
v-model="paymentMethod"
|
||
:options="paymentMethodOptionsPending"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
size="small"
|
||
class="asccd-method-select"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<Button label="Cancelar" severity="secondary" outlined size="small" @click="onCancel" />
|
||
<Button label="Confirmar" icon="pi pi-check" size="small" @click="onConfirm" />
|
||
</template>
|
||
</Dialog>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.asccd-head__title {
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--text-color);
|
||
}
|
||
.asccd-summary {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.2rem;
|
||
padding: 0.65rem 0.85rem;
|
||
border-radius: 6px;
|
||
background: color-mix(in srgb, var(--p-primary-500) 4%, var(--surface-card));
|
||
border: 1px solid var(--surface-border);
|
||
}
|
||
.asccd-summary__name {
|
||
font-size: 0.92rem;
|
||
font-weight: 700;
|
||
color: var(--text-color);
|
||
}
|
||
.asccd-summary__meta {
|
||
font-size: 0.74rem;
|
||
color: var(--text-color-secondary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
.asccd-summary__tag {
|
||
display: inline-block;
|
||
padding: 1px 7px;
|
||
border-radius: 999px;
|
||
background: color-mix(in srgb, var(--p-primary-500) 12%, transparent);
|
||
color: var(--p-primary-color);
|
||
font-weight: 600;
|
||
font-size: 0.68rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.asccd-block {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
padding: 0.75rem 0.85rem;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--surface-border);
|
||
background: var(--surface-card);
|
||
}
|
||
.asccd-block__title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
color: var(--text-color);
|
||
}
|
||
.asccd-block__title i {
|
||
color: var(--p-primary-color);
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.asccd-radio-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.4rem;
|
||
}
|
||
.asccd-radio {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-size: 0.82rem;
|
||
color: var(--text-color);
|
||
cursor: pointer;
|
||
}
|
||
.asccd-radio input[type='radio'] {
|
||
accent-color: var(--p-primary-color);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.asccd-fine-rule {
|
||
font-size: 0.74rem;
|
||
color: var(--text-color-secondary);
|
||
font-style: italic;
|
||
}
|
||
.asccd-fine-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
.asccd-fine-input {
|
||
flex: 1;
|
||
min-width: 8rem;
|
||
}
|
||
|
||
.asccd-method-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
margin-top: 0.25rem;
|
||
}
|
||
.asccd-method-label {
|
||
font-size: 0.78rem;
|
||
color: var(--text-color);
|
||
font-weight: 500;
|
||
flex-shrink: 0;
|
||
}
|
||
.asccd-method-select {
|
||
flex: 1;
|
||
min-width: 12rem;
|
||
}
|
||
|
||
.asccd-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.6rem 0.85rem;
|
||
border-radius: 6px;
|
||
background: color-mix(in srgb, var(--blue-400, #60a5fa) 8%, var(--surface-card));
|
||
border: 1px solid color-mix(in srgb, var(--blue-400, #60a5fa) 30%, transparent);
|
||
font-size: 0.78rem;
|
||
color: var(--text-color);
|
||
}
|
||
.asccd-info i {
|
||
color: var(--blue-600, #2563eb);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.asccd-hint {
|
||
font-size: 0.7rem;
|
||
color: var(--text-color-secondary);
|
||
font-style: italic;
|
||
}
|
||
|
||
.asccd-list {
|
||
margin-top: 0.3rem;
|
||
padding-left: 1.2rem;
|
||
font-size: 0.78rem;
|
||
color: var(--text-color);
|
||
}
|
||
.asccd-list li {
|
||
list-style: disc;
|
||
padding: 0.1rem 0;
|
||
}
|
||
</style>
|