mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
Merge pull request #645 from Tria-plc/uienhancement
Export Truck assignment Added truck assignment between payment and Loading status guarded assignment to maxmum number of truck to the number of containers
This commit is contained in:
@@ -2,18 +2,24 @@ import {
|
|||||||
BadRequestException,
|
BadRequestException,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
Injectable,
|
Injectable,
|
||||||
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { DataSource, EntityManager, IsNull } from 'typeorm';
|
import { DataSource, EntityManager, IsNull } from 'typeorm';
|
||||||
|
import { NotificationAudience, NotificationType } from '@edr/types';
|
||||||
|
|
||||||
import { AddCustomerTruckDto } from './dto/add-customer-truck.dto';
|
import { AddCustomerTruckDto } from './dto/add-customer-truck.dto';
|
||||||
import { DepartCustomerTruckDto } from './dto/depart-customer-truck.dto';
|
import { DepartCustomerTruckDto } from './dto/depart-customer-truck.dto';
|
||||||
import { CustomerTruckAssignment } from './entities/customer-truck-assignment.entity';
|
import { CustomerTruckAssignment } from './entities/customer-truck-assignment.entity';
|
||||||
import { CustomerTruckContainer } from './entities/customer-truck-container.entity';
|
import { CustomerTruckContainer } from './entities/customer-truck-container.entity';
|
||||||
import { CustomerTruckAssignmentsRepository } from './customer-truck-assignments.repository';
|
import { CustomerTruckAssignmentsRepository } from './customer-truck-assignments.repository';
|
||||||
|
import { NotificationInboxService } from '../notification-inbox/notification-inbox.service';
|
||||||
|
import { NotificationsService } from '../notifications/notifications.service';
|
||||||
|
import { sendCompanyChannels } from '../notifications/notify-company.util';
|
||||||
|
|
||||||
interface BookingGuardRow {
|
interface BookingGuardRow {
|
||||||
tradeDirection: string | null;
|
tradeDirection: string | null;
|
||||||
|
freightType: string | null;
|
||||||
firstMile: string | null;
|
firstMile: string | null;
|
||||||
lastMile: string | null;
|
lastMile: string | null;
|
||||||
paymentStatus: string | null;
|
paymentStatus: string | null;
|
||||||
@@ -29,9 +35,13 @@ interface BookingGuardRow {
|
|||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CustomerTruckService {
|
export class CustomerTruckService {
|
||||||
|
private readonly logger = new Logger(CustomerTruckService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly dataSource: DataSource,
|
private readonly dataSource: DataSource,
|
||||||
private readonly assignments: CustomerTruckAssignmentsRepository,
|
private readonly assignments: CustomerTruckAssignmentsRepository,
|
||||||
|
private readonly inbox: NotificationInboxService,
|
||||||
|
private readonly notifications: NotificationsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
listTrucks(bookingId: string): Promise<CustomerTruckAssignment[]> {
|
listTrucks(bookingId: string): Promise<CustomerTruckAssignment[]> {
|
||||||
@@ -41,14 +51,20 @@ export class CustomerTruckService {
|
|||||||
async addTruck(bookingId: string, dto: AddCustomerTruckDto): Promise<CustomerTruckAssignment[]> {
|
async addTruck(bookingId: string, dto: AddCustomerTruckDto): Promise<CustomerTruckAssignment[]> {
|
||||||
const booking = await this.loadBookingGuard(bookingId);
|
const booking = await this.loadBookingGuard(bookingId);
|
||||||
this.assertSelfHaulPaid(booking);
|
this.assertSelfHaulPaid(booking);
|
||||||
|
this.assertAssignmentWindow(booking);
|
||||||
|
|
||||||
const requested = (dto.containerNumbers ?? []).map((n) => n.trim().toUpperCase());
|
// Bulk bookings have no containers — the truck hauls loose tonnage and is
|
||||||
|
// weighed out on departure (gross_weight_kg). Container bookings assign the
|
||||||
|
// 1–2 specific containers each truck carries.
|
||||||
|
const isBulk = booking.freightType === 'BULK';
|
||||||
|
const requested = isBulk
|
||||||
|
? []
|
||||||
|
: (dto.containerNumbers ?? []).map((n) => n.trim().toUpperCase());
|
||||||
|
|
||||||
// Both import and export specify the containers each truck carries. Capacity
|
// Container capacity is size-based: a 40ft container fills the truck (max 1);
|
||||||
// is size-based: a 40ft container fills the truck (max 1); two 20ft containers
|
// two 20ft containers fit (max 2), no size mixing. #trucks <= #containers
|
||||||
// fit (max 2), no size mixing. #trucks <= #containers follows naturally since
|
// follows naturally since each container is assigned to exactly one truck.
|
||||||
// each container is assigned to exactly one truck.
|
if (!isBulk && requested.length < 1) {
|
||||||
if (requested.length < 1) {
|
|
||||||
throw new BadRequestException('Select at least one container for this truck');
|
throw new BadRequestException('Select at least one container for this truck');
|
||||||
}
|
}
|
||||||
if (requested.length > 2) {
|
if (requested.length > 2) {
|
||||||
@@ -395,20 +411,74 @@ export class CustomerTruckService {
|
|||||||
});
|
});
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
|
const assignment = await m
|
||||||
|
.getRepository(CustomerTruckAssignment)
|
||||||
|
.findOne({ where: { id: container.assignmentId } });
|
||||||
|
const justArrived = Boolean(assignment) && !assignment?.arrivedAt;
|
||||||
|
|
||||||
await m
|
await m
|
||||||
.getRepository(CustomerTruckAssignment)
|
.getRepository(CustomerTruckAssignment)
|
||||||
.update({ id: container.assignmentId, arrivedAt: IsNull() }, { arrivedAt: new Date() });
|
.update({ id: container.assignmentId, arrivedAt: IsNull() }, { arrivedAt: new Date() });
|
||||||
|
|
||||||
await this.syncBookingArrival(bookingId, m);
|
await this.syncBookingArrival(bookingId, m);
|
||||||
|
|
||||||
|
if (justArrived && assignment) {
|
||||||
|
await this.notifyTruckArrival(bookingId, assignment.plateNumber, m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mark every truck on the booking arrived (fallback when no container is known). */
|
/** Mark every truck on the booking arrived (fallback when no container is known). */
|
||||||
async markAllArrived(bookingId: string, manager?: EntityManager): Promise<void> {
|
async markAllArrived(bookingId: string, manager?: EntityManager): Promise<void> {
|
||||||
const m = manager ?? this.dataSource.manager;
|
const m = manager ?? this.dataSource.manager;
|
||||||
|
const justArrived = await m
|
||||||
|
.getRepository(CustomerTruckAssignment)
|
||||||
|
.find({ where: { bookingId, arrivedAt: IsNull() } });
|
||||||
await m
|
await m
|
||||||
.getRepository(CustomerTruckAssignment)
|
.getRepository(CustomerTruckAssignment)
|
||||||
.update({ bookingId, arrivedAt: IsNull() }, { arrivedAt: new Date() });
|
.update({ bookingId, arrivedAt: IsNull() }, { arrivedAt: new Date() });
|
||||||
await this.syncBookingArrival(bookingId, m);
|
await this.syncBookingArrival(bookingId, m);
|
||||||
|
for (const truck of justArrived) {
|
||||||
|
await this.notifyTruckArrival(bookingId, truck.plateNumber, m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Best-effort truck-arrival notification to the booking's company across every
|
||||||
|
* channel: in-app (portal inbox) + SMS + email. Never throws — a missing
|
||||||
|
* provider or contact must not break the arrival flow.
|
||||||
|
*/
|
||||||
|
private async notifyTruckArrival(
|
||||||
|
bookingId: string,
|
||||||
|
plateNumber: string | null,
|
||||||
|
m: EntityManager,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const [booking]: Array<{ companyId: string | null; reference: string | null }> =
|
||||||
|
await m.query(
|
||||||
|
`SELECT company_id AS "companyId", reference
|
||||||
|
FROM freight.bookings
|
||||||
|
WHERE id = $1 AND deleted_at IS NULL`,
|
||||||
|
[bookingId],
|
||||||
|
);
|
||||||
|
if (!booking?.companyId) return;
|
||||||
|
const ref = booking.reference ?? bookingId;
|
||||||
|
const truck = plateNumber ? `Truck ${plateNumber}` : 'A customer truck';
|
||||||
|
const body = `${truck} has arrived at the terminal for booking ${ref}.`;
|
||||||
|
await this.inbox.notify({
|
||||||
|
recipients: { companyId: booking.companyId },
|
||||||
|
audience: NotificationAudience.PORTAL,
|
||||||
|
type: NotificationType.BOOKING_STATUS,
|
||||||
|
title: 'Truck arrived',
|
||||||
|
body,
|
||||||
|
link: `/bookings/${bookingId}`,
|
||||||
|
data: { bookingId, plateNumber, action: 'TRUCK_ARRIVED' },
|
||||||
|
});
|
||||||
|
await sendCompanyChannels(this.dataSource, this.notifications, booking.companyId, body);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Truck-arrival notify failed for ${bookingId}: ${(err as Error).message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -430,6 +500,7 @@ export class CustomerTruckService {
|
|||||||
private async loadBookingGuard(bookingId: string): Promise<BookingGuardRow> {
|
private async loadBookingGuard(bookingId: string): Promise<BookingGuardRow> {
|
||||||
const [row]: BookingGuardRow[] = await this.dataSource.query(
|
const [row]: BookingGuardRow[] = await this.dataSource.query(
|
||||||
`SELECT trade_direction AS "tradeDirection",
|
`SELECT trade_direction AS "tradeDirection",
|
||||||
|
freight_type AS "freightType",
|
||||||
first_mile_pickup_address AS "firstMile",
|
first_mile_pickup_address AS "firstMile",
|
||||||
last_mile_delivery_address AS "lastMile",
|
last_mile_delivery_address AS "lastMile",
|
||||||
payment_status AS "paymentStatus",
|
payment_status AS "paymentStatus",
|
||||||
@@ -463,6 +534,30 @@ export class CustomerTruckService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assignment window by direction:
|
||||||
|
* - IMPORT: pickup trucks are assigned only AFTER the train has arrived.
|
||||||
|
* - EXPORT / DOMESTIC: delivery trucks are assigned only BEFORE the cargo is
|
||||||
|
* loaded onto the train (booking still PAID / TRUCK_ASSIGNED). Once loaded
|
||||||
|
* (IN_TRANSIT and beyond) assignment is closed.
|
||||||
|
*/
|
||||||
|
private assertAssignmentWindow(booking: BookingGuardRow): void {
|
||||||
|
const status = booking.status ?? '';
|
||||||
|
if (booking.tradeDirection === 'IMPORT') {
|
||||||
|
if (status !== 'ARRIVED') {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Import pickup trucks can only be assigned after the train has arrived',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!['PAID', 'TRUCK_ASSIGNED'].includes(status)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'Export delivery trucks can only be assigned before the cargo is loaded onto the train',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async bookingContainerNumbers(bookingId: string): Promise<string[]> {
|
private async bookingContainerNumbers(bookingId: string): Promise<string[]> {
|
||||||
const rows: Array<{ containerNumber: string }> = await this.dataSource.query(
|
const rows: Array<{ containerNumber: string }> = await this.dataSource.query(
|
||||||
`SELECT bcu.container_number AS "containerNumber"
|
`SELECT bcu.container_number AS "containerNumber"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||||
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
import { Between, DataSource, EntityManager, FindManyOptions, ILike, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
|
import { Between, DataSource, EntityManager, FindManyOptions, ILike, LessThanOrEqual, MoreThanOrEqual } from 'typeorm';
|
||||||
|
|
||||||
import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
|
import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
|
||||||
@@ -396,6 +397,54 @@ export class WarehouseInventoryService {
|
|||||||
* but has no customer truck assigned yet, nudge the customer to assign one — with
|
* but has no customer truck assigned yet, nudge the customer to assign one — with
|
||||||
* a deep-link to the booking's truck-assignment card. Fire-and-forget.
|
* a deep-link to the booking's truck-assignment card. Fire-and-forget.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Recurring nudge: keep reminding self-haul IMPORT customers to assign a
|
||||||
|
* collection truck while their goods are still in the warehouse
|
||||||
|
* (READY_FOR_PICKUP) and no truck has been assigned yet. Stops once a truck is
|
||||||
|
* assigned (customer_truck_assigned_at set) or the goods leave (DELIVERED).
|
||||||
|
*/
|
||||||
|
@Cron(CronExpression.EVERY_30_MINUTES, { name: 'import-truck-assignment-reminder' })
|
||||||
|
async remindImportTruckAssignment(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const rows: Array<{
|
||||||
|
bookingId: string;
|
||||||
|
companyId: string | null;
|
||||||
|
reference: string | null;
|
||||||
|
}> = await this.dataSource.query(
|
||||||
|
`SELECT DISTINCT b.id AS "bookingId",
|
||||||
|
b.company_id AS "companyId",
|
||||||
|
b.reference
|
||||||
|
FROM freight.warehouse_inventory inv
|
||||||
|
JOIN freight.bookings b ON b.id = inv.booking_id AND b.deleted_at IS NULL
|
||||||
|
WHERE inv.deleted_at IS NULL
|
||||||
|
AND inv.status = 'READY_FOR_PICKUP'
|
||||||
|
AND b.trade_direction = 'IMPORT'
|
||||||
|
AND b.customer_truck_assigned_at IS NULL
|
||||||
|
AND COALESCE(NULLIF(TRIM(b.last_mile_delivery_address), ''), '') = ''`,
|
||||||
|
);
|
||||||
|
if (!rows.length) return;
|
||||||
|
this.logger.log(
|
||||||
|
`Import truck-assignment reminder: ${rows.length} booking(s) awaiting a collection truck`,
|
||||||
|
);
|
||||||
|
for (const row of rows) {
|
||||||
|
await this.notifyTruckAssignmentNeeded(
|
||||||
|
{
|
||||||
|
companyId: row.companyId,
|
||||||
|
reference: row.reference,
|
||||||
|
hasFirstMile: false,
|
||||||
|
hasLastMile: false,
|
||||||
|
customerTruckAssignedAt: null,
|
||||||
|
},
|
||||||
|
row.bookingId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Import truck-assignment reminder tick failed: ${(err as Error).message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async notifyTruckAssignmentNeeded(booking: {
|
private async notifyTruckAssignmentNeeded(booking: {
|
||||||
companyId?: string | null;
|
companyId?: string | null;
|
||||||
reference?: string | null;
|
reference?: string | null;
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ export const BOOKING_STATUS_STYLES: Record<string, StatusStyle> = {
|
|||||||
label: "Paid",
|
label: "Paid",
|
||||||
color: "bg-[color:var(--freight-brand-muted)] text-[color:var(--freight-brand)] border-[color:var(--freight-brand-border)]",
|
color: "bg-[color:var(--freight-brand-muted)] text-[color:var(--freight-brand)] border-[color:var(--freight-brand-border)]",
|
||||||
},
|
},
|
||||||
|
TRUCK_ASSIGNED: {
|
||||||
|
label: "Truck Assigned",
|
||||||
|
color: "bg-[color:var(--freight-brand-muted)] text-[color:var(--freight-brand)] border-[color:var(--freight-brand-border)]",
|
||||||
|
},
|
||||||
IN_TRANSIT: {
|
IN_TRANSIT: {
|
||||||
label: "In Transit",
|
label: "In Transit",
|
||||||
color: "bg-sky-50 text-sky-700 border-sky-200",
|
color: "bg-sky-50 text-sky-700 border-sky-200",
|
||||||
@@ -206,6 +210,12 @@ export const BOOKING_STATUS_META: Record<string, StatusMeta> = {
|
|||||||
color: "text-[color:var(--freight-brand)]",
|
color: "text-[color:var(--freight-brand)]",
|
||||||
stage: 4,
|
stage: 4,
|
||||||
},
|
},
|
||||||
|
TRUCK_ASSIGNED: {
|
||||||
|
title: "Truck Assigned",
|
||||||
|
description: "Customer truck assigned for self-haul; ready for operations.",
|
||||||
|
color: "text-[color:var(--freight-brand)]",
|
||||||
|
stage: 4,
|
||||||
|
},
|
||||||
IN_TRANSIT: {
|
IN_TRANSIT: {
|
||||||
title: "In Transit",
|
title: "In Transit",
|
||||||
description: "Shipment is on the railway network.",
|
description: "Shipment is on the railway network.",
|
||||||
@@ -300,7 +310,7 @@ export const BOOKING_LIST_TABS = [
|
|||||||
{
|
{
|
||||||
key: "operations",
|
key: "operations",
|
||||||
label: "Operations",
|
label: "Operations",
|
||||||
statuses: ["PAID", "IN_TRANSIT", "ARRIVED", "ROAD_DISPATCH_PENDING"],
|
statuses: ["PAID", "TRUCK_ASSIGNED", "IN_TRANSIT", "ARRIVED", "ROAD_DISPATCH_PENDING"],
|
||||||
},
|
},
|
||||||
{ key: "completed", label: "Completed", statuses: ["COMPLETED"] },
|
{ key: "completed", label: "Completed", statuses: ["COMPLETED"] },
|
||||||
{ key: "closed", label: "Closed", statuses: ["REJECTED", "CANCELLED"] },
|
{ key: "closed", label: "Closed", statuses: ["REJECTED", "CANCELLED"] },
|
||||||
@@ -338,7 +348,7 @@ export const WORKFLOW_STAGES = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Operations",
|
label: "Operations",
|
||||||
statuses: ["PAID", "IN_TRANSIT", "ARRIVED"],
|
statuses: ["PAID", "TRUCK_ASSIGNED", "IN_TRANSIT", "ARRIVED"],
|
||||||
},
|
},
|
||||||
{ label: "Done", statuses: ["COMPLETED"] },
|
{ label: "Done", statuses: ["COMPLETED"] },
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -123,9 +123,14 @@ export function ReadonlyBookingView({
|
|||||||
const canAssignCustomerTruck =
|
const canAssignCustomerTruck =
|
||||||
booking.paymentStatus === "PAID" &&
|
booking.paymentStatus === "PAID" &&
|
||||||
usesCustomerTruck &&
|
usesCustomerTruck &&
|
||||||
["PAID", "IN_TRANSIT", "ARRIVED", "COMPLETED", "TRUCK_ASSIGNED"].includes(
|
(booking.tradeDirection === "IMPORT"
|
||||||
status,
|
? // Import self-haul: pickup trucks are assigned only after the train has
|
||||||
);
|
// arrived at the destination.
|
||||||
|
status === "ARRIVED"
|
||||||
|
: // Export / domestic self-haul: delivery trucks are assigned only before
|
||||||
|
// the cargo is loaded onto the train (PAID / TRUCK_ASSIGNED). Once loaded
|
||||||
|
// (IN_TRANSIT and beyond) assignment is closed.
|
||||||
|
["PAID", "TRUCK_ASSIGNED"].includes(status));
|
||||||
const showCountdown = canPay && !!booking.paymentDeadline;
|
const showCountdown = canPay && !!booking.paymentDeadline;
|
||||||
const isExpired = status === "EXPIRED";
|
const isExpired = status === "EXPIRED";
|
||||||
const isPendingConsolidation = status === "PENDING_CONSOLIDATION";
|
const isPendingConsolidation = status === "PENDING_CONSOLIDATION";
|
||||||
|
|||||||
@@ -97,13 +97,17 @@ export function CustomerTruckAssignmentCard({
|
|||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bulk bookings have no containers — the truck hauls loose tonnage (weighed on
|
||||||
|
// departure), so the container picker and its validation are skipped.
|
||||||
|
const isBulk = String(booking.freightType) === "BULK";
|
||||||
|
|
||||||
const addMutation = useMutation({
|
const addMutation = useMutation({
|
||||||
mutationFn: () => {
|
mutationFn: () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
truckPlateNumber: plateNumber.trim().toUpperCase(),
|
truckPlateNumber: plateNumber.trim().toUpperCase(),
|
||||||
driverName: driverName.trim(),
|
driverName: driverName.trim(),
|
||||||
truckType: truckType.trim(),
|
truckType: truckType.trim(),
|
||||||
containerNumbers: containers,
|
containerNumbers: isBulk ? [] : containers,
|
||||||
};
|
};
|
||||||
return editingId
|
return editingId
|
||||||
? customerTrucksService.update(booking.id, editingId, payload)
|
? customerTrucksService.update(booking.id, editingId, payload)
|
||||||
@@ -138,7 +142,7 @@ export function CustomerTruckAssignmentCard({
|
|||||||
setError("Plate number, driver name and truck type are required.");
|
setError("Plate number, driver name and truck type are required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (containers.length < 1 || containers.length > 2) {
|
if (!isBulk && (containers.length < 1 || containers.length > 2)) {
|
||||||
setError("Select 1 or 2 container numbers for this truck.");
|
setError("Select 1 or 2 container numbers for this truck.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -232,8 +236,9 @@ export function CustomerTruckAssignmentCard({
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Add-truck form — both directions assign the containers each truck carries. */}
|
{/* Add-truck form. Container bookings assign 1–2 containers per truck;
|
||||||
{availableContainers.length > 0 ? (
|
bulk bookings just register the truck (no container picker). */}
|
||||||
|
{isBulk || availableContainers.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<Divider label={editingId ? "Edit truck" : "Add a truck"} labelPosition="center" />
|
<Divider label={editingId ? "Edit truck" : "Add a truck"} labelPosition="center" />
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
|
||||||
@@ -256,18 +261,20 @@ export function CustomerTruckAssignmentCard({
|
|||||||
value={truckType || null}
|
value={truckType || null}
|
||||||
onChange={(value) => setTruckType(value ?? "")}
|
onChange={(value) => setTruckType(value ?? "")}
|
||||||
/>
|
/>
|
||||||
<MultiSelect
|
{!isBulk && (
|
||||||
label="Containers to load"
|
<MultiSelect
|
||||||
description="20ft: up to 2 per truck · 40ft: 1 per truck"
|
label="Containers to load"
|
||||||
required
|
description="20ft: up to 2 per truck · 40ft: 1 per truck"
|
||||||
placeholder="Select container numbers"
|
required
|
||||||
data={availableContainers}
|
placeholder="Select container numbers"
|
||||||
value={containers}
|
data={availableContainers}
|
||||||
onChange={setContainers}
|
value={containers}
|
||||||
maxValues={2}
|
onChange={setContainers}
|
||||||
searchable
|
maxValues={2}
|
||||||
nothingFoundMessage="No unassigned containers"
|
searchable
|
||||||
/>
|
nothingFoundMessage="No unassigned containers"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
{editingId && (
|
{editingId && (
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const PROGRESS_STAGES = [
|
|||||||
icon: Ship,
|
icon: Ship,
|
||||||
statuses: [
|
statuses: [
|
||||||
"PAID",
|
"PAID",
|
||||||
|
"TRUCK_ASSIGNED",
|
||||||
"PNR_GENERATED",
|
"PNR_GENERATED",
|
||||||
"PENDING_CONSOLIDATION",
|
"PENDING_CONSOLIDATION",
|
||||||
"CONSOLIDATED",
|
"CONSOLIDATED",
|
||||||
@@ -162,6 +163,7 @@ export const CONTRACT_PROGRESS_STAGES = [
|
|||||||
icon: Ship,
|
icon: Ship,
|
||||||
statuses: [
|
statuses: [
|
||||||
"PAID",
|
"PAID",
|
||||||
|
"TRUCK_ASSIGNED",
|
||||||
"PNR_GENERATED",
|
"PNR_GENERATED",
|
||||||
"PENDING_CONSOLIDATION",
|
"PENDING_CONSOLIDATION",
|
||||||
"CONSOLIDATED",
|
"CONSOLIDATED",
|
||||||
@@ -325,6 +327,11 @@ export const STATUS_MAP: Record<
|
|||||||
description: "Payment has been confirmed for this booking.",
|
description: "Payment has been confirmed for this booking.",
|
||||||
stage: 5,
|
stage: 5,
|
||||||
},
|
},
|
||||||
|
TRUCK_ASSIGNED: {
|
||||||
|
title: "Truck assigned",
|
||||||
|
description: "Customer truck assigned for self-haul; ready for loading.",
|
||||||
|
stage: 5,
|
||||||
|
},
|
||||||
IN_TRANSIT: {
|
IN_TRANSIT: {
|
||||||
title: "Cargo moving",
|
title: "Cargo moving",
|
||||||
description: "Your shipment is currently moving through the rail network.",
|
description: "Your shipment is currently moving through the rail network.",
|
||||||
|
|||||||
Reference in New Issue
Block a user