"use client"; import { useState, useMemo } from "react"; import { TrendingUp, TrendingDown, LayoutGrid, Sparkles, ChevronRight } from "lucide-react"; import { Sparkline } from "@/components/Sparkline"; import { MONTH_NAMES } from "@/lib/data"; import type { IncomeRow } from "@/lib/rows"; import { ZAR, ZAR2 } from "@/lib/utils"; import { ScrollFade } from "@/components/ScrollFade"; export function IncomeView({ rows, palette }: { rows: IncomeRow[]; palette: { income: string; profit: string; loss: string } }) { const [view, setView] = useState<"cards" | "grid">("cards"); const [sort, setSort] = useState<"profit" | "income">("profit"); const [expanded, setExpanded] = useState(null); const sorted = useMemo(() => { return [...rows].sort((a, b) => sort === "profit" ? b.ytdProfit - a.ytdProfit : b.income - a.income ); }, [rows, sort]); const totals = useMemo(() => { const totalIncome = rows.reduce((s, r) => s + r.income, 0); const totalProfit = rows.reduce((s, r) => s + r.ytdProfit, 0); const profitable = rows.filter((r) => r.ytdProfit > 0).length; // VAT already inside the booked figures — booked amounts are VAT-inclusive, so this // is part of the income total and is called out rather than hidden. const invoiceVat = rows.reduce((s, r) => s + r.invoiceVat, 0); const invoiceIncome = rows.reduce((s, r) => s + r.invoiceIncome, 0); return { totalIncome, totalProfit, profitable, invoiceVat, invoiceIncome }; }, [rows]); return ( <> {/* Round 4 Part B.1/B.3/B.5: toolbar wraps onto two rows on mobile, controls collapse gracefully */}
{totals.profitable} profitable {rows.length - totals.profitable} loss-making {totals.invoiceVat > 0 && ( includes {ZAR(totals.invoiceVat)} VAT )} · sorted by {sort}
{/* Round 4 Part B.3: Sort collapses to a select on mobile. View toggle stays as buttons. */}
Sort: {[ { id: "profit", label: "Profit" }, { id: "income", label: "Income" }, ].map((s) => ( ))}
{/* Part B.3: mobile sort dropdown */}
{view === "cards" ? ( ) : ( )} ); } function CardsView({ rows, expanded, setExpanded, palette, }: { rows: IncomeRow[]; expanded: string | null; setExpanded: (id: string | null) => void; palette: { income: string; profit: string; loss: string }; }) { return (
{rows.map((r) => { const isOpen = expanded === r.id; const positive = r.ytdProfit >= 0; return (
{isOpen && (
{/* 12-col on md+, 3-col on mobile — Part 3: unreadable at 375px otherwise */}
{r.months.map((v, i) => (
{MONTH_NAMES[i]}
0 ? "bg-[#00798C]/5 text-[#00798C] dark:bg-[#00798C]/15" : "text-gray-300 dark:text-text-soft" }`} > {v == null ? "—" : (v > 0 ? ZAR2(v) : "R 0")}
))}
YTD income
{ZAR(r.income)}
Fixed + running
{ZAR(r.fixed + r.running)}
Profit
{positive ? "+" : ""}{ZAR(r.ytdProfit)}
)}
); })}
); } function GridView({ rows }: { rows: IncomeRow[] }) { return (
{MONTH_NAMES.map((m) => ( ))} {rows.map((r) => { const positive = r.ytdProfit >= 0; return ( {r.months.map((v, i) => ( ))} ); })} {/* Totals row: the sum of every machine for each month. */} {MONTH_NAMES.map((m, i) => { const sum = rows.reduce((s, r) => s + (r.months[i] ?? 0), 0); return ( ); })}
Code Machine{m}YTD
{r.code}
{positive ? : } {r.name}
0 ? "text-gray-700 dark:text-gray-300" : "text-gray-300 dark:text-text-soft"}`}> {v == null ? "—" : (v > 0 ? ZAR2(v) : "R 0")}
{positive ? "+" : ""}{ZAR(r.ytdProfit)}
Total All machines {sum > 0 ? ZAR(sum) : "—"} {ZAR(rows.reduce((s, r) => s + r.income, 0))}
); }