mega-plant/components/IncomeOverview.tsx

22 lines
2.1 KiB
TypeScript
Raw Normal View History

feat(income): posted income per site, and the LIVE month on the overview Two changes to the income overview, sharing one rule for what "the month" means (lib/income-month.ts). POSTED INCOME PER SITE /income gains Vehicles / Sites views, and /sites defaults to the SAME posted-income view with a separate Internal billed invoices view. buildSiteIncomeRows (lib/site-income.ts) is the shared calculation: actual monthly_income supplies the amount, year and month, and the linked invoice supplies its historical site. It never uses assets.site, fuel-log locations or today's map assignment. - Unposted invoice lines are billing, not posted income. - Historical workbook income with no site evidence stays in Unallocated historical income; external income, whose site field names the SUPPLIER, stays in Unallocated external income. - Missing source references stay visible as unallocated invoice income. No group is dropped to make a site list look cleaner and no site is guessed to force a total. - No site profit is calculated without an explicit cost-allocation rule. - Invoice writes use revalidateInvoicePages, which invalidates both registers, Income, Sites/details, Charts and Dashboard. THE LIVE MONTH The tiles show the year-to-date income and profit AND the current month, and every vehicle card shows the current month income beside its year figure. - The month is the live CALENDAR month and it is shown even at R0. Income for a month is usually entered near the end of it, so the current month is a figure that starts at R0 and grows. - The month index is computed on the SERVER and passed down as a number. The browser never works it out for itself: two clocks disagreeing would show two different months on the same page. - A month profit is a dash until that month costs are posted. September and October carry invoice income with no posted fixed costs (their costs exist only as forecast rows), so a profit there would be income minus zero - a 100% margin that does not tie back to the year-to-date profit. It becomes a number by itself the moment the workbook posts that month. - The 12-month strip shows the live month as R0 rather than a dash, so a card cannot contradict itself. Months after it stay a dash. - The sparkline on a collapsed card is hidden below xl: the content column is only ~700px wide between md and xl, and the vehicle name must not be truncated to pay for a decorative trend.
2026-11-04 22:19:06 +00:00
"use client";
import { useId, useState } from "react";
import { TrendingUp } from "lucide-react";
import { IncomeView } from "./IncomeView";
import { SiteIncomeView } from "./SiteIncomeView";
import type { IncomeRow } from "@/lib/rows";
import type { SiteIncomeRow } from "@/lib/site-income";
import { cn } from "@/lib/utils";
export function IncomeOverview({ vehicleRows, siteRows, year, vehicleMonthIndex, siteMonthIndex }: { vehicleRows: IncomeRow[]; siteRows: SiteIncomeRow[]; year: number; vehicleMonthIndex: number; siteMonthIndex: number }) {
const [group, setGroup] = useState<"vehicles" | "sites">("vehicles");
const id = useId();
const heading = <div className="min-w-0"><div className="mb-1 flex items-center gap-3"><div className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-gradient-to-br from-[#00798C]/15 to-[#003D5B]/15 text-[#00798C]"><TrendingUp className="h-5 w-5" /></div><h1 className="text-2xl font-semibold text-[#003D5B] dark:text-white">Income</h1></div><p className="text-sm text-gray-500 dark:text-gray-400">{group === "vehicles" ? "Project income per vehicle per month" : "Posted income per invoice site per month"} · {year}</p></div>;
return <div className="min-w-0">
<div role="group" aria-label="Group income by" className="mb-5 inline-flex max-w-full gap-1 rounded-xl bg-gray-100 p-1 dark:bg-white/5">
{(["vehicles", "sites"] as const).map(value => <button key={value} type="button" onClick={() => setGroup(value)} aria-pressed={group === value} aria-controls={`${id}-panel`} className={cn("min-h-[44px] min-w-[44px] rounded-lg px-4 text-sm font-medium transition", group === value ? "bg-[#003D5B] text-white shadow-sm dark:bg-[#00798C]" : "text-gray-600 hover:bg-white dark:text-gray-400 dark:hover:bg-white/10")}>{value === "vehicles" ? "Vehicles" : "Sites"}</button>)}
</div>
<div id={`${id}-panel`}>{group === "vehicles" ? <IncomeView rows={vehicleRows} palette={{ income: "#00798C", profit: "#00798C", loss: "#D1495B" }} title={heading} monthIndex={vehicleMonthIndex} /> : <SiteIncomeView rows={siteRows} year={year} title={heading} monthIndex={siteMonthIndex} />}</div>
</div>;
}