mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
fix(operations): prefill booking context on single assign; enforce per-truck container loads
- single-row Assign vehicle uses the full single-record flow (details + containers) - release() rejects exit containers not assigned to the departing truck - weighing modal offers only the selected truck's assigned containers
This commit is contained in:
@@ -20,12 +20,14 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { OverviewPageHeader } from "@/components/overview/OverviewPageHeader";
|
||||
import { OverviewQuickLinks } from "@/components/overview/OverviewQuickLinks";
|
||||
import { OverviewTabContent } from "@/components/overview/OverviewTabContent";
|
||||
import "@/components/overview/overview.css";
|
||||
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
|
||||
import { useOverview } from "@/hooks/useOverview";
|
||||
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||
import type { OverviewRange, OverviewTabKey } from "@/types/overview";
|
||||
|
||||
const TAB_ITEMS: Array<{
|
||||
@@ -40,6 +42,8 @@ const TAB_ITEMS: Array<{
|
||||
| "customers"
|
||||
| "staff";
|
||||
metricKey: string;
|
||||
/** Any of these keys grants the tab. */
|
||||
permission: string[];
|
||||
}> = [
|
||||
{
|
||||
value: "bookings",
|
||||
@@ -47,6 +51,7 @@ const TAB_ITEMS: Array<{
|
||||
icon: FileText,
|
||||
kpiKey: "bookings",
|
||||
metricKey: "totalActive",
|
||||
permission: [FREIGHT_PERMS.bookings.view],
|
||||
},
|
||||
{
|
||||
value: "contracts",
|
||||
@@ -54,6 +59,7 @@ const TAB_ITEMS: Array<{
|
||||
icon: FileSignature,
|
||||
kpiKey: "contracts",
|
||||
metricKey: "totalActive",
|
||||
permission: [FREIGHT_PERMS.contracts.view],
|
||||
},
|
||||
{
|
||||
value: "billing",
|
||||
@@ -61,6 +67,7 @@ const TAB_ITEMS: Array<{
|
||||
icon: Banknote,
|
||||
kpiKey: "billing",
|
||||
metricKey: "successfulPaymentsMtd",
|
||||
permission: [FREIGHT_PERMS.bookings.view, FREIGHT_PERMS.payments.view],
|
||||
},
|
||||
{
|
||||
value: "operations",
|
||||
@@ -68,6 +75,12 @@ const TAB_ITEMS: Array<{
|
||||
icon: Train,
|
||||
kpiKey: "operations",
|
||||
metricKey: "trainsActive",
|
||||
permission: [
|
||||
FREIGHT_PERMS.trainScheduling.view,
|
||||
FREIGHT_PERMS.warehouseInventory.view,
|
||||
FREIGHT_PERMS.firstMile.view,
|
||||
FREIGHT_PERMS.lastMile.view,
|
||||
],
|
||||
},
|
||||
{
|
||||
value: "customers",
|
||||
@@ -75,6 +88,7 @@ const TAB_ITEMS: Array<{
|
||||
icon: Users,
|
||||
kpiKey: "customers",
|
||||
metricKey: "totalCustomers",
|
||||
permission: [FREIGHT_PERMS.customers.view],
|
||||
},
|
||||
{
|
||||
value: "staff",
|
||||
@@ -82,6 +96,12 @@ const TAB_ITEMS: Array<{
|
||||
icon: UserCheck,
|
||||
kpiKey: "staff",
|
||||
metricKey: "activeEmployees",
|
||||
permission: [
|
||||
FREIGHT_PERMS.admin,
|
||||
FREIGHT_PERMS.staff.roles.view,
|
||||
FREIGHT_PERMS.staff.employeeRegistration.view,
|
||||
FREIGHT_PERMS.staff.roleAssignment.view,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -98,7 +118,19 @@ const OverviewPage = () => {
|
||||
const [range, setRange] = useState<OverviewRange>("30d");
|
||||
const [activeTab, setActiveTab] = useState<OverviewTabKey>("bookings");
|
||||
const queryClient = useQueryClient();
|
||||
const { data: summary, isLoading, isError, refetch, isFetching } = useOverview(range);
|
||||
const { user } = useAuth();
|
||||
const { data: summary, isLoading, isError, error, refetch, isFetching } = useOverview(range);
|
||||
|
||||
// Permission-scoped view: only tabs the user may see; a restricted role
|
||||
// (e.g. operations) gets a summary 403 — that is not a connection problem.
|
||||
const visibleTabs = TAB_ITEMS.filter((tab) =>
|
||||
tab.permission.some((key) => hasPermission(user, key)),
|
||||
);
|
||||
const currentTab = visibleTabs.some((t) => t.value === activeTab)
|
||||
? activeTab
|
||||
: visibleTabs[0]?.value;
|
||||
const accessDenied =
|
||||
(error as { response?: { status?: number } } | null)?.response?.status === 403;
|
||||
|
||||
const handleRefresh = () => {
|
||||
void refetch();
|
||||
@@ -126,7 +158,7 @@ const OverviewPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
{isError && !accessDenied && (
|
||||
<Alert
|
||||
icon={<AlertCircle size={16} />}
|
||||
color="red"
|
||||
@@ -142,48 +174,52 @@ const OverviewPage = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(value) => setActiveTab((value as OverviewTabKey) ?? "bookings")}
|
||||
variant="pills"
|
||||
color="edr-green"
|
||||
keepMounted={false}
|
||||
classNames={{ list: "ov-tablist", tab: "ov-tab" }}
|
||||
>
|
||||
<Tabs.List>
|
||||
{TAB_ITEMS.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.value;
|
||||
return (
|
||||
<Tabs.Tab
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
leftSection={<Icon size={17} />}
|
||||
rightSection={
|
||||
summary ? (
|
||||
<Badge
|
||||
size="sm"
|
||||
radius="sm"
|
||||
variant={isActive ? "white" : "light"}
|
||||
color={isActive ? "edr-green" : "gray"}
|
||||
>
|
||||
{getTabBadge(tab)}
|
||||
</Badge>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{tab.label}
|
||||
</Tabs.Tab>
|
||||
);
|
||||
})}
|
||||
</Tabs.List>
|
||||
{visibleTabs.length > 0 && (
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
onChange={(value) =>
|
||||
setActiveTab((value as OverviewTabKey) ?? visibleTabs[0].value)
|
||||
}
|
||||
variant="pills"
|
||||
color="edr-green"
|
||||
keepMounted={false}
|
||||
classNames={{ list: "ov-tablist", tab: "ov-tab" }}
|
||||
>
|
||||
<Tabs.List>
|
||||
{visibleTabs.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = currentTab === tab.value;
|
||||
return (
|
||||
<Tabs.Tab
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
leftSection={<Icon size={17} />}
|
||||
rightSection={
|
||||
summary ? (
|
||||
<Badge
|
||||
size="sm"
|
||||
radius="sm"
|
||||
variant={isActive ? "white" : "light"}
|
||||
color={isActive ? "edr-green" : "gray"}
|
||||
>
|
||||
{getTabBadge(tab)}
|
||||
</Badge>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{tab.label}
|
||||
</Tabs.Tab>
|
||||
);
|
||||
})}
|
||||
</Tabs.List>
|
||||
|
||||
{TAB_ITEMS.map((tab) => (
|
||||
<Tabs.Panel key={tab.value} value={tab.value} pt="lg">
|
||||
<OverviewTabContent tab={tab.value} range={range} />
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
{visibleTabs.map((tab) => (
|
||||
<Tabs.Panel key={tab.value} value={tab.value} pt="lg">
|
||||
<OverviewTabContent tab={tab.value} range={range} />
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
<Paper p="lg" radius="lg" withBorder>
|
||||
<OverviewQuickLinks />
|
||||
|
||||
@@ -936,6 +936,12 @@ const FirstMilePage = () => {
|
||||
};
|
||||
|
||||
const openBulkAssign = () => {
|
||||
// A single selection has full booking context (details, container list) —
|
||||
// use the richer single-record flow instead of the blank bulk form.
|
||||
if (selectedIds.length === 1) {
|
||||
openAssign(selectedIds[0]);
|
||||
return;
|
||||
}
|
||||
setBulkMode(true);
|
||||
setActiveId(null);
|
||||
setVehicleRows([{ vehicleId: null, containerNumber: "" }]);
|
||||
|
||||
@@ -1066,6 +1066,12 @@ const LastMilePage = () => {
|
||||
};
|
||||
|
||||
const openBulkAssign = () => {
|
||||
// A single selection has full booking context (details, container list) —
|
||||
// use the richer single-record flow instead of the blank bulk form.
|
||||
if (selectedIds.length === 1) {
|
||||
openAssign(selectedIds[0]);
|
||||
return;
|
||||
}
|
||||
setBulkMode(true);
|
||||
setActiveId(null);
|
||||
setVehicleRows([{ vehicleId: null, containerNumbers: [] }]);
|
||||
|
||||
Reference in New Issue
Block a user