add company stamp upload functionality for contract signing

- Introduced StampUpload component for uploading company stamp images.
- Integrated stamp upload in contract signing modal, supporting PNG and JPG formats.
- Implemented validation for file type and size (max 5 MB).
- Added visual feedback for drag-and-drop functionality.
- Updated contract-related pages to handle duplicate contract alerts and pricing notices.
- Enhanced contract expiry management with a nightly sweep service.
- Added unit tests for new features and updated existing tests for contract handling.
This commit is contained in:
Marshal
2026-07-25 17:14:58 +00:00
parent 54b5882355
commit fde5e6de4b
68 changed files with 1858 additions and 289 deletions

View File

@@ -57,6 +57,7 @@ import type {
EligibleContainerBookingsResponse,
FreightType,
ImportLoadingBookingsResponse,
DocReviewAlert,
LoadingStatus,
LocomotiveRecord,
PinWagonsPayload,
@@ -276,6 +277,13 @@ export const api = {
() => ["train-scheduling", "all-booking-windows"],
),
docReviewAlert: endpoint<void, DocReviewAlert | null>(
"train-scheduling",
"doc-review-alert",
() => trainSchedulingService.getDocReviewAlert(),
() => ["train-scheduling", "doc-review-alert"],
),
batchBoardDetail: endpoint<
{ scheduleId: string },
BatchBoardScheduleDetail
@@ -1660,15 +1668,6 @@ export const api = {
() => [["wagons"]],
),
reorder: endpoint<{ trainId: string; wagonIds: string[] }, Wagon[]>(
"wagons",
"reorder",
({ trainId, wagonIds }) =>
wagonService.reorder(trainId, wagonIds).then((r) => r.data),
undefined,
() => [["wagons"]],
),
create: endpoint<Partial<Wagon>, Wagon>(
"wagons",
"create",

View File

@@ -109,6 +109,7 @@ export interface ContractView {
signerDisplayName: string;
signedAt: string;
signatureImageUrl?: string | null;
stampImageUrl?: string | null;
}>;
savedSignature?: {
signerDisplayName: string;
@@ -119,6 +120,8 @@ export interface ContractView {
export interface SignContractPayload {
role: "CUSTOMER" | "STAFF";
signatureImageBase64: string;
/** Company stamp/seal image; required to sign a contract. */
stampImageBase64?: string;
signerDisplayName: string;
consentText?: string;
}

View File

@@ -35,9 +35,6 @@ export interface Locomotive {
overageToleranceTons?: number | null;
/** Metres a train may exceed maxTrainLengthMeters by before scheduling blocks it. */
overageToleranceMeters?: number | null;
powerKw?: number | null;
tractionForceKn?: number | null;
maxSpeedKmh?: number | null;
createdAt: string;
updatedAt: string;
}

View File

@@ -12,6 +12,7 @@ import type {
BookingLoadResult,
BookingUnloadResult,
CompositionRemovalEntry,
DocReviewAlert,
UnassignedBookingsResponse,
CreateTrainSchedulePayload,
EligibleContainerBookingsResponse,
@@ -709,6 +710,15 @@ export const trainSchedulingService = {
return unwrap(response.data);
},
getDocReviewAlert: async (): Promise<DocReviewAlert | null> => {
const response = await client.get<DocReviewAlert | null>(
URL_CONSTANTS.TRAIN_SCHEDULING.DOC_REVIEW_ALERT,
);
// "No alert" comes back as null — which Nest sends as an empty body, so
// coerce anything falsy to null (react-query rejects undefined).
return unwrap(response.data) || null;
},
updateGlobalRules: async (
payload: Partial<Omit<TrainSchedulingGlobalRules, "id">>,
): Promise<TrainSchedulingGlobalRules> => {

View File

@@ -88,8 +88,6 @@ export const wagonService = {
assignToTrain: (wagonId: string, trainId: string, sequenceNumber?: number) =>
apiClient.post(`/wagons/${wagonId}/assign-train`, { trainId, sequenceNumber }),
unassign: (wagonId: string) => apiClient.post(`/wagons/${wagonId}/unassign-train`),
reorder: (trainId: string, wagonIds: string[]) =>
apiClient.post(`/trains/${trainId}/reorder-wagons`, { wagonIds }),
create: (data: Partial<Wagon>) => apiClient.post('/wagons', data),
update: (id: string, data: Partial<Wagon>) => apiClient.patch(`/wagons/${id}`, data),
delete: (id: string) => apiClient.delete(`/wagons/${id}`),