Build standalone CiviCRM check-in middleware

This commit is contained in:
Joel Brock
2026-05-09 20:08:15 -07:00
parent 0899e6ae9a
commit 54555c74d2
21 changed files with 1894 additions and 457 deletions

176
README.md
View File

@@ -1,36 +1,168 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Co-op Check-in (WebForm-mw)
## Getting Started
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.
First, run the development server:
This app is the **v2** delivery path described in
`../docs/superpowers/specs/2026-05-08-civi-webform-design.md`.
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
## What it does
A form-filler clicks a tokenized link in an email:
```
https://check-in.fci.coop/?cid=<contactId>&cs=<checksum>
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
The app:
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
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.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Tech stack
## Learn More
- 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`)
To learn more about Next.js, take a look at the following resources:
## Project structure
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
```
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
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
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
## Deploy on Vercel
config/
form.ts The 123-field form definition
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
lib/
civicrm.ts APIv4 client (Bearer-style auth)
conditional.ts Visibility rule engine
prefill.ts Per-field-most-recent walk
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
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.