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:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions
@@ -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>