From 4420b648d4d93914df186c2ce4226b398bd11932 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Tue, 7 Apr 2026 07:33:25 +0530 Subject: [PATCH] feat(onboarding/phase-3): clinics, doctors, team invite/role CRUD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces Phase 2 placeholder routes for /settings/clinics and /settings/doctors with real list + add/edit slideouts backed directly by the platform's ClinicCreateInput / DoctorCreateInput mutations. Rewrites /settings/team to fetch roles via getRoles and let admins invite members (sendInvitations) and change roles (updateWorkspaceMemberRole). - src/components/forms/clinic-form.tsx — reusable form + GraphQL input transformer, handles address/phone/email composite types - src/components/forms/doctor-form.tsx — reusable form with clinic dropdown and currency conversion for consultation fees - src/components/forms/invite-member-form.tsx — multi-email chip input with comma-to-commit UX (AriaTextField doesn't expose onKeyDown) - src/pages/clinics.tsx — list + slideout using ClinicForm, marks the clinics setup step complete on first successful add - src/pages/doctors.tsx — list + slideout with parallel clinic fetch, disabled-state when no clinics exist, marks doctors step complete - src/pages/team-settings.tsx — replaces email-pattern role inference with real getRoles + in-row role Select, adds invite slideout, marks team step complete on successful invitation - src/main.tsx — routes /settings/clinics and /settings/doctors to real pages instead of SettingsPlaceholder stubs Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/forms/clinic-form.tsx | 263 ++++++++++++ src/components/forms/doctor-form.tsx | 260 ++++++++++++ src/components/forms/invite-member-form.tsx | 143 +++++++ src/main.tsx | 24 +- src/pages/clinics.tsx | 331 +++++++++++++++ src/pages/doctors.tsx | 358 ++++++++++++++++ src/pages/team-settings.tsx | 436 +++++++++++++++----- 7 files changed, 1682 insertions(+), 133 deletions(-) create mode 100644 src/components/forms/clinic-form.tsx create mode 100644 src/components/forms/doctor-form.tsx create mode 100644 src/components/forms/invite-member-form.tsx create mode 100644 src/pages/clinics.tsx create mode 100644 src/pages/doctors.tsx diff --git a/src/components/forms/clinic-form.tsx b/src/components/forms/clinic-form.tsx new file mode 100644 index 0000000..b928481 --- /dev/null +++ b/src/components/forms/clinic-form.tsx @@ -0,0 +1,263 @@ +import { Input } from '@/components/base/input/input'; +import { Select } from '@/components/base/select/select'; +import { TextArea } from '@/components/base/textarea/textarea'; +import { Toggle } from '@/components/base/toggle/toggle'; + +// Reusable clinic form used by both the /settings/clinics slideout and the +// /setup wizard step. The parent owns the form state so it can also own the +// submit button and the loading/error UI. +// +// Field shapes mirror the platform's ClinicCreateInput (which uses +// `addressCustom`, not `address`, and `onlineBooking`, not +// `onlineBookingEnabled` as the SDK field). See +// FortyTwoApps/helix-engage/src/objects/clinic.object.ts for the SDK source of +// truth; GraphQL-level differences are normalised in clinicFormToGraphQLInput. + +export type ClinicStatus = 'ACTIVE' | 'TEMPORARILY_CLOSED' | 'PERMANENTLY_CLOSED'; + +export type ClinicFormValues = { + clinicName: string; + addressStreet1: string; + addressStreet2: string; + addressCity: string; + addressState: string; + addressPostcode: string; + phone: string; + email: string; + weekdayHours: string; + saturdayHours: string; + sundayHours: string; + status: ClinicStatus; + walkInAllowed: boolean; + onlineBooking: boolean; + cancellationWindowHours: string; + arriveEarlyMin: string; + requiredDocuments: string; +}; + +export const emptyClinicFormValues = (): ClinicFormValues => ({ + clinicName: '', + addressStreet1: '', + addressStreet2: '', + addressCity: '', + addressState: '', + addressPostcode: '', + phone: '', + email: '', + weekdayHours: '9:00 AM - 6:00 PM', + saturdayHours: '9:00 AM - 2:00 PM', + sundayHours: 'Closed', + status: 'ACTIVE', + walkInAllowed: true, + onlineBooking: true, + cancellationWindowHours: '24', + arriveEarlyMin: '15', + requiredDocuments: '', +}); + +const STATUS_ITEMS = [ + { id: 'ACTIVE', label: 'Active' }, + { id: 'TEMPORARILY_CLOSED', label: 'Temporarily closed' }, + { id: 'PERMANENTLY_CLOSED', label: 'Permanently closed' }, +]; + +// Convert form state into the shape the platform's createClinic / updateClinic +// mutations expect. Only non-empty fields are included so the platform can +// apply its own defaults for the rest. +export const clinicFormToGraphQLInput = (v: ClinicFormValues): Record => { + const input: Record = { + clinicName: v.clinicName.trim(), + status: v.status, + walkInAllowed: v.walkInAllowed, + onlineBooking: v.onlineBooking, + }; + + const hasAddress = v.addressStreet1 || v.addressCity || v.addressState || v.addressPostcode; + if (hasAddress) { + input.addressCustom = { + addressStreet1: v.addressStreet1 || null, + addressStreet2: v.addressStreet2 || null, + addressCity: v.addressCity || null, + addressState: v.addressState || null, + addressPostcode: v.addressPostcode || null, + addressCountry: 'India', + }; + } + + if (v.phone.trim()) { + input.phone = { + primaryPhoneNumber: v.phone.trim(), + primaryPhoneCountryCode: 'IN', + primaryPhoneCallingCode: '+91', + additionalPhones: null, + }; + } + + if (v.email.trim()) { + input.email = { + primaryEmail: v.email.trim(), + additionalEmails: null, + }; + } + + if (v.weekdayHours.trim()) input.weekdayHours = v.weekdayHours.trim(); + if (v.saturdayHours.trim()) input.saturdayHours = v.saturdayHours.trim(); + if (v.sundayHours.trim()) input.sundayHours = v.sundayHours.trim(); + if (v.cancellationWindowHours.trim()) { + const n = Number(v.cancellationWindowHours); + if (!Number.isNaN(n)) input.cancellationWindowHours = n; + } + if (v.arriveEarlyMin.trim()) { + const n = Number(v.arriveEarlyMin); + if (!Number.isNaN(n)) input.arriveEarlyMin = n; + } + if (v.requiredDocuments.trim()) input.requiredDocuments = v.requiredDocuments.trim(); + + return input; +}; + +type ClinicFormProps = { + value: ClinicFormValues; + onChange: (value: ClinicFormValues) => void; +}; + +export const ClinicForm = ({ value, onChange }: ClinicFormProps) => { + const patch = (updates: Partial) => onChange({ ...value, ...updates }); + + return ( +
+ patch({ clinicName: v })} + /> + + + +
+

Address

+ patch({ addressStreet1: v })} + /> + patch({ addressStreet2: v })} + /> +
+ patch({ addressCity: v })} + /> + patch({ addressState: v })} + /> +
+ patch({ addressPostcode: v })} + /> +
+ +
+

Contact

+ patch({ phone: v })} + /> + patch({ email: v })} + /> +
+ +
+

Visiting hours

+ patch({ weekdayHours: v })} + /> +
+ patch({ saturdayHours: v })} + /> + patch({ sundayHours: v })} + /> +
+
+ +
+

Booking policy

+
+ patch({ walkInAllowed: checked })} + /> + patch({ onlineBooking: checked })} + /> +
+
+ patch({ cancellationWindowHours: v })} + /> + patch({ arriveEarlyMin: v })} + /> +
+