mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-05-18 20:08:19 +00:00
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:
@@ -1,45 +1,76 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCircleCheck, faCircle } from '@fortawesome/pro-duotone-svg-icons';
|
||||
import { Button } from '@/components/base/buttons/button';
|
||||
import { cx } from '@/utils/cx';
|
||||
import { SETUP_STEP_NAMES, SETUP_STEP_LABELS, type SetupStepName, type SetupState } from '@/lib/setup-state';
|
||||
import { WizardLayoutContext } from './wizard-layout-context';
|
||||
|
||||
type WizardShellProps = {
|
||||
state: SetupState;
|
||||
activeStep: SetupStepName;
|
||||
onSelectStep: (step: SetupStepName) => void;
|
||||
onDismiss: () => void;
|
||||
// Form column (middle pane). The active step component renders
|
||||
// its form into this slot. The right pane is filled via the
|
||||
// WizardLayoutContext + a portal — see wizard-step.tsx.
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// Layout shell for the onboarding wizard. Renders a left-side step navigator
|
||||
// (with completed/active/upcoming visual states) and a right-side content
|
||||
// pane fed by the parent. The header has a "Skip for now" affordance that
|
||||
// dismisses the wizard for this workspace — once dismissed it never auto-shows
|
||||
// Layout shell for the onboarding wizard. Three-pane layout:
|
||||
// left — step navigator (fixed width)
|
||||
// middle — form (flexible, the focus column)
|
||||
// right — preview pane fed by the active step component (sticky,
|
||||
// hides below xl breakpoint)
|
||||
//
|
||||
// The whole shell is `fixed inset-0` so the document body cannot
|
||||
// scroll while the wizard is mounted — fixes the double-scrollbar
|
||||
// bug where the body was rendered taller than the viewport and
|
||||
// scrolled alongside the form column. The form and preview columns
|
||||
// each scroll independently inside the shell.
|
||||
//
|
||||
// The header has a "Skip for now" affordance that dismisses the
|
||||
// wizard for this workspace; once dismissed it never auto-shows
|
||||
// again on login.
|
||||
export const WizardShell = ({ state, activeStep, onSelectStep, onDismiss, children }: WizardShellProps) => {
|
||||
const completedCount = SETUP_STEP_NAMES.filter(s => state.steps[s].completed).length;
|
||||
export const WizardShell = ({
|
||||
state,
|
||||
activeStep,
|
||||
onSelectStep,
|
||||
onDismiss,
|
||||
children,
|
||||
}: WizardShellProps) => {
|
||||
const completedCount = SETUP_STEP_NAMES.filter((s) => state.steps[s].completed).length;
|
||||
const totalSteps = SETUP_STEP_NAMES.length;
|
||||
const progressPct = Math.round((completedCount / totalSteps) * 100);
|
||||
|
||||
// Callback ref → state — guarantees that consumers re-render once
|
||||
// the aside is mounted (a plain useRef would not propagate the
|
||||
// attached node back through the context). The element is also
|
||||
// updated to null on unmount so the context is always honest about
|
||||
// whether the slot is currently available for portals.
|
||||
const [rightPaneEl, setRightPaneEl] = useState<HTMLElement | null>(null);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-primary">
|
||||
{/* header */}
|
||||
<header className="border-b border-secondary bg-primary px-8 py-5">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between gap-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-primary">Set up your hospital</h1>
|
||||
<p className="mt-1 text-sm text-tertiary">
|
||||
{completedCount} of {totalSteps} steps complete · finish setup to start using your workspace
|
||||
</p>
|
||||
<WizardLayoutContext.Provider value={{ rightPaneEl }}>
|
||||
<div className="fixed inset-0 z-50 flex flex-col bg-primary">
|
||||
{/* Header — pinned. Progress bar always visible (grey
|
||||
track when 0%), sits flush under the title row. */}
|
||||
<header className="shrink-0 border-b border-secondary bg-primary">
|
||||
<div className="mx-auto flex w-full max-w-screen-2xl items-center justify-between gap-6 px-8 pt-4 pb-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-primary">Set up your hospital</h1>
|
||||
<p className="text-xs text-tertiary">
|
||||
{completedCount} of {totalSteps} steps complete · finish setup to start
|
||||
using your workspace
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button color="link-gray" size="sm" onClick={onDismiss}>
|
||||
Skip for now
|
||||
</Button>
|
||||
</div>
|
||||
{/* progress bar */}
|
||||
<div className="mx-auto mt-4 max-w-6xl">
|
||||
<div className="mx-auto w-full max-w-screen-2xl px-8 pb-3">
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-secondary">
|
||||
<div
|
||||
className="h-full rounded-full bg-brand-solid transition-all duration-300"
|
||||
@@ -49,9 +80,13 @@ export const WizardShell = ({ state, activeStep, onSelectStep, onDismiss, childr
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* body — step navigator + content */}
|
||||
<div className="mx-auto flex max-w-6xl gap-8 px-8 py-8">
|
||||
<nav className="w-72 shrink-0">
|
||||
{/* Body — three columns inside a fixed-height flex row.
|
||||
min-h-0 on the row + each column lets the inner
|
||||
overflow-y-auto actually take effect. */}
|
||||
<div className="mx-auto flex min-h-0 w-full max-w-screen-2xl flex-1 gap-6 px-8 py-6">
|
||||
{/* Left — step navigator. Scrolls if it overflows on
|
||||
very short viewports, but in practice it fits. */}
|
||||
<nav className="w-60 shrink-0 overflow-y-auto">
|
||||
<ol className="flex flex-col gap-1">
|
||||
{SETUP_STEP_NAMES.map((step, idx) => {
|
||||
const meta = SETUP_STEP_LABELS[step];
|
||||
@@ -64,7 +99,7 @@ export const WizardShell = ({ state, activeStep, onSelectStep, onDismiss, childr
|
||||
type="button"
|
||||
onClick={() => onSelectStep(step)}
|
||||
className={cx(
|
||||
'group flex w-full items-start gap-3 rounded-lg border px-3 py-3 text-left transition',
|
||||
'group flex w-full items-start gap-3 rounded-lg border px-3 py-2.5 text-left transition',
|
||||
isActive
|
||||
? 'border-brand bg-brand-primary'
|
||||
: 'border-transparent hover:bg-secondary',
|
||||
@@ -75,7 +110,9 @@ export const WizardShell = ({ state, activeStep, onSelectStep, onDismiss, childr
|
||||
icon={isComplete ? faCircleCheck : faCircle}
|
||||
className={cx(
|
||||
'size-5',
|
||||
isComplete ? 'text-success-primary' : 'text-quaternary',
|
||||
isComplete
|
||||
? 'text-success-primary'
|
||||
: 'text-quaternary',
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
@@ -99,8 +136,24 @@ export const WizardShell = ({ state, activeStep, onSelectStep, onDismiss, childr
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<main className="min-w-0 flex-1">{children}</main>
|
||||
{/* Middle — form column. min-w-0 prevents children from
|
||||
forcing the column wider than its flex basis (long
|
||||
inputs, etc.). overflow-y-auto so it scrolls
|
||||
independently of the right pane. */}
|
||||
<main className="flex min-w-0 flex-1 flex-col overflow-y-auto">{children}</main>
|
||||
|
||||
{/* Right — preview pane. Always rendered as a stable
|
||||
portal target (so the active step's WizardStep can
|
||||
createPortal into it via WizardLayoutContext).
|
||||
Hidden below xl breakpoint (1280px) so the wizard
|
||||
collapses cleanly to two columns on smaller screens.
|
||||
Independent scroll. */}
|
||||
<aside
|
||||
ref={setRightPaneEl}
|
||||
className="hidden w-80 shrink-0 overflow-y-auto xl:block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</WizardLayoutContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user