This commit is contained in:
natib21
2026-07-03 16:21:07 +00:00
parent ed388042af
commit 8661586301
4 changed files with 75 additions and 8 deletions

View File

@@ -1,5 +1,5 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { FindOptionsWhere, In } from 'typeorm';
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { FindOptionsWhere, In, IsNull, Not } from 'typeorm';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { VehicleAvailability } from '../vehicles/entities/vehicle.entity';
@@ -66,6 +66,19 @@ export class FirstMileService {
}
}
/** A leg counts as having a vehicle if it has a direct assignment or at least
* one container allocation carrying a vehicle. Gates the IN_TRANSIT move. */
private async hasAssignedVehicle(
recordId: string,
directVehicleId?: string | null,
): Promise<boolean> {
if (directVehicleId) return true;
const count = await this.dataSource.manager.count(FirstMileContainerAllocation, {
where: { firstMileId: recordId, vehicleId: Not(IsNull()) },
});
return count > 0;
}
/** 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 } })
@@ -297,6 +310,18 @@ export class FirstMileService {
async update(id: string, dto: UpdateFirstMileDto): Promise<FirstMile> {
const existing = await this.findById(id);
// A leg can only go IN_TRANSIT once a vehicle is assigned (allowing a vehicle
// assigned in this same request).
if (dto.status === 'IN_TRANSIT' && existing.status !== 'IN_TRANSIT') {
const vehicleId =
dto.vehicleId !== undefined ? dto.vehicleId : existing.vehicleId;
if (!(await this.hasAssignedVehicle(id, vehicleId))) {
throw new BadRequestException(
'Assign a vehicle before marking this first-mile leg in transit',
);
}
}
const dtoAny = dto as any;
const updated = await this.firstMileRepository.update(id, {
...(dto.bookingId !== undefined ? { bookingId: dto.bookingId } : {}),
@@ -396,6 +421,15 @@ export class FirstMileService {
async updateStatus(id: string, status: FirstMileStatus): Promise<FirstMile> {
const existing = await this.findById(id);
if (status === 'IN_TRANSIT' && existing.status !== 'IN_TRANSIT') {
if (!(await this.hasAssignedVehicle(id, existing.vehicleId))) {
throw new BadRequestException(
'Assign a vehicle before marking this first-mile leg in transit',
);
}
}
const updated = await this.firstMileRepository.update(id, { status });
if (!updated) {

View File

@@ -1,5 +1,5 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { DataSource, FindOptionsWhere, In } from 'typeorm';
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { DataSource, FindOptionsWhere, In, IsNull, Not } from 'typeorm';
import { BookingsRepository } from '../bookings/bookings.repository';
import { DriversService } from '../drivers/drivers.service';
@@ -65,6 +65,19 @@ export class LastMileService {
}
}
/** A leg counts as having a vehicle if it has a direct assignment or at least
* one container allocation carrying a vehicle. Gates the IN_TRANSIT move. */
private async hasAssignedVehicle(
recordId: string,
directVehicleId?: string | null,
): Promise<boolean> {
if (directVehicleId) return true;
const count = await this.dataSource.manager.count(LastMileContainerAllocation, {
where: { lastMileId: recordId, vehicleId: Not(IsNull()) },
});
return count > 0;
}
/** Human booking reference for a last-mile record, for the history timeline.
* Uses the already-loaded relation when present, else looks it up. */
private async resolveBookingRef(
@@ -218,6 +231,18 @@ export class LastMileService {
async update(id: string, dto: UpdateLastMileDto): Promise<LastMile> {
const existing = await this.findById(id);
// A leg can only go IN_TRANSIT once a vehicle is assigned (allowing a vehicle
// assigned in this same request).
if (dto.status === 'IN_TRANSIT' && existing.status !== 'IN_TRANSIT') {
const vehicleId =
dto.vehicleId !== undefined ? dto.vehicleId : existing.vehicleId;
if (!(await this.hasAssignedVehicle(id, vehicleId))) {
throw new BadRequestException(
'Assign a vehicle before marking this last-mile leg in transit',
);
}
}
const dtoAny = dto as any;
const updated = await this.lastMileRepository.update(id, {
...(dto.bookingId !== undefined ? { bookingId: dto.bookingId } : {}),

View File

@@ -879,10 +879,14 @@ const FirstMilePage = () => {
<Menu.Dropdown>
<Menu.Item
leftSection={<ArrowRight size={15} />}
disabled={!nextStatus}
disabled={!nextStatus || (nextStatus === "IN_TRANSIT" && !assigned)}
onClick={() => handleAdvanceStatus(row.original)}
>
{nextStatus ? `Mark ${STATUS_META[nextStatus].label}` : STATUS_META[row.original.status].label}
{nextStatus === "IN_TRANSIT" && !assigned
? "Assign a vehicle first"
: nextStatus
? `Mark ${STATUS_META[nextStatus].label}`
: STATUS_META[row.original.status].label}
</Menu.Item>
<Menu.Divider />
<Menu.Item

View File

@@ -967,10 +967,14 @@ const LastMilePage = () => {
<Menu.Dropdown>
<Menu.Item
leftSection={<ArrowRight size={15} />}
disabled={!nextStatus}
disabled={!nextStatus || (nextStatus === "IN_TRANSIT" && !assigned)}
onClick={() => handleAdvanceStatus(row.original)}
>
{nextStatus ? `Mark ${STATUS_META[nextStatus].label}` : STATUS_META[row.original.status].label}
{nextStatus === "IN_TRANSIT" && !assigned
? "Assign a vehicle first"
: nextStatus
? `Mark ${STATUS_META[nextStatus].label}`
: STATUS_META[row.original.status].label}
</Menu.Item>
<Menu.Divider />
<Menu.Item