diff --git a/src/domain/work-orders/utils/work-order-media-limits.ts b/src/domain/work-orders/utils/work-order-media-limits.ts index 58cc358a..43b5546d 100644 --- a/src/domain/work-orders/utils/work-order-media-limits.ts +++ b/src/domain/work-orders/utils/work-order-media-limits.ts @@ -1,9 +1,32 @@ +import { isWorkOrderCompletionSurfaceReadOnly } from "@/domain/work-orders/utils/work-order-lock"; + /** Backend RequestSizeLimit for media and completion-doc uploads. */ -export const MAX_WORK_ORDER_MEDIA_BYTES = 30 * 1024 * 1024; +export const MAX_WORK_ORDER_MEDIA_BYTES = 50_000_000; -const ALLOWED_MIME_TYPES = new Set(["image/jpeg", "image/png", "video/mp4", "video/quicktime"]); +const MEDIA_MIME_TYPES = new Set(["image/jpeg", "image/png", "video/mp4", "video/quicktime"]); -const ALLOWED_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".mp4", ".mov"]); +const MEDIA_EXTENSIONS = new Set([".jpg", ".jpeg", ".png", ".mp4", ".mov"]); + +const EXTRA_DOC_MIME_TYPES = new Set([ + ...MEDIA_MIME_TYPES, + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", +]); + +const EXTRA_DOC_EXTENSIONS = new Set([...MEDIA_EXTENSIONS, ".pdf", ".doc", ".docx"]); + +function fileExtension(file: File): string { + const name = file.name.toLowerCase(); + const dot = name.lastIndexOf("."); + return dot >= 0 ? name.slice(dot) : ""; +} + +function isTypeAllowed(file: File, mimes: Set, extensions: Set): boolean { + const mimeOk = Boolean(file.type) && mimes.has(file.type.toLowerCase()); + const extOk = extensions.has(fileExtension(file)); + return mimeOk || extOk; +} export function isWorkOrderFileTooLarge( file: File, @@ -13,23 +36,24 @@ export function isWorkOrderFileTooLarge( } export function isWorkOrderMediaTypeAllowed(file: File): boolean { - const mimeOk = Boolean(file.type) && ALLOWED_MIME_TYPES.has(file.type.toLowerCase()); - const name = file.name.toLowerCase(); - const dot = name.lastIndexOf("."); - const ext = dot >= 0 ? name.slice(dot) : ""; - const extOk = ALLOWED_EXTENSIONS.has(ext); - return mimeOk || extOk; + return isTypeAllowed(file, MEDIA_MIME_TYPES, MEDIA_EXTENSIONS); +} + +export function isWorkOrderExtraDocTypeAllowed(file: File): boolean { + return isTypeAllowed(file, EXTRA_DOC_MIME_TYPES, EXTRA_DOC_EXTENSIONS); } export function workOrderMediaAcceptAttribute(): string { return "image/jpeg,image/png,video/mp4,video/quicktime,.jpg,.jpeg,.png,.mp4,.mov"; } -export function formatMediaSizeLimit(maxBytes = MAX_WORK_ORDER_MEDIA_BYTES): string { - return `${Math.round(maxBytes / (1024 * 1024))} MB`; +export function workOrderExtraDocsAcceptAttribute(): string { + return `${workOrderMediaAcceptAttribute()},.pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document`; } -import { isWorkOrderCompletionSurfaceReadOnly } from "@/domain/work-orders/utils/work-order-lock"; +export function formatMediaSizeLimit(maxBytes = MAX_WORK_ORDER_MEDIA_BYTES): string { + return `${Math.round(maxBytes / 1_000_000)} MB`; +} /** Completion-doc / completion-media lock: Canceled or Completed. Extra Docs use canMutateWorkOrderExtraDocs. */ export function isWorkOrderDocumentsReadOnly(status: string | null | undefined): boolean { diff --git a/src/test/domain/work-orders/utils/work-order-media-limits.test.ts b/src/test/domain/work-orders/utils/work-order-media-limits.test.ts index 2ae1f827..bc80696e 100644 --- a/src/test/domain/work-orders/utils/work-order-media-limits.test.ts +++ b/src/test/domain/work-orders/utils/work-order-media-limits.test.ts @@ -1,10 +1,14 @@ import { describe, expect, it } from "vitest"; import { canDeleteWorkOrderMedia, + formatMediaSizeLimit, isWorkOrderDocumentsReadOnly, + isWorkOrderExtraDocTypeAllowed, isWorkOrderFileTooLarge, isWorkOrderMediaTypeAllowed, MAX_WORK_ORDER_MEDIA_BYTES, + workOrderExtraDocsAcceptAttribute, + workOrderMediaAcceptAttribute, } from "@/domain/work-orders/utils/work-order-media-limits"; describe("work-order-media-limits", () => { @@ -20,7 +24,9 @@ describe("work-order-media-limits", () => { expect(canDeleteWorkOrderMedia({ id: "0" })).toBe(false); }); - it("flags files over 30 MB", () => { + it("caps uploads at 50_000_000 bytes and formats decimal MB", () => { + expect(MAX_WORK_ORDER_MEDIA_BYTES).toBe(50_000_000); + expect(formatMediaSizeLimit()).toBe("50 MB"); expect(isWorkOrderFileTooLarge({ size: MAX_WORK_ORDER_MEDIA_BYTES + 1 } as File)).toBe(true); expect(isWorkOrderFileTooLarge({ size: 10 } as File)).toBe(false); }); @@ -36,5 +42,31 @@ describe("work-order-media-limits", () => { expect(isWorkOrderMediaTypeAllowed({ name: "a.pdf", type: "application/pdf" } as File)).toBe( false, ); + expect(workOrderMediaAcceptAttribute()).not.toContain(".pdf"); + }); + + it("allows Extra Docs PDF DOC DOCX plus media types", () => { + expect(isWorkOrderExtraDocTypeAllowed({ name: "a.pdf", type: "application/pdf" } as File)).toBe( + true, + ); + expect( + isWorkOrderExtraDocTypeAllowed({ name: "a.doc", type: "application/msword" } as File), + ).toBe(true); + expect( + isWorkOrderExtraDocTypeAllowed({ + name: "a.docx", + type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + } as File), + ).toBe(true); + expect(isWorkOrderExtraDocTypeAllowed({ name: "a.jpg", type: "image/jpeg" } as File)).toBe( + true, + ); + expect(isWorkOrderExtraDocTypeAllowed({ name: "a.gif", type: "image/gif" } as File)).toBe( + false, + ); + const accept = workOrderExtraDocsAcceptAttribute(); + expect(accept).toContain(".pdf"); + expect(accept).toContain(".doc"); + expect(accept).toContain(".docx"); }); });