Commit Graph
27 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 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 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 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 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 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 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 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 e452fbb15f Email delivery: /api/preview-link admin endpoint + EMAIL_DELIVERY.md guide 2026-05-09 22:31:51 -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 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
Joel Brock c58a49be6d Wire real CiviCRM DEV custom fields and dynamic option fetching 2026-05-09 20:42:35 -07:00
Joel Brock 54555c74d2 Build standalone CiviCRM check-in middleware 2026-05-09 20:08:15 -07:00
google-labs-jules[bot]andjoelbrock 0899e6ae9a Implement CiviCRM middleware form with stage-based visibility
- 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>
2026-05-09 08:12:39 +00:00