"use client"; import { useEffect, useId, useMemo, useState } from "react"; import type { FieldConfig, FieldHistoryEntry, FormConfig, ReportPayload, SelectOption, StageSectionConfig, } from "@/types/form"; import { STAGE_OPTION_GROUP_ID } from "@/config/form"; import { StageIcon } from "./StageIcon"; import { FieldHistoryGroup, FieldHistoryRow, formatShortDate, computeDateRange, Chevron, } from "./report/FieldHistory"; import { DateTimeline } from "./report/DateTimeline"; import { MembershipChart, MEMBERS_ACTUAL_NAME, MEMBERS_GOAL_NAME, } from "./report/MembershipChart"; import { LoadingState, EmptyState, ErrorState } from "./report/ReportStates"; interface ReportViewProps { config: FormConfig; cid: string; cs: string; } type LoadState = | { kind: "loading" } | { kind: "error"; message: string } | { kind: "ready"; data: ReportPayload }; type PathwayState = "past" | "current" | "future"; const STAGE_RANK: Record = { Inquiry: 0, Organizing: 1, Feasibility: 2, "Business feasibility": 3, "Store Implementation": 4, "Stabilize newly opened co-op": 5, }; export function ReportView({ config, cid, cs }: ReportViewProps) { const [load, setLoad] = useState({ kind: "loading" }); useEffect(() => { let cancelled = false; async function go() { try { const url = new URL("/api/report", window.location.origin); url.searchParams.set("cid", cid); url.searchParams.set("cs", cs); const res = await fetch(url.toString(), { cache: "no-store" }); if (!res.ok) { const text = await res.text(); let message = `Could not load report (HTTP ${res.status}).`; try { const j = JSON.parse(text) as { error?: string }; if (j.error) message = j.error; } catch { /* default */ } if (!cancelled) setLoad({ kind: "error", message }); return; } const data: ReportPayload = await res.json(); if (!cancelled) setLoad({ kind: "ready", data }); } catch (e) { if (!cancelled) { setLoad({ kind: "error", message: e instanceof Error ? e.message : "Unexpected error loading report.", }); } } } void go(); return () => { cancelled = true; }; }, [cid, cs]); const sectionsToRender = useMemo(() => { if (load.kind !== "ready") return []; const { currentStage, fieldHistory } = load.data; const currentRank = STAGE_RANK[currentStage] ?? 0; return config.sections .map((section) => { const fieldsWithHistory = section.fields.filter( (f) => fieldHistory[f.name] && fieldHistory[f.name].length > 0, ); const pathwayState: PathwayState = section.rank < currentRank ? "past" : section.rank === currentRank ? "current" : "future"; return { section, fieldsWithHistory, pathwayState }; }) .filter((e) => e.fieldsWithHistory.length > 0); }, [load, config.sections]); if (load.kind === "loading") return ; if (load.kind === "error") return ; const { data } = load; const stageOpts = data.options?.[STAGE_OPTION_GROUP_ID] ?? []; const currentStageLabel = stageOpts.find((o) => o.value === data.currentStage)?.label ?? data.currentStage; const currentRank = STAGE_RANK[data.currentStage] ?? 0; const dateRange = computeDateRange(data.activities.map((a) => a.date)); const totalActivities = data.activities.length; const totalFieldsTracked = Object.keys(data.fieldHistory).length; return (
{sectionsToRender.length === 0 ? ( ) : (
    {sectionsToRender.map((entry, i) => { const { section, pathwayState, fieldsWithHistory } = entry; const nextState = i < sectionsToRender.length - 1 ? sectionsToRender[i + 1].pathwayState : undefined; return (
  1. 0} state={pathwayState} />
  2. ); })}
)}
); } function ReportContextHeader({ orgName, currentStageLabel, currentRank, totalActivities, totalFieldsTracked, dateRange, }: { orgName: string; currentStageLabel: string; currentRank: number; totalActivities: number; totalFieldsTracked: number; dateRange: { from: string; to: string } | null; }) { return (

Report for

{orgName}

Current stage

{currentStageLabel || "—"}

); } function Stat({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); } function StageProgress({ currentRank }: { currentRank: number }) { return (
{[0, 1, 2, 3, 4, 5].map((r) => ( ))}
); } function ReportSection({ section, fields, history, options, isCurrent, defaultOpen, }: { section: StageSectionConfig; fields: FieldConfig[]; history: Record; options: Record; isCurrent: boolean; defaultOpen: boolean; }) { const [open, setOpen] = useState(defaultOpen); const headingId = useId(); const panelId = useId(); const cardClass = isCurrent ? "border-2 border-leaf-600 shadow-[0_1px_0_0_rgba(0,0,0,0.04),0_8px_24px_-12px_rgba(60,80,40,0.18)] bg-white/95" : "border border-rule bg-white/95 shadow-sm"; return (

); } /** * Mirrors the form's FieldGroupCard: a leaf-tinted left rule + small * uppercase mini-label, with the grouped history rows stacked beneath * and separated by the same divide-y as the standalone rows. Eats only * ~14px of horizontal space (vs ~40px for a fully-boxed treatment). */ function StageRankMark({ rank, isCurrent }: { rank: number; isCurrent: boolean }) { return ( {rank} ); } function RailMarker({ rank, state, nextState, }: { rank: number; state: PathwayState; nextState?: PathwayState; }) { return (
{nextState && ( )}
); } function MarkerCircle({ rank, state }: { rank: number; state: PathwayState }) { if (state === "past") { return ( Stage {rank} (past) ); } if (state === "current") { return ( {rank} Stage {rank} (current) ); } return ( {rank} Stage {rank} (no entries) ); } function MobileStem({ show, state }: { show: boolean; state: PathwayState }) { if (!show) return null; return (
); }