mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
Merge pull request #253 from Tria-plc/freight/feature/vehicle_2
Freight/feature/vehicle 2
This commit is contained in:
@@ -55,6 +55,7 @@ export class CreateFirstMileDto {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => (value === '' ? undefined : value))
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
vehicleId?: string | null;
|
vehicleId?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ export class FirstMileController {
|
|||||||
return this.firstMileService.findById(id);
|
return this.firstMileService.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('accept/:reference')
|
||||||
|
@TrainSchedulingManage()
|
||||||
|
@ApiOperation({ summary: 'Accept a paid booking and create a first-mile leg' })
|
||||||
|
acceptBooking(@Param('reference') reference: string) {
|
||||||
|
return this.firstMileService.acceptBooking(reference);
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Create a first-mile leg' })
|
@ApiOperation({ summary: 'Create a first-mile leg' })
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { BookingsModule } from '../bookings/bookings.module';
|
||||||
import { FirstMile } from './entities/first-mile.entity';
|
import { FirstMile } from './entities/first-mile.entity';
|
||||||
import { FirstMileController } from './first-mile.controller';
|
import { FirstMileController } from './first-mile.controller';
|
||||||
import { FirstMileRepository } from './first-mile.repository';
|
import { FirstMileRepository } from './first-mile.repository';
|
||||||
import { FirstMileService } from './first-mile.service';
|
import { FirstMileService } from './first-mile.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([FirstMile])],
|
imports: [TypeOrmModule.forFeature([FirstMile]), BookingsModule],
|
||||||
controllers: [FirstMileController],
|
controllers: [FirstMileController],
|
||||||
providers: [FirstMileRepository, FirstMileService],
|
providers: [FirstMileRepository, FirstMileService],
|
||||||
exports: [FirstMileRepository, FirstMileService],
|
exports: [FirstMileRepository, FirstMileService],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { FindOptionsWhere } from 'typeorm';
|
import { FindOptionsWhere } from 'typeorm';
|
||||||
|
|
||||||
|
import { BookingsRepository } from '../bookings/bookings.repository';
|
||||||
import { CreateFirstMileDto } from './dto/create-first-mile.dto';
|
import { CreateFirstMileDto } from './dto/create-first-mile.dto';
|
||||||
import { UpdateFirstMileDto } from './dto/update-first-mile.dto';
|
import { UpdateFirstMileDto } from './dto/update-first-mile.dto';
|
||||||
import { FirstMile, FirstMileStatus } from './entities/first-mile.entity';
|
import { FirstMile, FirstMileStatus } from './entities/first-mile.entity';
|
||||||
@@ -25,7 +26,32 @@ const SORTABLE_FIELDS: (keyof FirstMile)[] = [
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class FirstMileService {
|
export class FirstMileService {
|
||||||
constructor(private readonly firstMileRepository: FirstMileRepository) {}
|
constructor(
|
||||||
|
private readonly firstMileRepository: FirstMileRepository,
|
||||||
|
private readonly bookingsRepository: BookingsRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a booking by its human-readable reference and confirm it has been
|
||||||
|
* paid before any first-mile work proceeds. Throws if the reference is
|
||||||
|
* unknown or the booking has not reached PAID status.
|
||||||
|
*/
|
||||||
|
async acceptBooking(bookingReference: string): Promise<FirstMile | null> {
|
||||||
|
const booking = await this.bookingsRepository.findByReference(bookingReference);
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (booking.paymentStatus !== 'PAID') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.create({
|
||||||
|
bookingId: booking.id,
|
||||||
|
advancedPayment: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async findAll(filter: FirstMileListFilter = {}): Promise<{
|
async findAll(filter: FirstMileListFilter = {}): Promise<{
|
||||||
data: FirstMile[];
|
data: FirstMile[];
|
||||||
@@ -45,7 +71,10 @@ export class FirstMileService {
|
|||||||
|
|
||||||
const [data, total] = await this.firstMileRepository.findAndCount({
|
const [data, total] = await this.firstMileRepository.findAndCount({
|
||||||
where,
|
where,
|
||||||
relations: { booking: true, vehicle: true },
|
relations: {
|
||||||
|
booking: { company: true, serviceType: true, originYard: true, destinationYard: true, cargoType: true },
|
||||||
|
vehicle: true,
|
||||||
|
},
|
||||||
order: { [sortBy]: sortOrder },
|
order: { [sortBy]: sortOrder },
|
||||||
skip: (page - 1) * pageSize,
|
skip: (page - 1) * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
@@ -64,7 +93,10 @@ export class FirstMileService {
|
|||||||
|
|
||||||
async findById(id: string): Promise<FirstMile> {
|
async findById(id: string): Promise<FirstMile> {
|
||||||
const record = await this.firstMileRepository.findById(id, {
|
const record = await this.firstMileRepository.findById(id, {
|
||||||
relations: { booking: true, vehicle: true },
|
relations: {
|
||||||
|
booking: { company: true, serviceType: true, originYard: true, destinationYard: true, cargoType: true },
|
||||||
|
vehicle: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export class CreateLastMileDto {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => (value === '' ? undefined : value))
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
vehicleId?: string | null;
|
vehicleId?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ export class LastMileController {
|
|||||||
return this.lastMileService.findById(id);
|
return this.lastMileService.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('accept/:reference')
|
||||||
|
@TrainSchedulingManage()
|
||||||
|
@ApiOperation({ summary: 'Accept a paid booking and create a last-mile leg' })
|
||||||
|
acceptBooking(@Param('reference') reference: string) {
|
||||||
|
return this.lastMileService.acceptBooking(reference);
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Create a last-mile leg' })
|
@ApiOperation({ summary: 'Create a last-mile leg' })
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { BookingsModule } from '../bookings/bookings.module';
|
||||||
import { LastMile } from './entities/last-mile.entity';
|
import { LastMile } from './entities/last-mile.entity';
|
||||||
import { LastMileController } from './last-mile.controller';
|
import { LastMileController } from './last-mile.controller';
|
||||||
import { LastMileRepository } from './last-mile.repository';
|
import { LastMileRepository } from './last-mile.repository';
|
||||||
import { LastMileService } from './last-mile.service';
|
import { LastMileService } from './last-mile.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([LastMile])],
|
imports: [TypeOrmModule.forFeature([LastMile]), BookingsModule],
|
||||||
controllers: [LastMileController],
|
controllers: [LastMileController],
|
||||||
providers: [LastMileRepository, LastMileService],
|
providers: [LastMileRepository, LastMileService],
|
||||||
exports: [LastMileRepository, LastMileService],
|
exports: [LastMileRepository, LastMileService],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { FindOptionsWhere } from 'typeorm';
|
import { FindOptionsWhere } from 'typeorm';
|
||||||
|
|
||||||
|
import { BookingsRepository } from '../bookings/bookings.repository';
|
||||||
import { CreateLastMileDto } from './dto/create-last-mile.dto';
|
import { CreateLastMileDto } from './dto/create-last-mile.dto';
|
||||||
import { UpdateLastMileDto } from './dto/update-last-mile.dto';
|
import { UpdateLastMileDto } from './dto/update-last-mile.dto';
|
||||||
import { LastMile, LastMileStatus } from './entities/last-mile.entity';
|
import { LastMile, LastMileStatus } from './entities/last-mile.entity';
|
||||||
@@ -25,7 +26,29 @@ const SORTABLE_FIELDS: (keyof LastMile)[] = [
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LastMileService {
|
export class LastMileService {
|
||||||
constructor(private readonly lastMileRepository: LastMileRepository) {}
|
constructor(
|
||||||
|
private readonly lastMileRepository: LastMileRepository,
|
||||||
|
private readonly bookingsRepository: BookingsRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async acceptBooking(bookingReference: string): Promise<LastMile> {
|
||||||
|
const booking = await this.bookingsRepository.findByReference(bookingReference);
|
||||||
|
|
||||||
|
if (!booking) {
|
||||||
|
throw new NotFoundException(`Booking ${bookingReference} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (booking.paymentStatus !== 'PAID') {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Booking ${bookingReference} is not paid (payment status: ${booking.paymentStatus})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.create({
|
||||||
|
bookingId: booking.id,
|
||||||
|
advancedPayment: booking.totalAmount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async findAll(filter: LastMileListFilter = {}): Promise<{
|
async findAll(filter: LastMileListFilter = {}): Promise<{
|
||||||
data: LastMile[];
|
data: LastMile[];
|
||||||
@@ -45,7 +68,10 @@ export class LastMileService {
|
|||||||
|
|
||||||
const [data, total] = await this.lastMileRepository.findAndCount({
|
const [data, total] = await this.lastMileRepository.findAndCount({
|
||||||
where,
|
where,
|
||||||
relations: { booking: true, vehicle: true },
|
relations: {
|
||||||
|
booking: { company: true, serviceType: true, originYard: true, destinationYard: true, cargoType: true },
|
||||||
|
vehicle: true,
|
||||||
|
},
|
||||||
order: { [sortBy]: sortOrder },
|
order: { [sortBy]: sortOrder },
|
||||||
skip: (page - 1) * pageSize,
|
skip: (page - 1) * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
@@ -64,7 +90,10 @@ export class LastMileService {
|
|||||||
|
|
||||||
async findById(id: string): Promise<LastMile> {
|
async findById(id: string): Promise<LastMile> {
|
||||||
const record = await this.lastMileRepository.findById(id, {
|
const record = await this.lastMileRepository.findById(id, {
|
||||||
relations: { booking: true, vehicle: true },
|
relations: {
|
||||||
|
booking: { company: true, serviceType: true, originYard: true, destinationYard: true, cargoType: true },
|
||||||
|
vehicle: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!record) {
|
if (!record) {
|
||||||
|
|||||||
@@ -69,6 +69,18 @@ export const QUERY_KEYS = {
|
|||||||
list: (resource: FleetResourceSlug | string) => ["fleet", "list", resource] as const,
|
list: (resource: FleetResourceSlug | string) => ["fleet", "list", resource] as const,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
FIRST_MILE: {
|
||||||
|
ROOT: ["first-mile"] as const,
|
||||||
|
list: (filter?: Record<string, unknown>) => ["first-mile", "list", filter ?? {}] as const,
|
||||||
|
byId: (id: string) => ["first-mile", "detail", id] as const,
|
||||||
|
},
|
||||||
|
|
||||||
|
LAST_MILE: {
|
||||||
|
ROOT: ["last-mile"] as const,
|
||||||
|
list: (filter?: Record<string, unknown>) => ["last-mile", "list", filter ?? {}] as const,
|
||||||
|
byId: (id: string) => ["last-mile", "detail", id] as const,
|
||||||
|
},
|
||||||
|
|
||||||
RULE_ENGINE: {
|
RULE_ENGINE: {
|
||||||
ROOT: ["rule-engine"] as const,
|
ROOT: ["rule-engine"] as const,
|
||||||
list: (resource: RuleEngineResourceSlug | string, params?: RuleEngineListParams) =>
|
list: (resource: RuleEngineResourceSlug | string, params?: RuleEngineListParams) =>
|
||||||
|
|||||||
@@ -365,6 +365,18 @@ export const URL_CONSTANTS = {
|
|||||||
BY_ID: (id: string) => `/vehicles/${id}`,
|
BY_ID: (id: string) => `/vehicles/${id}`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
FIRST_MILE: {
|
||||||
|
BASE: '/first-mile',
|
||||||
|
BY_ID: (id: string) => `/first-mile/${id}`,
|
||||||
|
ACCEPT: (reference: string) => `/first-mile/accept/${reference}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
LAST_MILE: {
|
||||||
|
BASE: '/last-mile',
|
||||||
|
BY_ID: (id: string) => `/last-mile/${id}`,
|
||||||
|
ACCEPT: (reference: string) => `/last-mile/accept/${reference}`,
|
||||||
|
},
|
||||||
|
|
||||||
DRIVERS: {
|
DRIVERS: {
|
||||||
BASE: '/drivers',
|
BASE: '/drivers',
|
||||||
BY_ID: (id: string) => `/drivers/${id}`,
|
BY_ID: (id: string) => `/drivers/${id}`,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
|||||||
|
import { api } from '../auth/http';
|
||||||
|
import { URL_CONSTANTS } from '@/constants/URLS';
|
||||||
|
|
||||||
|
export const FIRST_MILE_STATUSES = [
|
||||||
|
'PAYMENT_PENDING',
|
||||||
|
'READY_TO_TRANSIT',
|
||||||
|
'IN_TRANSIT',
|
||||||
|
'RECEIVED_TO_PORT',
|
||||||
|
] as const;
|
||||||
|
export type FirstMileApiStatus = (typeof FIRST_MILE_STATUSES)[number];
|
||||||
|
|
||||||
|
export interface FirstMileBooking {
|
||||||
|
id: string;
|
||||||
|
reference: string;
|
||||||
|
firstMilePickupAddress?: string | null;
|
||||||
|
cargoFreeText?: string | null;
|
||||||
|
cargoTotalWeightVgm: number;
|
||||||
|
totalAmount: number;
|
||||||
|
scheduledDate?: string | null;
|
||||||
|
company?: { id: string; name?: string; phone?: string | null; contactPersonName?: string | null; contactPersonPhone?: string | null } | null;
|
||||||
|
serviceType?: { id: string; name?: string } | null;
|
||||||
|
originYard?: { id: string; name?: string } | null;
|
||||||
|
destinationYard?: { id: string; name?: string } | null;
|
||||||
|
cargoType?: { id: string; name?: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirstMileVehicle {
|
||||||
|
id: string;
|
||||||
|
plateNumber: string;
|
||||||
|
manufacturer: string;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirstMileRecord {
|
||||||
|
id: string;
|
||||||
|
bookingId: string;
|
||||||
|
status: FirstMileApiStatus;
|
||||||
|
advancedPayment: number;
|
||||||
|
remainingPayment: number;
|
||||||
|
estimatedKm?: number | null;
|
||||||
|
exactKm?: number | null;
|
||||||
|
vehicleId?: string | null;
|
||||||
|
booking?: FirstMileBooking | null;
|
||||||
|
vehicle?: FirstMileVehicle | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FirstMileListResponse {
|
||||||
|
data: FirstMileRecord[];
|
||||||
|
meta: { total: number; page: number; pageSize: number; totalPages: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
const FM = URL_CONSTANTS.FIRST_MILE;
|
||||||
|
|
||||||
|
export const firstMileService = {
|
||||||
|
list: (pageSize = 1000) =>
|
||||||
|
api.get<FirstMileListResponse>(`${FM.BASE}?pageSize=${pageSize}`),
|
||||||
|
getById: (id: string) => api.get<FirstMileRecord>(FM.BY_ID(id)),
|
||||||
|
update: (id: string, data: { status?: FirstMileApiStatus; vehicleId?: string | null }) =>
|
||||||
|
api.patch<FirstMileRecord>(FM.BY_ID(id), data),
|
||||||
|
accept: (bookingReference: string) =>
|
||||||
|
api.post<FirstMileRecord>(FM.ACCEPT(bookingReference)),
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { api } from '../auth/http';
|
||||||
|
import { URL_CONSTANTS } from '@/constants/URLS';
|
||||||
|
|
||||||
|
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;
|
||||||
|
scheduledDate?: string | null;
|
||||||
|
company?: { id: string; name?: string; phone?: string | null; contactPersonName?: string | null; contactPersonPhone?: string | null } | null;
|
||||||
|
serviceType?: { id: string; name?: string } | null;
|
||||||
|
originYard?: { id: string; name?: string } | null;
|
||||||
|
destinationYard?: { id: string; name?: string } | null;
|
||||||
|
cargoType?: { id: string; name?: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LastMileVehicle {
|
||||||
|
id: string;
|
||||||
|
plateNumber: string;
|
||||||
|
manufacturer: string;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 }) =>
|
||||||
|
api.patch<LastMileRecord>(LM.BY_ID(id), data),
|
||||||
|
accept: (bookingReference: string) =>
|
||||||
|
api.post<LastMileRecord>(LM.ACCEPT(bookingReference)),
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user