marmar-context / web / context / src / logic / records.ts
records.ts
Raw
import type { RecordType } from "../api/types";

/** REC-3: limits per check. Files go straight to storage, so nginx's 11 MB cap does not apply. */
export const RECORD_LIMITS = { perCheck: 20, pagesPerDocument: 20, bytesPerFile: 15 * 1024 * 1024 };
export const ACCEPTED: Record<string, string> = {
  "text/plain": "TXT",
  "application/pdf": "PDF",
  "image/jpeg": "JPG",
  "image/png": "PNG",
  "image/heic": "HEIC",
  "image/heif": "HEIC",
};
const byExtension: Record<string, string> = { txt: "text/plain", pdf: "application/pdf", jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", heic: "image/heic", heif: "image/heic" };

export const recordTypes: Record<RecordType, string> = {
  prescription: "Prescription",
  lab_result: "Lab result",
  discharge_summary: "Discharge summary",
  clinic_note: "Clinic note",
  other: "Other",
};

/** Some browsers give HEIC and TXT files no type; fall back to the extension. */
export function contentTypeOf(file: { name: string; type: string }): string {
  if (file.type && ACCEPTED[file.type]) return file.type === "image/heif" ? "image/heic" : file.type;
  return byExtension[file.name.split(".").pop()?.toLowerCase() ?? ""] ?? file.type ?? "";
}

const mb = (bytes: number) => (bytes / 1024 / 1024).toFixed(1);

/** The message names the file and says by how much it is over (REC-3). */
export function fileProblem(file: { name: string; type: string; size: number }): string | null {
  if (!ACCEPTED[contentTypeOf(file)]) return `${file.name} is not a file Context reads. Use TXT, PDF, JPG, PNG or HEIC.`;
  if (file.size === 0) return `${file.name} is empty.`;
  if (file.size > RECORD_LIMITS.bytesPerFile)
    return `${file.name} is ${mb(file.size)} MB, ${mb(file.size - RECORD_LIMITS.bytesPerFile)} MB over the 15 MB limit per file.`;
  return null;
}

/** How many of the chosen files fit, and the message when some do not. */
export function roomFor(existing: number, adding: number): { fits: number; message: string | null } {
  const room = Math.max(0, RECORD_LIMITS.perCheck - existing);
  if (adding <= room) return { fits: adding, message: null };
  return {
    fits: room,
    message: `A check can use up to ${RECORD_LIMITS.perCheck} records. This patient has ${existing}, so ${adding - room} of these ${adding === 1 ? "is" : "are"} left out.`,
  };
}

/** "01-clinic_prescription.pdf" becomes "Clinic prescription" (REC-4: from the file name, editable). */
export function titleFromFile(name: string): string {
  const title = name
    .replace(/\.[a-z0-9]{2,5}$/i, "")
    .replace(/^\d+(?=[\s._-])/, "")
    .replace(/[-_.]+/g, " ")
    .replace(/\s+/g, " ")
    .trim()
    .slice(0, 120);
  return title ? title[0].toUpperCase() + title.slice(1) : "Untitled record";
}

export function guessType(name: string): RecordType {
  const n = name.toLowerCase();
  if (/lab|egfr|blood|chemistry|result|e\/?u\/?cr|urea|creatinine/.test(n)) return "lab_result";
  if (/prescri|\brx\b|medic/.test(n)) return "prescription";
  if (/discharge|hospital|summary/.test(n)) return "discharge_summary";
  if (/note|clinic|consult/.test(n)) return "clinic_note";
  return "other";
}