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.
Mirrors the form's IA and Field Almanac aesthetic; same auth (cid/cs
checksum) so the org owner who can fill the form can also view its
history.
New routes:
- GET /api/report — verifies checksum, resolves the org via the
Primary Contact relationship, fires Contact.get + Activity.get +
OptionValue.get in parallel. For every form field with a civiField,
walks all the org's Check-in (organizing) activities and collects
every non-empty value into a sorted-DESC history list. Returns
ReportPayload (orgName, currentStage, activities, fieldHistory,
options). Has stub-mode payload for env-less local dev.
- /report — page entry; same layout shell (SiteHeader + SiteFooter,
3xl page width). Eyebrow "Activity report - Co-op organizing".
ReportView component:
- ReportContextHeader: large org name, progress dots + uppercase
"Current stage" eyebrow + the Civi option *label* on its own line
at display-font xl/2xl leaf-800 (matches the form's header). Below
it a 3-up stat band: total check-ins, fields tracked, date span.
- One accordion card per stage section, in stage-rank order. Only
sections that have at least one field-with-entries render — past,
current, or "future-with-data" all welcome; truly empty stages stay
hidden so the page is calm.
- Same journey rail (md+) and mobile stem (md-) with past =
check-filled-leaf, current = filled-leaf-with-ring, future = dashed
hollow ring; solid leaf line vs dashed muted between markers.
- Within each card: divide-y rows. Field label and help on the left,
most-recent value on the right in display-font lg leaf-800, dated
beneath with an "{N} earlier entries" disclosure that expands a
small vertical timeline (date on left, value on right).
- FormattedValue handles currency (Intl), percent, number (tabular
nums), date (long, timezone-safe for YYYY-MM-DD), boolean (Yes/No),
select/readonly (resolved via option group), multiselect (handles
array or delimited string), file (filename), text-like (as-is).
- Loading / empty / error states match the form's treatments.
types/form.ts: new FieldHistoryEntry, ActivitySummary, ReportPayload.
The fieldHistory map keys by FieldConfig.name and only includes fields
that have at least one non-empty entry.
- New STAGE_OPTION_GROUP_ID constant (=75) added to config and consumed
by both the in-card readonly and the header. Because the field carries
optionGroupId, /api/data auto-includes the Stage option group in its
parallel fetch (via the existing optionGroupIds derivation).
- SubmissionContextHeader: dropped the small inline 'Stage Organizing'
line. Replaced with an uppercase 'Current stage' eyebrow next to the
progress dots, followed by a display-font 20/24px leaf-toned label
underneath. Resolves the stored option value ('Organizing') to its
Civi option label ('Stage 1 — Convene & Prepare') via the new
resolveStageLabel helper; falls back to the raw value if the option
group hasn't loaded.
- Stage 0 'Check-in (organizing)' section: re-added a current_stage
readonly field at the top. With optionGroupId set, FieldRenderer's
readonly branch resolves the value to the Civi label for display.
The current_stage readonly field is removed from Stage 0 — the activity-
derived stage value already displays in the SubmissionContextHeader, so
showing it again in-card was redundant. current_stage remains in form
state (RHF defaults from /api/data) because every Stage 1-5 visibleWhen
rule and the journey rail still key off it; it just isn't rendered.
Section labels for Stages 1-5 updated to match the CiviCRM Stage option
group labels exactly (ampersand instead of 'and'; Stage 2 corrected from
'Feasibility' to 'Grow & Plan'). Stage 0 keeps its 'Check-in (organizing)'
label since it represents the always-visible core fields, not the Inquiry
stage rank.
Also folded in: in-progress visibleWhen additions on several Stage 0
fields and commented-out field stubs from your working tree.
Stage authority moves from Organization.Food_Co_op_Organizing.Stage to the
most recent Check-in (organizing) activity whose Stage custom field is set.
Staff create these activities manually to mark transitions; org-owner form
submissions no longer write the Stage field at all, so they cannot override
a staff-set transition.
- /api/data: removed the Contact.get for org-side Stage; added an
Activity.get filtered to ACTIVITY_TYPE_NAME + ACTIVITY_STAGE_FIELD IS
NOT EMPTY, ordered by activity_date_time DESC, id DESC, limit 1.
Fallback when no such activity exists: Inquiry (rank 0). Org-name
lookup, stage activity, prefill, and option-group fetch all run in
parallel via Promise.all.
- /api/submit: removed the stageAtSubmission read + the
[ACTIVITY_STAGE_FIELD] write on the activity record. The form's
activities are stage-null by design now.
- config/form.ts: dropped the stage_at_submission readonly field (no
longer being set or displayed). Kept ACTIVITY_STAGE_FIELD export — it's
now used by /api/data to find stage-bearing activities. Updated the
current_stage field comment to reflect the new source.
- components/EngagementForm.tsx: dropped stage_at_submission from
evalState (no longer referenced by any visibility rule or readonly
display).
Org.Food_Co_op_Organizing.Stage remains in CiviCRM for staff list views;
the middleware no longer reads or writes it. No backfill required —
orgs without a stage-bearing activity simply read as Inquiry.
Drops the stylized SVG mark in favor of public/fci-logo.png (the official
Food Co-op Initiative logo, including its wordmark). Because the logo now
contains the 'Food Co-op Initiative' text, the duplicate display-font
wordmark is removed; the 'Co-op Check-in' eyebrow stays as a quiet hairline
divider to the right (sm+ only, hidden on mobile to keep the header tight).
Gutter shrinks from md:pl-20 (80px) to md:pl-12 (48px) — a 32px (40%) gain
back to the form column. Markers, pulse halo, and 'Now' pill scale down to
match so the rail still reads at a glance:
- Marker container: -left-14 w-12 -> -left-9 w-7
- Past/future markers: h-6 -> h-5
- Current marker: h-7 ring-4 -> h-6 ring-2; rank label 11px -> 10px
- 'Now' pill: 9px -> 8px; tracking and offset re-balanced for the shorter
drop from the smaller marker bottom
- Rail-pulse keyframe shadow radius: 8px -> 6px to suit the smaller disc
Future stages now render as preview-only "look ahead" cards instead of being
hidden. A user at stage 2 can see headers and contents for stages 3, 4, 5,
but those sections are visibly locked and uneditable.
Locked-card treatment:
- Dashed-rule border, paper-2 fill, no shadow — visually quieter than
active cards
- "Upcoming" pill in the header with a small lock glyph
- Muted stage rank mark (dashed badge, low-opacity icon)
- Panel content wrapped in fieldset[disabled] so every form control inside
is natively non-interactive, with an opacity tweak for affordance
- "A look ahead" banner explaining that fields will become editable when
the co-op reaches this stage
Section visibleWhen is still consulted on submit, so locked-stage values
never get written back to CiviCRM even if data is prefilled.
Journey rail:
- Vertical rail (md+) in a new left gutter; each card carries an aligned
marker. Past stages = filled leaf circle with check; current = filled
leaf disc with rank number, leaf-100 halo ring, and a subtle rail-pulse
box-shadow animation (motion-safe). A "Now" pill sits beneath the
current marker. Future stages = dashed hollow ring with lock glyph.
- Connector segments between markers are solid leaf when the next stage
is past-or-current, dashed muted when future — so the transition from
"traveled" to "ahead" reads at the right place in the journey.
- Mobile fallback: a small vertical stem in the gap between adjacent
cards, styled the same way (solid vs dashed) so the progression cue
still reads on narrow viewports.
- H3: Live currency preview below currency inputs shows the value formatted
with thousands separators (en-US, USD) using Intl.NumberFormat. Skipped
inside matrix cells to keep the Y1 monthly table compact.
- M1: Date inputs now apply min/max bounds. Default window is 1900-01-01 to
2100-12-31; per-field override via FieldConfig.min/max as ISO strings.
- H6: On successful submit, replace the form with a SuccessDestination card
(large checkmark, org name, "Submit another" + "safe to close" affordance).
Prevents accidental duplicate submits from back-button / autofill replay.
- M6: Sticky submit bar respects iOS safe-area-inset-bottom.
FieldRenderer now takes a control prop so the currency preview can subscribe
to its single field via useWatch without re-rendering the whole form.
- Initialize Next.js project with Tailwind CSS
- Create CiviCRM APIv4 integration layer
- Implement stage-based form visibility (stages 0-5)
- Add field mapping configuration for CRM-to-form linking
- Create API routes for data retrieval and submission
- Record form submissions as CiviCRM activities
- Support dynamic contactId and orgId via URL parameters
- Ensure robust form state management with react-hook-form
Co-authored-by: joelbrock <52835+joelbrock@users.noreply.github.com>