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
@@ -16,472 +16,406 @@
-->
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { supabase } from '@/lib/supabase/client'
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { supabase } from '@/lib/supabase/client';
// ─── helpers ─────────────────────────────────────────────────────────────────
const router = useRouter()
const router = useRouter();
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);
}
async function getUid () {
const { data } = await supabase.auth.getUser()
return data?.user?.id ?? null
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);
}
async function getUid() {
const { data } = await supabase.auth.getUser();
return data?.user?.id ?? null;
}
// ─── meses para o gráfico ────────────────────────────────────────────────────
function getLast6Months () {
const months = []
const now = new Date()
for (let i = 5; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
months.push({ year: d.getFullYear(), month: d.getMonth() + 1, label: d.toLocaleDateString('pt-BR', { month: 'short', year: '2-digit' }) })
}
return months
function getLast6Months() {
const months = [];
const now = new Date();
for (let i = 5; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
months.push({ year: d.getFullYear(), month: d.getMonth() + 1, label: d.toLocaleDateString('pt-BR', { month: 'short', year: '2-digit' }) });
}
return months;
}
// ─── estado: cards de resumo ─────────────────────────────────────────────────
const summaryLoading = ref(true)
const totalRecebido = ref(0)
const totalPendente = ref(0)
const totalVencido = ref(0)
const totalDespesas = ref(0)
const summaryLoading = ref(true);
const totalRecebido = ref(0);
const totalPendente = ref(0);
const totalVencido = ref(0);
const totalDespesas = ref(0);
async function loadSummary (uid) {
summaryLoading.value = true
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
async function loadSummary(uid) {
summaryLoading.value = true;
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
try {
// Receitas e despesas pagas no mês via RPC
const { data: rpc } = await supabase.rpc('get_financial_summary', {
p_owner_id: uid, p_year: year, p_month: month,
})
const s = Array.isArray(rpc) ? rpc[0] : rpc
totalRecebido.value = Number(s?.total_receitas ?? 0)
totalDespesas.value = Number(s?.total_despesas ?? 0)
try {
// Receitas e despesas pagas no mês via RPC
const { data: rpc } = await supabase.rpc('get_financial_summary', {
p_owner_id: uid,
p_year: year,
p_month: month
});
const s = Array.isArray(rpc) ? rpc[0] : rpc;
totalRecebido.value = Number(s?.total_receitas ?? 0);
totalDespesas.value = Number(s?.total_despesas ?? 0);
// Pending e overdue separados (sem filtro de mês)
const { data: pendRows } = await supabase
.from('financial_records')
.select('status, final_amount')
.eq('owner_id', uid)
.is('deleted_at', null)
.in('status', ['pending', 'overdue'])
// Pending e overdue separados (sem filtro de mês)
const { data: pendRows } = await supabase.from('financial_records').select('status, final_amount').eq('owner_id', uid).is('deleted_at', null).in('status', ['pending', 'overdue']);
let pen = 0, ove = 0
for (const r of pendRows ?? []) {
if (r.status === 'pending') pen += Number(r.final_amount ?? 0)
else ove += Number(r.final_amount ?? 0)
let pen = 0,
ove = 0;
for (const r of pendRows ?? []) {
if (r.status === 'pending') pen += Number(r.final_amount ?? 0);
else ove += Number(r.final_amount ?? 0);
}
totalPendente.value = pen;
totalVencido.value = ove;
} finally {
summaryLoading.value = false;
}
totalPendente.value = pen
totalVencido.value = ove
} finally {
summaryLoading.value = false
}
}
// ─── estado: gráfico 6 meses ─────────────────────────────────────────────────
const chartLoading = ref(true)
const chartData = ref(null)
const chartLoading = ref(true);
const chartData = ref(null);
const chartOptions = ref({
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' },
tooltip: {
callbacks: {
label: ctx => ` ${_brl.format(ctx.parsed.y)}`,
},
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top' },
tooltip: {
callbacks: {
label: (ctx) => ` ${_brl.format(ctx.parsed.y)}`
}
}
},
},
scales: {
y: {
ticks: { callback: v => _brl.format(v) },
beginAtZero: true,
},
},
})
async function loadChart (uid) {
chartLoading.value = true
const months = getLast6Months()
try {
const results = await Promise.all(
months.map(m =>
supabase.rpc('get_financial_summary', { p_owner_id: uid, p_year: m.year, p_month: m.month })
)
)
const receitas = results.map(r => Number((Array.isArray(r.data) ? r.data[0] : r.data)?.total_receitas ?? 0))
const despesas = results.map(r => Number((Array.isArray(r.data) ? r.data[0] : r.data)?.total_despesas ?? 0))
chartData.value = {
labels: months.map(m => m.label),
datasets: [
{ label: 'Receita', data: receitas, backgroundColor: '#22c55e', borderRadius: 4 },
{ label: 'Despesa', data: despesas, backgroundColor: '#ef4444', borderRadius: 4 },
],
scales: {
y: {
ticks: { callback: (v) => _brl.format(v) },
beginAtZero: true
}
}
});
async function loadChart(uid) {
chartLoading.value = true;
const months = getLast6Months();
try {
const results = await Promise.all(months.map((m) => supabase.rpc('get_financial_summary', { p_owner_id: uid, p_year: m.year, p_month: m.month })));
const receitas = results.map((r) => Number((Array.isArray(r.data) ? r.data[0] : r.data)?.total_receitas ?? 0));
const despesas = results.map((r) => Number((Array.isArray(r.data) ? r.data[0] : r.data)?.total_despesas ?? 0));
chartData.value = {
labels: months.map((m) => m.label),
datasets: [
{ label: 'Receita', data: receitas, backgroundColor: '#22c55e', borderRadius: 4 },
{ label: 'Despesa', data: despesas, backgroundColor: '#ef4444', borderRadius: 4 }
]
};
} finally {
chartLoading.value = false;
}
} finally {
chartLoading.value = false
}
}
// ─── estado: fluxo de caixa ──────────────────────────────────────────────────
const cashflowLoading = ref(true)
const cashflowRows = ref([])
const cashflowError = ref(false)
const cashflowLoading = ref(true);
const cashflowRows = ref([]);
const cashflowError = ref(false);
async function loadCashflow () {
cashflowLoading.value = true
cashflowError.value = false
try {
const { data, error } = await supabase
.from('v_cashflow_projection')
.select('mes_label, receitas_projetadas, despesas_projetadas, saldo_projetado, count_registros')
.order('mes', { ascending: true })
if (error) throw error
cashflowRows.value = data ?? []
} catch {
cashflowError.value = true
} finally {
cashflowLoading.value = false
}
async function loadCashflow() {
cashflowLoading.value = true;
cashflowError.value = false;
try {
const { data, error } = await supabase.from('v_cashflow_projection').select('mes_label, receitas_projetadas, despesas_projetadas, saldo_projetado, count_registros').order('mes', { ascending: true });
if (error) throw error;
cashflowRows.value = data ?? [];
} catch {
cashflowError.value = true;
} finally {
cashflowLoading.value = false;
}
}
// ─── estado: últimos lançamentos ─────────────────────────────────────────────
const recentLoading = ref(true)
const recentRecords = ref([])
const recentLoading = ref(true);
const recentRecords = ref([]);
const STATUS_CFG = {
pending: { label: 'Pendente', severity: 'warn' },
paid: { label: 'Pago', severity: 'success' },
overdue: { label: 'Vencido', severity: 'danger' },
partial: { label: 'Parcial', severity: 'info' },
cancelled: { label: 'Cancelado', severity: 'secondary' },
refunded: { label: 'Estornado', severity: 'contrast' },
}
pending: { label: 'Pendente', severity: 'warn' },
paid: { label: 'Pago', severity: 'success' },
overdue: { label: 'Vencido', severity: 'danger' },
partial: { label: 'Parcial', severity: 'info' },
cancelled: { label: 'Cancelado', severity: 'secondary' },
refunded: { label: 'Estornado', severity: 'contrast' }
};
async function loadRecent (uid) {
recentLoading.value = true
try {
const { data } = await supabase.rpc('list_financial_records', {
p_owner_id: uid,
p_limit: 5,
p_offset: 0,
})
recentRecords.value = data ?? []
} finally {
recentLoading.value = false
}
async function loadRecent(uid) {
recentLoading.value = true;
try {
const { data } = await supabase.rpc('list_financial_records', {
p_owner_id: uid,
p_limit: 5,
p_offset: 0
});
recentRecords.value = data ?? [];
} finally {
recentLoading.value = false;
}
}
// ─── navegação ───────────────────────────────────────────────────────────────
function goToLancamentos () {
// Tenta navegar pelo nome da rota; fallback para push relativo
const route = router.currentRoute.value
const base = route.matched[0]?.path ?? ''
router.push(base + '/financeiro/lancamentos')
function goToLancamentos() {
// Tenta navegar pelo nome da rota; fallback para push relativo
const route = router.currentRoute.value;
const base = route.matched[0]?.path ?? '';
router.push(base + '/financeiro/lancamentos');
}
// ─── mount ───────────────────────────────────────────────────────────────────
onMounted(async () => {
const uid = await getUid()
if (!uid) return
await Promise.all([
loadSummary(uid),
loadChart(uid),
loadCashflow(),
loadRecent(uid),
])
})
const uid = await getUid();
if (!uid) return;
await Promise.all([loadSummary(uid), loadChart(uid), loadCashflow(), loadRecent(uid)]);
});
</script>
<template>
<div class="min-h-[calc(100vh-4.5rem)] bg-[var(--surface-ground,#f5f7fa)]">
<div class="px-3 md:px-4 pt-3 pb-4 flex flex-col gap-3.5">
<!--
<div class="min-h-[calc(100vh-4.5rem)] bg-[var(--surface-ground,#f5f7fa)]">
<div class="px-3 md:px-4 pt-3 pb-4 flex flex-col gap-3.5">
<!--
Hero
-->
<section class="relative overflow-hidden rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] p-2.5">
<section class="relative overflow-hidden rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] p-2.5">
<!-- Blobs decorativos -->
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute w-72 h-72 -top-16 -right-12 rounded-full blur-[70px] bg-indigo-500/10" />
<div class="absolute w-80 h-80 top-2 -left-20 rounded-full blur-[70px] bg-emerald-400/[0.08]" />
</div>
<!-- Blobs decorativos -->
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute w-72 h-72 -top-16 -right-12 rounded-full blur-[70px] bg-indigo-500/10" />
<div class="absolute w-80 h-80 top-2 -left-20 rounded-full blur-[70px] bg-emerald-400/[0.08]" />
</div>
<!-- Linha 1: icon + título + botão -->
<div class="relative z-[1] flex items-center gap-3">
<div class="flex items-center gap-2.5 flex-1 min-w-0">
<div class="cfg-subheader__icon grid place-items-center w-10 h-10 rounded-md flex-shrink-0" style="background: color-mix(in srgb, #10b981 15%, transparent); color: #059669">
<i class="pi pi-wallet text-lg" />
</div>
<div class="min-w-0">
<div class="text-[1.05rem] font-bold tracking-tight text-[var(--text-color)]">Financeiro</div>
<div class="text-[0.78rem] text-[var(--text-color-secondary)] mt-0.5">Resumo e visão geral do período</div>
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<Button label="Ver lançamentos" icon="pi pi-list" severity="secondary" outlined class="rounded-full hidden sm:flex" @click="goToLancamentos" />
</div>
</div>
<!-- Linha 1: icon + título + botão -->
<div class="relative z-[1] flex items-center gap-3">
<div class="flex items-center gap-2.5 flex-1 min-w-0">
<div class="cfg-subheader__icon grid place-items-center w-10 h-10 rounded-md flex-shrink-0" style="background:color-mix(in srgb,#10b981 15%,transparent);color:#059669">
<i class="pi pi-wallet text-lg" />
</div>
<div class="min-w-0">
<div class="text-[1.05rem] font-bold tracking-tight text-[var(--text-color)]">Financeiro</div>
<div class="text-[0.78rem] text-[var(--text-color-secondary)] mt-0.5">Resumo e visão geral do período</div>
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<Button
label="Ver lançamentos"
icon="pi pi-list"
severity="secondary"
outlined
class="rounded-full hidden sm:flex"
@click="goToLancamentos"
/>
</div>
</div>
<!-- Linha 2: quick stats -->
<div class="relative z-[1] mt-2.5">
<template v-if="summaryLoading">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2.5">
<div v-for="n in 4" :key="n" class="flex flex-col gap-1.5 px-4 py-2.5 rounded-md border border-[var(--surface-border)]">
<Skeleton width="3rem" height="20px" />
<Skeleton width="4.5rem" height="10px" />
</div>
</div>
</template>
<!-- Linha 2: quick stats -->
<div class="relative z-[1] mt-2.5">
<template v-if="summaryLoading">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2.5">
<div v-for="n in 4" :key="n" class="flex flex-col gap-1.5 px-4 py-2.5 rounded-md border border-[var(--surface-border)]">
<Skeleton width="3rem" height="20px" />
<Skeleton width="4.5rem" height="10px" />
</div>
</div>
</template>
<template v-else>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2.5">
<!-- Recebido -->
<div class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border border-emerald-500/25 bg-emerald-500/5">
<div class="text-[1.35rem] font-bold leading-none text-emerald-600">{{ fmtBRL(totalRecebido) }}</div>
<div class="flex items-center gap-1.5 text-[0.7rem] text-emerald-700/80 font-semibold">
<i class="pi pi-check-circle text-xs" />
Recebido (mês)
</div>
</div>
<template v-else>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-2.5">
<!-- Pendente -->
<div class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border border-amber-500/25 bg-amber-500/5">
<div class="text-[1.35rem] font-bold leading-none text-amber-500">{{ fmtBRL(totalPendente) }}</div>
<div class="flex items-center gap-1.5 text-[0.7rem] text-amber-600/80 font-semibold">
<span class="h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse flex-shrink-0" />
Pendente
</div>
</div>
<!-- Recebido -->
<div class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border border-emerald-500/25 bg-emerald-500/5">
<div class="text-[1.35rem] font-bold leading-none text-emerald-600">{{ fmtBRL(totalRecebido) }}</div>
<div class="flex items-center gap-1.5 text-[0.7rem] text-emerald-700/80 font-semibold">
<i class="pi pi-check-circle text-xs" />
Recebido (mês)
</div>
</div>
<!-- Vencido -->
<div class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border" :class="totalVencido > 0 ? 'border-red-500/25 bg-red-500/5' : 'border-[var(--surface-border)] bg-[var(--surface-ground)]'">
<div class="text-[1.35rem] font-bold leading-none" :class="totalVencido > 0 ? 'text-red-500' : 'text-[var(--text-color)]'">{{ fmtBRL(totalVencido) }}</div>
<div class="flex items-center gap-1.5 text-[0.7rem] font-semibold" :class="totalVencido > 0 ? 'text-red-600/80' : 'text-[var(--text-color-secondary)] opacity-75'">
<i class="pi pi-exclamation-circle text-xs" />
Vencido
</div>
</div>
<!-- Pendente -->
<div class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border border-amber-500/25 bg-amber-500/5">
<div class="text-[1.35rem] font-bold leading-none text-amber-500">{{ fmtBRL(totalPendente) }}</div>
<div class="flex items-center gap-1.5 text-[0.7rem] text-amber-600/80 font-semibold">
<span class="h-1.5 w-1.5 rounded-full bg-amber-400 animate-pulse flex-shrink-0" />
Pendente
</div>
</div>
<!-- Despesas -->
<div class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)]">
<div class="text-[1.35rem] font-bold leading-none text-[var(--text-color)]">{{ fmtBRL(totalDespesas) }}</div>
<div class="flex items-center gap-1.5 text-[0.7rem] text-[var(--text-color-secondary)] opacity-75 font-semibold">
<i class="pi pi-arrow-down-left text-xs" />
Despesas (mês)
</div>
</div>
</div>
</template>
</div>
</section>
<!-- Vencido -->
<div
class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border"
:class="totalVencido > 0 ? 'border-red-500/25 bg-red-500/5' : 'border-[var(--surface-border)] bg-[var(--surface-ground)]'"
>
<div class="text-[1.35rem] font-bold leading-none" :class="totalVencido > 0 ? 'text-red-500' : 'text-[var(--text-color)]'">{{ fmtBRL(totalVencido) }}</div>
<div
class="flex items-center gap-1.5 text-[0.7rem] font-semibold"
:class="totalVencido > 0 ? 'text-red-600/80' : 'text-[var(--text-color-secondary)] opacity-75'"
>
<i class="pi pi-exclamation-circle text-xs" />
Vencido
</div>
</div>
<!-- Despesas -->
<div class="flex flex-col gap-0.5 px-4 py-2.5 rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)]">
<div class="text-[1.35rem] font-bold leading-none text-[var(--text-color)]">{{ fmtBRL(totalDespesas) }}</div>
<div class="flex items-center gap-1.5 text-[0.7rem] text-[var(--text-color-secondary)] opacity-75 font-semibold">
<i class="pi pi-arrow-down-left text-xs" />
Despesas (mês)
</div>
</div>
</div>
</template>
</div>
</section>
<!--
<!--
Gráfico Receita × Despesa
-->
<section class="dash-card rounded-md">
<div class="dash-card__head gap-2.5 p-2.5">
<i class="pi pi-chart-bar cfg-subheader__icon w-10 h-10 rounded-md flex-shrink-0" />
<div>
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Receita × Despesa</div>
<div class="dash-card__sub">Comparativo dos últimos 6 meses</div>
</div>
</div>
<div class="px-3 pb-3 pt-2">
<div v-if="chartLoading" class="h-[240px]">
<Skeleton height="100%" />
</div>
<div v-else-if="chartData" class="h-[240px]">
<Chart type="bar" :data="chartData" :options="chartOptions" style="height:100%" />
</div>
<div v-else class="flex items-center justify-center gap-2 py-10 text-[var(--text-color-secondary)]">
<i class="pi pi-info-circle" /> Sem dados para o gráfico.
</div>
</div>
</section>
<section class="dash-card rounded-md">
<div class="dash-card__head gap-2.5 p-2.5">
<i class="pi pi-chart-bar cfg-subheader__icon w-10 h-10 rounded-md flex-shrink-0" />
<div>
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Receita × Despesa</div>
<div class="dash-card__sub">Comparativo dos últimos 6 meses</div>
</div>
</div>
<div class="px-3 pb-3 pt-2">
<div v-if="chartLoading" class="h-[240px]">
<Skeleton height="100%" />
</div>
<div v-else-if="chartData" class="h-[240px]">
<Chart type="bar" :data="chartData" :options="chartOptions" style="height: 100%" />
</div>
<div v-else class="flex items-center justify-center gap-2 py-10 text-[var(--text-color-secondary)]"><i class="pi pi-info-circle" /> Sem dados para o gráfico.</div>
</div>
</section>
<!--
<!--
Projeção de Caixa
-->
<section class="dash-card rounded-md">
<div class="dash-card__head gap-2.5 p-2.5">
<i class="pi pi-calendar cfg-subheader__icon w-10 h-10 rounded-md flex-shrink-0" />
<div>
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Projeção de Caixa</div>
<div class="dash-card__sub">Cobranças em aberto próximos 6 meses</div>
</div>
</div>
<div class="px-3 pb-3 pt-1">
<section class="dash-card rounded-md">
<div class="dash-card__head gap-2.5 p-2.5">
<i class="pi pi-calendar cfg-subheader__icon w-10 h-10 rounded-md flex-shrink-0" />
<div>
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Projeção de Caixa</div>
<div class="dash-card__sub">Cobranças em aberto próximos 6 meses</div>
</div>
</div>
<div class="px-3 pb-3 pt-1">
<div v-if="cashflowLoading" class="flex flex-col gap-2 pt-1">
<Skeleton v-for="n in 6" :key="n" height="2.4rem" border-radius="6px" />
</div>
<div v-if="cashflowLoading" class="flex flex-col gap-2 pt-1">
<Skeleton v-for="n in 6" :key="n" height="2.4rem" border-radius="6px" />
</div>
<div v-else-if="cashflowError" class="flex items-center justify-center gap-2 py-8 text-[var(--text-color-secondary)]"><i class="pi pi-info-circle" /> Projeção indisponível.</div>
<div v-else-if="cashflowError" class="flex items-center justify-center gap-2 py-8 text-[var(--text-color-secondary)]">
<i class="pi pi-info-circle" /> Projeção indisponível.
</div>
<div v-else-if="!cashflowRows.length" class="flex items-center justify-center gap-2 py-8 text-[var(--text-color-secondary)]"><i class="pi pi-check-circle" /> Sem cobranças futuras em aberto.</div>
<div v-else-if="!cashflowRows.length" class="flex items-center justify-center gap-2 py-8 text-[var(--text-color-secondary)]">
<i class="pi pi-check-circle" /> Sem cobranças futuras em aberto.
</div>
<div v-else class="flex flex-col gap-1.5 pt-1">
<div v-for="row in cashflowRows" :key="row.mes_label" class="flex items-center gap-3 px-3 py-2.5 rounded-md bg-[var(--surface-ground,#f8fafc)] hover:bg-[var(--surface-hover,#f1f5f9)] transition-colors duration-100">
<span class="font-bold text-[0.8rem] uppercase tracking-wide text-[var(--text-color)] min-w-[3.5rem] flex-shrink-0">{{ row.mes_label }}</span>
<div class="flex items-center gap-2 flex-1 flex-wrap text-[0.8rem]">
<span class="flex items-center gap-1 text-emerald-600 font-semibold">
<i class="pi pi-arrow-up-right text-xs" />
{{ fmtBRL(row.receitas_projetadas) }}
</span>
<span class="text-[var(--text-color-secondary)] opacity-30">·</span>
<span class="flex items-center gap-1 text-red-500 font-semibold">
<i class="pi pi-arrow-down-left text-xs" />
{{ fmtBRL(row.despesas_projetadas) }}
</span>
<span class="text-[var(--text-color-secondary)] opacity-30">·</span>
<span class="font-bold" :class="Number(row.saldo_projetado) >= 0 ? 'text-emerald-600' : 'text-red-500'"> saldo {{ fmtBRL(row.saldo_projetado) }} </span>
</div>
<Tag :value="row.count_registros + ' cobranças'" severity="secondary" class="ml-auto text-xs flex-shrink-0" />
</div>
</div>
</div>
</section>
<div v-else class="flex flex-col gap-1.5 pt-1">
<div
v-for="row in cashflowRows"
:key="row.mes_label"
class="flex items-center gap-3 px-3 py-2.5 rounded-md bg-[var(--surface-ground,#f8fafc)] hover:bg-[var(--surface-hover,#f1f5f9)] transition-colors duration-100"
>
<span class="font-bold text-[0.8rem] uppercase tracking-wide text-[var(--text-color)] min-w-[3.5rem] flex-shrink-0">{{ row.mes_label }}</span>
<div class="flex items-center gap-2 flex-1 flex-wrap text-[0.8rem]">
<span class="flex items-center gap-1 text-emerald-600 font-semibold">
<i class="pi pi-arrow-up-right text-xs" />
{{ fmtBRL(row.receitas_projetadas) }}
</span>
<span class="text-[var(--text-color-secondary)] opacity-30">·</span>
<span class="flex items-center gap-1 text-red-500 font-semibold">
<i class="pi pi-arrow-down-left text-xs" />
{{ fmtBRL(row.despesas_projetadas) }}
</span>
<span class="text-[var(--text-color-secondary)] opacity-30">·</span>
<span class="font-bold" :class="Number(row.saldo_projetado) >= 0 ? 'text-emerald-600' : 'text-red-500'">
saldo {{ fmtBRL(row.saldo_projetado) }}
</span>
</div>
<Tag :value="row.count_registros + ' cobranças'" severity="secondary" class="ml-auto text-xs flex-shrink-0" />
</div>
</div>
</div>
</section>
<!--
<!--
Últimos lançamentos
-->
<section class="dash-card rounded-md shadow-[0_0_0_3px_color-mix(in_srgb,var(--primary-color)_7%,transparent)]">
<div class="dash-card__head gap-2.5 p-2.5">
<i class="pi pi-list cfg-subheader__icon w-10 h-10 rounded-md flex-shrink-0" />
<div class="flex-1">
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Últimos lançamentos</div>
<div class="dash-card__sub">Cobranças e receitas recentes</div>
<section class="dash-card rounded-md shadow-[0_0_0_3px_color-mix(in_srgb,var(--primary-color)_7%,transparent)]">
<div class="dash-card__head gap-2.5 p-2.5">
<i class="pi pi-list cfg-subheader__icon w-10 h-10 rounded-md flex-shrink-0" />
<div class="flex-1">
<div class="font-bold tracking-tight text-[var(--text-color-secondary)]">Últimos lançamentos</div>
<div class="dash-card__sub">Cobranças e receitas recentes</div>
</div>
<button class="flex items-center gap-1 bg-transparent border-none cursor-pointer text-xs font-semibold text-[var(--primary-color,#6366f1)] p-0 flex-shrink-0" @click="goToLancamentos">
Ver todos <i class="pi pi-arrow-right text-xs" />
</button>
</div>
<div v-if="recentLoading" class="px-3 pb-3 pt-1 flex flex-col gap-2">
<Skeleton v-for="n in 5" :key="n" height="2.5rem" border-radius="6px" />
</div>
<div v-else-if="!recentRecords.length" class="flex items-center justify-center gap-2 py-10 text-[var(--text-color-secondary)]"><i class="pi pi-wallet opacity-40" /> Nenhum lançamento encontrado.</div>
<DataTable v-else :value="recentRecords" size="small" :show-gridlines="false" class="recent-table">
<Column field="due_date" header="Data">
<template #body="{ data }">
{{ fmtDate(data.paid_at ?? data.due_date) }}
</template>
</Column>
<Column field="description" header="Descrição">
<template #body="{ data }">
{{ data.description || data.notes || '—' }}
</template>
</Column>
<Column header="Tipo">
<template #body="{ data }">
<Tag :value="data.type === 'receita' ? 'Receita' : 'Despesa'" :severity="data.type === 'receita' ? 'success' : 'danger'" class="text-xs" />
</template>
</Column>
<Column header="Valor">
<template #body="{ data }">
<span :class="data.type === 'receita' ? 'text-emerald-600 font-semibold' : 'text-red-500 font-semibold'">
{{ fmtBRL(data.final_amount ?? data.amount) }}
</span>
</template>
</Column>
<Column header="Status">
<template #body="{ data }">
<Tag :value="STATUS_CFG[data.status]?.label ?? data.status" :severity="STATUS_CFG[data.status]?.severity ?? 'secondary'" :class="data.status === 'refunded' ? 'tag-refunded' : ''" class="text-xs" />
</template>
</Column>
</DataTable>
</section>
</div>
<button
class="flex items-center gap-1 bg-transparent border-none cursor-pointer text-xs font-semibold text-[var(--primary-color,#6366f1)] p-0 flex-shrink-0"
@click="goToLancamentos"
>
Ver todos <i class="pi pi-arrow-right text-xs" />
</button>
</div>
<div v-if="recentLoading" class="px-3 pb-3 pt-1 flex flex-col gap-2">
<Skeleton v-for="n in 5" :key="n" height="2.5rem" border-radius="6px" />
</div>
<div
v-else-if="!recentRecords.length"
class="flex items-center justify-center gap-2 py-10 text-[var(--text-color-secondary)]"
>
<i class="pi pi-wallet opacity-40" /> Nenhum lançamento encontrado.
</div>
<DataTable
v-else
:value="recentRecords"
size="small"
:show-gridlines="false"
class="recent-table"
>
<Column field="due_date" header="Data">
<template #body="{ data }">
{{ fmtDate(data.paid_at ?? data.due_date) }}
</template>
</Column>
<Column field="description" header="Descrição">
<template #body="{ data }">
{{ data.description || data.notes || '—' }}
</template>
</Column>
<Column header="Tipo">
<template #body="{ data }">
<Tag
:value="data.type === 'receita' ? 'Receita' : 'Despesa'"
:severity="data.type === 'receita' ? 'success' : 'danger'"
class="text-xs"
/>
</template>
</Column>
<Column header="Valor">
<template #body="{ data }">
<span :class="data.type === 'receita' ? 'text-emerald-600 font-semibold' : 'text-red-500 font-semibold'">
{{ fmtBRL(data.final_amount ?? data.amount) }}
</span>
</template>
</Column>
<Column header="Status">
<template #body="{ data }">
<Tag
:value="STATUS_CFG[data.status]?.label ?? data.status"
:severity="STATUS_CFG[data.status]?.severity ?? 'secondary'"
:class="data.status === 'refunded' ? 'tag-refunded' : ''"
class="text-xs"
/>
</template>
</Column>
</DataTable>
</section>
</div>
</div>
</div>
</template>
<style scoped>
/* ── recent table ───────────────────────────────────────────── */
.recent-table :deep(.p-datatable-thead > tr > th) {
background: var(--surface-ground);
font-size: .75rem;
text-transform: uppercase;
letter-spacing: .04em;
padding: .5rem .75rem;
background: var(--surface-ground);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.5rem 0.75rem;
}
.recent-table :deep(.p-datatable-tbody > tr > td) {
padding: .55rem .75rem;
font-size: .85rem;
padding: 0.55rem 0.75rem;
font-size: 0.85rem;
}
/* ── tag: refunded (roxo) ───────────────────────────────────── */
:deep(.tag-refunded) { background: #a855f7 !important; color: #fff !important; }
:deep(.tag-refunded) {
background: #a855f7 !important;
color: #fff !important;
}
</style>
File diff suppressed because it is too large Load Diff