diff --git a/components/dashboard/expenses-by-category-widget-with-chart.tsx b/components/dashboard/expenses-by-category-widget-with-chart.tsx index 9a7a537..03dd812 100644 --- a/components/dashboard/expenses-by-category-widget-with-chart.tsx +++ b/components/dashboard/expenses-by-category-widget-with-chart.tsx @@ -3,6 +3,11 @@ import MoneyValues from "@/components/money-values"; import { ChartContainer, type ChartConfig } from "@/components/ui/chart"; import type { ExpensesByCategoryData } from "@/lib/dashboard/categories/expenses-by-category"; +import { + buildCategoryInitials, + getCategoryBgColor, + getCategoryColor, +} from "@/lib/utils/category-colors"; import { getIconComponent } from "@/lib/utils/icons"; import { formatPeriodForUrl } from "@/lib/utils/period"; import { @@ -25,20 +30,6 @@ type ExpensesByCategoryWidgetWithChartProps = { period: string; }; -const buildInitials = (value: string) => { - const parts = value.trim().split(/\s+/).filter(Boolean); - if (parts.length === 0) { - return "CT"; - } - if (parts.length === 1) { - const firstPart = parts[0]; - return firstPart ? firstPart.slice(0, 2).toUpperCase() : "CT"; - } - const firstChar = parts[0]?.[0] ?? ""; - const secondChar = parts[1]?.[0] ?? ""; - return `${firstChar}${secondChar}`.toUpperCase() || "CT"; -}; - const formatPercentage = (value: number) => { return `${Math.abs(value).toFixed(0)}%`; }; @@ -170,11 +161,13 @@ export function ExpensesByCategoryWidgetWithChart({
- {data.categories.map((category) => { + {data.categories.map((category, index) => { const IconComponent = category.categoryIcon ? getIconComponent(category.categoryIcon) : null; - const initials = buildInitials(category.categoryName); + const initials = buildCategoryInitials(category.categoryName); + const color = getCategoryColor(index); + const bgColor = getCategoryBgColor(index); const hasIncrease = category.percentageChange !== null && category.percentageChange > 0; @@ -199,11 +192,17 @@ export function ExpensesByCategoryWidgetWithChart({ >
-
+
{IconComponent ? ( - + ) : ( - + {initials} )} diff --git a/components/dashboard/income-by-category-widget-with-chart.tsx b/components/dashboard/income-by-category-widget-with-chart.tsx index 133fcda..fec5527 100644 --- a/components/dashboard/income-by-category-widget-with-chart.tsx +++ b/components/dashboard/income-by-category-widget-with-chart.tsx @@ -3,6 +3,11 @@ import MoneyValues from "@/components/money-values"; import { ChartContainer, type ChartConfig } from "@/components/ui/chart"; import type { IncomeByCategoryData } from "@/lib/dashboard/categories/income-by-category"; +import { + buildCategoryInitials, + getCategoryBgColor, + getCategoryColor, +} from "@/lib/utils/category-colors"; import { getIconComponent } from "@/lib/utils/icons"; import { formatPeriodForUrl } from "@/lib/utils/period"; import { @@ -25,20 +30,6 @@ type IncomeByCategoryWidgetWithChartProps = { period: string; }; -const buildInitials = (value: string) => { - const parts = value.trim().split(/\s+/).filter(Boolean); - if (parts.length === 0) { - return "CT"; - } - if (parts.length === 1) { - const firstPart = parts[0]; - return firstPart ? firstPart.slice(0, 2).toUpperCase() : "CT"; - } - const firstChar = parts[0]?.[0] ?? ""; - const secondChar = parts[1]?.[0] ?? ""; - return `${firstChar}${secondChar}`.toUpperCase() || "CT"; -}; - const formatPercentage = (value: number) => { return `${Math.abs(value).toFixed(1)}%`; }; @@ -170,11 +161,13 @@ export function IncomeByCategoryWidgetWithChart({
- {data.categories.map((category) => { + {data.categories.map((category, index) => { const IconComponent = category.categoryIcon ? getIconComponent(category.categoryIcon) : null; - const initials = buildInitials(category.categoryName); + const initials = buildCategoryInitials(category.categoryName); + const color = getCategoryColor(index); + const bgColor = getCategoryBgColor(index); const hasIncrease = category.percentageChange !== null && category.percentageChange > 0; @@ -199,11 +192,17 @@ export function IncomeByCategoryWidgetWithChart({ >
-
+
{IconComponent ? ( - + ) : ( - + {initials} )} diff --git a/components/top-estabelecimentos/top-categories.tsx b/components/top-estabelecimentos/top-categories.tsx new file mode 100644 index 0000000..0c1bcb8 --- /dev/null +++ b/components/top-estabelecimentos/top-categories.tsx @@ -0,0 +1,130 @@ +"use client"; + +import MoneyValues from "@/components/money-values"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { WidgetEmptyState } from "@/components/widget-empty-state"; +import type { TopEstabelecimentosData } from "@/lib/top-estabelecimentos/fetch-data"; +import { + buildCategoryInitials, + getCategoryBgColor, + getCategoryColor, +} from "@/lib/utils/category-colors"; +import { getIconComponent } from "@/lib/utils/icons"; +import { title_font } from "@/public/fonts/font_index"; +import { RiPriceTag3Line } from "@remixicon/react"; + +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 IconComponent = category.icon + ? getIconComponent(category.icon) + : null; + const color = getCategoryColor(index); + const bgColor = getCategoryBgColor(index); + const initials = buildCategoryInitials(category.name); + const percent = + totalAmount > 0 ? (category.totalAmount / totalAmount) * 100 : 0; + + return ( +
+
+
+
+ {IconComponent ? ( + + ) : ( + + {initials} + + )} +
+ + {/* Name and percentage */} +
+ + {category.name} + + + {percent.toFixed(0)}% do total •{" "} + {category.transactionCount}x + +
+
+ + {/* Value */} +
+ +
+
+ + {/* Progress bar */} +
+
+
+
+
+
+ ); + })} +
+ + + ); +} diff --git a/lib/utils/category-colors.ts b/lib/utils/category-colors.ts new file mode 100644 index 0000000..f05ada6 --- /dev/null +++ b/lib/utils/category-colors.ts @@ -0,0 +1,46 @@ +/** + * Cores para categorias em widgets e listas + * Usadas para colorir ícones e backgrounds de categorias + */ +export const CATEGORY_COLORS = [ + "#ef4444", // red + "#3b82f6", // blue + "#10b981", // emerald + "#f59e0b", // amber + "#8b5cf6", // violet + "#ec4899", // pink + "#14b8a6", // teal + "#f97316", // orange + "#6366f1", // indigo + "#84cc16", // lime +] as const; + +/** + * Retorna a cor para um índice específico (com ciclo) + */ +export function getCategoryColor(index: number): string { + return CATEGORY_COLORS[index % CATEGORY_COLORS.length]; +} + +/** + * Retorna a cor de background com transparência + */ +export function getCategoryBgColor(index: number): string { + const color = getCategoryColor(index); + return `${color}15`; +} + +/** + * Gera iniciais a partir de um nome + */ +export function buildCategoryInitials(value: string): string { + const parts = value.trim().split(/\s+/).filter(Boolean); + if (parts.length === 0) return "CT"; + if (parts.length === 1) { + const firstPart = parts[0]; + return firstPart ? firstPart.slice(0, 2).toUpperCase() : "CT"; + } + const firstChar = parts[0]?.[0] ?? ""; + const secondChar = parts[1]?.[0] ?? ""; + return `${firstChar}${secondChar}`.toUpperCase() || "CT"; +}