Files
WebForm-mw/app/staff/report/page.tsx
T
Joel Brock b548b6425b Staff report: compact rows, anchor nav, Civi file links, Y1 matrix
UX iteration after first live look:
- Sticky anchor strip below the header with a chip per section (incl.
  Submissions) so staff can jump around a long page.
- Compact one-line rows that show only the latest value; multi-history
  fields get a muted 'N earlier entries' toggle that reveals the rest
  inline. Same affordance for file fields.
- Empty fields collapse under a single 'N empty fields' toggle per
  section instead of taking a row each.
- Stage 5: Y1_Q<n>_<metric> fields render as a read-only matrix table
  (rows: metrics; columns: Q1..Q4) matching the form's matrix layout.

File proxy (/api/staff/file) deleted. APIv4 Attachment isn't exposed
on this Civi instance (per the June upload spike), which is why the
previous proxy returned broken images. Staff are already authenticated
to Civi when they arrive here, so file fields now render as outbound
links to CIVI_BASE_URL/civicrm/file?reset=1&id=<id> and the browser
uses the staff session. No more proxy auth, no more SSRF surface to
harden, no broken images.

CIVI_BASE_URL flows from the staff page (server component) into the
client as a prop. No secret material crosses the boundary.
2026-06-05 17:28:17 -07:00

86 lines
2.6 KiB
TypeScript

import { Suspense } from "react";
import { StaffReportView } from "@/components/StaffReportView";
import { SiteHeader, SiteFooter } from "@/components/SiteChrome";
import { isStaffKeyValid } from "@/lib/staff-auth";
interface PageProps {
searchParams: Promise<{ org?: string; key?: string }>;
}
export const metadata = {
title: "Staff report — Food Co-op Initiative",
robots: { index: false, follow: false },
};
export default async function StaffReportPage({ searchParams }: PageProps) {
const { org, key } = await searchParams;
// Generic "not found" if the key is missing or wrong — don't confirm
// route existence.
if (!isStaffKeyValid(key)) {
return <NotFound />;
}
const orgId = Number(org);
const orgValid = !!org && Number.isFinite(orgId) && orgId > 0;
// CIVI_BASE_URL flows from server config to client only as a base for
// outbound file links. No secret material is exposed.
const civiBaseUrl = process.env.CIVI_BASE_URL ?? "";
return (
<>
<a
href="#main"
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded focus:bg-paper focus:px-3 focus:py-2 focus:text-ink focus:shadow"
>
Skip to content
</a>
<SiteHeader />
<main id="main" className="flex-1">
<div className="mx-auto max-w-5xl px-4 py-10 sm:px-6 sm:py-14">
{orgValid ? (
<Suspense fallback={null}>
<StaffReportView org={orgId} authKey={key!} civiBaseUrl={civiBaseUrl} />
</Suspense>
) : (
<MissingOrg />
)}
</div>
</main>
<SiteFooter />
</>
);
}
function NotFound() {
return (
<>
<SiteHeader />
<main className="flex-1">
<div className="mx-auto max-w-2xl px-4 py-20 text-center">
<h1 className="font-display text-3xl text-ink">Not found</h1>
<p className="mt-3 text-ink-soft">
The page you requested doesn&apos;t exist.
</p>
</div>
</main>
<SiteFooter />
</>
);
}
function MissingOrg() {
return (
<div role="alert" className="rounded-lg border-2 border-clay-200 bg-clay-100/30 px-6 py-7">
<h2 className="font-display text-xl font-medium text-clay-700">
Missing or invalid org id.
</h2>
<p className="mt-3 leading-relaxed text-ink-soft">
Add <code className="font-mono">?org=&lt;civi-org-id&gt;</code> to the URL. The org
id is the Civi Contact id of the organization (visible in the URL when
viewing the org in CiviCRM).
</p>
</div>
);
}