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 */}
+
+
+ );
+ })}
+
+
+
+ );
+}