refactor(dashboard): extract scope hook and header actions to satisfy governance caps

Split DashboardPage's scope/paging state into useDashboardScope and the header
controls into DashboardHeaderActions, and flatten parseDashboardDrilldownParams'
hasAny check, keeping every function under the complexity/line-count gates.
This commit is contained in:
Codex Review Integration 2026-09-17 03:14:15 -03:00
parent 1271d3c11e
commit 827f8ca8ba
4 changed files with 185 additions and 102 deletions

View file

@ -0,0 +1,67 @@
import { Box, Button, CircularProgress } from "@mui/material";
import { Users } from "lucide-react";
import { Text } from "@/components/ui/text";
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
import type { DashboardRangeKey } from "@/domain/dashboard/types/dashboard-range";
import { DashboardRangeToggle } from "./dashboard-range-toggle";
import { DashboardScopePicker } from "./dashboard-scope-picker";
import type { DashboardScope } from "./dashboard-scope";
interface DashboardHeaderActionsProps {
isRefreshing: boolean;
showReset: boolean;
onResetFilters: () => void;
canViewAll: boolean;
scope: DashboardScope;
onScopeChange: (scope: DashboardScope) => void;
dispatcherUsers: WorkOrderUser[];
dispatcherUsersLoading: boolean;
currentUserId: string | number | undefined;
range: DashboardRangeKey;
onRangeChange: (range: DashboardRangeKey) => void;
}
export function DashboardHeaderActions({
isRefreshing,
showReset,
onResetFilters,
canViewAll,
scope,
onScopeChange,
dispatcherUsers,
dispatcherUsersLoading,
currentUserId,
range,
onRangeChange,
}: DashboardHeaderActionsProps) {
return (
<Box className="flex items-center gap-2">
{isRefreshing && <CircularProgress size={16} />}
{showReset && (
<Button size="small" onClick={onResetFilters}>
Reset filters
</Button>
)}
{canViewAll ? (
<DashboardScopePicker
scope={scope}
onChange={onScopeChange}
users={dispatcherUsers}
usersLoading={dispatcherUsersLoading}
currentUserId={currentUserId}
/>
) : (
<Box
className="flex shrink-0 items-center gap-1.5 whitespace-nowrap"
style={{ fontSize: 13, fontWeight: 500, color: "var(--muted-foreground)" }}
>
<Users size={13} aria-hidden style={{ color: "var(--muted-foreground)" }} />
<Text variant="description" tone="muted">
My WOs
</Text>
</Box>
)}
<DashboardRangeToggle value={range} onChange={onRangeChange} />
</Box>
);
}

View file

@ -0,0 +1,87 @@
import { useState } from "react";
import { canViewAllDispatchersOnDashboard } from "@/lib/auth/user-utils";
import { useAuthContext } from "@/providers/auth-context";
import {
DEFAULT_DASHBOARD_RANGE,
type DashboardRangeKey,
} from "@/domain/dashboard/types/dashboard-range";
import { DASHBOARD_SCOPE_ALL, scopeDispatcherId, type DashboardScope } from "./dashboard-scope";
export interface DashboardScopeState {
canViewAll: boolean;
currentUserId: string | number | undefined;
range: DashboardRangeKey;
scope: DashboardScope;
/** The `dispatcherId` query param for the scoped endpoints (null = company-wide). */
dispatcherId: string | null;
workloadPage: number;
performancePage: number;
showReset: boolean;
setWorkloadPage: (page: number) => void;
setPerformancePage: (page: number) => void;
changeRange: (next: DashboardRangeKey) => void;
changeScope: (next: DashboardScope) => void;
resetFilters: () => void;
}
/**
* SH-336 dashboard scope/paging state. A Dispatcher (no
* `viewAllDispatchersOnDashboard`) is locked to their own work orders and sends
* no dispatcherId; Scheduler/Admin drive an explicit scope through the picker.
*/
export function useDashboardScope(): DashboardScopeState {
const { user } = useAuthContext();
const canViewAll = canViewAllDispatchersOnDashboard(user?.userRoles);
const [range, setRange] = useState<DashboardRangeKey>(DEFAULT_DASHBOARD_RANGE);
const [scope, setScope] = useState<DashboardScope>(DASHBOARD_SCOPE_ALL);
const [workloadPage, setWorkloadPage] = useState(1);
const [performancePage, setPerformancePage] = useState(1);
const dispatcherId = canViewAll ? scopeDispatcherId(scope, user?.id) : null;
// A range or scope change re-scopes both dispatcher tables, so their page must
// reset — otherwise a switch while on page 3 lands on an out-of-range empty
// page that reads as a false "no dispatchers" state.
const resetPages = () => {
setWorkloadPage(1);
setPerformancePage(1);
};
const changeRange = (next: DashboardRangeKey) => {
setRange(next);
resetPages();
};
const changeScope = (next: DashboardScope) => {
setScope(next);
resetPages();
};
const resetFilters = () => {
setRange(DEFAULT_DASHBOARD_RANGE);
setScope(DASHBOARD_SCOPE_ALL);
resetPages();
};
// A Dispatcher's forced own-WOs scope never triggers the reset affordance (only
// an active date range can, for that role); a Scheduler/Admin's non-All scope does.
const scopeIsFiltered = canViewAll && scope.mode !== "all";
const showReset = range !== DEFAULT_DASHBOARD_RANGE || scopeIsFiltered;
return {
canViewAll,
currentUserId: user?.id,
range,
scope,
dispatcherId,
workloadPage,
performancePage,
showReset,
setWorkloadPage,
setPerformancePage,
changeRange,
changeScope,
resetFilters,
};
}

View file

@ -1,32 +1,18 @@
import { useState } from "react";
import { useNavigate } from "react-router";
import EventOutlinedIcon from "@mui/icons-material/EventOutlined";
import PendingActionsIcon from "@mui/icons-material/PendingActions";
import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined";
import { Box, Button, CircularProgress } from "@mui/material";
import { Users } from "lucide-react";
import { Box } from "@mui/material";
import { PageHeader } from "@/components/ui/page-header";
import { Text } from "@/components/ui/text";
import { canViewAllDispatchersOnDashboard } from "@/lib/auth/user-utils";
import { useAuthContext } from "@/providers/auth-context";
import { DashboardHeaderActions } from "@/app/(protected)/dashboard/_components/dashboard-header-actions";
import { DashboardKpiCard } from "@/app/(protected)/dashboard/_components/dashboard-kpi-card";
import { DashboardRangeToggle } from "@/app/(protected)/dashboard/_components/dashboard-range-toggle";
import { DashboardTrendCard } from "@/app/(protected)/dashboard/_components/dashboard-trend-card";
import { DashboardScopePicker } from "@/app/(protected)/dashboard/_components/dashboard-scope-picker";
import {
DASHBOARD_SCOPE_ALL,
scopeDispatcherId,
type DashboardScope,
} from "@/app/(protected)/dashboard/_components/dashboard-scope";
import { DispatcherTablesSection } from "@/app/(protected)/dashboard/_components/dispatcher-tables-section";
import { StatusDistributionCard } from "@/app/(protected)/dashboard/_components/status-distribution-card";
import { useDashboardScope } from "@/app/(protected)/dashboard/_components/use-dashboard-scope";
import { VendorInsightsCard } from "@/app/(protected)/dashboard/_components/vendor-insights-card";
import { WorkOrderBreakdownCard } from "@/app/(protected)/dashboard/_components/work-order-breakdown-card";
import { WorkOrdersByRegionCard } from "@/app/(protected)/dashboard/_components/work-orders-by-region-card";
import {
DEFAULT_DASHBOARD_RANGE,
type DashboardRangeKey,
} from "@/domain/dashboard/types/dashboard-range";
import { useDashboardPerformance } from "@/domain/dashboard/use-cases/use-dashboard-performance";
import { useDashboardRegions } from "@/domain/dashboard/use-cases/use-dashboard-regions";
import { useDashboardStats } from "@/domain/dashboard/use-cases/use-dashboard-stats";
@ -42,59 +28,16 @@ import {
export default function DashboardPage() {
const navigate = useNavigate();
const { user } = useAuthContext();
// SH-336: only Scheduler/Admin may broaden the Dashboard past their own work
// orders. A Dispatcher is locked to their own WOs (scoped server-side by
// backend#126) and never sees the picker.
const canViewAll = canViewAllDispatchersOnDashboard(user?.userRoles);
const [range, setRange] = useState<DashboardRangeKey>(DEFAULT_DASHBOARD_RANGE);
const [scope, setScope] = useState<DashboardScope>(DASHBOARD_SCOPE_ALL);
const [workloadPage, setWorkloadPage] = useState(1);
const [performancePage, setPerformancePage] = useState(1);
const scope = useDashboardScope();
const { canViewAll, range, dispatcherId, workloadPage, performancePage } = scope;
const dispatcherUsers = useWorkOrderUsers();
// A Dispatcher sends no dispatcherId (the server already scopes them); only the
// picker, available to Scheduler/Admin, drives an explicit scope.
const dispatcherId = canViewAll ? scopeDispatcherId(scope, user?.id) : null;
const stats = useDashboardStats(range, dispatcherId);
const workload = useDashboardWorkload(range, workloadPage, dispatcherId, canViewAll);
const performance = useDashboardPerformance(range, performancePage, dispatcherId, canViewAll);
const regions = useDashboardRegions(range, dispatcherId);
const vendors = useDashboardVendorInsights();
// A range change re-scopes both dispatcher tables, so their page must reset —
// otherwise a switch while on page 3 lands on an out-of-range empty page that
// reads as a false "no dispatchers" state.
const resetDispatcherPages = () => {
setWorkloadPage(1);
setPerformancePage(1);
};
const changeRange = (next: DashboardRangeKey) => {
setRange(next);
resetDispatcherPages();
};
const changeScope = (next: DashboardScope) => {
setScope(next);
resetDispatcherPages();
};
const resetFilters = () => {
setRange(DEFAULT_DASHBOARD_RANGE);
setScope(DASHBOARD_SCOPE_ALL);
resetDispatcherPages();
};
// A Dispatcher's forced own-WOs scope never triggers the reset affordance (there
// is nothing for them to reset but the date range); a Scheduler/Admin's non-All
// scope does.
const scopeIsFiltered = canViewAll && scope.mode !== "all";
const showReset = range !== DEFAULT_DASHBOARD_RANGE || scopeIsFiltered;
const isRefreshing =
[stats, workload, performance, regions, vendors].some((query) => query.isFetching) &&
!stats.isLoading;
@ -104,34 +47,19 @@ export default function DashboardPage() {
<PageHeader
title="Dashboard"
actions={
<Box className="flex items-center gap-2">
{isRefreshing && <CircularProgress size={16} />}
{showReset && (
<Button size="small" onClick={resetFilters}>
Reset filters
</Button>
)}
{canViewAll ? (
<DashboardScopePicker
scope={scope}
onChange={changeScope}
users={dispatcherUsers.data ?? []}
usersLoading={dispatcherUsers.isLoading}
currentUserId={user?.id}
/>
) : (
<Box
className="flex shrink-0 items-center gap-1.5 whitespace-nowrap"
style={{ fontSize: 13, fontWeight: 500, color: "var(--muted-foreground)" }}
>
<Users size={13} aria-hidden style={{ color: "var(--muted-foreground)" }} />
<Text variant="description" tone="muted">
My WOs
</Text>
</Box>
)}
<DashboardRangeToggle value={range} onChange={changeRange} />
</Box>
<DashboardHeaderActions
isRefreshing={isRefreshing}
showReset={scope.showReset}
onResetFilters={scope.resetFilters}
canViewAll={canViewAll}
scope={scope.scope}
onScopeChange={scope.changeScope}
dispatcherUsers={dispatcherUsers.data ?? []}
dispatcherUsersLoading={dispatcherUsers.isLoading}
currentUserId={scope.currentUserId}
range={range}
onRangeChange={scope.changeRange}
/>
}
/>
<Box className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
@ -189,8 +117,8 @@ export default function DashboardPage() {
performance={performance}
workloadPage={workloadPage}
performancePage={performancePage}
onWorkloadPageChange={setWorkloadPage}
onPerformancePageChange={setPerformancePage}
onWorkloadPageChange={scope.setWorkloadPage}
onPerformancePageChange={scope.setPerformancePage}
/>
)}
</Box>

View file

@ -75,16 +75,17 @@ export function parseDashboardDrilldownParams(
const upliftStatuses = parseUpliftStatuses(params.get("upliftStatuses"));
const hasUplift = params.get("hasUplift") === "1";
const hasAny =
hasDateRange ||
avetaOnly ||
docs.length > 0 ||
statuses.length > 0 ||
types.length > 0 ||
dispatchers.length > 0 ||
regions.length > 0 ||
upliftStatuses.length > 0 ||
hasUplift;
const hasAny = [
hasDateRange,
avetaOnly,
docs.length,
statuses.length,
types.length,
dispatchers.length,
regions.length,
upliftStatuses.length,
hasUplift,
].some(Boolean);
if (!hasAny) {
return null;