/** * Form definition for the Org Engagement Check-in form. * * Field references and types are sourced from the live CiviCRM DEV * (`client.crm.fci.coop`) inventory pulled 2026-05-09 via APIv4 * `CustomField.get` filtered to the six custom groups bound to the * `Check-in (organizing)` activity type: * - Check_in_data__organizing_ (Stage 0) * - Stage_1, Stage_2, Stage_3, Stage_4, Stage_5 * * `civiField` uses APIv4's `.` syntax. * * Fields with `optionGroupId` set have their `options` populated at runtime * via `/api/data` — the server fetches OptionValue rows for those option * groups and embeds them in the FormDataPayload. The hardcoded `options` * arrays here are placeholder fallbacks if the live fetch fails. */ import type { FieldConfig, FormConfig, StageSectionConfig, VisibilityRule } from "@/types/form"; // Active stage values (stored values in CiviCRM's `Stage` option group). const S = { Inquiry: "Inquiry", Organizing: "Organizing", Feasibility: "Feasibility", BusinessFeasibility: "Business feasibility", StoreImplementation: "Store Implementation", Stabilize: "Stabilize newly opened co-op", } as const; const visibleAtOrAfter = (...stages: string[]): VisibilityRule => ({ field: "current_stage", op: "in", values: stages, }); /** * Option group ID for the `Stage` option group on `client.crm.fci.coop`. * Used to resolve the current stage value (e.g. "Organizing") to its Civi * label (e.g. "Stage 1 — Convene & Prepare") for display. */ export const STAGE_OPTION_GROUP_ID = 75; // CiviCRM custom group machine names per the APIv4 inventory. const G0 = "Check_in_data__organizing_"; const G1 = "Stage_1"; const G2 = "Stage_2"; const G3 = "Stage_3"; const G4 = "Stage_4"; const G5 = "Stage_5"; // Stage 0 — Check-in (always visible) const stage0: StageSectionConfig = { rank: 0, id: "stage_0", label: "Check-in (organizing)", intro: "Core check-in fields. These are visible at every stage and capture the data we follow over time across the lifecycle of the co-op.", fields: [ // current_stage is a UI-only readonly field. Its value is the Civi // option *value* (e.g. "Organizing") sourced by /api/data from the // most recent stage-bearing Check-in (organizing) activity. Setting // `optionGroupId` makes the readonly renderer resolve the value to // its Civi option *label* (e.g. "Stage 1 — Convene & Prepare") for // display. The form never writes Stage back to the activity. { name: "current_stage", label: "Current stage", type: "readonly", optionGroupId: STAGE_OPTION_GROUP_ID, }, { name: "Peer_Group_Participation", label: "Peer Group Participation", type: "select", civiField: `${G0}.Peer_Group_Participation`, optionGroupId: 140, help: "Is this co-op currently participating in Peer Learning Groups?", visibleWhen: [] }, { name: "Internal_Startup_Assessment", label: "Internal Startup Assessment", type: "select", civiField: `${G0}.Internal_Startup_Assessment`, optionGroupId: 132, visibleWhen: [] }, { name: "Internal_Startup_Assessment_Date", label: "Internal Startup Assessment Date", type: "date", civiField: `${G0}.Internal_Startup_Assessment_Date`, help: "Date of the latest Internal Startup Assessment rating.", visibleWhen: [] }, { name: "Member_Goal_for_current_Stage", label: "Member Goal for current Stage", type: "number", civiField: `${G0}.Member_Goal_for_current_Stage`, help: "What is your member / owner goal for your current co-op development Stage?", }, { name: "Members__current_", label: "Members (current)", type: "number", civiField: `${G0}.Members__current_`, help: "How many paid members does the co-op currently have? Include partially paid (e.g. installment plans).", }, { name: "Total_members_at_opening", label: "Total members at opening", type: "number", civiField: `${G0}.Total_members_at_opening`, visibleWhen: visibleAtOrAfter(S.Stabilize), }, // { // name: "Volunteers_Helping_In_Store", // label: "Volunteers Helping In Store", // type: "select", // civiField: `${G0}.Volunteers_Helping_In_Store`, // optionGroupId: 141, // help: "Does this co-op use volunteers for day-to-day operations? (Not occasional events.)", // }, // { // name: "Work_from_Members", // label: "Work from Members", // type: "select", // civiField: `${G0}.Work_from_Members`, // optionGroupId: 142, // help: "Does this co-op require work from members (e.g. annual hours)?", // }, { name: "Total_square_ft", label: "Total square ft", type: "number", civiField: `${G0}.Total_square_ft`, step: 1, visibleWhen: visibleAtOrAfter(S.BusinessFeasibility, S.StoreImplementation, S.Stabilize), }, { name: "Retail_sq_ft", label: "Retail sq ft", type: "number", civiField: `${G0}.Retail_sq_ft`, step: 1, visibleWhen: visibleAtOrAfter(S.BusinessFeasibility, S.StoreImplementation, S.Stabilize), }, // { // name: "Latest_Sources_and_Uses_doc", // label: "Latest Sources and Uses doc", // type: "file", // civiField: `${G0}.Latest_Sources_and_Uses_doc`, // }, // { // name: "Latest_Pro_Forma_doc", // label: "Latest Pro Forma doc", // type: "file", // civiField: `${G0}.Latest_Pro_Forma_doc`, // }, // { name: "Projected_Year_1_Sales", label: "Projected Year 1 Sales", type: "currency", civiField: `${G0}.Projected_Year_1_Sales` }, // { name: "Projected_Year_2_Sales", label: "Projected Year 2 Sales", type: "currency", civiField: `${G0}.Projected_Year_2_Sales` }, // { name: "Projected_Year_3_Sales", label: "Projected Year 3 Sales", type: "currency", civiField: `${G0}.Projected_Year_3_Sales` }, // { // name: "Projected_sales_at_maturity", // label: "Projected sales once established", // type: "currency", // civiField: `${G0}.Projected_sales_at_maturity`, // }, { name: "FTEs", label: "Projected FTEs", type: "number", visibleWhen: visibleAtOrAfter(S.BusinessFeasibility, S.StoreImplementation, S.Stabilize), civiField: `${G0}.FTEs`, step: 0.1, help: "Number of full-time equivalent (FTE) employees planned to work at the store.", }, { name: "Actual_FTE", label: "Actual FTE", type: "number", civiField: `${G0}.Actual_FTE`, step: 0.1, visibleWhen: visibleAtOrAfter(S.Stabilize), }, { name: "Total_cost_of_project", label: "Total cost of project", type: "currency", visibleWhen: visibleAtOrAfter(S.Stabilize), civiField: `${G0}.Total_cost_of_project`, }, { name: "Member_equity_total", label: "Member equity total needed", type: "currency", civiField: `${G0}.Member_equity_total` }, { name: "Member_equity_raised", label: "Member equity raised", type: "currency", visibleWhen: visibleAtOrAfter(S.Stabilize), civiField: `${G0}.Member_equity_raised` }, { name: "Member_loans_total", label: "Member loans total needed", type: "currency", civiField: `${G0}.Member_loans_total` }, { name: "Member_loans_raised", label: "Member loans raised", type: "currency", visibleWhen: visibleAtOrAfter(S.Stabilize), civiField: `${G0}.Member_loans_raised` }, // { // name: "Member_preferred_shares_total", // label: "Member preferred shares total needed", // type: "currency", // civiField: `${G0}.Member_preferred_shares_total`, // }, { name: "Member_preferred_shares_raised", label: "Member preferred shares raised", type: "currency", visibleWhen: visibleAtOrAfter(S.Stabilize), civiField: `${G0}.Member_preferred_shares_raised`, }, { name: "Bank_debt_total", label: "Bank debt total needed", type: "currency", civiField: `${G0}.Bank_debt_total` }, { name: "Bank_debt_raised", label: "Bank debt raised", type: "currency", visibleWhen: visibleAtOrAfter(S.Stabilize), civiField: `${G0}.Bank_debt_raised` }, { name: "Grant_Donations_Needed", label: "Grants / Donations Needed", type: "currency", civiField: `${G0}.Grant_Donations_Needed` }, { name: "Grants_Donations_Raised", label: "Grants / Donations Raised", type: "currency", visibleWhen: visibleAtOrAfter(S.Stabilize), civiField: `${G0}.Grants_Donations_Raised` }, { name: "Other_sources_total", label: "Other sources total needed", type: "currency", civiField: `${G0}.Other_sources_total` }, { name: "Other_sources_raised", label: "Other sources raised", type: "currency", visibleWhen: visibleAtOrAfter(S.Stabilize), civiField: `${G0}.Other_sources_raised` }, { name: "Date_Closed_Folded", label: "Date Closed / Folded", type: "date", civiField: `${G0}.Date_Closed_Folded` }, ] as FieldConfig[], }; // Stage 1 — Convene & Prepare (visible from Organizing onward) const stage1: StageSectionConfig = { rank: 1, id: "stage_1", label: "Stage 1 — Convene & Prepare", visibleWhen: visibleAtOrAfter(S.Organizing, S.Feasibility, S.BusinessFeasibility, S.StoreImplementation, S.Stabilize), fields: [ { name: "Preliminary_Market_Assessment", label: "Preliminary Market Assessment", type: "date", required: true, civiField: `${G1}.Preliminary_Market_Assessment`, help: "What date was your Preliminary Market Assessment completed?", }, { name: "Preliminary_Market_Assessment_Upload", label: "Preliminary Market Assessment — Upload", type: "file", civiField: `${G1}.Preliminary_Market_Assessment_Upload`, }, { name: "Preliminary_Sources_Uses", label: "Preliminary Sources And Uses", type: "date", civiField: `${G1}.Preliminary_Sources_Uses`, help: "When was your Preliminary Sources & Uses completed?", }, { name: "Preliminary_Sources_Uses_Upload", label: "Preliminary Sources and Uses — Upload", type: "file", civiField: `${G1}.Preliminary_Sources_Uses_Upload`, }, { name: "Vision", label: "Vision", type: "date", civiField: `${G1}.Vision`, help: "What date was your vision document completed?", }, { name: "Vision_Upload", label: "Vision — Upload", type: "file", civiField: `${G1}.Vision_Upload` }, { name: "Business_Concept", label: "Business Concept", type: "date", civiField: `${G1}.Business_Concept`, help: "When was your business concept finalized?", }, { name: "Business_Concept_Upload", label: "Business Concept — Upload", type: "file", civiField: `${G1}.Business_Concept_Upload`, }, ] as FieldConfig[], }; // Stage 2 — Feasibility const stage2: StageSectionConfig = { rank: 2, id: "stage_2", label: "Stage 2 — Grow & Plan", visibleWhen: visibleAtOrAfter(S.Feasibility, S.BusinessFeasibility, S.StoreImplementation, S.Stabilize), fields: [ { name: "Market_Study_Date", label: "Market Study Date", type: "date", civiField: `${G2}.Market_Study_Date` }, { name: "Market_Study_Upload", label: "Market Study — Upload", type: "file", civiField: `${G2}.Market_Study_Upload` }, { name: "Pro_Forma_date_completed", label: "Pro Forma — date completed", type: "date", civiField: `${G2}.Pro_Forma_date_completed` }, { name: "Pro_Forma_Upload", label: "Pro Forma — Upload", type: "file", civiField: `${G2}.Pro_Forma_Upload` }, { name: "Pro_Forma_Viability", label: "Pro Forma Viability", type: "select", civiField: `${G2}.Pro_Forma_Viability`, optionGroupId: 133, help: "Rate the viability of the project.", }, { name: "Business_Plan", label: "Business Plan", type: "date", civiField: `${G2}.Business_Plan`, help: "What date was your most recent business plan completed?", }, { name: "Business_Plan_Upload", label: "Business Plan — Upload", type: "file", civiField: `${G2}.Business_Plan_Upload` }, { name: "Board_Self_Assessment", label: "Board Self Assessment", type: "date", civiField: `${G2}.Board_Self_Assessment`, help: "When was your last Board Self Assessment completed?", }, { name: "Board_Self_Assessment_Upload", label: "Board Self Assessment — Upload", type: "file", civiField: `${G2}.Board_Self_Assessment_Upload`, }, { name: "Governance_System_Used", label: "Governance System Used", type: "text", civiField: `${G2}.Governance_System_Used`, help: "What governance system does your board use, or how do you make decisions?", }, ] as FieldConfig[], }; // Stage 3 — Connect & Gather const stage3: StageSectionConfig = { rank: 3, id: "stage_3", label: "Stage 3 — Connect & Gather", visibleWhen: visibleAtOrAfter(S.BusinessFeasibility, S.StoreImplementation, S.Stabilize), fields: [ { name: "Site_Letter_of_Intent_Date", label: "Site: Letter of Intent Date", type: "date", civiField: `${G3}.Site_Letter_of_Intent_Date`, }, { name: "Site_Letter_of_Intent_Upload", label: "Site: Letter of Intent — Upload", type: "file", civiField: `${G3}.Site_Letter_of_Intent_Upload`, }, { name: "Own_the_building_property", label: "Own the building / property", type: "select", civiField: `${G3}.Own_the_building_property`, optionGroupId: 139, help: "Will the co-op own the building or property?", }, { name: "Capital_Campaign_Owner_Participation_", label: "Capital Campaign: Owner Participation %", type: "percent", civiField: `${G3}.Capital_Campaign_Owner_Participation_`, }, { name: "Capital_Campaign_Average_Owner_Investment", label: "Capital Campaign: Average Owner Investment", type: "currency", civiField: `${G3}.Capital_Campaign_Average_Owner_Investment`, }, { name: "Capital_Stack", label: "Capital Stack", type: "multiselect", civiField: `${G3}.Capital_Stack`, optionGroupId: 134, help: "Select all the types of funding that are part of your Sources.", }, { name: "Project_Manager_Date_of_Hire", label: "Project Manager — Date of Hire", type: "date", civiField: `${G3}.Project_Manager_Date_of_Hire`, }, { name: "Store_Design_Plan_Completion", label: "Store Design — Plan Completion", type: "date", civiField: `${G3}.Store_Design_Plan_Completion`, }, { name: "Store_Designer", label: "Store Designer", type: "text", civiField: `${G3}.Store_Designer` }, { name: "NCG_Member", label: "NCG Member", type: "select", civiField: `${G3}.NCG_Member`, optionGroupId: 136 }, { name: "NCG_Corridor", label: "NCG Corridor", type: "select", civiField: `${G3}.NCG_Corridor`, optionGroupId: 143 }, { name: "INFRA_Member", label: "INFRA Member", type: "select", civiField: `${G3}.INFRA_Member`, optionGroupId: 137 }, { name: "Other_Distributors", label: "Other Distributors", type: "select", civiField: `${G3}.Other_Distributors`, optionGroupId: 138, help: "Are you using or considering one of these other distributors as your primary?", }, ] as FieldConfig[], }; // Stage 4 — Excite & Build const stage4: StageSectionConfig = { rank: 4, id: "stage_4", label: "Stage 4 — Excite & Build", visibleWhen: visibleAtOrAfter(S.StoreImplementation, S.Stabilize), fields: [ { name: "Projected_Opening_Date", label: "Projected Opening Date", type: "date", civiField: `${G4}.Projected_Opening_Date` }, { name: "Construction_Completion_Date", label: "Construction Completion Date", type: "date", civiField: `${G4}.Construction_Completion_Date`, }, { name: "Missions_Transition_Plan_Date", label: "Mission Transition Plan — Date", type: "date", civiField: `${G4}.Missions_Transition_Plan_Date`, }, { name: "Mission_Transition_Plan_Upload", label: "Mission Transition Plan — Upload", type: "file", civiField: `${G4}.Mission_Transition_Plan_Upload`, }, { name: "General_Manager_Name", label: "General Manager — Name", type: "text", civiField: `${G4}.General_Manager_Name` }, { name: "General_Manager_Phone", label: "General Manager Phone", type: "phone", civiField: `${G4}.General_Manager_Phone` }, { name: "GM_Email", label: "General Manager Email", type: "email", civiField: `${G4}.GM_Email` }, { name: "General_Manager_Date_of_Hire", label: "General Manager — Date of Hire", type: "date", civiField: `${G4}.General_Manager_Date_of_Hire`, }, { name: "General_Manager_Background", label: "General Manager Background", type: "select", civiField: `${G4}.General_Manager_Background`, optionGroupId: 135, help: "What professional background does your General Manager have?", }, { name: "GM_Support_Training", label: "GM Support and Training", type: "text", civiField: `${G4}.GM_Support_Training`, help: "What training and/or onboarding support is being provided to your GM?", }, { name: "GM_Support_Team", label: "GM Support Team", type: "text", civiField: `${G4}.GM_Support_Team`, help: "Who is providing support and training to your GM?", }, ] as FieldConfig[], }; // Stage 5 — Fulfill & Stabilize. Field machine names have historical // inconsistencies; we list each one explicitly rather than building // programmatically to keep the mapping auditable. /* const STAGE_5_FIELDS: FieldConfig[] = [ // Y1 Monthly Sales Targets — note the irregular machine names! { name: "Y1_Monthly_Sales_Targets", label: "Y1 Monthly Sales Target: M1", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Targets` }, { name: "Y1_Monthly_Sales_Targets_M2", label: "Y1 Monthly Sales Target: M2", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Targets_M2` }, { name: "Y1_Monthly_Sales_Target_M2", label: "Y1 Monthly Sales Target: M3", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Target_M2`, help: "(Civi field name says M2 but represents M3.)" }, { name: "Y1_Monthly_Sales_Target_M4", label: "Y1 Monthly Sales Target: M4", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Target_M4` }, { name: "Y1_Monthly_Sales_Target_M5", label: "Y1 Monthly Sales Target: M5", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Target_M5` }, { name: "Y1_Monthly_Sales_Target_M6", label: "Y1 Monthly Sales Target: M6", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Target_M6` }, { name: "Y1_Monthly_Sales_Target_M7", label: "Y1 Monthly Sales Target: M7", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Target_M7` }, { name: "Y1_Monthly_Sales_Target_M8", label: "Y1 Monthly Sales Target: M8", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Target_M8` }, { name: "Y1_Monthly_Sales_Target_M9", label: "Y1 Monthly Sales Target: M9", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Target_M9` }, { name: "Y1_Monthly_Sales_Target_M10", label: "Y1 Monthly Sales Target: M10", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Target_M10` }, { name: "Y1_Monthly_Sales_Target_M11", label: "Y1 Monthly Sales Target: M11", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Target_M11` }, { name: "Y1_Monthly_Sales_Target_M12", label: "Y1 Monthly Sales Target: M12", type: "currency", civiField: `${G5}.Y1_Monthly_Sales_Target_M12` }, // Y1 Actual Sales (M1..M12) ...[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((m) => ({ name: `Y1_M${m}_Actual_Sales`, label: `Y1 M${m} Actual Sales`, type: "currency" as const, civiField: `${G5}.Y1_M${m}_Actual_Sales`, })), // Y1 Transactions (M1..M12) ...[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((m) => ({ name: `Y1_M${m}_Transactions`, label: `Y1 M${m} Transactions`, type: "number" as const, civiField: `${G5}.Y1_M${m}_Transactions`, })), // Y1 Quarterly Labor (Q1..Q4) — stored as % of sales ...[1, 2, 3, 4].map((q) => ({ name: `Y1_Q${q}_Labor`, label: `Y1 Q${q} Labor`, type: "percent" as const, civiField: `${G5}.Y1_Q${q}_Labor`, help: `Labor, as a percent of sales for Y1 Q${q}.`, })), // Y1 Quarterly Margin (Q1..Q4) ...[1, 2, 3, 4].map((q) => ({ name: `Y1_Q${q}_Margin`, label: `Y1 Q${q} Margin`, type: "percent" as const, civiField: `${G5}.Y1_Q${q}_Margin`, help: `Margin, as a percent of sales, for Y1 Q${q}.`, })), // Y1 Quarterly Member Sales % (Q1..Q4) — note the trailing underscore in Civi machine names! ...[1, 2, 3, 4].map((q) => ({ name: `Y1_Q${q}_Member_Sales_`, label: `Y1 Q${q} Member Sales %`, type: "percent" as const, civiField: `${G5}.Y1_Q${q}_Member_Sales_`, help: `Percentage of sales to member-owners in Y1 Q${q}.`, })), ]; // Helpers for building the field-name lists in the matrices below. const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; const monthCols = months.map((m) => ({ key: `M${m}`, label: `M${m}` })); const quarters = [1, 2, 3, 4]; const quarterCols = quarters.map((q) => ({ key: `Q${q}`, label: `Q${q}` })); // Y1 Monthly Sales Target field names (irregular — built explicitly). const Y1_TARGET_FIELDS = [ "Y1_Monthly_Sales_Targets", // M1 "Y1_Monthly_Sales_Targets_M2", // M2 "Y1_Monthly_Sales_Target_M2", // M3 (Civi field name says M2; label says M3) "Y1_Monthly_Sales_Target_M4", "Y1_Monthly_Sales_Target_M5", "Y1_Monthly_Sales_Target_M6", "Y1_Monthly_Sales_Target_M7", "Y1_Monthly_Sales_Target_M8", "Y1_Monthly_Sales_Target_M9", "Y1_Monthly_Sales_Target_M10", "Y1_Monthly_Sales_Target_M11", "Y1_Monthly_Sales_Target_M12", ]; */ const stage5: StageSectionConfig = { rank: 5, id: "stage_5", label: "Stage 5 — Fulfill & Stabilize", visibleWhen: visibleAtOrAfter(S.Stabilize), fields: [ { name: "Date_Opened", label: "Date Opened", type: "date", civiField: `${G5}.Date_Opened` }, { name: "Y1_Actual_Sales", label: "Y1 Actual Sales", type: "currency", civiField: `${G5}.Y1_Actual_Sales` } ] as FieldConfig[], // STAGE_5_FIELDS, // matrixGroups: [ // { // id: "y1_monthly_metrics", // label: "Year 1 — Monthly Tracking", // intro: "Targets and actuals by month. Leave blank for months you don't yet have data for.", // cols: monthCols, // rows: [ // { label: "Sales Target", type: "currency", fields: Y1_TARGET_FIELDS }, // { // label: "Actual Sales", // type: "currency", // fields: months.map((m) => `Y1_M${m}_Actual_Sales`), // }, // { // label: "Transactions", // type: "number", // fields: months.map((m) => `Y1_M${m}_Transactions`), // }, // ], // }, // { // id: "y1_quarterly_metrics", // label: "Year 1 — Quarterly Tracking", // intro: "Quarterly operating ratios.", // cols: quarterCols, // rows: [ // { // label: "Labor", // type: "percent", // fields: quarters.map((q) => `Y1_Q${q}_Labor`), // }, // { // label: "Margin", // type: "percent", // fields: quarters.map((q) => `Y1_Q${q}_Margin`), // }, // { // label: "Member Sales", // type: "percent", // fields: quarters.map((q) => `Y1_Q${q}_Member_Sales_`), // }, // ], // }, // ], }; export const formConfig: FormConfig = { id: "org_engagement_check_in", title: "Co-op Check-in", subtitle: "Update tracking data for your co-op as you progress through the organizing stages. The questions you'll see depend on where you are in the framework.", stageField: "current_stage", sections: [stage0, stage1, stage2, stage3, stage4, stage5], }; /** Convenience: flatten all field configs across all sections for lookup by name. */ export const allFields = formConfig.sections.flatMap((s) => s.fields); /** Activity type the form writes its submissions as. */ export const ACTIVITY_TYPE_NAME = "Check-in (organizing)"; /** * Stage-snapshot field on the activity. The form does NOT write this field; * it's set manually by staff on dedicated stage-change check-in activities. * /api/data uses this field to derive the org's current stage by finding * the most recent activity where it is non-empty. */ export const ACTIVITY_STAGE_FIELD = `${G0}.Stage`; /** * RelationshipType.name_a_b for the relationship that links the form-filler * (Individual side) to the organization the form is about (Organization side). * * On `client.crm.fci.coop` we reuse the existing "Primary Contact" * relationship type (id 24) rather than introducing a dedicated * "Primary Form Contact" type. If you change this, also update the docs in * the parent repo's spec. */ export const FORM_CONTACT_RELATIONSHIP = "Primary Contact"; /** All distinct option_group_ids referenced by fields in this form. */ export const optionGroupIds = Array.from( new Set( allFields .map((f) => (f as FieldConfig).optionGroupId) .filter((id): id is number => typeof id === "number"), ), );