MEGAINFRA-WEBAPP/src/app/dashboard/management/retention-view.tsx
DEls 9a6aed50a4 feat(management): add admin planning dashboards
Add turnover, project, retention, and year comparison pages from the supplied workbook, protected by named admin access. Include the local chart gallery, grouped monthly hover, smooth comparison toggle, and monthly income minus cost series.

Require named PocketBase sign-in in every environment and remove the shared PIN bypass. Update access documentation and business-route test fixtures.
2026-10-09 10:16:50 +02:00

24 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import type { ManagementBoard } from "@/lib/management/model";
import { chartColors, RetentionChart } from "./management-charts";
import { compactMoney, dateLabel, Filter, Legend, ManagementShell, money, Panel, Stat } from "./management-ui";
export function RetentionView({ board }: { board: ManagementBoard }) {
const [month, setMonth] = useState("all");
const [query, setQuery] = useState("");
const entries = board.retentionSchedule.flatMap((m) => m.entries.map((entry) => ({ ...entry, date: m.date }))).filter((e) => (month === "all" || e.date === month) && `${e.project} ${e.debtor ?? ""}`.toLowerCase().includes(query.toLowerCase()));
const total = entries.reduce((sum, e) => sum + e.amount, 0);
return <ManagementShell active="retention" title="Retention" description="Follow retained income through the plan and inspect scheduled project releases." board={board}>
<div className="mgmt-stat-grid"><Stat label="Opening retention" value={compactMoney(board.totals.retentionOpening)} note="Carried into the March–February plan" /><Stat label="New retention" value={compactMoney(board.totals.retentionAdded)} note="10% of selected project income" /><Stat label="Planned payouts" value={compactMoney(board.totals.payouts)} note="Payouts in the income planning sheet" /><Stat label="Closing retention" value={compactMoney(board.totals.retentionClosing)} note="Opening + additions − payouts" /></div>
<div className="mgmt-retention-layout"><Panel title="Retention across the plan" subtitle={board.periodLabel} action={<Legend items={[{ label: "Additions", color: chartColors.income }, { label: "Payouts", color: chartColors.cost }, { label: "Balance", color: chartColors.retention }]} />}><RetentionChart months={board.months} /></Panel>
<Panel title="Balance rollforward" subtitle="March–February income planning sheet"><div className="mgmt-retention-bridge"><div><span>Opening balance</span><strong>{money(board.totals.retentionOpening)}</strong></div><div><span>New retention</span><strong>{money(board.totals.retentionAdded)}</strong></div><div><span>Less planned payouts</span><strong>{money(board.totals.payouts)}</strong></div><div><span>Closing balance</span><strong>{money(board.totals.retentionClosing)}</strong></div></div><p className="mgmt-source-note">The release calendar below comes from the separate “Retention 2026” sheet. Its dates and amounts are preserved separately; it is not deducted again from this balance.</p></Panel>
</div>
<Panel title="Monthly retention balances" subtitle="10% applies to the workbook’s selected rows, including months with no payouts"><div className="mgmt-table-scroll"><table className="mgmt-table"><thead><tr><th>Month</th><th>Opening</th><th>Additions</th><th>Payouts</th><th>Closing</th></tr></thead><tbody>{board.months.map((m) => <tr key={m.name}><td>{m.name}</td><td>{money(m.retentionOpening)}</td><td>{money(m.retentionAdded)}</td><td>{money(m.payout)}</td><td className="font-semibold">{money(m.retentionClosing)}</td></tr>)}</tbody></table></div></Panel>
<div className="mb-5 mt-8 flex flex-wrap items-end justify-between gap-3"><div><h2 className="text-lg font-semibold tracking-tight">Scheduled releases</h2><p className="mt-1 text-xs text-muted-foreground">November 2025–October 2026 · Separate payout schedule</p></div><div className="text-right"><p className="text-2xl font-semibold tracking-tight tabular-nums">{compactMoney(board.totals.scheduledPayouts)}</p><p className="mt-1 text-xs text-muted-foreground">19 scheduled project entries</p></div></div>
<div className="mgmt-schedule">{board.retentionSchedule.map((m) => <div key={m.date} className={`mgmt-schedule-month${m.entries.length === 0 ? " is-empty" : ""}`}><div><span>{dateLabel(m.date)}</span><span className="text-muted-foreground">{m.entries.length ? `${m.entries.length} ${m.entries.length === 1 ? "project" : "projects"}` : "—"}</span></div><strong>{m.entries.length === 0 ? "—" : compactMoney(m.total)}</strong><p>{m.entries.length === 0 ? "No release listed" : m.entries.map((e) => e.project).join(" · ")}</p></div>)}</div>
<div className="mgmt-filters"><Filter label="Release month" value={month} onChange={setMonth}><option value="all">All release months</option>{board.retentionSchedule.map((m) => <option key={m.date} value={m.date}>{dateLabel(m.date)}</option>)}</Filter><label className="mgmt-filter mgmt-search"><span>Project or debtor</span><input type="search" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search scheduled releases…" /></label></div>
<Panel title="Project release schedule" subtitle={`${entries.length} entries in view · ${money(total)}`}><div className="mgmt-table-scroll"><table className="mgmt-table"><thead><tr><th>Project</th><th>Debtor / source note</th><th>Release month</th><th>Planned payout</th></tr></thead><tbody>{entries.map((e) => <tr key={e.id}><td>{e.project}</td><td className="mgmt-debtor">{e.debtor ?? "Not supplied"}</td><td>{dateLabel(e.date)}</td><td>{money(e.amount)}</td></tr>)}</tbody><tfoot><tr><td>Total in view</td><td /><td /><td>{money(total)}</td></tr></tfoot></table></div>{entries.length === 0 && <p className="mgmt-empty">No releases match these filters.</p>}</Panel>
</ManagementShell>;
}