feat: enhance contract clearance process with linked booking details

- Added  and  to  for better visibility of GL-created shipment bookings.
- Implemented  method in  to fetch the latest clearance phase for contracts, improving list responses.
- Introduced  property in the  entity to store the latest clearance cycle's phase.
- Updated  to surface linked booking information in the clearance view.
- Created  component to display detailed container information in booking details.
- Refactored booking actions to remove contract-related actions from the booking request page.
- Enhanced the  component to reflect the current phase of clearance actions.
- Updated UI components to provide clearer messaging regarding the status of clearance and linked bookings.
- Adjusted action handling in  to include duty payment actions.
- Improved the  to show hints for each phase of the clearance process.
This commit is contained in:
Marshal
2026-07-03 21:04:46 +00:00
parent c3f06b6aa9
commit 85c2fd1428
22 changed files with 537 additions and 204 deletions

View File

@@ -77,6 +77,9 @@ export interface ContractClearanceView {
/** Export post-booking clearance finalized (transit permit uploaded + GL confirmed). */
exportClearanceFinalized?: boolean;
linkedBookingId?: string | null;
/** Reference + status of the GL-created shipment booking, once it exists. */
linkedBookingReference?: string | null;
linkedBookingStatus?: string | null;
dutyAdvice?: {
amount: number;
currency: string;
@@ -284,13 +287,22 @@ export class ContractClearanceService {
);
let nextAction = this.workflowService.computeNextAction(contract, cycle, milestones);
if (cycle?.bookingId && contract.tradeDirection === 'EXPORT') {
// Once GL creates the shipment booking, surface its reference + status so the
// customer sees the concrete booking instead of a stale "will be created
// shortly" message. Reuse the export booking load; fetch for import too.
let linkedBookingReference: string | null = null;
let linkedBookingStatus: string | null = null;
if (cycle?.bookingId) {
const booking = await this.bookingsService.findById(cycle.bookingId);
if (booking) {
nextAction = this.workflowService.computeNextActionForBooking(
booking,
bookingMilestones,
);
linkedBookingReference = booking.reference ?? null;
linkedBookingStatus = booking.status ?? null;
if (contract.tradeDirection === 'EXPORT') {
nextAction = this.workflowService.computeNextActionForBooking(
booking,
bookingMilestones,
);
}
}
}
@@ -326,6 +338,8 @@ export class ContractClearanceService {
preClearanceFinalized: Boolean(cycle?.preClearanceFinalizedAt),
exportClearanceFinalized: Boolean(cycle?.completedAt),
linkedBookingId: cycle?.bookingId ?? null,
linkedBookingReference,
linkedBookingStatus,
dutyAdvice,
workflowFiles,
t1,

View File

@@ -135,6 +135,7 @@ export class ContractsRepository extends BaseRepository<Contract> {
// Attach the generated contract PDF to each row so list/home can offer a
// direct download. Loaded separately to keep pagination counts correct.
await this.attachContractFiles(items);
await this.attachClearancePhases(items);
const totalPages = pageSize > 0 ? Math.ceil(total / pageSize) : 0;
return {
@@ -173,6 +174,30 @@ export class ContractsRepository extends BaseRepository<Contract> {
}
}
/**
* Attach each contract's persisted clearance phase (latest cycle's
* current_phase) so list consumers can show step-accurate customer actions
* ("Pay duty & upload slip" vs generic "Update clearance") without a
* per-contract clearance-view request. One query per page, like
* `attachContractFiles`.
*/
private async attachClearancePhases(contracts: Contract[]): Promise<void> {
if (contracts.length === 0) return;
const ids = contracts.map((c) => c.id);
const rows: Array<{ contract_id: string; current_phase: string | null }> =
await this.dataSource.query(
`SELECT DISTINCT ON (contract_id) contract_id, current_phase
FROM freight.contract_clearance_cycles
WHERE contract_id = ANY($1)
ORDER BY contract_id, cycle_number DESC`,
[ids],
);
const byContract = new Map(rows.map((r) => [r.contract_id, r.current_phase]));
for (const contract of contracts) {
contract.clearancePhase = byContract.get(contract.id) ?? null;
}
}
async getStatusCounts(): Promise<Record<string, number>> {
const rows = await this.repository
.createQueryBuilder('contract')

View File

@@ -254,4 +254,10 @@ export class Contract extends BaseEntity {
createForeignKeyConstraints: false,
})
files?: FileRecord[];
/**
* Latest clearance cycle's current_phase, attached by
* ContractsRepository.attachClearancePhases for list responses. Not a column.
*/
clearancePhase?: string | null;
}