fix(vendor-portal): advertise phone photo and video types for uplift evidence

The evidence picker filtered to PDF/JPG/PNG and its label and missing-file
messages said the same, while the pre-check (and the server, which applies
the SH-116 allowlist and work-order counts to UpliftEvidence uploads)
accepts HEIC, MP4 and MOV. On a phone the picker hid exactly the photos and
clips the validator would pass.

The evidence input now uses the same accept filter as the completion
upload, exported once from document-validation, and the label and both
missing-evidence messages name all six types.
This commit is contained in:
Alexandre Brandizzi 2026-09-25 01:43:34 -03:00
parent ef6cde64c8
commit 39ee98e09b
7 changed files with 40 additions and 7 deletions

View file

@ -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"),

View file

@ -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({
</div>
<div className="vp-row">
<strong>
<label htmlFor={`uplift-evidence-${mode}`}>Evidence (PDF/JPG/PNG)</label>
<label htmlFor={`uplift-evidence-${mode}`}>
Evidence (PDF, JPG, PNG, HEIC, MP4, MOV)
</label>
</strong>
<input
id={`uplift-evidence-${mode}`}
type="file"
className="vp-input"
accept=".pdf,.jpg,.jpeg,.png,application/pdf,image/jpeg,image/png"
accept={VENDOR_UPLOAD_ACCEPT}
onChange={(event) => void chooseFile(event.target.files?.[0])}
/>
</div>

View file

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

View file

@ -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])}
/>

View file

@ -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

View file

@ -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 "";
}

View file

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