mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +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,
|
||||
})
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value === '' ? undefined : value))
|
||||
@IsUUID()
|
||||
vehicleId?: string | null;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,13 @@ export class FirstMileController {
|
||||
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()
|
||||
@TrainSchedulingManage()
|
||||
@ApiOperation({ summary: 'Create a first-mile leg' })
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BookingsModule } from '../bookings/bookings.module';
|
||||
import { FirstMile } from './entities/first-mile.entity';
|
||||
import { FirstMileController } from './first-mile.controller';
|
||||
import { FirstMileRepository } from './first-mile.repository';
|
||||
import { FirstMileService } from './first-mile.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([FirstMile])],
|
||||
imports: [TypeOrmModule.forFeature([FirstMile]), BookingsModule],
|
||||
controllers: [FirstMileController],
|
||||
providers: [FirstMileRepository, FirstMileService],
|
||||
exports: [FirstMileRepository, FirstMileService],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { FindOptionsWhere } from 'typeorm';
|
||||
|
||||
import { BookingsRepository } from '../bookings/bookings.repository';
|
||||
import { CreateFirstMileDto } from './dto/create-first-mile.dto';
|
||||
import { UpdateFirstMileDto } from './dto/update-first-mile.dto';
|
||||
import { FirstMile, FirstMileStatus } from './entities/first-mile.entity';
|
||||
@@ -25,7 +26,32 @@ const SORTABLE_FIELDS: (keyof FirstMile)[] = [
|
||||
|
||||
@Injectable()
|
||||
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<{
|
||||
data: FirstMile[];
|
||||
@@ -45,7 +71,10 @@ export class FirstMileService {
|
||||
|
||||
const [data, total] = await this.firstMileRepository.findAndCount({
|
||||
where,
|
||||
relations: { booking: true, vehicle: true },
|
||||
relations: {
|
||||
booking: { company: true, serviceType: true, originYard: true, destinationYard: true, cargoType: true },
|
||||
vehicle: true,
|
||||
},
|
||||
order: { [sortBy]: sortOrder },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
@@ -64,7 +93,10 @@ export class FirstMileService {
|
||||
|
||||
async findById(id: string): Promise<FirstMile> {
|
||||
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) {
|
||||
|
||||
@@ -55,6 +55,7 @@ export class CreateLastMileDto {
|
||||
nullable: true,
|
||||
})
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => (value === '' ? undefined : value))
|
||||
@IsUUID()
|
||||
vehicleId?: string | null;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,13 @@ export class LastMileController {
|
||||
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()
|
||||
@TrainSchedulingManage()
|
||||
@ApiOperation({ summary: 'Create a last-mile leg' })
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BookingsModule } from '../bookings/bookings.module';
|
||||
import { LastMile } from './entities/last-mile.entity';
|
||||
import { LastMileController } from './last-mile.controller';
|
||||
import { LastMileRepository } from './last-mile.repository';
|
||||
import { LastMileService } from './last-mile.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([LastMile])],
|
||||
imports: [TypeOrmModule.forFeature([LastMile]), BookingsModule],
|
||||
controllers: [LastMileController],
|
||||
providers: [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 { BookingsRepository } from '../bookings/bookings.repository';
|
||||
import { CreateLastMileDto } from './dto/create-last-mile.dto';
|
||||
import { UpdateLastMileDto } from './dto/update-last-mile.dto';
|
||||
import { LastMile, LastMileStatus } from './entities/last-mile.entity';
|
||||
@@ -25,7 +26,29 @@ const SORTABLE_FIELDS: (keyof LastMile)[] = [
|
||||
|
||||
@Injectable()
|
||||
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<{
|
||||
data: LastMile[];
|
||||
@@ -45,7 +68,10 @@ export class LastMileService {
|
||||
|
||||
const [data, total] = await this.lastMileRepository.findAndCount({
|
||||
where,
|
||||
relations: { booking: true, vehicle: true },
|
||||
relations: {
|
||||
booking: { company: true, serviceType: true, originYard: true, destinationYard: true, cargoType: true },
|
||||
vehicle: true,
|
||||
},
|
||||
order: { [sortBy]: sortOrder },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
@@ -64,7 +90,10 @@ export class LastMileService {
|
||||
|
||||
async findById(id: string): Promise<LastMile> {
|
||||
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) {
|
||||
|
||||
@@ -69,6 +69,18 @@ export const QUERY_KEYS = {
|
||||
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: {
|
||||
ROOT: ["rule-engine"] as const,
|
||||
list: (resource: RuleEngineResourceSlug | string, params?: RuleEngineListParams) =>
|
||||
|
||||
@@ -365,6 +365,18 @@ export const URL_CONSTANTS = {
|
||||
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: {
|
||||
BASE: '/drivers',
|
||||
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