Build standalone CiviCRM check-in middleware

This commit is contained in:
Joel Brock
2026-05-09 20:08:15 -07:00
parent 0899e6ae9a
commit 54555c74d2
21 changed files with 1894 additions and 457 deletions

146
types/form.ts Normal file
View File

@@ -0,0 +1,146 @@
/**
* Form configuration types.
*
* A FormConfig declares an ordered list of stage sections. Each section has
* its own visibility rule (so Stage 1..5 can be shown/hidden based on the
* org's current Framework Stage), and each field within a section can also
* carry its own visibility rule (so individual fields can be gated).
*
* The conditional language is intentionally narrow: a rule references one
* field by name, an operator, and a value (or set of values). Composition
* is via { all: [...] } / { any: [...] }. Anything else stays out of the
* config — it goes in code.
*/
export type FieldType =
| "text"
| "textarea"
| "number"
| "currency"
| "percent"
| "date"
| "email"
| "phone"
| "boolean"
| "select"
| "multiselect"
| "file"
| "readonly"; // display-only; the value comes from API and is not editable
export type ConditionOp =
| "equals"
| "notEquals"
| "in"
| "notIn"
| "isEmpty"
| "isNotEmpty";
export interface SimpleCondition {
field: string;
op: ConditionOp;
value?: string | number | boolean | null;
values?: Array<string | number | boolean>;
}
export interface AllCondition {
all: VisibilityRule[];
}
export interface AnyCondition {
any: VisibilityRule[];
}
export type VisibilityRule = SimpleCondition | AllCondition | AnyCondition;
export interface SelectOption {
value: string;
label: string;
}
export interface FieldConfig {
/** Form-side machine name. Use `stage_<N>_<short>` per the spec convention. */
name: string;
/** Human-readable label shown to the user. */
label: string;
type: FieldType;
/** Optional helper text rendered below the label. */
help?: string;
required?: boolean;
placeholder?: string;
/** For select / multiselect. */
options?: SelectOption[];
/** For number / currency / percent — min/max/step constraints. */
min?: number;
max?: number;
step?: number;
/** For text / textarea — max length. */
maxLength?: number;
/** Visibility rule. If absent, field is always visible (within its visible section). */
visibleWhen?: VisibilityRule;
/**
* The CiviCRM custom-field reference, if this maps to a custom field on the
* Org Engagement Submission activity. Format: `custom_<id>` or
* `custom_<group_name>.<field_name>` depending on APIv4 conventions.
* Leave undefined for fields that don't write back to Civi (e.g. transient
* UI helpers).
*/
civiField?: string;
}
export interface StageSectionConfig {
/** Stage rank, 0..5. Used by the conditional engine and the accordion. */
rank: number;
/** Internal id, e.g. "stage_0", "stage_1", … */
id: string;
/** Human label, e.g. "Inquiry / Check-in", "Stage 1 — Convene & Prepare". */
label: string;
/** Optional intro text rendered at the top of the section. */
intro?: string;
/**
* Visibility rule for the entire section. Stage 0 has no rule (always
* visible). Stages 15 each declare a multi-value list match against the
* current Framework Stage.
*/
visibleWhen?: VisibilityRule;
fields: FieldConfig[];
}
export interface FormConfig {
/** Unique form identifier (e.g. "org_engagement_check_in"). */
id: string;
/** Public title shown in the form header. */
title: string;
/** Short subtitle / description. */
subtitle?: string;
/**
* The form's stage-source field name. The conditional engine reads this
* field's current value when evaluating rules that reference "current_stage".
* Conventionally `current_stage`.
*/
stageField: string;
sections: StageSectionConfig[];
}
/**
* The shape returned by /api/data. Used by the form on initial load.
*/
export interface FormDataPayload {
/** The organization's display name. */
orgName: string;
/** Current Framework Stage (text value). */
currentStage: string;
/**
* Per-field-most-recent prefill values, keyed by the FieldConfig.name.
* Fields with no prior value are simply absent from this map.
*/
prefill: Record<string, unknown>;
}
/**
* The shape submitted to /api/submit.
*/
export interface SubmitPayload {
cid: string;
cs: string;
values: Record<string, unknown>;
}