pelagia-portal/App/components/reports/report-header.tsx
Hardik 47ac2c7813
All checks were successful
PR checks / checks (pull_request) Successful in 48s
PR checks / integration (pull_request) Successful in 31s
feat(reports): weekly granularity, custom compare, line-item allocation
Picks up the three pieces deferred from the initial reports PR:

#3 Line-item account allocation — allocatePoSpend() splits each PO across the
accounting codes its line items carry (line accountId, falling back to the
PO-level account), proportionally so per-PO rows sum back to totalAmount. The
accounting-code report now attributes multi-account POs correctly. SpendRow
gains poId; poCount is now distinct POs, not row count.

#2 Custom "Add to graph" — tick rows on either index (SelectCheckbox links
write ?sel=id1,id2), then "Compare selected" (?cmp=1) shows a custom comparison
of just those entities. Fully server-rendered + shareable; export honours sel.

#1 Weekly granularity — a third Granularity that focuses one FY month and
buckets spend by week-of-month (W1–W5) from approvedAt, with a Month picker in
the toolbar. Real buckets (not the mockup's synthetic split).

All three are URL-driven like the rest, so no client fetching. Charts/KPIs/
detail trends all branch on the new mode.

Tests: +8 unit cases (allocation proportional/fallback/empty, weekly buckets,
sel parse/toggle, month + granularity parsing); fixture updated for poId/week.
Full unit suite 311 green; tsc clean. Smoke-tested weekly + custom-compare +
exports end-to-end (all 200). Docs + wiki updated to mark them implemented.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 11:25:05 +05:30

103 lines
3.7 KiB
TypeScript

import Link from "next/link";
import { ChevronRight, Check } 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>
);
}
// A checkbox rendered as a navigation link — toggles this row's id in the
// `?sel=` custom-comparison selection (keeps the report fully server-rendered).
export function SelectCheckbox({ checked, href, title }: { checked: boolean; href: string; title?: string }) {
return (
<Link
href={href}
title={title ?? "Select to graph"}
scroll={false}
className={
"flex h-4 w-4 shrink-0 items-center justify-center rounded border " +
(checked ? "border-primary-600 bg-primary-600 text-white" : "border-neutral-300 bg-white hover:border-primary-500")
}
>
{checked && <Check className="h-3 w-3" />}
</Link>
);
}
// Sticky banner shown while rows are selected: jump to the custom comparison or clear.
export function CompareBar({ count, compareHref, clearHref }: { count: number; compareHref: string; clearHref: string }) {
return (
<div className="mb-4 flex items-center justify-between rounded-lg border border-primary-200 bg-primary-50 px-4 py-2.5">
<span className="text-sm font-medium text-primary-800">{count} selected</span>
<div className="flex items-center gap-2">
<Link href={compareHref} className="rounded-lg bg-primary-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-primary-700">
Compare selected
</Link>
<Link href={clearHref} className="rounded-lg border border-neutral-300 bg-white px-3 py-1.5 text-sm font-medium text-neutral-600 hover:bg-neutral-50">
Clear
</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>
);
}