From 2caf86871a2684ef0722187f793c8d9d771dc570 Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Tue, 20 Jan 2026 15:21:02 +0000 Subject: [PATCH] =?UTF-8?q?feat(cartoes):=20adiciona=20filtro=20de=20per?= =?UTF-8?q?=C3=ADodo=20e=20logo=20no=20gr=C3=A1fico=20de=20uso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona filtros de 3, 6 e 12 meses no CardUsageChart - Exibe logo e nome do cartão no header do gráfico - Atualiza fetchCardDetail para buscar 12 meses de dados --- .../relatorios/cartoes/card-usage-chart.tsx | 95 +++++++++++++++++-- lib/relatorios/cartoes-report.ts | 68 +++++++------ 2 files changed, 128 insertions(+), 35 deletions(-) diff --git a/components/relatorios/cartoes/card-usage-chart.tsx b/components/relatorios/cartoes/card-usage-chart.tsx index dad2442..cf0eea8 100644 --- a/components/relatorios/cartoes/card-usage-chart.tsx +++ b/components/relatorios/cartoes/card-usage-chart.tsx @@ -1,17 +1,33 @@ "use client"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { ChartContainer, ChartTooltip, type ChartConfig, } from "@/components/ui/chart"; import type { CardDetailData } from "@/lib/relatorios/cartoes-report"; -import { Bar, BarChart, CartesianGrid, XAxis, YAxis, ReferenceLine } from "recharts"; +import { cn } from "@/lib/utils"; +import { RiBankCard2Line } from "@remixicon/react"; +import Image from "next/image"; +import { useState } from "react"; +import { + Bar, + BarChart, + CartesianGrid, + ReferenceLine, + XAxis, + YAxis, +} from "recharts"; type CardUsageChartProps = { data: CardDetailData["monthlyUsage"]; limit: number; + card: { + name: string; + logo: string | null; + }; }; const chartConfig = { @@ -21,7 +37,29 @@ const chartConfig = { }, } satisfies ChartConfig; -export function CardUsageChart({ data, limit }: CardUsageChartProps) { +type PeriodFilter = "3" | "6" | "12"; + +const filterOptions: { value: PeriodFilter; label: string }[] = [ + { value: "3", label: "3 meses" }, + { value: "6", label: "6 meses" }, + { value: "12", label: "12 meses" }, +]; + +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 [period, setPeriod] = useState("6"); + const formatCurrency = (value: number) => { return new Intl.NumberFormat("pt-BR", { style: "currency", @@ -44,17 +82,58 @@ export function CardUsageChart({ data, limit }: CardUsageChartProps) { return formatCurrency(value); }; - const chartData = data.map((item) => ({ + // Filter data based on selected period + const filteredData = data.slice(-Number(period)); + + const chartData = filteredData.map((item) => ({ month: item.periodLabel, amount: item.amount, })); + const logoPath = resolveLogoPath(card.logo); + return ( - - Uso Mensal (6 meses) - +
+ {/* Card logo and name on the left */} +
+ {logoPath ? ( +
+ {`Logo +
+ ) : ( +
+ +
+ )} + {card.name} +
+ + {/* Filters on the right */} +
+ {filterOptions.map((option) => ( + + ))} +
+
@@ -131,7 +210,7 @@ export function CardUsageChart({ data, limit }: CardUsageChartProps) { /> diff --git a/lib/relatorios/cartoes-report.ts b/lib/relatorios/cartoes-report.ts index b2f9b1c..037ee0f 100644 --- a/lib/relatorios/cartoes-report.ts +++ b/lib/relatorios/cartoes-report.ts @@ -1,9 +1,15 @@ -import { lancamentos, pagadores, cartoes, categorias, faturas } from "@/db/schema"; +import { + cartoes, + categorias, + faturas, + lancamentos, + pagadores, +} from "@/db/schema"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; -import { getPreviousPeriod } from "@/lib/utils/period"; import { safeToNumber } from "@/lib/utils/number"; -import { and, eq, sum, gte, lte, inArray, not, ilike } from "drizzle-orm"; +import { getPreviousPeriod } from "@/lib/utils/period"; +import { and, eq, gte, ilike, inArray, lte, not, sum } from "drizzle-orm"; const DESPESA = "Despesa"; @@ -60,7 +66,7 @@ export type CartoesReportData = { export async function fetchCartoesReportData( userId: string, currentPeriod: string, - selectedCartaoId?: string | null + selectedCartaoId?: string | null, ): Promise { const previousPeriod = getPreviousPeriod(currentPeriod); @@ -75,7 +81,9 @@ export async function fetchCartoesReportData( status: cartoes.status, }) .from(cartoes) - .where(and(eq(cartoes.userId, userId), not(ilike(cartoes.status, "inativo")))); + .where( + and(eq(cartoes.userId, userId), not(ilike(cartoes.status, "inativo"))), + ); if (allCards.length === 0) { return { @@ -103,8 +111,8 @@ export async function fetchCartoesReportData( eq(lancamentos.period, currentPeriod), eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(lancamentos.transactionType, DESPESA), - inArray(lancamentos.cartaoId, cardIds) - ) + inArray(lancamentos.cartaoId, cardIds), + ), ) .groupBy(lancamentos.cartaoId); @@ -122,15 +130,18 @@ export async function fetchCartoesReportData( eq(lancamentos.period, previousPeriod), eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(lancamentos.transactionType, DESPESA), - inArray(lancamentos.cartaoId, cardIds) - ) + inArray(lancamentos.cartaoId, cardIds), + ), ) .groupBy(lancamentos.cartaoId); const currentUsageMap = new Map(); for (const row of currentUsageData) { if (row.cartaoId) { - currentUsageMap.set(row.cartaoId, Math.abs(safeToNumber(row.totalAmount))); + currentUsageMap.set( + row.cartaoId, + Math.abs(safeToNumber(row.totalAmount)), + ); } } @@ -139,7 +150,7 @@ export async function fetchCartoesReportData( if (row.cartaoId) { previousUsageMap.set( row.cartaoId, - Math.abs(safeToNumber(row.totalAmount)) + Math.abs(safeToNumber(row.totalAmount)), ); } } @@ -183,11 +194,13 @@ export async function fetchCartoesReportData( // Calculate totals const totalLimit = cards.reduce((acc, c) => acc + c.limit, 0); const totalUsage = cards.reduce((acc, c) => acc + c.currentUsage, 0); - const totalUsagePercent = totalLimit > 0 ? (totalUsage / totalLimit) * 100 : 0; + const totalUsagePercent = + totalLimit > 0 ? (totalUsage / totalLimit) * 100 : 0; // Fetch selected card details if provided let selectedCard: CardDetailData | null = null; - const targetCardId = selectedCartaoId || (cards.length > 0 ? cards[0].id : null); + const targetCardId = + selectedCartaoId || (cards.length > 0 ? cards[0].id : null); if (targetCardId) { const cardSummary = cards.find((c) => c.id === targetCardId); @@ -196,7 +209,7 @@ export async function fetchCartoesReportData( userId, targetCardId, cardSummary, - currentPeriod + currentPeriod, ); } } @@ -214,12 +227,12 @@ async function fetchCardDetail( userId: string, cardId: string, cardSummary: CardSummary, - currentPeriod: string + currentPeriod: string, ): Promise { - // Build period range for last 6 months + // Build period range for last 12 months const periods: string[] = []; let p = currentPeriod; - for (let i = 0; i < 6; i++) { + for (let i = 0; i < 12; i++) { periods.unshift(p); p = getPreviousPeriod(p); } @@ -256,8 +269,8 @@ async function fetchCardDetail( gte(lancamentos.period, startPeriod), lte(lancamentos.period, currentPeriod), eq(pagadores.role, PAGADOR_ROLE_ADMIN), - eq(lancamentos.transactionType, DESPESA) - ) + eq(lancamentos.transactionType, DESPESA), + ), ) .groupBy(lancamentos.period) .orderBy(lancamentos.period); @@ -286,8 +299,8 @@ async function fetchCardDetail( eq(lancamentos.cartaoId, cardId), eq(lancamentos.period, currentPeriod), eq(pagadores.role, PAGADOR_ROLE_ADMIN), - eq(lancamentos.transactionType, DESPESA) - ) + eq(lancamentos.transactionType, DESPESA), + ), ) .groupBy(lancamentos.categoriaId); @@ -312,7 +325,7 @@ async function fetchCardDetail( const totalCategoryAmount = categoryData.reduce( (acc, c) => acc + Math.abs(safeToNumber(c.totalAmount)), - 0 + 0, ); const categoryBreakdown = categoryData @@ -326,7 +339,8 @@ async function fetchCardDetail( name: catInfo?.name || "Sem categoria", icon: catInfo?.icon || null, amount, - percent: totalCategoryAmount > 0 ? (amount / totalCategoryAmount) * 100 : 0, + percent: + totalCategoryAmount > 0 ? (amount / totalCategoryAmount) * 100 : 0, }; }) .sort((a, b) => b.amount - a.amount) @@ -349,8 +363,8 @@ async function fetchCardDetail( eq(lancamentos.cartaoId, cardId), eq(lancamentos.period, currentPeriod), eq(pagadores.role, PAGADOR_ROLE_ADMIN), - eq(lancamentos.transactionType, DESPESA) - ) + eq(lancamentos.transactionType, DESPESA), + ), ) .orderBy(lancamentos.amount) .limit(10); @@ -382,8 +396,8 @@ async function fetchCardDetail( eq(faturas.userId, userId), eq(faturas.cartaoId, cardId), gte(faturas.period, startPeriod), - lte(faturas.period, currentPeriod) - ) + lte(faturas.period, currentPeriod), + ), ) .orderBy(faturas.period);