Implements the wiki "Reports Mockup" as a Reports → Purchasing sidebar section, wired to real approved-PO spend. Two report families, each index → drill/detail: - Cost Centres (/reports/cost-centres) — spend compared across vessels; row opens a cost-centre report with a Top-accounting-codes breakdown re-pivotable by tier (Heading/Sub/Leaf) + Top-N. - Accounting Codes (/reports/accounting-codes) — drills the Account tree (headings → sub → leaves) via ?parent=; a leaf opens its report broken down by cost centre (or, for a non-leaf, by sub-account). Shared: a pinned filter toolbar (Granularity Monthly/Yearly, Financial Year, Show Top5/Top10/Bottom5/All) whose values live in the URL query so the server component re-renders — no client fetching. KPI tiles, recharts comparison/trend/ breakdown charts, per-row trend sparklines, and CSV export (/api/reports/spend). - lib/reports.ts: the pure, unit-tested aggregation core. Spend = a PO once it reaches POST_APPROVAL_STATUSES, dated by approvedAt, valued at totalAmount (the dashboard's basis); Indian Apr–Mar FY; each PO's leaf accountId rolled up to parents. One query in getReportDataset(), everything else pure. - Sidebar: new collapsible "Reports" section with a "Purchasing" subheading (subgroup support added to the Section model). Gated by view_analytics (Manager/SuperUser/Auditor/Admin); export by the same. Deferred (documented): synthetic Weekly granularity, the "Add to graph" custom multi-select, and line-item-level account allocation (v1 uses the PO-level account). Sites are not cost centres — only vessels. Tests: 11 unit cases for the aggregation core + 3 sidebar cases for the Reports section. Full unit suite 303 green; tsc clean. Smoke-tested all routes end to end against seed data (index/drill/detail/export 200; non-analytics role 307/403). Wiki: "Reports Mockup" marked implemented; "Pages and Navigation" lists the new routes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
2.2 KiB
TypeScript
68 lines
2.2 KiB
TypeScript
import Link from "next/link";
|
|
import { ChevronRight } from "lucide-react";
|
|
|
|
// Reports breadcrumb: always rooted at "Reports", then the section and any
|
|
// drill/detail crumbs. A crumb with an href is a link; the last is the current.
|
|
export function ReportBreadcrumb({ trail }: { trail: { label: string; href?: string }[] }) {
|
|
return (
|
|
<nav className="mb-4 flex flex-wrap items-center gap-2 text-sm text-neutral-500">
|
|
<span>Reports</span>
|
|
{trail.map((t, i) => (
|
|
<span key={i} className="flex items-center gap-2">
|
|
<ChevronRight className="h-3.5 w-3.5 shrink-0" />
|
|
{t.href ? (
|
|
<Link href={t.href} className="hover:text-neutral-800">{t.label}</Link>
|
|
) : (
|
|
<span className="font-medium text-neutral-900">{t.label}</span>
|
|
)}
|
|
</span>
|
|
))}
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
// Server-rendered segmented control: each option is a link that re-renders the
|
|
// page with the new value in the query string (used for tier / break-down / top-N).
|
|
export function SegLink({
|
|
label,
|
|
options,
|
|
current,
|
|
hrefFor,
|
|
}: {
|
|
label: string;
|
|
options: { value: string; label: string }[];
|
|
current: string;
|
|
hrefFor: (v: string) => string;
|
|
}) {
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-neutral-400">{label}</span>
|
|
<div className="inline-flex rounded-lg border border-neutral-200 bg-white p-0.5 text-xs">
|
|
{options.map((o) => (
|
|
<Link
|
|
key={o.value}
|
|
href={hrefFor(o.value)}
|
|
className={
|
|
"rounded-md px-2.5 py-1 font-medium " +
|
|
(o.value === current ? "bg-primary-600 text-white" : "text-neutral-500 hover:text-neutral-800")
|
|
}
|
|
>
|
|
{o.label}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ReportTitle({ title, subtitle, badge }: { title: string; subtitle?: string; badge?: React.ReactNode }) {
|
|
return (
|
|
<div className="mb-6">
|
|
<div className="flex items-center gap-3">
|
|
<h1 className="text-2xl font-semibold text-neutral-900">{title}</h1>
|
|
{badge}
|
|
</div>
|
|
{subtitle && <p className="mt-1 text-sm text-neutral-500">{subtitle}</p>}
|
|
</div>
|
|
);
|
|
}
|