feat(onboarding/phase-6): setup wizard polish, seed script alignment, doctor visit slots

- Setup wizard: 3-pane layout with right-side live previews, resume
  banner, edit/copy icons on team step, AI prompt configuration
- Forms: employee-create replaces invite-member (no email invites),
  clinic form with address/hours/payment, doctor form with visit slots
- Seed script: aligned to current SDK schema — doctors created as
  workspace members (HelixEngage Manager role), visitingHours replaced
  by doctorVisitSlot entity, clinics seeded, portalUserId linked
  dynamically, SUB/ORIGIN/GQL configurable via env vars
- Pages: clinics + doctors CRUD updated for new schema, team settings
  with temp password + role assignment
- New components: time-picker, day-selector, wizard-right-panes,
  wizard-layout-context, resume-setup-banner
- Removed: invite-member-form (replaced by employee-create-form per
  no-email-invites rule)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 08:37:34 +05:30
parent efe67dc28b
commit f57fbc1f24
25 changed files with 3461 additions and 706 deletions

View File

@@ -121,22 +121,44 @@ export const AppointmentForm = ({
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
// Fetch doctors on mount
// Fetch doctors on mount. Doctors are hospital-wide — no single
// `clinic` field anymore. We pull the full visit-slot list via the
// doctorVisitSlots reverse relation so the agent can see which
// clinics + days this doctor covers in the picker.
useEffect(() => {
if (!isOpen) return;
apiClient.graphql<{ doctors: { edges: Array<{ node: any }> } }>(
`{ doctors(first: 50) { edges { node {
id name fullName { firstName lastName } department clinic { id name clinicName }
id name fullName { firstName lastName } department
doctorVisitSlots(first: 50) {
edges { node { id clinic { id clinicName } dayOfWeek startTime endTime } }
}
} } } }`,
).then(data => {
const docs = data.doctors.edges.map(e => ({
id: e.node.id,
name: e.node.fullName
? `Dr. ${e.node.fullName.firstName} ${e.node.fullName.lastName}`.trim()
: e.node.name,
department: e.node.department ?? '',
clinic: e.node.clinic?.clinicName ?? e.node.clinic?.name ?? '',
}));
const docs = data.doctors.edges.map(e => {
// Flatten the visit-slot list into a comma-separated
// clinic summary for display. Keep full slot data on
// the record in case future UX needs it (e.g., show
// only slots matching the selected date's weekday).
const slotEdges: Array<{ node: any }> = e.node.doctorVisitSlots?.edges ?? [];
const clinicNames = Array.from(
new Set(
slotEdges
.map((se) => se.node.clinic?.clinicName)
.filter((n): n is string => !!n),
),
);
return {
id: e.node.id,
name: e.node.fullName
? `Dr. ${e.node.fullName.firstName} ${e.node.fullName.lastName}`.trim()
: e.node.name,
department: e.node.department ?? '',
// `clinic` here is a display-only summary: "Koramangala, Whitefield"
// or empty if the doctor has no slots yet.
clinic: clinicNames.join(', '),
};
});
setDoctors(docs);
}).catch(() => {});
}, [isOpen]);