mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
Merge branch 'alpha' of github.com:Tria-plc/edr-platform into alpha
This commit is contained in:
@@ -328,18 +328,21 @@ export class CustomerTruckService {
|
|||||||
if (assignment.departedAt) {
|
if (assignment.departedAt) {
|
||||||
throw new ConflictException('This truck has already left — its load is locked');
|
throw new ConflictException('This truck has already left — its load is locked');
|
||||||
}
|
}
|
||||||
// Containers can only be loaded after the truck has physically arrived at the
|
// Loading a truck at the warehouse implies it is physically present, so a
|
||||||
// warehouse (arrival weighing recorded). Assignment alone is just planning.
|
// truck that is still only assigned (not yet marked arrived) is auto-arrived
|
||||||
if (!assignment.arrivedAt) {
|
// here rather than blocking the operator — the real gross is weighed on
|
||||||
throw new BadRequestException(
|
// departure anyway.
|
||||||
'Record the truck arrival before loading — containers can only be loaded onto an arrived truck',
|
const needsArrival = !assignment.arrivedAt;
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const requested = (dto.containerNumbers ?? []).map((n) => n.trim().toUpperCase());
|
const requested = (dto.containerNumbers ?? []).map((n) => n.trim().toUpperCase());
|
||||||
if (!requested.length) {
|
if (!requested.length) {
|
||||||
throw new BadRequestException('Select at least one container to load onto the truck');
|
throw new BadRequestException('Select at least one container to load onto the truck');
|
||||||
}
|
}
|
||||||
|
// Capacity is size-based: a truck carries at most 2 containers, and a 40ft
|
||||||
|
// container fills the truck (max 1) — mirror the addTruck/updateTruck rule.
|
||||||
|
if (requested.length > 2) {
|
||||||
|
throw new BadRequestException('A truck carries at most 2 containers');
|
||||||
|
}
|
||||||
const bookingNumbers = await this.bookingContainerNumbers(bookingId);
|
const bookingNumbers = await this.bookingContainerNumbers(bookingId);
|
||||||
for (const n of requested) {
|
for (const n of requested) {
|
||||||
if (!bookingNumbers.includes(n)) {
|
if (!bookingNumbers.includes(n)) {
|
||||||
@@ -352,6 +355,12 @@ export class CustomerTruckService {
|
|||||||
throw new ConflictException(`Container ${n} is already loaded onto another truck`);
|
throw new ConflictException(`Container ${n} is already loaded onto another truck`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const sizes = await this.containerSizes(bookingId, requested);
|
||||||
|
if (sizes.some((s) => s.includes('40')) && requested.length > 1) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'A 40ft container fills the truck — load only 1 container onto this truck',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const grossTons = await this.vgmTonsForContainers(bookingId, requested);
|
const grossTons = await this.vgmTonsForContainers(bookingId, requested);
|
||||||
await this.dataSource.transaction(async (manager) => {
|
await this.dataSource.transaction(async (manager) => {
|
||||||
@@ -371,9 +380,20 @@ export class CustomerTruckService {
|
|||||||
);
|
);
|
||||||
// Provisional gross (tonnes) from the loaded containers' VGM — overridden
|
// Provisional gross (tonnes) from the loaded containers' VGM — overridden
|
||||||
// by the weighed gross on departure. (Column is *_kg but holds tonnes.)
|
// by the weighed gross on departure. (Column is *_kg but holds tonnes.)
|
||||||
|
// Auto-stamp arrival if the truck was still only assigned.
|
||||||
await manager.getRepository(CustomerTruckAssignment).update(assignmentId, {
|
await manager.getRepository(CustomerTruckAssignment).update(assignmentId, {
|
||||||
grossWeightKg: grossTons,
|
grossWeightKg: grossTons,
|
||||||
|
...(needsArrival ? { arrivedAt: new Date() } : {}),
|
||||||
});
|
});
|
||||||
|
if (needsArrival) {
|
||||||
|
await manager.query(
|
||||||
|
`UPDATE freight.bookings
|
||||||
|
SET customer_truck_arrived_at = COALESCE(customer_truck_arrived_at, NOW()),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $1`,
|
||||||
|
[bookingId],
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return this.listTrucks(bookingId);
|
return this.listTrucks(bookingId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { ArrayMinSize, ArrayUnique, IsArray, Matches } from 'class-validator';
|
import { ArrayMaxSize, ArrayMinSize, ArrayUnique, IsArray, Matches } from 'class-validator';
|
||||||
|
|
||||||
/** Containers loaded onto a truck at Truck_dispatch (after arrival, before it leaves). */
|
/** Containers loaded onto a truck at Truck_dispatch (after arrival, before it leaves). */
|
||||||
export class LoadCustomerTruckDto {
|
export class LoadCustomerTruckDto {
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ArrayMinSize(1)
|
@ArrayMinSize(1)
|
||||||
|
// A truck carries at most 2 containers (two 20ft, or one 40ft).
|
||||||
|
@ArrayMaxSize(2)
|
||||||
@ArrayUnique()
|
@ArrayUnique()
|
||||||
@Matches(/^[A-Z]{4}\d{7}$/, {
|
@Matches(/^[A-Z]{4}\d{7}$/, {
|
||||||
each: true,
|
each: true,
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ export class LastMileController {
|
|||||||
return this.lastMileService.findById(id);
|
return this.lastMileService.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('booking/:bookingId/arrival-trucks')
|
||||||
|
@ApiOperation({ summary: "Assigned EDR last-mile trucks for a booking (arrival/exit weighing prefill)" })
|
||||||
|
arrivalTrucks(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||||||
|
return this.lastMileService.arrivalTrucksForBooking(bookingId);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('accept/:reference')
|
@Post('accept/:reference')
|
||||||
@BookingStaff(FREIGHT_PERMS.lastMile.accept)
|
@BookingStaff(FREIGHT_PERMS.lastMile.accept)
|
||||||
@ApiOperation({ summary: 'Accept a paid booking and create a last-mile leg' })
|
@ApiOperation({ summary: 'Accept a paid booking and create a last-mile leg' })
|
||||||
|
|||||||
@@ -256,7 +256,95 @@ export class LastMileService {
|
|||||||
return this.findById(id);
|
return this.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The EDR last-mile trucks assigned to a booking, joined with driver details,
|
||||||
|
* shaped for the arrival/exit weighing prefill (plate, driver, type, container).
|
||||||
|
* Returns [] when the booking has no last-mile truck assigned. Lets the
|
||||||
|
* warehouse arrival/load modals surface an assigned EDR truck the same way the
|
||||||
|
* self-haul customer trucks are surfaced.
|
||||||
|
*/
|
||||||
|
async arrivalTrucksForBooking(bookingId: string): Promise<
|
||||||
|
Array<{
|
||||||
|
vehicleId: string;
|
||||||
|
truckPlateNumber: string | null;
|
||||||
|
trailerPlateNumber: string | null;
|
||||||
|
driverName: string | null;
|
||||||
|
driverLicense: string | null;
|
||||||
|
driverPhone: string | null;
|
||||||
|
truckType: string | null;
|
||||||
|
containerNumber: string | null;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const [lm] = await this.lastMileRepository.findAll({
|
||||||
|
where: { bookingId },
|
||||||
|
relations: { vehicle: true, vehicleAssignments: { vehicle: true } },
|
||||||
|
take: 1,
|
||||||
|
});
|
||||||
|
if (!lm) return [];
|
||||||
|
|
||||||
|
// Prefer the multi-truck junction; fall back to the legacy single vehicle.
|
||||||
|
const sources = lm.vehicleAssignments?.length
|
||||||
|
? lm.vehicleAssignments.map((va) => ({
|
||||||
|
vehicle: va.vehicle,
|
||||||
|
containerNumber: va.containerNumber ?? null,
|
||||||
|
}))
|
||||||
|
: lm.vehicle
|
||||||
|
? [{ vehicle: lm.vehicle, containerNumber: null }]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const out: Array<{
|
||||||
|
vehicleId: string;
|
||||||
|
truckPlateNumber: string | null;
|
||||||
|
trailerPlateNumber: string | null;
|
||||||
|
driverName: string | null;
|
||||||
|
driverLicense: string | null;
|
||||||
|
driverPhone: string | null;
|
||||||
|
truckType: string | null;
|
||||||
|
containerNumber: string | null;
|
||||||
|
}> = [];
|
||||||
|
for (const { vehicle, containerNumber } of sources) {
|
||||||
|
if (!vehicle) continue;
|
||||||
|
let driverName = vehicle.assignedDriverName ?? null;
|
||||||
|
let driverLicense: string | null = null;
|
||||||
|
let driverPhone: string | null = null;
|
||||||
|
if (vehicle.assignedDriverId) {
|
||||||
|
try {
|
||||||
|
const d = await this.driversService.findById(vehicle.assignedDriverId);
|
||||||
|
driverName = driverName || `${d.firstName ?? ''} ${d.lastName ?? ''}`.trim() || null;
|
||||||
|
driverLicense = d.licenseNumber ?? null;
|
||||||
|
driverPhone = d.phoneNumber ?? null;
|
||||||
|
} catch {
|
||||||
|
/* driver lookup is best-effort — plate still prefills */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push({
|
||||||
|
vehicleId: vehicle.id,
|
||||||
|
truckPlateNumber: vehicle.powerPlateNo || vehicle.plateNumber || null,
|
||||||
|
trailerPlateNumber: vehicle.trailerPlateNo || null,
|
||||||
|
driverName,
|
||||||
|
driverLicense,
|
||||||
|
driverPhone,
|
||||||
|
truckType: vehicle.vehicleType || null,
|
||||||
|
containerNumber,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
async create(dto: CreateLastMileDto): Promise<LastMile> {
|
async create(dto: CreateLastMileDto): Promise<LastMile> {
|
||||||
|
// Idempotent: a booking gets exactly one last-mile record. Extra trucks live
|
||||||
|
// inside that record (vehicleAssignments), never as additional rows — so if a
|
||||||
|
// last-mile already exists for this booking, return it instead of inserting a
|
||||||
|
// duplicate delivery row (which is what made the same booking appear twice in
|
||||||
|
// the Assign-Mile list).
|
||||||
|
const [existing] = await this.lastMileRepository.findAll({
|
||||||
|
where: { bookingId: dto.bookingId },
|
||||||
|
take: 1,
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
const record = await this.lastMileRepository.create({
|
const record = await this.lastMileRepository.create({
|
||||||
bookingId: dto.bookingId,
|
bookingId: dto.bookingId,
|
||||||
status: dto.status ?? 'READY_TO_TRANSIT',
|
status: dto.status ?? 'READY_TO_TRANSIT',
|
||||||
@@ -318,6 +406,17 @@ export class LastMileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A last-mile truck must have a driver before it can be assigned (same rule
|
||||||
|
// as setVehicles) — block driverless single-vehicle (re)assignment too.
|
||||||
|
if (dto.vehicleId && dto.vehicleId !== existing.vehicleId) {
|
||||||
|
const vehicle = await this.vehiclesService.findById(dto.vehicleId);
|
||||||
|
if (!vehicle?.assignedDriverId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Truck ${vehicle?.plateNumber ?? dto.vehicleId} has no assigned driver — assign a driver to the truck before adding it to this last-mile delivery`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dtoAny = dto as any;
|
const dtoAny = dto as any;
|
||||||
const updated = await this.lastMileRepository.update(id, {
|
const updated = await this.lastMileRepository.update(id, {
|
||||||
...(dto.bookingId !== undefined ? { bookingId: dto.bookingId } : {}),
|
...(dto.bookingId !== undefined ? { bookingId: dto.bookingId } : {}),
|
||||||
@@ -478,6 +577,17 @@ export class LastMileService {
|
|||||||
)];
|
)];
|
||||||
const added = desired.filter((v) => !junctionSet.has(v));
|
const added = desired.filter((v) => !junctionSet.has(v));
|
||||||
const removed = releaseIds.filter((v) => !desiredSet.has(v));
|
const removed = releaseIds.filter((v) => !desiredSet.has(v));
|
||||||
|
|
||||||
|
// A last-mile truck must have a driver before it can be assigned — a delivery
|
||||||
|
// can't run driverless, and the arrival/exit weighing needs the driver.
|
||||||
|
for (const vehicleId of added) {
|
||||||
|
const vehicle = await this.vehiclesService.findById(vehicleId);
|
||||||
|
if (!vehicle?.assignedDriverId) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Truck ${vehicle?.plateNumber ?? vehicleId} has no assigned driver — assign a driver to the truck before adding it to this last-mile delivery`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Vehicles that stay but whose container number changed.
|
// Vehicles that stay but whose container number changed.
|
||||||
const changed = current.filter(
|
const changed = current.filter(
|
||||||
(a) =>
|
(a) =>
|
||||||
|
|||||||
@@ -2830,6 +2830,7 @@ export class WarehouseInventoryService {
|
|||||||
Array<{
|
Array<{
|
||||||
containerNumber: string;
|
containerNumber: string;
|
||||||
goods: string | null;
|
goods: string | null;
|
||||||
|
containerSize: string | null;
|
||||||
stage: 'PENDING' | 'RECEIVED' | 'GRN' | 'ASSIGNED' | 'LOADED' | 'LEFT' | 'DELIVERED';
|
stage: 'PENDING' | 'RECEIVED' | 'GRN' | 'ASSIGNED' | 'LOADED' | 'LEFT' | 'DELIVERED';
|
||||||
grnNumber: string | null;
|
grnNumber: string | null;
|
||||||
truckAssignmentId: string | null;
|
truckAssignmentId: string | null;
|
||||||
@@ -2846,6 +2847,7 @@ export class WarehouseInventoryService {
|
|||||||
const rows: Array<{
|
const rows: Array<{
|
||||||
containerNumber: string;
|
containerNumber: string;
|
||||||
goods: string | null;
|
goods: string | null;
|
||||||
|
containerSize: string | null;
|
||||||
received: boolean;
|
received: boolean;
|
||||||
grnNumber: string | null;
|
grnNumber: string | null;
|
||||||
truckAssignmentId: string | null;
|
truckAssignmentId: string | null;
|
||||||
@@ -2860,6 +2862,7 @@ export class WarehouseInventoryService {
|
|||||||
}> = await this.dataSource.query(
|
}> = await this.dataSource.query(
|
||||||
`SELECT bcu.container_number AS "containerNumber",
|
`SELECT bcu.container_number AS "containerNumber",
|
||||||
COALESCE(ct.cargo_type_name, b.cargo_free_text) AS goods,
|
COALESCE(ct.cargo_type_name, b.cargo_free_text) AS goods,
|
||||||
|
bc.container_size AS "containerSize",
|
||||||
bcu.received_to_port AS received,
|
bcu.received_to_port AS received,
|
||||||
bcu.grn_number AS "grnNumber",
|
bcu.grn_number AS "grnNumber",
|
||||||
ctc.assignment_id AS "truckAssignmentId",
|
ctc.assignment_id AS "truckAssignmentId",
|
||||||
@@ -2896,6 +2899,7 @@ export class WarehouseInventoryService {
|
|||||||
return rows.map((r) => ({
|
return rows.map((r) => ({
|
||||||
containerNumber: r.containerNumber,
|
containerNumber: r.containerNumber,
|
||||||
goods: r.goods,
|
goods: r.goods,
|
||||||
|
containerSize: r.containerSize,
|
||||||
// A container the customer assigned to a truck is ASSIGNED (planned); it
|
// A container the customer assigned to a truck is ASSIGNED (planned); it
|
||||||
// only becomes LOADED once the operator loads it (loaded_at) on truck
|
// only becomes LOADED once the operator loads it (loaded_at) on truck
|
||||||
// leaving. Departed → LEFT, delivered → DELIVERED.
|
// leaving. Departed → LEFT, delivered → DELIVERED.
|
||||||
|
|||||||
@@ -80,14 +80,17 @@ export function ContainerItemsModal({ opened, onClose, bookingId, bookingReferen
|
|||||||
() => (tab === 'ALL' ? items : items.filter((i) => i.stage === tab)),
|
() => (tab === 'ALL' ? items : items.filter((i) => i.stage === tab)),
|
||||||
[items, tab],
|
[items, tab],
|
||||||
);
|
);
|
||||||
// Only arrived, not-yet-departed trucks can be loaded.
|
// Any assigned, not-yet-departed truck can be loaded here — loading a truck at
|
||||||
|
// the warehouse auto-marks it arrived on the backend, so assigned-but-not-yet-
|
||||||
|
// arrived trucks are selectable too (labelled "assigned" until they arrive).
|
||||||
const truckOptions = trucks
|
const truckOptions = trucks
|
||||||
.filter(
|
.filter((t) => !(t as { departedAt?: string }).departedAt)
|
||||||
(t) =>
|
.map((t) => ({
|
||||||
Boolean((t as { arrivedAt?: string }).arrivedAt) &&
|
value: t.id,
|
||||||
!(t as { departedAt?: string }).departedAt,
|
label: `${t.plateNumber} · ${t.driverName}${
|
||||||
)
|
(t as { arrivedAt?: string }).arrivedAt ? '' : ' (assigned)'
|
||||||
.map((t) => ({ value: t.id, label: `${t.plateNumber} · ${t.driverName}` }));
|
}`,
|
||||||
|
}));
|
||||||
|
|
||||||
const loadMutation = useMutation({
|
const loadMutation = useMutation({
|
||||||
mutationFn: () => warehouseService.loadTruck(bookingId as string, truckId as string, selected),
|
mutationFn: () => warehouseService.loadTruck(bookingId as string, truckId as string, selected),
|
||||||
@@ -125,7 +128,28 @@ export function ContainerItemsModal({ opened, onClose, bookingId, bookingReferen
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggle = (n: string) => setSelected((s) => (s.includes(n) ? s.filter((x) => x !== n) : [...s, n]));
|
const is40 = (n: string) =>
|
||||||
|
(items.find((i) => i.containerNumber === n)?.containerSize ?? '').includes('40');
|
||||||
|
|
||||||
|
// A truck carries at most 2 containers, and a 40ft fills the truck (max 1).
|
||||||
|
const toggle = (n: string) =>
|
||||||
|
setSelected((s) => {
|
||||||
|
if (s.includes(n)) return s.filter((x) => x !== n);
|
||||||
|
const next = [...s, n];
|
||||||
|
if (next.length > 2) {
|
||||||
|
toast({ variant: 'destructive', title: 'A truck carries at most 2 containers' });
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
if (next.length > 1 && next.some(is40)) {
|
||||||
|
toast({
|
||||||
|
variant: 'destructive',
|
||||||
|
title: 'A 40ft container fills the truck',
|
||||||
|
description: 'Load only one 40ft container per truck.',
|
||||||
|
});
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -162,6 +186,7 @@ export function ContainerItemsModal({ opened, onClose, bookingId, bookingReferen
|
|||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Th />
|
<Table.Th />
|
||||||
<Table.Th>Container</Table.Th>
|
<Table.Th>Container</Table.Th>
|
||||||
|
<Table.Th>Size</Table.Th>
|
||||||
<Table.Th>Goods</Table.Th>
|
<Table.Th>Goods</Table.Th>
|
||||||
<Table.Th>Stage</Table.Th>
|
<Table.Th>Stage</Table.Th>
|
||||||
<Table.Th>Truck</Table.Th>
|
<Table.Th>Truck</Table.Th>
|
||||||
@@ -182,6 +207,15 @@ export function ContainerItemsModal({ opened, onClose, bookingId, bookingReferen
|
|||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td><Text fw={600}>{i.containerNumber}</Text></Table.Td>
|
<Table.Td><Text fw={600}>{i.containerNumber}</Text></Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{i.containerSize ? (
|
||||||
|
<Badge variant="light" color={i.containerSize.includes('40') ? 'grape' : 'blue'}>
|
||||||
|
{i.containerSize}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed" size="sm">bulk</Text>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
<Table.Td>{i.goods ?? '—'}</Table.Td>
|
<Table.Td>{i.goods ?? '—'}</Table.Td>
|
||||||
<Table.Td><Badge color={STAGE_COLOR[i.stage]} variant="light">{i.stage}</Badge></Table.Td>
|
<Table.Td><Badge color={STAGE_COLOR[i.stage]} variant="light">{i.stage}</Badge></Table.Td>
|
||||||
<Table.Td>{i.truckPlate ?? '—'}</Table.Td>
|
<Table.Td>{i.truckPlate ?? '—'}</Table.Td>
|
||||||
@@ -221,11 +255,17 @@ export function ContainerItemsModal({ opened, onClose, bookingId, bookingReferen
|
|||||||
|
|
||||||
{/* Multiselect → load onto a truck */}
|
{/* Multiselect → load onto a truck */}
|
||||||
<Group justify="space-between" align="flex-end">
|
<Group justify="space-between" align="flex-end">
|
||||||
<Text size="sm" c="dimmed">{selected.length} selected</Text>
|
<Text size="sm" c="dimmed">
|
||||||
|
{selected.length} selected
|
||||||
|
{(() => {
|
||||||
|
const pending = items.filter((i) => !i.truckAssignmentId).length;
|
||||||
|
return pending > 0 ? ` · ${pending} container${pending === 1 ? '' : 's'} pending assignment` : '';
|
||||||
|
})()}
|
||||||
|
</Text>
|
||||||
<Group gap="sm" align="flex-end">
|
<Group gap="sm" align="flex-end">
|
||||||
<Select
|
<Select
|
||||||
label="Load onto truck"
|
label="Load onto truck"
|
||||||
placeholder={truckOptions.length ? 'Select truck' : 'No arrived truck'}
|
placeholder={truckOptions.length ? 'Select truck' : 'No truck assigned'}
|
||||||
data={truckOptions}
|
data={truckOptions}
|
||||||
value={truckId}
|
value={truckId}
|
||||||
onChange={setTruckId}
|
onChange={setTruckId}
|
||||||
|
|||||||
@@ -121,6 +121,14 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|||||||
queryFn: () => warehouseService.getCustomerTrucks(bookingId as string),
|
queryFn: () => warehouseService.getCustomerTrucks(bookingId as string),
|
||||||
enabled: opened && Boolean(bookingId),
|
enabled: opened && Boolean(bookingId),
|
||||||
});
|
});
|
||||||
|
// EDR last-mile trucks assigned to this booking — surfaced even when the modal
|
||||||
|
// is opened from the warehouse flow (which passes no truckPrefill prop), so an
|
||||||
|
// assigned EDR truck no longer shows as "not assigned yet".
|
||||||
|
const { data: lastMileTrucks = [] } = useQuery({
|
||||||
|
queryKey: ['release-last-mile-trucks', bookingId],
|
||||||
|
queryFn: () => warehouseService.getLastMileTrucks(bookingId as string),
|
||||||
|
enabled: opened && Boolean(bookingId),
|
||||||
|
});
|
||||||
// Per-container cargo weights — the truck's net (gross − tare) must equal the
|
// Per-container cargo weights — the truck's net (gross − tare) must equal the
|
||||||
// total cargo weight of the containers selected as loaded on it.
|
// total cargo weight of the containers selected as loaded on it.
|
||||||
const { data: containerWeights = [] } = useQuery({
|
const { data: containerWeights = [] } = useQuery({
|
||||||
@@ -176,6 +184,24 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|||||||
const isCustomerAssignedTruck = Boolean(item?.booking?.customerTruckAssignedAt);
|
const isCustomerAssignedTruck = Boolean(item?.booking?.customerTruckAssignedAt);
|
||||||
const hasLastMileTruckPrefill = Boolean(truckPrefill?.truckPlateNumber || truckPrefill?.trailerPlateNumber);
|
const hasLastMileTruckPrefill = Boolean(truckPrefill?.truckPlateNumber || truckPrefill?.trailerPlateNumber);
|
||||||
|
|
||||||
|
// Opened from the warehouse flow (no truckPrefill prop): once the last-mile
|
||||||
|
// truck query resolves, auto-fill the first assigned EDR truck — without
|
||||||
|
// overwriting anything the operator typed or the locked exit-step values.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!opened || truckPrefill || isExitStep) return;
|
||||||
|
const first = lastMileTrucks[0];
|
||||||
|
if (!first) return;
|
||||||
|
setTruckPlateNumber((p) => p || first.truckPlateNumber || '');
|
||||||
|
setTrailerPlateNumber((p) => p || first.trailerPlateNumber || '');
|
||||||
|
setDriverName((p) => p || first.driverName || '');
|
||||||
|
setDriverLicense((p) => p || first.driverLicense || '');
|
||||||
|
setDriverPhone((p) => p || first.driverPhone || '');
|
||||||
|
setTruckType((p) => p || first.truckType || '');
|
||||||
|
setContainerNumbers((prev) =>
|
||||||
|
prev.length === 1 && !prev[0] && first.containerNumber ? [first.containerNumber] : prev,
|
||||||
|
);
|
||||||
|
}, [opened, truckPrefill, isExitStep, lastMileTrucks]);
|
||||||
|
|
||||||
// Registered trucks for THIS booking, from both sources: EDR last-mile
|
// Registered trucks for THIS booking, from both sources: EDR last-mile
|
||||||
// (truckPrefill) and the customer portal (customer_truck_assignments).
|
// (truckPrefill) and the customer portal (customer_truck_assignments).
|
||||||
const assignedTruckOptions = [
|
const assignedTruckOptions = [
|
||||||
@@ -199,6 +225,16 @@ export function ReleaseOrderModal({ opened, onClose, item, truckPrefill }: Relea
|
|||||||
driverPhone: '',
|
driverPhone: '',
|
||||||
truckType: t.truckType,
|
truckType: t.truckType,
|
||||||
})),
|
})),
|
||||||
|
...lastMileTrucks
|
||||||
|
.filter((t) => t.truckPlateNumber || t.vehicleId)
|
||||||
|
.map((t) => ({
|
||||||
|
value: (t.truckPlateNumber || t.vehicleId) as string,
|
||||||
|
label: `Last-mile · ${t.truckPlateNumber ?? ''}${t.driverName ? ` — ${t.driverName}` : ''}`,
|
||||||
|
trailerPlate: t.trailerPlateNumber ?? '',
|
||||||
|
driverName: t.driverName ?? '',
|
||||||
|
driverPhone: t.driverPhone ?? '',
|
||||||
|
truckType: t.truckType ?? '',
|
||||||
|
})),
|
||||||
];
|
];
|
||||||
// Only trucks actually assigned to THIS booking (last-mile prefill or customer
|
// Only trucks actually assigned to THIS booking (last-mile prefill or customer
|
||||||
// portal) are selectable. No global fleet list — if nothing is assigned, the
|
// portal) are selectable. No global fleet list — if nothing is assigned, the
|
||||||
|
|||||||
@@ -666,8 +666,12 @@ const LastMilePage = () => {
|
|||||||
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT });
|
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT });
|
||||||
void qc.invalidateQueries({ queryKey: ["vehicles"] });
|
void qc.invalidateQueries({ queryKey: ["vehicles"] });
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (e: unknown) => {
|
||||||
toast({ title: "Assign failed", variant: "destructive" });
|
// Surface the backend reason (e.g. "Truck … has no assigned driver …").
|
||||||
|
const raw = (e as { response?: { data?: { message?: string | string[] } } })?.response?.data
|
||||||
|
?.message;
|
||||||
|
const description = Array.isArray(raw) ? raw.join(", ") : raw;
|
||||||
|
toast({ title: "Assign failed", description, variant: "destructive" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ export type ContainerItemStage = 'PENDING' | 'RECEIVED' | 'GRN' | 'ASSIGNED' | '
|
|||||||
export interface ContainerItem {
|
export interface ContainerItem {
|
||||||
containerNumber: string;
|
containerNumber: string;
|
||||||
goods: string | null;
|
goods: string | null;
|
||||||
|
/** Container size, e.g. "20ft" / "40ft"; null for bulk. */
|
||||||
|
containerSize: string | null;
|
||||||
stage: ContainerItemStage;
|
stage: ContainerItemStage;
|
||||||
grnNumber: string | null;
|
grnNumber: string | null;
|
||||||
truckAssignmentId: string | null;
|
truckAssignmentId: string | null;
|
||||||
@@ -126,6 +128,18 @@ const cleanParams = (params: object) =>
|
|||||||
Object.entries(params).filter(([, value]) => value !== undefined && value !== '' && value !== null),
|
Object.entries(params).filter(([, value]) => value !== undefined && value !== '' && value !== null),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** An assigned EDR last-mile truck, shaped for the arrival/exit weighing prefill. */
|
||||||
|
export interface LastMileArrivalTruck {
|
||||||
|
vehicleId: string;
|
||||||
|
truckPlateNumber: string | null;
|
||||||
|
trailerPlateNumber: string | null;
|
||||||
|
driverName: string | null;
|
||||||
|
driverLicense: string | null;
|
||||||
|
driverPhone: string | null;
|
||||||
|
truckType: string | null;
|
||||||
|
containerNumber: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const warehouseService = {
|
export const warehouseService = {
|
||||||
/** Customer self-haul trucks assigned to a booking (portal multi-truck). */
|
/** Customer self-haul trucks assigned to a booking (portal multi-truck). */
|
||||||
getCustomerTrucks: async (bookingId: string): Promise<Freight.ICustomerTruck[]> => {
|
getCustomerTrucks: async (bookingId: string): Promise<Freight.ICustomerTruck[]> => {
|
||||||
@@ -133,6 +147,12 @@ export const warehouseService = {
|
|||||||
return data?.data ?? data ?? [];
|
return data?.data ?? data ?? [];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Assigned EDR last-mile trucks for a booking (arrival/exit weighing prefill). */
|
||||||
|
getLastMileTrucks: async (bookingId: string): Promise<LastMileArrivalTruck[]> => {
|
||||||
|
const { data } = await apiClient.get(`/last-mile/booking/${bookingId}/arrival-trucks`);
|
||||||
|
return data?.data ?? data ?? [];
|
||||||
|
},
|
||||||
|
|
||||||
/** Per-container/bulk items of a booking with lifecycle stage + refs. */
|
/** Per-container/bulk items of a booking with lifecycle stage + refs. */
|
||||||
getContainerItems: async (bookingId: string): Promise<ContainerItem[]> => {
|
getContainerItems: async (bookingId: string): Promise<ContainerItem[]> => {
|
||||||
const { data } = await apiClient.get(
|
const { data } = await apiClient.get(
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ export function CustomerTruckAssignmentCard({
|
|||||||
const availableContainers = (booking.containerNumbers ?? []).filter(
|
const availableContainers = (booking.containerNumbers ?? []).filter(
|
||||||
(n) => !assignedNumbers.has(n) || editingOwn.has(n),
|
(n) => !assignedNumbers.has(n) || editingOwn.has(n),
|
||||||
);
|
);
|
||||||
|
// Containers on the booking not yet assigned to any truck (independent of edit).
|
||||||
|
const pendingAssignmentCount = (booking.containerNumbers ?? []).filter(
|
||||||
|
(n) => !assignedNumbers.has(n),
|
||||||
|
).length;
|
||||||
|
|
||||||
// Both import and export specify the containers each truck carries.
|
// Both import and export specify the containers each truck carries.
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
@@ -158,11 +162,18 @@ export function CustomerTruckAssignmentCard({
|
|||||||
<Truck size={18} color="#0a9f6a" />
|
<Truck size={18} color="#0a9f6a" />
|
||||||
<CardTitle>External Truck Assignment</CardTitle>
|
<CardTitle>External Truck Assignment</CardTitle>
|
||||||
</Group>
|
</Group>
|
||||||
{trucks.length > 0 && (
|
<Group gap={12}>
|
||||||
<Text size="sm" fw={700} c="#0a9f6a">
|
{pendingAssignmentCount > 0 && (
|
||||||
{trucks.length} truck{trucks.length !== 1 ? "s" : ""}
|
<Text size="sm" fw={600} c="#b45309">
|
||||||
</Text>
|
{pendingAssignmentCount} container{pendingAssignmentCount !== 1 ? "s" : ""} pending assignment
|
||||||
)}
|
</Text>
|
||||||
|
)}
|
||||||
|
{trucks.length > 0 && (
|
||||||
|
<Text size="sm" fw={700} c="#0a9f6a">
|
||||||
|
{trucks.length} truck{trucks.length !== 1 ? "s" : ""}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Assigned trucks */}
|
{/* Assigned trucks */}
|
||||||
|
|||||||
@@ -1745,30 +1745,32 @@ export class BookingsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolves the passenger's actual boarding/alighting stations for one leg from
|
// Resolves the passenger's actual boarding/alighting stations AND times for one leg
|
||||||
// originStationId/destinationStationId (set when the booking covers only part of a
|
// from originStationId/destinationStationId (set when the booking covers only part of
|
||||||
// longer multi-stop schedule, e.g. train runs A→D but the passenger booked B→D) via
|
// a longer multi-stop schedule, e.g. train runs A→D but the passenger booked B→D) via
|
||||||
// the schedule's stopTimes, falling back to the schedule's own full-route endpoints
|
// the schedule's stopTimes, falling back to the schedule's own full-route endpoints/
|
||||||
// when there's no segment override (older records, or a booking that covers the
|
// times when there's no segment override (older records, or a booking that covers the
|
||||||
// whole run). Mirrors notifications.service.ts's resolveSegmentStations — that's
|
// whole run). Station resolution mirrors notifications.service.ts's
|
||||||
// already applied to SMS/email; this brings the booking API (voucher, detail page,
|
// resolveSegmentStations (already applied to SMS/email); the departureAt/arrivalAt
|
||||||
// confirmation) to the same behavior instead of always showing the train's full route.
|
// resolution mirrors search.service.ts's leg construction (originStop.plannedDepartureAt
|
||||||
|
// / destStop.plannedArrivalAt) — this brings the booking API (voucher, detail page,
|
||||||
|
// confirmation) to the same behavior search results already have, instead of always
|
||||||
|
// showing the train's full-route span.
|
||||||
private resolveSegmentStations(
|
private resolveSegmentStations(
|
||||||
schedule: any,
|
schedule: any,
|
||||||
originStationId: string | null | undefined,
|
originStationId: string | null | undefined,
|
||||||
destinationStationId: string | null | undefined,
|
destinationStationId: string | null | undefined,
|
||||||
): { origin: any; destination: any } {
|
): { origin: any; destination: any; departureAt: any; arrivalAt: any } {
|
||||||
const stopTimes: any[] = schedule?.stopTimes ?? [];
|
const stopTimes: any[] = schedule?.stopTimes ?? [];
|
||||||
const findStation = (stationId: string | null | undefined, fallback: any) => {
|
const findStop = (stationId: string | null | undefined) =>
|
||||||
if (stationId && stopTimes.length > 0) {
|
stationId && stopTimes.length > 0 ? stopTimes.find((st: any) => st.stationId === stationId) : undefined;
|
||||||
const stop = stopTimes.find((st: any) => st.stationId === stationId);
|
const originStop = findStop(originStationId);
|
||||||
if (stop?.station) return stop.station;
|
const destStop = findStop(destinationStationId);
|
||||||
}
|
|
||||||
return fallback ?? null;
|
|
||||||
};
|
|
||||||
return {
|
return {
|
||||||
origin: findStation(originStationId, schedule?.originStation),
|
origin: originStop?.station ?? schedule?.originStation ?? null,
|
||||||
destination: findStation(destinationStationId, schedule?.destinationStation),
|
destination: destStop?.station ?? schedule?.destinationStation ?? null,
|
||||||
|
departureAt: originStop?.plannedDepartureAt ?? schedule?.departureAt ?? null,
|
||||||
|
arrivalAt: destStop?.plannedArrivalAt ?? schedule?.arrivalAt ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1878,7 +1880,7 @@ export class BookingsService {
|
|||||||
trainName: (booking as any).schedule.train.name,
|
trainName: (booking as any).schedule.train.name,
|
||||||
origin: { id: outboundSegment.origin.id, name: outboundSegment.origin.name, code: outboundSegment.origin.code, city: outboundSegment.origin.city },
|
origin: { id: outboundSegment.origin.id, name: outboundSegment.origin.name, code: outboundSegment.origin.code, city: outboundSegment.origin.city },
|
||||||
destination: { id: outboundSegment.destination.id, name: outboundSegment.destination.name, code: outboundSegment.destination.code, city: outboundSegment.destination.city },
|
destination: { id: outboundSegment.destination.id, name: outboundSegment.destination.name, code: outboundSegment.destination.code, city: outboundSegment.destination.city },
|
||||||
departureAt: (booking as any).schedule.departureAt, arrivalAt: (booking as any).schedule.arrivalAt,
|
departureAt: outboundSegment.departureAt, arrivalAt: outboundSegment.arrivalAt,
|
||||||
},
|
},
|
||||||
returnSchedule: (booking as any).returnSchedule
|
returnSchedule: (booking as any).returnSchedule
|
||||||
? {
|
? {
|
||||||
@@ -1887,23 +1889,35 @@ export class BookingsService {
|
|||||||
trainName: (booking as any).returnSchedule.train.name,
|
trainName: (booking as any).returnSchedule.train.name,
|
||||||
origin: { id: returnSegment!.origin.id, name: returnSegment!.origin.name, code: returnSegment!.origin.code, city: returnSegment!.origin.city },
|
origin: { id: returnSegment!.origin.id, name: returnSegment!.origin.name, code: returnSegment!.origin.code, city: returnSegment!.origin.city },
|
||||||
destination: { id: returnSegment!.destination.id, name: returnSegment!.destination.name, code: returnSegment!.destination.code, city: returnSegment!.destination.city },
|
destination: { id: returnSegment!.destination.id, name: returnSegment!.destination.name, code: returnSegment!.destination.code, city: returnSegment!.destination.city },
|
||||||
departureAt: (booking as any).returnSchedule.departureAt, arrivalAt: (booking as any).returnSchedule.arrivalAt,
|
departureAt: returnSegment!.departureAt, arrivalAt: returnSegment!.arrivalAt,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
passengers: (booking as any).seats?.map((bs: any) => ({
|
passengers: (booking as any).seats?.map((bs: any) => {
|
||||||
fullName: bs.passengerName,
|
// A coach type can have several seat classes (e.g. a VIP Bed coach has separate
|
||||||
category: bs.passengerCategory,
|
// Upper/Lower classes) — seatClasses[0] is whichever was seeded first, so it always
|
||||||
leg: bs.leg ?? 1,
|
// showed the SAME class for every seat in the coach regardless of that seat's own
|
||||||
fareMinor: bs.fareMinor,
|
// bed position. Match against the seat's actual bedPosition instead (Seat.bedPosition
|
||||||
verifaydaVerified: bs.verifaydaVerified,
|
// is lowercase, SeatClass.bedPosition is uppercase — compare case-insensitively).
|
||||||
seat: {
|
// Falls back to [0] for non-bed seats (bedPosition is null, single class per coach).
|
||||||
id: bs.seat.id,
|
const classes = bs.seat.coach.coachType?.seatClasses ?? [];
|
||||||
number: bs.seat.seatNumber,
|
const matchedClass = bs.seat.bedPosition
|
||||||
coach: bs.seat.coach.number,
|
? classes.find((sc: any) => sc.bedPosition?.toLowerCase() === bs.seat.bedPosition.toLowerCase())
|
||||||
coachId: bs.seat.coach.id,
|
: null;
|
||||||
seatClass: bs.seat.coach.coachType?.seatClasses?.[0]?.name ?? null,
|
return {
|
||||||
},
|
fullName: bs.passengerName,
|
||||||
})),
|
category: bs.passengerCategory,
|
||||||
|
leg: bs.leg ?? 1,
|
||||||
|
fareMinor: bs.fareMinor,
|
||||||
|
verifaydaVerified: bs.verifaydaVerified,
|
||||||
|
seat: {
|
||||||
|
id: bs.seat.id,
|
||||||
|
number: bs.seat.seatNumber,
|
||||||
|
coach: bs.seat.coach.number,
|
||||||
|
coachId: bs.seat.coach.id,
|
||||||
|
seatClass: (matchedClass ?? classes[0])?.name ?? null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
payment: (booking as any).paymentIntent
|
payment: (booking as any).paymentIntent
|
||||||
? {
|
? {
|
||||||
method: (booking as any).paymentIntent.method,
|
method: (booking as any).paymentIntent.method,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useAuthStore } from '@/lib/auth-store';
|
import { useAuthStore } from '@/lib/auth-store';
|
||||||
import { useBookingStore } from '@/lib/booking-store';
|
import { useBookingStore } from '@/lib/booking-store';
|
||||||
import { UserPlus, ChevronLeft } from 'lucide-react';
|
import { UserPlus, LogIn, ChevronLeft } from 'lucide-react';
|
||||||
|
|
||||||
function Tooltip({ children, content }: { children: React.ReactNode; content: string[] }) {
|
function Tooltip({ children, content }: { children: React.ReactNode; content: string[] }) {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
@@ -95,7 +95,6 @@ export default function AuthCheckPage() {
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* TODO: re-enable once auth is integrated
|
|
||||||
<Tooltip content={[
|
<Tooltip content={[
|
||||||
'Saved passenger details',
|
'Saved passenger details',
|
||||||
'View booking history',
|
'View booking history',
|
||||||
@@ -109,7 +108,6 @@ export default function AuthCheckPage() {
|
|||||||
SignIn or Register
|
SignIn or Register
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
*/}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 text-center">
|
<div className="mt-8 text-center">
|
||||||
|
|||||||
@@ -192,9 +192,7 @@ export default function AppSidebar() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// TODO: Sign in / Register temporarily disabled — re-enable later.
|
<div className="flex items-center gap-2 px-1 pt-1">
|
||||||
null
|
|
||||||
/* <div className="flex items-center gap-2 px-1 pt-1">
|
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
className="flex-1 text-center px-3 py-2 text-sm font-medium text-gray-100 hover:bg-white/10 rounded-lg transition-colors"
|
className="flex-1 text-center px-3 py-2 text-sm font-medium text-gray-100 hover:bg-white/10 rounded-lg transition-colors"
|
||||||
@@ -207,7 +205,7 @@ export default function AppSidebar() {
|
|||||||
>
|
>
|
||||||
Register
|
Register
|
||||||
</Link>
|
</Link>
|
||||||
</div> */
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
// NOTE: User icon + useAuthStore are unused while the Sign in / Account tab is
|
import { Home, Phone, Ticket, User } from 'lucide-react';
|
||||||
// temporarily disabled below. Re-add them when that tab is restored.
|
|
||||||
import { Home, Phone, Ticket } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useAuthStore } from '@/lib/auth-store';
|
||||||
|
|
||||||
// The linear, one-screen-at-a-time booking flow — each of these pages already
|
// The linear, one-screen-at-a-time booking flow — each of these pages already
|
||||||
// has its own sticky mobile CTA bar (and the mobile step strip at the top),
|
// has its own sticky mobile CTA bar (and the mobile step strip at the top),
|
||||||
@@ -22,6 +21,7 @@ const LINEAR_FLOW_PREFIXES = [
|
|||||||
|
|
||||||
export default function BottomTabBar() {
|
export default function BottomTabBar() {
|
||||||
const pathname = usePathname() ?? '';
|
const pathname = usePathname() ?? '';
|
||||||
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||||
|
|
||||||
const isInLinearFlow = LINEAR_FLOW_PREFIXES.some((p) => pathname.startsWith(p));
|
const isInLinearFlow = LINEAR_FLOW_PREFIXES.some((p) => pathname.startsWith(p));
|
||||||
if (isInLinearFlow) return null;
|
if (isInLinearFlow) return null;
|
||||||
@@ -30,13 +30,12 @@ export default function BottomTabBar() {
|
|||||||
{ href: '/', label: 'Home', icon: Home, match: (p: string) => p === '/' },
|
{ href: '/', label: 'Home', icon: Home, match: (p: string) => p === '/' },
|
||||||
{ href: '/booking/lookup', label: 'Bookings', icon: Ticket, match: (p: string) => p.startsWith('/booking/lookup') || p.startsWith('/booking/detail') },
|
{ href: '/booking/lookup', label: 'Bookings', icon: Ticket, match: (p: string) => p.startsWith('/booking/lookup') || p.startsWith('/booking/detail') },
|
||||||
{ href: '/contact', label: 'Contact', icon: Phone, match: (p: string) => p.startsWith('/contact') },
|
{ href: '/contact', label: 'Contact', icon: Phone, match: (p: string) => p.startsWith('/contact') },
|
||||||
// TODO: Sign in / Account tab temporarily disabled — re-enable later.
|
{
|
||||||
// {
|
href: isAuthenticated ? '/profile' : '/login',
|
||||||
// href: isAuthenticated ? '/profile' : '/login',
|
label: isAuthenticated ? 'Account' : 'Sign in',
|
||||||
// label: isAuthenticated ? 'Account' : 'Sign in',
|
icon: User,
|
||||||
// icon: User,
|
match: (p: string) => p.startsWith('/profile') || p.startsWith('/login') || p.startsWith('/register'),
|
||||||
// match: (p: string) => p.startsWith('/profile') || p.startsWith('/login') || p.startsWith('/register'),
|
},
|
||||||
// },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user