mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge branch 'dev' of https://github.com/Tria-plc/edr-platform into alpha
This commit is contained in:
@@ -848,8 +848,10 @@ export class ContractClearanceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GL ET clearance hub: every customs (Path B) contract in phased clearance,
|
* GL ET clearance hub, Contracts tab: ONE_TIME customs (Path B) contracts in
|
||||||
* including after booking is created.
|
* phased clearance that already carry at least one uploaded clearance
|
||||||
|
* document — a contract still waiting for its first document has nothing to
|
||||||
|
* review, and GENERAL contracts clear per booking, not at contract level.
|
||||||
*/
|
*/
|
||||||
async queue(filter: FilterContractDto): Promise<PaginatedContracts> {
|
async queue(filter: FilterContractDto): Promise<PaginatedContracts> {
|
||||||
return this.contractsRepository.findAllPaginated({
|
return this.contractsRepository.findAllPaginated({
|
||||||
@@ -857,6 +859,8 @@ export class ContractClearanceService {
|
|||||||
pageSize: filter.pageSize ?? 100,
|
pageSize: filter.pageSize ?? 100,
|
||||||
statuses: [...PHASED_CUSTOMS_CONTRACT_QUEUE_STATUSES],
|
statuses: [...PHASED_CUSTOMS_CONTRACT_QUEUE_STATUSES],
|
||||||
customsClearingEnabled: true,
|
customsClearingEnabled: true,
|
||||||
|
contractKind: 'ONE_TIME',
|
||||||
|
hasClearanceDocuments: true,
|
||||||
sortBy: filter.sortBy,
|
sortBy: filter.sortBy,
|
||||||
sortOrder: filter.sortOrder,
|
sortOrder: filter.sortOrder,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export interface ContractListFilterOptions {
|
|||||||
tradeDirection?: string;
|
tradeDirection?: string;
|
||||||
paymentCurrency?: string;
|
paymentCurrency?: string;
|
||||||
customsClearingEnabled?: boolean;
|
customsClearingEnabled?: boolean;
|
||||||
|
/** true → only contracts with at least one uploaded clearance document. */
|
||||||
|
hasClearanceDocuments?: boolean;
|
||||||
createdFrom?: string;
|
createdFrom?: string;
|
||||||
createdTo?: string;
|
createdTo?: string;
|
||||||
}
|
}
|
||||||
@@ -284,6 +286,12 @@ export class ContractsRepository extends BaseRepository<Contract> {
|
|||||||
customsClearingEnabled: options.customsClearingEnabled,
|
customsClearingEnabled: options.customsClearingEnabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (options.hasClearanceDocuments) {
|
||||||
|
qb.andWhere(
|
||||||
|
'EXISTS (SELECT 1 FROM freight.contract_document_review cdr ' +
|
||||||
|
'WHERE cdr.contract_id = contract.id AND cdr.deleted_at IS NULL)',
|
||||||
|
);
|
||||||
|
}
|
||||||
if (options.serviceTypeId) {
|
if (options.serviceTypeId) {
|
||||||
qb.andWhere('contract.service_type_id = :serviceTypeId', {
|
qb.andWhere('contract.service_type_id = :serviceTypeId', {
|
||||||
serviceTypeId: options.serviceTypeId,
|
serviceTypeId: options.serviceTypeId,
|
||||||
|
|||||||
@@ -1136,14 +1136,35 @@ export class TrainSchedulingService {
|
|||||||
throw new BadRequestException('Schedule has no train set');
|
throw new BadRequestException('Schedule has no train set');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch parity: a schedule may only allocate bookings that targeted it. This mirrors
|
// Batch parity: a schedule may only allocate bookings from its route-day POOL.
|
||||||
// the automatic fill, which only pulls bookings whose train_schedule_id is this schedule.
|
// Under day-level pooling (see fillRouteDayInternal) an unreserved booking has
|
||||||
|
// a NULL train_schedule_id and is only pinned by reserve(); a reserved one is
|
||||||
|
// pinned to whichever train in the day's group first held it. Every train
|
||||||
|
// sharing this origin + destination + EAT departure day draws from ONE shared
|
||||||
|
// pool (one shared booking window), so a booking is allocatable here when it is
|
||||||
|
// either unpinned (NULL) or pinned to THIS train or a GROUP SIBLING. A booking
|
||||||
|
// pinned to a train on a DIFFERENT route/day is a real stray. Genuine route/
|
||||||
|
// day/capacity fit is enforced downstream by validateBookingsForScheduling.
|
||||||
|
// EXPORT never groups, so its pool is this schedule alone (plus NULL pool).
|
||||||
if (dto.bookingIds.length) {
|
if (dto.bookingIds.length) {
|
||||||
|
const groupScheduleIds = new Set<string>([scheduleId]);
|
||||||
|
if (schedule.direction !== 'EXPORT') {
|
||||||
|
const siblings = await this.findGroupSiblings(
|
||||||
|
this.dataSource.manager,
|
||||||
|
schedule.originStationId,
|
||||||
|
schedule.destinationStationId,
|
||||||
|
schedule.scheduledDepartureDate,
|
||||||
|
scheduleId,
|
||||||
|
);
|
||||||
|
for (const sib of siblings) groupScheduleIds.add(sib.id);
|
||||||
|
}
|
||||||
const targeted = await this.bookingsRepository.findByIdsForScheduling(dto.bookingIds);
|
const targeted = await this.bookingsRepository.findByIdsForScheduling(dto.bookingIds);
|
||||||
const stray = targeted.filter((b) => b.trainScheduleId !== scheduleId);
|
const stray = targeted.filter(
|
||||||
|
(b) => b.trainScheduleId != null && !groupScheduleIds.has(b.trainScheduleId),
|
||||||
|
);
|
||||||
if (stray.length) {
|
if (stray.length) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`These bookings are not assigned to this schedule: ${stray
|
`These bookings are pinned to a train on a different route or day: ${stray
|
||||||
.map((b) => b.reference ?? b.id)
|
.map((b) => b.reference ?? b.id)
|
||||||
.join(', ')}`,
|
.join(', ')}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -149,8 +149,12 @@ export function ExportClearanceStepper({
|
|||||||
const entityId = contractId ?? bookingId ?? "";
|
const entityId = contractId ?? bookingId ?? "";
|
||||||
// The booking that carries the post-booking steps (gate pass, T1, invoice).
|
// The booking that carries the post-booking steps (gate pass, T1, invoice).
|
||||||
const actionBookingId = clearance.linkedBookingId ?? bookingId ?? null;
|
const actionBookingId = clearance.linkedBookingId ?? bookingId ?? null;
|
||||||
|
// Per-booking GENERAL clearance runs on a bare instance that only becomes a
|
||||||
|
// real booking once GL completes it — the caller's bookingCreated prop carries
|
||||||
|
// that signal, so a booking-keyed view must NOT count as "created" by itself
|
||||||
|
// (it would lock GL Ethiopia out of the declaration step right after the RO).
|
||||||
const effectiveBookingCreated =
|
const effectiveBookingCreated =
|
||||||
bookingCreated || Boolean(clearance.linkedBookingId) || isBooking;
|
bookingCreated || Boolean(clearance.linkedBookingId);
|
||||||
|
|
||||||
const activeStep = useMemo(
|
const activeStep = useMemo(
|
||||||
() => computeExportActiveStep(clearance, bookingMilestones, effectiveBookingCreated),
|
() => computeExportActiveStep(clearance, bookingMilestones, effectiveBookingCreated),
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
// stepper's "Create booking" step must read as NOT-yet-created
|
// stepper's "Create booking" step must read as NOT-yet-created
|
||||||
// so it never claims the booking is done before GL completes it.
|
// so it never claims the booking is done before GL completes it.
|
||||||
bookingCreated={Number(booking?.totalAmount ?? 0) > 0}
|
bookingCreated={Number(booking?.totalAmount ?? 0) > 0}
|
||||||
|
bookingMilestones={bookingMilestones ?? []}
|
||||||
onChanged={() => void refetch()}
|
onChanged={() => void refetch()}
|
||||||
onViewFile={view}
|
onViewFile={view}
|
||||||
onDownloadFile={(f) => void downloadBookingFile(f.id, f.name)}
|
onDownloadFile={(f) => void downloadBookingFile(f.id, f.name)}
|
||||||
|
|||||||
Reference in New Issue
Block a user