mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge pull request #556 from Tria-plc/freight/feature/first_mile_invoice
Freight/feature/first mile invoice
This commit is contained in:
@@ -68,6 +68,8 @@ import { AddCustomerTruckDto } from './dto/add-customer-truck.dto';
|
||||
import { DepartCustomerTruckDto } from './dto/depart-customer-truck.dto';
|
||||
import { LoadCustomerTruckDto } from './dto/load-customer-truck.dto';
|
||||
import { CustomerTruckService } from './customer-truck.service';
|
||||
import { FirstMileService } from '../first-mile/first-mile.service';
|
||||
import { LastMileService } from '../last-mile/last-mile.service';
|
||||
import { GenerateGrnDto } from './dto/generate-grn.dto';
|
||||
import { ContainerReceiptService } from './container-receipt.service';
|
||||
import { SignContractDto } from './dto/sign-contract.dto';
|
||||
@@ -81,6 +83,60 @@ import {
|
||||
hasFreightPermission,
|
||||
} from "../../common/freight-permission.util";
|
||||
|
||||
interface MileVehicleSummary {
|
||||
plate: string | null;
|
||||
code: string | null;
|
||||
driverName: string | null;
|
||||
containerNumber: string | null;
|
||||
distanceKm: number | null;
|
||||
}
|
||||
|
||||
interface MileLegSummary {
|
||||
status: string;
|
||||
exactKm: number | null;
|
||||
remainingPayment: number | null;
|
||||
currency: string;
|
||||
invoiced: boolean;
|
||||
vehicles: MileVehicleSummary[];
|
||||
}
|
||||
|
||||
/** Trim a first/last-mile record down to a customer-safe operational summary. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function summarizeMileLeg(rec?: Record<string, any>): MileLegSummary | null {
|
||||
if (!rec) return null;
|
||||
const num = (v: unknown) => (v == null ? null : Number(v));
|
||||
const assignments: Array<Record<string, any>> = rec.vehicleAssignments ?? []; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const currency =
|
||||
rec.vehicle?.currency ??
|
||||
assignments[0]?.vehicle?.currency ??
|
||||
rec.booking?.paymentCurrency ??
|
||||
'ETB';
|
||||
const vehicles: MileVehicleSummary[] = assignments.map((a) => ({
|
||||
plate: a.vehicle?.plateNumber ?? null,
|
||||
code: a.vehicle?.code ?? null,
|
||||
driverName: a.vehicle?.assignedDriverName ?? null,
|
||||
containerNumber: a.containerNumber ?? null,
|
||||
distanceKm: num(a.distanceKm),
|
||||
}));
|
||||
if (!vehicles.length && rec.vehicle) {
|
||||
vehicles.push({
|
||||
plate: rec.vehicle.plateNumber ?? null,
|
||||
code: rec.vehicle.code ?? null,
|
||||
driverName: rec.vehicle.assignedDriverName ?? null,
|
||||
containerNumber: null,
|
||||
distanceKm: num(rec.exactKm),
|
||||
});
|
||||
}
|
||||
return {
|
||||
status: rec.status ?? '',
|
||||
exactKm: num(rec.exactKm),
|
||||
remainingPayment: num(rec.remainingPayment),
|
||||
currency,
|
||||
invoiced: Boolean(rec.invoice),
|
||||
vehicles,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiTags("bookings")
|
||||
@Controller("bookings")
|
||||
@ApiBearerAuth()
|
||||
@@ -94,6 +150,8 @@ export class BookingsController {
|
||||
private readonly bookingClearanceService: BookingClearanceService,
|
||||
private readonly customerTruckService: CustomerTruckService,
|
||||
private readonly containerReceiptService: ContainerReceiptService,
|
||||
private readonly firstMileService: FirstMileService,
|
||||
private readonly lastMileService: LastMileService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@@ -290,6 +348,33 @@ export class BookingsController {
|
||||
return this.transitionService.enrichBookingResponse(booking);
|
||||
}
|
||||
|
||||
@Get(':id/mile-summary')
|
||||
@ApiOperation({
|
||||
summary: 'First/last-mile operational summary for a booking (customer-safe)',
|
||||
})
|
||||
async mileSummary(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@CurrentUser() user: TCurrentUser,
|
||||
) {
|
||||
// Customers may only see their own booking's mile summary.
|
||||
const booking = await this.bookingsService.findById(id);
|
||||
if (
|
||||
!hasFreightPermission(user, FREIGHT_PERMS.bookings.view) &&
|
||||
!hasFreightPermission(user, FREIGHT_PERMS.bookings.clearanceView)
|
||||
) {
|
||||
await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
|
||||
}
|
||||
|
||||
const [first, last] = await Promise.all([
|
||||
this.firstMileService.findAll({ bookingId: id, pageSize: 1 }),
|
||||
this.lastMileService.findAll({ bookingId: id, pageSize: 1 }),
|
||||
]);
|
||||
return {
|
||||
firstMile: summarizeMileLeg(first.data[0]),
|
||||
lastMile: summarizeMileLeg(last.data[0]),
|
||||
};
|
||||
}
|
||||
|
||||
@Post(':id/customer-truck-assignment')
|
||||
@ApiOperation({ summary: 'Customer assigns external truck and driver for terminal pickup' })
|
||||
async assignCustomerTruck(
|
||||
|
||||
@@ -12,6 +12,7 @@ import { FileUploadSettingsModule } from '../file-upload-settings/file-upload-se
|
||||
import { SignaturesModule } from '../signatures/signatures.module';
|
||||
import { BillingModule } from '../billing/billing.module';
|
||||
import { FirstMileModule } from '../first-mile/first-mile.module';
|
||||
import { LastMileModule } from '../last-mile/last-mile.module';
|
||||
import { BookingContractService } from './booking-contract.service';
|
||||
import { BookingInvoiceService } from './booking-invoice.service';
|
||||
// import { BookingPaymentController } from './booking-payment.controller';
|
||||
@@ -70,6 +71,7 @@ import { VehiclesModule } from "../vehicles/vehicles.module";
|
||||
NotificationsModule,
|
||||
NotificationInboxModule,
|
||||
forwardRef(() => FirstMileModule),
|
||||
forwardRef(() => LastMileModule),
|
||||
forwardRef(() => TrainSchedulingModule),
|
||||
forwardRef(() => ContractsModule),
|
||||
forwardRef(() => ContractsModule),
|
||||
|
||||
@@ -11,14 +11,15 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { FleetManage, FleetView } from '../../common/booking-guards';
|
||||
import { BookingStaff } from '../../common/booking-guards';
|
||||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||
import { GpsTrackingService } from './gps-tracking.service';
|
||||
import { RegisterDeviceDto, UpdateDeviceDto } from './dto/gps-device.dto';
|
||||
|
||||
@ApiTags('gps-tracking')
|
||||
@ApiBearerAuth()
|
||||
@Controller('gps')
|
||||
@FleetView()
|
||||
@BookingStaff(FREIGHT_PERMS.tracking.view)
|
||||
export class GpsTrackingController {
|
||||
constructor(private readonly gps: GpsTrackingService) {}
|
||||
|
||||
@@ -44,21 +45,21 @@ export class GpsTrackingController {
|
||||
}
|
||||
|
||||
@Post('devices')
|
||||
@FleetManage()
|
||||
@BookingStaff(FREIGHT_PERMS.tracking.manage)
|
||||
@ApiOperation({ summary: 'Register a GPS tracker' })
|
||||
register(@Body() dto: RegisterDeviceDto) {
|
||||
return this.gps.registerDevice(dto);
|
||||
}
|
||||
|
||||
@Patch('devices/:id')
|
||||
@FleetManage()
|
||||
@BookingStaff(FREIGHT_PERMS.tracking.manage)
|
||||
@ApiOperation({ summary: 'Update a GPS tracker (name / assigned vehicle)' })
|
||||
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateDeviceDto) {
|
||||
return this.gps.updateDevice(id, dto);
|
||||
}
|
||||
|
||||
@Delete('devices/:id')
|
||||
@FleetManage()
|
||||
@BookingStaff(FREIGHT_PERMS.tracking.manage)
|
||||
@ApiOperation({ summary: 'Delete a GPS tracker' })
|
||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.gps.removeDevice(id);
|
||||
|
||||
@@ -204,6 +204,7 @@ export const FLEET_ROAD_PERMISSIONS: FreightPermissionSeed[] = [
|
||||
perm('e2b00001-0001-4000-8000-000000000003', 'edr_freight_app:drivers:update', 'Update driver'),
|
||||
perm('e2b00001-0001-4000-8000-000000000004', 'edr_freight_app:drivers:delete', 'Delete driver'),
|
||||
perm('e2c00001-0001-4000-8000-000000000001', 'edr_freight_app:tracking:view', 'Track vehicles'),
|
||||
perm('e2c00001-0001-4000-8000-000000000002', 'edr_freight_app:tracking:manage', 'Manage GPS trackers'),
|
||||
perm('e2d00001-0001-4000-8000-000000000001', 'edr_freight_app:fuel:view', 'View fuel purchases'),
|
||||
perm('e2d00001-0001-4000-8000-000000000002', 'edr_freight_app:fuel:create', 'Create fuel purchase'),
|
||||
perm('e2d00001-0001-4000-8000-000000000003', 'edr_freight_app:fuel:update', 'Update fuel purchase'),
|
||||
@@ -477,6 +478,7 @@ export const FREIGHT_PERMS = {
|
||||
},
|
||||
tracking: {
|
||||
view: 'edr_freight_app:tracking:view',
|
||||
manage: 'edr_freight_app:tracking:manage',
|
||||
},
|
||||
fuel: {
|
||||
view: 'edr_freight_app:fuel:view',
|
||||
|
||||
@@ -145,6 +145,7 @@ export const FREIGHT_PERMS = {
|
||||
},
|
||||
tracking: {
|
||||
view: "edr_freight_app:tracking:view",
|
||||
manage: "edr_freight_app:tracking:manage",
|
||||
},
|
||||
fuel: {
|
||||
view: "edr_freight_app:fuel:view",
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
import { Activity, Pencil, Plus, Radio, Trash2 } from "lucide-react";
|
||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||
import { vehiclesService } from "@/services/vehicles.service";
|
||||
import { gpsTrackingService, type GpsDevice } from "@/services/gps-tracking.service";
|
||||
import { freightBrand } from "@/theme/freight-brand";
|
||||
@@ -151,6 +153,8 @@ function RouteTrail({ path }: { path: LatLng[] }) {
|
||||
export function TrackingPage() {
|
||||
const { toast } = useToast();
|
||||
const qc = useQueryClient();
|
||||
const { user } = useAuth();
|
||||
const canManage = hasPermission(user, FREIGHT_PERMS.tracking.manage);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [hoverId, setHoverId] = useState<string | null>(null);
|
||||
const [mapsReady, setMapsReady] = useState(false);
|
||||
@@ -288,9 +292,11 @@ export function TrackingPage() {
|
||||
<Text fw={700} size="xl">Real-Time Vehicle Tracking</Text>
|
||||
<Text c="dimmed" size="sm">Live GPS positions from GT06 trackers</Text>
|
||||
</div>
|
||||
<Button leftSection={<Plus size={16} />} color="edr-green" onClick={openRegister}>
|
||||
Register tracker
|
||||
</Button>
|
||||
{canManage && (
|
||||
<Button leftSection={<Plus size={16} />} color="edr-green" onClick={openRegister}>
|
||||
Register tracker
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Grid>
|
||||
@@ -358,9 +364,11 @@ export function TrackingPage() {
|
||||
<Badge color={selected.online ? "edr-green" : "gray"} leftSection={<Activity size={12} />}>
|
||||
{selected.online ? "Live" : "Offline"}
|
||||
</Badge>
|
||||
<ActionIcon variant="subtle" color="red" aria-label="Remove tracker" onClick={() => deleteMutation.mutate(selected.id)}>
|
||||
<Trash2 size={16} />
|
||||
</ActionIcon>
|
||||
{canManage && (
|
||||
<ActionIcon variant="subtle" color="red" aria-label="Remove tracker" onClick={() => deleteMutation.mutate(selected.id)}>
|
||||
<Trash2 size={16} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@@ -393,6 +401,7 @@ export function TrackingPage() {
|
||||
data={vehicleOptions}
|
||||
value={selected.vehicleId ?? null}
|
||||
onChange={(v) => assignMutation.mutate({ id: selected.id, vehicleId: v })}
|
||||
disabled={!canManage}
|
||||
searchable
|
||||
clearable
|
||||
/>
|
||||
@@ -421,14 +430,16 @@ export function TrackingPage() {
|
||||
<Table.Td align="right">
|
||||
<Group gap={6} justify="flex-end" wrap="nowrap">
|
||||
<Badge color={d.online ? "edr-green" : "gray"} size="sm">{d.online ? "Live" : "Offline"}</Badge>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
aria-label="Edit tracker"
|
||||
onClick={(e) => { e.stopPropagation(); openEdit(d); }}
|
||||
>
|
||||
<Pencil size={15} />
|
||||
</ActionIcon>
|
||||
{canManage && (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
aria-label="Edit tracker"
|
||||
onClick={(e) => { e.stopPropagation(); openEdit(d); }}
|
||||
>
|
||||
<Pencil size={15} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ContainersCard } from "./components/ContainersCard";
|
||||
import { ContractCard } from "./components/ContractCard";
|
||||
import { CustomerTruckAssignmentCard } from "./components/CustomerTruckAssignmentCard";
|
||||
import { KeyFactsStrip } from "./components/KeyFactsStrip";
|
||||
import { MileSummaryCard } from "./components/MileSummaryCard";
|
||||
import { BodyGrid, PageShell } from "./components/layout";
|
||||
import {
|
||||
CancelledBanner,
|
||||
@@ -220,6 +221,8 @@ export function ReadonlyBookingView({
|
||||
<WarehousePaymentsSection bookingId={booking.id} />
|
||||
|
||||
<ActivityCard booking={booking} />
|
||||
|
||||
<MileSummaryCard booking={booking} />
|
||||
</>
|
||||
}
|
||||
right={
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
import { bookingsService } from "@/services/bookings.service";
|
||||
import type {
|
||||
MileLegSummary,
|
||||
MileVehicleSummary,
|
||||
} from "@/services/bookings.service";
|
||||
|
||||
import { CardTitle, SectionCard } from "./layout";
|
||||
|
||||
function StatusPill({ status }: { status: string }) {
|
||||
const s = status.toUpperCase();
|
||||
const done = s.includes("DELIVER") || s.includes("COMPLET") || s.includes("PAID");
|
||||
const active = s.includes("TRANSIT") || s.includes("PROGRESS") || s.includes("ASSIGN");
|
||||
const dot = done ? "#0EA371" : active ? "#2563EB" : "#94A3B8";
|
||||
const color = done ? "#0A6F4D" : active ? "#1E40AF" : "#475569";
|
||||
const bg = done ? "#ECF6F1" : active ? "#EAF1FE" : "#F1F4F7";
|
||||
const border = done ? "#CDEBDD" : active ? "#CFDDFB" : "#E1E7EE";
|
||||
const label = status
|
||||
.replace(/_/g, " ")
|
||||
.toLowerCase()
|
||||
.replace(/\b\w/g, (m) => m.toUpperCase());
|
||||
|
||||
return (
|
||||
<Group
|
||||
component="span"
|
||||
gap={6}
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
borderRadius: 999,
|
||||
backgroundColor: bg,
|
||||
border: `1px solid ${border}`,
|
||||
padding: "4px 10px",
|
||||
fontSize: 11.5,
|
||||
fontWeight: 700,
|
||||
color,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
style={{ width: 6, height: 6, borderRadius: 999, backgroundColor: dot }}
|
||||
/>
|
||||
{label}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function VehicleRow({ v }: { v: MileVehicleSummary }) {
|
||||
const parts: string[] = [];
|
||||
if (v.driverName) parts.push(v.driverName);
|
||||
if (v.containerNumber) parts.push(`Container ${v.containerNumber}`);
|
||||
if (v.distanceKm != null) parts.push(`${v.distanceKm} km`);
|
||||
|
||||
return (
|
||||
<Group
|
||||
justify="space-between"
|
||||
align="flex-start"
|
||||
wrap="nowrap"
|
||||
py={8}
|
||||
style={{ borderTop: "1px solid #F2F5F8" }}
|
||||
>
|
||||
<Stack gap={2} style={{ minWidth: 0 }}>
|
||||
<Text fz="13px" fw={700} c="#10202F">
|
||||
{v.plate || v.code || "Vehicle"}
|
||||
</Text>
|
||||
{parts.length > 0 && (
|
||||
<Text fz="12px" c="#6B7C8E">
|
||||
{parts.join(" · ")}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
{v.code && v.plate && (
|
||||
<Text fz="12px" c="#9AA8B5" style={{ whiteSpace: "nowrap" }}>
|
||||
{v.code}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function LegBlock({
|
||||
title,
|
||||
leg,
|
||||
address,
|
||||
}: {
|
||||
title: string;
|
||||
leg: MileLegSummary | null;
|
||||
address?: string | null;
|
||||
}) {
|
||||
const fmtMoney = (n: number | null, currency: string) =>
|
||||
n == null
|
||||
? null
|
||||
: `${currency} ${Number(n).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}`;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Group justify="space-between" align="center" pb={8}>
|
||||
<Text fz="13px" fw={700} c="#10202F">
|
||||
{title}
|
||||
</Text>
|
||||
{leg ? (
|
||||
<StatusPill status={leg.status} />
|
||||
) : (
|
||||
<StatusPill status="AWAITING_ASSIGNMENT" />
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{address && (
|
||||
<Text fz="12px" c="#6B7C8E" pb={4}>
|
||||
{title.startsWith("First") ? "Pickup" : "Delivery"}:{" "}
|
||||
<b style={{ color: "#10202F" }}>{address}</b>
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!leg ? (
|
||||
<Text fz="12px" c="#9AA8B5" fs="italic" py={6}>
|
||||
Requested — a vehicle and driver will be assigned soon.
|
||||
</Text>
|
||||
) : leg.vehicles.length > 0 ? (
|
||||
<Box>
|
||||
{leg.vehicles.map((v, i) => (
|
||||
<VehicleRow key={`${v.plate ?? v.code ?? "v"}-${i}`} v={v} />
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Text fz="12px" c="#9AA8B5" fs="italic" py={6}>
|
||||
No vehicle assigned yet.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{leg && (
|
||||
<Group
|
||||
justify="space-between"
|
||||
align="center"
|
||||
pt={8}
|
||||
mt={4}
|
||||
style={{ borderTop: "1px solid #F2F5F8" }}
|
||||
>
|
||||
<Group gap={16}>
|
||||
{leg.exactKm != null && (
|
||||
<Text fz="12px" c="#6B7C8E">
|
||||
Total distance{" "}
|
||||
<b style={{ color: "#10202F" }}>{leg.exactKm} km</b>
|
||||
</Text>
|
||||
)}
|
||||
{leg.remainingPayment != null && leg.remainingPayment > 0 && (
|
||||
<Text fz="12px" c="#6B7C8E">
|
||||
Balance{" "}
|
||||
<b style={{ color: "#10202F" }}>
|
||||
{fmtMoney(leg.remainingPayment, leg.currency)}
|
||||
</b>
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
{leg.invoiced && (
|
||||
<Text fz="11px" fw={700} c="#0A6F4D">
|
||||
Invoiced
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function MileSummaryCard({ booking }: { booking: Freight.IBooking }) {
|
||||
const { data } = useQuery({
|
||||
queryKey: ["booking-mile-summary", booking.id],
|
||||
queryFn: () => bookingsService.mileSummary(booking.id),
|
||||
});
|
||||
|
||||
const firstLeg = data?.firstMile ?? null;
|
||||
const lastLeg = data?.lastMile ?? null;
|
||||
|
||||
// The backend doesn't persist an "enabled" flag — the presence of a
|
||||
// pickup/delivery address is the request signal. Also show a leg once its
|
||||
// record exists, regardless of address.
|
||||
const showFirst = !!booking.firstMilePickupAddress || !!firstLeg;
|
||||
const showLast = !!booking.lastMileDeliveryAddress || !!lastLeg;
|
||||
|
||||
if (!showFirst && !showLast) return null;
|
||||
|
||||
return (
|
||||
<SectionCard p={22}>
|
||||
<Box pb={12}>
|
||||
<CardTitle>First & Last Mile</CardTitle>
|
||||
</Box>
|
||||
<Stack gap={20}>
|
||||
{showFirst && (
|
||||
<LegBlock
|
||||
title="First mile"
|
||||
leg={firstLeg}
|
||||
address={booking.firstMilePickupAddress}
|
||||
/>
|
||||
)}
|
||||
{showLast && (
|
||||
<LegBlock
|
||||
title="Last mile"
|
||||
leg={lastLeg}
|
||||
address={booking.lastMileDeliveryAddress}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</SectionCard>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,26 @@ import { client } from "../utils/api";
|
||||
|
||||
const B = URL_CONSTANTS.BOOKINGS;
|
||||
|
||||
export interface MileVehicleSummary {
|
||||
plate: string | null;
|
||||
code: string | null;
|
||||
driverName: string | null;
|
||||
containerNumber: string | null;
|
||||
distanceKm: number | null;
|
||||
}
|
||||
export interface MileLegSummary {
|
||||
status: string;
|
||||
exactKm: number | null;
|
||||
remainingPayment: number | null;
|
||||
currency: string;
|
||||
invoiced: boolean;
|
||||
vehicles: MileVehicleSummary[];
|
||||
}
|
||||
export interface MileSummaryResponse {
|
||||
firstMile: MileLegSummary | null;
|
||||
lastMile: MileLegSummary | null;
|
||||
}
|
||||
|
||||
export type CreateBookingPayload = Freight.CreateBookingDto;
|
||||
|
||||
export interface ContractView {
|
||||
@@ -150,6 +170,10 @@ export const bookingsService = {
|
||||
const { data } = await client.get(`/api/bookings/${id}`);
|
||||
return data.data;
|
||||
},
|
||||
mileSummary: async (id: string): Promise<MileSummaryResponse> => {
|
||||
const { data } = await client.get(`/api/bookings/${id}/mile-summary`);
|
||||
return data.data;
|
||||
},
|
||||
assignCustomerTruck: async (
|
||||
id: string,
|
||||
payload: CustomerTruckAssignmentPayload,
|
||||
|
||||
Reference in New Issue
Block a user