feat: add Transit Clearance Action Panel for managing delivery and release orders

- Implemented TransitClearanceActionPanel component for transit agents to handle DO/RO uploads and amendments.
- Added file picker for uploading documents with validation for vessel arrival and collection dates.
- Introduced modals for uploading T1 documents and requesting RO amendments.
- Enhanced transit assignments service with new API endpoints for clearance history, GL exchange documents, and incident reports.
- Updated types to include new document upload sources and statuses.
- Created CSS styles for transit bookings table to improve layout and responsiveness.
- Exported new TransitAgentBookingDetailPage for detailed booking views.
This commit is contained in:
Marshal
2026-08-31 13:56:39 +00:00
parent 678c5d7d49
commit b5ad46f317
23 changed files with 3524 additions and 385 deletions

View File

@@ -244,6 +244,14 @@ export class TransitAssignmentsService {
assignments: items.length,
open: items.filter((i) => i.status !== TransitAssignmentStatus.Finished)
.length,
// The open half split by status, so the roster's tab counts do not have
// to be derived from a single paginated page.
notStarted: items.filter(
(i) => i.status === TransitAssignmentStatus.NotStarted,
).length,
inProgress: items.filter(
(i) => i.status === TransitAssignmentStatus.InProgress,
).length,
finished: items.filter(
(i) => i.status === TransitAssignmentStatus.Finished,
).length,
@@ -393,6 +401,50 @@ export class TransitAssignmentsService {
return this.findMineById(userId, id);
}
/**
* Make `transitAgentId` the officer working `bookingId`, as the clearance
* desk's "assign transit assignee" step means it.
*
* The booking itself only records the officer's NAME, which is all the
* clearance UI needs; the officer's own portal reads `transit_assignments`.
* This keeps the two in step, and is deliberately forgiving where `create()`
* is strict:
* - assigning the same agent twice is a no-op, not a 409 — the desk may
* re-save the step without meaning to start over;
* - a REASSIGNMENT retires the previous officer's row, so a shipment does
* not sit in the work list of someone who no longer handles it. Finished
* rows stay, since they are that officer's record of work already done.
*/
async ensureAssignment(
bookingId: string,
transitAgentId: string,
assignedByUserId?: string,
): Promise<void> {
const existing =
await this.assignmentsRepository.findByBooking(bookingId);
for (const row of existing) {
if (
row.transitAgentId !== transitAgentId &&
row.status !== TransitAssignmentStatus.Finished
) {
await this.assignmentsRepository.softDelete(row.id);
}
}
if (existing.some((row) => row.transitAgentId === transitAgentId)) return;
await this.assignmentsRepository.create({
bookingId,
transitAgentId,
status: TransitAssignmentStatus.NotStarted,
startedAt: null,
finishedAt: null,
assignedByUserId: assignedByUserId ?? null,
note: null,
});
}
async create(
dto: CreateTransitAssignmentDto,
assignedByUserId?: string,