Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras
This commit is contained in:
@@ -30,387 +30,340 @@
|
||||
cobranca-atualizada — após qualquer mutação, para o pai recarregar
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useAgendaFinanceiro } from '@/composables/useAgendaFinanceiro'
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useAgendaFinanceiro } from '@/composables/useAgendaFinanceiro';
|
||||
|
||||
// ── props / emits ─────────────────────────────────────────────────────────────
|
||||
const props = defineProps({
|
||||
evento: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
evento: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['cobranca-atualizada'])
|
||||
const emit = defineEmits(['cobranca-atualizada']);
|
||||
|
||||
// ── external ──────────────────────────────────────────────────────────────────
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
const { gerarCobrancaManual, loading: finLoading, error: finError } = useAgendaFinanceiro()
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const { gerarCobrancaManual, loading: finLoading, error: finError } = useAgendaFinanceiro();
|
||||
|
||||
// ── estado local ──────────────────────────────────────────────────────────────
|
||||
const record = ref(null) // financial_record vinculado
|
||||
const fetching = ref(false)
|
||||
const generating = ref(false)
|
||||
const record = ref(null); // financial_record vinculado
|
||||
const fetching = ref(false);
|
||||
const generating = ref(false);
|
||||
|
||||
// ── opções de método de pagamento ─────────────────────────────────────────────
|
||||
const PAYMENT_METHODS = [
|
||||
{ label: 'Pix', value: 'pix' },
|
||||
{ label: 'Depósito', value: 'deposito' },
|
||||
{ label: 'Dinheiro', value: 'dinheiro' },
|
||||
{ label: 'Cartão', value: 'cartao' },
|
||||
{ label: 'Convênio', value: 'convenio' },
|
||||
]
|
||||
{ label: 'Pix', value: 'pix' },
|
||||
{ label: 'Depósito', value: 'deposito' },
|
||||
{ label: 'Dinheiro', value: 'dinheiro' },
|
||||
{ label: 'Cartão', value: 'cartao' },
|
||||
{ label: 'Convênio', value: 'convenio' }
|
||||
];
|
||||
|
||||
function paymentLabel (method) {
|
||||
return PAYMENT_METHODS.find(o => o.value === method)?.label ?? method ?? '—'
|
||||
function paymentLabel(method) {
|
||||
return PAYMENT_METHODS.find((o) => o.value === method)?.label ?? method ?? '—';
|
||||
}
|
||||
|
||||
// ── formatação ─────────────────────────────────────────────────────────────────
|
||||
const _brl = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' })
|
||||
function fmtBRL (v) { return _brl.format(v ?? 0) }
|
||||
function fmtDate (iso) {
|
||||
if (!iso) return '—'
|
||||
const d = iso.includes('T') ? new Date(iso) : new Date(iso + 'T00:00:00')
|
||||
return new Intl.DateTimeFormat('pt-BR').format(d)
|
||||
const _brl = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' });
|
||||
function fmtBRL(v) {
|
||||
return _brl.format(v ?? 0);
|
||||
}
|
||||
function fmtDate(iso) {
|
||||
if (!iso) return '—';
|
||||
const d = iso.includes('T') ? new Date(iso) : new Date(iso + 'T00:00:00');
|
||||
return new Intl.DateTimeFormat('pt-BR').format(d);
|
||||
}
|
||||
|
||||
// ── config visual de status ────────────────────────────────────────────────────
|
||||
const STATUS_CFG = {
|
||||
pending: { label: 'Pendente', severity: 'warn' },
|
||||
paid: { label: 'Pago', severity: 'success' },
|
||||
overdue: { label: 'Vencido', severity: 'danger' },
|
||||
cancelled: { label: 'Cancelado', severity: 'secondary' },
|
||||
}
|
||||
pending: { label: 'Pendente', severity: 'warn' },
|
||||
paid: { label: 'Pago', severity: 'success' },
|
||||
overdue: { label: 'Vencido', severity: 'danger' },
|
||||
cancelled: { label: 'Cancelado', severity: 'secondary' }
|
||||
};
|
||||
|
||||
// ── computed: cenário a renderizar ────────────────────────────────────────────
|
||||
const scenario = computed(() => {
|
||||
if (props.evento.tipo !== 'sessao') return 'noop' // bloqueio
|
||||
if (props.evento.billing_contract_id) return 'contrato' // pacote
|
||||
if (fetching.value) return 'carregando'
|
||||
if (record.value) return 'com-cobranca'
|
||||
return 'sem-cobranca'
|
||||
})
|
||||
if (props.evento.tipo !== 'sessao') return 'noop'; // bloqueio
|
||||
if (props.evento.billing_contract_id) return 'contrato'; // pacote
|
||||
if (fetching.value) return 'carregando';
|
||||
if (record.value) return 'com-cobranca';
|
||||
return 'sem-cobranca';
|
||||
});
|
||||
|
||||
const canAct = computed(() =>
|
||||
record.value && (record.value.status === 'pending' || record.value.status === 'overdue')
|
||||
)
|
||||
const canAct = computed(() => record.value && (record.value.status === 'pending' || record.value.status === 'overdue'));
|
||||
|
||||
// ── buscar financial_record pelo evento ───────────────────────────────────────
|
||||
async function fetchRecord () {
|
||||
if (!props.evento.id) return
|
||||
async function fetchRecord() {
|
||||
if (!props.evento.id) return;
|
||||
|
||||
fetching.value = true
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, amount, discount_amount, final_amount, status, due_date, paid_at, payment_method')
|
||||
.eq('agenda_evento_id', props.evento.id)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle()
|
||||
fetching.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('financial_records')
|
||||
.select('id, amount, discount_amount, final_amount, status, due_date, paid_at, payment_method')
|
||||
.eq('agenda_evento_id', props.evento.id)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) throw error
|
||||
record.value = data ?? null
|
||||
} catch (e) {
|
||||
console.warn('[AgendaEventoFinanceiroPanel] fetchRecord:', e?.message)
|
||||
record.value = null
|
||||
} finally {
|
||||
fetching.value = false
|
||||
}
|
||||
if (error) throw error;
|
||||
record.value = data ?? null;
|
||||
} catch (e) {
|
||||
console.warn('[AgendaEventoFinanceiroPanel] fetchRecord:', e?.message);
|
||||
record.value = null;
|
||||
} finally {
|
||||
fetching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.evento?.id, () => {
|
||||
record.value = null
|
||||
fetchRecord()
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
() => props.evento?.id,
|
||||
() => {
|
||||
record.value = null;
|
||||
fetchRecord();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// ── gerar cobrança ─────────────────────────────────────────────────────────────
|
||||
async function onGerarCobranca () {
|
||||
generating.value = true
|
||||
try {
|
||||
const result = await gerarCobrancaManual(props.evento)
|
||||
if (!result.ok) throw new Error(result.error)
|
||||
async function onGerarCobranca() {
|
||||
generating.value = true;
|
||||
try {
|
||||
const result = await gerarCobrancaManual(props.evento);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
|
||||
await fetchRecord()
|
||||
emit('cobranca-atualizada')
|
||||
toast.add({ severity: 'success', summary: 'Cobrança gerada', detail: `${fmtBRL(props.evento.price ?? 0)} agendado para recebimento.`, life: 3000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível gerar a cobrança.', life: 4000 })
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
await fetchRecord();
|
||||
emit('cobranca-atualizada');
|
||||
toast.add({ severity: 'success', summary: 'Cobrança gerada', detail: `${fmtBRL(props.evento.price ?? 0)} agendado para recebimento.`, life: 3000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível gerar a cobrança.', life: 4000 });
|
||||
} finally {
|
||||
generating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── dialog: registrar pagamento ───────────────────────────────────────────────
|
||||
const payDlgVisible = ref(false)
|
||||
const payDlgMethod = ref(null)
|
||||
const payDlgLoading = ref(false)
|
||||
const payDlgVisible = ref(false);
|
||||
const payDlgMethod = ref(null);
|
||||
const payDlgLoading = ref(false);
|
||||
|
||||
function openPayDialog () {
|
||||
payDlgMethod.value = null
|
||||
payDlgVisible.value = true
|
||||
function openPayDialog() {
|
||||
payDlgMethod.value = null;
|
||||
payDlgVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmPayment () {
|
||||
if (!payDlgMethod.value || !record.value) return
|
||||
payDlgLoading.value = true
|
||||
try {
|
||||
const { data, error } = await supabase.rpc('mark_as_paid', {
|
||||
p_financial_record_id: record.value.id,
|
||||
p_payment_method: payDlgMethod.value,
|
||||
})
|
||||
if (error) throw error
|
||||
async function confirmPayment() {
|
||||
if (!payDlgMethod.value || !record.value) return;
|
||||
payDlgLoading.value = true;
|
||||
try {
|
||||
const { data, error } = await supabase.rpc('mark_as_paid', {
|
||||
p_financial_record_id: record.value.id,
|
||||
p_payment_method: payDlgMethod.value
|
||||
});
|
||||
if (error) throw error;
|
||||
|
||||
payDlgVisible.value = false
|
||||
await fetchRecord()
|
||||
emit('cobranca-atualizada')
|
||||
toast.add({ severity: 'success', summary: 'Pago!', detail: `Recebimento via ${paymentLabel(payDlgMethod.value)} registrado.`, life: 3000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível registrar pagamento.', life: 4000 })
|
||||
} finally {
|
||||
payDlgLoading.value = false
|
||||
}
|
||||
payDlgVisible.value = false;
|
||||
await fetchRecord();
|
||||
emit('cobranca-atualizada');
|
||||
toast.add({ severity: 'success', summary: 'Pago!', detail: `Recebimento via ${paymentLabel(payDlgMethod.value)} registrado.`, life: 3000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível registrar pagamento.', life: 4000 });
|
||||
} finally {
|
||||
payDlgLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── cancelar cobrança ─────────────────────────────────────────────────────────
|
||||
function requestCancel () {
|
||||
confirm.require({
|
||||
message: `Cancelar a cobrança de ${fmtBRL(record.value?.final_amount)} desta sessão?`,
|
||||
header: 'Cancelar cobrança',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectLabel: 'Não',
|
||||
acceptLabel: 'Sim, cancelar',
|
||||
acceptSeverity: 'danger',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('financial_records')
|
||||
.update({ status: 'cancelled', updated_at: new Date().toISOString() })
|
||||
.eq('id', record.value.id)
|
||||
function requestCancel() {
|
||||
confirm.require({
|
||||
message: `Cancelar a cobrança de ${fmtBRL(record.value?.final_amount)} desta sessão?`,
|
||||
header: 'Cancelar cobrança',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectLabel: 'Não',
|
||||
acceptLabel: 'Sim, cancelar',
|
||||
acceptSeverity: 'danger',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { error } = await supabase.from('financial_records').update({ status: 'cancelled', updated_at: new Date().toISOString() }).eq('id', record.value.id);
|
||||
|
||||
if (error) throw error
|
||||
if (error) throw error;
|
||||
|
||||
await fetchRecord()
|
||||
emit('cobranca-atualizada')
|
||||
toast.add({ severity: 'info', summary: 'Cancelado', detail: 'Cobrança cancelada.', life: 3000 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao cancelar.', life: 4000 })
|
||||
}
|
||||
},
|
||||
})
|
||||
await fetchRecord();
|
||||
emit('cobranca-atualizada');
|
||||
toast.add({ severity: 'info', summary: 'Cancelado', detail: 'Cobrança cancelada.', life: 3000 });
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao cancelar.', life: 4000 });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Painel principal — noop (bloqueios) não renderiza nada -->
|
||||
<div v-if="scenario !== 'noop'" class="fin-panel">
|
||||
<div>
|
||||
<!-- Painel principal — noop (bloqueios) não renderiza nada -->
|
||||
<div v-if="scenario !== 'noop'" class="fin-panel">
|
||||
<!-- Cabeçalho do painel -->
|
||||
<div class="fin-panel__header">
|
||||
<i class="pi pi-wallet" />
|
||||
<span>Cobrança</span>
|
||||
<Button v-if="props.evento.billed && !fetching" icon="pi pi-refresh" text size="small" severity="secondary" class="ml-auto h-6 w-6" v-tooltip.top="'Recarregar'" @click="fetchRecord" />
|
||||
</div>
|
||||
|
||||
<!-- Cabeçalho do painel -->
|
||||
<div class="fin-panel__header">
|
||||
<i class="pi pi-wallet" />
|
||||
<span>Cobrança</span>
|
||||
<Button
|
||||
v-if="props.evento.billed && !fetching"
|
||||
icon="pi pi-refresh"
|
||||
text
|
||||
size="small"
|
||||
severity="secondary"
|
||||
class="ml-auto h-6 w-6"
|
||||
v-tooltip.top="'Recarregar'"
|
||||
@click="fetchRecord"
|
||||
/>
|
||||
</div>
|
||||
<!-- ── Sessão de pacote / contrato ──────────────────────────────────── -->
|
||||
<div v-if="scenario === 'contrato'" class="fin-panel__body">
|
||||
<span class="fin-badge fin-badge--contract">
|
||||
<i class="pi pi-box text-xs" />
|
||||
Sessão de pacote
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- ── Sessão de pacote / contrato ──────────────────────────────────── -->
|
||||
<div v-if="scenario === 'contrato'" class="fin-panel__body">
|
||||
<span class="fin-badge fin-badge--contract">
|
||||
<i class="pi pi-box text-xs" />
|
||||
Sessão de pacote
|
||||
</span>
|
||||
</div>
|
||||
<!-- ── Sem cobrança gerada ──────────────────────────────────────────── -->
|
||||
<div v-else-if="scenario === 'sem-cobranca'" class="fin-panel__body fin-panel__body--empty">
|
||||
<div class="flex items-center gap-2 text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-minus-circle text-sm opacity-50" />
|
||||
<span class="text-sm">Sem cobrança gerada</span>
|
||||
</div>
|
||||
<Button label="Gerar cobrança" icon="pi pi-plus" size="small" class="rounded-full mt-2" :loading="generating || finLoading" @click="onGerarCobranca" />
|
||||
<div v-if="props.evento.price" class="text-xs text-[var(--text-color-secondary)] mt-1">Valor da sessão: {{ fmtBRL(props.evento.price) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Sem cobrança gerada ──────────────────────────────────────────── -->
|
||||
<div v-else-if="scenario === 'sem-cobranca'" class="fin-panel__body fin-panel__body--empty">
|
||||
<div class="flex items-center gap-2 text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-minus-circle text-sm opacity-50" />
|
||||
<span class="text-sm">Sem cobrança gerada</span>
|
||||
</div>
|
||||
<Button
|
||||
label="Gerar cobrança"
|
||||
icon="pi pi-plus"
|
||||
size="small"
|
||||
class="rounded-full mt-2"
|
||||
:loading="generating || finLoading"
|
||||
@click="onGerarCobranca"
|
||||
/>
|
||||
<div v-if="props.evento.price" class="text-xs text-[var(--text-color-secondary)] mt-1">
|
||||
Valor da sessão: {{ fmtBRL(props.evento.price) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- ── Carregando o financial_record ────────────────────────────────── -->
|
||||
<div v-else-if="scenario === 'carregando'" class="fin-panel__body">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Skeleton height="1rem" class="w-24" />
|
||||
<Skeleton height="1.5rem" class="w-32" />
|
||||
<Skeleton height="1rem" class="w-20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Carregando o financial_record ────────────────────────────────── -->
|
||||
<div v-else-if="scenario === 'carregando'" class="fin-panel__body">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Skeleton height="1rem" class="w-24" />
|
||||
<Skeleton height="1.5rem" class="w-32" />
|
||||
<Skeleton height="1rem" class="w-20" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- ── Com cobrança ─────────────────────────────────────────────────── -->
|
||||
<div v-else-if="scenario === 'com-cobranca'" class="fin-panel__body">
|
||||
<!-- Linha de status + valor -->
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Tag :value="STATUS_CFG[record.status]?.label ?? record.status" :severity="STATUS_CFG[record.status]?.severity" class="text-xs" />
|
||||
<span class="font-bold text-sm text-[var(--text-color)]">{{ fmtBRL(record.final_amount) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- ── Com cobrança ─────────────────────────────────────────────────── -->
|
||||
<div v-else-if="scenario === 'com-cobranca'" class="fin-panel__body">
|
||||
<!-- Vencimento / data de pagamento -->
|
||||
<div class="flex items-center gap-1.5 text-xs text-[var(--text-color-secondary)] mt-1.5">
|
||||
<template v-if="record.status === 'paid'">
|
||||
<i class="pi pi-check-circle text-emerald-500" />
|
||||
<span class="text-emerald-600">{{ paymentLabel(record.payment_method) }} · {{ fmtDate(record.paid_at) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="pi pi-calendar" />
|
||||
<span :class="record.status === 'overdue' ? 'text-red-500 font-semibold' : ''"> Vence {{ fmtDate(record.due_date) }} </span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Linha de status + valor -->
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<Tag
|
||||
:value="STATUS_CFG[record.status]?.label ?? record.status"
|
||||
:severity="STATUS_CFG[record.status]?.severity"
|
||||
class="text-xs"
|
||||
/>
|
||||
<span class="font-bold text-sm text-[var(--text-color)]">{{ fmtBRL(record.final_amount) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Vencimento / data de pagamento -->
|
||||
<div class="flex items-center gap-1.5 text-xs text-[var(--text-color-secondary)] mt-1.5">
|
||||
<template v-if="record.status === 'paid'">
|
||||
<i class="pi pi-check-circle text-emerald-500" />
|
||||
<span class="text-emerald-600">{{ paymentLabel(record.payment_method) }} · {{ fmtDate(record.paid_at) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="pi pi-calendar" />
|
||||
<span :class="record.status === 'overdue' ? 'text-red-500 font-semibold' : ''">
|
||||
Vence {{ fmtDate(record.due_date) }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Ações: pendente / vencido -->
|
||||
<div v-if="canAct" class="flex gap-1.5 mt-3">
|
||||
<Button
|
||||
label="Receber"
|
||||
icon="pi pi-check"
|
||||
size="small"
|
||||
class="rounded-full flex-1"
|
||||
@click="openPayDialog"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
size="small"
|
||||
severity="danger"
|
||||
outlined
|
||||
class="rounded-full h-7 w-7"
|
||||
v-tooltip.top="'Cancelar cobrança'"
|
||||
@click="requestCancel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Dialog: Registrar Pagamento ──────────────────────────────────────── -->
|
||||
<Dialog
|
||||
v-model:visible="payDlgVisible"
|
||||
modal
|
||||
:draggable="false"
|
||||
pt:mask:class="backdrop-blur-xs"
|
||||
header="Registrar pagamento"
|
||||
class="w-[92vw] max-w-sm"
|
||||
>
|
||||
<div class="flex flex-col gap-4 pt-1">
|
||||
|
||||
<!-- Valor -->
|
||||
<div class="flex items-center justify-between px-4 py-2.5 rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)]">
|
||||
<span class="text-sm text-[var(--text-color-secondary)]">Valor a receber</span>
|
||||
<span class="font-bold text-[var(--text-color)]">{{ fmtBRL(record?.final_amount) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Método (grid de botões) -->
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-[var(--text-color)] mb-2">Método de pagamento</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="opt in PAYMENT_METHODS"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="flex flex-col items-center gap-1 px-2 py-2 rounded-md border text-xs font-medium transition-all duration-150 cursor-pointer select-none"
|
||||
:class="payDlgMethod === opt.value
|
||||
? 'border-[var(--primary-color,#6366f1)] bg-[var(--primary-color,#6366f1)]/10 text-[var(--primary-color,#6366f1)]'
|
||||
: 'border-[var(--surface-border)] bg-[var(--surface-ground)] text-[var(--text-color-secondary)] hover:border-[var(--primary-color,#6366f1)]/40'"
|
||||
@click="payDlgMethod = opt.value"
|
||||
>
|
||||
<i
|
||||
class="text-base"
|
||||
:class="{
|
||||
'pi pi-bolt': opt.value === 'pix',
|
||||
'pi pi-building': opt.value === 'deposito',
|
||||
'pi pi-money-bill': opt.value === 'dinheiro',
|
||||
'pi pi-credit-card': opt.value === 'cartao',
|
||||
'pi pi-id-card': opt.value === 'convenio',
|
||||
}"
|
||||
/>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
<!-- Ações: pendente / vencido -->
|
||||
<div v-if="canAct" class="flex gap-1.5 mt-3">
|
||||
<Button label="Receber" icon="pi pi-check" size="small" class="rounded-full flex-1" @click="openPayDialog" />
|
||||
<Button icon="pi pi-times" size="small" severity="danger" outlined class="rounded-full h-7 w-7" v-tooltip.top="'Cancelar cobrança'" @click="requestCancel" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="Cancelar" severity="secondary" outlined class="rounded-full" :disabled="payDlgLoading" @click="payDlgVisible = false" />
|
||||
<Button label="Confirmar" icon="pi pi-check" class="rounded-full" :loading="payDlgLoading" :disabled="!payDlgMethod" @click="confirmPayment" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
<!-- ── Dialog: Registrar Pagamento ──────────────────────────────────────── -->
|
||||
<Dialog v-model:visible="payDlgVisible" modal :draggable="false" pt:mask:class="backdrop-blur-xs" header="Registrar pagamento" class="w-[92vw] max-w-sm">
|
||||
<div class="flex flex-col gap-4 pt-1">
|
||||
<!-- Valor -->
|
||||
<div class="flex items-center justify-between px-4 py-2.5 rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)]">
|
||||
<span class="text-sm text-[var(--text-color-secondary)]">Valor a receber</span>
|
||||
<span class="font-bold text-[var(--text-color)]">{{ fmtBRL(record?.final_amount) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Método (grid de botões) -->
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-[var(--text-color)] mb-2">Método de pagamento</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="opt in PAYMENT_METHODS"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="flex flex-col items-center gap-1 px-2 py-2 rounded-md border text-xs font-medium transition-all duration-150 cursor-pointer select-none"
|
||||
:class="
|
||||
payDlgMethod === opt.value
|
||||
? 'border-[var(--primary-color,#6366f1)] bg-[var(--primary-color,#6366f1)]/10 text-[var(--primary-color,#6366f1)]'
|
||||
: 'border-[var(--surface-border)] bg-[var(--surface-ground)] text-[var(--text-color-secondary)] hover:border-[var(--primary-color,#6366f1)]/40'
|
||||
"
|
||||
@click="payDlgMethod = opt.value"
|
||||
>
|
||||
<i
|
||||
class="text-base"
|
||||
:class="{
|
||||
'pi pi-bolt': opt.value === 'pix',
|
||||
'pi pi-building': opt.value === 'deposito',
|
||||
'pi pi-money-bill': opt.value === 'dinheiro',
|
||||
'pi pi-credit-card': opt.value === 'cartao',
|
||||
'pi pi-id-card': opt.value === 'convenio'
|
||||
}"
|
||||
/>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="Cancelar" severity="secondary" outlined class="rounded-full" :disabled="payDlgLoading" @click="payDlgVisible = false" />
|
||||
<Button label="Confirmar" icon="pi pi-check" class="rounded-full" :loading="payDlgLoading" :disabled="!payDlgMethod" @click="confirmPayment" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fin-panel {
|
||||
border: 1px solid var(--surface-border, #e2e8f0);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
background: var(--surface-card, #fff);
|
||||
border: 1px solid var(--surface-border, #e2e8f0);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
background: var(--surface-card, #fff);
|
||||
}
|
||||
|
||||
.fin-panel__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--surface-ground, #f8fafc);
|
||||
border-bottom: 1px solid var(--surface-border, #e2e8f0);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--surface-ground, #f8fafc);
|
||||
border-bottom: 1px solid var(--surface-border, #e2e8f0);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.fin-panel__body {
|
||||
padding: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.fin-panel__body--empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fin-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 9999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.fin-badge--contract {
|
||||
background: color-mix(in srgb, var(--p-indigo-500, #6366f1) 10%, transparent);
|
||||
color: var(--p-indigo-600, #4f46e5);
|
||||
border: 1px solid color-mix(in srgb, var(--p-indigo-500, #6366f1) 20%, transparent);
|
||||
background: color-mix(in srgb, var(--p-indigo-500, #6366f1) 10%, transparent);
|
||||
color: var(--p-indigo-600, #4f46e5);
|
||||
border: 1px solid color-mix(in srgb, var(--p-indigo-500, #6366f1) 20%, transparent);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user