Adds an Invoices sub-menu under /income with a summary table (one row per invoice, click to expand the full detail) and the data model behind it. Data: - invoices + invoice_lines PocketBase collections, created idempotently by scripts/setup-invoice-collections.mjs - line items transcribed from PHI 166/167/171 with per-invoice arithmetic validation in the seed; a bad invoice is refused without blocking the rest - invoice-*.json in data/source are auto-imported, so adding an invoice is one file Booking: - manual book-in per line (Booked to), grouped by category - POST /api/invoice-lines/:id/book moves the income row rather than adding a second one, so a booking can be changed freely - scripts/book-invoice.mjs is idempotent: rows are stamped with monthly_income.invoice_line and replaced on re-run - booked amount follows lib/invoice-booking-policy.json (bookInclusiveVat), read by both the app and the CLI so they cannot disagree Money: - the R24,205.13 column on these invoices is PLUS VAT, not escalation. The seed now rejects VAT that disagrees with the stated rate. - booked amounts are VAT-inclusive per the client ruling of 2026-10-01; the VAT stays visible rather than hidden. Uploads: - drag-and-drop PDF attached to invoices.source_pdf - GET /api/invoices/:id/document proxies it — the collection is rule-locked, so a direct PocketBase /api/files link 404s in a browser Fixes a month-aggregation bug this introduced: buildIncomeRows assigned into months[i], so a machine with two income rows in one month (one per invoice, which is normal multi-site use) showed only the last. SS01 September read R20,700 instead of R35,305 while its own YTD included both. Months now accumulate, per-month profit is computed once after accumulation, and lib/rows.test.ts pins it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
5.6 KiB
TypeScript
138 lines
5.6 KiB
TypeScript
import "./globals.css";
|
|
import type { Metadata } from "next";
|
|
import Link from "next/link";
|
|
import {
|
|
LayoutDashboard,
|
|
Truck,
|
|
TrendingUp,
|
|
Wrench,
|
|
Wallet,
|
|
LineChart as LineChartIcon,
|
|
Receipt,
|
|
Boxes,
|
|
} from "lucide-react";
|
|
import { Sidebar, type NavItem } from "@/components/Sidebar";
|
|
import { ThemeProvider } from "@/components/ThemeProvider";
|
|
import { ThemeToggle } from "@/components/ThemeToggle";
|
|
import { GlobalSearch } from "@/components/GlobalSearch";
|
|
import { getAssets } from "@/lib/data";
|
|
|
|
// The layout itself reads PocketBase (GlobalSearch needs the asset list), so it
|
|
// must be dynamic too — otherwise Next prerenders the built-in /_not-found page,
|
|
// which inherits this layout, and the build fails without a reachable database.
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export const metadata: Metadata = {
|
|
title: "Mega Plant Dashboard",
|
|
description: "Mega Plant cost & P&L dashboard",
|
|
manifest: "/manifest.json",
|
|
themeColor: "#30638E",
|
|
appleWebApp: {
|
|
title: "Mega Plant",
|
|
capable: true,
|
|
statusBarStyle: "default",
|
|
},
|
|
icons: {
|
|
icon: [
|
|
{ url: "/icons/favicon-32.png", sizes: "32x32" },
|
|
{ url: "/icons/icon-192.png", sizes: "192x192" },
|
|
{ url: "/icons/icon-512.png", sizes: "512x512" },
|
|
],
|
|
apple: [
|
|
{ url: "/icons/icon-192.png", sizes: "192x192" },
|
|
{ url: "/icons/icon-512.png", sizes: "512x512" },
|
|
],
|
|
},
|
|
};
|
|
|
|
const navItems: NavItem[] = [
|
|
{ kind: "link", href: "/", label: "Dashboard", icon: <LayoutDashboard className="w-4 h-4" /> },
|
|
{ kind: "link", href: "/plant", label: "Plant Register", icon: <Boxes className="w-4 h-4" /> },
|
|
{
|
|
kind: "group",
|
|
label: "Income",
|
|
icon: <TrendingUp className="w-4 h-4" />,
|
|
children: [
|
|
{ href: "/income", label: "Overview" },
|
|
{ href: "/income/invoices", label: "Invoices" },
|
|
],
|
|
},
|
|
{ kind: "link", href: "/running", label: "Running Costs", icon: <Wrench className="w-4 h-4" /> },
|
|
{
|
|
kind: "group",
|
|
label: "Expenses",
|
|
icon: <Wallet className="w-4 h-4" />,
|
|
children: [
|
|
{ href: "/expenses", label: "Overview" },
|
|
{ href: "/expenses/vehicles", label: "Vehicle Costs" },
|
|
{ href: "/expenses/finance", label: "Finance (Bastion)" },
|
|
{ href: "/expenses/personnel", label: "Personnel" },
|
|
{ href: "/expenses/subscriptions", label: "Subscriptions" },
|
|
{ href: "/expenses/rent", label: "Rent & Utilities" },
|
|
{ href: "/expenses/petty-cash", label: "Cash Floats" },
|
|
],
|
|
},
|
|
{ kind: "link", href: "/overheads", label: "Overheads Roll-up", icon: <Receipt className="w-4 h-4" /> },
|
|
{ kind: "link", href: "/charts", label: "Machine Charts", icon: <LineChartIcon className="w-4 h-4" /> },
|
|
];
|
|
|
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
const assets = await getAssets();
|
|
return (
|
|
<html lang="en" suppressHydrationWarning>
|
|
<head>
|
|
{/* Part 4: Next 16 emits apple-mobile-web-app-status-bar-style but NOT apple-mobile-web-app-capable.
|
|
Without this tag, iOS Add-to-Home-Screen opens in Safari instead of standalone. */}
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-title" content="Mega Plant" />
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png" />
|
|
<link rel="icon" type="image/png" sizes="64x64" href="/icons/favicon.png" />
|
|
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
|
|
{/* Umami analytics — website id 86d0bd41-e705-4677-af2d-a4228e7c4b95 */}
|
|
<script
|
|
defer
|
|
src="https://umami-vmyngbmelll45wokwr270wbx.169.239.180.130.sslip.io/script.js"
|
|
data-website-id="86d0bd41-e705-4677-af2d-a4228e7c4b95"
|
|
/>
|
|
</head>
|
|
<body className="min-h-screen">
|
|
<ThemeProvider>
|
|
<header className="sticky top-0 z-30 h-16 bg-white/80 dark:bg-[#0b1014]/80 backdrop-blur border-b border-gray-200 dark:border-white/5">
|
|
<div className="flex items-center gap-3 sm:gap-6 px-4 sm:px-6 h-full">
|
|
<Link href="/" className="flex items-center gap-2 shrink-0">
|
|
<img
|
|
src="/brand/logo.png"
|
|
alt="Mega Plant"
|
|
className="w-9 h-9 rounded-xl object-contain bg-white dark:bg-[#0b1014] shadow-sm"
|
|
/>
|
|
<div className="leading-tight hidden sm:block">
|
|
<div className="text-sm font-semibold text-[#003D5B] dark:text-white">Mega Plant</div>
|
|
<div className="text-[10px] text-gray-500 dark:text-gray-400 uppercase tracking-wider">Overheads 2026</div>
|
|
</div>
|
|
</Link>
|
|
|
|
<GlobalSearch assets={assets.map((a) => ({
|
|
id: a.id, code: a.code, name: a.name, category: a.category, supplier: a.supplier,
|
|
}))} />
|
|
|
|
<div className="flex items-center gap-3 shrink-0">
|
|
<div className="hidden md:flex items-center gap-2 px-3 py-1.5 rounded-xl bg-gradient-to-br from-[#30638E] to-[#003D5B] text-white shadow-lg shadow-blue-900/20">
|
|
<span className="text-[10px] uppercase tracking-wider text-white/70">FY</span>
|
|
<span className="text-sm font-semibold">2026</span>
|
|
</div>
|
|
<ThemeToggle />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="flex">
|
|
<Sidebar items={navItems} />
|
|
<main className="flex-1 min-w-0">{children}</main>
|
|
</div>
|
|
</ThemeProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|