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