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:
266
README.md
266
README.md
@@ -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
|
||||
organization's tracking data on CiviCRM, sidestepping the limitations of
|
||||
Webform CiviCRM's admin UI.
|
||||
A tokenized, mobile-friendly web form that lets external co-op contacts
|
||||
update their organization's tracking data on CiviCRM, plus a read-only
|
||||
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
|
||||
`../docs/superpowers/specs/2026-05-08-civi-webform-design.md`.
|
||||
## How it works
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
https://check-in.fci.coop/?cid=<contactId>&cs=<checksum>
|
||||
```
|
||||
When the link is opened, the app verifies the checksum against CiviCRM,
|
||||
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.
|
||||
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.
|
||||
**Stage authority.** The current stage is derived from the most recent
|
||||
"Check-in (organizing)" activity whose Stage field is set. Staff own
|
||||
stage transitions by manually setting Stage on a check-in activity they
|
||||
create; the form itself never writes Stage, so org self-submissions
|
||||
can't override a staff transition. Orgs with no stage-bearing activity
|
||||
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
|
||||
- Tailwind CSS 4 (CSS-based `@theme` config)
|
||||
- `react-hook-form` for state + validation
|
||||
- `axios` (only used in earlier scaffolding; this version uses native `fetch`)
|
||||
## Visual & UX details
|
||||
|
||||
## 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
|
||||
```
|
||||
- **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
|
||||
CSS gradients.
|
||||
- **Journey rail** runs down the left side on desktop, with a marker
|
||||
per stage and a "Now" pill at the current stage. Past stages are
|
||||
filled with a checkmark, future stages are dashed and locked. A small
|
||||
inter-card stem stands in for the rail on mobile.
|
||||
- **Draft auto-save** to `localStorage` (30-day TTL) so partial answers
|
||||
survive page refreshes; a "Draft restored" banner appears on return.
|
||||
- **Successful submit** lands on a destination screen instead of a
|
||||
reloaded form, preventing accidental duplicate submits from
|
||||
back-button or autofill replay.
|
||||
- **Currency live preview**, date min/max bounds (1900–2100), and
|
||||
thousands-separator formatting on numeric fields.
|
||||
- **Hand-drawn stage icons** for each of the six stages; FCI brand logo
|
||||
in the header.
|
||||
|
||||
## 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.
|
||||
- Real `<label htmlFor>` on every control; help text and inline errors
|
||||
linked via `aria-describedby`.
|
||||
- Required fields use `aria-required`, a visible `*` with
|
||||
`aria-label="required"`, and typed error messages.
|
||||
- Failed-validation submit auto-expands the section containing the
|
||||
first error, scrolls it into view, and focuses the field.
|
||||
- Accordion sections follow the disclosure pattern: `<button>` headers
|
||||
with `aria-expanded` + `aria-controls`, `role="region"` panels.
|
||||
- All animations honour `prefers-reduced-motion`.
|
||||
- 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
|
||||
(`lib/conditional.ts`) supports:
|
||||
The app runs in **stub mode** when any of the CiviCRM variables below
|
||||
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`
|
||||
- Composition via `{all: [...]}` (AND) and `{any: [...]}` (OR)
|
||||
| Variable | Required? | Purpose |
|
||||
|---|---|---|
|
||||
| `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'
|
||||
values are stripped from the submitted payload, so users never accidentally
|
||||
write data they couldn't see.
|
||||
Copy `.env.example` to `.env.local` for local development.
|
||||
|
||||
## 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
|
||||
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.
|
||||
```
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open `http://localhost:3000/?cid=1&cs=anything` — any non-empty `cs`
|
||||
works while in stub mode. The report is at `/report?cid=1&cs=anything`.
|
||||
|
||||
## Deploy
|
||||
|
||||
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 M1–M12 / Q1–Q4 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.
|
||||
|
||||
Reference in New Issue
Block a user