This commit is contained in:
ghost2023
2026-06-16 14:54:07 +03:00
parent e47340c2c8
commit e0e6020020
8 changed files with 28 additions and 473 deletions

View File

@@ -173,7 +173,7 @@ const App = () => {
<Route path="/onboarding" element={<OnboardingPage />} />
</Route>
<Route element={<RequireCompany path={location.pathname} />}>
<Route element={<RequireCompany />}>
<Route
element={
<AppLayout

View File

@@ -147,7 +147,6 @@ export function AppLayout({
flexShrink: 0,
cursor: "pointer",
};
const toggleStyle: CSSProperties = { ...islandStyle, borderRadius: 10 };
return (
<AppShell

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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