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.
24 lines
5.2 KiB
TypeScript
24 lines
5.2 KiB
TypeScript
"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>;
|
||
}
|