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.
This commit is contained in:
+57
-4
@@ -9,10 +9,63 @@ applications:
|
|||||||
# Tailwind plugins which live in devDependencies; without it,
|
# Tailwind plugins which live in devDependencies; without it,
|
||||||
# NODE_ENV=production in the Amplify env causes npm to skip them.
|
# NODE_ENV=production in the Amplify env causes npm to skip them.
|
||||||
- npm ci --include=dev --cache .npm --prefer-offline
|
- npm ci --include=dev --cache .npm --prefer-offline
|
||||||
# Amplify exposes Environment Variables + Secrets in the build
|
# Amplify exposes Environment Variables in the build shell as
|
||||||
# shell but does NOT inject them into the SSR Lambda runtime.
|
# plain `$VAR` references, but Secrets are SecureString entries
|
||||||
# Write them to .env.production so Next.js bundles them into
|
# in SSM Parameter Store at `/amplify/<appId>/<branch>/<name>`
|
||||||
# the server output. .env* is gitignored.
|
# and are NOT always injected automatically into the build
|
||||||
|
# shell on older build images. If a Secret is unset as an env
|
||||||
|
# var, fall back to fetching it from SSM directly.
|
||||||
|
#
|
||||||
|
# Once resolved, the values get baked into .env.production so
|
||||||
|
# Next bundles them into the SSR Lambda. .env* is gitignored.
|
||||||
|
- |
|
||||||
|
# Try SSM for any of these that arrive empty (they were set
|
||||||
|
# via the Amplify Secrets tab, not Environment variables).
|
||||||
|
# Requires the Amplify build role to have ssm:GetParameter on
|
||||||
|
# /amplify/$AWS_APP_ID/$AWS_BRANCH/* — granted by default.
|
||||||
|
fetch_secret() {
|
||||||
|
local name="$1"
|
||||||
|
local current="${!name}"
|
||||||
|
if [ -n "$current" ]; then return 0; fi
|
||||||
|
local path="/amplify/${AWS_APP_ID}/${AWS_BRANCH}/${name}"
|
||||||
|
local val
|
||||||
|
val=$(aws ssm get-parameter --name "$path" --with-decryption \
|
||||||
|
--query "Parameter.Value" --output text 2>/dev/null || true)
|
||||||
|
if [ -n "$val" ] && [ "$val" != "None" ]; then
|
||||||
|
export "$name=$val"
|
||||||
|
echo "[secrets] $name resolved from SSM ($path)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
for v in CIVI_API_KEY CIVI_SITE_KEY CIVI_HTTP_AUTH_PASS HEALTH_TOKEN PREVIEW_ADMIN_TOKEN; do
|
||||||
|
fetch_secret "$v"
|
||||||
|
done
|
||||||
|
- |
|
||||||
|
# Length-only diagnostic (no values leaked to the log).
|
||||||
|
for v in CIVI_BASE_URL CIVI_API_KEY CIVI_SITE_KEY CIVI_HTTP_AUTH_USER CIVI_HTTP_AUTH_PASS HEALTH_TOKEN PREVIEW_ADMIN_TOKEN; do
|
||||||
|
val="${!v}"
|
||||||
|
if [ -n "$val" ]; then
|
||||||
|
echo "[env check] $v set (${#val} chars)"
|
||||||
|
else
|
||||||
|
echo "[env check] $v UNSET"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
- |
|
||||||
|
# Required-vars guard. Fails the build now (with a clear
|
||||||
|
# message) instead of letting an empty .env.production crash
|
||||||
|
# the SSR Lambda at runtime with "Refusing to run in
|
||||||
|
# production" from lib/env.ts.
|
||||||
|
missing=""
|
||||||
|
for v in CIVI_BASE_URL CIVI_API_KEY CIVI_SITE_KEY; do
|
||||||
|
if [ -z "${!v}" ]; then missing="$missing $v"; fi
|
||||||
|
done
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
echo "::error::Amplify build env missing required vars:$missing"
|
||||||
|
echo "Confirm: (1) the var is in the Amplify console under either"
|
||||||
|
echo "Hosting → Environment variables OR Hosting → Secrets;"
|
||||||
|
echo "(2) it is scoped to branch '${AWS_BRANCH}' (or All branches);"
|
||||||
|
echo "(3) the Amplify build role can read /amplify/${AWS_APP_ID}/${AWS_BRANCH}/* from SSM."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
- |
|
- |
|
||||||
{
|
{
|
||||||
echo "CIVI_BASE_URL=$CIVI_BASE_URL"
|
echo "CIVI_BASE_URL=$CIVI_BASE_URL"
|
||||||
|
|||||||
+30
-3
@@ -39,6 +39,11 @@ const DEFAULT_STAGE = "Inquiry";
|
|||||||
const STUB_PAYLOAD: FormDataPayload = {
|
const STUB_PAYLOAD: FormDataPayload = {
|
||||||
orgName: "Sample Co-op (stub)",
|
orgName: "Sample Co-op (stub)",
|
||||||
currentStage: "Organizing",
|
currentStage: "Organizing",
|
||||||
|
contact: {
|
||||||
|
firstName: "Jordan",
|
||||||
|
lastName: "Sample",
|
||||||
|
email: "jordan.sample@example.coop",
|
||||||
|
},
|
||||||
prefill: {
|
prefill: {
|
||||||
Peer_Group_Participation: "Yes",
|
Peer_Group_Participation: "Yes",
|
||||||
Members__current_: 87,
|
Members__current_: 87,
|
||||||
@@ -158,13 +163,25 @@ export async function GET(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
const orgId = orgs[0].contact_id_b;
|
const orgId = orgs[0].contact_id_b;
|
||||||
|
|
||||||
// Fire org-name lookup, stage-bearing-activity lookup, prefill walk, and
|
// Fire org-name lookup, contact-identity lookup, stage-bearing-activity
|
||||||
// option-group fetch in parallel — they're independent.
|
// lookup, prefill walk, and option-group fetch in parallel — they're
|
||||||
const [orgRes, stageActivityRes, { values: prefill }, options] = await Promise.all([
|
// independent.
|
||||||
|
const [orgRes, contactRes, stageActivityRes, { values: prefill }, options] = await Promise.all([
|
||||||
civi<{ id: number; display_name: string }>("Contact", "get", {
|
civi<{ id: number; display_name: string }>("Contact", "get", {
|
||||||
select: ["id", "display_name"],
|
select: ["id", "display_name"],
|
||||||
where: [["id", "=", orgId]],
|
where: [["id", "=", orgId]],
|
||||||
}),
|
}),
|
||||||
|
// Identifying details for the form-filler. APIv4 lets us chain through
|
||||||
|
// the primary-email join in the same call.
|
||||||
|
civi<{
|
||||||
|
id: number;
|
||||||
|
first_name: string | null;
|
||||||
|
last_name: string | null;
|
||||||
|
"email_primary.email": string | null;
|
||||||
|
}>("Contact", "get", {
|
||||||
|
select: ["id", "first_name", "last_name", "email_primary.email"],
|
||||||
|
where: [["id", "=", Number(cid)]],
|
||||||
|
}),
|
||||||
// Most recent Check-in (organizing) activity whose Stage custom field
|
// Most recent Check-in (organizing) activity whose Stage custom field
|
||||||
// is set. Staff own this field; the form never writes it. Tiebreaker on
|
// is set. Staff own this field; the form never writes it. Tiebreaker on
|
||||||
// equal activity_date_time is id DESC.
|
// equal activity_date_time is id DESC.
|
||||||
@@ -190,9 +207,19 @@ export async function GET(req: NextRequest) {
|
|||||||
const stageRaw = stageActivityRes.values?.[0]?.[ACTIVITY_STAGE_FIELD];
|
const stageRaw = stageActivityRes.values?.[0]?.[ACTIVITY_STAGE_FIELD];
|
||||||
const currentStage = typeof stageRaw === "string" && stageRaw ? stageRaw : DEFAULT_STAGE;
|
const currentStage = typeof stageRaw === "string" && stageRaw ? stageRaw : DEFAULT_STAGE;
|
||||||
|
|
||||||
|
const contactRow = contactRes.values?.[0];
|
||||||
|
const contact = contactRow
|
||||||
|
? {
|
||||||
|
firstName: contactRow.first_name ?? "",
|
||||||
|
lastName: contactRow.last_name ?? "",
|
||||||
|
email: contactRow["email_primary.email"] ?? "",
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const payload: FormDataPayload = {
|
const payload: FormDataPayload = {
|
||||||
orgName: org.display_name,
|
orgName: org.display_name,
|
||||||
currentStage,
|
currentStage,
|
||||||
|
contact,
|
||||||
prefill,
|
prefill,
|
||||||
options,
|
options,
|
||||||
};
|
};
|
||||||
|
|||||||
+56
-60
@@ -1,62 +1,76 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────────────────
|
/* ─────────────────────────────────────────────────────────────────────────
|
||||||
* Theme — "Field Almanac"
|
* Theme — FCI Brand
|
||||||
*
|
*
|
||||||
* Inspiration: 1970s ecological-movement print, agricultural cooperative
|
* Aligned with the Food Co-op Initiative brand guidelines (12.2022).
|
||||||
* publications, hand-bound field journals. Warm cream paper, deep
|
* Primary palette: Eggplant (#801d7f), Spring Pea (#96bc33), Squash
|
||||||
* botanical green ink, sparing terracotta accent for emphasis.
|
* (#c9ad2d). Secondary neutrals: FCI Dark Gray (#4b5657), FCI Beige
|
||||||
|
* (#D2C9B0), Seed Grant Green (#679038). Type: Open Sans (FCI's free
|
||||||
|
* substitute for Museo Sans, per the guidelines).
|
||||||
*
|
*
|
||||||
* The palette is built in OKLCH for perceptual uniformity. Every neutral
|
* Semantic token names from the previous theme are preserved so existing
|
||||||
* is tinted toward the leaf hue (h≈130) so cream and green feel like they
|
* component class names continue to apply:
|
||||||
* came from the same press run.
|
* - leaf-* → Spring Pea / Seed Grant green family
|
||||||
|
* - clay-* → Eggplant purple family (brand primary accent)
|
||||||
|
* - squash-*→ FCI yellow accent (new)
|
||||||
|
* - paper/ink → clean neutrals tuned to brand gray
|
||||||
* ─────────────────────────────────────────────────────────────────────── */
|
* ─────────────────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* Typography */
|
/* Typography
|
||||||
|
* Headings: existing display serif (Fraunces) retained for editorial voice.
|
||||||
|
* Body: Open Sans — FCI brand's free fallback for Museo Sans. */
|
||||||
--font-display: var(--font-display), ui-serif, Georgia, "Times New Roman", serif;
|
--font-display: var(--font-display), ui-serif, Georgia, "Times New Roman", serif;
|
||||||
--font-body: var(--font-body), ui-sans-serif, system-ui, -apple-system, sans-serif;
|
--font-body: var(--font-body), ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
|
||||||
/* Custom utility names */
|
/* Neutrals — anchored on FCI Dark Gray #4b5657 with FCI Beige #D2C9B0
|
||||||
--color-paper: oklch(97.5% 0.012 95); /* warm cream */
|
* peeking through in tinted surfaces. */
|
||||||
--color-paper-2: oklch(95.5% 0.018 90); /* deeper cream — section bands */
|
--color-paper: #ffffff; /* clean white */
|
||||||
--color-ink: oklch(20% 0.02 130); /* near-black, slight green tint */
|
--color-paper-2: #f7f4ec; /* tinted band — FCI beige, very light */
|
||||||
--color-ink-soft: oklch(35% 0.018 130);
|
--color-ink: #2d3536; /* near-black, brand-aligned */
|
||||||
--color-ink-mute: oklch(55% 0.014 120);
|
--color-ink-soft: #4b5657; /* FCI Dark Gray (exact) */
|
||||||
--color-rule: oklch(82% 0.025 100); /* hairline rules — like ink on cream */
|
--color-ink-mute: #7a8384;
|
||||||
--color-rule-soft: oklch(89% 0.02 100);
|
--color-rule: #d7d3c7; /* FCI beige, desaturated */
|
||||||
|
--color-rule-soft: #ebe7dc;
|
||||||
|
|
||||||
/* Botanical green — primary */
|
/* Leaf — Spring Pea #96bc33 + Seed Grant #679038 (FCI brand greens). */
|
||||||
--color-leaf-50: oklch(97% 0.025 135);
|
--color-leaf-50: #f3f8e8;
|
||||||
--color-leaf-100: oklch(93% 0.05 135);
|
--color-leaf-100: #e6f2cd;
|
||||||
--color-leaf-200: oklch(87% 0.085 135);
|
--color-leaf-200: #d0e5a6;
|
||||||
--color-leaf-300: oklch(78% 0.115 135);
|
--color-leaf-300: #b8d97c;
|
||||||
--color-leaf-400: oklch(68% 0.13 135);
|
--color-leaf-400: #a4cf57;
|
||||||
--color-leaf-500: oklch(57% 0.135 135);
|
--color-leaf-500: #96bc33; /* FCI Spring Pea (exact) */
|
||||||
--color-leaf-600: oklch(47% 0.13 135);
|
--color-leaf-600: #7faa2a;
|
||||||
--color-leaf-700: oklch(38% 0.115 135);
|
--color-leaf-700: #679038; /* FCI Seed Grant (exact) */
|
||||||
--color-leaf-800: oklch(30% 0.09 135);
|
--color-leaf-800: #4f7029;
|
||||||
--color-leaf-900: oklch(22% 0.06 135);
|
--color-leaf-900: #344f1a;
|
||||||
|
|
||||||
/* Terracotta — accent. Used sparingly: current-stage marker, key CTAs. */
|
/* Clay — repurposed as FCI Eggplant #801d7f (brand primary accent). */
|
||||||
--color-clay-100: oklch(94% 0.04 50);
|
--color-clay-100: #f5e6f4;
|
||||||
--color-clay-200: oklch(86% 0.075 50);
|
--color-clay-200: #e3c1e1;
|
||||||
--color-clay-400: oklch(70% 0.13 45);
|
--color-clay-400: #b34db1;
|
||||||
--color-clay-500: oklch(63% 0.15 42);
|
--color-clay-500: #9f2d9d;
|
||||||
--color-clay-600: oklch(54% 0.155 40);
|
--color-clay-600: #801d7f; /* FCI Eggplant (exact) */
|
||||||
--color-clay-700: oklch(45% 0.135 38);
|
--color-clay-700: #6a1869;
|
||||||
|
|
||||||
/* Stone — kept as a familiar alias mapping to our warm neutrals so any
|
/* Squash — FCI yellow #c9ad2d. Reserved for warning glyphs and small
|
||||||
* earlier `text-stone-*` / `border-stone-*` references stay readable. */
|
* highlights; not currently consumed by components but available. */
|
||||||
|
--color-squash-100: #faf3d0;
|
||||||
|
--color-squash-300: #e6d066;
|
||||||
|
--color-squash-500: #c9ad2d; /* FCI Squash (exact) */
|
||||||
|
--color-squash-700: #9a8623;
|
||||||
|
|
||||||
|
/* Stone alias — mapped to FCI neutrals for any lingering text-stone-* refs. */
|
||||||
--color-stone-50: var(--color-paper);
|
--color-stone-50: var(--color-paper);
|
||||||
--color-stone-100: var(--color-paper-2);
|
--color-stone-100: var(--color-paper-2);
|
||||||
--color-stone-200: var(--color-rule-soft);
|
--color-stone-200: var(--color-rule-soft);
|
||||||
--color-stone-300: var(--color-rule);
|
--color-stone-300: var(--color-rule);
|
||||||
--color-stone-400: oklch(70% 0.018 110);
|
--color-stone-400: #a8aeae;
|
||||||
--color-stone-500: var(--color-ink-mute);
|
--color-stone-500: var(--color-ink-mute);
|
||||||
--color-stone-600: oklch(48% 0.015 120);
|
--color-stone-600: #5e6868;
|
||||||
--color-stone-700: var(--color-ink-soft);
|
--color-stone-700: var(--color-ink-soft);
|
||||||
--color-stone-800: oklch(28% 0.018 130);
|
--color-stone-800: #353c3d;
|
||||||
--color-stone-900: var(--color-ink);
|
--color-stone-900: var(--color-ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,28 +87,10 @@ html {
|
|||||||
body {
|
body {
|
||||||
background-color: var(--color-paper);
|
background-color: var(--color-paper);
|
||||||
color: var(--color-ink);
|
color: var(--color-ink);
|
||||||
/* Subtle paper grain — two diagonal repeating linear gradients combined
|
|
||||||
* with a soft radial overlay. No external image. Survives dark mode if
|
|
||||||
* we ever add one. */
|
|
||||||
background-image:
|
|
||||||
radial-gradient(ellipse 90% 60% at 50% 0%, oklch(99% 0.01 95 / 0.7), transparent 60%),
|
|
||||||
repeating-linear-gradient(
|
|
||||||
105deg,
|
|
||||||
oklch(96% 0.02 95 / 0.4) 0,
|
|
||||||
oklch(96% 0.02 95 / 0.4) 1px,
|
|
||||||
transparent 1px,
|
|
||||||
transparent 6px
|
|
||||||
),
|
|
||||||
repeating-linear-gradient(
|
|
||||||
15deg,
|
|
||||||
oklch(94% 0.02 95 / 0.25) 0,
|
|
||||||
oklch(94% 0.02 95 / 0.25) 1px,
|
|
||||||
transparent 1px,
|
|
||||||
transparent 9px
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography defaults */
|
/* Typography defaults. Display serif keeps optical-size tuning (Fraunces);
|
||||||
|
* body switches to Open Sans (FCI brand fallback for Museo Sans). */
|
||||||
.font-display { font-family: var(--font-display); font-feature-settings: "ss01", "cv01"; }
|
.font-display { font-family: var(--font-display); font-feature-settings: "ss01", "cv01"; }
|
||||||
.font-body { font-family: var(--font-body); }
|
.font-body { font-family: var(--font-body); }
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -1,5 +1,5 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Fraunces, DM_Sans } from "next/font/google";
|
import { Fraunces, Open_Sans } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,13 +16,14 @@ const display = Fraunces({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Body face: DM Sans. A humanist geometric sans with a slightly soft feel —
|
* Body face: Open Sans. Per the FCI brand guidelines, Open Sans is the free
|
||||||
* legible at small sizes for forms, doesn't read as cold/corporate the way
|
* substitute for the brand's primary type family (Museo Sans). It pairs
|
||||||
* Inter or Helvetica does.
|
* cleanly with the Fraunces headings while keeping the form copy on-brand.
|
||||||
*/
|
*/
|
||||||
const body = DM_Sans({
|
const body = Open_Sans({
|
||||||
variable: "--font-body",
|
variable: "--font-body",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
weight: ["300", "400", "500", "600", "700"],
|
||||||
display: "swap",
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -76,13 +76,22 @@ export function EngagementForm({ config, cid, cs }: EngagementFormProps) {
|
|||||||
(useWatch({ control, name: "current_stage" }) as string | undefined) ?? "";
|
(useWatch({ control, name: "current_stage" }) as string | undefined) ?? "";
|
||||||
|
|
||||||
// State map passed to the conditional engine and to FieldRenderer for
|
// State map passed to the conditional engine and to FieldRenderer for
|
||||||
// readonly display values. Only fields referenced by visibility rules or
|
// readonly display values. Includes:
|
||||||
// by readonly displays need to be here — currently just current_stage,
|
// - current_stage: gates every Stage 1–5 visibleWhen rule and renders
|
||||||
// which gates every Stage 1–5 visibleWhen rule and is shown as a readonly
|
// as a readonly field in Stage 0.
|
||||||
// field in the Stage 0 header.
|
// - contact_first_name / contact_last_name / contact_email: readonly
|
||||||
|
// identity fields in Stage 0, sourced from /api/data (the cid contact).
|
||||||
|
// Static after load, so we read straight from load.data.contact rather
|
||||||
|
// than subscribing through react-hook-form.
|
||||||
|
const readyContact = load.kind === "ready" ? load.data.contact : undefined;
|
||||||
const evalState = useMemo(
|
const evalState = useMemo(
|
||||||
() => ({ current_stage: currentStageValue }),
|
() => ({
|
||||||
[currentStageValue],
|
current_stage: currentStageValue,
|
||||||
|
contact_first_name: readyContact?.firstName ?? "",
|
||||||
|
contact_last_name: readyContact?.lastName ?? "",
|
||||||
|
contact_email: readyContact?.email ?? "",
|
||||||
|
}),
|
||||||
|
[currentStageValue, readyContact],
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Initial load ───────────────────────────────────────────────────────
|
// ── Initial load ───────────────────────────────────────────────────────
|
||||||
@@ -107,17 +116,28 @@ export function EngagementForm({ config, cid, cs }: EngagementFormProps) {
|
|||||||
const data: FormDataPayload = await res.json();
|
const data: FormDataPayload = await res.json();
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
// Build defaults: server prefill + current stage. Then check if a
|
// Build defaults: server prefill + current stage + readonly contact
|
||||||
// local draft exists newer than the server data; if so, layer it on top.
|
// identity. Then check if a local draft exists newer than the server
|
||||||
|
// data; if so, layer it on top. Contact-identity fields are never
|
||||||
|
// user-editable, so a draft can't override them — they're force-set
|
||||||
|
// again after the draft merge below.
|
||||||
|
const contactDefaults: Record<string, unknown> = {
|
||||||
|
contact_first_name: data.contact?.firstName ?? "",
|
||||||
|
contact_last_name: data.contact?.lastName ?? "",
|
||||||
|
contact_email: data.contact?.email ?? "",
|
||||||
|
};
|
||||||
const defaults: Record<string, unknown> = {
|
const defaults: Record<string, unknown> = {
|
||||||
[config.stageField]: data.currentStage,
|
[config.stageField]: data.currentStage,
|
||||||
|
...contactDefaults,
|
||||||
...data.prefill,
|
...data.prefill,
|
||||||
};
|
};
|
||||||
const draft = loadDraft(cid);
|
const draft = loadDraft(cid);
|
||||||
if (draft && Object.keys(draft.values).length > 0) {
|
if (draft && Object.keys(draft.values).length > 0) {
|
||||||
Object.assign(defaults, draft.values);
|
Object.assign(defaults, draft.values);
|
||||||
// Re-set the stage from server (draft can never override the org's actual stage).
|
// Re-set the stage and contact identity from server (drafts can
|
||||||
|
// never override the org's actual stage nor the cid's contact).
|
||||||
defaults[config.stageField] = data.currentStage;
|
defaults[config.stageField] = data.currentStage;
|
||||||
|
Object.assign(defaults, contactDefaults);
|
||||||
setDraftRestored(true);
|
setDraftRestored(true);
|
||||||
setDraftSavedAt(draft.savedAt);
|
setDraftSavedAt(draft.savedAt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,24 @@ const stage0: StageSectionConfig = {
|
|||||||
label: "Current stage",
|
label: "Current stage",
|
||||||
type: "readonly",
|
type: "readonly",
|
||||||
optionGroupId: STAGE_OPTION_GROUP_ID,
|
optionGroupId: STAGE_OPTION_GROUP_ID,
|
||||||
|
},
|
||||||
|
// Contact identity, sourced from the cid in /api/data. Display-only —
|
||||||
|
// never written back to Civi. These confirm to the form-filler which
|
||||||
|
// contact record their submission will be attributed to.
|
||||||
|
{
|
||||||
|
name: "contact_first_name",
|
||||||
|
label: "First name",
|
||||||
|
type: "readonly",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contact_last_name",
|
||||||
|
label: "Last name",
|
||||||
|
type: "readonly",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "contact_email",
|
||||||
|
label: "Email",
|
||||||
|
type: "readonly",
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// name: "Peer_Group_Participation",
|
// name: "Peer_Group_Participation",
|
||||||
|
|||||||
@@ -168,6 +168,17 @@ export interface FormConfig {
|
|||||||
sections: StageSectionConfig[];
|
sections: StageSectionConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifying details for the contact submitting the form, derived from the
|
||||||
|
* cid resolved by /api/data. Surfaced as readonly fields in Stage 0 so the
|
||||||
|
* person filling out the check-in can confirm who's logged as the submitter.
|
||||||
|
*/
|
||||||
|
export interface ContactIdentity {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The shape returned by /api/data. Used by the form on initial load.
|
* The shape returned by /api/data. Used by the form on initial load.
|
||||||
*/
|
*/
|
||||||
@@ -176,6 +187,8 @@ export interface FormDataPayload {
|
|||||||
orgName: string;
|
orgName: string;
|
||||||
/** Current Framework Stage (text value). */
|
/** Current Framework Stage (text value). */
|
||||||
currentStage: string;
|
currentStage: string;
|
||||||
|
/** Identifying details of the contact behind the cid (readonly display). */
|
||||||
|
contact?: ContactIdentity;
|
||||||
/**
|
/**
|
||||||
* Per-field-most-recent prefill values, keyed by the FieldConfig.name.
|
* Per-field-most-recent prefill values, keyed by the FieldConfig.name.
|
||||||
* Fields with no prior value are simply absent from this map.
|
* Fields with no prior value are simply absent from this map.
|
||||||
|
|||||||
Reference in New Issue
Block a user