This commit is contained in:
natib21
2026-07-03 15:47:12 +00:00
parent f2f6c89c0e
commit c643d30624
4 changed files with 157 additions and 45 deletions

View File

@@ -48,13 +48,33 @@ export class FirstMileService {
private readonly history: FleetHistoryService, private readonly history: FleetHistoryService,
) { } ) { }
/** Resolve the driver currently assigned to a vehicle, for stamping mile /** Resolve a vehicle's driver + human labels, for stamping mile events onto
* events onto that driver's timeline. Best-effort — never throws. */ * the driver's timeline and naming the vehicle. Best-effort — never throws. */
private async resolveDriverId(vehicleId?: string | null): Promise<string | null> { private async vehicleInfo(
if (!vehicleId) return null; vehicleId?: string | null,
): Promise<{ driverId: string | null; plate: string | null; driverName: string | null }> {
if (!vehicleId) return { driverId: null, plate: null, driverName: null };
try { try {
const vehicle = await this.vehiclesService.findById(vehicleId); const v = await this.vehiclesService.findById(vehicleId);
return vehicle.assignedDriverId ?? null; return {
driverId: v.assignedDriverId ?? null,
plate: v.plateNumber ?? v.code ?? null,
driverName: v.assignedDriverName ?? null,
};
} catch {
return { driverId: null, plate: null, driverName: null };
}
}
/** Human booking reference for a first-mile record, for the history timeline. */
private async resolveBookingRef(record: FirstMile): Promise<string | null> {
const loaded = (record as FirstMile & { booking?: { reference?: string } })
.booking?.reference;
if (loaded) return loaded;
if (!record.bookingId) return null;
try {
const b = await this.bookingsRepository.findById(record.bookingId);
return (b as { reference?: string } | null)?.reference ?? null;
} catch { } catch {
return null; return null;
} }
@@ -225,13 +245,19 @@ export class FirstMileService {
if (dto.vehicleId) { if (dto.vehicleId) {
await this.vehiclesService.setAvailability(dto.vehicleId, VehicleAvailability.BUSY); await this.vehiclesService.setAvailability(dto.vehicleId, VehicleAvailability.BUSY);
const info = await this.vehicleInfo(dto.vehicleId);
await this.history.record({ await this.history.record({
eventType: FleetEventType.MILE_VEHICLE_ASSIGNED, eventType: FleetEventType.MILE_VEHICLE_ASSIGNED,
vehicleId: dto.vehicleId, vehicleId: dto.vehicleId,
firstMileId: record.id, firstMileId: record.id,
driverId: await this.resolveDriverId(dto.vehicleId), driverId: info.driverId,
label: record.status, label: record.status,
metadata: { mile: 'FIRST' }, metadata: {
mile: 'FIRST',
bookingRef: await this.resolveBookingRef(record),
vehiclePlate: info.plate,
driverName: info.driverName,
},
}); });
} }
@@ -302,23 +328,36 @@ export class FirstMileService {
await this.vehiclesService.releaseIfUnused([existing.vehicleId]); await this.vehiclesService.releaseIfUnused([existing.vehicleId]);
} }
// Audit the mile↔vehicle (re)assignment on both vehicle and driver lines. // Audit the mile↔vehicle (re)assignment on both vehicle and driver lines.
const bookingRef = await this.resolveBookingRef(existing);
if (existing.vehicleId) { if (existing.vehicleId) {
const info = await this.vehicleInfo(existing.vehicleId);
await this.history.record({ await this.history.record({
eventType: FleetEventType.MILE_VEHICLE_RELEASED, eventType: FleetEventType.MILE_VEHICLE_RELEASED,
vehicleId: existing.vehicleId, vehicleId: existing.vehicleId,
firstMileId: id, firstMileId: id,
driverId: await this.resolveDriverId(existing.vehicleId), driverId: info.driverId,
metadata: { mile: 'FIRST' }, metadata: {
mile: 'FIRST',
bookingRef,
vehiclePlate: info.plate,
driverName: info.driverName,
},
}); });
} }
if (dto.vehicleId) { if (dto.vehicleId) {
const info = await this.vehicleInfo(dto.vehicleId);
await this.history.record({ await this.history.record({
eventType: FleetEventType.MILE_VEHICLE_ASSIGNED, eventType: FleetEventType.MILE_VEHICLE_ASSIGNED,
vehicleId: dto.vehicleId, vehicleId: dto.vehicleId,
firstMileId: id, firstMileId: id,
driverId: await this.resolveDriverId(dto.vehicleId), driverId: info.driverId,
label: updated.status, label: updated.status,
metadata: { mile: 'FIRST' }, metadata: {
mile: 'FIRST',
bookingRef,
vehiclePlate: info.plate,
driverName: info.driverName,
},
}); });
} }
} }
@@ -330,14 +369,20 @@ export class FirstMileService {
if (dto.status !== undefined && dto.status !== existing.status) { if (dto.status !== undefined && dto.status !== existing.status) {
const vehicleId = updated.vehicleId ?? existing.vehicleId ?? null; const vehicleId = updated.vehicleId ?? existing.vehicleId ?? null;
const info = await this.vehicleInfo(vehicleId);
await this.history.record({ await this.history.record({
eventType: FleetEventType.MILE_STATUS_CHANGED, eventType: FleetEventType.MILE_STATUS_CHANGED,
firstMileId: id, firstMileId: id,
vehicleId, vehicleId,
driverId: await this.resolveDriverId(vehicleId), driverId: info.driverId,
fromValue: existing.status, fromValue: existing.status,
toValue: dto.status, toValue: dto.status,
metadata: { mile: 'FIRST' }, metadata: {
mile: 'FIRST',
bookingRef: await this.resolveBookingRef(existing),
vehiclePlate: info.plate,
driverName: info.driverName,
},
}); });
} }
@@ -359,14 +404,20 @@ export class FirstMileService {
if (status !== existing.status) { if (status !== existing.status) {
const vehicleId = updated.vehicleId ?? existing.vehicleId ?? null; const vehicleId = updated.vehicleId ?? existing.vehicleId ?? null;
const info = await this.vehicleInfo(vehicleId);
await this.history.record({ await this.history.record({
eventType: FleetEventType.MILE_STATUS_CHANGED, eventType: FleetEventType.MILE_STATUS_CHANGED,
firstMileId: id, firstMileId: id,
vehicleId, vehicleId,
driverId: await this.resolveDriverId(vehicleId), driverId: info.driverId,
fromValue: existing.status, fromValue: existing.status,
toValue: status, toValue: status,
metadata: { mile: 'FIRST' }, metadata: {
mile: 'FIRST',
bookingRef: await this.resolveBookingRef(existing),
vehiclePlate: info.plate,
driverName: info.driverName,
},
}); });
} }

View File

@@ -46,15 +46,21 @@ export class LastMileService {
private readonly history: FleetHistoryService, private readonly history: FleetHistoryService,
) {} ) {}
/** Resolve the driver currently assigned to a vehicle, for stamping mile /** Resolve a vehicle's driver + human labels, for stamping mile events onto
* events onto that driver's timeline. Best-effort — never throws. */ * the driver's timeline and naming the vehicle. Best-effort — never throws. */
private async resolveDriverId(vehicleId?: string | null): Promise<string | null> { private async vehicleInfo(
if (!vehicleId) return null; vehicleId?: string | null,
): Promise<{ driverId: string | null; plate: string | null; driverName: string | null }> {
if (!vehicleId) return { driverId: null, plate: null, driverName: null };
try { try {
const vehicle = await this.vehiclesService.findById(vehicleId); const v = await this.vehiclesService.findById(vehicleId);
return vehicle.assignedDriverId ?? null; return {
driverId: v.assignedDriverId ?? null,
plate: v.plateNumber ?? v.code ?? null,
driverName: v.assignedDriverName ?? null,
};
} catch { } catch {
return null; return { driverId: null, plate: null, driverName: null };
} }
} }
@@ -176,13 +182,19 @@ export class LastMileService {
}); });
if (dto.vehicleId) { if (dto.vehicleId) {
const info = await this.vehicleInfo(dto.vehicleId);
await this.history.record({ await this.history.record({
eventType: FleetEventType.MILE_VEHICLE_ASSIGNED, eventType: FleetEventType.MILE_VEHICLE_ASSIGNED,
vehicleId: dto.vehicleId, vehicleId: dto.vehicleId,
lastMileId: record.id, lastMileId: record.id,
driverId: await this.resolveDriverId(dto.vehicleId), driverId: info.driverId,
label: record.status, label: record.status,
metadata: { mile: 'LAST', bookingRef: await this.resolveBookingRef(record) }, metadata: {
mile: 'LAST',
bookingRef: await this.resolveBookingRef(record),
vehiclePlate: info.plate,
driverName: info.driverName,
},
}); });
} }
@@ -229,36 +241,54 @@ export class LastMileService {
const bookingRef = await this.resolveBookingRef(existing); const bookingRef = await this.resolveBookingRef(existing);
if (dto.vehicleId !== undefined && dto.vehicleId !== existing.vehicleId) { if (dto.vehicleId !== undefined && dto.vehicleId !== existing.vehicleId) {
if (existing.vehicleId) { if (existing.vehicleId) {
const info = await this.vehicleInfo(existing.vehicleId);
await this.history.record({ await this.history.record({
eventType: FleetEventType.MILE_VEHICLE_RELEASED, eventType: FleetEventType.MILE_VEHICLE_RELEASED,
vehicleId: existing.vehicleId, vehicleId: existing.vehicleId,
lastMileId: id, lastMileId: id,
driverId: await this.resolveDriverId(existing.vehicleId), driverId: info.driverId,
metadata: { mile: 'LAST', bookingRef }, metadata: {
mile: 'LAST',
bookingRef,
vehiclePlate: info.plate,
driverName: info.driverName,
},
}); });
} }
if (dto.vehicleId) { if (dto.vehicleId) {
const info = await this.vehicleInfo(dto.vehicleId);
await this.history.record({ await this.history.record({
eventType: FleetEventType.MILE_VEHICLE_ASSIGNED, eventType: FleetEventType.MILE_VEHICLE_ASSIGNED,
vehicleId: dto.vehicleId, vehicleId: dto.vehicleId,
lastMileId: id, lastMileId: id,
driverId: await this.resolveDriverId(dto.vehicleId), driverId: info.driverId,
label: updated.status, label: updated.status,
metadata: { mile: 'LAST', bookingRef }, metadata: {
mile: 'LAST',
bookingRef,
vehiclePlate: info.plate,
driverName: info.driverName,
},
}); });
} }
} }
if (dto.status !== undefined && dto.status !== existing.status) { if (dto.status !== undefined && dto.status !== existing.status) {
const vehicleId = updated.vehicleId ?? existing.vehicleId ?? null; const vehicleId = updated.vehicleId ?? existing.vehicleId ?? null;
const info = await this.vehicleInfo(vehicleId);
await this.history.record({ await this.history.record({
eventType: FleetEventType.MILE_STATUS_CHANGED, eventType: FleetEventType.MILE_STATUS_CHANGED,
lastMileId: id, lastMileId: id,
vehicleId, vehicleId,
driverId: await this.resolveDriverId(vehicleId), driverId: info.driverId,
fromValue: existing.status, fromValue: existing.status,
toValue: dto.status, toValue: dto.status,
metadata: { mile: 'LAST', bookingRef }, metadata: {
mile: 'LAST',
bookingRef,
vehiclePlate: info.plate,
driverName: info.driverName,
},
}); });
} }

View File

@@ -51,6 +51,10 @@ export class VehiclesService {
vehicleId: saved.id, vehicleId: saved.id,
driverId: saved.assignedDriverId, driverId: saved.assignedDriverId,
label: saved.assignedDriverName ?? null, label: saved.assignedDriverName ?? null,
metadata: {
vehiclePlate: saved.plateNumber ?? saved.code ?? null,
driverName: saved.assignedDriverName ?? null,
},
}); });
} }
@@ -133,12 +137,14 @@ export class VehiclesService {
dto.assignedDriverId !== undefined && dto.assignedDriverId !== undefined &&
dto.assignedDriverId !== prev.assignedDriverId dto.assignedDriverId !== prev.assignedDriverId
) { ) {
const vehiclePlate = saved.plateNumber ?? saved.code ?? null;
if (prev.assignedDriverId) { if (prev.assignedDriverId) {
await this.history.record({ await this.history.record({
eventType: FleetEventType.DRIVER_UNASSIGNED, eventType: FleetEventType.DRIVER_UNASSIGNED,
vehicleId: id, vehicleId: id,
driverId: prev.assignedDriverId, driverId: prev.assignedDriverId,
label: prev.assignedDriverName ?? null, label: prev.assignedDriverName ?? null,
metadata: { vehiclePlate, driverName: prev.assignedDriverName ?? null },
}); });
} }
if (saved.assignedDriverId) { if (saved.assignedDriverId) {
@@ -147,6 +153,7 @@ export class VehiclesService {
vehicleId: id, vehicleId: id,
driverId: saved.assignedDriverId, driverId: saved.assignedDriverId,
label: saved.assignedDriverName ?? null, label: saved.assignedDriverName ?? null,
metadata: { vehiclePlate, driverName: saved.assignedDriverName ?? null },
}); });
} }
} }

View File

@@ -41,11 +41,24 @@ const mileLabel = (e: FleetHistoryEvent) =>
const arrow = (from?: string | null, to?: string | null) => const arrow = (from?: string | null, to?: string | null) =>
`${from ?? "—"}${to ?? "—"}`; `${from ?? "—"}${to ?? "—"}`;
const metaStr = (e: FleetHistoryEvent, key: string) => {
const v = e.metadata?.[key];
return typeof v === "string" && v ? v : null;
};
function describe(e: FleetHistoryEvent, entity: "driver" | "vehicle") { function describe(e: FleetHistoryEvent, entity: "driver" | "vehicle") {
const bookingRef = const vehiclePlate = metaStr(e, "vehiclePlate");
typeof e.metadata?.bookingRef === "string" ? e.metadata.bookingRef : null; const driverName = metaStr(e, "driverName") ?? (e.label || null);
const withBooking = (rest?: string) => const bookingRef = metaStr(e, "bookingRef");
[bookingRef ? `Booking ${bookingRef}` : "", rest ?? ""]
// Compose the detail line with whatever the current view doesn't already
// know: on a driver's timeline show which vehicle; always show the booking.
const detail = (extra?: string) =>
[
entity === "driver" && vehiclePlate ? `Vehicle ${vehiclePlate}` : "",
bookingRef ? `Booking ${bookingRef}` : "",
extra ?? "",
]
.filter(Boolean) .filter(Boolean)
.join(" · "); .join(" · ");
@@ -65,20 +78,31 @@ function describe(e: FleetHistoryEvent, entity: "driver" | "vehicle") {
case "DRIVER_ASSIGNED": case "DRIVER_ASSIGNED":
return { return {
icon: <UserPlus size={14} />, icon: <UserPlus size={14} />,
title: title: entity === "vehicle" ? "Driver assigned" : "Assigned to vehicle",
text:
entity === "vehicle" entity === "vehicle"
? `Driver assigned${e.label ? `: ${e.label}` : ""}` ? driverName
: "Assigned to a vehicle", ? `Driver ${driverName}`
text: "", : ""
: vehiclePlate
? `Vehicle ${vehiclePlate}`
: "",
}; };
case "DRIVER_UNASSIGNED": case "DRIVER_UNASSIGNED":
return { return {
icon: <UserMinus size={14} />, icon: <UserMinus size={14} />,
title: title:
entity === "vehicle" entity === "vehicle"
? `Driver unassigned${e.label ? `: ${e.label}` : ""}` ? "Driver unassigned"
: "Unassigned from a vehicle", : "Unassigned from vehicle",
text: "", text:
entity === "vehicle"
? driverName
? `Driver ${driverName}`
: ""
: vehiclePlate
? `Vehicle ${vehiclePlate}`
: "",
}; };
case "VEHICLE_STATUS_CHANGED": case "VEHICLE_STATUS_CHANGED":
return { return {
@@ -96,19 +120,19 @@ function describe(e: FleetHistoryEvent, entity: "driver" | "vehicle") {
return { return {
icon: <Route size={14} />, icon: <Route size={14} />,
title: `${mileLabel(e)}: vehicle assigned`, title: `${mileLabel(e)}: vehicle assigned`,
text: withBooking(e.label ? `Status: ${e.label}` : ""), text: detail(e.label ? `Status: ${e.label}` : ""),
}; };
case "MILE_VEHICLE_RELEASED": case "MILE_VEHICLE_RELEASED":
return { return {
icon: <Route size={14} />, icon: <Route size={14} />,
title: `${mileLabel(e)}: vehicle released`, title: `${mileLabel(e)}: vehicle released`,
text: withBooking(), text: detail(),
}; };
case "MILE_STATUS_CHANGED": case "MILE_STATUS_CHANGED":
return { return {
icon: <Route size={14} />, icon: <Route size={14} />,
title: `${mileLabel(e)} status`, title: `${mileLabel(e)} status`,
text: withBooking(arrow(e.fromValue, e.toValue)), text: detail(arrow(e.fromValue, e.toValue)),
}; };
default: default:
return { icon: <CircleDot size={14} />, title: e.eventType, text: "" }; return { icon: <CircleDot size={14} />, title: e.eventType, text: "" };