mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-03 20:13:15 +00:00
- SH-336: add the dispatcher-scope picker (All / My WOs / individual) for Scheduler/Admin via a new viewAllDispatchersOnDashboard role gate, a static 'My WOs' label for Dispatcher, and hide the dispatcher tables (and skip their queries) when the viewer cannot see all dispatchers. Scope drives dispatcherId on stats, workload, performance, and regions (SH-347 contract). - SH-352: add the Status Distribution module with its empty state and status drill-down. - SH-347: show Assigned/Completed counts and colour the completion rate (green >=90, amber 70-89, red <70) on Dispatcher Performance. - SH-348: drill a region bar into the board region filter; hold the unconfirmed 'Unmapped/Other' bucket off the chart. - SH-349: label Vendor Insights as all-time, company-wide. - Make the drilldown-filters effect fire only on URL change; add a deterministic clock to the status-drilldown test; add the dashboard visual regression spec and baselines.
118 lines
3.6 KiB
TypeScript
118 lines
3.6 KiB
TypeScript
import { useMemo } from "react";
|
|
import { Popover, Skeleton } from "@mui/material";
|
|
import { ChevronDown, Users } from "lucide-react";
|
|
import type { WorkOrderUser } from "@/domain/work-orders/types/work-order";
|
|
import { CheckboxMenuItem } from "@/components/ui/checkbox-menu-item";
|
|
import { MenuAnchorButton } from "@/components/ui/menu-anchor-button";
|
|
import { useMenuAnchor } from "@/hooks/use-menu-anchor";
|
|
import type { DashboardScope } from "./dashboard-scope";
|
|
|
|
type DashboardScopePickerProps = {
|
|
scope: DashboardScope;
|
|
onChange: (scope: DashboardScope) => void;
|
|
users: WorkOrderUser[];
|
|
usersLoading?: boolean;
|
|
currentUserId?: string | number;
|
|
};
|
|
|
|
export function DashboardScopePicker({
|
|
scope,
|
|
onChange,
|
|
users,
|
|
usersLoading = false,
|
|
currentUserId,
|
|
}: DashboardScopePickerProps) {
|
|
const { anchorEl, open, setAnchor, close } = useMenuAnchor();
|
|
const meId = currentUserId != null && currentUserId !== "" ? String(currentUserId) : null;
|
|
|
|
const dispatchers = useMemo(
|
|
() => [...users].sort((a, b) => a.name.localeCompare(b.name)),
|
|
[users],
|
|
);
|
|
|
|
const label = useMemo(() => {
|
|
if (scope.mode === "all") return "All dispatchers";
|
|
if (scope.mode === "mine") return "My WOs";
|
|
return dispatchers.find((user) => String(user.id) === scope.id)?.name ?? "Dispatcher";
|
|
}, [scope, dispatchers]);
|
|
|
|
const select = (next: DashboardScope) => {
|
|
onChange(next);
|
|
close();
|
|
};
|
|
|
|
if (usersLoading && users.length === 0) {
|
|
return (
|
|
<Skeleton
|
|
variant="rounded"
|
|
width={140}
|
|
height={18}
|
|
animation="wave"
|
|
aria-label="Loading dispatchers"
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<MenuAnchorButton
|
|
onAnchor={setAnchor}
|
|
open={open}
|
|
className="flex w-auto shrink-0 items-center gap-1.5 whitespace-nowrap transition-colors"
|
|
style={{ fontSize: 13, fontWeight: 500, color: "var(--muted-foreground)" }}
|
|
aria-haspopup="listbox"
|
|
>
|
|
<Users size={13} aria-hidden style={{ color: "var(--muted-foreground)" }} />
|
|
{label}
|
|
<ChevronDown size={13} aria-hidden style={{ color: "var(--muted-foreground)" }} />
|
|
</MenuAnchorButton>
|
|
|
|
<Popover
|
|
open={open}
|
|
anchorEl={anchorEl}
|
|
onClose={close}
|
|
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
|
transformOrigin={{ vertical: "top", horizontal: "left" }}
|
|
slotProps={{
|
|
paper: {
|
|
className: "mt-1 w-[220px] rounded-lg border border-border p-1.5 shadow-md",
|
|
},
|
|
}}
|
|
>
|
|
<CheckboxMenuItem
|
|
checked={scope.mode === "all"}
|
|
label="All dispatchers"
|
|
onClick={() => select({ mode: "all" })}
|
|
/>
|
|
|
|
{meId != null && (
|
|
<CheckboxMenuItem
|
|
checked={scope.mode === "mine"}
|
|
label="My WOs only"
|
|
labelClassName="font-medium text-primary"
|
|
className="mb-1 pb-2"
|
|
style={{ borderBottom: "1px solid var(--color-border)" }}
|
|
onClick={() => select({ mode: "mine" })}
|
|
/>
|
|
)}
|
|
|
|
{dispatchers.map((user) => {
|
|
const id = String(user.id);
|
|
return (
|
|
<CheckboxMenuItem
|
|
key={id}
|
|
checked={scope.mode === "individual" && scope.id === id}
|
|
label={user.name}
|
|
right={
|
|
meId === id ? (
|
|
<span className="text-[10px] text-muted-foreground">me</span>
|
|
) : undefined
|
|
}
|
|
onClick={() => select({ mode: "individual", id })}
|
|
/>
|
|
);
|
|
})}
|
|
</Popover>
|
|
</>
|
|
);
|
|
}
|