import { Attachment } from 'fhir/r4'; /** * WHAT A PATIENT MAY ATTACH TO A QUESTIONNAIRE ANSWER, and it MIRRORS the server rather than deciding. * * The authority is `UPLOAD_TYPES.document.allowedContentTypes` in the api's `presignUpload.ts`. Nothing * makes the two repos agree automatically, so this list is deliberately the NARROWER of the two: it * stops a patient choosing a file that was never going to be accepted, and it never claims to be the * rule. Same arrangement, and the same reasoning, as the public form's `attachmentUpload.ts`. * * HEIC AND HEIF ARE HERE BECAUSE OF THE IPHONE. HEIC is that camera's default format, and a patient * photographing the letter their practice asked for is the commonest way one of these gets answered. * Omitting it would reject that answer at the file picker, which reads as the form being broken. * * The Word types are the server's and are not offered here: a patient sending a `.doc` is not a case * anybody has asked for, and narrower is the safe direction for this list to differ in. */ export const QUESTIONNAIRE_FILE_TYPES = [ 'application/pdf', 'image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif', ] as const; /** What an `` puts in its `accept`. */ export const QUESTIONNAIRE_FILE_ACCEPT = QUESTIONNAIRE_FILE_TYPES.join(','); /** * 10 MB, matching the public form's cap. * * The presign itself does NOT cap a document upload, so this is the only limit a patient meets - which * is why it must refuse BEFORE the upload rather than after. These files land in the practice's own * 5 GB, and an accidental 200 MB video is bought storage nobody chose. */ export const QUESTIONNAIRE_FILE_MAX_BYTES = 10 * 1024 * 1024; /** * Refuse, in the patient's words, a file that cannot be accepted - BEFORE anything is uploaded. * * A REFUSAL NAMES WHAT IS WRONG AND WHAT WOULD WORK. "Upload failed" tells somebody holding a photograph * of a letter nothing about what to do next, and this is the surface where they are least able to guess. * * AN EMPTY CONTENT TYPE IS REFUSED RATHER THAN WAVED THROUGH. A browser that cannot identify a file * gives `type: ''`, and the presign would refuse it a moment later with a sentence written for a * developer. */ export const questionnaireFileRejection = (file: { type: string; size: number; name?: string }): string | undefined => { if (!(QUESTIONNAIRE_FILE_TYPES as readonly string[]).includes(file.type)) { return 'That kind of file cannot be attached here. Please choose a PDF or a photo.'; } if (file.size > QUESTIONNAIRE_FILE_MAX_BYTES) { return 'That file is larger than 10 MB. Please attach a smaller file, or photograph the page rather than scanning it at full size.'; } return undefined; }; /** * The answer itself. * * `url` HOLDS THE OBJECT KEY, NEVER A LINK. A presigned URL expires, so storing one would put a dead * address in the record within the hour; every reader presigns the key when it needs to show the file. * This is the same convention `PhotoAttachment` already writes. * * NO INLINE `data`. A profile photograph carries a thumbnail because every screen renders it small and * often; a questionnaire answer is read once by one member of staff, and inlining a 10 MB file into the * QuestionnaireResponse would put it in the FHIR record, in every history version of it, forever. */ export const questionnaireFileAnswer = (params: { key: string; file: { name: string; type: string; size: number }; now?: Date; }): Attachment => ({ url: params.key, title: params.file.name, contentType: params.file.type, size: params.file.size, creation: (params.now ?? new Date()).toISOString(), }); /** * What the patient reads back, so they can tell they attached the right thing. * * DUPLICATED FROM `Chat.tsx`, WHICH KEEPS ITS OWN COPY as a component-local const. Converging them is a * change to the messaging surface and belongs to whoever is in that file; naming it here is what stops * the next person believing this is the only one. */ export const describeFileSize = (bytes?: number): string => { if (!bytes || bytes < 0) return ''; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; /** * Can a browser show this file inline, or must the reader be told rather than shown? * * HEIC IS THE REASON THIS EXISTS. It is accepted for upload because it is what an iPhone writes, and no * major browser can DECODE it - so an `` pointed at one renders a broken image icon on the staff * surface that reads the response. A broken image is a claim that something went wrong; a sentence * naming the file is the truth. */ export const isBrowserRenderableAttachment = (contentType?: string): boolean => ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'].includes((contentType ?? '').toLowerCase());