mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 02:28:18 +00:00
Merge pull request #806 from Tria-plc/freight_feature/usermanagement
add contract clearance detail page and enhance contract requests fil…
This commit is contained in:
@@ -28,9 +28,15 @@ export const BookingRow = memo(function BookingRow({
|
||||
const AIcon = cfg.action.icon;
|
||||
const ap = ACTION_PROPS[cfg.action.kind];
|
||||
// Payable bookings get an inline "Pay now" that opens the payment modal
|
||||
// instead of navigating to the detail page.
|
||||
// instead of navigating to the detail page. A general contract is payable as
|
||||
// soon as it's FULLY_EXECUTED (signed); a one-time booking only after it's
|
||||
// SELECTED_FOR_BATCH — same rule as the bookings list's PrimaryAction.
|
||||
const payableStatus =
|
||||
booking.bookingType === "GENERAL_CONTRACT"
|
||||
? "FULLY_EXECUTED"
|
||||
: "SELECTED_FOR_BATCH";
|
||||
const canPay =
|
||||
booking.status === "SELECTED_FOR_BATCH" && booking.paymentStatus !== "PAID";
|
||||
booking.status === payableStatus && booking.paymentStatus !== "PAID";
|
||||
// Clearance/operation steps + changes-requested resubmit can be done in place
|
||||
// via a modal on the row.
|
||||
const hasInlineAction = bookingHasInlineAction(booking);
|
||||
|
||||
@@ -37,6 +37,11 @@ import { ShipmentTrackingModal } from "./tracking/ShipmentTrackingModal";
|
||||
import { PayNowButton } from "./payments/PayNowButton";
|
||||
import { BookingActionButton } from "./clearance/BookingActionButton";
|
||||
import { bookingHasInlineAction } from "./clearance/bookingNextAction";
|
||||
import {
|
||||
ContractSignButton,
|
||||
bookingIsSignable,
|
||||
} from "./contract/ContractSignButton";
|
||||
import { ApproveDeliveryButton } from "./delivery/ApproveDeliveryButton";
|
||||
import {
|
||||
BookingStatusBadge as StatusBadge,
|
||||
BookingTypeBadge,
|
||||
@@ -54,6 +59,7 @@ import {
|
||||
type ColumnDef,
|
||||
usePagination,
|
||||
} from "@edr/ui-common";
|
||||
import "./bookings-table.css";
|
||||
|
||||
// Bookings that have left (or are leaving) the yard can be tracked live.
|
||||
const TRACKABLE_STATUSES = new Set([
|
||||
@@ -200,6 +206,14 @@ function PrimaryAction({
|
||||
if (status === payableStatus && booking.paymentStatus !== "PAID") {
|
||||
return <PayNowButton booking={booking} />;
|
||||
}
|
||||
// Contract ready for the customer's signature → full-page contract viewer.
|
||||
if (bookingIsSignable(booking)) {
|
||||
return <ContractSignButton booking={booking} size="xs" />;
|
||||
}
|
||||
// Delivered cargo with a handover awaiting the customer's signature.
|
||||
if (booking.handoverAwaitingSignature) {
|
||||
return <ApproveDeliveryButton bookingId={id} size="xs" stopPropagation />;
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -886,7 +900,7 @@ export default function BookingsListPage() {
|
||||
manualPagination: true,
|
||||
pageCount,
|
||||
}}
|
||||
containerClassName="border-0 shadow-none rounded-none"
|
||||
containerClassName="edr-bookings-table border-0 shadow-none rounded-none"
|
||||
footer={DataTableFooter}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Scoped to .edr-bookings-table — the DataTable container div on the bookings
|
||||
* list only; no other DataTable is affected. Mirrors contracts-table.css:
|
||||
* content-sized columns with a 40px floor, horizontal scroll when the table
|
||||
* outgrows the card, and a sticky shadowed action column.
|
||||
*/
|
||||
.edr-bookings-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* width: max-content — the table is exactly as wide as its columns' content
|
||||
* needs, never squeezed to fit the viewport; the container scrolls instead.
|
||||
* min-width: 100% keeps it filling the card when content is narrow.
|
||||
*/
|
||||
.edr-bookings-table table {
|
||||
table-layout: auto;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.edr-bookings-table th,
|
||||
.edr-bookings-table td {
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Mantine Badge caps itself at max-width: 100%; inside an auto-layout table
|
||||
* cell that resolves against min-content and clips the label. Let badges size
|
||||
* to their text so the column grows to fit them.
|
||||
*/
|
||||
.edr-bookings-table .mantine-Badge-root {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Full-width rows (error / empty state) span every column via colspan — leave
|
||||
* their wrapping alone.
|
||||
*/
|
||||
.edr-bookings-table th,
|
||||
.edr-bookings-table td:not([colspan]) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Sticky header row. */
|
||||
.edr-bookings-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sticky action column, shrunk to its content. The width overrides the inline
|
||||
* width DataTable stamps from tanstack's default column size — hence
|
||||
* !important. `:not([colspan])` keeps the full-width error/empty rows out.
|
||||
*/
|
||||
.edr-bookings-table th:last-child,
|
||||
.edr-bookings-table td:last-child:not([colspan]) {
|
||||
width: 1% !important;
|
||||
position: sticky;
|
||||
right: 0;
|
||||
box-shadow: -12px 0 16px -6px rgba(16, 32, 47, 0.3);
|
||||
}
|
||||
|
||||
/*
|
||||
* Sticky cells sit above the scrolling ones, so they need their own opaque
|
||||
* background or the columns underneath show through.
|
||||
*/
|
||||
.edr-bookings-table td:last-child:not([colspan]) {
|
||||
/* Very light blue-grey tint sets the action column apart from the rows. */
|
||||
background: #f5f8fb;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Row hover uses the tailwind `hover:bg-accent` class on the <tr>. */
|
||||
.edr-bookings-table tbody tr:hover td:last-child:not([colspan]) {
|
||||
background: var(--accent, #f4fbf8);
|
||||
}
|
||||
|
||||
/* Header cell is sticky on both axes — it must outrank the body's sticky column. */
|
||||
.edr-bookings-table th:last-child {
|
||||
background: #f4f7fa;
|
||||
z-index: 3;
|
||||
}
|
||||
@@ -403,6 +403,7 @@ export default function ContractsList() {
|
||||
<Table.Th>Cargo</Table.Th>
|
||||
<Table.Th>Route</Table.Th>
|
||||
<Table.Th>Trade</Table.Th>
|
||||
<Table.Th>Customs</Table.Th>
|
||||
<Table.Th>Currency</Table.Th>
|
||||
<Table.Th>Created</Table.Th>
|
||||
<Table.Th>Valid Until</Table.Th>
|
||||
@@ -413,7 +414,7 @@ export default function ContractsList() {
|
||||
<Table.Tbody>
|
||||
{isLoading && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={11}>
|
||||
<Table.Td colSpan={12}>
|
||||
<Center py={48}>
|
||||
<Loader color="edr-green" size="sm" />
|
||||
</Center>
|
||||
@@ -423,7 +424,7 @@ export default function ContractsList() {
|
||||
|
||||
{!isLoading && isError && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={11}>
|
||||
<Table.Td colSpan={12}>
|
||||
<Center py={48}>
|
||||
<Text fz={13} c="red">
|
||||
Failed to load contracts. Please try again.
|
||||
@@ -435,7 +436,7 @@ export default function ContractsList() {
|
||||
|
||||
{!isLoading && !isError && rows.length === 0 && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={11}>
|
||||
<Table.Td colSpan={12}>
|
||||
<Stack align="center" gap={8} py={48}>
|
||||
<Inbox
|
||||
size={26}
|
||||
@@ -560,6 +561,15 @@ export default function ContractsList() {
|
||||
{tradeLabel}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={c.customsClearingEnabled ? "edr-green" : "gray"}
|
||||
radius="sm"
|
||||
>
|
||||
{c.customsClearingEnabled ? "With customs" : "Without"}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={13} style={{ color: INK }}>
|
||||
{c.paymentCurrency ?? "—"}
|
||||
@@ -612,7 +622,7 @@ export default function ContractsList() {
|
||||
{isOpen && (
|
||||
<Table.Tr style={{ background: "#F4FBF8" }}>
|
||||
<Table.Td
|
||||
colSpan={11}
|
||||
colSpan={12}
|
||||
style={{ padding: "6px 20px 18px" }}
|
||||
>
|
||||
<ContractStepBanner contract={c} />
|
||||
|
||||
@@ -1670,8 +1670,39 @@ function ContainerLineEditor({
|
||||
</Text>
|
||||
) : null}
|
||||
<Stack gap={10} mt={8}>
|
||||
{/* Header row — input labels + handling-service labels, one aligned
|
||||
grid shared by every unit row below. */}
|
||||
{Math.max(quantity, units.length) > 0 && (
|
||||
<Group gap={10} wrap="nowrap" align="flex-end">
|
||||
<Text fz={12} fw={600} c="#10202F" style={{ flex: 1 }}>
|
||||
Container number *
|
||||
</Text>
|
||||
<Text fz={12} fw={600} c="#10202F" style={{ flex: 1 }}>
|
||||
Seal number
|
||||
</Text>
|
||||
<Text fz={12} fw={600} c="#10202F" style={{ flex: 1 }}>
|
||||
VGM (tons) *
|
||||
</Text>
|
||||
{handlingColumns.map((col) => (
|
||||
<Group
|
||||
key={col.key}
|
||||
gap={4}
|
||||
wrap="nowrap"
|
||||
justify="center"
|
||||
style={{ width: 96, flexShrink: 0 }}
|
||||
>
|
||||
<span style={{ color: col.color, display: "flex" }}>
|
||||
{col.icon}
|
||||
</span>
|
||||
<Text fz={12} fw={600} c="#10202F">
|
||||
{col.label}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Group>
|
||||
)}
|
||||
{Array.from({ length: Math.max(quantity, units.length) }).map((_, u) => (
|
||||
<Group key={u} gap={10} grow align="flex-start">
|
||||
<Group key={u} gap={10} wrap="nowrap" align="flex-start">
|
||||
<Controller
|
||||
name={`containers.${index}.units.${u}.containerNumber`}
|
||||
control={form.control}
|
||||
@@ -1681,11 +1712,11 @@ function ContainerLineEditor({
|
||||
onChange={(e) =>
|
||||
field.onChange(e.currentTarget.value.toUpperCase())
|
||||
}
|
||||
label={u === 0 ? "Container number *" : undefined}
|
||||
placeholder="e.g. MSCU1234567"
|
||||
error={fieldState.error?.message}
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -1695,10 +1726,10 @@ function ContainerLineEditor({
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
label={u === 0 ? "Seal number" : undefined}
|
||||
placeholder="Optional"
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -1710,13 +1741,13 @@ function ContainerLineEditor({
|
||||
{...field}
|
||||
type="number"
|
||||
onKeyDown={blockNegative}
|
||||
label={u === 0 ? "VGM (tons) *" : undefined}
|
||||
placeholder="e.g. 24.5"
|
||||
min={0}
|
||||
step={0.01}
|
||||
error={fieldState.error?.message}
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -1726,28 +1757,25 @@ function ContainerLineEditor({
|
||||
name={`containers.${index}.units.${u}.${col.key}`}
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<Switch
|
||||
checked={Boolean(field.value)}
|
||||
aria-label={`${col.label} — container ${u + 1}`}
|
||||
onChange={(e) =>
|
||||
toggleUnitHandling(u, col.key, e.currentTarget.checked)
|
||||
}
|
||||
label={
|
||||
u === 0 ? (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<span style={{ color: col.color, display: "flex" }}>
|
||||
{col.icon}
|
||||
</span>
|
||||
<Text fz={12} fw={600} c="#10202F">
|
||||
{col.label}
|
||||
</Text>
|
||||
</Group>
|
||||
) : undefined
|
||||
}
|
||||
labelPosition="right"
|
||||
size="sm"
|
||||
mt={u === 0 ? 26 : 6}
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
width: 96,
|
||||
flexShrink: 0,
|
||||
height: 42,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Switch
|
||||
checked={Boolean(field.value)}
|
||||
aria-label={`${col.label} — container ${u + 1}`}
|
||||
onChange={(e) =>
|
||||
toggleUnitHandling(u, col.key, e.currentTarget.checked)
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -2,35 +2,51 @@
|
||||
* Scoped to .edr-contracts-table — every rule below is prefixed, so no other
|
||||
* Mantine Table in the portal is affected.
|
||||
*
|
||||
* Column sizing: table-layout stays `auto`, so a column with short content
|
||||
* (a currency code, a badge) keeps its natural narrow width. The cap only
|
||||
* kicks in for columns whose content would otherwise push past it — those
|
||||
* wrap onto extra lines instead of widening the table.
|
||||
* Column sizing: table-layout stays `auto`. Every column sizes to its content
|
||||
* with a 30px floor and no wrapping — when the columns together outgrow the
|
||||
* viewport, the table widens and the wrapper's overflow-x takes over.
|
||||
*/
|
||||
/*
|
||||
* width: max-content — the table is exactly as wide as its columns' content
|
||||
* needs, never squeezed to fit the viewport; the overflow-x wrapper scrolls
|
||||
* instead. min-width: 100% keeps it filling the card when content is narrow.
|
||||
*/
|
||||
.edr-contracts-table {
|
||||
/* Single knob for the cap — raise this if the columns read too cramped. */
|
||||
--edr-col-max: 60px;
|
||||
table-layout: auto;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.edr-contracts-table th,
|
||||
.edr-contracts-table td {
|
||||
max-width: var(--edr-col-max);
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
/*
|
||||
* The two fixed-purpose columns are exempt from the cap: the expander is a
|
||||
* 28px icon button that must not wrap, and the action column holds two
|
||||
* buttons side by side.
|
||||
* Mantine Badge caps itself at max-width: 100%; inside an auto-layout table
|
||||
* cell that resolves against min-content and clips the label ("General" →
|
||||
* "Gen…"). Let the badge size to its text so the column grows to fit it.
|
||||
*/
|
||||
.edr-contracts-table .mantine-Badge-root {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Full-width rows (loading / error / empty / expanded step banner) span every
|
||||
* column via colspan — leave their wrapping alone.
|
||||
*/
|
||||
.edr-contracts-table th,
|
||||
.edr-contracts-table td:not([colspan]) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/*
|
||||
* Action column hugs its content: width 1% + nowrap makes the browser give it
|
||||
* the minimum width its buttons need and nothing more.
|
||||
*/
|
||||
.edr-contracts-table th:first-child,
|
||||
.edr-contracts-table td:first-child:not([colspan]),
|
||||
.edr-contracts-table th:last-child,
|
||||
.edr-contracts-table td:last-child:not([colspan]) {
|
||||
max-width: none;
|
||||
white-space: nowrap;
|
||||
width: 1%;
|
||||
}
|
||||
|
||||
/* Sticky header row (moved off the Mantine `styles` prop — see ContractsList). */
|
||||
@@ -49,7 +65,7 @@
|
||||
.edr-contracts-table td:last-child:not([colspan]) {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
box-shadow: -8px 0 8px -8px rgba(16, 32, 47, 0.18);
|
||||
box-shadow: -12px 0 16px -6px rgba(16, 32, 47, 0.3);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -58,7 +74,8 @@
|
||||
* the background the row already has.
|
||||
*/
|
||||
.edr-contracts-table td:last-child:not([colspan]) {
|
||||
background: #ffffff;
|
||||
/* Very light blue-grey tint sets the action column apart from the rows. */
|
||||
background: #f5f8fb;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user