import { byDesignCopy } from './bookabilityCopy'; import { sortBlockers, type BookabilityBlocker } from './bookabilityReasons'; import type { Relaxation, RelaxableAxis } from './softConstraints'; /** * WHAT TO SAY WHEN A COMBINATION YIELDS NOTHING. * * Two halves, and they answer different questions: WHY nothing matched, and WHAT to change. Today the * person gets neither - an empty picker and a guess. Section 3 of * * BOTH HALVES COME FROM THE MATCHER. The blockers are what `evaluateScheduleMatch` recorded and the * remedy is what `softConstraints` proved would open something, so neither sentence can describe a * combination the flow would then refuse. Nothing here reasons about availability on its own. */ export interface DeadEndExplanation { /** Why nothing matched. Absent when there is no blocker to name - see `explainDeadEnd`. */ readonly why?: string; /** What to change. Absent when nothing would help; the caller must not invent one. */ readonly remedy?: string; } /** * How each axis is named to a person, and it is deliberately the thing rather than the field. * "practitionerId" is an identifier; "provider" is what they chose. */ const AXIS_NOUN: Record = { service: 'a different type of visit', location: 'another location', practitionerId: 'a different provider', }; /** * ONE SENTENCE EACH, AND NEITHER IS INVENTED. * * `why` is the FIRST blocker after sorting, because `BLOCKER_ORDER` exists precisely so the leading one * is the thing to act on - somebody told "no hours set" when the role is also deactivated fixes the * wrong thing. It uses `byDesignCopy`, so a deliberate curation says "staff can still book it" where * that is true rather than reading as a fault. * * `remedy` names the relaxation that opens the MOST schedules, which `softConstraints` has already * ranked and already proved non-empty. * * AND EITHER HALF MAY BE ABSENT, which is the whole discipline of this function. No blocker means * nothing is wrong with the configuration - the combination simply does not exist - and inventing a * cause would send somebody to fix a setting that is correct. No relaxation means no single change * helps, and a remedy there is a door that does not open. */ export const explainDeadEnd = ( blockers: readonly BookabilityBlocker[], relaxations: readonly Relaxation[], ): DeadEndExplanation => { const leading = sortBlockers([...blockers])[0]; const best = relaxations[0]; return { why: leading ? byDesignCopy(leading) : undefined, remedy: best ? `Try ${AXIS_NOUN[best.axis]}.` : undefined, }; }; /** * The two halves joined, for a surface with room for a string and not a layout - the same shape * `selfBookingGuardText` provides beside its own two-part message. * * Returns `undefined` rather than an empty string when there is nothing to say, so a caller cannot * render a blank alert by accident. A dead end nobody can explain is better met with the picker's own * empty state than with a sentence that says nothing. */ export const deadEndText = ( blockers: readonly BookabilityBlocker[], relaxations: readonly Relaxation[], ): string | undefined => { const { why, remedy } = explainDeadEnd(blockers, relaxations); const parts = [why, remedy].filter(Boolean); return parts.length > 0 ? parts.join(' ') : undefined; }; /** Exported for the copy test, which pins that no implementation vocabulary reaches a person. */ export const AXIS_NOUNS = AXIS_NOUN;