mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-04 10:12:05 +00:00
Photos up to 10 MB (JPG/PNG/HEIC), videos up to 100 MB and 90 s (MP4/MOV), at most 10 photos and 3 videos per work order, pre-validated with stable generic messages on the Photos & Videos modal, the Completion Doc media tab, Extra Docs and the vendor portal. Video duration is read from metadata when the browser can; unreadable metadata never blocks. Mobile MIME variants (empty type, octet-stream with a video extension, QuickTime) stay accepted. The signed completion PDF keeps its 50 MB cap.
166 lines
5 KiB
TypeScript
166 lines
5 KiB
TypeScript
import { useRef, useState } from "react";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
|
|
import { VendorPortalFeedback } from "@/app/v/_components/vendor-portal-feedback";
|
|
import { vendorPortalApi } from "@/domain/vendor-portal/api/vendor-portal-api";
|
|
import { validateVendorDocument } from "@/domain/vendor-portal/lib/document-validation";
|
|
import type { VendorPortalDocument } from "@/domain/vendor-portal/types/vendor-portal";
|
|
|
|
type VendorPortalDocumentsProps = {
|
|
token: string;
|
|
dispatchId: number;
|
|
documents: VendorPortalDocument[];
|
|
locked: boolean;
|
|
onChanged: () => Promise<void>;
|
|
};
|
|
|
|
type UploadRequest = {
|
|
file: File;
|
|
replacementId?: number;
|
|
};
|
|
|
|
export function VendorPortalDocuments({
|
|
token,
|
|
dispatchId,
|
|
documents,
|
|
locked,
|
|
onChanged,
|
|
}: VendorPortalDocumentsProps) {
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const [replacementId, setReplacementId] = useState<number>();
|
|
const [progress, setProgress] = useState<number>();
|
|
const [validationError, setValidationError] = useState<string>();
|
|
|
|
const uploadMutation = useMutation({
|
|
mutationFn: ({ file, replacementId: requestedReplacementId }: UploadRequest) =>
|
|
vendorPortalApi.uploadDocument({
|
|
token,
|
|
dispatchId,
|
|
file,
|
|
replacesDocumentId: requestedReplacementId,
|
|
onProgress: setProgress,
|
|
}),
|
|
onSuccess: onChanged,
|
|
onSettled: () => {
|
|
setProgress(undefined);
|
|
setReplacementId(undefined);
|
|
if (inputRef.current) {
|
|
inputRef.current.value = "";
|
|
}
|
|
},
|
|
});
|
|
|
|
const chooseFile = (documentId?: number) => {
|
|
setValidationError(undefined);
|
|
uploadMutation.reset();
|
|
setReplacementId(documentId);
|
|
inputRef.current?.click();
|
|
};
|
|
|
|
const uploadSelectedFile = async (file?: File) => {
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
const validationMessage = await validateVendorDocument(file);
|
|
if (validationMessage) {
|
|
setValidationError(validationMessage);
|
|
return;
|
|
}
|
|
|
|
setValidationError(undefined);
|
|
setProgress(0);
|
|
uploadMutation.mutate({ file, replacementId });
|
|
};
|
|
|
|
const uploadError =
|
|
validationError ??
|
|
(uploadMutation.error ? "Unable to upload the document. Please try again." : undefined);
|
|
|
|
return (
|
|
<>
|
|
<input
|
|
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"
|
|
hidden
|
|
onChange={(event) => void uploadSelectedFile(event.target.files?.[0])}
|
|
/>
|
|
|
|
{documents.length === 0 && (
|
|
<VendorPortalFeedback>No completion documents uploaded.</VendorPortalFeedback>
|
|
)}
|
|
|
|
{documents.map((document) => (
|
|
<article key={document.id} className="vp-uplift-row">
|
|
<div className="vp-row">
|
|
<strong>File</strong>
|
|
<span>
|
|
{document.originalFileName} · v{document.version}
|
|
</span>
|
|
</div>
|
|
<div className="vp-row">
|
|
<strong>Security scan</strong>
|
|
<span>{document.scanStatus}</span>
|
|
</div>
|
|
<div className="vp-row">
|
|
<strong>Review</strong>
|
|
<span>{document.reviewStatus}</span>
|
|
</div>
|
|
{Boolean(document.rejectionReason) && (
|
|
<VendorPortalFeedback tone="error">{document.rejectionReason}</VendorPortalFeedback>
|
|
)}
|
|
<div className="vp-actions">
|
|
<button
|
|
type="button"
|
|
className="vp-btn vp-btn-ghost"
|
|
disabled={!document.canDownload}
|
|
onClick={() => void vendorPortalApi.downloadDocument(token, dispatchId, document)}
|
|
>
|
|
{document.canDownload ? "Download" : "Quarantined"}
|
|
</button>
|
|
{!locked && (
|
|
<button
|
|
type="button"
|
|
className="vp-btn vp-btn-ghost"
|
|
disabled={uploadMutation.isPending}
|
|
onClick={() => chooseFile(document.id)}
|
|
>
|
|
Replace
|
|
</button>
|
|
)}
|
|
</div>
|
|
</article>
|
|
))}
|
|
|
|
{progress !== undefined && (
|
|
<div role="status" aria-live="polite">
|
|
<progress max={100} value={progress} style={{ width: "100%" }} />
|
|
<VendorPortalFeedback>Uploading… {progress}%</VendorPortalFeedback>
|
|
</div>
|
|
)}
|
|
|
|
{Boolean(uploadError) && (
|
|
<VendorPortalFeedback tone="error">{uploadError}</VendorPortalFeedback>
|
|
)}
|
|
|
|
{!locked && (
|
|
<div className="vp-actions">
|
|
<button
|
|
type="button"
|
|
className="vp-btn vp-btn-primary"
|
|
disabled={uploadMutation.isPending}
|
|
onClick={() => chooseFile()}
|
|
>
|
|
Upload document
|
|
</button>
|
|
<span className="vp-hint">
|
|
Accepted: PDF, JPG, PNG, HEIC, MP4, MOV · Photos up to 10 MB · Videos up to 100 MB and
|
|
90 s
|
|
</span>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|