Files
agenciapsilmno/src/features/agenda/components/AgendaStatusChangeConfirmDialog.vue
T
Leonardo 00c4168393 agenda: C12 prep — detectar paid pre-existente em pacote saldo realizada
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>
2026-05-20 12:28:19 -03:00

672 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
|--------------------------------------------------------------------------
| 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) 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 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 foi pago. Nenhuma multa configurada 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 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>
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 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, 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>