Build standalone CiviCRM check-in middleware
This commit is contained in:
220
config/form.ts
Normal file
220
config/form.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Form definition for the Org Engagement Check-in form.
|
||||
*
|
||||
* Fields and stages are sourced from the spec at
|
||||
* docs/superpowers/specs/2026-05-08-civi-webform-design.md and the
|
||||
* concrete field list provided 2026-05-09.
|
||||
*
|
||||
* The `civiField` property on each field is currently a placeholder
|
||||
* (`custom_<machine_name>`). Replace these with the actual CiviCRM custom
|
||||
* field references (`custom_<id>` or `<group_name>.<field_name>`) once the
|
||||
* field IDs are known. See README.md.
|
||||
*/
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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: [
|
||||
{ name: "current_stage", label: "Framework Stage", type: "readonly", civiField: "Food_Co_op_Organizing.Stage" },
|
||||
{ name: "stage_0_peer_group_participation", label: "Peer Group Participation", type: "text" },
|
||||
{ name: "stage_0_internal_startup_assessment", label: "Internal Startup Assessment", type: "textarea" },
|
||||
{ name: "stage_0_internal_startup_assessment_date", label: "Internal Startup Assessment Date", type: "date" },
|
||||
{ name: "stage_0_member_goal", label: "Member Goal for current Stage", type: "number" },
|
||||
{ name: "stage_0_members_current", label: "Members (current)", type: "number" },
|
||||
{ name: "stage_0_total_members_at_opening", label: "Total members at opening", type: "number" },
|
||||
{ name: "stage_0_volunteers_helping_in_store", label: "Volunteers Helping In Store", type: "number" },
|
||||
{ name: "stage_0_work_from_members", label: "Work from Members", type: "text", help: "Hours, dollars, or descriptive — your choice." },
|
||||
{ name: "stage_0_total_square_ft", label: "Total square ft", type: "number" },
|
||||
{ name: "stage_0_retail_sq_ft", label: "Retail sq ft", type: "number" },
|
||||
{ name: "stage_0_latest_sources_uses_doc", label: "Latest Sources and Uses doc", type: "file" },
|
||||
{ name: "stage_0_latest_pro_forma_doc", label: "Latest Pro Forma doc", type: "file" },
|
||||
{ name: "stage_0_projected_y1_sales", label: "Projected Year 1 Sales", type: "currency" },
|
||||
{ name: "stage_0_projected_y2_sales", label: "Projected Year 2 Sales", type: "currency" },
|
||||
{ name: "stage_0_projected_y3_sales", label: "Projected Year 3 Sales", type: "currency" },
|
||||
{ name: "stage_0_projected_sales_established", label: "Projected sales once established", type: "currency" },
|
||||
{ name: "stage_0_projected_ftes", label: "Projected FTEs", type: "number", step: 0.1 },
|
||||
{ name: "stage_0_actual_fte", label: "Actual FTE", type: "number", step: 0.1 },
|
||||
{ name: "stage_0_total_cost_of_project", label: "Total cost of project", type: "currency" },
|
||||
{ name: "stage_0_member_equity_total_needed", label: "Member equity total needed", type: "currency" },
|
||||
{ name: "stage_0_member_equity_raised", label: "Member equity raised", type: "currency" },
|
||||
{ name: "stage_0_member_loans_total_needed", label: "Member loans total needed", type: "currency" },
|
||||
{ name: "stage_0_member_loans_raised", label: "Member loans raised", type: "currency" },
|
||||
{ name: "stage_0_member_preferred_shares_total_needed", label: "Member preferred shares total needed", type: "currency" },
|
||||
{ name: "stage_0_member_preferred_shares_raised", label: "Member preferred shares raised", type: "currency" },
|
||||
{ name: "stage_0_bank_debt_total_needed", label: "Bank debt total needed", type: "currency" },
|
||||
{ name: "stage_0_bank_debt_raised", label: "Bank debt raised", type: "currency" },
|
||||
{ name: "stage_0_grant_donations_needed", label: "Grants / Donations Needed", type: "currency" },
|
||||
{ name: "stage_0_grants_donations_raised", label: "Grants / Donations Raised", type: "currency" },
|
||||
{ name: "stage_0_other_sources_total_needed", label: "Other sources total needed", type: "currency" },
|
||||
{ name: "stage_0_other_sources_raised", label: "Other sources raised", type: "currency" },
|
||||
{ name: "stage_0_date_closed_folded", label: "Date Closed / Folded", type: "date" },
|
||||
].map((f) => ({ ...f, civiField: f.civiField ?? `custom_${f.name}` })) as FieldConfig[],
|
||||
};
|
||||
|
||||
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: "stage_1_preliminary_market_assessment", label: "Preliminary Market Assessment", type: "textarea" },
|
||||
{ name: "stage_1_preliminary_market_assessment_upload", label: "Preliminary Market Assessment — Upload", type: "file" },
|
||||
{ name: "stage_1_preliminary_sources_uses", label: "Preliminary Sources And Uses", type: "textarea" },
|
||||
{ name: "stage_1_preliminary_sources_uses_upload", label: "Preliminary Sources and Uses — Upload", type: "file" },
|
||||
{ name: "stage_1_vision", label: "Vision", type: "textarea" },
|
||||
{ name: "stage_1_vision_upload", label: "Vision — Upload", type: "file" },
|
||||
{ name: "stage_1_business_concept", label: "Business Concept", type: "textarea" },
|
||||
{ name: "stage_1_business_concept_upload", label: "Business Concept — Upload", type: "file" },
|
||||
].map((f) => ({ ...f, civiField: `custom_${f.name}` })) as FieldConfig[],
|
||||
};
|
||||
|
||||
const stage2: StageSectionConfig = {
|
||||
rank: 2,
|
||||
id: "stage_2",
|
||||
label: "Stage 2 — Feasibility",
|
||||
visibleWhen: visibleAtOrAfter(S.Feasibility, S.BusinessFeasibility, S.StoreImplementation, S.Stabilize),
|
||||
fields: [
|
||||
{ name: "stage_2_market_study_date", label: "Market Study Date", type: "date" },
|
||||
{ name: "stage_2_market_study_upload", label: "Market Study — Upload", type: "file" },
|
||||
{ name: "stage_2_pro_forma_date", label: "Pro Forma — date completed", type: "date" },
|
||||
{ name: "stage_2_pro_forma_upload", label: "Pro Forma — Upload", type: "file" },
|
||||
{
|
||||
name: "stage_2_pro_forma_viability",
|
||||
label: "Pro Forma Viability",
|
||||
type: "select",
|
||||
options: [
|
||||
{ value: "viable", label: "Viable" },
|
||||
{ value: "marginal", label: "Marginal" },
|
||||
{ value: "not_viable", label: "Not viable" },
|
||||
{ value: "tbd", label: "Not yet determined" },
|
||||
],
|
||||
},
|
||||
{ name: "stage_2_business_plan", label: "Business Plan", type: "textarea" },
|
||||
{ name: "stage_2_business_plan_upload", label: "Business Plan — Upload", type: "file" },
|
||||
{ name: "stage_2_board_self_assessment", label: "Board Self Assessment", type: "textarea" },
|
||||
{ name: "stage_2_board_self_assessment_upload", label: "Board Self Assessment — Upload", type: "file" },
|
||||
{ name: "stage_2_governance_system", label: "Governance System Used", type: "text" },
|
||||
].map((f) => ({ ...f, civiField: `custom_${f.name}` })) as FieldConfig[],
|
||||
};
|
||||
|
||||
const stage3: StageSectionConfig = {
|
||||
rank: 3,
|
||||
id: "stage_3",
|
||||
label: "Stage 3 — Connect & Gather",
|
||||
visibleWhen: visibleAtOrAfter(S.BusinessFeasibility, S.StoreImplementation, S.Stabilize),
|
||||
fields: [
|
||||
{ name: "stage_3_site_loi_date", label: "Site: Letter of Intent Date", type: "date" },
|
||||
{ name: "stage_3_site_loi_upload", label: "Site: Letter of Intent — Upload", type: "file" },
|
||||
{ name: "stage_3_own_building_property", label: "Own the building / property", type: "boolean" },
|
||||
{ name: "stage_3_capital_campaign_owner_participation_pct", label: "Capital Campaign: Owner Participation %", type: "percent" },
|
||||
{ name: "stage_3_capital_campaign_avg_owner_investment", label: "Capital Campaign: Average Owner Investment", type: "currency" },
|
||||
{ name: "stage_3_capital_stack", label: "Capital Stack", type: "textarea" },
|
||||
{ name: "stage_3_project_manager_hire_date", label: "Project Manager — Date of Hire", type: "date" },
|
||||
{ name: "stage_3_store_design_plan_completion", label: "Store Design — Plan Completion", type: "date" },
|
||||
{ name: "stage_3_store_designer", label: "Store Designer", type: "text" },
|
||||
{ name: "stage_3_ncg_member", label: "NCG Member", type: "boolean" },
|
||||
{ name: "stage_3_ncg_corridor", label: "NCG Corridor", type: "text" },
|
||||
{ name: "stage_3_infra_member", label: "INFRA Member", type: "boolean" },
|
||||
{ name: "stage_3_other_distributors", label: "Other Distributors", type: "text" },
|
||||
].map((f) => ({ ...f, civiField: `custom_${f.name}` })) as FieldConfig[],
|
||||
};
|
||||
|
||||
const stage4: StageSectionConfig = {
|
||||
rank: 4,
|
||||
id: "stage_4",
|
||||
label: "Stage 4 — Excite & Build",
|
||||
visibleWhen: visibleAtOrAfter(S.StoreImplementation, S.Stabilize),
|
||||
fields: [
|
||||
{ name: "stage_4_projected_opening_date", label: "Projected Opening Date", type: "date" },
|
||||
{ name: "stage_4_construction_completion_date", label: "Construction Completion Date", type: "date" },
|
||||
{ name: "stage_4_mission_transition_plan_date", label: "Mission Transition Plan — Date", type: "date" },
|
||||
{ name: "stage_4_mission_transition_plan_upload", label: "Mission Transition Plan — Upload", type: "file" },
|
||||
{ name: "stage_4_gm_name", label: "General Manager — Name", type: "text" },
|
||||
{ name: "stage_4_gm_phone", label: "General Manager Phone", type: "phone" },
|
||||
{ name: "stage_4_gm_email", label: "General Manager Email", type: "email" },
|
||||
{ name: "stage_4_gm_hire_date", label: "General Manager — Date of Hire", type: "date" },
|
||||
{ name: "stage_4_gm_background", label: "General Manager Background", type: "textarea" },
|
||||
{ name: "stage_4_gm_support_training", label: "GM Support and Training", type: "textarea" },
|
||||
{ name: "stage_4_gm_support_team", label: "GM Support Team", type: "textarea" },
|
||||
].map((f) => ({ ...f, civiField: `custom_${f.name}` })) as FieldConfig[],
|
||||
};
|
||||
|
||||
// Stage 5 has 12 monthly + 4 quarterly trackers across multiple metrics; build programmatically.
|
||||
const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
||||
const quarters = [1, 2, 3, 4];
|
||||
|
||||
const stage5: StageSectionConfig = {
|
||||
rank: 5,
|
||||
id: "stage_5",
|
||||
label: "Stage 5 — Fulfill & Stabilize",
|
||||
visibleWhen: visibleAtOrAfter(S.Stabilize),
|
||||
fields: [
|
||||
{ name: "stage_5_date_opened", label: "Date Opened", type: "date" as const },
|
||||
{ name: "stage_5_y1_actual_sales", label: "Y1 Actual Sales", type: "currency" as const },
|
||||
...months.map((m) => ({
|
||||
name: `stage_5_y1_target_m${m}`,
|
||||
label: `Y1 Monthly Sales Target: M${m}`,
|
||||
type: "currency" as const,
|
||||
})),
|
||||
...months.map((m) => ({
|
||||
name: `stage_5_y1_actual_m${m}`,
|
||||
label: `Y1 M${m} Actual Sales`,
|
||||
type: "currency" as const,
|
||||
})),
|
||||
...months.map((m) => ({
|
||||
name: `stage_5_y1_transactions_m${m}`,
|
||||
label: `Y1 M${m} Transactions`,
|
||||
type: "number" as const,
|
||||
})),
|
||||
...quarters.map((q) => ({
|
||||
name: `stage_5_y1_q${q}_labor`,
|
||||
label: `Y1 Q${q} Labor`,
|
||||
type: "number" as const,
|
||||
step: 0.1,
|
||||
help: "FTEs",
|
||||
})),
|
||||
...quarters.map((q) => ({
|
||||
name: `stage_5_y1_q${q}_margin`,
|
||||
label: `Y1 Q${q} Margin`,
|
||||
type: "percent" as const,
|
||||
})),
|
||||
...quarters.map((q) => ({
|
||||
name: `stage_5_y1_q${q}_member_sales_pct`,
|
||||
label: `Y1 Q${q} Member Sales %`,
|
||||
type: "percent" as const,
|
||||
})),
|
||||
].map((f) => ({ ...f, civiField: `custom_${f.name}` })) as FieldConfig[],
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"stageField": "custom_stage_field",
|
||||
"stages": [
|
||||
{
|
||||
"id": 0,
|
||||
"label": "Stage 0: Intake",
|
||||
"fields": [
|
||||
{ "name": "first_name", "label": "First Name", "entity": "Contact", "crmField": "first_name" },
|
||||
{ "name": "last_name", "label": "Last Name", "entity": "Contact", "crmField": "last_name" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"label": "Stage 1: Assessment",
|
||||
"fields": [
|
||||
{ "name": "email", "label": "Email", "entity": "Contact", "crmField": "email_primary.email" },
|
||||
{ "name": "phone", "label": "Phone", "entity": "Contact", "crmField": "phone_primary.phone" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"label": "Stage 2: Planning",
|
||||
"fields": [
|
||||
{ "name": "custom_planning_date", "label": "Planning Date", "entity": "Contact", "crmField": "custom_123" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"label": "Stage 3: Implementation",
|
||||
"fields": [
|
||||
{ "name": "custom_status", "label": "Implementation Status", "entity": "Contact", "crmField": "custom_124" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"label": "Stage 4: Evaluation",
|
||||
"fields": [
|
||||
{ "name": "custom_results", "label": "Evaluation Results", "entity": "Contact", "crmField": "custom_125" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"label": "Stage 5: Completion",
|
||||
"fields": [
|
||||
{ "name": "custom_completion_notes", "label": "Completion Notes", "entity": "Contact", "crmField": "custom_126" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user