mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-12 02:38:15 +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:
73
src/components/application/date-picker/time-picker.tsx
Normal file
73
src/components/application/date-picker/time-picker.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Select } from "@/components/base/select/select";
|
||||
|
||||
// 30-minute increments from 05:00 to 23:00 → 37 slots.
|
||||
// Covers every realistic clinic opening / closing time.
|
||||
// Values are 24-hour HH:MM strings — the same format stored on the
|
||||
// Clinic + DoctorVisitSlot entities in the platform. Labels are
|
||||
// 12-hour format with AM/PM for readability.
|
||||
const TIME_SLOTS = Array.from({ length: 37 }, (_, i) => {
|
||||
const totalMinutes = 5 * 60 + i * 30;
|
||||
const hour = Math.floor(totalMinutes / 60);
|
||||
const minute = totalMinutes % 60;
|
||||
const h12 = hour % 12 || 12;
|
||||
const period = hour >= 12 ? "PM" : "AM";
|
||||
const id = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
|
||||
const label = `${h12}:${String(minute).padStart(2, "0")} ${period}`;
|
||||
return { id, label };
|
||||
});
|
||||
|
||||
type TimePickerProps = {
|
||||
/** Field label rendered above the select. */
|
||||
label?: string;
|
||||
/** Current value in 24-hour HH:MM format, or null when unset. */
|
||||
value: string | null;
|
||||
/** Called with the new HH:MM string when the user picks a slot. */
|
||||
onChange: (value: string) => void;
|
||||
isRequired?: boolean;
|
||||
isDisabled?: boolean;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
// A minimal time-of-day picker built on top of the existing base
|
||||
// Select component. Intentionally dropdown-based rather than the
|
||||
// full DateTimePicker popover pattern from the reference demo —
|
||||
// the clinic + doctor flows only need time, not date, and a
|
||||
// dropdown is faster to use when the agent already knows the time.
|
||||
//
|
||||
// Use this for: clinic.opensAt / closesAt, doctorVisitSlot.startTime /
|
||||
// endTime. For time-AND-date (appointment scheduling), stick with the
|
||||
// existing DatePicker in the same directory.
|
||||
export const TimePicker = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
isRequired,
|
||||
isDisabled,
|
||||
placeholder = "Select time",
|
||||
}: TimePickerProps) => (
|
||||
<Select
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
items={TIME_SLOTS}
|
||||
selectedKey={value}
|
||||
onSelectionChange={(key) => {
|
||||
if (key !== null) onChange(String(key));
|
||||
}}
|
||||
isRequired={isRequired}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{(slot) => <Select.Item id={slot.id} label={slot.label} />}
|
||||
</Select>
|
||||
);
|
||||
|
||||
// Format a 24-hour HH:MM string as a 12-hour display label (e.g.
|
||||
// "09:30" → "9:30 AM"). Useful on list/detail pages that render
|
||||
// stored clinic hours without re-mounting the picker.
|
||||
export const formatTimeLabel = (hhmm: string | null | undefined): string => {
|
||||
if (!hhmm) return "—";
|
||||
const [h, m] = hhmm.split(":").map(Number);
|
||||
if (Number.isNaN(h) || Number.isNaN(m)) return hhmm;
|
||||
const h12 = h % 12 || 12;
|
||||
const period = h >= 12 ? "PM" : "AM";
|
||||
return `${h12}:${String(m).padStart(2, "0")} ${period}`;
|
||||
};
|
||||
108
src/components/application/day-selector/day-selector.tsx
Normal file
108
src/components/application/day-selector/day-selector.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
// Keys match the Clinic entity's openMonday..openSunday fields
|
||||
// directly — no translation layer needed when reading/writing the
|
||||
// form value into GraphQL mutations.
|
||||
export type DayKey =
|
||||
| "monday"
|
||||
| "tuesday"
|
||||
| "wednesday"
|
||||
| "thursday"
|
||||
| "friday"
|
||||
| "saturday"
|
||||
| "sunday";
|
||||
|
||||
export type DaySelection = Record<DayKey, boolean>;
|
||||
|
||||
const DAYS: { key: DayKey; label: string }[] = [
|
||||
{ key: "monday", label: "Mon" },
|
||||
{ key: "tuesday", label: "Tue" },
|
||||
{ key: "wednesday", label: "Wed" },
|
||||
{ key: "thursday", label: "Thu" },
|
||||
{ key: "friday", label: "Fri" },
|
||||
{ key: "saturday", label: "Sat" },
|
||||
{ key: "sunday", label: "Sun" },
|
||||
];
|
||||
|
||||
type DaySelectorProps = {
|
||||
/** Selected-state for each weekday. */
|
||||
value: DaySelection;
|
||||
/** Fires with the full updated selection whenever a pill is tapped. */
|
||||
onChange: (value: DaySelection) => void;
|
||||
/** Optional heading above the pills. */
|
||||
label?: string;
|
||||
/** Optional helper text below the pills. */
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
// Seven tappable Mon–Sun pills. Used on the Clinic form to pick which
|
||||
// days the clinic is open, since the Clinic entity has seven separate
|
||||
// BOOLEAN fields (openMonday..openSunday) — SDK has no MULTI_SELECT.
|
||||
// Also reusable anywhere else we need a weekly-recurrence picker
|
||||
// (future: follow-up schedules, on-call rotations).
|
||||
export const DaySelector = ({ value, onChange, label, hint }: DaySelectorProps) => (
|
||||
<div className="flex flex-col gap-2">
|
||||
{label && (
|
||||
<span className="text-sm font-medium text-secondary">{label}</span>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{DAYS.map(({ key, label: dayLabel }) => {
|
||||
const isSelected = !!value[key];
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => onChange({ ...value, [key]: !isSelected })}
|
||||
className={cx(
|
||||
"flex h-10 min-w-12 items-center justify-center rounded-full border px-4 text-sm font-semibold transition duration-100 ease-linear",
|
||||
isSelected
|
||||
? "border-brand bg-brand-solid text-white hover:bg-brand-solid_hover"
|
||||
: "border-secondary bg-primary text-secondary hover:border-primary hover:bg-secondary_hover",
|
||||
)}
|
||||
aria-pressed={isSelected}
|
||||
>
|
||||
{dayLabel}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{hint && <span className="text-xs text-tertiary">{hint}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Helper factories — use these instead of spelling out the empty
|
||||
// object literal everywhere.
|
||||
export const emptyDaySelection = (): DaySelection => ({
|
||||
monday: false,
|
||||
tuesday: false,
|
||||
wednesday: false,
|
||||
thursday: false,
|
||||
friday: false,
|
||||
saturday: false,
|
||||
sunday: false,
|
||||
});
|
||||
|
||||
// The default new-clinic state: Mon–Sat open, Sun closed. Matches the
|
||||
// typical Indian outpatient hospital schedule.
|
||||
export const defaultDaySelection = (): DaySelection => ({
|
||||
monday: true,
|
||||
tuesday: true,
|
||||
wednesday: true,
|
||||
thursday: true,
|
||||
friday: true,
|
||||
saturday: true,
|
||||
sunday: false,
|
||||
});
|
||||
|
||||
// Format a DaySelection as a compact human-readable string for list
|
||||
// pages (e.g. "Mon–Fri", "Mon–Sat", "Mon Wed Fri"). Collapses
|
||||
// consecutive selected days into ranges.
|
||||
export const formatDaySelection = (sel: DaySelection): string => {
|
||||
const openKeys = DAYS.filter((d) => sel[d.key]).map((d) => d.label);
|
||||
if (openKeys.length === 0) return "Closed";
|
||||
if (openKeys.length === 7) return "Every day";
|
||||
// Monday-Friday, Monday-Saturday shorthand
|
||||
if (openKeys.length === 5 && openKeys.join(",") === "Mon,Tue,Wed,Thu,Fri") return "Mon–Fri";
|
||||
if (openKeys.length === 6 && openKeys.join(",") === "Mon,Tue,Wed,Thu,Fri,Sat") return "Mon–Sat";
|
||||
return openKeys.join(" ");
|
||||
};
|
||||
Reference in New Issue
Block a user