import { isPatientOnly, type BookabilityBlocker, type BookabilityWidener } from './bookabilityReasons'; // One sentence per cause, shared by every surface that explains bookability. // // Why it is centralised: two phrasings for one cause reads to an operator as two different problems. The // readiness alert, the authoring consequence lines and the S4 why-not panel all speak from here. // // Written for an office manager, not an engineer - no resource names, no field names, no FHIR. A test pins // that, because implementation vocabulary leaks in easily when the person writing the copy is reading code. export const BLOCKER_COPY: Record = { ROLE_INACTIVE: 'This provider’s role is deactivated.', // Says the THING and what to do about it, because "not enabled" is not a sentence an administrator // can act on. The term is "an active clinician seat" and not "paid" - the first seat is free and its // holder is fully bookable, so the word "paid" sends people to billing, which is the wrong screen. NO_CLINICIAN_SEAT: 'This provider doesn’t hold an active clinician seat, so they can’t be booked or offer services.', BLOCK_BOOKING_DISABLED: 'Booking is turned off for this schedule.', BLOCK_SELF_BOOKING_DISABLED: 'Patient self-booking is turned off for this schedule.', ORG_TELEHEALTH_DISABLED: 'This practice doesn’t offer video visits.', SERVICE_INACTIVE: 'This service is switched off.', SERVICE_NOT_OFFERED_AT_LOCATION: 'This service isn’t offered at this location.', // Phrased as a property of the SERVICE, not as something withheld: a practice marks surgery // not-eligible for video because it cannot be done by video. "Isn't offered as a video visit" would // read as an availability gap someone should go and fix. SERVICE_NOT_TELEHEALTH_ELIGIBLE: 'This service can’t be done as a video visit.', NOT_SELF_BOOKABLE_ORG: 'This service isn’t available for patient self-booking organization-wide.', NOT_SELF_BOOKABLE_PRACTITIONER: 'Self-booking is switched off for this provider on this service.', NOT_SELF_BOOKABLE_BLOCK: 'Self-booking is switched off for this service on this schedule block.', PRACTITIONER_NOT_OFFERING_SERVICE: 'This provider doesn’t offer this service.', BLOCK_DOES_NOT_OFFER_SERVICE: 'This schedule block doesn’t list this service.', NO_SCHEDULE_AT_LOCATION: 'This provider has no schedule at this location.', // A GAP, and worded so the fix is obvious - video has to be turned on for the specific schedule, it // is never inherited from an "all locations" block (see NO_VIRTUAL_LOCATION_ON_BLOCK's rationale). NO_VIRTUAL_LOCATION_ON_BLOCK: 'This schedule isn’t set up for video visits.', NEW_PATIENT_EXAM_NO_ELIGIBLE_BLOCK: 'New patients must book a consultation first, and no schedule here allows one.', BLOCK_NO_HOURS: 'This schedule has no weekly hours set.', BLOCK_NO_PLANNING_HORIZON: 'This schedule has no start and end date.', BLOCK_HORIZON_EXPIRED: 'This schedule’s date range has ended.', }; export const blockerCopy = (code: BookabilityBlocker): string => BLOCKER_COPY[code] ?? 'This isn’t bookable by patients.'; // For a BY-DESIGN blocker, say the other half of the truth. Reading "patients can't self-book this" // without "staff can still book it" is how a deliberate curation gets mistaken for breakage - and the // obvious "fix" would be to undo a decision the practice made on purpose. // // BUT ONLY WHERE IT IS TRUE. This used to append the reassurance unconditionally, which was // correct only while every by-design blocker was also patient-only. The telehealth gates are by-design and // stop EVERYONE: a practice with video switched off cannot take a video booking from the front desk // either. Promising a receptionist they can book it would send them to try something the server refuses. // The audience question is `isPatientOnly`, deliberately NOT `isByDesign` - see bookabilityReasons. export const byDesignCopy = (code: BookabilityBlocker): string => isPatientOnly(code) ? `${blockerCopy(code)} Staff can still book it for a patient.` : blockerCopy(code); export interface WidenerCounts { orgCatalogSize: number; orgLocationCount: number; } // Wideners are NOT errors - they describe config that is broader than the practice may have intended, and the // permissive fallbacks behind them are what let a small practice skip steps. The wording is deliberately // neutral-to-informative, never alarming. export const widenerCopy = (code: BookabilityWidener, counts: WidenerCounts): string => { switch (code) { // This sentence used to read "No services chosen - patients will see all N of the organization's // services here", and it was wrong in the one way that costs a practice a bookable provider. The // widening it reports is real, but it widens the PICKER, not what a patient is offered. // // The two questions are different and only one of them governs patient booking: // "which services may I pick for this block?" the full catalog, because the provider set none // "which blocks offer service X?" NOT one that lists none // A patient chooses a service FIRST, so patient booking only ever asks the second - and a block with // an empty service list answers no to all of them (resolveScheduleSelfBookableServices returns []). // Promising "patients will see all N" told a practice the permissive thing while the restrictive thing // was happening, so nobody had a reason to fill the list in. case 'WIDENED_ALL_SERVICES': return `This provider has no services of their own set, so you can choose from all ${counts.orgCatalogSize} of the organization’s services.`; case 'WIDENED_ALL_LOCATIONS': return `Left empty — this service is offered at all ${counts.orgLocationCount} location${ counts.orgLocationCount === 1 ? '' : 's' }.`; case 'WIDENED_SELF_BOOKABLE_BY_DEFAULT': return 'Patients can book this online. Online booking is on unless you turn it off.'; default: return ''; } };