mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 22:58:17 +00:00
Merge remote-tracking branch 'origin/dev' into dj-franc
This commit is contained in:
@@ -91,6 +91,7 @@ import type {
|
||||
TrainScheduleFilters,
|
||||
TrainScheduleListFilters,
|
||||
TrainScheduleListResponse,
|
||||
ReduceScheduleCloseOffsetPayload,
|
||||
UpdateScheduleWindowRulePayload,
|
||||
TrainSchedulePreviewPayload,
|
||||
TrainSchedulePreviewResponse,
|
||||
@@ -713,6 +714,18 @@ export const api = {
|
||||
() => TRAIN_SCHEDULING_INVALIDATIONS,
|
||||
),
|
||||
|
||||
reduceScheduleCloseOffset: endpoint<
|
||||
{ id: string; payload: ReduceScheduleCloseOffsetPayload },
|
||||
TrainScheduleDetail
|
||||
>(
|
||||
"train-scheduling",
|
||||
"reduce-schedule-close-offset",
|
||||
({ id, payload }) =>
|
||||
trainSchedulingService.reduceScheduleCloseOffset(id, payload),
|
||||
undefined,
|
||||
() => TRAIN_SCHEDULING_INVALIDATIONS,
|
||||
),
|
||||
|
||||
updateScheduleDate: endpoint<
|
||||
{ id: string; scheduleDate: string },
|
||||
TrainScheduleDetail
|
||||
@@ -3220,11 +3233,23 @@ export const api = {
|
||||
bookingsService.reviewOperation(id, decision, { note }),
|
||||
),
|
||||
|
||||
proceedToOperation: endpoint<
|
||||
{ id: string; scheduledDate: string },
|
||||
rescheduleOperation: endpoint<
|
||||
{
|
||||
id: string;
|
||||
scheduledDate: string;
|
||||
trainScheduleId?: string;
|
||||
note?: string;
|
||||
},
|
||||
BookingDetail
|
||||
>("bookings", "proceedToOperation", ({ id, scheduledDate }) =>
|
||||
bookingsService.proceedToOperation(id, scheduledDate),
|
||||
>("bookings", "rescheduleOperation", ({ id, ...payload }) =>
|
||||
bookingsService.rescheduleOperation(id, payload),
|
||||
),
|
||||
|
||||
proceedToOperation: endpoint<
|
||||
{ id: string; scheduledDate: string; trainScheduleId?: string },
|
||||
BookingDetail
|
||||
>("bookings", "proceedToOperation", ({ id, scheduledDate, trainScheduleId }) =>
|
||||
bookingsService.proceedToOperation(id, scheduledDate, trainScheduleId),
|
||||
),
|
||||
|
||||
generateContract: endpoint<{ id: string }, BookingDetail>(
|
||||
|
||||
@@ -355,8 +355,21 @@ export const bookingsService = {
|
||||
* customer path uses the same endpoint from the portal; GL needs it here
|
||||
* because a customs booking is GL's to fix, not the customer's.
|
||||
*/
|
||||
proceedToOperation: (id: string, scheduledDate: string) =>
|
||||
postBooking<BookingDetail>(B.CLEARANCE_PROCEED(id), { scheduledDate }),
|
||||
proceedToOperation: (id: string, scheduledDate: string, trainScheduleId?: string) =>
|
||||
postBooking<BookingDetail>(B.CLEARANCE_PROCEED(id), {
|
||||
scheduledDate,
|
||||
...(trainScheduleId ? { trainScheduleId } : {}),
|
||||
}),
|
||||
|
||||
/**
|
||||
* Operations changes the shipment day (and, for export rail, the train) of a
|
||||
* pending operation request on the customer's behalf — the booking stays at
|
||||
* OPERATION_REQUEST_PENDING for the normal accept.
|
||||
*/
|
||||
rescheduleOperation: (
|
||||
id: string,
|
||||
payload: { scheduledDate: string; trainScheduleId?: string; note?: string },
|
||||
) => postBooking<BookingDetail>(B.OPERATION_RESCHEDULE(id), payload),
|
||||
|
||||
generateContract: (id: string) =>
|
||||
postBooking<BookingDetail>(B.CONTRACT_GENERATE(id)),
|
||||
|
||||
@@ -303,6 +303,14 @@ export const contractsService = {
|
||||
resume: (id: string, note?: string) =>
|
||||
postContract<Freight.IContract>(C.RESUME(id), { note }),
|
||||
|
||||
/**
|
||||
* Add validity days to an EXPIRED contract the customer asked to extend; it
|
||||
* returns to the status it held before it lapsed. The API refuses it while
|
||||
* no customer request is pending.
|
||||
*/
|
||||
extend: (id: string, payload: Freight.ExtendContractDto) =>
|
||||
postContract<Freight.IContract>(C.EXTEND(id), payload),
|
||||
|
||||
/**
|
||||
* Approve the next pending step. The server resolves the step's required role
|
||||
* and authorizes against it — the client never declares its own role.
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { api as apiClient } from '../auth/http';
|
||||
import { URL_CONSTANTS } from '@/constants/URLS';
|
||||
|
||||
export type TrainCrewRole =
|
||||
| 'TRAIN_DRIVER'
|
||||
| 'FEDERAL_POLICE'
|
||||
| 'TECHNICIAN'
|
||||
| 'REEFER_TECHNICIAN'
|
||||
| 'HAZMAT_ESCORT'
|
||||
| 'LASHING_INSPECTOR'
|
||||
| 'LIVESTOCK_HANDLER';
|
||||
|
||||
export type TrainCrewNationality = 'ETHIOPIAN' | 'DJIBOUTIAN';
|
||||
|
||||
export type TrainCrewStatus = 'ACTIVE' | 'INACTIVE' | 'SUSPENDED' | 'ON_LEAVE';
|
||||
|
||||
export interface TrainCrewMember {
|
||||
id: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
role: TrainCrewRole;
|
||||
nationality: TrainCrewNationality;
|
||||
status: TrainCrewStatus;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface TrainCrewListFilters {
|
||||
search?: string;
|
||||
role?: TrainCrewRole;
|
||||
nationality?: TrainCrewNationality;
|
||||
status?: TrainCrewStatus;
|
||||
isActive?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'ASC' | 'DESC';
|
||||
}
|
||||
|
||||
/** Paginated envelope returned by GET /train-crew. */
|
||||
export interface TrainCrewListResponse {
|
||||
data: TrainCrewMember[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export type SaveTrainCrewMemberPayload = Omit<
|
||||
TrainCrewMember,
|
||||
'id' | 'createdAt' | 'updatedAt'
|
||||
>;
|
||||
|
||||
export const trainCrewService = {
|
||||
getAll: (filters: TrainCrewListFilters = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.search) params.set('search', filters.search);
|
||||
if (filters.role) params.set('role', filters.role);
|
||||
if (filters.nationality) params.set('nationality', filters.nationality);
|
||||
if (filters.status) params.set('status', filters.status);
|
||||
if (filters.isActive !== undefined) {
|
||||
params.set('isActive', String(filters.isActive));
|
||||
}
|
||||
if (filters.page) params.set('page', String(filters.page));
|
||||
if (filters.limit) params.set('limit', String(filters.limit));
|
||||
if (filters.sortBy) params.set('sortBy', filters.sortBy);
|
||||
if (filters.sortOrder) params.set('sortOrder', filters.sortOrder);
|
||||
const qs = params.toString();
|
||||
return apiClient.get<TrainCrewListResponse>(
|
||||
`${URL_CONSTANTS.TRAIN_CREW.BASE}${qs ? `?${qs}` : ''}`,
|
||||
);
|
||||
},
|
||||
getById: (id: string) =>
|
||||
apiClient.get<TrainCrewMember>(URL_CONSTANTS.TRAIN_CREW.BY_ID(id)),
|
||||
create: (data: Partial<SaveTrainCrewMemberPayload>) =>
|
||||
apiClient.post(URL_CONSTANTS.TRAIN_CREW.BASE, data),
|
||||
update: (id: string, data: Partial<SaveTrainCrewMemberPayload>) =>
|
||||
apiClient.patch(URL_CONSTANTS.TRAIN_CREW.BY_ID(id), data),
|
||||
delete: (id: string) => apiClient.delete(URL_CONSTANTS.TRAIN_CREW.BY_ID(id)),
|
||||
};
|
||||
|
||||
export const TRAIN_CREW_ROLE_OPTIONS: Array<{ label: string; value: TrainCrewRole }> = [
|
||||
{ label: 'Train Driver', value: 'TRAIN_DRIVER' },
|
||||
{ label: 'Federal Police', value: 'FEDERAL_POLICE' },
|
||||
{ label: 'Technician', value: 'TECHNICIAN' },
|
||||
{ label: 'Reefer Technician', value: 'REEFER_TECHNICIAN' },
|
||||
{ label: 'HAZMAT Escort', value: 'HAZMAT_ESCORT' },
|
||||
{ label: 'Lashing Inspector', value: 'LASHING_INSPECTOR' },
|
||||
{ label: 'Livestock Handler', value: 'LIVESTOCK_HANDLER' },
|
||||
];
|
||||
|
||||
export const TRAIN_CREW_NATIONALITY_OPTIONS: Array<{
|
||||
label: string;
|
||||
value: TrainCrewNationality;
|
||||
}> = [
|
||||
{ label: 'Ethiopian', value: 'ETHIOPIAN' },
|
||||
{ label: 'Djiboutian', value: 'DJIBOUTIAN' },
|
||||
];
|
||||
|
||||
export const TRAIN_CREW_STATUS_OPTIONS: Array<{ label: string; value: TrainCrewStatus }> = [
|
||||
{ label: 'Active', value: 'ACTIVE' },
|
||||
{ label: 'Inactive', value: 'INACTIVE' },
|
||||
{ label: 'Suspended', value: 'SUSPENDED' },
|
||||
{ label: 'On leave', value: 'ON_LEAVE' },
|
||||
];
|
||||
|
||||
export const trainCrewRoleLabel = (role: TrainCrewRole): string =>
|
||||
TRAIN_CREW_ROLE_OPTIONS.find((o) => o.value === role)?.label ?? role;
|
||||
|
||||
export const trainCrewNationalityLabel = (n: TrainCrewNationality): string =>
|
||||
TRAIN_CREW_NATIONALITY_OPTIONS.find((o) => o.value === n)?.label ?? n;
|
||||
|
||||
export const trainCrewStatusLabel = (s: TrainCrewStatus): string =>
|
||||
TRAIN_CREW_STATUS_OPTIONS.find((o) => o.value === s)?.label ?? s;
|
||||
@@ -0,0 +1,104 @@
|
||||
import { api as apiClient } from '../auth/http';
|
||||
import type { TrainCrewMember, TrainCrewRole } from './trainCrew.service';
|
||||
|
||||
export type CrewDutyRole = 'PRIMARY' | 'ASSISTANT' | 'BENCH_RELIEF';
|
||||
|
||||
/** A yard on the schedule's route, ordered along the corridor. */
|
||||
export interface CorridorYard {
|
||||
id: string;
|
||||
label: string;
|
||||
country: string;
|
||||
displayOrder: number;
|
||||
}
|
||||
|
||||
export interface CrewAssignment {
|
||||
id: string;
|
||||
trainScheduleId: string;
|
||||
crewMemberId: string;
|
||||
role: TrainCrewRole;
|
||||
dutyRole?: CrewDutyRole | null;
|
||||
fromYardId?: string | null;
|
||||
toYardId?: string | null;
|
||||
status: string;
|
||||
crewMember?: TrainCrewMember;
|
||||
}
|
||||
|
||||
/** What the consist and its cargo demand (§1.2), detected server-side. */
|
||||
export interface CrewDemand {
|
||||
hasBadOrderWagon: boolean;
|
||||
badOrderWagonLabels: string[];
|
||||
hasReeferCargo: boolean;
|
||||
reeferSources: string[];
|
||||
hasHazmatCargo: boolean;
|
||||
hazmatSources: string[];
|
||||
hasBreakBulkCargo: boolean;
|
||||
breakBulkSources: string[];
|
||||
hasLivestockCargo: boolean;
|
||||
livestockSources: string[];
|
||||
}
|
||||
|
||||
export interface CrewRequirement {
|
||||
role: TrainCrewRole;
|
||||
/** Hard floor — 0 unless the cargo or consist forces someone aboard. */
|
||||
min: number;
|
||||
/** The count §1.2 suggests. A hint only; nothing enforces it. */
|
||||
typical: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface CrewValidation {
|
||||
complete: boolean;
|
||||
issues: Array<{ code: string; message: string }>;
|
||||
runType: 'SHORT_RUN' | 'LONG_RUN' | null;
|
||||
}
|
||||
|
||||
export interface ScheduleCrewResponse {
|
||||
scheduleId: string;
|
||||
assignments: CrewAssignment[];
|
||||
/** Yards a driver leg may use — bounded by the schedule's own endpoints. */
|
||||
corridorYards: CorridorYard[];
|
||||
demand: CrewDemand;
|
||||
requirements: {
|
||||
technician: CrewRequirement;
|
||||
specialized: CrewRequirement[];
|
||||
};
|
||||
validation: CrewValidation;
|
||||
}
|
||||
|
||||
export interface SaveCrewPayload {
|
||||
assignments: Array<{
|
||||
crewMemberId: string;
|
||||
role: TrainCrewRole;
|
||||
dutyRole?: CrewDutyRole;
|
||||
fromYardId?: string;
|
||||
toYardId?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const base = (scheduleId: string) => `/train-schedules/${scheduleId}/crew`;
|
||||
|
||||
export const trainCrewAssignmentService = {
|
||||
get: (scheduleId: string) =>
|
||||
apiClient.get<ScheduleCrewResponse>(base(scheduleId)),
|
||||
eligibleDrivers: (scheduleId: string, fromYardId: string, toYardId: string) =>
|
||||
apiClient.get<TrainCrewMember[]>(
|
||||
`${base(scheduleId)}/eligible-drivers?fromYardId=${fromYardId}&toYardId=${toYardId}`,
|
||||
),
|
||||
corridorYards: (scheduleId: string) =>
|
||||
apiClient.get<CorridorYard[]>(`${base(scheduleId)}/corridor-yards`),
|
||||
save: (scheduleId: string, payload: SaveCrewPayload) =>
|
||||
apiClient.put<CrewValidation>(base(scheduleId), payload),
|
||||
};
|
||||
|
||||
export const DUTY_ROLE_OPTIONS: Array<{ value: CrewDutyRole; label: string }> = [
|
||||
{ value: 'PRIMARY', label: 'Primary Driver' },
|
||||
{ value: 'ASSISTANT', label: 'Assistant Driver' },
|
||||
{ value: 'BENCH_RELIEF', label: 'Bench/Relief Driver' },
|
||||
];
|
||||
|
||||
export const dutyRoleLabel = (dutyRole: CrewDutyRole): string =>
|
||||
({
|
||||
PRIMARY: 'Primary Driver',
|
||||
ASSISTANT: 'Assistant Driver',
|
||||
BENCH_RELIEF: 'Bench/Relief Driver',
|
||||
})[dutyRole];
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
MarshallingStop,
|
||||
ScheduleMergePreview,
|
||||
TrainScheduleDetail,
|
||||
ReduceScheduleCloseOffsetPayload,
|
||||
UpdateScheduleWindowRulePayload,
|
||||
TrainScheduleFilters,
|
||||
TrainScheduleListFilters,
|
||||
@@ -307,6 +308,17 @@ export const trainSchedulingService = {
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
reduceScheduleCloseOffset: async (
|
||||
scheduleId: string,
|
||||
payload: ReduceScheduleCloseOffsetPayload,
|
||||
): Promise<TrainScheduleDetail> => {
|
||||
const response = await client.patch<TrainScheduleDetail>(
|
||||
URL_CONSTANTS.TRAIN_SCHEDULING.CLOSE_OFFSET(scheduleId),
|
||||
payload,
|
||||
);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
updateScheduleDate: async (
|
||||
scheduleId: string,
|
||||
scheduleDate: string,
|
||||
|
||||
Reference in New Issue
Block a user