Merge pull request #1145 from Tria-plc/freight_feature/usermanagement

Freight feature/usermanagement
This commit is contained in:
marshal
2026-08-07 01:35:39 +03:00
committed by GitHub
44 changed files with 1824 additions and 202 deletions

View File

@@ -23,7 +23,13 @@ interface AuthEmployeePosition {
permissions?: AuthPermission[];
/** Some IAM payloads nest the position record instead of flattening its key. */
position?: { id?: string; key?: string; name?: LocaleText };
positionType?: { id?: string; key?: string; name?: LocaleText } | null;
positionType?: {
id?: string;
key?: string;
name?: LocaleText;
/** Grants held by the TYPE — where admin-created positions keep theirs. */
permissions?: AuthPermission[];
} | null;
}
interface AuthEmployeeRecord {

View File

@@ -1,6 +1,6 @@
import { Alert, Button, Group, Paper, Stack, Text } from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { AlertTriangle, Send } from "lucide-react";
import { AlertTriangle, Pencil, Send } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
import toast from "react-hot-toast";
@@ -16,6 +16,9 @@ export interface BookingChangesRequestedAlertProps {
scheduledDate?: string | null;
/** GL Ethiopia owns customs bookings, so only they get the resubmit control. */
canResubmit: boolean;
/** Completion-form route for editing the cargo before resubmitting —
* rendered only for resubmit-capable users when provided. */
editHref?: string;
onResubmitted?: () => void;
}
@@ -33,6 +36,7 @@ export function BookingChangesRequestedAlert({
note,
scheduledDate,
canResubmit,
editHref,
onResubmitted,
}: BookingChangesRequestedAlertProps) {
const [day, setDay] = useState<Date | null>(
@@ -122,6 +126,18 @@ export function BookingChangesRequestedAlert({
>
Resubmit to Operations
</Button>
{editHref ? (
<Button
component={Link}
to={editHref}
variant="default"
radius="md"
size="sm"
leftSection={<Pencil size={15} />}
>
Edit cargo & resubmit
</Button>
) : null}
</Group>
) : null}
</Stack>

View File

@@ -961,7 +961,7 @@ export default function GlCreateBookingForm() {
// modal falls back to the contract unit-rate estimate while it loads.
const validateShipmentMutation = useMutation({
mutationFn: (dto: Freight.CreateBookingUnderContractDto) =>
contractsService.validateShipment(id ?? "", dto),
contractsService.validateShipment(id ?? "", dto, completeBookingId),
});
const validation = validateShipmentMutation.data ?? null;

View File

@@ -348,6 +348,14 @@ export function getPermissionKeys(user: AuthUser | null | undefined): string[] {
for (const p of pos.permissions ?? []) {
if (p.key) keys.add(p.key);
}
// Positions created through the admin UI keep their grants on the
// position TYPE, not the position — miss these and such staff resolve to
// zero permissions and every gated route rejects them. `/api/me` folds
// them into the position's permission list, but older payloads may still
// carry them separately.
for (const p of pos.positionType?.permissions ?? []) {
if (p.key) keys.add(p.key);
}
}
}
return [...keys];

View File

@@ -311,6 +311,7 @@ export default function ContractClearanceDetailPage() {
note={clearance.linkedBookingReviewNote}
scheduledDate={clearance.linkedBookingScheduledDate}
canResubmit={canResubmitBooking}
editHref={`/dashboard/contracts/${id}/bookings/${linkedBookingId}/complete?copyFrom=${linkedBookingId}`}
onResubmitted={() => {
void refetch();
void refetchContract();

View File

@@ -450,6 +450,35 @@ export default function ContractRequestDetailPage() {
description={statusMeta.description}
/>
{/* A contract resting in APPROVED means the automatic PDF generation on
final approval failed — on success it moves straight to
CONTRACT_READY. Offer the manual retry. */}
{contract.status === "APPROVED" ? (
<Alert
color="orange"
radius="md"
icon={<AlertTriangle size={18} />}
title="Contract document was not generated"
>
<Stack gap="sm" align="flex-start">
<Text size="sm">
All approvals are complete, but generating the contract PDF
failed. Retry the generation below.
</Text>
<Button
color="edr-green"
size="compact-sm"
radius="lg"
leftSection={<RefreshCw size={15} />}
loading={mutations.generateContract.isPending}
onClick={() => mutations.generateContract.mutate()}
>
Regenerate contract
</Button>
</Stack>
</Alert>
) : null}
{contract.status === "REJECTED" && contract.latestRejectionNote ? (
<Alert
color="red"

View File

@@ -916,10 +916,10 @@ export default function TrainScheduleV2DetailPage() {
<Title order={2} fw={700} style={{ color: "#0f172a" }}>
{schedule.route?.name ?? "Train schedule"}
</Title>
{schedule.trainNumber ? (
<Badge variant="light" color="#F2A516" radius="sm" style={{ fontWeight: 600 }}>
{schedule.trainNumber}
</Badge>
{schedule.train?.trainName ? (
<Text fw={700} style={{ color: "#0f172a" }}>
{schedule.train.trainName}
</Text>
) : null}
{schedule.train ? (
<Text size="xs" c="dimmed" ff="monospace">
@@ -927,6 +927,51 @@ export default function TrainScheduleV2DetailPage() {
</Text>
) : null}
</Group>
{/* Voyage (train) number and trade direction — the two things
operations identify a run by, so they read at a glance
rather than as small badges among the rest. */}
<Group gap="lg" align="center" wrap="wrap">
{schedule.trainNumber ? (
<Box>
<Text size="xs" c="dimmed" fw={600} tt="uppercase" lh={1.2}>
Voyage No.
</Text>
<Text
ff="monospace"
fw={800}
lh={1.1}
style={{ fontSize: 32, color: "#0f172a" }}
>
{schedule.trainNumber}
</Text>
</Box>
) : null}
{schedule.direction ? (
<Box>
<Text size="xs" c="dimmed" fw={600} tt="uppercase" lh={1.2}>
Direction
</Text>
<Text
fw={800}
lh={1.1}
tt="uppercase"
style={{
fontSize: 32,
letterSpacing: 0.5,
color:
schedule.direction === "IMPORT"
? "#2E5B96"
: schedule.direction === "EXPORT"
? "#0A6F4D"
: "#0f172a",
}}
>
{schedule.direction}
</Text>
</Box>
) : null}
</Group>
{(schedule.stops?.length ?? 0) >= 3 ||
(schedule.bookings ?? []).some(
(b) => b.tradeDirection === "DOMESTIC",

View File

@@ -673,8 +673,16 @@ export const contractsService = {
validateShipment: (
id: string,
payload: Freight.CreateBookingUnderContractDto,
// Completion/resubmit preview: exclude this booking's own containers from
// the same-train clash check.
excludeBookingId?: string,
) =>
postContract<ShipmentValidation>(C.VALIDATE_SHIPMENT(id), payload),
postContract<ShipmentValidation>(
excludeBookingId
? `${C.VALIDATE_SHIPMENT(id)}?bookingId=${excludeBookingId}`
: C.VALIDATE_SHIPMENT(id),
payload,
),
/** Remaining bookable quantity per cargo line (GENERAL draw-down cap). */
getCapacity: async (id: string): Promise<Freight.ContractCapacityLine[]> => {