import { scannedPhotoSrc } from "../../utils/fhirPhoto"; import type { Organization } from "fhir/r4"; import { ORG_LOGO_EXTENSION_URL } from "../../config/organizationExtensions"; /** * WHAT THE PORTAL HEADER SHOWS: the practice's logo when it has one, and its name, always. * * This used to be a choice. Spencer, 2026-09-04: "maybe we just get rid of the option for orgs to upload * their own custom banners and backgrounds. not really worth the rabbit hole on how to design around it." * * WHAT THE CHOICE COST, and it is the reason the removal is an improvement rather than a retreat. The * header carried a second uploadable image, three switches deciding whether the logo, that image and the * name each appeared, and a treatment putting the image behind the whole header under a scrim. The name * defaulted OFF whenever an image would render - so the ordinary outcome for a practice that uploaded a * banner was a patient portal header that never said whose practice it was. Every approved deck draws the * name as text. * * So the header is now the same component for every practice: a mark if they have one, and the name. * * This module decides WHAT to show, never how it looks, and it reads the record only - no clock, no fetch, * no DOM. */ export interface HeaderBranding { /** The practice's logo, when the record carries one this component can actually render. */ logoUrl?: string; } /** * SOMETHING AN img CAN ACTUALLY LOAD - which is NOT always what is stored. * * The defect this handles: valueAttachment.url holds an S3 object key, not an address. PhotoUploadService * returns a key, the server's email processor reads it with a GetObjectCommand, and every other client * surface presigns it. Rendering the raw key as a src paints a broken image. * * The same upload writes a base64 thumbnail alongside the key, which is why data is checked first - and a * 30px-tall header wants the thumbnail anyway. Presigning here was tried and rejected: it drags the upload * and http services into a frame that is deliberately dependency-free, and the header's own test is that * it must not take the app down. * * An unresolvable key returns undefined rather than itself, so the header simply shows the name. Handing * back a key would paint a broken image instead. */ const renderableAttachmentUrl = ( organization: Organization | null | undefined, extensionUrl: string, ): string | undefined => { const attachment = organization?.extension?.find((e) => e.url === extensionUrl)?.valueAttachment; const scanned = scannedPhotoSrc(attachment); if (scanned) { return scanned; } const stored = attachment?.url?.trim(); if (!stored) return undefined; // An address we can use as-is. A stored https:// is legitimate - some records were written that way - // and presigning one would turn a working image into a failure. return /^(https?:|data:|blob:)/i.test(stored) ? stored : undefined; }; export const resolveHeaderBranding = (organization?: Organization | null): HeaderBranding => ({ logoUrl: renderableAttachmentUrl(organization, ORG_LOGO_EXTENSION_URL), });