34. Facility onboarding intake survey
Date: 2026-06-14
Status
Accepted
Context
New clients are onboarded via a "New Client Intake Form" (a 40-question Google Form: facility
profile, every named contact — Administrator, DON, MDS coordinators, IT/EMR — service request, EMR,
access, compliance/BAA, billing recipient, and an authorized representative). That data lived only
in a spreadsheet. We want the survey in-app so onboarding produces a real client record, and we want
to seed the existing responses. Two of the three current responses already exist as facilities
(Huntington Hills as a prospect, Westchester Center as active), so seeding must not duplicate.
Decision
- Dedicated
facility_intakestable (migration0033), one row per submission, capturing all 40 questions verbatim. Multi-selects (facility types, services, access requirements, policies) aretext[](each option is atomic). Dirty single values (e.g. target start"3/6/0026", phone extensions, the"SAA"email placeholder) are kept as text — only the legal name is required, so fidelity is preserved over premature validation. Company-scoped admin RLS +set_updated_at/audit_rowtriggers, like other PII-bearing tables. Linked to the facility viafacility_id. - Intake → operational records. Submitting the survey (submitFacilityIntake)
creates the client or matches an existing facility by name (DBA, else legal) and re-activates
it (sets
status='active'), records the full intake, and seedsfacility_contactsfor the three operational roles: Administrator →admin, DON + MDS coordinators →clinical, invoice recipient →billing(only contacts with a real name + email; de-duped by email). The IT/EMR contact and authorized representative don't map to those three roles, so they live only on the intake record (avoids widening thefacility_contactsrole check). The name→facility match keeps the survey idempotent and prevents duplicate clients. Mapping (address parse, name resolution, contact derivation) is a pure, unit-tested lib (src/lib/facility/intake.ts). - Admin-filled survey at
/admin/facilities/new— the "Add a facility" flow (FacilityIntakeForm) — with visible labels and checkbox groups for the multi-selects (ux §Forms), organised into tabs (Facility · Key contacts · Service request · Compliance · Billing & notes) to limit scrolling. Panels stay mounted (hidden via thehiddenutility) so all fields submit together; on a validation error the form opens the tab holding the offending field (no nativerequired, since a hidden control can't be focused). The facility detail page shows the latest intake read-only. - Seed script scripts/seed-facility-intakes.mjs
transcribes the three responses and applies the same match-by-name / re-activate / insert-intake /
seed-contacts logic over Supabase REST (service key from
.env.local), DRY-RUN by default and idempotent (skips a facility's intake if one with the same legal name already exists).
Consequences
- The three responses seed cleanly: Huntington Hills flips
prospect → active, Westchester stays active, Pelham Parkway is created active — no duplicates. facility_intakesis the immutable submission record;facilities+facility_contactsare the editable operational projection. Re-submitting for the same facility adds a new intake row (history) and re-activates the facility; the detail page shows the latest.- The survey is admin-filled (the app has no facility-user role). A public/self-service intake link is a possible follow-up. Converting a won CRM lead straight into this intake is another.
- Like every admin service-role write in the app (other than the payroll-close DB function), these
inserts run through the service client with no JWT, so the
audit_rowtrigger records a null actor. The trigger tolerates this; capturingadmin.userIdvia anapp.actor_idGUC for all service-role actions is a known, codebase-wide follow-up rather than something done only here.