/** * 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 }); }