mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
Merge pull request #968 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
This commit is contained in:
@@ -72,12 +72,19 @@ export function computeGlShipmentTotal(
|
||||
(i) => i.containerSize === line.containerSize && !i.isClearance,
|
||||
);
|
||||
if (rate) {
|
||||
// A per-wagon rate bills the wagons this line occupies — two 20ft share
|
||||
// one — not the box count. Mirrors the API's base-freight line so the
|
||||
// quote matches the invoice.
|
||||
const billedQty =
|
||||
rate.unit === "per_wagon"
|
||||
? Math.ceil(qty * (line.containerSize === "40ft" ? 1 : 0.5))
|
||||
: qty;
|
||||
lines.push({
|
||||
label: rate.label,
|
||||
unitPrice: rate.unitPrice,
|
||||
unit: rate.unit,
|
||||
quantity: qty,
|
||||
amount: rate.unitPrice * qty,
|
||||
quantity: billedQty,
|
||||
amount: rate.unitPrice * billedQty,
|
||||
});
|
||||
}
|
||||
hazardTotalQty += line.hazardousQuantity;
|
||||
|
||||
@@ -116,8 +116,8 @@ const FreightDashboardHeader = ({
|
||||
</Group>
|
||||
|
||||
{/* Right: actions + avatar. The doc-review alarm leads the group — it
|
||||
only renders in the last half of a review phase that still has
|
||||
undecided requests, so it never competes for space otherwise. */}
|
||||
renders only during a review phase that still has undecided
|
||||
requests, so it never competes for space otherwise. */}
|
||||
<Group gap={10} wrap="nowrap" align="center">
|
||||
<DocReviewAlertButton />
|
||||
|
||||
|
||||
@@ -391,6 +391,12 @@ const RuleEngineFormDialog = ({
|
||||
// display order) is a non-negative magnitude — reject negatives outright
|
||||
// rather than letting a typed "-" reach the API.
|
||||
min={isNumber ? 0 : undefined}
|
||||
// Without an explicit step the browser assumes 1 and refuses to submit
|
||||
// anything fractional — which blocked decimal rates, tonnages and
|
||||
// distances. The API is the one that decides which of these are whole
|
||||
// numbers (@IsInt on points/sizes/order, @IsNumber on money, tons, km),
|
||||
// so let the field carry decimals and let a 400 catch the rest.
|
||||
step={isNumber ? "any" : undefined}
|
||||
disabled={field.disabled || computed !== undefined}
|
||||
value={String((computed !== undefined ? computed : values[field.name]) ?? "")}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -36,11 +36,12 @@ function formatRemaining(ms: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Header alarm for the document-review deadline. Appears only once the review
|
||||
* phase is half spent AND requests are still undecided — everything still
|
||||
* pending when the clock runs out is expired automatically, so this is the last
|
||||
* call to accept or reject. Clicking opens the booking requests already
|
||||
* filtered to those undecided import requests.
|
||||
* Header alarm for the document-review deadline. Runs for the whole review
|
||||
* phase — from the moment it opens until the clock runs out — whenever requests
|
||||
* are still undecided. Everything left pending at the deadline is expired
|
||||
* automatically, so staff get the full window to accept or reject rather than
|
||||
* only its back half. Clicking opens the booking requests already filtered to
|
||||
* those undecided requests.
|
||||
*/
|
||||
export default function DocReviewAlertButton() {
|
||||
const navigate = useNavigate();
|
||||
@@ -67,10 +68,12 @@ export default function DocReviewAlertButton() {
|
||||
}, [deadlineMs]);
|
||||
|
||||
if (!alert) return null;
|
||||
// Half the review phase has to be gone before staff are alarmed — a 30-minute
|
||||
// review warns with 15 minutes left.
|
||||
const halfMs = (Math.max(alert.docReviewMinutes, 1) * 60_000) / 2;
|
||||
if (remaining <= 0 || remaining > halfMs) return null;
|
||||
// Alarm for the WHOLE review phase, from the moment it opens: anything still
|
||||
// undecided when the clock runs out is expired automatically, so staff need
|
||||
// the full window to act, not the back half of it. The endpoint only returns
|
||||
// a schedule that is in DOC_REVIEW with undecided requests behind it, so the
|
||||
// remaining check just hides the pill once the deadline passes.
|
||||
if (remaining <= 0) return null;
|
||||
|
||||
const requestLabel = alert.pendingCount === 1 ? "request" : "requests";
|
||||
|
||||
@@ -85,7 +88,16 @@ export default function DocReviewAlertButton() {
|
||||
<UnstyledButton
|
||||
onClick={() => navigate(pendingRequestsHref(alert.tradeDirection))}
|
||||
aria-label={`${alert.pendingCount} import booking ${requestLabel} awaiting a decision — document review ends in ${formatRemaining(remaining)}`}
|
||||
className="group flex h-9 shrink-0 items-center gap-2 rounded-full border border-red-600/60 bg-red-600 pl-2.5 pr-2 text-white shadow-[0_2px_10px_rgba(220,38,38,0.35)] transition-transform hover:scale-[1.02] hover:bg-red-700"
|
||||
className="group flex h-9 shrink-0 items-center gap-2 rounded-full pl-2.5 pr-2 transition-transform hover:scale-[1.02]"
|
||||
// Inline, not Tailwind: Mantine's UnstyledButton resets the background
|
||||
// in unlayered CSS, which beats a `@layer utilities` class whatever its
|
||||
// specificity — `bg-red-600` alone renders the pill white.
|
||||
style={{
|
||||
background: "var(--mantine-color-red-6)",
|
||||
border: "1px solid var(--mantine-color-red-7)",
|
||||
color: "#fff",
|
||||
boxShadow: "0 2px 10px rgba(220, 38, 38, 0.35)",
|
||||
}}
|
||||
>
|
||||
{/* Live dot: a ping ring behind a solid core, so the pill reads as
|
||||
active without animating the whole chip. */}
|
||||
|
||||
@@ -112,6 +112,13 @@ const SORT_OPTIONS = [
|
||||
{ value: "contractValidUntil:DESC", label: "Expiring latest" },
|
||||
];
|
||||
|
||||
/** Every column is 120px wide and wraps its content instead of truncating. */
|
||||
const COLUMN_WIDTH = 120;
|
||||
const COLUMN_META = {
|
||||
headerClassName: "whitespace-normal break-words",
|
||||
cellClassName: "whitespace-normal break-words align-top",
|
||||
};
|
||||
|
||||
/** Local start-of-day → ISO, for inclusive "from" date filters. */
|
||||
function startOfDayIso(d: Date): string {
|
||||
const x = new Date(d);
|
||||
@@ -260,6 +267,8 @@ export default function ContractRequestsPage() {
|
||||
const columns: ColumnDef<ContractListRow>[] = [
|
||||
{
|
||||
id: "contract",
|
||||
size: COLUMN_WIDTH,
|
||||
meta: COLUMN_META,
|
||||
header: () => <span className={bookingTable.headerCell}>Customer</span>,
|
||||
cell: ({ row }) => {
|
||||
const c = row.original;
|
||||
@@ -269,10 +278,8 @@ export default function ContractRequestsPage() {
|
||||
<User className="size-4" strokeWidth={1.75} />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium text-foreground">
|
||||
{c.customerLabel}
|
||||
</p>
|
||||
<p className="mt-0.5 flex items-center gap-1 truncate text-xs text-muted-foreground">
|
||||
<p className="font-medium text-foreground">{c.customerLabel}</p>
|
||||
<p className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<FileText className="size-3 shrink-0 opacity-70" />
|
||||
{c.reference}
|
||||
</p>
|
||||
@@ -283,17 +290,17 @@ export default function ContractRequestsPage() {
|
||||
},
|
||||
{
|
||||
id: "route",
|
||||
size: COLUMN_WIDTH,
|
||||
meta: COLUMN_META,
|
||||
header: () => <span className={bookingTable.headerCell}>Route</span>,
|
||||
cell: ({ row }) => {
|
||||
const c = row.original;
|
||||
return (
|
||||
<div className="space-y-1 py-1">
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<span className="max-w-[8rem] truncate">{c.originLabel}</span>
|
||||
<span>{c.originLabel}</span>
|
||||
<ArrowRight className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
<span className="max-w-[8rem] truncate">
|
||||
{c.destinationLabel}
|
||||
</span>
|
||||
<span>{c.destinationLabel}</span>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<Badge
|
||||
@@ -315,8 +322,8 @@ export default function ContractRequestsPage() {
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
size: 200,
|
||||
minSize: 180,
|
||||
size: COLUMN_WIDTH,
|
||||
meta: COLUMN_META,
|
||||
header: () => <span className={bookingTable.headerCell}>Status</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="py-1">
|
||||
@@ -326,18 +333,18 @@ export default function ContractRequestsPage() {
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
meta: {
|
||||
headerClassName: "min-w-[11rem]",
|
||||
cellClassName: "min-w-[11rem]",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "approval",
|
||||
size: COLUMN_WIDTH,
|
||||
meta: COLUMN_META,
|
||||
header: () => <span className={bookingTable.headerCell}>Approval</span>,
|
||||
cell: ({ row }) => <ContractApprovalProgressCell row={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "validity",
|
||||
size: COLUMN_WIDTH,
|
||||
meta: COLUMN_META,
|
||||
header: () => <span className={bookingTable.headerCell}>Validity</span>,
|
||||
cell: ({ row }) => {
|
||||
const c = row.original;
|
||||
@@ -362,6 +369,8 @@ export default function ContractRequestsPage() {
|
||||
},
|
||||
{
|
||||
id: "kind",
|
||||
size: COLUMN_WIDTH,
|
||||
meta: COLUMN_META,
|
||||
header: () => <span className={bookingTable.headerCell}>Kind</span>,
|
||||
cell: ({ row }) => {
|
||||
const isGeneral = row.original.contractKind === "GENERAL";
|
||||
@@ -383,6 +392,8 @@ export default function ContractRequestsPage() {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
size: COLUMN_WIDTH,
|
||||
meta: COLUMN_META,
|
||||
header: () => <span className={bookingTable.headerCell}>Action</span>,
|
||||
cell: ({ row }) => {
|
||||
const action = getStaffRowAction(row.original);
|
||||
@@ -653,7 +664,9 @@ export default function ContractRequestsPage() {
|
||||
manualPagination: true,
|
||||
pageCount,
|
||||
}}
|
||||
containerClassName="border-0 shadow-none bg-transparent"
|
||||
// table-fixed makes the per-column 120px widths stick; without
|
||||
// it auto-layout re-widens columns once cells wrap.
|
||||
containerClassName="border-0 shadow-none bg-transparent [&_table]:table-fixed [&_table]:min-w-[840px]"
|
||||
footer={DataTableFooter}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user