mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 17:23:38 +00:00
Merge pull request #738 from Tria-plc/freight_feature/usermanagement
fix issue
This commit is contained in:
@@ -204,17 +204,27 @@ export class ContractsRepository extends BaseRepository<Contract> {
|
|||||||
private async attachClearancePhases(contracts: Contract[]): Promise<void> {
|
private async attachClearancePhases(contracts: Contract[]): Promise<void> {
|
||||||
if (contracts.length === 0) return;
|
if (contracts.length === 0) return;
|
||||||
const ids = contracts.map((c) => c.id);
|
const ids = contracts.map((c) => c.id);
|
||||||
const rows: Array<{ contract_id: string; current_phase: string | null }> =
|
const rows: Array<{
|
||||||
await this.dataSource.query(
|
contract_id: string;
|
||||||
`SELECT DISTINCT ON (contract_id) contract_id, current_phase
|
current_phase: string | null;
|
||||||
FROM freight.contract_clearance_cycles
|
booking_id: string | null;
|
||||||
WHERE contract_id = ANY($1)
|
booking_status: string | null;
|
||||||
ORDER BY contract_id, cycle_number DESC`,
|
}> = await this.dataSource.query(
|
||||||
[ids],
|
`SELECT DISTINCT ON (ccc.contract_id)
|
||||||
);
|
ccc.contract_id, ccc.current_phase,
|
||||||
const byContract = new Map(rows.map((r) => [r.contract_id, r.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) {
|
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;
|
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
|
* Body of the most recent CHANGES_REQUESTED review note, attached by
|
||||||
* ContractsService.findById so the portal can show the customer what staff
|
* 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}`;
|
const base = `${API_BASE_URL}/api/files/${fileId}`;
|
||||||
return download ? `${base}?download=1` : base;
|
return download ? `${base}?download=1` : base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
@@ -21,10 +22,18 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock,
|
Clock,
|
||||||
PackageCheck,
|
PackageCheck,
|
||||||
|
RefreshCw,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Link } from "react-router-dom";
|
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 { ClearanceOpsTabs } from "@/components/contracts/ClearanceOpsTabs";
|
||||||
import { PageContainer } from "@/components/page/PageContainer";
|
import { PageContainer } from "@/components/page/PageContainer";
|
||||||
import { PageHeader } from "@/components/page/PageHeader";
|
import { PageHeader } from "@/components/page/PageHeader";
|
||||||
@@ -44,6 +53,7 @@ import {
|
|||||||
export default function ContractClearanceDetailPage() {
|
export default function ContractClearanceDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const { view, viewer } = useFileViewer();
|
const { view, viewer } = useFileViewer();
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
const { data: contract, refetch: refetchContract } = useContractDetail(id);
|
const { data: contract, refetch: refetchContract } = useContractDetail(id);
|
||||||
const {
|
const {
|
||||||
@@ -106,6 +116,16 @@ export default function ContractClearanceDetailPage() {
|
|||||||
const reviewReadOnly = shipmentLocked;
|
const reviewReadOnly = shipmentLocked;
|
||||||
const queriesLocked = Boolean(clearance?.preClearanceFinalized);
|
const queriesLocked = Boolean(clearance?.preClearanceFinalized);
|
||||||
const bookingHref = `/dashboard/contracts/${id}/create-booking`;
|
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 } =
|
const { data: bookingMilestones, refetch: refetchBookingMilestones } =
|
||||||
useBookingMilestones(linkedBookingId);
|
useBookingMilestones(linkedBookingId);
|
||||||
@@ -165,7 +185,16 @@ export default function ContractClearanceDetailPage() {
|
|||||||
{ label: reference },
|
{ label: reference },
|
||||||
]}
|
]}
|
||||||
meta={
|
meta={
|
||||||
bookingAlreadyCreated ? (
|
bookingExpired ? (
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
color="orange"
|
||||||
|
radius="sm"
|
||||||
|
leftSection={<RefreshCw size={13} />}
|
||||||
|
>
|
||||||
|
Payment expired — rebook
|
||||||
|
</Badge>
|
||||||
|
) : bookingAlreadyCreated ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
@@ -211,7 +240,49 @@ export default function ContractClearanceDetailPage() {
|
|||||||
it can actually create the booking without checking the schedule board. */}
|
it can actually create the booking without checking the schedule board. */}
|
||||||
{id ? <GlUpcomingWindowsSection contractId={id} /> : null}
|
{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
|
<Alert
|
||||||
color="blue"
|
color="blue"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -92,6 +92,10 @@ interface ClearanceRow {
|
|||||||
ready: boolean;
|
ready: boolean;
|
||||||
/** true once GL Ethiopia created the shipment booking. */
|
/** true once GL Ethiopia created the shipment booking. */
|
||||||
bookingCreated: boolean;
|
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(
|
function yardLabel(
|
||||||
@@ -145,6 +149,13 @@ function toClearanceRow(contract: Freight.IContract): ClearanceRow {
|
|||||||
status: contract.status,
|
status: contract.status,
|
||||||
ready: contract.status === "CLEARANCE_READY_FOR_BOOKING",
|
ready: contract.status === "CLEARANCE_READY_FOR_BOOKING",
|
||||||
bookingCreated: contract.status === "ACTIVE_SHIPMENT_IN_PROGRESS",
|
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 }) {
|
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) {
|
if (row.bookingCreated) {
|
||||||
return (
|
return (
|
||||||
<Tooltip label="GL Ethiopia created the shipment booking" withArrow>
|
<Tooltip label="GL Ethiopia created the shipment booking" withArrow>
|
||||||
@@ -519,6 +548,27 @@ export default function ContractClearanceListPage() {
|
|||||||
Create booking
|
Create booking
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</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">
|
<Group justify="flex-end" pr="xs">
|
||||||
<ChevronRight size={16} className="text-muted-foreground" />
|
<ChevronRight size={16} className="text-muted-foreground" />
|
||||||
|
|||||||
@@ -620,6 +620,12 @@ export interface IContract extends BaseEntity {
|
|||||||
* clearance view per contract.
|
* clearance view per contract.
|
||||||
*/
|
*/
|
||||||
clearancePhase?: ContractDocPhase | string | null;
|
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;
|
pricingBreakdown?: ContractPricingBreakdown | null;
|
||||||
pricingDisplayMode?: "UNIT_RATES";
|
pricingDisplayMode?: "UNIT_RATES";
|
||||||
|
|||||||
Reference in New Issue
Block a user