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:
@@ -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
Reference in New Issue
Block a user