:Merge branch 'dev' of github.com:Tria-plc/edr-platform into uienhancement

Please enter a commit message to explain why this merge is necessary,
:qa!
This commit is contained in:
Hagernesh
2026-07-13 09:57:21 +00:00
631 changed files with 7373 additions and 156702 deletions

View File

@@ -605,6 +605,7 @@ export class BookingsRepository extends BaseRepository<Booking> {
async findAllPaginated(options: BookingListFilterOptions & {
page: number;
pageSize: number;
search?: string;
sortBy?: string;
sortOrder?: 'ASC' | 'DESC';
}): Promise<{
@@ -640,6 +641,16 @@ export class BookingsRepository extends BaseRepository<Booking> {
this.applyListFilters(qb, options);
// Free-text search spans joined columns (company, contract) that only this
// list query joins — so it lives here, not in applyListFilters (shared
// with getListSummaryMetrics, whose query builder has no joins).
if (options.search) {
qb.andWhere(
'(booking.reference ILIKE :search OR company.name ILIKE :search OR contract.reference ILIKE :search)',
{ search: `%${options.search}%` },
);
}
if (options.sortBy === 'isGovernment') {
qb.orderBy('booking.isGovernment', 'DESC')
.addOrderBy('booking.priorityScore', 'DESC')

View File

@@ -31,6 +31,7 @@ import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
import { Yard } from '../rule-engine/entities/yard.entity';
import { ServiceType } from '../rule-engine/entities/service-type.entity';
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
import { Contract } from '../contracts/entities/contract.entity';
import { BookingsRepository } from './bookings.repository';
import { ConsolidationService } from './consolidation.service';
import { VehiclesService } from '../vehicles/vehicles.service';
@@ -1211,6 +1212,13 @@ export class BookingsService {
destinationYardId: filter.destinationYardId,
isGovernment: filter.isGovernment,
consolidationPaired: filter.consolidationPaired,
// DTO carries 'true'/'false' strings (query params); the repo option is a
// real boolean — convert, preserving "not filtered" when absent.
customsClearingEnabled:
filter.customsClearingEnabled === undefined
? undefined
: filter.customsClearingEnabled === 'true',
search: filter.search,
sortBy: filter.sortBy,
sortOrder: filter.sortOrder,
});
@@ -1262,6 +1270,7 @@ export class BookingsService {
// Global Logistics only clears customs bookings; non-customs clearance is
// reviewed by Marketing from the booking detail, not this queue.
customsClearingEnabled: true,
search: filter.search,
sortBy: filter.sortBy,
sortOrder: filter.sortOrder,
});
@@ -1286,6 +1295,7 @@ export class BookingsService {
// Company-wide: payables span all of the customer's services.
companyId: company.id,
companyProfileId: filter.companyProfileId,
search: filter.search,
sortBy: filter.sortBy,
sortOrder: filter.sortOrder,
});
@@ -1473,6 +1483,18 @@ export class BookingsService {
);
}
// Surface the parent contract's reference for drawdown bookings — the
// portal detail header shows it (the entity has no contract relation, so
// the list attaches it via a raw join and the detail attaches it here).
if (booking.contractId) {
const contract = await this.dataSource.getRepository(Contract).findOne({
where: { id: booking.contractId },
select: { reference: true },
});
(booking as Booking & { contractReference?: string | null }).contractReference =
contract?.reference ?? null;
}
// Surface the assigned train's operational status so the portal stepper
// can show the Arrival stage: the booking status stays IN_TRANSIT from
// dispatch until delivery, so arrival is only knowable from the schedule.

View File

@@ -106,6 +106,14 @@ export class FilterBookingDto {
@IsIn(['true', 'false'])
isGovernment?: 'true' | 'false';
@ApiPropertyOptional({
enum: ['true', 'false'],
description: 'Filter customs vs self-clearance (non-customs) bookings',
})
@IsOptional()
@IsIn(['true', 'false'])
customsClearingEnabled?: 'true' | 'false';
@ApiPropertyOptional({ enum: TRADE_DIRECTIONS })
@IsOptional()
@IsIn([...TRADE_DIRECTIONS])
@@ -125,6 +133,16 @@ export class FilterBookingDto {
@IsOptional()
consolidationPaired?: string;
@ApiPropertyOptional({
description:
'Free-text search across booking reference, company name, and contract reference.',
})
@IsOptional()
@Transform(({ value }) =>
typeof value === 'string' && value.trim() ? value.trim() : undefined,
)
search?: string;
@ApiPropertyOptional({ default: 1 })
@IsOptional()
@Transform(({ value }) => (value ? parseInt(value, 10) : 1))

View File

@@ -166,6 +166,24 @@ export class Booking extends BaseEntity {
@Column({ name: 'contract_kind', type: 'varchar', length: 20, nullable: true })
contractKind?: string | null;
/**
* The customer paid a partial batch offer and this booking was reduced to the
* offered part (see BookingSplitService.applySplit). On a ONE_TIME contract a
* split booking releases the single-active-booking slot for the remainder —
* the contract kind itself is never changed.
*/
@Column({ name: 'is_split', type: 'boolean', default: false })
isSplit!: boolean;
/**
* Quantities this booking carried BEFORE it was reduced by a split — the
* split chain's source of truth for the outstanding remainder (ONE_TIME
* contracts have no quantity cap to derive it from). Bulk: total tons;
* container: units per size. Null until the booking is split.
*/
@Column({ name: 'pre_split_quantities', type: 'jsonb', nullable: true })
preSplitQuantities?: { bulkTons?: number; bySize?: Record<string, number> } | null;
/** Who created this booking: CUSTOMER (Path A), GL_ET (Path B), or STAFF. */
@Column({ name: 'created_by_role', type: 'varchar', length: 20, default: 'CUSTOMER', nullable: true })
createdByRole?: string | null;