mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
Enable per-truck editing of warehouse gate arrival/departure times (arrivedAt/departedAt) via modal on Import Trucks page. Accessible via row action menu for EDR-haulage trucks. Includes backend endpoint POST /last-mile/:id/warehouse-gate-times and frontend modal with DateTimePicker inputs. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
168 lines
6.7 KiB
TypeScript
168 lines
6.7 KiB
TypeScript
import { api } from '../auth/http';
|
|
import { URL_CONSTANTS } from '@/constants/URLS';
|
|
import type { FeePreview } from '@/types/warehouse';
|
|
|
|
export const LAST_MILE_STATUSES = [
|
|
'PAYMENT_PENDING',
|
|
'READY_TO_TRANSIT',
|
|
'IN_TRANSIT',
|
|
'DELIVERED',
|
|
] as const;
|
|
export type LastMileApiStatus = (typeof LAST_MILE_STATUSES)[number];
|
|
|
|
export interface LastMileBooking {
|
|
id: string;
|
|
reference: string;
|
|
lastMileDeliveryAddress?: string | null;
|
|
cargoFreeText?: string | null;
|
|
cargoTotalWeightVgm: number;
|
|
totalAmount: number;
|
|
paymentCurrency?: string | null;
|
|
scheduledDate?: string | null;
|
|
company?: { id: string; name?: string; phone?: string | null; contactPersonName?: string | null; contactPersonPhone?: string | null } | null;
|
|
serviceType?: { id: string; name?: string; label?: string } | null;
|
|
originYard?: { id: string; name?: string; label?: string } | null;
|
|
destinationYard?: { id: string; name?: string; label?: string } | null;
|
|
cargoType?: { id: string; name?: string; label?: string; cargoTypeName?: string } | null;
|
|
/** Container lines — total container count drives how many trucks are needed. */
|
|
bookingContainers?: Array<{
|
|
id: string;
|
|
quantity: number;
|
|
containerNumber?: string | null;
|
|
containerSize?: string | null;
|
|
containerType?: { id: string; name?: string; label?: string; code?: string } | null;
|
|
/** Physical containers under this line — their real numbers (line-level
|
|
* containerNumber is often a TBD placeholder). */
|
|
units?: Array<{ id: string; containerNumber?: string | null; sortOrder?: number }>;
|
|
}>;
|
|
}
|
|
|
|
export interface LastMileVehicle {
|
|
id: string;
|
|
plateNumber: string;
|
|
manufacturer: string;
|
|
model: string;
|
|
vehicleType?: string | null;
|
|
code?: string | null;
|
|
powerPlateNo?: string | null;
|
|
trailerPlateNo?: string | null;
|
|
assignedDriverId?: string | null;
|
|
assignedDriverName?: string | null;
|
|
pricePerKm?: number | string | null;
|
|
currency?: string | null;
|
|
}
|
|
|
|
export interface LastMileRecord {
|
|
id: string;
|
|
bookingId: string;
|
|
status: LastMileApiStatus;
|
|
advancedPayment: number;
|
|
remainingPayment: number;
|
|
estimatedKm?: number | null;
|
|
exactKm?: number | null;
|
|
vehicleId?: string | null;
|
|
booking?: LastMileBooking | null;
|
|
vehicle?: LastMileVehicle | null;
|
|
/** Full set of vehicles serving this delivery (multi-truck). */
|
|
vehicleAssignments?: Array<{
|
|
id: string;
|
|
vehicleId: string;
|
|
/** @deprecated Legacy single container — `containers` is authoritative. */
|
|
containerNumber?: string | null;
|
|
/** Containers riding this truck: one 40ft, or up to two 20ft. */
|
|
containers?: Array<{ id: string; containerNumber: string }>;
|
|
distanceKm?: number | null;
|
|
/** Per-truck arrival / exit, stamped by the warehouse weighing steps. */
|
|
arrivedAt?: string | null;
|
|
departedAt?: string | null;
|
|
/** This truck's own detention window (destination arrival → released). */
|
|
destinationArrivedAt?: string | null;
|
|
returnedAt?: string | null;
|
|
grossWeightTons?: number | null;
|
|
netWeightTons?: number | null;
|
|
vehicle?: LastMileVehicle | null;
|
|
}>;
|
|
/** Present only when an invoice has actually been generated (not on distance). */
|
|
invoice?: { id: string; number: string; status: string } | null;
|
|
/** Truck-detention clock: vehicle arrival + delivery/return times. */
|
|
arrivedAt?: string | null;
|
|
deliveredAt?: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface LastMileListResponse {
|
|
data: LastMileRecord[];
|
|
meta: { total: number; page: number; pageSize: number; totalPages: number };
|
|
}
|
|
|
|
const LM = URL_CONSTANTS.LAST_MILE;
|
|
|
|
export const lastMileService = {
|
|
list: (pageSize = 1000) =>
|
|
api.get<LastMileListResponse>(`${LM.BASE}?pageSize=${pageSize}`),
|
|
getById: (id: string) => api.get<LastMileRecord>(LM.BY_ID(id)),
|
|
update: (id: string, data: { status?: LastMileApiStatus; vehicleId?: string | null; estimatedKm?: number | null; exactKm?: number | null; paid?: boolean; arrivedAt?: string | null; deliveredAt?: string | null }) =>
|
|
api.patch<LastMileRecord>(LM.BY_ID(id), data),
|
|
accept: (bookingReference: string) =>
|
|
api.post<LastMileRecord>(LM.ACCEPT(encodeURIComponent(bookingReference))),
|
|
remove: (id: string) =>
|
|
api.delete<void>(LM.BY_ID(id)),
|
|
setVehicles: (
|
|
id: string,
|
|
vehicles: Array<{ vehicleId: string; containerNumbers?: string[] }>,
|
|
) => api.post<LastMileRecord>(`${LM.BASE}/${id}/vehicles`, { vehicles }),
|
|
/** Bulk drawdown: tonnage still to be hauled on this booking. */
|
|
remainingTons: (bookingId: string) =>
|
|
api.get<{ totalTons: number; hauledTons: number; remainingTons: number; complete: boolean }>(
|
|
`${LM.BASE}/booking/${bookingId}/remaining-tons`,
|
|
),
|
|
setDistances: (
|
|
id: string,
|
|
distances: Array<{ vehicleId: string; distanceKm: number }>,
|
|
remainingPayment?: number,
|
|
) => api.post<LastMileRecord>(`${LM.BASE}/${id}/distances`, { distances, remainingPayment }),
|
|
generateInvoice: (id: string) =>
|
|
api.post<{ id: string; invoiceNumber?: string } | null>(`${LM.BASE}/${id}/invoice`),
|
|
/** Record proof of delivery (recipient signature + photos) and complete the leg. */
|
|
recordProofOfDelivery: (
|
|
id: string,
|
|
payload: { recipientName: string; notes?: string; signature?: Blob | null; photos?: File[] },
|
|
) => {
|
|
const form = new FormData();
|
|
form.append("recipientName", payload.recipientName);
|
|
if (payload.notes) form.append("notes", payload.notes);
|
|
if (payload.signature) form.append("signature", payload.signature, "signature.png");
|
|
(payload.photos ?? []).forEach((photo) => form.append("photos", photo));
|
|
return api.post<LastMileRecord>(LM.PROOF_OF_DELIVERY(id), form, {
|
|
headers: { "Content-Type": "multipart/form-data" },
|
|
});
|
|
},
|
|
/** Generate a truck-detention invoice (per truck per day after the grace window). */
|
|
generateTruckDetentionInvoice: (id: string) =>
|
|
api.post<{ id: string; invoiceNumber?: string } | null>(
|
|
`${LM.BASE}/${id}/generate-truck-detention-invoice`,
|
|
),
|
|
/** Preview the truck-detention charge for a last-mile leg. */
|
|
truckDetentionPreview: (id: string) =>
|
|
api.get<FeePreview>(`${LM.BASE}/${id}/truck-detention-preview`),
|
|
/** Per-truck detention windows — each truck has its own clock. */
|
|
setDetentionTimes: (
|
|
id: string,
|
|
trucks: Array<{
|
|
vehicleId: string;
|
|
destinationArrivedAt?: string | null;
|
|
returnedAt?: string | null;
|
|
}>,
|
|
) => api.post<LastMileRecord>(`${LM.BASE}/${id}/detention-times`, { trucks }),
|
|
/** Set warehouse gate arrival/departure times for each truck. */
|
|
setWarehouseGateTimes: (
|
|
id: string,
|
|
trucks: Array<{
|
|
vehicleId: string;
|
|
arrivedAt?: string | null;
|
|
departedAt?: string | null;
|
|
}>,
|
|
) => api.post<LastMileRecord>(`${LM.BASE}/${id}/warehouse-gate-times`, { trucks }),
|
|
};
|