Files
edr-platform/apps/edr-freight-web/backoffice/src/services/last-mile.service.ts
Hagernesh 4e8d35a7fb Warehouse zone occupancy heatmap
Visualize live per-zone occupancy on the warehouse detail page.
2026-07-13 12:09:43 +00:00

134 lines
5.2 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;
containerNumber?: string | null;
distanceKm?: 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; containerNumber?: string | null }>,
) => api.post<LastMileRecord>(`${LM.BASE}/${id}/vehicles`, { vehicles }),
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`),
};