Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions frontend/components/CertificationUpload/CertificationCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="rounded-lg border p-4">

<div className="flex items-center justify-between">
<h4 className="font-medium">
{certification.type}
</h4>

<CertificationStatusBadge
status={
certification.status
}
/>
</div>

<div className="mt-2 text-sm text-gray-500">
Uploaded:
{" "}
{certification.uploadDate}
</div>

{certification.expiryDate && (
<div className="text-sm text-gray-500">
Expiry:
{" "}
{certification.expiryDate}
</div>
)}

{canDelete && (
<button
type="button"
onClick={() =>
onDelete(
certification.id
)
}
className="
mt-3
text-red-600
text-sm
"
>
Delete
</button>
)}
</div>
);
}
39 changes: 39 additions & 0 deletions frontend/components/CertificationUpload/CertificationList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="space-y-4">
{certifications.map(
(certification) => (
<CertificationCard
key={
certification.id
}
certification={
certification
}
onDelete={onDelete}
/>
)
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -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 (
<span
className={`
inline-flex
rounded-full
px-2
py-1
text-xs
font-medium
${styles[status]}
`}
>
{status}
</span>
);
}
194 changes: 194 additions & 0 deletions frontend/components/CertificationUpload/CertificationUploadForm.tsx
Original file line number Diff line number Diff line change
@@ -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<File | null>(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 (
<div className="space-y-6">

<form
onSubmit={handleSubmit}
className="space-y-4"
>
<select
required
value={type}
onChange={(e) =>
setType(
e.target.value
)
}
>
<option value="">
Select certification
</option>

{CERTIFICATION_OPTIONS.map(
(option) => (
<option
key={
option.value
}
value={
option.value
}
>
{option.label}
</option>
)
)}
</select>

<input
type="file"
accept="application/pdf"
onChange={(e) =>
setFile(
e.target.files?.[0] ??
null
)
}
/>

<input
type="date"
value={expiryDate}
onChange={(e) =>
setExpiryDate(
e.target.value
)
}
/>

<textarea
placeholder="Notes"
value={notes}
onChange={(e) =>
setNotes(
e.target.value
)
}
/>

{error && (
<p className="text-red-500">
{error}
</p>
)}

<button type="submit">
Upload Certification
</button>
</form>

<CertificationList
certifications={
certifications
}
onDelete={handleDelete}
/>
</div>
);
}
27 changes: 27 additions & 0 deletions frontend/components/CertificationUpload/certification.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CertificationType } from "./certification.types";

export const CERTIFICATION_OPTIONS: {
label: string;
value: CertificationType;
}[] = [
{
label: "Operating License",
value: "OPERATING_LICENSE",
},
{
label: "Insurance Certificate",
value: "INSURANCE_CERTIFICATE",
},
{
label: "Safety Certification",
value: "SAFETY_CERTIFICATION",
},
{
label: "Hazmat License",
value: "HAZMAT_LICENSE",
},
{
label: "Vehicle Registration",
value: "VEHICLE_REGISTRATION",
},
];
Loading
Loading