mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 23:35:42 +00:00
fix
This commit is contained in:
@@ -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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: "" };
|
||||||
|
|||||||
Reference in New Issue
Block a user