mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix issue
This commit is contained in:
@@ -204,17 +204,27 @@ export class ContractsRepository extends BaseRepository<Contract> {
|
||||
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]));
|
||||
const rows: Array<{
|
||||
contract_id: string;
|
||||
current_phase: string | null;
|
||||
booking_id: string | null;
|
||||
booking_status: string | null;
|
||||
}> = await this.dataSource.query(
|
||||
`SELECT DISTINCT ON (ccc.contract_id)
|
||||
ccc.contract_id, ccc.current_phase,
|
||||
b.id AS booking_id, b.status AS booking_status
|
||||
FROM freight.contract_clearance_cycles ccc
|
||||
LEFT JOIN freight.bookings b ON b.id = ccc.booking_id
|
||||
WHERE ccc.contract_id = ANY($1)
|
||||
ORDER BY ccc.contract_id, ccc.cycle_number DESC`,
|
||||
[ids],
|
||||
);
|
||||
const byContract = new Map(rows.map((r) => [r.contract_id, r]));
|
||||
for (const contract of contracts) {
|
||||
contract.clearancePhase = byContract.get(contract.id) ?? null;
|
||||
const row = byContract.get(contract.id);
|
||||
contract.clearancePhase = row?.current_phase ?? null;
|
||||
contract.latestCycleBookingId = row?.booking_id ?? null;
|
||||
contract.latestCycleBookingStatus = row?.booking_status ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -312,6 +312,14 @@ export class Contract extends BaseEntity {
|
||||
*/
|
||||
clearancePhase?: string | null;
|
||||
|
||||
/**
|
||||
* Latest clearance cycle's linked booking (id + status), attached alongside
|
||||
* clearancePhase. Lets the GL queue tell an expired (unpaid) booking apart
|
||||
* from a live one so it can offer a rebook. Not columns.
|
||||
*/
|
||||
latestCycleBookingId?: string | null;
|
||||
latestCycleBookingStatus?: string | null;
|
||||
|
||||
/**
|
||||
* Body of the most recent CHANGES_REQUESTED review note, attached by
|
||||
* ContractsService.findById so the portal can show the customer what staff
|
||||
|
||||
@@ -13,3 +13,6 @@ export function fileViewUrl(fileId: string, download = false): string {
|
||||
const base = `${API_BASE_URL}/api/files/${fileId}`;
|
||||
return download ? `${base}?download=1` : base;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Alert,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Grid,
|
||||
Group,
|
||||
Loader,
|
||||
@@ -21,10 +22,18 @@ import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
PackageCheck,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import {
|
||||
FREIGHT_PERMS,
|
||||
hasPermission,
|
||||
isDjiboutiGl,
|
||||
} from "@/lib/permissions";
|
||||
|
||||
import { ClearanceOpsTabs } from "@/components/contracts/ClearanceOpsTabs";
|
||||
import { PageContainer } from "@/components/page/PageContainer";
|
||||
import { PageHeader } from "@/components/page/PageHeader";
|
||||
@@ -44,6 +53,7 @@ import {
|
||||
export default function ContractClearanceDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { view, viewer } = useFileViewer();
|
||||
const { user } = useAuth();
|
||||
|
||||
const { data: contract, refetch: refetchContract } = useContractDetail(id);
|
||||
const {
|
||||
@@ -106,6 +116,16 @@ export default function ContractClearanceDetailPage() {
|
||||
const reviewReadOnly = shipmentLocked;
|
||||
const queriesLocked = Boolean(clearance?.preClearanceFinalized);
|
||||
const bookingHref = `/dashboard/contracts/${id}/create-booking`;
|
||||
// The GL-created booking expired unpaid — the slot is free again and GL
|
||||
// rebooks on the customer's behalf (customs bookings are never self-booked).
|
||||
const bookingExpired = clearance?.linkedBookingStatus === "EXPIRED";
|
||||
const canRebook =
|
||||
bookingExpired &&
|
||||
hasPermission(user, FREIGHT_PERMS.contracts.createBooking) &&
|
||||
!isDjiboutiGl(user);
|
||||
const rebookHref = linkedBookingId
|
||||
? `${bookingHref}?copyFrom=${linkedBookingId}`
|
||||
: bookingHref;
|
||||
|
||||
const { data: bookingMilestones, refetch: refetchBookingMilestones } =
|
||||
useBookingMilestones(linkedBookingId);
|
||||
@@ -158,7 +178,16 @@ export default function ContractClearanceDetailPage() {
|
||||
{ label: reference },
|
||||
]}
|
||||
meta={
|
||||
bookingAlreadyCreated ? (
|
||||
bookingExpired ? (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="orange"
|
||||
radius="sm"
|
||||
leftSection={<RefreshCw size={13} />}
|
||||
>
|
||||
Payment expired — rebook
|
||||
</Badge>
|
||||
) : bookingAlreadyCreated ? (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="blue"
|
||||
@@ -204,7 +233,49 @@ export default function ContractClearanceDetailPage() {
|
||||
it can actually create the booking without checking the schedule board. */}
|
||||
{id ? <GlUpcomingWindowsSection contractId={id} /> : null}
|
||||
|
||||
{bookingAlreadyCreated ? (
|
||||
{bookingExpired ? (
|
||||
<Alert
|
||||
color="orange"
|
||||
radius="md"
|
||||
icon={<RefreshCw size={16} />}
|
||||
title="Booking expired — payment not received"
|
||||
>
|
||||
<Stack gap="sm" align="flex-start">
|
||||
<Text size="sm">
|
||||
The customer did not pay before the deadline, so the booking
|
||||
expired and its train slot was released. The contract slot is
|
||||
free again — GL Ethiopia can rebook on the customer's
|
||||
behalf without re-running clearance.
|
||||
{linkedBookingId ? (
|
||||
<>
|
||||
{" "}
|
||||
<Text
|
||||
component={Link}
|
||||
to={`/dashboard/bookings/${linkedBookingId}/clearance`}
|
||||
inherit
|
||||
fw={600}
|
||||
c="orange.8"
|
||||
>
|
||||
View expired booking →
|
||||
</Text>
|
||||
</>
|
||||
) : null}
|
||||
</Text>
|
||||
{canRebook ? (
|
||||
<Button
|
||||
component={Link}
|
||||
to={rebookHref}
|
||||
color="grape"
|
||||
radius="md"
|
||||
size="sm"
|
||||
leftSection={<RefreshCw size={15} />}
|
||||
>
|
||||
Rebook for customer
|
||||
</Button>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Alert>
|
||||
) : bookingAlreadyCreated ? (
|
||||
<Alert
|
||||
color="blue"
|
||||
radius="md"
|
||||
|
||||
@@ -92,6 +92,10 @@ interface ClearanceRow {
|
||||
ready: boolean;
|
||||
/** true once GL Ethiopia created the shipment booking. */
|
||||
bookingCreated: boolean;
|
||||
/** true when the created booking EXPIRED unpaid — GL must rebook. */
|
||||
paymentExpired: boolean;
|
||||
/** The expired booking, so rebook can copy its cargo. */
|
||||
expiredBookingId: string | null;
|
||||
}
|
||||
|
||||
function yardLabel(
|
||||
@@ -145,6 +149,13 @@ function toClearanceRow(contract: Freight.IContract): ClearanceRow {
|
||||
status: contract.status,
|
||||
ready: contract.status === "CLEARANCE_READY_FOR_BOOKING",
|
||||
bookingCreated: contract.status === "ACTIVE_SHIPMENT_IN_PROGRESS",
|
||||
paymentExpired:
|
||||
contract.status === "ACTIVE_SHIPMENT_IN_PROGRESS" &&
|
||||
contract.latestCycleBookingStatus === "EXPIRED",
|
||||
expiredBookingId:
|
||||
contract.latestCycleBookingStatus === "EXPIRED"
|
||||
? (contract.latestCycleBookingId ?? null)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -186,6 +197,24 @@ function DirectionIcon({ direction }: { direction: string }) {
|
||||
}
|
||||
|
||||
function StatusBadge({ row }: { row: ClearanceRow }) {
|
||||
if (row.paymentExpired) {
|
||||
return (
|
||||
<Tooltip
|
||||
label="The customer did not pay in time — the booking expired. GL rebooks on the customer's behalf."
|
||||
withArrow
|
||||
>
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
color="orange"
|
||||
radius="sm"
|
||||
leftSection={<RefreshCw size={12} />}
|
||||
>
|
||||
Payment expired
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
if (row.bookingCreated) {
|
||||
return (
|
||||
<Tooltip label="GL Ethiopia created the shipment booking" withArrow>
|
||||
@@ -519,6 +548,27 @@ export default function ContractClearanceListPage() {
|
||||
Create booking
|
||||
</Button>
|
||||
</Group>
|
||||
) : row.original.paymentExpired && canCreateBooking ? (
|
||||
<Group justify="flex-end" pr="xs">
|
||||
<Button
|
||||
size="compact-sm"
|
||||
color="grape"
|
||||
radius="md"
|
||||
leftSection={<RefreshCw size={14} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(
|
||||
`/dashboard/contracts/${row.original.id}/create-booking${
|
||||
row.original.expiredBookingId
|
||||
? `?copyFrom=${row.original.expiredBookingId}`
|
||||
: ""
|
||||
}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Rebook
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
<Group justify="flex-end" pr="xs">
|
||||
<ChevronRight size={16} className="text-muted-foreground" />
|
||||
|
||||
@@ -620,6 +620,12 @@ export interface IContract extends BaseEntity {
|
||||
* clearance view per contract.
|
||||
*/
|
||||
clearancePhase?: ContractDocPhase | string | null;
|
||||
/**
|
||||
* Latest clearance cycle's linked booking id + status (list responses only).
|
||||
* An EXPIRED status means the customer never paid — GL may rebook.
|
||||
*/
|
||||
latestCycleBookingId?: string | null;
|
||||
latestCycleBookingStatus?: string | null;
|
||||
|
||||
pricingBreakdown?: ContractPricingBreakdown | null;
|
||||
pricingDisplayMode?: "UNIT_RATES";
|
||||
|
||||
Reference in New Issue
Block a user