README: refresh for current architecture, drop stale Framework Stage / mock-field notes

Trimmed the in-the-weeds tech tour (file tree, lib names, custom-field
inventory tutorial) and replaced with a brief functional overview, the
visual/UX highlights that survived the polish passes, the accessibility
features, an env-var table, and the minimum to run/deploy. Outdated
material removed: org-side Framework Stage as authority, 'Org Engagement
Submission' activity type, stage_at_submission write, placeholder
custom_<name> civiField refs, Inter+SourceSerif typography, leaf+stone
palette. Documents stage-from-activity, /report, locked future stages,
draft auto-save, success destination, HTTP-Basic-Auth proxy env vars,
HEALTH_TOKEN, and PREVIEW_ADMIN_TOKEN.
This commit is contained in:
Joel Brock
2026-05-13 13:19:11 -07:00
parent ba88eb0165
commit 899dad2323

266
README.md
View File

@@ -1,168 +1,136 @@
# Co-op Check-in (WebForm-mw) # Co-op Check-in
Standalone Next.js middleware that lets external co-op contacts update their A tokenized, mobile-friendly web form that lets external co-op contacts
organization's tracking data on CiviCRM, sidestepping the limitations of update their organization's tracking data on CiviCRM, plus a read-only
Webform CiviCRM's admin UI. activity report showing every value the co-op has shared over time. Built
to wrap CiviCRM's existing custom-field schema without modifying it.
This app is the **v2** delivery path described in ## How it works
`../docs/superpowers/specs/2026-05-08-civi-webform-design.md`.
## What it does Each co-op has a designated **Primary Contact** (the individual) linked
to the **Organization** record in CiviCRM. Staff send that contact a
personalized link generated against the contact's CiviCRM checksum:
A form-filler clicks a tokenized link in an email: - `https://check-in.fci.coop/?cid=<contactId>&cs=<checksum>` — the form
- `https://check-in.fci.coop/report?cid=<contactId>&cs=<checksum>` — the report
``` When the link is opened, the app verifies the checksum against CiviCRM,
https://check-in.fci.coop/?cid=<contactId>&cs=<checksum> resolves the organization through the Primary Contact relationship, and
``` loads the right view.
The app: **Form.** Sections are organized by the co-op development stages
(Inquiry → Convene & Prepare → Grow & Plan → Connect & Gather → Excite &
Build → Fulfill & Stabilize). The org's current stage controls which
sections are editable; past and current stages are open for editing,
future stages render as previews with their fields locked so the co-op
can see the framework ahead. Fields prefill with each value's most
recent non-empty entry from past check-ins. On submit, a new
"Check-in (organizing)" activity is created; nothing is overwritten.
1. Verifies the checksum against CiviCRM. **Stage authority.** The current stage is derived from the most recent
2. Resolves the org from the contact's **Primary Contact** relationship (Individual → Organization). Configurable via `FORM_CONTACT_RELATIONSHIP` in `config/form.ts`. "Check-in (organizing)" activity whose Stage field is set. Staff own
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`. stage transitions by manually setting Stage on a check-in activity they
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). create; the form itself never writes Stage, so org self-submissions
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. can't override a staff transition. Orgs with no stage-bearing activity
6. On submit, creates a **new immutable** `Org Engagement Submission` activity with the visible-field values plus a `stage_at_submission` audit field. default to "Inquiry."
## Tech stack **Report.** A read-only view of every field that has ever held a value,
grouped by stage. Each row shows the most recent value prominently; an
"N earlier entries" disclosure expands a chronological list of prior
values with their dates. Empty stages and untouched fields are hidden so
the page stays calm.
- Next.js 16 (App Router) + React 19 + TypeScript ## Visual & UX details
- 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 - **Field Almanac** aesthetic: warm cream paper, deep botanical green
ink, a sparing terracotta accent. Fraunces (display) and DM Sans
``` (body) at variable weights. Subtle paper-grain texture via layered
app/ CSS gradients.
layout.tsx Root layout, fonts (Inter + Source Serif 4) - **Journey rail** runs down the left side on desktop, with a marker
page.tsx Public form entry; reads cid+cs from URL per stage and a "Now" pill at the current stage. Past stages are
globals.css Tailwind 4 + theme tokens (leaf + stone palettes) filled with a checkmark, future stages are dashed and locked. A small
api/ inter-card stem stands in for the rail on mobile.
data/route.ts GET form data + per-field-most-recent prefill - **Draft auto-save** to `localStorage` (30-day TTL) so partial answers
submit/route.ts POST submission → creates new Civi activity survive page refreshes; a "Draft restored" banner appears on return.
- **Successful submit** lands on a destination screen instead of a
components/ reloaded form, preventing accidental duplicate submits from
EngagementForm.tsx Top-level orchestrator back-button or autofill replay.
StageSection.tsx Collapsible accordion card per stage section - **Currency live preview**, date min/max bounds (19002100), and
SiteChrome.tsx Header + footer chrome (logo placeholder included) thousands-separator formatting on numeric fields.
fields/ - **Hand-drawn stage icons** for each of the six stages; FCI brand logo
FieldRenderer.tsx One renderer for all 12 field types in the header.
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.
```bash
# .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
```bash
npm install
npm run dev
# open http://localhost:3000/?cid=anything&cs=anything (any non-empty cs while in stub mode)
```
## Building
```bash
npm run build
```
## Deploying
The simplest deployment is Vercel. The app is a standard App Router project:
```bash
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:
```bash
npm run build && npm start
```
## Accessibility ## Accessibility
- All inputs have proper `<label htmlFor>` associations. - Real `<label htmlFor>` on every control; help text and inline errors
- Help text and error messages are linked via `aria-describedby`. linked via `aria-describedby`.
- Errors set `aria-invalid` and use `role="alert"` for assistive tech. - Required fields use `aria-required`, a visible `*` with
- Required fields use `aria-required` plus a visible `*` (with `aria-label="required"`, and typed error messages.
`aria-label="required"`). - Failed-validation submit auto-expands the section containing the
- Sections use semantic `<section>` + `<h2>` + `aria-controls` accordion pattern. first error, scrolls it into view, and focuses the field.
- Keyboard-only flow works end-to-end. - Accordion sections follow the disclosure pattern: `<button>` headers
- Color contrast meets WCAG AA against the leaf + stone palette (verified with `aria-expanded` + `aria-controls`, `role="region"` panels.
visually; an audit pass is recommended before launch). - All animations honour `prefers-reduced-motion`.
- A skip-to-content link is present for keyboard users. - Skip-to-content link present for keyboard users.
- iOS safe-area inset respected on the sticky submit bar.
## Conditional logic ## Environment variables
Rules live alongside fields and sections in `config/form.ts`. The engine The app runs in **stub mode** when any of the CiviCRM variables below
(`lib/conditional.ts`) supports: are missing — the UI is fully exercisable against synthesized data with
no live CRM calls. Production startup will refuse to boot in stub mode.
- `equals`, `notEquals`, `in`, `notIn`, `isEmpty`, `isNotEmpty` | Variable | Required? | Purpose |
- Composition via `{all: [...]}` (AND) and `{any: [...]}` (OR) |---|---|---|
| `CIVI_BASE_URL` | yes | CiviCRM root, e.g. `https://crm.fci.coop` |
| `CIVI_API_KEY` | yes | API key of a Civi user with permission to read contacts/activities and create activities |
| `CIVI_SITE_KEY` | yes | The `site_key` from `civicrm.settings.php` |
| `CIVI_HTTP_AUTH_USER` | optional | Username, if CiviCRM sits behind HTTP Basic Auth at the webserver layer |
| `CIVI_HTTP_AUTH_PASS` | optional | Password for the above |
| `HEALTH_TOKEN` | optional | If set, `/api/health` requires `?token=…` in production |
| `PREVIEW_ADMIN_TOKEN` | optional | If set, `/api/preview-link` requires a matching `Authorization: Bearer …` header (admin convenience endpoint for minting form links) |
| `NODE_ENV` | — | Set to `production` for the production build |
Section visibility re-evaluates live on every form change. Hidden fields' Copy `.env.example` to `.env.local` for local development.
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 ## Run locally
- **Browser-based form builder.** Forms are configured by editing ```
`config/form.ts`. An admin UI for non-developers to build forms is a npm install
follow-on project. npm run dev
- **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. Open `http://localhost:3000/?cid=1&cs=anything` — any non-empty `cs`
- **CSRF protection on the submit endpoint.** Checksum gating is the works while in stub mode. The report is at `/report?cid=1&cs=anything`.
current trust mechanism. Add CSRF tokens if you put this behind a
long-lived session. ## Deploy
- **i18n.** Copy is English-only.
The repo ships with `render.yaml` for [Render](https://render.com) and a
detailed `DEPLOYMENT.md` covering Render specifically. The app is a
standard Next.js 16 App Router project and runs anywhere Node 20+ can
run `next start` — Vercel, AWS Amplify Hosting, App Runner, Fly,
self-hosted, etc.
Build + start:
```
npm run build
npm start
```
Set all three required CiviCRM variables in the host's secret store;
the app auto-leaves stub mode the moment they're all present.
A `/healthz` route returns a lightweight 200 for platform health checks;
`/api/health` returns a richer diagnostic payload (gated by
`HEALTH_TOKEN` in production).
## Editing the form
The form definition lives in `config/form.ts` — one TypeScript file
declaring sections, fields, types, visibility rules, and matrix groups
(used for compact M1M12 / Q1Q4 layouts in Stage 5). Each field
references its CiviCRM custom field via APIv4's `<group_name>.<field_name>`
syntax. Edit the file, restart `npm run dev`, you're done.
A browser-based form builder is out of scope for this version.