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

@@ -548,6 +548,26 @@ export class TrainBuilderService {
return rows.length > 0;
}
/** Batched form of {@link isWagonPinnedToLiveSchedule} for a whole consist. */
private async isAnyWagonPinnedToLiveSchedule(
manager: EntityManager,
wagonIds: string[],
): Promise<boolean> {
if (!wagonIds.length) return false;
const rows: { exists: boolean }[] = await manager.query(
`SELECT TRUE AS exists
FROM freight.train_set_wagons tsw
JOIN freight.train_schedules ts ON ts.train_set_id = tsw.train_set_id
WHERE tsw.physical_wagon_id = ANY($1::uuid[])
AND ts.status IN ('DRAFT', 'SCHEDULED', 'DISPATCHED')
AND ts.deleted_at IS NULL
AND tsw.deleted_at IS NULL
LIMIT 1`,
[wagonIds],
);
return rows.length > 0;
}
/** Persist a drag-reorder: `wagonIds` is the full consist in its new order. */
async reorderWagons(id: string, dto: ReorderTrainWagonsDto) {
await this.dataSource.transaction(async (manager) => {
@@ -560,6 +580,17 @@ export class TrainBuilderService {
if (current.size !== incoming.size || [...current].some((wid) => !incoming.has(wid))) {
throw new BadRequestException('Reorder must include every wagon of the train exactly once');
}
// A live schedule (DRAFT/SCHEDULED/DISPATCHED) reads each wagon's slot at
// its OWN frozen sequenceNo, never the wagon's live sequenceNumber — so
// renumbering here would silently desync that schedule's drawn consist
// from the built train's real order (loaded slots keep the old order,
// empty ones show the new one). Same guard as remove/maintenance.
if (await this.isAnyWagonPinnedToLiveSchedule(manager, [...current])) {
throw new ConflictException(
'This train has wagons pinned to an active schedule and cannot be reordered — ' +
"it would desync the schedule's consist view from the built train's real order.",
);
}
for (let i = 0; i < dto.wagonIds.length; i++) {
await manager.getRepository(Wagon).update(dto.wagonIds[i], { sequenceNumber: i + 1 });
}