import type { HealthcareService, Location } from 'fhir/r4'; import { formatSingleAddress } from './fhirAddress'; import { formatFhirCodeableConcept } from './fhirCodeableConcept'; // scope item 4 - a LOCATION IS A SUB-PRACTICE, NOT AN ADDRESS. // // The finding, and it is about the UI rather than the data: each location already carries its own // name, address, practice type and services - Great River's are typed `HOSP` / `ER` / `PHARM` - and the // screen still presented them as addresses with a type appended. // // WHY THAT MATTERS BEYOND TIDINESS: a practice reading its own locations as a list of addresses // cannot see that its pharmacy and its emergency department are DIFFERENT PRACTICES with different // services. They then look for the difference somewhere else, or conclude the product cannot express it // - when it already does. The data was never the problem. export interface LocationPractice { /** What this place is - its practice type, not its street. */ practiceType?: string; address?: string; /** The services offered HERE. The part that makes it a practice rather than a pin on a map. */ services: string[]; /** True when the place is named and typed but offers nothing yet - a real, actionable state. */ hasNoServices: boolean; } const serviceName = (service: HealthcareService): string => service.name?.trim() || (service.type?.[0] ? formatFhirCodeableConcept(service.type[0]) : '') || (service.category?.[0] ? formatFhirCodeableConcept(service.category[0]) : '') || ''; /** * The services a HealthcareService says it is offered at. * * A service naming NO location is not offered everywhere - it is unplaced, and claiming it belongs to * every site would put a pharmacy's dispensing under the emergency department. An org-wide service is a * real thing, but it has to say so; silence is not a claim. */ export function servicesAtLocation( location: Location, services: HealthcareService[], ): HealthcareService[] { const locationId = location.id; if (!locationId) return []; return services.filter((service) => (service.location ?? []).some((reference) => reference.reference === `Location/${locationId}`)); } export function describeLocationAsPractice( location: Location, services: HealthcareService[], ): LocationPractice { const practiceType = location.type?.[0] ? formatFhirCodeableConcept(location.type[0]) || undefined : undefined; // Guarded rather than asserted: `formatSingleAddress` takes a required Address, and a location // with none is an ordinary state (a telehealth-only practice has no street), not something to // coerce past the type. const address = location.address ? formatSingleAddress(location.address) || undefined : undefined; const offered = servicesAtLocation(location, services) .map(serviceName) .filter((name) => name !== ''); // Deduplicated and stable: two services can share a display name, and a list that repeats one // reads as a data error rather than as two bookable things. const unique = [...new Set(offered)].sort((a, b) => a.localeCompare(b)); return { practiceType, address, services: unique, // Only meaningful once the place is actually set up. A location with no type and no address is // half-created, and telling that admin "no services" points at the wrong problem. hasNoServices: unique.length === 0 && Boolean(practiceType || address), }; } /** * The one-line summary under a location's name. * * IT USED TO READ `type - address`, which is an ADDRESS with a label on it. Leading with what the * place IS, and naming what it OFFERS, is the whole change - the address is where it is, not what it is. */ export function locationPracticeSummary(practice: LocationPractice): string { const parts = [ practice.practiceType, practice.services.length > 0 ? practice.services.slice(0, 3).join(', ') + (practice.services.length > 3 ? ` +${practice.services.length - 3} more` : '') : undefined, ].filter(Boolean); // Never an empty string on a row - a blank line reads as a broken render rather than as a place // nobody has finished setting up. if (parts.length === 0) return practice.address || 'Nothing set up here yet'; return parts.join(' ยท '); }