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.
86 lines
2.6 KiB
TypeScript
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'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=<civi-org-id></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>
|
|
);
|
|
}
|