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());