Skip to content

Commit 1aceafe

Browse files
Thales-Chagasclaude
andcommitted
Design (parte 2): graficos do dashboard + corrige uuid vazio no sync
- Barras: gradiente vertical (esmeralda/ardosia), cantos arredondados, tooltip proprio ciente do tema escuro, legenda compacta no titulo. - Rosca: cada fatia usa a COR da categoria (gradientes SVG), total no centro, legenda com bolinha gradiente + % + valor, cantos arredondados. - BUG real corrigido: lancamento manual com cliente/fornecedor/centro vazios mandava "" para colunas uuid (erro 22P02) e o sync entrava em loop de erro; agora "" vira null (fkOuNull em txParaLinha). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ad255f1 commit 1aceafe

2 files changed

Lines changed: 134 additions & 35 deletions

File tree

src/App.jsx

Lines changed: 127 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ import {
5656
XAxis,
5757
YAxis,
5858
Tooltip,
59-
Legend,
6059
CartesianGrid,
6160
PieChart,
6261
Pie,
@@ -1372,7 +1371,27 @@ function PaginaTransacoes({ tipo, espaco, empresarial, ano, mesIdx, acoes, showT
13721371
DASHBOARD
13731372
============================================================ */
13741373

1375-
const PIE_COLORS = ["#10b981", "#34d399", "#6ee7b7", "#fbbf24", "#f87171", "#94a3b8", "#60a5fa", "#c084fc"];
1374+
// Tooltip dos gráficos: cartão flutuante com bolinha colorida, ciente do tema
1375+
// escuro. `cores` mapeia série -> cor (barras); na rosca a cor vem do próprio
1376+
// dado (payload.gCss = gradiente da categoria).
1377+
function TooltipGrafico({ active, payload, label, cores = {} }) {
1378+
if (!active || !payload?.length) return null;
1379+
return (
1380+
<div className="rounded-xl border border-slate-200 bg-white/95 px-3 py-2 text-xs shadow-lg backdrop-blur dark:border-slate-700 dark:bg-slate-900/95">
1381+
{label != null && <p className="mb-1 font-semibold text-slate-600 dark:text-slate-300">{label}</p>}
1382+
{payload.map((p) => (
1383+
<p key={p.name} className="flex items-center gap-1.5 py-0.5 text-slate-500 dark:text-slate-400">
1384+
<span
1385+
className="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
1386+
style={{ background: p.payload?.gCss || cores[p.name] || p.color }}
1387+
/>
1388+
<span className="font-medium text-slate-700 dark:text-slate-200">{p.name}</span>
1389+
<span className="ml-auto pl-3 font-semibold tabular-nums text-slate-700 dark:text-slate-100">{fmtBRL(p.value)}</span>
1390+
</p>
1391+
))}
1392+
</div>
1393+
);
1394+
}
13761395

13771396
function PaginaDashboard({ espaco, ano, mesIdx, escuro, irPara }) {
13781397
const ts = espaco.transacoes;
@@ -1417,17 +1436,22 @@ function PaginaDashboard({ espaco, ano, mesIdx, escuro, irPara }) {
14171436
});
14181437
}
14191438

1420-
// pizza — despesas do mês por categoria
1439+
// rosca — despesas do mês por categoria, cada fatia com a COR da categoria
14211440
const porCat = {};
14221441
doMes
14231442
.filter((t) => t.tipo === "despesa" && t.status === "ok")
14241443
.forEach((t) => {
1425-
const nome = espaco.categorias.find((c) => c.id === t.categoriaId)?.nome || "Outros";
1426-
porCat[nome] = (porCat[nome] || 0) + t.valor;
1444+
const cat = espaco.categorias.find((c) => c.id === t.categoriaId) || null;
1445+
const nome = cat?.nome || "Outros";
1446+
if (!porCat[nome]) porCat[nome] = { value: 0, g: cat ? gradCat(cat) : gradPorId("grafite") };
1447+
porCat[nome].value += t.valor;
14271448
});
14281449
const pieData = Object.entries(porCat)
1429-
.map(([name, value]) => ({ name, value }))
1450+
.map(([name, { value, g }]) => ({ name, value, g, gCss: cssGrad(g) }))
14301451
.sort((a, b) => b.value - a.value);
1452+
const totalPie = pieData.reduce((a, p) => a + p.value, 0);
1453+
// gradientes únicos usados (viram <linearGradient> no SVG da rosca)
1454+
const gradsPie = [...new Map(pieData.map((p) => [p.g.id, p.g])).values()];
14311455

14321456
return (
14331457
<div className="space-y-4">
@@ -1476,48 +1500,120 @@ function PaginaDashboard({ espaco, ano, mesIdx, escuro, irPara }) {
14761500

14771501
<div className="grid gap-4 lg:grid-cols-2">
14781502
<Card>
1479-
<SectionTitle>Entradas × Saídas (últimos 6 meses)</SectionTitle>
1503+
<SectionTitle
1504+
right={
1505+
<div className="flex items-center gap-3 text-[11px] text-slate-400">
1506+
<span className="flex items-center gap-1.5">
1507+
<span className="h-2.5 w-2.5 rounded-full" style={{ background: "linear-gradient(180deg,#34d399,#059669)" }} />
1508+
Entradas
1509+
</span>
1510+
<span className="flex items-center gap-1.5">
1511+
<span className="h-2.5 w-2.5 rounded-full" style={{ background: escuro ? "linear-gradient(180deg,#64748b,#334155)" : "linear-gradient(180deg,#cbd5e1,#94a3b8)" }} />
1512+
Saídas
1513+
</span>
1514+
</div>
1515+
}
1516+
>
1517+
Entradas × Saídas (últimos 6 meses)
1518+
</SectionTitle>
14801519
<div className="h-64">
14811520
<ResponsiveContainer width="100%" height="100%">
1482-
<BarChart data={barData} margin={{ top: 5, right: 5, left: -10, bottom: 0 }}>
1521+
<BarChart data={barData} margin={{ top: 8, right: 5, left: -10, bottom: 0 }} barGap={5}>
1522+
<defs>
1523+
<linearGradient id="gradEntradas" x1="0" y1="0" x2="0" y2="1">
1524+
<stop offset="0%" stopColor="#34d399" />
1525+
<stop offset="100%" stopColor="#059669" />
1526+
</linearGradient>
1527+
<linearGradient id="gradSaidas" x1="0" y1="0" x2="0" y2="1">
1528+
{escuro ? (
1529+
<>
1530+
<stop offset="0%" stopColor="#64748b" />
1531+
<stop offset="100%" stopColor="#334155" />
1532+
</>
1533+
) : (
1534+
<>
1535+
<stop offset="0%" stopColor="#cbd5e1" />
1536+
<stop offset="100%" stopColor="#94a3b8" />
1537+
</>
1538+
)}
1539+
</linearGradient>
1540+
</defs>
14831541
<CartesianGrid strokeDasharray="3 3" stroke={escuro ? "#1e293b" : "#f1f5f9"} vertical={false} />
1484-
<XAxis dataKey="name" tick={{ fontSize: 12, fill: "#64748b" }} axisLine={false} tickLine={false} />
1542+
<XAxis dataKey="name" tick={{ fontSize: 12, fill: "#64748b" }} axisLine={false} tickLine={false} dy={4} />
14851543
<YAxis
14861544
tick={{ fontSize: 11, fill: "#94a3b8" }}
14871545
axisLine={false}
14881546
tickLine={false}
14891547
tickFormatter={(v) => (v >= 1000 ? (v / 1000).toLocaleString("pt-BR") + "k" : v)}
14901548
/>
1491-
<Tooltip formatter={(v) => fmtBRL(v)} contentStyle={{ borderRadius: 12, border: "1px solid #e2e8f0", fontSize: 13 }} />
1492-
<Legend wrapperStyle={{ fontSize: 12 }} />
1493-
<Bar dataKey="Entradas" fill="#10b981" radius={[6, 6, 0, 0]} />
1494-
<Bar dataKey="Saídas" fill={escuro ? "#475569" : "#cbd5e1"} radius={[6, 6, 0, 0]} />
1549+
<Tooltip
1550+
cursor={{ fill: escuro ? "rgba(148,163,184,0.07)" : "rgba(100,116,139,0.06)", radius: 8 }}
1551+
content={<TooltipGrafico cores={{ Entradas: "#10b981", Saídas: "#94a3b8" }} />}
1552+
/>
1553+
<Bar dataKey="Entradas" fill="url(#gradEntradas)" radius={[7, 7, 2, 2]} maxBarSize={26} />
1554+
<Bar dataKey="Saídas" fill="url(#gradSaidas)" radius={[7, 7, 2, 2]} maxBarSize={26} />
14951555
</BarChart>
14961556
</ResponsiveContainer>
14971557
</div>
14981558
</Card>
14991559

15001560
<Card>
15011561
<SectionTitle>Despesas do mês por categoria</SectionTitle>
1502-
<div className="h-64">
1503-
{pieData.length === 0 ? (
1504-
<div className="flex h-full items-center justify-center text-sm text-slate-400">
1505-
Sem despesas pagas neste mês.
1562+
{pieData.length === 0 ? (
1563+
<div className="flex h-64 items-center justify-center text-sm text-slate-400">
1564+
Sem despesas pagas neste mês.
1565+
</div>
1566+
) : (
1567+
<div className="flex flex-col items-center gap-2 sm:flex-row sm:gap-4">
1568+
{/* rosca com as cores das categorias + total no centro */}
1569+
<div className="relative h-52 w-52 shrink-0">
1570+
<ResponsiveContainer width="100%" height="100%">
1571+
<PieChart>
1572+
<defs>
1573+
{gradsPie.map((g) => (
1574+
<linearGradient key={g.id} id={"gradPie-" + g.id} x1="0" y1="0" x2="1" y2="1">
1575+
<stop offset="0%" stopColor={g.de} />
1576+
<stop offset="100%" stopColor={g.para} />
1577+
</linearGradient>
1578+
))}
1579+
</defs>
1580+
<Pie
1581+
data={pieData}
1582+
dataKey="value"
1583+
nameKey="name"
1584+
innerRadius={62}
1585+
outerRadius={88}
1586+
paddingAngle={pieData.length > 1 ? 3 : 0}
1587+
cornerRadius={6}
1588+
strokeWidth={0}
1589+
>
1590+
{pieData.map((p) => (
1591+
<Cell key={p.name} fill={`url(#gradPie-${p.g.id})`} />
1592+
))}
1593+
</Pie>
1594+
<Tooltip content={<TooltipGrafico />} />
1595+
</PieChart>
1596+
</ResponsiveContainer>
1597+
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
1598+
<p className="text-[10px] font-medium uppercase tracking-wide text-slate-400">Total</p>
1599+
<p className="max-w-[7.5rem] break-words text-center text-sm font-bold text-slate-700 dark:text-slate-100">
1600+
{fmtBRL(totalPie)}
1601+
</p>
1602+
</div>
15061603
</div>
1507-
) : (
1508-
<ResponsiveContainer width="100%" height="100%">
1509-
<PieChart>
1510-
<Pie data={pieData} dataKey="value" nameKey="name" innerRadius={55} outerRadius={85} paddingAngle={3}>
1511-
{pieData.map((_, i) => (
1512-
<Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} />
1513-
))}
1514-
</Pie>
1515-
<Tooltip formatter={(v) => fmtBRL(v)} contentStyle={{ borderRadius: 12, border: "1px solid #e2e8f0", fontSize: 13 }} />
1516-
<Legend wrapperStyle={{ fontSize: 12 }} />
1517-
</PieChart>
1518-
</ResponsiveContainer>
1519-
)}
1520-
</div>
1604+
{/* legenda: bolinha gradiente + nome + % */}
1605+
<div className="max-h-52 w-full flex-1 space-y-1 overflow-y-auto pr-1">
1606+
{pieData.map((p) => (
1607+
<div key={p.name} className="flex items-center gap-2 rounded-lg px-2 py-1 text-xs hover:bg-slate-50 dark:hover:bg-slate-800/60">
1608+
<span className="h-2.5 w-2.5 shrink-0 rounded-full" style={{ background: p.gCss }} />
1609+
<span className="min-w-0 flex-1 truncate font-medium text-slate-600 dark:text-slate-300">{p.name}</span>
1610+
<span className="tabular-nums text-slate-400">{((p.value / totalPie) * 100).toFixed(0)}%</span>
1611+
<span className="w-20 text-right font-semibold tabular-nums text-slate-600 dark:text-slate-200">{fmtBRL(p.value)}</span>
1612+
</div>
1613+
))}
1614+
</div>
1615+
</div>
1616+
)}
15211617
</Card>
15221618
</div>
15231619
</div>

src/cloudData.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ function uuidDeString(str) {
4040
const idFinal = (oldId) => (ehUuid(oldId) ? oldId : uuidDeString(oldId));
4141

4242
/* ---------- transações: app <-> linha do banco ---------- */
43+
// FK opcional: o formulário usa "" quando não escolhe cliente/fornecedor/centro,
44+
// mas a coluna é uuid — string vazia dá erro 22P02 no Postgres. "" → null.
45+
const fkOuNull = (v) => (v ? v : null);
4346
function txParaLinha(userId, modo, t) {
4447
return {
4548
id: t.id,
@@ -50,10 +53,10 @@ function txParaLinha(userId, modo, t) {
5053
data: t.data,
5154
valor: t.valor,
5255
descricao: t.descricao ?? null,
53-
categoria_id: t.categoriaId ?? null,
54-
cliente_id: t.clienteId ?? null,
55-
fornecedor_id: t.fornecedorId ?? null,
56-
centro_custo_id: t.centroCustoId ?? null,
56+
categoria_id: fkOuNull(t.categoriaId),
57+
cliente_id: fkOuNull(t.clienteId),
58+
fornecedor_id: fkOuNull(t.fornecedorId),
59+
centro_custo_id: fkOuNull(t.centroCustoId),
5760
origem: t.origem || "manual",
5861
};
5962
}

0 commit comments

Comments
 (0)