diff --git a/e2e/vendors/vendor-uplift-workflow.spec.ts b/e2e/vendors/vendor-uplift-workflow.spec.ts index 69205a2e..76dbe2cd 100644 --- a/e2e/vendors/vendor-uplift-workflow.spec.ts +++ b/e2e/vendors/vendor-uplift-workflow.spec.ts @@ -97,7 +97,7 @@ test("vendor uploads scanned evidence and submits a dispatch-scoped uplift", asy await page.getByLabel("New total NTE").fill("125"); await page.getByLabel("Reason").fill("Replacement compressor"); - await page.getByLabel("Evidence (PDF/JPG/PNG)").setInputFiles({ + await page.getByLabel("Evidence (PDF, JPG, PNG, HEIC, MP4, MOV)").setInputFiles({ name: "estimate.pdf", mimeType: "application/pdf", buffer: Buffer.from("%PDF-1.4\nmock estimate"), diff --git a/src/app/v/[token]/dispatch/_components/uplift-request-form.tsx b/src/app/v/[token]/dispatch/_components/uplift-request-form.tsx index 63acfec8..fa50f2b9 100644 --- a/src/app/v/[token]/dispatch/_components/uplift-request-form.tsx +++ b/src/app/v/[token]/dispatch/_components/uplift-request-form.tsx @@ -2,6 +2,7 @@ import { useRef, useState } from "react"; import { Text } from "@/components/ui/text"; import { validateVendorDocument, + VENDOR_UPLOAD_ACCEPT, type VendorMediaUsage, } from "@/domain/vendor-portal/lib/document-validation"; import { @@ -122,13 +123,15 @@ export function UpliftRequestForm({
- + void chooseFile(event.target.files?.[0])} />
diff --git a/src/app/v/[token]/dispatch/_components/uplift-requests-section.tsx b/src/app/v/[token]/dispatch/_components/uplift-requests-section.tsx index c8a10c01..625bdda9 100644 --- a/src/app/v/[token]/dispatch/_components/uplift-requests-section.tsx +++ b/src/app/v/[token]/dispatch/_components/uplift-requests-section.tsx @@ -77,7 +77,7 @@ function useVendorUpliftActions({ if (requiresApproval) { if (!values.evidenceFile) { setFormError( - "Attach PDF, JPG, or PNG evidence — approval-required requests need evidence.", + "Attach PDF, JPG, PNG, HEIC, MP4, or MOV evidence — approval-required requests need evidence.", ); return; } diff --git a/src/app/v/_components/vendor-portal-documents.tsx b/src/app/v/_components/vendor-portal-documents.tsx index d1881ba4..97fed559 100644 --- a/src/app/v/_components/vendor-portal-documents.tsx +++ b/src/app/v/_components/vendor-portal-documents.tsx @@ -5,6 +5,7 @@ import { VendorPortalFeedback } from "@/app/v/_components/vendor-portal-feedback import { vendorPortalApi } from "@/domain/vendor-portal/api/vendor-portal-api"; import { validateVendorDocument, + VENDOR_UPLOAD_ACCEPT, vendorMediaUsage, } from "@/domain/vendor-portal/lib/document-validation"; import type { @@ -160,7 +161,7 @@ export function VendorPortalDocuments({ ref={inputRef} type="file" aria-label="Choose completion document" - accept=".pdf,.jpg,.jpeg,.png,.heic,.mp4,.mov,application/pdf,image/jpeg,image/png,image/heic,video/mp4,video/quicktime" + accept={VENDOR_UPLOAD_ACCEPT} hidden onChange={(event) => void uploadSelectedFile(event.target.files?.[0])} /> diff --git a/src/domain/vendor-portal/lib/document-validation.ts b/src/domain/vendor-portal/lib/document-validation.ts index 9d9ddc39..ee7969ab 100644 --- a/src/domain/vendor-portal/lib/document-validation.ts +++ b/src/domain/vendor-portal/lib/document-validation.ts @@ -13,6 +13,10 @@ const MAX_VIDEO_DURATION_SECONDS = 90; const UNSUPPORTED_TYPE_MESSAGE = "Only PDF, JPG, PNG, HEIC, MP4, and MOV files are allowed."; +/** File-picker filter for every vendor portal upload, matching the allowlist below. */ +export const VENDOR_UPLOAD_ACCEPT = + ".pdf,.jpg,.jpeg,.png,.heic,.mp4,.mov,application/pdf,image/jpeg,image/png,image/heic,video/mp4,video/quicktime"; + type VendorUploadKind = "document" | "photo" | "video"; // Mirrors the server allowlist: an allowlisted browser type wins, otherwise the diff --git a/src/domain/vendor-portal/lib/uplift-validation.ts b/src/domain/vendor-portal/lib/uplift-validation.ts index b8fafe04..7b3187ed 100644 --- a/src/domain/vendor-portal/lib/uplift-validation.ts +++ b/src/domain/vendor-portal/lib/uplift-validation.ts @@ -48,7 +48,7 @@ export function validateUpliftAmount( return "Please include a reason for the uplift request."; } if (!options.file && !options.hasExisting) { - return "Attach PDF, JPG, or PNG evidence — approval-required requests need evidence."; + return "Attach PDF, JPG, PNG, HEIC, MP4, or MOV evidence — approval-required requests need evidence."; } return ""; } diff --git a/src/test/app/v/uplift-request-form.test.tsx b/src/test/app/v/uplift-request-form.test.tsx index 84a612b6..d39d6d47 100644 --- a/src/test/app/v/uplift-request-form.test.tsx +++ b/src/test/app/v/uplift-request-form.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { UpliftRequestForm } from "@/app/v/[token]/dispatch/_components/uplift-request-form"; @@ -48,6 +48,31 @@ describe("UpliftRequestForm revise mode", () => { expect(await screen.findByText("A work order can have at most 10 photos.")).toBeInTheDocument(); }); + it("offers and accepts the same phone photo and video types as the upload allowlist", async () => { + renderForm(); + + const input = screen.getByLabelText("Evidence (PDF, JPG, PNG, HEIC, MP4, MOV)"); + expect(input.getAttribute("accept")?.split(",")).toEqual( + expect.arrayContaining([".pdf", ".jpg", ".png", ".heic", ".mp4", ".mov"]), + ); + + fireEvent.change(input, { + target: { files: [new File(["gif"], "clip.gif", { type: "image/gif" })] }, + }); + expect( + await screen.findByText("Only PDF, JPG, PNG, HEIC, MP4, and MOV files are allowed."), + ).toBeInTheDocument(); + + fireEvent.change(input, { + target: { files: [new File(["heic"], "site.heic", { type: "" })] }, + }); + await waitFor(() => + expect( + screen.queryByText("Only PDF, JPG, PNG, HEIC, MP4, and MOV files are allowed."), + ).not.toBeInTheDocument(), + ); + }); + it("reports the current NTE threshold in the actionable error", () => { renderForm({ initialRequested: "900", currentNte: 1000 });