mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 14:15:44 +00:00
add contract link
This commit is contained in:
@@ -1,19 +1,12 @@
|
||||
import { useMyTradeAccess } from "@/hooks/useMyTradeAccess";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Modal,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { Box, Button, Card, Group, Modal, Stack, Text } from "@mantine/core";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowRight,
|
||||
Calendar,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
FileText,
|
||||
LayoutList,
|
||||
Link2,
|
||||
Package,
|
||||
@@ -23,13 +16,19 @@ import {
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { BookingActionsMenu } from "@/components/bookings/BookingActionsMenu";
|
||||
import { ExportButton } from "@/components/export/ExportButton";
|
||||
import { formatDate, humanize } from "@/lib/format";
|
||||
import { FilterBar, dateRangeParams, routeParams, useFilters, type FilterDef } from "@/components/filters";
|
||||
import {
|
||||
FilterBar,
|
||||
dateRangeParams,
|
||||
routeParams,
|
||||
useFilters,
|
||||
type FilterDef,
|
||||
} from "@/components/filters";
|
||||
import { BookingPriorityBadge } from "@/components/bookings/BookingPriorityBadge";
|
||||
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
|
||||
// BookingStatusTabs / Operations* queues removed — replaced by booking-kind tabs.
|
||||
@@ -150,36 +149,97 @@ export default function BookingRequestsPage() {
|
||||
// split), so a deep link can never land behind "More filters" unseen.
|
||||
const bookingFilterDefs: FilterDef[] = useMemo(
|
||||
() => [
|
||||
{ key: "customerKind", label: "Booked by", type: "enum", multiple: false, options: CUSTOMER_KIND_OPTIONS },
|
||||
{ key: "bookingType", label: "Kind", type: "enum", multiple: false, options: BOOKING_KIND_OPTIONS },
|
||||
{ key: "statuses", label: "Status", type: "enum", options: STATUS_OPTIONS },
|
||||
{
|
||||
key: "tradeDirection", label: "Direction", type: "enum", multiple: false,
|
||||
key: "customerKind",
|
||||
label: "Booked by",
|
||||
type: "enum",
|
||||
multiple: false,
|
||||
options: CUSTOMER_KIND_OPTIONS,
|
||||
},
|
||||
{
|
||||
key: "bookingType",
|
||||
label: "Kind",
|
||||
type: "enum",
|
||||
multiple: false,
|
||||
options: BOOKING_KIND_OPTIONS,
|
||||
},
|
||||
{
|
||||
key: "statuses",
|
||||
label: "Status",
|
||||
type: "enum",
|
||||
options: STATUS_OPTIONS,
|
||||
},
|
||||
{
|
||||
key: "tradeDirection",
|
||||
label: "Direction",
|
||||
type: "enum",
|
||||
multiple: false,
|
||||
options: filterOptions(TRADE_DIRECTION_OPTIONS),
|
||||
},
|
||||
{ key: "freightType", label: "Freight", type: "enum", multiple: false, options: FREIGHT_TYPE_OPTIONS },
|
||||
{ key: "serviceTypeId", label: "Service", type: "enum", multiple: false, options: serviceTypeOptions },
|
||||
{ key: "paymentStatus", label: "Payment", type: "enum", multiple: false, options: PAYMENT_STATUS_OPTIONS, secondary: true },
|
||||
{
|
||||
key: "freightType",
|
||||
label: "Freight",
|
||||
type: "enum",
|
||||
multiple: false,
|
||||
options: FREIGHT_TYPE_OPTIONS,
|
||||
},
|
||||
{
|
||||
key: "serviceTypeId",
|
||||
label: "Service",
|
||||
type: "enum",
|
||||
multiple: false,
|
||||
options: serviceTypeOptions,
|
||||
},
|
||||
{
|
||||
key: "paymentStatus",
|
||||
label: "Payment",
|
||||
type: "enum",
|
||||
multiple: false,
|
||||
options: PAYMENT_STATUS_OPTIONS,
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
// Wins over the `paymentStatus` filter above — the queue is by
|
||||
// definition PAID — because it's later in this array: toApiParams
|
||||
// merges defs in order, so a later toParams overwrites an earlier one.
|
||||
key: "paidUnallocated", label: "Allocation", type: "boolean", secondary: true,
|
||||
key: "paidUnallocated",
|
||||
label: "Allocation",
|
||||
type: "boolean",
|
||||
secondary: true,
|
||||
trueLabel: "Paid, not allocated",
|
||||
toParams: (v) => (v.v[0] === "true" ? { paymentStatus: "PAID", assignedToSchedule: "false" } : {}),
|
||||
toParams: (v) =>
|
||||
v.v[0] === "true"
|
||||
? { paymentStatus: "PAID", assignedToSchedule: "false" }
|
||||
: {},
|
||||
},
|
||||
{ key: "isGovernment", label: "Ownership", type: "enum", multiple: false, options: OWNERSHIP_OPTIONS, secondary: true },
|
||||
{
|
||||
key: "route", label: "Route", type: "route", options: yardOptions,
|
||||
key: "isGovernment",
|
||||
label: "Ownership",
|
||||
type: "enum",
|
||||
multiple: false,
|
||||
options: OWNERSHIP_OPTIONS,
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
key: "route",
|
||||
label: "Route",
|
||||
type: "route",
|
||||
options: yardOptions,
|
||||
toParams: routeParams("originYardId", "destinationYardId"),
|
||||
},
|
||||
{
|
||||
key: "created", label: "Created", type: "date", secondary: true,
|
||||
key: "created",
|
||||
label: "Created",
|
||||
type: "date",
|
||||
secondary: true,
|
||||
operators: ["between", "before", "after"],
|
||||
toParams: dateRangeParams("createdFrom", "createdTo"),
|
||||
},
|
||||
{
|
||||
key: "scheduled", label: "Scheduled", type: "date", secondary: true,
|
||||
key: "scheduled",
|
||||
label: "Scheduled",
|
||||
type: "date",
|
||||
secondary: true,
|
||||
operators: ["between", "before", "after"],
|
||||
toParams: dateRangeParams("scheduledFrom", "scheduledTo"),
|
||||
},
|
||||
@@ -187,19 +247,24 @@ export default function BookingRequestsPage() {
|
||||
[filterOptions, yardOptions, serviceTypeOptions],
|
||||
);
|
||||
|
||||
const controls = useFilters(bookingFilterDefs, { defaultSort: "createdAt:DESC", pageSize: 10 });
|
||||
const controls = useFilters(bookingFilterDefs, {
|
||||
defaultSort: "createdAt:DESC",
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const filter: BookingListFilter = useMemo(
|
||||
() => ({
|
||||
...(controls.params as unknown as BookingListFilter),
|
||||
// React Query cache key per kind selection ("ALL" when unfiltered) —
|
||||
// kept as a param the API ignores, matching the pre-migration cache key.
|
||||
tab: (controls.values.bookingType?.v[0] as BookingKind | undefined) ?? "ALL",
|
||||
tab:
|
||||
(controls.values.bookingType?.v[0] as BookingKind | undefined) ?? "ALL",
|
||||
}),
|
||||
[controls.params, controls.values.bookingType],
|
||||
);
|
||||
|
||||
const { data, isLoading, isError, refetch, isFetching } = useBookingList(filter);
|
||||
const { data, isLoading, isError, refetch, isFetching } =
|
||||
useBookingList(filter);
|
||||
const primaryAllocateId = allocateIds[0];
|
||||
const { data: allocateBooking } = useBookingDetail(
|
||||
allocateOpen ? primaryAllocateId : undefined,
|
||||
@@ -262,8 +327,9 @@ export default function BookingRequestsPage() {
|
||||
async (row: BookingListRow) => {
|
||||
setAllocatingId(row.id);
|
||||
try {
|
||||
const candidates =
|
||||
await trainSchedulingService.getAllocationCandidates(row.id);
|
||||
const candidates = await trainSchedulingService.getAllocationCandidates(
|
||||
row.id,
|
||||
);
|
||||
if (candidates.sameDay.length > 0) {
|
||||
const target = candidates.sameDay[0];
|
||||
await trainSchedulingService.allocatePaidBooking(row.id, target.id);
|
||||
@@ -330,7 +396,9 @@ export default function BookingRequestsPage() {
|
||||
</div>
|
||||
<div className="min-w-0 max-w-[220px]">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="truncate font-medium text-foreground">{b.reference}</p>
|
||||
<p className="truncate font-medium text-foreground">
|
||||
{b.reference}
|
||||
</p>
|
||||
<Badge
|
||||
variant={isGeneral ? "secondary" : "outline"}
|
||||
className="h-5 shrink-0 px-1.5 text-[10px] font-medium"
|
||||
@@ -338,6 +406,26 @@ export default function BookingRequestsPage() {
|
||||
{isGeneral ? "General" : "One-time"}
|
||||
</Badge>
|
||||
</div>
|
||||
{b.contractReference ? (
|
||||
<p className="mt-0.5 flex items-center gap-1 truncate text-xs">
|
||||
<FileText className="size-3 shrink-0 text-muted-foreground opacity-70" />
|
||||
{b.contractId ? (
|
||||
<Link
|
||||
to={`/dashboard/contract-requests/${b.contractId}/view`}
|
||||
// The row itself opens the booking — without this the
|
||||
// contract link would never win the click.
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="truncate text-blue-600 hover:underline"
|
||||
>
|
||||
{b.contractReference}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="truncate text-muted-foreground">
|
||||
{b.contractReference}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="mt-0.5 flex items-center gap-1 truncate text-xs text-muted-foreground">
|
||||
{b.isShippingLine ? (
|
||||
<Ship className="size-3 shrink-0 opacity-70" />
|
||||
@@ -346,7 +434,10 @@ export default function BookingRequestsPage() {
|
||||
)}
|
||||
{b.customerLabel}
|
||||
{b.isShippingLine ? (
|
||||
<Badge variant="secondary" className="h-4 shrink-0 px-1 text-[9px] font-medium">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-4 shrink-0 px-1 text-[9px] font-medium"
|
||||
>
|
||||
Shipping line
|
||||
</Badge>
|
||||
) : null}
|
||||
@@ -382,7 +473,9 @@ export default function BookingRequestsPage() {
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<span className="max-w-[8rem] truncate">{b.originLabel}</span>
|
||||
<ArrowRight className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="max-w-[8rem] truncate">{b.destinationLabel}</span>
|
||||
<span className="max-w-[8rem] truncate">
|
||||
{b.destinationLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<Badge
|
||||
@@ -443,7 +536,8 @@ export default function BookingRequestsPage() {
|
||||
size: 140,
|
||||
cell: ({ row }) => {
|
||||
const b = row.original;
|
||||
const needsAllocation = b.paymentStatus === "PAID" && !b.trainScheduleId;
|
||||
const needsAllocation =
|
||||
b.paymentStatus === "PAID" && !b.trainScheduleId;
|
||||
return (
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
{needsAllocation ? (
|
||||
@@ -474,61 +568,61 @@ export default function BookingRequestsPage() {
|
||||
return (
|
||||
<PageContainer>
|
||||
<Stack gap="lg">
|
||||
<PageHeader
|
||||
title="Booking requests"
|
||||
subtitle="Review, approve, and schedule freight booking requests."
|
||||
action={
|
||||
<>
|
||||
<Button
|
||||
color="edr-green"
|
||||
leftSection={<Plus size={18} />}
|
||||
onClick={() => navigate("/dashboard/booking-requests/new")}
|
||||
>
|
||||
Create booking
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
leftSection={<RefreshCw size={16} />}
|
||||
loading={isFetching}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<PageHeader
|
||||
title="Booking requests"
|
||||
subtitle="Review, approve, and schedule freight booking requests."
|
||||
action={
|
||||
<>
|
||||
<Button
|
||||
color="edr-green"
|
||||
leftSection={<Plus size={18} />}
|
||||
onClick={() => navigate("/dashboard/booking-requests/new")}
|
||||
>
|
||||
Create booking
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
leftSection={<RefreshCw size={16} />}
|
||||
loading={isFetching}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<KpiStrip
|
||||
loading={summaryLoading}
|
||||
items={[
|
||||
{
|
||||
label: "In queue",
|
||||
value: metrics?.inQueue ?? 0,
|
||||
icon: LayoutList,
|
||||
color: "edr-green",
|
||||
},
|
||||
{
|
||||
label: "Needs action",
|
||||
value: metrics?.needsAction ?? 0,
|
||||
icon: Clock,
|
||||
color: "yellow",
|
||||
},
|
||||
{
|
||||
label: "Urgent",
|
||||
value: metrics?.urgent ?? 0,
|
||||
icon: AlertTriangle,
|
||||
color: "red",
|
||||
},
|
||||
{
|
||||
label: "Completed",
|
||||
value: tabCounts?.completed ?? 0,
|
||||
icon: CheckCircle2,
|
||||
color: "edr-green",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<KpiStrip
|
||||
loading={summaryLoading}
|
||||
items={[
|
||||
{
|
||||
label: "In queue",
|
||||
value: metrics?.inQueue ?? 0,
|
||||
icon: LayoutList,
|
||||
color: "edr-green",
|
||||
},
|
||||
{
|
||||
label: "Needs action",
|
||||
value: metrics?.needsAction ?? 0,
|
||||
icon: Clock,
|
||||
color: "yellow",
|
||||
},
|
||||
{
|
||||
label: "Urgent",
|
||||
value: metrics?.urgent ?? 0,
|
||||
icon: AlertTriangle,
|
||||
color: "red",
|
||||
},
|
||||
{
|
||||
label: "Completed",
|
||||
value: tabCounts?.completed ?? 0,
|
||||
icon: CheckCircle2,
|
||||
color: "edr-green",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Status tabs replaced by booking-kind tabs (one-time / general). The
|
||||
{/* Status tabs replaced by booking-kind tabs (one-time / general). The
|
||||
old BookingStatusTabs is commented out — status is now a filter select.
|
||||
<BookingStatusTabs
|
||||
active={activeTab}
|
||||
@@ -540,92 +634,95 @@ export default function BookingRequestsPage() {
|
||||
/>
|
||||
*/}
|
||||
|
||||
<Card p={0}>
|
||||
<Stack gap={0}>
|
||||
<Box px="md" pt="sm" pb="xs" w="100%">
|
||||
<FilterBar
|
||||
defs={bookingFilterDefs}
|
||||
controls={controls}
|
||||
searchPlaceholder="Search booking, contract, customer or shipping line…"
|
||||
viewId="booking-requests"
|
||||
>
|
||||
<ExportButton datasetKey="bookings" params={controls.params} />
|
||||
</FilterBar>
|
||||
<Card p={0}>
|
||||
<Stack gap={0}>
|
||||
<Box px="md" pt="sm" pb="xs" w="100%">
|
||||
<FilterBar
|
||||
defs={bookingFilterDefs}
|
||||
controls={controls}
|
||||
searchPlaceholder="Search booking, contract, customer or shipping line…"
|
||||
viewId="booking-requests"
|
||||
>
|
||||
<ExportButton datasetKey="bookings" params={controls.params} />
|
||||
</FilterBar>
|
||||
</Box>
|
||||
|
||||
{showEmpty ? (
|
||||
<Box px="md" pb="md">
|
||||
<BookingTableEmpty
|
||||
isError={isError}
|
||||
hasSearch={hasSearch}
|
||||
onRetry={handleRefresh}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{showEmpty ? (
|
||||
<Box px="md" pb="md">
|
||||
<BookingTableEmpty
|
||||
isError={isError}
|
||||
hasSearch={hasSearch}
|
||||
onRetry={handleRefresh}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box style={{ overflowX: "auto" }} w="100%">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
status={isLoading ? "loading" : isError ? "error" : "success"}
|
||||
onRowClick={handleRowClick}
|
||||
{...controls.tableProps(total)}
|
||||
containerClassName="border-0 shadow-none bg-transparent"
|
||||
footer={DataTableFooter}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
|
||||
<Modal
|
||||
opened={otherDayModal !== null}
|
||||
onClose={() => setOtherDayModal(null)}
|
||||
title="Allocate to another date"
|
||||
centered
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" c="dimmed">
|
||||
No train on {otherDayModal ? formatDate(otherDayModal.booking.scheduledDate) : "the booking's day"}{" "}
|
||||
fits booking {otherDayModal?.booking.reference}. These trains on
|
||||
other dates do — the customer will be notified of the date change.
|
||||
</Text>
|
||||
{otherDayModal?.candidates.map((c) => (
|
||||
<Group key={c.id} justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="sm" fw={500}>
|
||||
{c.reference ?? "Train"}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Departs {formatDate(c.scheduledDepartureDate)}
|
||||
{c.direction ? ` · ${c.direction}` : ""}
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
color="edr-green"
|
||||
loading={allocatingId === otherDayModal.booking.id}
|
||||
onClick={() => void handleAllocateOtherDay(c)}
|
||||
>
|
||||
Allocate
|
||||
</Button>
|
||||
</Group>
|
||||
))}
|
||||
) : (
|
||||
<Box style={{ overflowX: "auto" }} w="100%">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
status={isLoading ? "loading" : isError ? "error" : "success"}
|
||||
onRowClick={handleRowClick}
|
||||
{...controls.tableProps(total)}
|
||||
containerClassName="border-0 shadow-none bg-transparent"
|
||||
footer={DataTableFooter}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Card>
|
||||
</Stack>
|
||||
|
||||
{allocateBooking ? (
|
||||
<AllocateBookingWizard
|
||||
booking={allocateBooking}
|
||||
opened={allocateOpen}
|
||||
onClose={() => {
|
||||
setAllocateOpen(false);
|
||||
setAllocateIds([]);
|
||||
void refetch();
|
||||
}}
|
||||
initialBookingIds={allocateIds}
|
||||
/>
|
||||
) : null}
|
||||
<Modal
|
||||
opened={otherDayModal !== null}
|
||||
onClose={() => setOtherDayModal(null)}
|
||||
title="Allocate to another date"
|
||||
centered
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" c="dimmed">
|
||||
No train on{" "}
|
||||
{otherDayModal
|
||||
? formatDate(otherDayModal.booking.scheduledDate)
|
||||
: "the booking's day"}{" "}
|
||||
fits booking {otherDayModal?.booking.reference}. These trains on
|
||||
other dates do — the customer will be notified of the date change.
|
||||
</Text>
|
||||
{otherDayModal?.candidates.map((c) => (
|
||||
<Group key={c.id} justify="space-between" wrap="nowrap">
|
||||
<div>
|
||||
<Text size="sm" fw={500}>
|
||||
{c.reference ?? "Train"}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Departs {formatDate(c.scheduledDepartureDate)}
|
||||
{c.direction ? ` · ${c.direction}` : ""}
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
color="edr-green"
|
||||
loading={allocatingId === otherDayModal.booking.id}
|
||||
onClick={() => void handleAllocateOtherDay(c)}
|
||||
>
|
||||
Allocate
|
||||
</Button>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{allocateBooking ? (
|
||||
<AllocateBookingWizard
|
||||
booking={allocateBooking}
|
||||
opened={allocateOpen}
|
||||
onClose={() => {
|
||||
setAllocateOpen(false);
|
||||
setAllocateIds([]);
|
||||
void refetch();
|
||||
}}
|
||||
initialBookingIds={allocateIds}
|
||||
/>
|
||||
) : null}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,15 @@ import {
|
||||
Textarea,
|
||||
ThemeIcon,
|
||||
} from "@mantine/core";
|
||||
import { AlertCircle, Check, Clock, Link2, User, X } from "lucide-react";
|
||||
import {
|
||||
AlertCircle,
|
||||
Check,
|
||||
Clock,
|
||||
FileText,
|
||||
Link2,
|
||||
User,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { PageContainer, PageHeader } from "@/components/page";
|
||||
@@ -234,6 +242,8 @@ export default function ConsolidationApprovalsPage() {
|
||||
row.booking?.reference ?? row.bookingReference
|
||||
}
|
||||
company={row.booking?.company?.name}
|
||||
contractReference={row.contractReference}
|
||||
contractId={row.booking?.contractId}
|
||||
/>
|
||||
<BookingSide
|
||||
id={row.partnerBookingId}
|
||||
@@ -242,6 +252,8 @@ export default function ConsolidationApprovalsPage() {
|
||||
row.partnerBookingReference
|
||||
}
|
||||
company={row.partnerBooking?.company?.name}
|
||||
contractReference={row.partnerContractReference}
|
||||
contractId={row.partnerBooking?.contractId}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
@@ -417,15 +429,23 @@ export default function ConsolidationApprovalsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/** One half of the wagon: its reference (linked) and whose cargo it is. */
|
||||
/**
|
||||
* One half of the wagon: its booking reference, the contract it was raised
|
||||
* under, and whose cargo it is. Both references link out — a reviewer deciding
|
||||
* a pairing usually wants the contract, not just the shipment.
|
||||
*/
|
||||
function BookingSide({
|
||||
id,
|
||||
reference,
|
||||
company,
|
||||
contractReference,
|
||||
contractId,
|
||||
}: {
|
||||
id: string;
|
||||
reference?: string | null;
|
||||
company?: string | null;
|
||||
contractReference?: string | null;
|
||||
contractId?: string | null;
|
||||
}) {
|
||||
return (
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
@@ -439,6 +459,32 @@ function BookingSide({
|
||||
>
|
||||
{reference ?? "—"}
|
||||
</Text>
|
||||
|
||||
{contractReference && (
|
||||
<Group gap={4} wrap="nowrap" mt={2}>
|
||||
<FileText
|
||||
size={11}
|
||||
className="shrink-0"
|
||||
color="var(--mantine-color-dimmed)"
|
||||
/>
|
||||
{contractId ? (
|
||||
<Text
|
||||
component={Link}
|
||||
to={`/dashboard/contract-requests/${contractId}/view`}
|
||||
fz={12}
|
||||
c="blue.7"
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
{contractReference}
|
||||
</Text>
|
||||
) : (
|
||||
<Text fz={12} c="dimmed">
|
||||
{contractReference}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
|
||||
<Text fz={12.5} c="dimmed">
|
||||
{company ?? "—"}
|
||||
</Text>
|
||||
|
||||
@@ -26,14 +26,19 @@ export interface ConsolidationApprovalRow {
|
||||
scheduledDate?: string | null;
|
||||
bookingReference?: string | null;
|
||||
partnerBookingReference?: string | null;
|
||||
/** Contract each half was created under — reviewers work by contract. */
|
||||
contractReference?: string | null;
|
||||
partnerContractReference?: string | null;
|
||||
booking?: {
|
||||
id: string;
|
||||
reference?: string;
|
||||
contractId?: string | null;
|
||||
company?: { name?: string } | null;
|
||||
} | null;
|
||||
partnerBooking?: {
|
||||
id: string;
|
||||
reference?: string;
|
||||
contractId?: string | null;
|
||||
company?: { name?: string } | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user