// - the one line the HR workspace exists for, assembled from the four places its facts live. // // THE DEFECT WAS NEVER A MISSING LIST, IT WAS A MISSING JOIN. Invite - role - location - schedule - // services - NPI live on five screens with nothing joining them, so a practitioner invisible to booking // looks perfectly normal on every one - and the receptionist meets it as *"the calendar is empty"*. // // IT DERIVES NOTHING ITSELF. Every fact comes from the module that already owns it: // - bookability `getProviderReadiness`, which derives from the SAME resolver the patient flow uses // - absence `getScheduleAvailability(schedule).notAvailable`, where this product stores it // - the verdict `staffReadiness`, which owns only the employment layer above those two // That is not tidiness. Each of those has already had a duplicate grow beside it and drift - see // `practitionerBookingReadiness.ts`'s header for what the last one cost. // // **KNOWN LIMIT OF THE `schedulesList` IT IS HANDED, and it is NOT this hook's to fix.** // `useFhirSchedules` fetches PRACTITIONER-SCOPED when the viewer's active workspace is Practitioner, and // the result is cached on the shared ScheduleService context - so whoever loaded it first fixes the // scope for everyone after. A viewer holding `staff:manage` from inside the Practitioner workspace can // therefore be handed a list containing only their OWN blocks, and every colleague reads as "no // schedule": a page telling a practice that working clinicians are broken. // Raised as its own card ("the schedule list a staff surface reads may be scoped to the VIEWER"). // The two sibling tabs on this same record (Patient Booking, Services Offered) already read // the same list the same way, so this is inherited rather than introduced, and the fix belongs at the // fetch - deliberately NOT worked around here, because a per-surface guard is how three surfaces end up // with three different ideas of when the answer is trustworthy. import { useMemo } from 'react'; import type { Schedule } from 'fhir/r4'; import type { PractitionerWithRoles } from '../services/PractitionerService'; import { getScheduleAvailability } from '../services/ScheduleService'; import { getProviderReadiness, schedulesForPractitioner } from '../utils/practitionerBookingReadiness'; import { getScheduleLocationReferences } from '../utils/scheduleLocations'; import { staffFactsFrom } from '../utils/staff/staffFacts'; import { isOnLeaveNow } from '../utils/staff/scheduleAbsence'; import { staffReadiness, type StaffReadiness } from '../utils/staff/staffReadiness'; import { useStaffBookabilityContext } from './useStaffBookabilityContext'; import type { ServiceCatalogueBasis } from '../utils/booking/serviceCatalogueBasis'; interface Options { entry: PractitionerWithRoles | undefined; schedulesList: Schedule[]; /** The verified catalogue: with it, booking is a step only once services are verified. */ serviceBasis?: ServiceCatalogueBasis; /** Optional override, chiefly for tests. */ now?: Date; } export const useStaffReadiness = ({ entry, schedulesList, serviceBasis, now }: Options): StaffReadiness | undefined => { // The hook is called unconditionally - `entry` may be undefined while the person loads, and // bailing before this line would break the rules of hooks on every first render. const bookabilityCtx = useStaffBookabilityContext({ practitioners: useMemo(() => (entry ? [entry] : []), [entry]), serviceBasis, now, }); return useMemo(() => { if (!entry) return undefined; // THIS PERSON'S OWN schedules - a colleague's absence must not read as theirs. // Uses the SAME predicate `getProviderReadiness` uses, deliberately: I first wrote a // role-reference filter here, and a schedule carries both a Practitioner and a PractitionerRole // actor, so the two can disagree - leaving the absence sentence and the booking verdict describing // different sets of blocks. See that function's own comment. const own = schedulesForPractitioner(entry, schedulesList); // Flattened ACROSS every block they hold. An absence is recorded per schedule BLOCK, so a // clinician with three blocks needs three entries - this answers the READINESS question, so the // sentence is right even when one block was missed. It does NOT make that block unbookable; // only an entry on it does. const periods = own.flatMap((s) => getScheduleAvailability(s).notAvailable); const readiness = getProviderReadiness(entry, schedulesList, bookabilityCtx); return staffReadiness(staffFactsFrom({ practitioner: entry.practitioner, roles: entry.roles, booking: { staffBookable: readiness.staffBookable, blockers: readiness.blockers, basis: readiness.basis }, onLeave: isOnLeaveNow(periods, now ?? new Date()), scheduleHasLocation: own.some((s) => getScheduleLocationReferences(s).length > 0), // ⛔ `inviteAccepted` is deliberately NOT passed: it is not on these resources, and // `staffFactsFrom` defaults it TRUE rather than telling a whole practice that every existing // staff member has never signed in. })); }, [entry, schedulesList, bookabilityCtx, now]); };