mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #277 from Tria-plc/freight/feature/vehicle_2
Freight/feature/vehicle 2
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
export class AddVehicleCodeAndPlates1810000000002 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const hasCode = await queryRunner.hasColumn('freight.vehicles', 'code');
|
||||
if (!hasCode) {
|
||||
await queryRunner.addColumn(
|
||||
'freight.vehicles',
|
||||
new TableColumn({ name: 'code', type: 'varchar', isNullable: true }),
|
||||
);
|
||||
}
|
||||
|
||||
const hasPower = await queryRunner.hasColumn('freight.vehicles', 'power_plate_no');
|
||||
if (!hasPower) {
|
||||
await queryRunner.addColumn(
|
||||
'freight.vehicles',
|
||||
new TableColumn({ name: 'power_plate_no', type: 'varchar', isNullable: true }),
|
||||
);
|
||||
}
|
||||
|
||||
const hasTrailer = await queryRunner.hasColumn('freight.vehicles', 'trailer_plate_no');
|
||||
if (!hasTrailer) {
|
||||
await queryRunner.addColumn(
|
||||
'freight.vehicles',
|
||||
new TableColumn({ name: 'trailer_plate_no', type: 'varchar', isNullable: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropColumn('freight.vehicles', 'trailer_plate_no');
|
||||
await queryRunner.dropColumn('freight.vehicles', 'power_plate_no');
|
||||
await queryRunner.dropColumn('freight.vehicles', 'code');
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BookingsModule } from '../bookings/bookings.module';
|
||||
import { DriversModule } from '../drivers/drivers.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { VehiclesModule } from '../vehicles/vehicles.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]), BookingsModule],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([FirstMile]),
|
||||
forwardRef(() => BookingsModule),
|
||||
VehiclesModule,
|
||||
DriversModule,
|
||||
NotificationsModule,
|
||||
],
|
||||
controllers: [FirstMileController],
|
||||
providers: [FirstMileRepository, FirstMileService],
|
||||
exports: [FirstMileRepository, FirstMileService],
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { FindOptionsWhere } from 'typeorm';
|
||||
|
||||
import { BookingsRepository } from '../bookings/bookings.repository';
|
||||
import { DriversService } from '../drivers/drivers.service';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { VehiclesService } from '../vehicles/vehicles.service';
|
||||
import { CreateFirstMileDto } from './dto/create-first-mile.dto';
|
||||
import { UpdateFirstMileDto } from './dto/update-first-mile.dto';
|
||||
import { FirstMile, FirstMileStatus } from './entities/first-mile.entity';
|
||||
@@ -26,9 +29,14 @@ const SORTABLE_FIELDS: (keyof FirstMile)[] = [
|
||||
|
||||
@Injectable()
|
||||
export class FirstMileService {
|
||||
private readonly logger = new Logger(FirstMileService.name);
|
||||
|
||||
constructor(
|
||||
private readonly firstMileRepository: FirstMileRepository,
|
||||
private readonly bookingsRepository: BookingsRepository,
|
||||
private readonly vehiclesService: VehiclesService,
|
||||
private readonly driversService: DriversService,
|
||||
private readonly notificationsService: NotificationsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -37,7 +45,7 @@ export class FirstMileService {
|
||||
* unknown or the booking has not reached PAID status.
|
||||
*/
|
||||
async acceptBooking(bookingReference: string): Promise<FirstMile | null> {
|
||||
const booking = await this.bookingsRepository.findByReference(bookingReference);
|
||||
const booking = await this.bookingsRepository.findById(bookingReference);
|
||||
|
||||
if (!booking) {
|
||||
return null;
|
||||
@@ -119,7 +127,7 @@ export class FirstMileService {
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateFirstMileDto): Promise<FirstMile> {
|
||||
await this.findById(id);
|
||||
const existing = await this.findById(id);
|
||||
|
||||
const updated = await this.firstMileRepository.update(id, {
|
||||
...(dto.bookingId !== undefined ? { bookingId: dto.bookingId } : {}),
|
||||
@@ -135,9 +143,45 @@ export class FirstMileService {
|
||||
throw new NotFoundException(`First-mile record ${id} not found`);
|
||||
}
|
||||
|
||||
// Notify assigned driver on every explicit vehicle assignment or reassignment
|
||||
if (dto.vehicleId) {
|
||||
void this.notifyDriverAssignment(dto.vehicleId, existing);
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private async notifyDriverAssignment(vehicleId: string, record: FirstMile): Promise<void> {
|
||||
try {
|
||||
const vehicle = await this.vehiclesService.findById(vehicleId);
|
||||
if (!vehicle.assignedDriverId) {
|
||||
this.logger.warn(`Vehicle ${vehicleId} has no assigned driver — skipping SMS`);
|
||||
return;
|
||||
}
|
||||
|
||||
const driver = await this.driversService.findById(vehicle.assignedDriverId);
|
||||
if (!driver.phoneNumber) {
|
||||
this.logger.warn(`Driver ${vehicle.assignedDriverId} has no phone number — skipping SMS`);
|
||||
return;
|
||||
}
|
||||
|
||||
const booking = (record as FirstMile & { booking?: { reference?: string; firstMilePickupAddress?: string | null; originYard?: { label?: string } | null } }).booking;
|
||||
|
||||
await this.notificationsService.notifyDriverVehicleAssignment({
|
||||
driverPhone: driver.phoneNumber,
|
||||
driverName: `${driver.firstName ?? ''} ${driver.lastName ?? ''}`.trim(),
|
||||
vehiclePlateNumber: vehicle.plateNumber ?? vehicleId,
|
||||
bookingReference: booking?.reference ?? record.bookingId,
|
||||
pickupAddress: booking?.firstMilePickupAddress,
|
||||
destinationYard: booking?.originYard?.label,
|
||||
});
|
||||
|
||||
this.logger.log(`SMS sent to driver ${driver.phoneNumber} for vehicle ${vehicleId} assignment`);
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to notify driver for vehicle ${vehicleId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.findById(id);
|
||||
await this.firstMileRepository.softDelete(id);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
|
||||
import { NotificationsService } from "./notifications.service";
|
||||
import { EmailNotificationStrategy } from "./strategies/notification.email.strategy";
|
||||
import { SmsNotificationStrategy } from "./strategies/notification.sms.strategy";
|
||||
import { HttpModule } from "@nestjs/axios";
|
||||
|
||||
@Module({
|
||||
imports: [HttpModule],
|
||||
imports: [ConfigModule],
|
||||
controllers: [],
|
||||
providers: [EmailNotificationStrategy, SmsNotificationStrategy, NotificationsService],
|
||||
exports: [NotificationsService],
|
||||
})
|
||||
export class NotificationsModule { }
|
||||
export class NotificationsModule {}
|
||||
|
||||
@@ -27,9 +27,29 @@ export class NotificationsService {
|
||||
if (!strategy) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
const sent = await strategy.send(recipient, message)
|
||||
this.logger.log(`is sent - ${sent}`)
|
||||
const sent = await strategy.send(recipient, message);
|
||||
this.logger.log(`is sent - ${sent}`);
|
||||
}
|
||||
|
||||
async notifyDriverVehicleAssignment(params: {
|
||||
driverPhone: string;
|
||||
driverName: string;
|
||||
vehiclePlateNumber: string;
|
||||
bookingReference: string;
|
||||
pickupAddress?: string | null;
|
||||
destinationYard?: string | null;
|
||||
}): Promise<void> {
|
||||
const { driverPhone, driverName, vehiclePlateNumber, bookingReference, pickupAddress, destinationYard } = params;
|
||||
const message =
|
||||
`Dear ${driverName}, you have been assigned to a first-mile pickup. ` +
|
||||
`Booking: ${bookingReference}. Vehicle: ${vehiclePlateNumber}. ` +
|
||||
(pickupAddress ? `Pickup: ${pickupAddress}. ` : '') +
|
||||
(destinationYard ? `Destination: ${destinationYard}.` : '');
|
||||
|
||||
try {
|
||||
await this.directSend('sms', driverPhone, message);
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to notify driver ${driverName} (${driverPhone}): ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,36 @@
|
||||
import { Injectable} from "@nestjs/common";
|
||||
import { NotificationStrategy } from "./notification.strategy";
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import axios from "axios";
|
||||
|
||||
import { NotificationStrategy } from "./notification.strategy";
|
||||
|
||||
@Injectable()
|
||||
export class SmsNotificationStrategy implements NotificationStrategy {
|
||||
constructor(private readonly httpService: HttpService, private readonly configService: ConfigService) { }
|
||||
async send(recipient: string, message: string) {
|
||||
const url = this.configService.get("OZIKING_SMS_URL")
|
||||
const body = {
|
||||
to: recipient,
|
||||
text: message
|
||||
}
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.post(
|
||||
url,
|
||||
body,
|
||||
),
|
||||
);
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
return response.status === 201;
|
||||
}
|
||||
async send(recipient: string, message: string): Promise<boolean> {
|
||||
const url =
|
||||
this.configService.get<string>("OZIKING_SMS_URL") ??
|
||||
"https://notification-dev.license.aafda.gov.et/api/sms-services/ozeking/sms";
|
||||
|
||||
await axios.post(
|
||||
url,
|
||||
{
|
||||
to: recipient,
|
||||
sourceId: this.configService.get<string>("OZIKING_SOURCE_ID") ?? "EDR",
|
||||
sourceName: this.configService.get<string>("OZIKING_SOURCE_NAME") ?? "EDR Freight",
|
||||
appKey: this.configService.get<string>("OZIKING_APP_KEY") ?? "",
|
||||
text: message,
|
||||
callbackUrl: "",
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
accept: "*/*",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { InternalPaymentController } from "./internal-payment.controller";
|
||||
import { ServiceAuthGuard } from "../../common/guards/service-auth.guard";
|
||||
import { TrainSchedulingModule } from "../train-scheduling/train-scheduling.module";
|
||||
import { DropdownSettingsModule } from "../dropdown-settings/dropdown-settings.module";
|
||||
import { FirstMileModule } from "../first-mile/first-mile.module";
|
||||
import { PaymentWebhookEventEntity } from "./entities/payment-webhook-event.entity";
|
||||
import { PaymentRefundEntity } from "./entities/payment-refund.entity";
|
||||
|
||||
@@ -29,6 +30,7 @@ const FREIGHT_QUEUE = PAYMENT_QUEUES[PaymentServiceEnum.FREIGHT];
|
||||
HttpModule.register({ timeout: 10_000 }),
|
||||
ConfigModule,
|
||||
DropdownSettingsModule,
|
||||
forwardRef(() => FirstMileModule),
|
||||
forwardRef(() => TrainSchedulingModule),
|
||||
TypeOrmModule.forFeature([PaymentWebhookEventEntity, PaymentRefundEntity]),
|
||||
RabbitMQModule.forRootAsync({
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "./payments.dto";
|
||||
import { BookingBatchService } from "../train-scheduling/booking-batch.service";
|
||||
import { DropdownSettingsService } from "../dropdown-settings/dropdown-settings.service";
|
||||
import { FirstMileService } from "../first-mile/first-mile.service";
|
||||
|
||||
/** Setting code holding the global ordering window (months) for general contracts. */
|
||||
const CONTRACT_PERIOD_SETTING_CODE = "general_contract_period";
|
||||
@@ -60,6 +61,7 @@ export class PaymentService {
|
||||
@Inject(forwardRef(() => BookingBatchService))
|
||||
private readonly bookingBatchService: BookingBatchService,
|
||||
private readonly dropdownSettings: DropdownSettingsService,
|
||||
private readonly firstMileService: FirstMileService,
|
||||
) { }
|
||||
|
||||
/** Configured general-contract ordering window in months (defaults to 3). */
|
||||
@@ -342,6 +344,8 @@ export class PaymentService {
|
||||
? { paymentStatus: "PAID", status: "CONTRACT_ACTIVE", expiresAt: contractExpiresAt }
|
||||
: { paymentStatus: "PAID", status: "PAID" },
|
||||
);
|
||||
await this.firstMileService.acceptBooking(input.bookingId);
|
||||
|
||||
});
|
||||
|
||||
if (isGeneralContract) {
|
||||
|
||||
@@ -37,4 +37,16 @@ export class CreateVehicleDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
assignedDriverName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
powerPlateNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
trailerPlateNo?: string;
|
||||
}
|
||||
|
||||
@@ -62,4 +62,13 @@ export class Vehicle extends BaseEntity {
|
||||
|
||||
@Column({ name: 'assigned_driver_name', nullable: true })
|
||||
assignedDriverName?: string;
|
||||
|
||||
@Column({ name: 'code', nullable: true })
|
||||
code?: string;
|
||||
|
||||
@Column({ name: 'power_plate_no', nullable: true })
|
||||
powerPlateNo?: string;
|
||||
|
||||
@Column({ name: 'trailer_plate_no', nullable: true })
|
||||
trailerPlateNo?: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
|
||||
// export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
|
||||
|
||||
// export const API_BASE_URL = 'http://localhost:3001';
|
||||
export const API_BASE_URL = 'http://localhost:3001';
|
||||
|
||||
@@ -47,7 +47,10 @@ export const vehiclesConfig: FleetResourceConfig = {
|
||||
],
|
||||
searchKeys: ["plateNumber", "registrationNumber", "manufacturer", "model", "vehicleType", "status"],
|
||||
columns: [
|
||||
{ id: "code", header: "Code", accessorKey: "code", format: "code", size: 110 },
|
||||
{ id: "plateNumber", header: "Plate Number", accessorKey: "plateNumber", format: "code", size: 130 },
|
||||
{ id: "powerPlateNo", header: "Power Plate No", accessorKey: "powerPlateNo", format: "code", size: 140 },
|
||||
{ id: "trailerPlateNo", header: "Trailer Plate No", accessorKey: "trailerPlateNo", format: "code", size: 140 },
|
||||
{ id: "registrationNumber", header: "Registration", accessorKey: "registrationNumber", format: "code", size: 140 },
|
||||
{ id: "manufacturer", header: "Manufacturer", accessorKey: "manufacturer", format: "code", size: 140 },
|
||||
{ id: "model", header: "Model", accessorKey: "model", format: "code", size: 120 },
|
||||
@@ -59,7 +62,10 @@ export const vehiclesConfig: FleetResourceConfig = {
|
||||
{ id: "status", header: "Status", accessorKey: "status", format: "statusBadge", size: 100 },
|
||||
],
|
||||
formFields: [
|
||||
{ name: "code", label: "Code", type: "text" },
|
||||
{ name: "plateNumber", label: "Plate Number", type: "text", required: true },
|
||||
{ name: "powerPlateNo", label: "Power Plate No", type: "text" },
|
||||
{ name: "trailerPlateNo", label: "Trailer Plate No", type: "text" },
|
||||
{ name: "vehicleType", label: "Vehicle Type", type: "select", required: true, options: VEHICLE_TYPE_OPTIONS },
|
||||
{ name: "manufacturer", label: "Manufacturer", type: "text", required: true },
|
||||
{ name: "model", label: "Model", type: "text", required: true },
|
||||
@@ -70,7 +76,10 @@ export const vehiclesConfig: FleetResourceConfig = {
|
||||
{ name: "description", label: "Description", type: "textarea" },
|
||||
],
|
||||
emptyValues: {
|
||||
code: "",
|
||||
plateNumber: "",
|
||||
powerPlateNo: "",
|
||||
trailerPlateNo: "",
|
||||
vehicleType: "TRUCK",
|
||||
manufacturer: "",
|
||||
model: "",
|
||||
|
||||
@@ -75,7 +75,11 @@ const FILTER_OPTIONS: { value: StatusFilter; label: string }[] = [
|
||||
const vehicleLabel = (record: FirstMileRecord) => {
|
||||
if (!record.vehicle) return null;
|
||||
const v = record.vehicle;
|
||||
return `${v.manufacturer} ${v.model} (${v.plateNumber})`;
|
||||
const parts = [`${v.manufacturer} ${v.model}`, v.plateNumber];
|
||||
if (v.code) parts.unshift(v.code);
|
||||
const plates = [v.powerPlateNo, v.trailerPlateNo].filter(Boolean).join(" / ");
|
||||
if (plates) parts.push(plates);
|
||||
return parts.join(" · ");
|
||||
};
|
||||
|
||||
const isAssigned = (record: FirstMileRecord) => Boolean(record.vehicleId);
|
||||
@@ -91,8 +95,9 @@ const cargoDesc = (r: FirstMileRecord) => {
|
||||
};
|
||||
const priceAmount = (r: FirstMileRecord) =>
|
||||
r.booking?.totalAmount ?? r.advancedPayment;
|
||||
// First-mile destination is the origin yard (pickup → origin yard)
|
||||
const destinationYardName = (r: FirstMileRecord) =>
|
||||
r.booking?.destinationYard?.name ?? "—";
|
||||
r.booking?.originYard?.label ?? "—";
|
||||
const contactPersonName = (r: FirstMileRecord) =>
|
||||
r.booking?.company?.contactPersonName ?? "—";
|
||||
const contactPhone = (r: FirstMileRecord) =>
|
||||
@@ -102,7 +107,7 @@ const requestedDate = (r: FirstMileRecord) => {
|
||||
return d ? new Date(d).toISOString().slice(0, 10) : "—";
|
||||
};
|
||||
const serviceTypeName = (r: FirstMileRecord) =>
|
||||
r.booking?.serviceType?.name ?? "—";
|
||||
r.booking?.serviceType?.label ?? "—";
|
||||
|
||||
const InfoRow = ({ label, value }: { label: string; value: string }) => (
|
||||
<Stack gap={2}>
|
||||
@@ -135,7 +140,7 @@ const BookingInfo = ({ record }: { record: FirstMileRecord }) => (
|
||||
<InfoRow label="Customer" value={customerName(record)} />
|
||||
<InfoRow label="Service type" value={serviceTypeName(record)} />
|
||||
<InfoRow label="Pickup location" value={pickupLocation(record)} />
|
||||
<InfoRow label="Destination yard" value={destinationYardName(record)} />
|
||||
<InfoRow label="Destination (origin yard)" value={destinationYardName(record)} />
|
||||
<InfoRow label="Cargo" value={cargoDesc(record)} />
|
||||
<InfoRow label="Price" value={formatPrice(priceAmount(record))} />
|
||||
<InfoRow label="Contact" value={contactPersonName(record)} />
|
||||
@@ -345,10 +350,13 @@ const FirstMilePage = () => {
|
||||
|
||||
const vehicleOptions = useMemo(
|
||||
() =>
|
||||
(Array.isArray(vehiclesData) ? vehiclesData : []).map((v) => ({
|
||||
value: v.id,
|
||||
label: `${v.manufacturer} ${v.model} (${v.plateNumber})`,
|
||||
})),
|
||||
(Array.isArray(vehiclesData) ? vehiclesData : []).map((v) => {
|
||||
const parts = [`${v.manufacturer} ${v.model}`, v.plateNumber];
|
||||
if (v.code) parts.unshift(v.code);
|
||||
const plates = [v.powerPlateNo, v.trailerPlateNo].filter(Boolean).join(" / ");
|
||||
if (plates) parts.push(plates);
|
||||
return { value: v.id, label: parts.join(" · ") };
|
||||
}),
|
||||
[vehiclesData],
|
||||
);
|
||||
|
||||
@@ -587,6 +595,12 @@ const FirstMilePage = () => {
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => pickupLocation(row.original),
|
||||
},
|
||||
{
|
||||
id: "destination",
|
||||
header: "Destination",
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => destinationYardName(row.original),
|
||||
},
|
||||
{
|
||||
id: "cargo",
|
||||
header: "Cargo",
|
||||
@@ -903,7 +917,7 @@ const FirstMilePage = () => {
|
||||
</Stack>
|
||||
<Stack gap={3} align="flex-end" style={{ flexShrink: 0 }}>
|
||||
<Text size="xs" c="dimmed">
|
||||
{b.originYard?.name ?? "—"} → {b.destinationYard?.name ?? "—"}
|
||||
{b.originYard?.label ?? "—"} → {b.destinationYard?.label ?? "—"}
|
||||
</Text>
|
||||
<Text size="sm" fw={600} c="blue">
|
||||
{formatPrice(b.totalAmount)}
|
||||
@@ -935,8 +949,8 @@ const FirstMilePage = () => {
|
||||
<InfoRow label="Customer" value={selectedBooking.company?.name ?? selectedBooking.company?.companyName ?? "—"} />
|
||||
<InfoRow label="Service type" value={selectedBooking.serviceType?.name ?? "—"} />
|
||||
<InfoRow label="Pickup address" value={selectedBooking.firstMilePickupAddress ?? "—"} />
|
||||
<InfoRow label="Origin yard" value={selectedBooking.originYard?.name ?? "—"} />
|
||||
<InfoRow label="Destination yard" value={selectedBooking.destinationYard?.name ?? "—"} />
|
||||
<InfoRow label="Destination (origin yard)" value={selectedBooking.originYard?.label ?? "—"} />
|
||||
<InfoRow label="Train destination yard" value={selectedBooking.destinationYard?.label ?? "—"} />
|
||||
<InfoRow label="Cargo type" value={selectedBooking.cargoType?.name ?? "—"} />
|
||||
<InfoRow label="Weight (VGM)" value={`${selectedBooking.cargoTotalWeightVgm} t`} />
|
||||
<InfoRow label="Total amount" value={formatPrice(selectedBooking.totalAmount)} />
|
||||
|
||||
@@ -71,7 +71,11 @@ const FILTER_OPTIONS: { value: StatusFilter; label: string }[] = [
|
||||
const vehicleLabel = (record: LastMileRecord) => {
|
||||
if (!record.vehicle) return null;
|
||||
const v = record.vehicle;
|
||||
return `${v.manufacturer} ${v.model} (${v.plateNumber})`;
|
||||
const parts = [`${v.manufacturer} ${v.model}`, v.plateNumber];
|
||||
if (v.code) parts.unshift(v.code);
|
||||
const plates = [v.powerPlateNo, v.trailerPlateNo].filter(Boolean).join(" / ");
|
||||
if (plates) parts.push(plates);
|
||||
return parts.join(" · ");
|
||||
};
|
||||
|
||||
const isAssigned = (record: LastMileRecord) => Boolean(record.vehicleId);
|
||||
@@ -313,10 +317,13 @@ const LastMilePage = () => {
|
||||
|
||||
const vehicleOptions = useMemo(
|
||||
() =>
|
||||
(Array.isArray(vehiclesData) ? vehiclesData : []).map((v) => ({
|
||||
value: v.id,
|
||||
label: `${v.manufacturer} ${v.model} (${v.plateNumber})`,
|
||||
})),
|
||||
(Array.isArray(vehiclesData) ? vehiclesData : []).map((v) => {
|
||||
const parts = [`${v.manufacturer} ${v.model}`, v.plateNumber];
|
||||
if (v.code) parts.unshift(v.code);
|
||||
const plates = [v.powerPlateNo, v.trailerPlateNo].filter(Boolean).join(" / ");
|
||||
if (plates) parts.push(plates);
|
||||
return { value: v.id, label: parts.join(" · ") };
|
||||
}),
|
||||
[vehiclesData],
|
||||
);
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ export interface FirstMileBooking {
|
||||
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;
|
||||
serviceType?: { id: string; label?: string } | null;
|
||||
originYard?: { id: string; label?: string } | null;
|
||||
destinationYard?: { id: string; label?: string } | null;
|
||||
cargoType?: { id: string; label?: string } | null;
|
||||
}
|
||||
|
||||
export interface FirstMileVehicle {
|
||||
@@ -29,6 +29,9 @@ export interface FirstMileVehicle {
|
||||
plateNumber: string;
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
code?: string | null;
|
||||
powerPlateNo?: string | null;
|
||||
trailerPlateNo?: string | null;
|
||||
}
|
||||
|
||||
export interface FirstMileRecord {
|
||||
|
||||
@@ -29,6 +29,9 @@ export interface LastMileVehicle {
|
||||
plateNumber: string;
|
||||
manufacturer: string;
|
||||
model: string;
|
||||
code?: string | null;
|
||||
powerPlateNo?: string | null;
|
||||
trailerPlateNo?: string | null;
|
||||
}
|
||||
|
||||
export interface LastMileRecord {
|
||||
|
||||
@@ -26,6 +26,9 @@ export interface Vehicle {
|
||||
capacity: number;
|
||||
status: VehicleStatus;
|
||||
description?: string | null;
|
||||
code?: string | null;
|
||||
powerPlateNo?: string | null;
|
||||
trailerPlateNo?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
|
||||
// export const API_BASE_URL = 'http://localhost:3001';
|
||||
// export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
|
||||
export const API_BASE_URL = 'http://localhost:3001';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user