mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 03:10:54 +00:00
fixes
This commit is contained in:
@@ -173,7 +173,7 @@ const App = () => {
|
||||
<Route path="/onboarding" element={<OnboardingPage />} />
|
||||
</Route>
|
||||
|
||||
<Route element={<RequireCompany path={location.pathname} />}>
|
||||
<Route element={<RequireCompany />}>
|
||||
<Route
|
||||
element={
|
||||
<AppLayout
|
||||
|
||||
@@ -147,7 +147,6 @@ export function AppLayout({
|
||||
flexShrink: 0,
|
||||
cursor: "pointer",
|
||||
};
|
||||
const toggleStyle: CSSProperties = { ...islandStyle, borderRadius: 10 };
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import type { Freight } from "@edr/types";
|
||||
import { Table, type TableColumn } from "@edr/ui-common";
|
||||
|
||||
export interface BookingTableProps {
|
||||
bookings: Freight.IBooking[];
|
||||
}
|
||||
|
||||
const columns: TableColumn<Freight.IBooking>[] = [
|
||||
{ key: "reference", header: "Reference" },
|
||||
{ key: "customerId", header: "Customer" },
|
||||
{ key: "status", header: "Status" },
|
||||
{
|
||||
key: "scheduledDate",
|
||||
header: "Scheduled",
|
||||
render: (row) => new Date(row.scheduledDate).toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
key: "totalAmount",
|
||||
header: "Total",
|
||||
render: (row) => row.totalAmount.toFixed(2),
|
||||
},
|
||||
];
|
||||
|
||||
const BookingTable = ({ bookings }: BookingTableProps) => (
|
||||
<Table
|
||||
columns={columns}
|
||||
data={bookings}
|
||||
rowKey={(row) => row.id}
|
||||
emptyMessage="No bookings yet"
|
||||
/>
|
||||
);
|
||||
|
||||
export default BookingTable;
|
||||
@@ -1,144 +0,0 @@
|
||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||
import { Field, FieldError, Input, Switch } from "@edr/ui-common";
|
||||
import { type BookingFormValues } from "./schema";
|
||||
import { StepHeader } from "./shared";
|
||||
|
||||
type BookingForm = UseFormReturn<BookingFormValues>;
|
||||
|
||||
export function Step3FirstLastMile({ form }: { form: BookingForm }) {
|
||||
const firstMileEnabled = form.watch("firstMileEnabled");
|
||||
const lastMileEnabled = form.watch("lastMileEnabled");
|
||||
const equipmentReturn = form.watch("equipmentReturn");
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<StepHeader
|
||||
title="First & Last Mile"
|
||||
description="Configure trucking and container return options."
|
||||
/>
|
||||
|
||||
<div className="divide-y divide-border rounded-xl border border-border">
|
||||
<div className="p-4">
|
||||
<Controller
|
||||
name="firstMileEnabled"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">First Mile - Pick-up</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
Truck pick-up from your premises (Door to Port) to the
|
||||
origin rail yard.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
if (!value) {
|
||||
form.setValue("pickUpAddress", "", {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{firstMileEnabled && (
|
||||
<Controller
|
||||
name="pickUpAddress"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field className="mt-3" data-invalid={fieldState.invalid}>
|
||||
<Input
|
||||
{...field}
|
||||
aria-invalid={fieldState.invalid}
|
||||
placeholder="Pick-up address *"
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<Controller
|
||||
name="lastMileEnabled"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Last Mile - Delivery</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
Truck delivery from the destination rail yard to the final
|
||||
address (Port to Door).
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
if (!value) {
|
||||
form.setValue("deliveryAddress", "", {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
{lastMileEnabled && (
|
||||
<Controller
|
||||
name="deliveryAddress"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field className="mt-3" data-invalid={fieldState.invalid}>
|
||||
<Input
|
||||
{...field}
|
||||
aria-invalid={fieldState.invalid}
|
||||
placeholder="Delivery address *"
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{lastMileEnabled && (
|
||||
<div className="mt-4 border-t border-border pt-4">
|
||||
<Controller
|
||||
name="equipmentReturn"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Equipment Return</p>
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">
|
||||
{field.value === "with_return"
|
||||
? "Container returned to EDR after unloading."
|
||||
: "Container retained by the customer after delivery."}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={field.value === "with_return"}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(
|
||||
value ? "with_return" : "without_return",
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||
import { Field, FieldError, SmartFileInput } from "@edr/ui-common";
|
||||
import {
|
||||
BOOKING_DOCS_SETTING,
|
||||
REQUIRED_DOC_KEYS,
|
||||
type BookingFormValues,
|
||||
} from "./schema";
|
||||
import { getUploadedRequiredCount, StepHeader } from "./shared";
|
||||
|
||||
type BookingForm = UseFormReturn<BookingFormValues>;
|
||||
|
||||
export function Step7Documents({ form }: { form: BookingForm }) {
|
||||
const documents = form.watch("documents");
|
||||
const uploadedRequired = getUploadedRequiredCount(documents);
|
||||
const documentErrors = form.formState.errors.documents as
|
||||
| Record<string, { message?: string }>
|
||||
| undefined;
|
||||
const smartFileErrors = Object.fromEntries(
|
||||
Object.entries(documentErrors ?? {}).map(([key, value]) => [
|
||||
key,
|
||||
value?.message ?? "",
|
||||
]),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<StepHeader
|
||||
title="Compliance Documents"
|
||||
description="Upload your company's legal credentials for EDR contract eligibility verification (US-04)."
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-3 rounded-xl border border-border bg-muted/30 px-4 py-3">
|
||||
<div
|
||||
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-bold ${uploadedRequired === REQUIRED_DOC_KEYS.length
|
||||
? "bg-emerald-100 text-emerald-700"
|
||||
: "bg-primary/10 text-primary"
|
||||
}`}
|
||||
>
|
||||
{uploadedRequired}/{REQUIRED_DOC_KEYS.length}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{uploadedRequired < REQUIRED_DOC_KEYS.length
|
||||
? `${REQUIRED_DOC_KEYS.length - uploadedRequired} mandatory document(s) still needed.`
|
||||
: "All mandatory documents uploaded. Power of Attorney is optional."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Controller
|
||||
name="documents"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<SmartFileInput
|
||||
file={BOOKING_DOCS_SETTING}
|
||||
value={field.value}
|
||||
onChange={(value) => field.onChange(value)}
|
||||
errors={smartFileErrors}
|
||||
/>
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { CheckCircle2, Loader2, Save, UserCheck, XCircle } from "lucide-react";
|
||||
import { CheckCircle2, Save, UserCheck, XCircle } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
Group,
|
||||
@@ -100,8 +100,8 @@ export default function TabPowerOfAttorney({
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack gap="md">
|
||||
<Text c="edr-muted" size="sm">
|
||||
Power of Attorney details are optional. Fill them in if you have
|
||||
an authorized representative, or leave blank.
|
||||
Power of Attorney details are optional. Fill them in if you have an
|
||||
authorized representative, or leave blank.
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
@@ -162,13 +162,17 @@ export default function TabPowerOfAttorney({
|
||||
{mutation.isSuccess && (
|
||||
<Group gap={6} c="green">
|
||||
<CheckCircle2 size={16} />
|
||||
<Text size="sm" fw={500}>Saved successfully</Text>
|
||||
<Text size="sm" fw={500}>
|
||||
Saved successfully
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
{mutation.isError && (
|
||||
<Group gap={6} c="red">
|
||||
<XCircle size={16} />
|
||||
<Text size="sm" fw={500}>Save failed</Text>
|
||||
<Text size="sm" fw={500}>
|
||||
Save failed
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Calendar, MapPin } from "lucide-react";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { consignments } from "../consignments/consignments.mock";
|
||||
import type { ShipmentMode, ShipmentStatus } from "./shipments.mock";
|
||||
|
||||
export interface ShipmentFormData {
|
||||
consignmentId?: number;
|
||||
consignmentReference?: string;
|
||||
originStation?: string;
|
||||
destinationStation?: string;
|
||||
mode?: ShipmentMode;
|
||||
status?: ShipmentStatus;
|
||||
currentLocation?: string;
|
||||
eta?: string;
|
||||
}
|
||||
|
||||
export interface NewShipmentPageProps {
|
||||
mode?: "create" | "edit";
|
||||
shipment?: ShipmentFormData;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const selectClass =
|
||||
"flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 shadow-xs outline-none transition hover:border-slate-300 focus:border-[#10B981]/50 focus:ring-2 focus:ring-[#10B981]/20";
|
||||
|
||||
export default function NewShipmentPage({
|
||||
mode = "create",
|
||||
shipment,
|
||||
children,
|
||||
}: NewShipmentPageProps = {}) {
|
||||
const isEdit = mode === "edit";
|
||||
const title = isEdit ? "Edit Shipment" : "New Shipment";
|
||||
const description = isEdit
|
||||
? "Update shipment tracking information."
|
||||
: "Create a new shipment for real-time tracking.";
|
||||
const submitLabel = isEdit ? "Save Changes" : "Create Shipment";
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
{children ?? <Button>{isEdit ? "Edit" : "New Shipment"}</Button>}
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-3xl rounded-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-5 py-4 md:grid-cols-2">
|
||||
{/* Consignment */}
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label>Consignment *</Label>
|
||||
<select
|
||||
defaultValue={shipment?.consignmentId ?? ""}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select consignment
|
||||
</option>
|
||||
{consignments.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.trackingNumber} — {c.customer} ({c.originStation} →{" "}
|
||||
{c.destinationStation})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-slate-500">
|
||||
Origin and destination auto-fill from the linked consignment.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="space-y-2">
|
||||
<Label>Status</Label>
|
||||
<select
|
||||
defaultValue={shipment?.status ?? "In Transit"}
|
||||
className={selectClass}
|
||||
>
|
||||
<option>In Transit</option>
|
||||
<option>Delivered</option>
|
||||
<option>Delayed</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Mode */}
|
||||
<div className="space-y-2">
|
||||
<Label>Transport Mode</Label>
|
||||
<select
|
||||
defaultValue={shipment?.mode ?? "rail"}
|
||||
className={selectClass}
|
||||
>
|
||||
<option value="rail">Rail</option>
|
||||
<option value="truck">Truck</option>
|
||||
<option value="multimodal">Multimodal</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Origin */}
|
||||
<div className="space-y-2">
|
||||
<Label>Origin Station</Label>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-3 top-3 h-4 w-4 text-slate-400" />
|
||||
<Input
|
||||
defaultValue={shipment?.originStation ?? ""}
|
||||
placeholder="Auto-filled from consignment"
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Destination */}
|
||||
<div className="space-y-2">
|
||||
<Label>Destination Station</Label>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-3 top-3 h-4 w-4 text-slate-400" />
|
||||
<Input
|
||||
defaultValue={shipment?.destinationStation ?? ""}
|
||||
placeholder="Auto-filled from consignment"
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ETA */}
|
||||
<div className="space-y-2">
|
||||
<Label>Estimated Arrival</Label>
|
||||
<div className="relative">
|
||||
<Calendar className="absolute left-3 top-3 h-4 w-4 text-slate-400" />
|
||||
<Input
|
||||
type="date"
|
||||
defaultValue={shipment?.eta ?? ""}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Location */}
|
||||
<div className="space-y-2">
|
||||
<Label>Current Location</Label>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-3 top-3 h-4 w-4 text-slate-400" />
|
||||
<Input
|
||||
defaultValue={shipment?.currentLocation ?? ""}
|
||||
placeholder="e.g. Dire Dawa Yard"
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button variant="outline">Cancel</Button>
|
||||
<Button className="bg-[#10B981] text-white hover:bg-[#10B981]/90">
|
||||
{submitLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
List,
|
||||
MapPin,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Plus,
|
||||
Search,
|
||||
Train,
|
||||
@@ -17,7 +16,6 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import Breadcrumbs from "@/components/Breadcrumbs";
|
||||
import NewShipmentPage from "./NewShipmentPage";
|
||||
import DeleteShipmentDialog from "./DeleteShipmentDialog";
|
||||
import {
|
||||
shipments,
|
||||
@@ -93,13 +91,6 @@ export default function TrackingPage() {
|
||||
className="pl-8!"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NewShipmentPage>
|
||||
<Button>
|
||||
<Plus />
|
||||
New Shipment
|
||||
</Button>
|
||||
</NewShipmentPage>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -279,7 +270,10 @@ function ShipmentCard({ shipment }: { shipment: Shipment }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 border-t pt-3" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
<div
|
||||
className="flex items-center justify-end gap-2 border-t pt-3"
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
@@ -292,27 +286,12 @@ function ShipmentCard({ shipment }: { shipment: Shipment }) {
|
||||
<Eye />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
<NewShipmentPage
|
||||
mode="edit"
|
||||
shipment={{
|
||||
bookingId: shipment.bookingId,
|
||||
bookingReference: shipment.bookingReference,
|
||||
originStation: shipment.originStation,
|
||||
destinationStation: shipment.destinationStation,
|
||||
mode: shipment.mode,
|
||||
status: shipment.status,
|
||||
currentLocation: shipment.currentLocation,
|
||||
eta: shipment.eta,
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()}>
|
||||
<Pencil />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</NewShipmentPage>
|
||||
<DropdownMenuSeparator />
|
||||
<DeleteShipmentDialog shipmentReference={shipment.reference}>
|
||||
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()} variant="destructive">
|
||||
<DropdownMenuItem
|
||||
onSelect={(e: Event) => e.preventDefault()}
|
||||
variant="destructive"
|
||||
>
|
||||
<Trash2 />
|
||||
Remove
|
||||
</DropdownMenuItem>
|
||||
@@ -331,7 +310,9 @@ function ShipmentTable({ shipments: rows }: { shipments: Shipment[] }) {
|
||||
<CardHeader className="flex flex-row items-center justify-between border-b">
|
||||
<div>
|
||||
<CardTitle>Shipment List</CardTitle>
|
||||
<CardDescription>All shipments and their current status.</CardDescription>
|
||||
<CardDescription>
|
||||
All shipments and their current status.
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -435,27 +416,14 @@ function ShipmentTable({ shipments: rows }: { shipments: Shipment[] }) {
|
||||
<Eye />
|
||||
View
|
||||
</DropdownMenuItem>
|
||||
<NewShipmentPage
|
||||
mode="edit"
|
||||
shipment={{
|
||||
bookingId: shipment.bookingId,
|
||||
bookingReference: shipment.bookingReference,
|
||||
originStation: shipment.originStation,
|
||||
destinationStation: shipment.destinationStation,
|
||||
mode: shipment.mode,
|
||||
status: shipment.status,
|
||||
currentLocation: shipment.currentLocation,
|
||||
eta: shipment.eta,
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()}>
|
||||
<Pencil />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</NewShipmentPage>
|
||||
<DropdownMenuSeparator />
|
||||
<DeleteShipmentDialog shipmentReference={shipment.reference}>
|
||||
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()} variant="destructive">
|
||||
<DeleteShipmentDialog
|
||||
shipmentReference={shipment.reference}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e: Event) => e.preventDefault()}
|
||||
variant="destructive"
|
||||
>
|
||||
<Trash2 />
|
||||
Remove
|
||||
</DropdownMenuItem>
|
||||
|
||||
Reference in New Issue
Block a user