feat(reports): melhora notas, calendario e analises

This commit is contained in:
Felipe Coutinho
2026-03-09 17:14:04 +00:00
parent ada1377640
commit 6205dee42a
35 changed files with 429 additions and 590 deletions

View File

@@ -2,10 +2,10 @@
import { RiPieChartLine } from "@remixicon/react";
import { CategoryIconBadge } from "@/components/categorias/category-icon-badge";
import MoneyValues from "@/components/money-values";
import MoneyValues from "@/components/shared/money-values";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { WidgetEmptyState } from "@/components/widget-empty-state";
import { WidgetEmptyState } from "@/components/shared/widget-empty-state";
import type { CardDetailData } from "@/lib/relatorios/cartoes-report";
type CardCategoryBreakdownProps = {

View File

@@ -10,36 +10,14 @@ import {
} from "@/components/ui/tooltip";
import type { CardDetailData } from "@/lib/relatorios/cartoes-report";
import { cn } from "@/lib/utils";
import { formatCurrency } from "@/lib/utils/currency";
import { formatPeriodMonthShort } from "@/lib/utils/period";
type CardInvoiceStatusProps = {
data: CardDetailData["invoiceStatus"];
};
const monthLabels = [
"Jan",
"Fev",
"Mar",
"Abr",
"Mai",
"Jun",
"Jul",
"Ago",
"Set",
"Out",
"Nov",
"Dez",
];
export function CardInvoiceStatus({ data }: CardInvoiceStatusProps) {
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
const getStatusColor = (status: string | null) => {
switch (status) {
case "pago":
@@ -66,11 +44,6 @@ export function CardInvoiceStatus({ data }: CardInvoiceStatusProps) {
}
};
const formatPeriodShort = (period: string) => {
const [, month] = period.split("-");
return monthLabels[parseInt(month, 10) - 1];
};
return (
<Card>
<CardHeader className="pb-2">
@@ -93,13 +66,16 @@ export function CardInvoiceStatus({ data }: CardInvoiceStatusProps) {
)}
/>
<span className="text-xs text-muted-foreground">
{formatPeriodShort(invoice.period)}
{formatPeriodMonthShort(invoice.period)}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="top">
<p className="font-medium">
{formatCurrency(invoice.amount)}
{formatCurrency(invoice.amount, {
maximumFractionDigits: 0,
minimumFractionDigits: 0,
})}
</p>
<p className="text-xs ">{getStatusLabel(invoice.status)}</p>
</TooltipContent>

View File

@@ -1,11 +1,11 @@
"use client";
import { RiShoppingBag3Line } from "@remixicon/react";
import MoneyValues from "@/components/money-values";
import MoneyValues from "@/components/shared/money-values";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { WidgetEmptyState } from "@/components/widget-empty-state";
import { WidgetEmptyState } from "@/components/shared/widget-empty-state";
import type { CardDetailData } from "@/lib/relatorios/cartoes-report";
type CardTopExpensesProps = {

View File

@@ -16,7 +16,10 @@ import {
ChartContainer,
ChartTooltip,
} from "@/components/ui/chart";
import { resolveLogoSrc } from "@/lib/logo";
import type { CardDetailData } from "@/lib/relatorios/cartoes-report";
import { formatCurrency, formatCurrencyCompact } from "@/lib/utils/currency";
import { formatPercentage } from "@/lib/utils/percentage";
type CardUsageChartProps = {
data: CardDetailData["monthlyUsage"];
@@ -34,48 +37,14 @@ const chartConfig = {
},
} satisfies ChartConfig;
const resolveLogoPath = (logo: string | null) => {
if (!logo) return null;
if (
logo.startsWith("http://") ||
logo.startsWith("https://") ||
logo.startsWith("data:")
) {
return logo;
}
return logo.startsWith("/") ? logo : `/logos/${logo}`;
};
export function CardUsageChart({ data, limit, card }: CardUsageChartProps) {
const formatCurrency = (value: number) => {
return new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
};
const formatCurrencyCompact = (value: number) => {
if (Math.abs(value) >= 1000) {
return new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
notation: "compact",
}).format(value);
}
return formatCurrency(value);
};
// Always show last 12 months
const chartData = data.slice(-12).map((item) => ({
month: item.periodLabel,
amount: item.amount,
}));
const logoPath = resolveLogoPath(card.logo);
const logoPath = resolveLogoSrc(card.logo);
return (
<Card>
@@ -124,7 +93,17 @@ export function CardUsageChart({ data, limit, card }: CardUsageChartProps) {
axisLine={false}
tickMargin={8}
className="text-xs"
tickFormatter={formatCurrencyCompact}
tickFormatter={(value) =>
Math.abs(Number(value)) >= 1000
? formatCurrencyCompact(Number(value), {
maximumFractionDigits: 0,
minimumFractionDigits: 0,
})
: formatCurrency(Number(value), {
maximumFractionDigits: 0,
minimumFractionDigits: 0,
})
}
/>
{limit > 0 && (
<ReferenceLine
@@ -159,7 +138,10 @@ export function CardUsageChart({ data, limit, card }: CardUsageChartProps) {
Uso
</span>
<span className="text-xs font-medium tabular-nums">
{formatCurrency(value)}
{formatCurrency(value, {
maximumFractionDigits: 0,
minimumFractionDigits: 0,
})}
</span>
</div>
{limit > 0 && (
@@ -168,7 +150,10 @@ export function CardUsageChart({ data, limit, card }: CardUsageChartProps) {
% do Limite
</span>
<span className="text-xs font-medium tabular-nums">
{usagePercent.toFixed(0)}%
{formatPercentage(usagePercent, {
maximumFractionDigits: 0,
minimumFractionDigits: 0,
})}
</span>
</div>
)}

View File

@@ -4,59 +4,24 @@ import { RiBankCard2Line } from "@remixicon/react";
import Image from "next/image";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import MoneyValues from "@/components/money-values";
import MoneyValues from "@/components/shared/money-values";
import { Card, CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { resolveCardBrandAsset } from "@/lib/cartoes/brand-assets";
import { resolveLogoSrc } from "@/lib/logo";
import type { CartoesReportData } from "@/lib/relatorios/cartoes-report";
import { cn } from "@/lib/utils";
import { formatCurrency } from "@/lib/utils/currency";
import { formatPercentage } from "@/lib/utils/percentage";
type CardsOverviewProps = {
data: CartoesReportData;
};
const BRAND_ASSETS: Record<string, string> = {
visa: "/bandeiras/visa.svg",
mastercard: "/bandeiras/mastercard.svg",
amex: "/bandeiras/amex.svg",
american: "/bandeiras/amex.svg",
elo: "/bandeiras/elo.svg",
hipercard: "/bandeiras/hipercard.svg",
hiper: "/bandeiras/hipercard.svg",
};
const resolveBrandAsset = (brand: string | null) => {
if (!brand) return null;
const normalized = brand.trim().toLowerCase();
const match = (
Object.keys(BRAND_ASSETS) as Array<keyof typeof BRAND_ASSETS>
).find((entry) => normalized.includes(entry));
return match ? BRAND_ASSETS[match] : null;
};
const resolveLogoPath = (logo: string | null) => {
if (!logo) return null;
if (
logo.startsWith("http://") ||
logo.startsWith("https://") ||
logo.startsWith("data:")
) {
return logo;
}
return logo.startsWith("/") ? logo : `/logos/${logo}`;
};
export function CardsOverview({ data }: CardsOverviewProps) {
const searchParams = useSearchParams();
const periodoParam = searchParams.get("periodo");
const formatCurrency = (value: number) =>
new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(value);
const getUsageColor = (percent: number) => {
if (percent < 50) return "bg-success";
if (percent < 80) return "bg-warning";
@@ -107,7 +72,10 @@ export function CardsOverview({ data }: CardsOverviewProps) {
/>
) : (
<p className="text-2xl font-semibold">
{card.value.toFixed(0)}%
{formatPercentage(card.value, {
maximumFractionDigits: 0,
minimumFractionDigits: 0,
})}
</p>
)}
</CardContent>
@@ -120,8 +88,8 @@ export function CardsOverview({ data }: CardsOverviewProps) {
{/* Cards list */}
<div className="grid gap-2 grid-cols-2 lg:grid-cols-4 xl:grid-cols-4">
{data.cards.map((card) => {
const logoPath = resolveLogoPath(card.logo);
const brandAsset = resolveBrandAsset(card.brand);
const logoPath = resolveLogoSrc(card.logo);
const brandAsset = resolveCardBrandAsset(card.brand);
const isSelected = data.selectedCard?.card.id === card.id;
return (
@@ -174,7 +142,10 @@ export function CardsOverview({ data }: CardsOverviewProps) {
)}
/>
<span className="text-xs font-medium tabular-nums">
{card.usagePercent.toFixed(0)}%
{formatPercentage(card.usagePercent, {
maximumFractionDigits: 0,
minimumFractionDigits: 0,
})}
</span>
</div>
</div>