Joel Brock ba88eb0165 Add /report — read-only activity history view
Mirrors the form's IA and Field Almanac aesthetic; same auth (cid/cs
checksum) so the org owner who can fill the form can also view its
history.

New routes:
- GET /api/report — verifies checksum, resolves the org via the
  Primary Contact relationship, fires Contact.get + Activity.get +
  OptionValue.get in parallel. For every form field with a civiField,
  walks all the org's Check-in (organizing) activities and collects
  every non-empty value into a sorted-DESC history list. Returns
  ReportPayload (orgName, currentStage, activities, fieldHistory,
  options). Has stub-mode payload for env-less local dev.
- /report — page entry; same layout shell (SiteHeader + SiteFooter,
  3xl page width). Eyebrow "Activity report - Co-op organizing".

ReportView component:
- ReportContextHeader: large org name, progress dots + uppercase
  "Current stage" eyebrow + the Civi option *label* on its own line
  at display-font xl/2xl leaf-800 (matches the form's header). Below
  it a 3-up stat band: total check-ins, fields tracked, date span.
- One accordion card per stage section, in stage-rank order. Only
  sections that have at least one field-with-entries render — past,
  current, or "future-with-data" all welcome; truly empty stages stay
  hidden so the page is calm.
- Same journey rail (md+) and mobile stem (md-) with past =
  check-filled-leaf, current = filled-leaf-with-ring, future = dashed
  hollow ring; solid leaf line vs dashed muted between markers.
- Within each card: divide-y rows. Field label and help on the left,
  most-recent value on the right in display-font lg leaf-800, dated
  beneath with an "{N} earlier entries" disclosure that expands a
  small vertical timeline (date on left, value on right).
- FormattedValue handles currency (Intl), percent, number (tabular
  nums), date (long, timezone-safe for YYYY-MM-DD), boolean (Yes/No),
  select/readonly (resolved via option group), multiselect (handles
  array or delimited string), file (filename), text-like (as-is).
- Loading / empty / error states match the form's treatments.

types/form.ts: new FieldHistoryEntry, ActivitySummary, ReportPayload.
The fieldHistory map keys by FieldConfig.name and only includes fields
that have at least one non-empty entry.
2026-05-13 12:33:26 -07:00

Co-op Check-in (WebForm-mw)

Standalone Next.js middleware that lets external co-op contacts update their organization's tracking data on CiviCRM, sidestepping the limitations of Webform CiviCRM's admin UI.

This app is the v2 delivery path described in ../docs/superpowers/specs/2026-05-08-civi-webform-design.md.

What it does

A form-filler clicks a tokenized link in an email:

https://check-in.fci.coop/?cid=<contactId>&cs=<checksum>

The app:

  1. Verifies the checksum against CiviCRM.
  2. Resolves the org from the contact's Primary Contact relationship (Individual → Organization). Configurable via FORM_CONTACT_RELATIONSHIP in config/form.ts.
  3. Reads the org's Framework Stage (Food_Co_op_Organizing.Stage) — text-keyed values like Inquiry, Organizing, Feasibility, Business feasibility, Store Implementation, Stabilize newly opened co-op.
  4. Walks the org's past Org Engagement Submission activities DESC and assembles per-field-most-recent prefill values (the exact behaviour the WCM admin UI cannot express).
  5. Renders the form with stage-conditional sections — Stage 0 (Inquiry / core check-in fields, ~32 of them) is always visible; Stages 15 each appear when current_stage is in their visibility set.
  6. On submit, creates a new immutable Org Engagement Submission activity with the visible-field values plus a stage_at_submission audit field.

Tech stack

  • Next.js 16 (App Router) + React 19 + TypeScript
  • Tailwind CSS 4 (CSS-based @theme config)
  • react-hook-form for state + validation
  • axios (only used in earlier scaffolding; this version uses native fetch)

Project structure

app/
  layout.tsx         Root layout, fonts (Inter + Source Serif 4)
  page.tsx           Public form entry; reads cid+cs from URL
  globals.css        Tailwind 4 + theme tokens (leaf + stone palettes)
  api/
    data/route.ts    GET form data + per-field-most-recent prefill
    submit/route.ts  POST submission → creates new Civi activity

components/
  EngagementForm.tsx Top-level orchestrator
  StageSection.tsx   Collapsible accordion card per stage section
  SiteChrome.tsx     Header + footer chrome (logo placeholder included)
  fields/
    FieldRenderer.tsx One renderer for all 12 field types

config/
  form.ts            The 123-field form definition

lib/
  civicrm.ts         APIv4 client (Bearer-style auth)
  conditional.ts     Visibility rule engine
  prefill.ts         Per-field-most-recent walk

types/
  form.ts            FormConfig, FieldConfig, VisibilityRule, etc.

Configuration

The app falls back to STUB MODE with hardcoded mock data when any of these environment variables are missing. The UI renders fully and the form is interactive, but no CiviCRM calls are made.

# .env.local
CIVI_BASE_URL=https://crm.fci.coop
CIVI_API_KEY=<a Civi user's API key>
CIVI_SITE_KEY=<the site_key from civicrm.settings.php>

The exact auth header strategy depends on which CiviCRM auth extension is active. The default in lib/civicrm.ts uses the AuthX-style Bearer header for the API key plus the legacy X-Civi-Key for the site key. If your instance uses a different scheme (e.g. classic APIv3 api_key+key query params), adjust lib/civicrm.ts accordingly.

Custom-field references (CiviCRM APIv4)

Form fields write to CiviCRM custom fields via the civiField property. The convention is APIv4's <group_name>.<field_name> format. For now, config/form.ts uses placeholder custom_<form_field_name> references for all but the Framework Stage. Before going live, replace each civiField value with the actual APIv4 reference for that custom field on the Org Engagement Submission activity type.

To inventory the actual field names, query APIv4 from the Civi UI:

/civicrm/api4#/explorer
  → CustomField.get
    select: ["name", "label", "custom_group_id:name"]
    where: [["custom_group_id:name", "IN", ["Stage_0_Inquiry", "Stage_1_*", ...]]]

Running locally

npm install
npm run dev
# open http://localhost:3000/?cid=anything&cs=anything (any non-empty cs while in stub mode)

Building

npm run build

Deploying

The simplest deployment is Vercel. The app is a standard App Router project:

vercel

Set the three CiviCRM env vars in the Vercel project settings; the app auto-leaves stub mode the moment all three are present.

For self-hosting, any Node 20+ environment supporting Next.js 16 will work:

npm run build && npm start

Accessibility

  • All inputs have proper <label htmlFor> associations.
  • Help text and error messages are linked via aria-describedby.
  • Errors set aria-invalid and use role="alert" for assistive tech.
  • Required fields use aria-required plus a visible * (with aria-label="required").
  • Sections use semantic <section> + <h2> + aria-controls accordion pattern.
  • Keyboard-only flow works end-to-end.
  • Color contrast meets WCAG AA against the leaf + stone palette (verified visually; an audit pass is recommended before launch).
  • A skip-to-content link is present for keyboard users.

Conditional logic

Rules live alongside fields and sections in config/form.ts. The engine (lib/conditional.ts) supports:

  • equals, notEquals, in, notIn, isEmpty, isNotEmpty
  • Composition via {all: [...]} (AND) and {any: [...]} (OR)

Section visibility re-evaluates live on every form change. Hidden fields' values are stripped from the submitted payload, so users never accidentally write data they couldn't see.

What's not in scope for this build

  • Browser-based form builder. Forms are configured by editing config/form.ts. An admin UI for non-developers to build forms is a follow-on project.
  • File upload to a storage backend. File fields render correctly but the Civi-side upload pipeline is a stub. Wire up to S3/R2 + CiviCRM's File entity when ready.
  • CSRF protection on the submit endpoint. Checksum gating is the current trust mechanism. Add CSRF tokens if you put this behind a long-lived session.
  • i18n. Copy is English-only.
Description
No description provided
Readme 215 KiB
Languages
TypeScript 96.3%
CSS 3.4%
JavaScript 0.3%