Files
WebForm-mw/README.md
2026-05-09 20:08:15 -07:00

169 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 Form Contact** relationship.
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.
```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
- 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.