diff --git a/app/api/staff/report/route.ts b/app/api/staff/report/route.ts new file mode 100644 index 0000000..88c6661 --- /dev/null +++ b/app/api/staff/report/route.ts @@ -0,0 +1,211 @@ +/** + * GET /api/staff/report?org=&key= + * + * 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 }); +}