From 4195f5356352b05b3aba91745dd6fd01366ddd37 Mon Sep 17 00:00:00 2001 From: mijinummi Date: Sat, 30 May 2026 09:17:14 +0100 Subject: [PATCH] feat(frontend): add carrier certification upload and verification management --- .../CertificationUpload/CertificationCard.tsx | 76 +++++++ .../CertificationUpload/CertificationList.tsx | 39 ++++ .../CertificationStatusBadge.tsx | 35 ++++ .../CertificationUploadForm.tsx | 194 ++++++++++++++++++ .../certification.constants.ts | 27 +++ .../certification.types.ts | 27 +++ .../certification.validation.ts | 18 ++ .../CertificationUpload/certifications.ts | 56 +++++ .../components/CertificationUpload/index.ts | 4 + frontend/hooks/useCarrierCertifications.ts | 44 ++++ 10 files changed, 520 insertions(+) create mode 100644 frontend/components/CertificationUpload/CertificationCard.tsx create mode 100644 frontend/components/CertificationUpload/CertificationList.tsx create mode 100644 frontend/components/CertificationUpload/CertificationStatusBadge.tsx create mode 100644 frontend/components/CertificationUpload/CertificationUploadForm.tsx create mode 100644 frontend/components/CertificationUpload/certification.constants.ts create mode 100644 frontend/components/CertificationUpload/certification.types.ts create mode 100644 frontend/components/CertificationUpload/certification.validation.ts create mode 100644 frontend/components/CertificationUpload/certifications.ts create mode 100644 frontend/components/CertificationUpload/index.ts create mode 100644 frontend/hooks/useCarrierCertifications.ts diff --git a/frontend/components/CertificationUpload/CertificationCard.tsx b/frontend/components/CertificationUpload/CertificationCard.tsx new file mode 100644 index 00000000..181225d1 --- /dev/null +++ b/frontend/components/CertificationUpload/CertificationCard.tsx @@ -0,0 +1,76 @@ +import { + CarrierCertification, +} from "./certification.types"; + +import { + CertificationStatusBadge, +} from "./CertificationStatusBadge"; + +interface Props { + certification: + CarrierCertification; + + onDelete: ( + id: string + ) => void; +} + +export function CertificationCard({ + certification, + onDelete, +}: Props) { + const canDelete = + certification.status === + "PENDING" || + certification.status === + "REJECTED"; + + return ( +
+ +
+

+ {certification.type} +

+ + +
+ +
+ Uploaded: + {" "} + {certification.uploadDate} +
+ + {certification.expiryDate && ( +
+ Expiry: + {" "} + {certification.expiryDate} +
+ )} + + {canDelete && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/components/CertificationUpload/CertificationList.tsx b/frontend/components/CertificationUpload/CertificationList.tsx new file mode 100644 index 00000000..b5c5b2f1 --- /dev/null +++ b/frontend/components/CertificationUpload/CertificationList.tsx @@ -0,0 +1,39 @@ +import { + CarrierCertification, +} from "./certification.types"; + +import { + CertificationCard, +} from "./CertificationCard"; + +interface Props { + certifications: + CarrierCertification[]; + + onDelete: ( + id: string + ) => void; +} + +export function CertificationList({ + certifications, + onDelete, +}: Props) { + return ( +
+ {certifications.map( + (certification) => ( + + ) + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/components/CertificationUpload/CertificationStatusBadge.tsx b/frontend/components/CertificationUpload/CertificationStatusBadge.tsx new file mode 100644 index 00000000..6ad10d56 --- /dev/null +++ b/frontend/components/CertificationUpload/CertificationStatusBadge.tsx @@ -0,0 +1,35 @@ +interface Props { + status: + | "PENDING" + | "VERIFIED" + | "REJECTED"; +} + +export function CertificationStatusBadge({ + status, +}: Props) { + const styles = { + PENDING: + "bg-yellow-100 text-yellow-800", + VERIFIED: + "bg-green-100 text-green-800", + REJECTED: + "bg-red-100 text-red-800", + }; + + return ( + + {status} + + ); +} \ No newline at end of file diff --git a/frontend/components/CertificationUpload/CertificationUploadForm.tsx b/frontend/components/CertificationUpload/CertificationUploadForm.tsx new file mode 100644 index 00000000..a564bcb1 --- /dev/null +++ b/frontend/components/CertificationUpload/CertificationUploadForm.tsx @@ -0,0 +1,194 @@ +import { useState } from "react"; + +import { + CERTIFICATION_OPTIONS, +} from "./certification.constants"; + +import { + validateCertificationFile, +} from "./certification.validation"; + +import { + uploadCertification, + deleteCertification, +} from "../../services/certifications"; + +import { + useCarrierCertifications, +} from "../../hooks/useCarrierCertifications"; + +import { + CertificationList, +} from "./CertificationList"; + +export function CertificationUploadForm() { + const { + certifications, + refresh, + } = + useCarrierCertifications(); + + const [type, setType] = + useState(""); + + const [file, setFile] = + useState(null); + + const [expiryDate, setExpiryDate] = + useState(""); + + const [notes, setNotes] = + useState(""); + + const [error, setError] = + useState(""); + + async function handleSubmit( + e: React.FormEvent + ) { + e.preventDefault(); + + if (!file) { + setError( + "Please upload a PDF file." + ); + return; + } + + const validation = + validateCertificationFile( + file + ); + + if (validation) { + setError(validation); + return; + } + + const formData = + new FormData(); + + formData.append("type", type); + formData.append("file", file); + + if (expiryDate) { + formData.append( + "expiryDate", + expiryDate + ); + } + + if (notes) { + formData.append( + "notes", + notes + ); + } + + await uploadCertification( + formData + ); + + setType(""); + setFile(null); + setExpiryDate(""); + setNotes(""); + + await refresh(); + } + + async function handleDelete( + id: string + ) { + await deleteCertification(id); + + await refresh(); + } + + return ( +
+ +
+ + + + setFile( + e.target.files?.[0] ?? + null + ) + } + /> + + + setExpiryDate( + e.target.value + ) + } + /> + +