Commit Graph
53 Commits
Author SHA1 Message Date
Joel Brock 8159b87074 File upload pipeline: spike + endpoint skeleton (Phase 1, in progress)
Lays groundwork for closing the file-upload gap discovered while wiring
the org-contact custom fields. Currently no file fields in the form
actually persist to CiviCRM -- the renderer FileList drops at the
onSubmit JSON.stringify, and there is no /api/upload route or
Attachment.create call anywhere.

This commit adds:

1. scripts/spike-attachment-upload.mjs

   One-off spike to answer the open question that gates the rest of the
   work: does APIv4 Attachment.create accept an unbound upload, or must
   we attach to an entity at create time? If unbound works we can use
   the planned two-step pattern (upload returns a file id; submit
   references it). If not, activity-bound file fields need a different
   flow because the activity does not exist yet at upload time.

   The spike also exercises the Contact.update + .file_name read-back
   path against the Certificate_of_Incorporation field on a real org
   contact, then cleans up after itself.

   Usage:
     node --env-file=.env.local scripts/spike-attachment-upload.mjs \
       --org-id=<id> [--keep]

2. app/api/upload/route.ts

   Structural pieces that do not depend on the spike outcome:
     - multipart parsing via Request.formData()
     - 5 MB hard cap (under Amplify Lambda 6 MB sync payload limit)
     - MIME allowlist (PDF, DOC/DOCX, XLS/XLSX, JPEG/PNG/GIF/WEBP)
     - magic-byte sniff to cross-check the client-reported MIME
     - filename sanitization (path traversal scrub, length cap)
     - checksum verification, rate limiting, field-ref allowlist
     - STUB-mode short-circuit for local dev without live Civi
     - explicit 501 where the Civi Attachment.create wiring goes,
       with a comment pointing at the spike that resolves it

   Result: endpoint compiles, registers as a Next route, returns 501
   with a clear message; build passes; nothing wired into the frontend
   yet so the existing form is unaffected.

Phase 2 (renderer upload-on-pick), Phase 3 (submit reshape), Phase 4
(promote Certificate_of_Incorporation to editable) follow once the
spike output picks the Attachment.create variant.
2026-06-05 07:11:18 -07:00
Joel Brock 8ecf64c79b Stage 0: org-contact custom fields (Food_Co_op_Organizing)
Adds four fields from the Organization Contact's Food_Co_op_Organizing
custom group to the Stage 0 (always-visible) section:

  Date Incorporated                (date, editable)
  Name on Incorporation Certificate (text, editable)
  Certificate of Incorporation     (readonly; see note)
  Equity share                     (currency, editable)

These live on the Organization Contact record, not on the Check-in
activity, so they read/write through a different code path:

  - FieldConfig gains civiContactField, mutually exclusive with civiField
  - /api/data extends the org Contact.get select to include them and
    merges values into the prefill payload keyed by form-side name
  - /api/submit splits incoming values: contact-bound fields go through
    Contact.update (run first), activity-bound fields stay in the
    Activity.create call (run second)
  - FieldRenderer readonly branch now detects file-shaped values
    ({id, file_name}) and displays the filename rather than [object Object]

Certificate_of_Incorporation is wired readonly only: the form's
file-upload pipeline is not actually wired end-to-end (FileList drops
at JSON.stringify in onSubmit; no /api/upload endpoint exists). A
follow-up will close that gap.

Also adds scripts/inspect-org-custom-fields.mjs, a one-off introspection
script for dumping CustomField metadata when wiring a new group.
2026-06-04 17:37:36 -07:00
Joel Brock b120075ca2 Rebrand: Co-op Check-in -> Co-op Survey in user-facing copy
Updates the tool product name across the app UI (header, page title,
section labels, form buttons, success/error states, report stat labels),
README, deployment docs, and the CiviCRM email template guidance.
Custom domain references move from check-in.fci.coop to survey.fci.coop
(DNS update still required).

The underlying CiviCRM "Check-in (organizing)" activity type, custom
group machine names (Check_in_data__organizing_), health-check ids,
and the internal org_engagement_check_in form id are unchanged --
those are CiviCRM contract surfaces, not product copy.
2026-06-04 17:12:03 -07:00
Joel Brock 9bc4bbb732 Field groups: tighter style + apply to report
Form: dropped the boxed card treatment for FieldGroupCard in favor
of a leaf-tinted left rule + small uppercase mini-label. Eats only
~14px of horizontal space (border + pl-3/sm:pl-4) instead of
~32-40px for the previous bg-tinted card with px-4/sm:px-5 on both
sides, so the inner 2-col grid keeps more breathing room for the
fields themselves.

Report: same field-group concept now applies to ReportSection.
Grouped FieldHistoryRows render together inside a leaf-tinted left
rule with a small label above. Walk preserves the declared field
order — a group is emitted at its first member's position; the
other members are skipped when the loop later reaches them. Mixes
cleanly with the existing MembershipChart inline insertion and the
divide-y rhythm of standalone rows.
2026-05-21 16:51:00 -07:00
Joel Brock 8766de5ed0 Copy: "member(s)" → "member-owner(s)" in user-facing strings
Sweeps every active label / help / intro / visible paragraph in the
app to use 'member-owner' terminology consistently. Stub option
labels for the Capital Stack (Member equity / Member loans) updated
on the label side; option `value:` strings stay as the CRM-side
stored values. Sentinel comparisons in ReportView's chart legend
updated to track the new field labels (otherwise the `(custom
label)` parenthetical would print spuriously even at default).

Untouched on purpose:
  - Civi machine names (`Members__current_`, `Member_*`,
    `*_Member*`) — wire-level identifiers, must match Civi.
  - Option-group `value:` strings — CRM-stored values, must match.
  - NCG_Member / INFRA_Member option labels — these refer to a
    co-op's membership in distributor networks, not member-owners.
  - Commented-out fields and technical comments referencing Civi
    field names.
2026-05-21 14:08:06 -07:00
Joel Brock 5e7774795b Style anchors with underline + leaf color for visibility
Body-copy mailto links ("Chris @ FCI") and the chrome nav link
were rendering as plain text, hard to spot. Added an @layer base
rule that gives every `a[href]` a 1px underline at 2px offset and
the FCI Seed Grant green, with a subtle hover thicken. Tailwind
utilities still win on a per-element basis (the nav link keeps its
`text-ink-soft` color, the brand-image wrapper opts out via
`no-underline`).
2026-05-21 13:46:51 -07:00
Joel Brock 42b25289f8 Repair two multi-line fields the sync script left without commas
Member_equity_raised and Member_loans_raised had their civiField
lines stripped of trailing commas when the script inserted help
text. Re-added the comma; build passes.
2026-05-21 13:21:54 -07:00
Joel Brock d2d733a1d6 sync-help: ensure trailing comma before inserting new help line
Multi-line field whose last property had no trailing comma got
corrupted into invalid JS:

  civiField: `${G0}.X`
  help: "...",

(JS requires the comma between properties.) The insertion path now
checks the last non-whitespace, non-comma char of the property line
just above the closing brace; if it isn't already a comma, one is
inserted before the new help line is spliced in.
2026-05-21 13:21:54 -07:00
Joel Brock d4a6709149 Stage 1/3: add date+upload field groups; drop Stage 2 group labels
More pair-clustering across stages: Stage 1 gets four (Preliminary
Market Assessment, Preliminary Sources & Uses, Vision, Business
Concept), Stage 3 gets one (Site Letter of Intent). Stage 2 groups
drop their labels — the pair structure reads on its own and the
extra heading was visual noise.
2026-05-21 12:51:01 -07:00
Joel Brock de53fde1f7 Submit bar: animated stage-progress pills mirroring the header
Bar's secondary line is now a horizontal cluster of the same six
pills used in the page header, followed by the viewed stage label.
Active pill expands and gains a slow halo (rail-pulse keyframe) when
in the bar; same component runs without the pulse in the static
header. Width transitions smoothly between ranks as the user scrolls,
so the indicator visibly tracks progress through the form.

StageProgress now takes an optional `pulse` prop and tightens its
transition timing for nicer scroll-driven animation.
2026-05-21 12:41:14 -07:00
Joel Brock 7e5b1e1197 Sticky submit bar: show org name + currently viewed stage
Repurpose the previously-empty left side of the floating submit bar.
Top line is the org name; secondary line updates as the user scrolls
so the currently viewed stage is always visible even after the
top-of-form header has scrolled out of sight.

IntersectionObserver with a top-biased rootMargin tracks which
section is in view; topmost intersecting section wins ties.
Submit-state feedback (error / in-flight) still takes priority over
the viewing/draft text when active.
2026-05-21 12:30:54 -07:00
Joel Brock 2ca2d378a4 Add sync-help-from-civi script + field-group rendering
Two additions, both touching the form-config story:

1. scripts/sync-help-from-civi.mjs

   Diffs per-field help text in config/form.ts against CustomField rows
   in CiviCRM and (with --write) updates the file in place. Reads env
   from .env.local via Node's --env-file flag. Run as `npm run sync-help`
   or `npm run sync-help -- --write`. A --debug mode prints the parser's
   field list without calling Civi.

   Rationale: this form is low-traffic and help text doesn't change
   often once in production. A manual one-off sync is leaner than
   coupling every page load (or every build) to a Civi API call.

2. fieldGroups: visual clustering of related fields within a section

   New optional FieldGroupConfig overlay on StageSectionConfig — pure
   presentation, names existing fields by name so submit/visibility
   logic walks them unchanged. StageSection.tsx pulls grouped fields
   out of the standalone per-field grid and renders each group as its
   own bordered card with an optional heading. Stage 2 now clusters
   Market Study, Pro Forma, Business Plan, and Board Self Assessment
   (each a date + upload pair) into their own cards.
2026-05-21 12:27:22 -07:00
Joel Brock 9460e8320f WIP: form copy and field visibility adjustments 2026-05-21 12:11:23 -07:00
Joel Brock 709d9bfd8b WIP: form copy and field visibility adjustments 2026-05-21 11:17:06 -07:00
Joel Brock 0545dc3bc4 Pin Turbopack root to app dir to stop dev recompile loop
Two package-lock.json files exist (parent civi-webform/ + this app);
Next 16 silently picked the outer one, so Turbopack watched the parent
node_modules/, .claude-flow/, .swarm/, ruvector.db. Background writes
in those trees triggered a recompile loop that thrashed .next/dev and
leaked memory until the dev server crashed. Setting turbopack.root
keeps the watcher scoped to WebForm-mw/.
2026-05-21 11:15:24 -07:00
Joel Brock f0530ed337 FCI theming refresh, contact identity fields, Amplify secrets fix
- Remap globals.css tokens to FCI brand palette (Eggplant #801d7f,
  Spring Pea #96bc33, Seed Grant #679038, Squash #c9ad2d, FCI gray
  #4b5657). Existing leaf-* / clay-* class names preserved.
- Switch body font to Open Sans (FCI's free fallback for Museo Sans).
  Headings keep Fraunces.
- Add contact identity (first name, last name, email) as readonly
  fields at the top of Stage 0. /api/data fetches via APIv4
  Contact.get with email_primary.email join; values flow through
  FormDataPayload.contact and into the form's evalState so the
  readonly renderer displays them. Draft restore re-applies them so
  a stale local draft can't override.
- amplify.yml: fetch Amplify Secrets from SSM Parameter Store when
  they don't arrive as build-shell env vars (the common failure mode
  behind "Refusing to run in production without CIVI_*"). Adds a
  length-only diagnostic echo and a hard-fail guard so a missing
  required var stops the build with a clear message instead of
  bundling empty strings and crashing the SSR Lambda at runtime.
2026-05-20 16:46:44 -07:00
Joel Brock 8caa851bcc Comment out three Stage 0 fields (Peer Group, Internal Startup Assessment x2) 2026-05-20 16:04:21 -07:00
Joel Brock fbf668800b Amplify: write env vars + secrets to .env.production at build time
Amplify Gen 2 exposes Environment Variables and Secrets in the build shell
but does not inject them into the SSR Lambda runtime. Writing them to
.env.production during preBuild lets Next.js bundle them into the server
output so process.env reads work at request time.
2026-05-20 16:03:33 -07:00
Joel Brock 97222afe58 AMPLIFY_DEPLOY: document Gen 2 Environment variables vs Secrets split
Amplify Gen 2's console has two separate pages for runtime config:
Environment variables (plaintext) and Secrets (SSM Parameter Store
SecureString). The earlier 'mark as Secret with eye icon' wording was
Gen 1; in Gen 2 you choose by which page you add the value on.

Step 2 rewritten:
- Brief explanation of both pages and how they're injected (both end
  up as plain env vars in the app, same name).
- Combined variable table with a Page column showing where each value
  lives.
- Rule-of-thumb: anything that would let someone impersonate the app
  to CiviCRM or bypass a gate is a Secret; hostnames/usernames are
  fine in Environment variables.
- Callout reminding not to duplicate names across both pages
  (precedence undefined in Gen 2).
2026-05-19 17:28:24 -07:00
Joel Brock e90d007513 Amplify: install devDependencies during build (Tailwind/PostCSS need them)
@tailwindcss/postcss lives in devDependencies (along with the rest of
the PostCSS toolchain). When NODE_ENV=production is set in the Amplify
build environment, npm ci skips devDependencies — and next build then
fails resolving @tailwindcss/postcss while compiling globals.css.

amplify.yml now passes --include=dev to npm ci so the build always
installs everything regardless of NODE_ENV. AMPLIFY_DEPLOY.md updated
to warn against setting NODE_ENV=production in the Amplify env vars
panel — it's redundant (Next sets it correctly) and an easy footgun.
2026-05-19 17:13:13 -07:00
Joel Brock 6e7df382c9 AWS Amplify Hosting deploy prep
- amplify.yml: build spec (preBuild npm ci with offline cache, build
  next build, artifacts at .next/**, cache node_modules/.next-cache/.npm).
- .nvmrc: pin Node 20 so Amplify uses the same runtime as local.
- AMPLIFY_DEPLOY.md: first-time walkthrough covering AWS-side setup
  (create app, connect GitHub via OAuth/App, branch/auto-detect),
  environment variable table with secret-flag guidance, smoke-test via
  /healthz and /api/health, optional custom domain + per-PR previews,
  cost estimate, and operational notes (cold starts, no static
  egress IPs, CloudWatch logs, secret rotation).
- README deploy section: now points at both AMPLIFY_DEPLOY.md and the
  existing DEPLOYMENT.md (Render).
2026-05-19 16:48:38 -07:00
Joel Brock d88f1229ae Report: stair-step actual line, drop sparklines, move chart into Stage 0
- Actual member line now uses the same carry-forward step pattern as
  the goal line — between measurements the chart holds the prior value
  instead of interpolating diagonally, and the final value extends flat
  to the right edge. Eliminates the apparent dips that arose when
  diagonal interpolation crossed missing periods or low intermediate
  values.
- Per-field Sparkline (and its isNumericField / formatScalarText
  helpers) removed entirely. Expanding 'earlier entries' now just shows
  the chronological list. Curated multi-metric charts (like the
  Membership chart) are the path forward for trend visualization.
- Membership chart relocated from the top-of-report band into the
  Stage 0 ('Check-in (organizing)') section, rendered inline after
  whichever of Members__current_ / Member_Goal_for_current_Stage
  appears last in the section's fields-with-history list. Naturally
  scopes the chart to wherever those questions live (no double-render
  if config later moves them). Chart props refactored to take field
  + history pairs directly instead of walking the full sections array.
2026-05-19 16:45:53 -07:00
Joel Brock ae4e47025c Report: dedicated Membership chart (goal vs actual over time)
New MembershipChart card sits between the DateTimeline and the section
accordions, rendering whenever Members__current_ or
Member_Goal_for_current_Stage has any historical data.

- Actual member count: smooth leaf-700 polyline with a faint leaf-500
  area fill underneath, dots at every measurement, an emphasized dot
  on the most recent point with the value labeled inline.
- Goal: dashed clay-600 step line — each goal value is treated as a
  target that holds until the next update, then extends flat to the
  right edge of the chart. Dots at each update; the most-recent goal
  value labeled at the right.
- Y-axis: niceYTicks picks 3–5 round-number ticks (snapped to
  1/2/2.5/5/10 × 10^N) spanning [min(0, dataMin), dataMax]; faint
  gridlines + tabular-num labels on the left. Anchoring at 0 keeps
  growth-from-small-base readable.
- X-axis: reuses generateAxisTicks for adaptive month/year stepping,
  matching the timeline above. Today gets a dashed clay vertical
  guide when in range.
- Header: title + subtitle + an inline 'NNN of MMM target · X to go'
  callout in tabular-nums, color-coded (clay-700 if behind goal,
  leaf-700 if above).
- Legend at the bottom with line+dot chips for both series.
2026-05-19 16:37:01 -07:00
Joel Brock 00e10a992f Timeline: hover/focus tooltips, stage 0 lane, adaptive date axis
Three improvements to the report's DateTimeline:

1. Tooltips on every dot. Each dot is now a focusable span (tabIndex,
   role=img, full aria-label). A small ink-tinted card appears above
   the dot on mouse hover or keyboard focus, showing field label,
   formatted date, stage rank, and an 'Opened' marker for Date_Opened.
   Anchor flips to left/center/right based on the dot's position so
   tooltips don't overflow the row at the edges.

2. Plot every date field, including stage 0. The previous version
   skipped Stage 0 dates (Internal_Startup_Assessment_Date,
   Date_Closed_Folded). Now there are six swim lanes (0-5) instead
   of five. Stage 0 gets bg-leaf-200 so the gradient extends one
   step lighter.

3. Adaptive month/year x-axis under the lanes. generateAxisTicks
   picks a 'nice' interval based on the visible span: 1mo / 2mo /
   3mo / 6mo / 1yr / 2yr. January-bordered ticks include the year
   so the reader has anchors. Today gets its own labeled clay tick
   when it falls in range.
2026-05-19 16:32:50 -07:00
Joel Brock 814b560363 Report: sparkline charts on numeric history + stage-grouped date timeline
Two visual additions to the read-only activity report.

Sparkline: when the user expands earlier-entries on a numeric field
(number/currency/percent) with two or more numeric points, the
expansion now leads with a 240x56 inline SVG trend chart — chronological
polyline, faint area fill, small dots at every measurement, a slightly
larger emphasized dot on the most recent point. Min and max captions
sit beneath in tabular-nums, formatted in the field's native style
(currency uses Intl, percent appends %, etc.). Non-numeric fields are
unchanged.

DateTimeline: a new card between the context header and the section
accordions. Walks every date-type field in stage sections 1-5 (Stage 0
omitted as it isn't a stage in the journey sense), pulls each field's
most-recent entered date, and lays the events out in five horizontal
swim lanes — one per stage rank, labeled at the left. Time axis
spans from the earliest event to max(latest event, Date_Opened).
Stage 5's Date_Opened is rendered as a larger clay-700 dot with a
heavier ring so it reads as the journey's anchor at the right end.
A faint clay-300 dashed vertical line marks 'today' if it falls
within the range. Color scale across stages is leaf-300 / leaf-500
/ leaf-600 / leaf-700 / clay-700 — a sprout-to-fruit gradient that
matches the existing palette. Empty stage rows still draw their lane
line at half opacity so the structure stays readable. SR-only event
list provides screen-reader access to all plotted dates with their
labels.

Stub payload enriched with four cross-stage date entries so the
timeline has content in dev preview.
2026-05-19 16:16:57 -07:00
Joel Brock 9209d6dc02 Prefill file fields with their file_name joined from CiviCRM
CiviCRM APIv4 file custom fields return a bare file id by default; an
extra '.file_name' join is required to get the human-readable filename.
Both the form prefill walk (lib/prefill.ts) and the report walk
(app/api/report/route.ts) now request '<civiField>.file_name' for every
file-type field alongside the primary value, and wrap the prefill into
a { id, file_name } object so downstream UI has both. Falls back to
file_name undefined when the join returns null (eg orphaned id).

The form's FilePriorIndicator already accepts the object shape, so it
now shows the filename inline. The report's FormattedValue gets a
matching case: renders the file_name string if present, falls back to
'Attachment #<id>' when only the id came through.
2026-05-19 15:51:35 -07:00
Joel Brock a8ed6073a9 File fields: surface a prior-attachment indicator from RHF prefill
The earlier 'Currently on file' indicator hung off readonlyValue, which
is sourced from evalState (only carries current_stage). For file fields
the prefill lives in RHF state, not in evalState, so the indicator
never fired.

Replaced with a FilePriorIndicator subcomponent that subscribes to the
field's RHF value via useWatch and renders a small leaf-50 banner with
a paperclip glyph when a previous attachment is present. Falls silent
the moment the user picks a new file (RHF value becomes a FileList).
Filename is derived from whatever shape Civi returned — bare string,
object with file_name/name/label/filename, or numeric file id (generic
message in that case).
2026-05-19 15:48:10 -07:00
Joel Brock dbd607b829 Prune scaffold cruft: unused Next.js template SVGs and axios dep
Removed five default Next.js scaffold SVGs from public/ that were
created by create-next-app and never referenced (file.svg, globe.svg,
next.svg, vercel.svg, window.svg). The actual brand mark
public/fci-logo.png is the only image the app uses.

Removed axios from dependencies — the app uses native fetch
everywhere, and axios hasn't been imported since the initial
scaffolding pass. Lockfile regenerated; build verified clean.
2026-05-13 13:36:21 -07:00
Joel Brock 899dad2323 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.
2026-05-13 13:19:11 -07:00
Joel Brock ba88eb0165 Add /report — read-only activity history view
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.
2026-05-13 12:33:26 -07:00
Joel Brock 04e69ca04c Header: show Civi stage label prominently; restore current_stage readonly in Stage 0
- 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.
2026-05-13 12:09:24 -07:00
Joel Brock 74a85d38fe Drop in-form Framework Stage field; align section labels with Civi
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.
2026-05-13 12:04:22 -07:00
Joel Brock b4e80517a7 Derive current stage from most-recent stage-bearing activity
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.
2026-05-13 11:41:25 -07:00
Joel Brock 18b7a67fa1 Swap inline 'Sown' SVG for the real FCI brand logo
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).
2026-05-11 13:42:06 -07:00
Joel Brock 762605f04b Compact the journey rail to reclaim form width
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
2026-05-11 13:37:28 -07:00
Joel Brock 9103ccaf9d Locked future-stage cards + journey rail
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.
2026-05-11 13:05:34 -07:00
Joel Brock a804650f65 Audit short-term: currency preview, date bounds, success destination, safe-area
- 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.
2026-05-11 09:48:03 -07:00
Joel Brock 0d84b9654b Polish pass: removed lastTouched dead code, selective useWatch perf, token sweep, required-field messages, scroll-to-first-error, file prefill display, matrix sticky shadow 2026-05-09 22:49:25 -07:00
Joel Brock e452fbb15f Email delivery: /api/preview-link admin endpoint + EMAIL_DELIVERY.md guide 2026-05-09 22:31:51 -07:00
Joel Brock 442740939e Allow CSP unsafe-eval in dev only (Next.js HMR); production stays strict 2026-05-09 21:51:28 -07:00
Joel Brock 656bf7fd0a Visual identity: Field Almanac — Fraunces+DM Sans, OKLCH cream/leaf/clay palette, paper texture, hand-drawn stage icons, draft auto-save, stage progress dots 2026-05-09 21:48:45 -07:00
Joel Brock dcdf315244 Production hardening: CSP, rate limit, env validation, health gating, Render blueprint, DEPLOYMENT.md 2026-05-09 21:43:43 -07:00
Joel Brock da3e48a874 Add matrix-group display for monthly/quarterly time-series fields in Stage 5 2026-05-09 21:29:21 -07:00
Joel Brock 082238d884 Fix Activity.create payload: pass values as object not array 2026-05-09 21:15:56 -07:00
Joel Brock 1655c485d9 Fix APIv4 relationship_type_id join: use dot syntax not colon 2026-05-09 21:10:56 -07:00
Joel Brock da830af533 Health probe: dump all relationships for a given contact, flag direction issues 2026-05-09 21:08:30 -07:00
Joel Brock 2e9d3855ba Use existing 'Primary Contact' relationship type via FORM_CONTACT_RELATIONSHIP constant 2026-05-09 20:57:29 -07:00
Joel Brock e132789a3e Sharper health probes: resolve stage option group via CustomField metadata; list near-match relationship types 2026-05-09 20:52:06 -07:00
Joel Brock 82d7849a30 Support webserver-level HTTP Basic Auth in front of CiviCRM 2026-05-09 20:49:37 -07:00
Joel Brock 234bec5766 Add /api/health diagnostic route; remove stale .js route duplicates 2026-05-09 20:45:14 -07:00