import React, { useState } from "react"; import { ChevronDown, ChevronRight, ListFilter } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "./card"; import { Spinner } from "./spinner"; import { InfoTooltip } from "./info-tooltip"; import { cn } from "@/lib/utils"; import { formatCurrency } from "@/lib/format-currency"; // ─── Types ─────────────────────────────────────────────────────────────────── export interface ExpenseSubCategory { id: string; name: string; /** Absolute dollar amount */ amount: number; /** Percentage of parent category 0–100 */ pct: number; } export interface ExpenseCategoryItem { id: string; name: string; /** Absolute dollar amount */ amount: number; /** Percentage of total budget 0–100 */ pct: number; /** Optional lucide-react icon element */ icon?: React.ReactNode; subCategories?: ExpenseSubCategory[]; } export interface DashboardExpenseCategoriesProps { categories?: ExpenseCategoryItem[]; title?: string; subtitle?: string; /** * Optional explanatory note shown as an info-icon tooltip next to the subtitle * (e.g. the Income bucket's internal-transfer caveat). Omit to hide the icon. */ tooltipText?: string; /** * Message shown when there are no categories to display. Lets each bucket give * a specific empty state (e.g. "No income categories available"). */ emptyMessage?: string; isLoading?: boolean; className?: string; /** Currently active filter category ID (parent or sub-category) */ selectedCategoryId?: string | null; /** Called with the category ID to filter by, or null to clear */ onCategorySelect?: (id: string | null) => void; /** * Color scheme for progress bars and active states. * Use "secondary" for the Expense tab to distinguish from Income (primary). * Defaults to "primary". */ colorScheme?: "primary" | "secondary"; } // ─── Constants ──────────────────────────────────────────────────────────────── const SECONDARY_ACTIVE_BG = "color-mix(in oklch, var(--brand-secondary) 8%, transparent)"; const SECONDARY_ACTIVE_HOVER_BG = "color-mix(in oklch, var(--brand-secondary) 12%, transparent)"; // ─── Progress bar ───────────────────────────────────────────────────────────── function ProgressBar({ pct, className, colorScheme = "primary", }: { pct: number; className?: string; colorScheme?: "primary" | "secondary"; }) { return (
); } // ─── Sub-category row ───────────────────────────────────────────────────────── function SubCategoryRow({ sub, isSelected, colorScheme = "primary", onClick, }: { sub: ExpenseSubCategory; isSelected: boolean; colorScheme?: "primary" | "secondary"; onClick: () => void; }) { const isSecondary = colorScheme === "secondary"; return (
{sub.name}
{formatCurrency(sub.amount)}
); } // ─── Category row ───────────────────────────────────────────────────────────── function CategoryRow({ item, selectedCategoryId, colorScheme = "primary", onFilterClick, }: { item: ExpenseCategoryItem; selectedCategoryId?: string | null; colorScheme?: "primary" | "secondary"; onFilterClick: (id: string | null) => void; }) { const [expanded, setExpanded] = useState(false); const [hovered, setHovered] = useState(false); const hasChildren = (item.subCategories?.length ?? 0) > 0; const isSelected = selectedCategoryId === item.id; const isChildSelected = hasChildren && (item.subCategories?.some((s) => s.id === selectedCategoryId) ?? false); const isActive = isSelected || isChildSelected; const isSecondary = colorScheme === "secondary"; // Secondary scheme active background is set via inline style (color-mix not // expressible as a Tailwind arbitrary value here). Hover darkens the tint. const wrapperStyle = isSecondary && isActive ? { backgroundColor: hovered ? SECONDARY_ACTIVE_HOVER_BG : SECONDARY_ACTIVE_BG, } : undefined; return (
{/* Main row — click expands/collapses; filter icon inside triggers filter */}
{item.pct.toFixed(0)}% {formatCurrency(item.amount)}
{/* Sub-categories */} {hasChildren && expanded && (
{item.subCategories!.map((sub) => ( onFilterClick(selectedCategoryId === sub.id ? null : sub.id) } /> ))}
)} ); } // ─── Component ──────────────────────────────────────────────────────────────── export function DashboardExpenseCategories({ categories = [], title = "Expenses Categories", subtitle, tooltipText, emptyMessage = "No expense categories available", isLoading = false, className, selectedCategoryId, onCategorySelect, colorScheme = "primary", }: DashboardExpenseCategoriesProps) { return (
{title} {subtitle && ( {subtitle} {tooltipText && } )}
{isLoading ? (
) : categories.length === 0 ? (

{emptyMessage}

) : (
{categories.map((item) => ( onCategorySelect?.(id)} /> ))}
)}
); }