169 lines
6.0 KiB
Markdown
169 lines
6.0 KiB
Markdown
# 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 1–5 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.
|