import { detectWideners } from './bookabilityWideners'; import { widenerCopy } from './bookabilityCopy'; // What the "Services offered here" picker is actually choosing FROM, said out loud. // // The fallback this narrates is DELIBERATE and stays. In ManageScheduleModal the block's candidate services // are scoped to the practitioner's offered-service set - and when that set is EMPTY it falls back to the whole // org catalog, so pre-migration schedules keep working and a one-provider practice never has to fill it in. // That is not a bug. It is just invisible, which IS the bug. We narrate; we do not remove. export interface BlockServiceScopeInput { /** Size of the practitioner's offered-service set (PractitionerRole.healthcareService). */ offeredCount: number; /** How many services survive the location filter and are therefore actually pickable right now. */ eligibleCount: number; orgCatalogSize: number; /** How many services this BLOCK currently lists. 0 no patient can self-book it, for anything. */ selectedCount: number; } export function describeBlockServiceScope(input: BlockServiceScopeInput): { /** Draw the operator's attention - amber + an icon. NOT "the config is broader than intended": the * sharpest case is the opposite, a block that offers nothing. It has only ever driven styling. */ warn: boolean; message: string; } { // What the picker is choosing FROM, said once, so the consequence sentence below can borrow it. const widened = detectWideners({ practitionerOfferedServiceCount: input.offeredCount, orgCatalogSize: input.orgCatalogSize, // Not the location or self-bookable axis - this line is only about the SERVICE scope of the picker. serviceLocationCount: 1, orgLocationCount: 1, selfBookableSetExplicitly: true, }).includes('WIDENED_ALL_SERVICES'); const choicesSentence = widened ? widenerCopy('WIDENED_ALL_SERVICES', { orgCatalogSize: input.orgCatalogSize, orgLocationCount: 1 }) : `Choosing from the ${input.offeredCount} service${ input.offeredCount === 1 ? '' : 's' } this provider offers.`; // Checked FIRST: telling someone they are "choosing from 3 services" while the picker is empty would be // actively misleading, so the dead-end message wins over everything below. if (input.eligibleCount === 0) { return { warn: true, message: 'This provider offers none of their services at the selected location. Patients cannot book this block here.', }; } // THE CONSEQUENCE OF LEAVING THIS EMPTY, which nothing on this screen used to say. // Patient booking asks a block "do you offer service X?" and a block listing nothing answers no to every // service - so the provider is not offered at all, with no error anywhere. The practice's only clue was a // dismissible save warning naming a single service. Staff are unaffected: the receptionist picker falls // back to the whole catalog rather than blocking (ReceptionistBookAppointment.tsx:740), which is why the // reassurance is TRUE here and must not be dropped - without it this reads as breakage rather than a step. if (input.selectedCount === 0) { return { warn: true, message: `No services are listed for this block. Add a service so patients can book it. Staff can still book it. ${choicesSentence}`, }; } return { warn: widened, message: choicesSentence }; }