Staff report: API route with stub payload and key validation
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* GET /api/staff/report?org=<id>&key=<secret>
|
||||
*
|
||||
* Returns a comprehensive read-only StaffReportPayload for the given
|
||||
* organization. Field list is built at request time from
|
||||
* CustomField.get (no static config).
|
||||
*
|
||||
* Auth: STAFF_REPORT_KEY must match the `key` query param.
|
||||
*
|
||||
* STUB MODE: if CiviCRM env vars are unset, returns a fabricated payload
|
||||
* exercising every render kind so the staff page is usable in dev.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { isStaffKeyValid } from "@/lib/staff-auth";
|
||||
import type { StaffReportPayload } from "@/types/form";
|
||||
|
||||
function isCiviStubMode(): boolean {
|
||||
return !(
|
||||
process.env.CIVI_BASE_URL &&
|
||||
process.env.CIVI_API_KEY &&
|
||||
process.env.CIVI_SITE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
function buildStubPayload(orgId: number): StaffReportPayload {
|
||||
const today = new Date();
|
||||
const daysAgo = (n: number) =>
|
||||
new Date(today.getTime() - n * 24 * 3600 * 1000).toISOString();
|
||||
const ymd = (n: number) => {
|
||||
const d = new Date(today.getTime() - n * 24 * 3600 * 1000);
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
};
|
||||
return {
|
||||
orgId,
|
||||
orgName: "Sample Co-op (stub)",
|
||||
currentStage: "Organizing",
|
||||
sections: [
|
||||
{
|
||||
groupName: "Food_Co_op_Organizing",
|
||||
groupTitle: "Organization profile",
|
||||
groupKind: "org",
|
||||
fields: [
|
||||
{
|
||||
descriptor: {
|
||||
groupName: "Food_Co_op_Organizing",
|
||||
groupTitle: "Organization profile",
|
||||
groupKind: "org",
|
||||
civiField: "Food_Co_op_Organizing.Date_Incorporated",
|
||||
name: "Date_Incorporated",
|
||||
label: "Date Incorporated",
|
||||
render: "date",
|
||||
},
|
||||
history: [{ activityId: 0, date: "", value: ymd(900) }],
|
||||
},
|
||||
{
|
||||
descriptor: {
|
||||
groupName: "Food_Co_op_Organizing",
|
||||
groupTitle: "Organization profile",
|
||||
groupKind: "org",
|
||||
civiField: "Food_Co_op_Organizing.Equity_share",
|
||||
name: "Equity_share",
|
||||
label: "Equity share (USD)",
|
||||
render: "currency",
|
||||
},
|
||||
history: [{ activityId: 0, date: "", value: 200 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: "Check_in_data__organizing_",
|
||||
groupTitle: "Check-in data (organizing)",
|
||||
groupKind: "activity",
|
||||
fields: [
|
||||
{
|
||||
descriptor: {
|
||||
groupName: "Check_in_data__organizing_",
|
||||
groupTitle: "Check-in data (organizing)",
|
||||
groupKind: "activity",
|
||||
civiField: "Check_in_data__organizing_.Members__current_",
|
||||
name: "Members__current_",
|
||||
label: "Members (current)",
|
||||
render: "number",
|
||||
},
|
||||
history: [
|
||||
{ activityId: 9012, date: daysAgo(3), value: 124 },
|
||||
{ activityId: 9008, date: daysAgo(34), value: 109 },
|
||||
{ activityId: 8995, date: daysAgo(95), value: 87 },
|
||||
],
|
||||
},
|
||||
{
|
||||
descriptor: {
|
||||
groupName: "Check_in_data__organizing_",
|
||||
groupTitle: "Check-in data (organizing)",
|
||||
groupKind: "activity",
|
||||
civiField: "Check_in_data__organizing_.Member_Goal_for_current_Stage",
|
||||
name: "Member_Goal_for_current_Stage",
|
||||
label: "Member goal for current stage",
|
||||
render: "number",
|
||||
},
|
||||
history: [{ activityId: 9008, date: daysAgo(34), value: 200 }],
|
||||
},
|
||||
{
|
||||
descriptor: {
|
||||
groupName: "Check_in_data__organizing_",
|
||||
groupTitle: "Check-in data (organizing)",
|
||||
groupKind: "activity",
|
||||
civiField: "Check_in_data__organizing_.Peer_Group_Participation",
|
||||
name: "Peer_Group_Participation",
|
||||
label: "Peer group participation",
|
||||
render: "select",
|
||||
optionGroupId: 140,
|
||||
},
|
||||
history: [
|
||||
{ activityId: 9012, date: daysAgo(3), value: "Yes" },
|
||||
{ activityId: 9008, date: daysAgo(34), value: "Considering" },
|
||||
],
|
||||
},
|
||||
{
|
||||
descriptor: {
|
||||
groupName: "Check_in_data__organizing_",
|
||||
groupTitle: "Check-in data (organizing)",
|
||||
groupKind: "activity",
|
||||
civiField: "Check_in_data__organizing_.Internal_Note",
|
||||
name: "Internal_Note",
|
||||
label: "Internal note",
|
||||
render: "longtext",
|
||||
},
|
||||
history: [
|
||||
{
|
||||
activityId: 9012,
|
||||
date: daysAgo(3),
|
||||
value:
|
||||
"Strong member momentum this quarter. Need a working group lead before next check-in.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: "Stage_1",
|
||||
groupTitle: "Stage 1",
|
||||
groupKind: "activity",
|
||||
fields: [
|
||||
{
|
||||
descriptor: {
|
||||
groupName: "Stage_1",
|
||||
groupTitle: "Stage 1",
|
||||
groupKind: "activity",
|
||||
civiField: "Stage_1.Vision_Upload",
|
||||
name: "Vision_Upload",
|
||||
label: "Vision — Upload",
|
||||
render: "file",
|
||||
},
|
||||
history: [
|
||||
{
|
||||
activityId: 9012,
|
||||
date: daysAgo(3),
|
||||
value: { id: 4242, file_name: "co-op-vision.pdf" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
activities: [
|
||||
{ id: 9012, date: daysAgo(3), subject: "Co-op Survey (form submission)", submittedBy: "Jane Doe" },
|
||||
{ id: 9008, date: daysAgo(34), subject: "Co-op Survey (form submission)", submittedBy: "Jane Doe" },
|
||||
{ id: 9001, date: daysAgo(62), stage: "Organizing", subject: "Stage transition (staff)" },
|
||||
{ id: 8995, date: daysAgo(95), subject: "Co-op Survey (form submission)", submittedBy: "John Roe" },
|
||||
],
|
||||
options: {
|
||||
140: [
|
||||
{ value: "Yes", label: "Yes" },
|
||||
{ value: "No", label: "No" },
|
||||
{ value: "Considering", label: "Considering" },
|
||||
],
|
||||
75: [
|
||||
{ value: "Inquiry", label: "Inquiry" },
|
||||
{ value: "Organizing", label: "Stage 1 — Convene & Prepare" },
|
||||
{ value: "Feasibility", label: "Stage 2 — Grow & Plan" },
|
||||
{ value: "Business feasibility", label: "Stage 3 — Connect & Gather" },
|
||||
{ value: "Store Implementation", label: "Stage 4 — Excite & Build" },
|
||||
{ value: "Stabilize newly opened co-op", label: "Stage 5 — Fulfill & Stabilize" },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const url = new URL(req.url);
|
||||
const key = url.searchParams.get("key");
|
||||
const orgStr = url.searchParams.get("org");
|
||||
|
||||
if (!isStaffKeyValid(key)) {
|
||||
// Don't leak whether the route exists.
|
||||
return new NextResponse("Not found", { status: 404 });
|
||||
}
|
||||
|
||||
const orgId = Number(orgStr);
|
||||
if (!orgStr || !Number.isFinite(orgId) || orgId <= 0) {
|
||||
return NextResponse.json({ error: "Missing or invalid org id." }, { status: 400 });
|
||||
}
|
||||
|
||||
if (isCiviStubMode()) {
|
||||
return NextResponse.json(buildStubPayload(orgId));
|
||||
}
|
||||
|
||||
// Live Civi branch added in Task 8.
|
||||
return NextResponse.json({ error: "Live mode not yet implemented." }, { status: 501 });
|
||||
}
|
||||
Reference in New Issue
Block a user