diff --git a/components/relatorios/estabelecimentos/establishments-list.tsx b/components/relatorios/estabelecimentos/establishments-list.tsx new file mode 100644 index 0000000..dc2d5a1 --- /dev/null +++ b/components/relatorios/estabelecimentos/establishments-list.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { RiStore2Line } from "@remixicon/react"; +import MoneyValues from "@/components/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 type { TopEstabelecimentosData } from "@/lib/relatorios/estabelecimentos/fetch-data"; + +type EstablishmentsListProps = { + establishments: TopEstabelecimentosData["establishments"]; +}; + +const buildInitials = (value: string) => { + const parts = value.trim().split(/\s+/).filter(Boolean); + if (parts.length === 0) return "ES"; + if (parts.length === 1) { + const firstPart = parts[0]; + return firstPart ? firstPart.slice(0, 2).toUpperCase() : "ES"; + } + const firstChar = parts[0]?.[0] ?? ""; + const secondChar = parts[1]?.[0] ?? ""; + return `${firstChar}${secondChar}`.toUpperCase() || "ES"; +}; + +export function EstablishmentsList({ + establishments, +}: EstablishmentsListProps) { + if (establishments.length === 0) { + return ( + + + + + Top Estabelecimentos + + + + } + title="Nenhum estabelecimento encontrado" + description="Quando houver compras registradas, elas aparecerão aqui." + /> + + + ); + } + + const maxCount = Math.max(...establishments.map((e) => e.count)); + + return ( + + + + + Top Estabelecimentos por Frequência + + + +
+ {establishments.map((establishment, index) => { + const _initials = buildInitials(establishment.name); + + return ( +
+
+
+ {/* Rank number - same size as icon containers */} +
+ + {index + 1} + +
+ + {/* Name and categories */} +
+ + {establishment.name} + +
+ {establishment.categories + .slice(0, 2) + .map((cat, catIndex) => ( + + {cat.name} + + ))} +
+
+
+ + {/* Value and stats */} +
+ + + {establishment.count}x • Média:{" "} + + +
+
+ + {/* Progress bar */} +
+ +
+
+ ); + })} +
+
+
+ ); +} diff --git a/components/relatorios/estabelecimentos/highlights-cards.tsx b/components/relatorios/estabelecimentos/highlights-cards.tsx new file mode 100644 index 0000000..2f72fdb --- /dev/null +++ b/components/relatorios/estabelecimentos/highlights-cards.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { RiFireLine, RiTrophyLine } from "@remixicon/react"; +import { Card, CardContent } from "@/components/ui/card"; +import type { TopEstabelecimentosData } from "@/lib/relatorios/estabelecimentos/fetch-data"; + +type HighlightsCardsProps = { + summary: TopEstabelecimentosData["summary"]; +}; + +export function HighlightsCards({ summary }: HighlightsCardsProps) { + return ( +
+ + +
+
+ +
+
+

+ Mais Frequente +

+

+ {summary.mostFrequent || "—"} +

+
+
+
+
+ + + +
+
+ +
+
+

+ Maior Gasto Total +

+

+ {summary.highestSpending || "—"} +

+
+
+
+
+
+ ); +} diff --git a/components/relatorios/estabelecimentos/period-filter.tsx b/components/relatorios/estabelecimentos/period-filter.tsx new file mode 100644 index 0000000..ee2421b --- /dev/null +++ b/components/relatorios/estabelecimentos/period-filter.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import type { PeriodFilter } from "@/lib/relatorios/estabelecimentos/fetch-data"; +import { cn } from "@/lib/utils"; + +type PeriodFilterProps = { + currentFilter: PeriodFilter; +}; + +const filterOptions: { value: PeriodFilter; label: string }[] = [ + { value: "3", label: "3 meses" }, + { value: "6", label: "6 meses" }, + { value: "12", label: "12 meses" }, +]; + +export function PeriodFilterButtons({ currentFilter }: PeriodFilterProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const handleFilterChange = (filter: PeriodFilter) => { + const params = new URLSearchParams(searchParams.toString()); + params.set("meses", filter); + router.push(`/relatorios/estabelecimentos?${params.toString()}`); + }; + + return ( +
+
+ {filterOptions.map((option) => ( + + ))} +
+
+ ); +} diff --git a/components/relatorios/estabelecimentos/summary-cards.tsx b/components/relatorios/estabelecimentos/summary-cards.tsx new file mode 100644 index 0000000..b35f231 --- /dev/null +++ b/components/relatorios/estabelecimentos/summary-cards.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { + RiExchangeLine, + RiMoneyDollarCircleLine, + RiRepeatLine, + RiStore2Line, +} from "@remixicon/react"; +import MoneyValues from "@/components/money-values"; +import { Card, CardContent } from "@/components/ui/card"; +import type { TopEstabelecimentosData } from "@/lib/relatorios/estabelecimentos/fetch-data"; + +type SummaryCardsProps = { + summary: TopEstabelecimentosData["summary"]; +}; + +export function SummaryCards({ summary }: SummaryCardsProps) { + const cards = [ + { + title: "Estabelecimentos", + value: summary.totalEstablishments, + isMoney: false, + icon: RiStore2Line, + description: "Locais diferentes", + }, + { + title: "Transações", + value: summary.totalTransactions, + isMoney: false, + icon: RiExchangeLine, + description: "Compras no período", + }, + { + title: "Total Gasto", + value: summary.totalSpent, + isMoney: true, + icon: RiMoneyDollarCircleLine, + description: "Soma de todas as compras", + }, + { + title: "Ticket Médio", + value: summary.avgPerTransaction, + isMoney: true, + icon: RiRepeatLine, + description: "Média por transação", + }, + ]; + + return ( +
+ {cards.map((card) => ( + + +
+
+

+ {card.title} +

+ {card.isMoney ? ( + + ) : ( +

{card.value}

+ )} +

+ {card.description} +

+
+ +
+
+
+ ))} +
+ ); +} diff --git a/components/relatorios/estabelecimentos/top-categories.tsx b/components/relatorios/estabelecimentos/top-categories.tsx new file mode 100644 index 0000000..fc1cdd2 --- /dev/null +++ b/components/relatorios/estabelecimentos/top-categories.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { RiPriceTag3Line } from "@remixicon/react"; +import { CategoryIconBadge } from "@/components/categorias/category-icon-badge"; +import MoneyValues from "@/components/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 type { TopEstabelecimentosData } from "@/lib/relatorios/estabelecimentos/fetch-data"; + +type TopCategoriesProps = { + categories: TopEstabelecimentosData["topCategories"]; +}; + +export function TopCategories({ categories }: TopCategoriesProps) { + if (categories.length === 0) { + return ( + + + + + Principais Categorias + + + + } + title="Nenhuma categoria encontrada" + description="Quando houver despesas categorizadas, elas aparecerão aqui." + /> + + + ); + } + + const totalAmount = categories.reduce((acc, c) => acc + c.totalAmount, 0); + + return ( + + + + + Principais Categorias + + + +
+ {categories.map((category, index) => { + const percent = + totalAmount > 0 ? (category.totalAmount / totalAmount) * 100 : 0; + + return ( +
+
+
+ + + {/* Name and percentage */} +
+ + {category.name} + + + {percent.toFixed(0)}% do total •{" "} + {category.transactionCount}x + +
+
+ + {/* Value */} +
+ +
+
+ + {/* Progress bar */} +
+ +
+
+ ); + })} +
+
+
+ ); +}