mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
Merge 8bb4c0c512 into 8b58eb5f64
This commit is contained in:
commit
4d57927ee4
25 changed files with 401 additions and 67 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 145 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 125 KiB |
|
|
@ -191,7 +191,11 @@ async function mockDashboardApi(page: Page, userRoles: string) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname.includes("/Dashboard/Stats")) return fulfillJson(route, { data: STATS });
|
if (pathname.includes("/Dashboard/Stats")) {
|
||||||
|
// The server reports no unassigned work for a dispatcher-scoped view.
|
||||||
|
const unassigned = userRoles === "Dispatcher" ? 0 : 11;
|
||||||
|
return fulfillJson(route, { data: { ...STATS, unassigned } });
|
||||||
|
}
|
||||||
if (pathname.includes("/Dashboard/Workload")) return fulfillJson(route, { data: WORKLOAD });
|
if (pathname.includes("/Dashboard/Workload")) return fulfillJson(route, { data: WORKLOAD });
|
||||||
if (pathname.includes("/Dashboard/Performance"))
|
if (pathname.includes("/Dashboard/Performance"))
|
||||||
return fulfillJson(route, { data: PERFORMANCE });
|
return fulfillJson(route, { data: PERFORMANCE });
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import AssignmentLateOutlinedIcon from "@mui/icons-material/AssignmentLateOutlined";
|
||||||
|
import EventOutlinedIcon from "@mui/icons-material/EventOutlined";
|
||||||
|
import PendingActionsIcon from "@mui/icons-material/PendingActions";
|
||||||
|
import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { DashboardKpiCard } from "@/app/(protected)/dashboard/_components/dashboard-kpi-card";
|
||||||
|
import type { DashboardRangeKey } from "@/domain/dashboard/types/dashboard-range";
|
||||||
|
import type { DashboardStats } from "@/domain/dashboard/types/dashboard-stats";
|
||||||
|
import {
|
||||||
|
avetaPendingDrilldownSearch,
|
||||||
|
scheduledTomorrowDrilldownSearch,
|
||||||
|
unassignedDrilldownSearch,
|
||||||
|
UPLIFTS_ROUTE,
|
||||||
|
workOrderDrilldownUrl,
|
||||||
|
} from "@/domain/dashboard/utils/work-order-drilldown-links";
|
||||||
|
|
||||||
|
interface DashboardKpiCardsProps {
|
||||||
|
stats?: DashboardStats;
|
||||||
|
loading: boolean;
|
||||||
|
range: DashboardRangeKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DashboardKpiCards({ stats, loading, range }: DashboardKpiCardsProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const scheduledTomorrow = stats?.scheduledTomorrow ?? 0;
|
||||||
|
const pendingUplifts = stats?.pendingUplifts ?? 0;
|
||||||
|
const avetaPending = stats?.avetaPending ?? 0;
|
||||||
|
const unassigned = stats?.unassigned ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
|
<DashboardKpiCard
|
||||||
|
title="Scheduled Tomorrow"
|
||||||
|
icon={<EventOutlinedIcon />}
|
||||||
|
tone="primary"
|
||||||
|
value={stats?.scheduledTomorrow}
|
||||||
|
loading={loading}
|
||||||
|
active={scheduledTomorrow > 0}
|
||||||
|
onSelect={() => navigate(workOrderDrilldownUrl(scheduledTomorrowDrilldownSearch()))}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
title="Pending Uplifts"
|
||||||
|
icon={<PendingActionsIcon />}
|
||||||
|
tone="info"
|
||||||
|
value={stats?.pendingUplifts}
|
||||||
|
loading={loading}
|
||||||
|
active={pendingUplifts > 0}
|
||||||
|
onSelect={() => navigate(UPLIFTS_ROUTE)}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
title="Aveta Pending"
|
||||||
|
icon={<ShieldOutlinedIcon />}
|
||||||
|
tone="warning"
|
||||||
|
value={stats?.avetaPending}
|
||||||
|
loading={loading}
|
||||||
|
active={avetaPending > 0}
|
||||||
|
onSelect={() => navigate(workOrderDrilldownUrl(avetaPendingDrilldownSearch()))}
|
||||||
|
/>
|
||||||
|
<DashboardKpiCard
|
||||||
|
title="Unassigned"
|
||||||
|
icon={<AssignmentLateOutlinedIcon />}
|
||||||
|
tone="warning"
|
||||||
|
value={stats?.unassigned}
|
||||||
|
loading={loading}
|
||||||
|
active={unassigned > 0}
|
||||||
|
onSelect={() => navigate(workOrderDrilldownUrl(unassignedDrilldownSearch(range)))}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
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 } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { PageHeader } from "@/components/ui/page-header";
|
import { PageHeader } from "@/components/ui/page-header";
|
||||||
import { DashboardHeaderActions } from "@/app/(protected)/dashboard/_components/dashboard-header-actions";
|
import { DashboardHeaderActions } from "@/app/(protected)/dashboard/_components/dashboard-header-actions";
|
||||||
import { DashboardKpiCard } from "@/app/(protected)/dashboard/_components/dashboard-kpi-card";
|
import { DashboardKpiCards } from "@/app/(protected)/dashboard/_components/dashboard-kpi-cards";
|
||||||
import { DashboardTrendCard } from "@/app/(protected)/dashboard/_components/dashboard-trend-card";
|
import { DashboardTrendCard } from "@/app/(protected)/dashboard/_components/dashboard-trend-card";
|
||||||
import { DispatcherTablesSection } from "@/app/(protected)/dashboard/_components/dispatcher-tables-section";
|
import { DispatcherTablesSection } from "@/app/(protected)/dashboard/_components/dispatcher-tables-section";
|
||||||
import { StatusDistributionCard } from "@/app/(protected)/dashboard/_components/status-distribution-card";
|
import { StatusDistributionCard } from "@/app/(protected)/dashboard/_components/status-distribution-card";
|
||||||
|
|
@ -19,15 +15,8 @@ import { useDashboardStats } from "@/domain/dashboard/use-cases/use-dashboard-st
|
||||||
import { useDashboardVendorInsights } from "@/domain/dashboard/use-cases/use-dashboard-vendor-insights";
|
import { useDashboardVendorInsights } from "@/domain/dashboard/use-cases/use-dashboard-vendor-insights";
|
||||||
import { useDashboardWorkload } from "@/domain/dashboard/use-cases/use-dashboard-workload";
|
import { useDashboardWorkload } from "@/domain/dashboard/use-cases/use-dashboard-workload";
|
||||||
import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list";
|
import { useWorkOrderUsers } from "@/domain/work-orders/use-cases/use-work-orders-list";
|
||||||
import {
|
|
||||||
avetaPendingDrilldownSearch,
|
|
||||||
scheduledTomorrowDrilldownSearch,
|
|
||||||
UPLIFTS_ROUTE,
|
|
||||||
workOrderDrilldownUrl,
|
|
||||||
} from "@/domain/dashboard/utils/work-order-drilldown-links";
|
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const scope = useDashboardScope();
|
const scope = useDashboardScope();
|
||||||
const { canViewAll, range, dispatcherId, workloadPage, performancePage } = scope;
|
const { canViewAll, range, dispatcherId, workloadPage, performancePage } = scope;
|
||||||
|
|
||||||
|
|
@ -62,35 +51,7 @@ export default function DashboardPage() {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Box className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<DashboardKpiCards stats={stats.data} loading={stats.isLoading} range={range} />
|
||||||
<DashboardKpiCard
|
|
||||||
title="Scheduled Tomorrow"
|
|
||||||
icon={<EventOutlinedIcon />}
|
|
||||||
tone="primary"
|
|
||||||
value={stats.data?.scheduledTomorrow}
|
|
||||||
loading={stats.isLoading}
|
|
||||||
active={(stats.data?.scheduledTomorrow ?? 0) > 0}
|
|
||||||
onSelect={() => navigate(workOrderDrilldownUrl(scheduledTomorrowDrilldownSearch()))}
|
|
||||||
/>
|
|
||||||
<DashboardKpiCard
|
|
||||||
title="Pending Uplifts"
|
|
||||||
icon={<PendingActionsIcon />}
|
|
||||||
tone="info"
|
|
||||||
value={stats.data?.pendingUplifts}
|
|
||||||
loading={stats.isLoading}
|
|
||||||
active={(stats.data?.pendingUplifts ?? 0) > 0}
|
|
||||||
onSelect={() => navigate(UPLIFTS_ROUTE)}
|
|
||||||
/>
|
|
||||||
<DashboardKpiCard
|
|
||||||
title="Aveta Pending"
|
|
||||||
icon={<ShieldOutlinedIcon />}
|
|
||||||
tone="warning"
|
|
||||||
value={stats.data?.avetaPending}
|
|
||||||
loading={stats.isLoading}
|
|
||||||
active={(stats.data?.avetaPending ?? 0) > 0}
|
|
||||||
onSelect={() => navigate(workOrderDrilldownUrl(avetaPendingDrilldownSearch()))}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<Box className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
<WorkOrderBreakdownCard
|
<WorkOrderBreakdownCard
|
||||||
breakdown={stats.data?.breakdown ?? []}
|
breakdown={stats.data?.breakdown ?? []}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,12 @@ export function AdvDateRangeSection({ draft, setDraft }: AdvDateRangeSectionProp
|
||||||
key={option.key}
|
key={option.key}
|
||||||
label={option.label}
|
label={option.label}
|
||||||
selected={draft.dateRange === option.key}
|
selected={draft.dateRange === option.key}
|
||||||
onClick={() => setDraft((prev) => ({ ...prev, dateRange: option.key }))}
|
onClick={() =>
|
||||||
|
setDraft((prev) => ({
|
||||||
|
...prev,
|
||||||
|
dateRange: prev.dateRange === option.key ? null : option.key,
|
||||||
|
}))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<AdvFilterPill
|
<AdvFilterPill
|
||||||
|
|
|
||||||
|
|
@ -251,6 +251,7 @@ function mapDashboardStats(raw: unknown): DashboardStats {
|
||||||
scheduledTomorrow: toNumber(readField(data, "scheduledTomorrow")),
|
scheduledTomorrow: toNumber(readField(data, "scheduledTomorrow")),
|
||||||
pendingUplifts: toNumber(readField(data, "pendingUplifts")),
|
pendingUplifts: toNumber(readField(data, "pendingUplifts")),
|
||||||
avetaPending: toNumber(readField(data, "avetaPending")),
|
avetaPending: toNumber(readField(data, "avetaPending")),
|
||||||
|
unassigned: toNumber(readField(data, "unassigned")),
|
||||||
breakdown: mapBreakdown(readField(data, "breakdown") ?? readField(data, "workOrderBreakdown")),
|
breakdown: mapBreakdown(readField(data, "breakdown") ?? readField(data, "workOrderBreakdown")),
|
||||||
statusDistribution: mapStatusDistribution(readField(data, "statusDistribution")),
|
statusDistribution: mapStatusDistribution(readField(data, "statusDistribution")),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ export interface DashboardStats {
|
||||||
scheduledTomorrow: number;
|
scheduledTomorrow: number;
|
||||||
pendingUplifts: number;
|
pendingUplifts: number;
|
||||||
avetaPending: number;
|
avetaPending: number;
|
||||||
|
/** Open WOs with no dispatcher in the selected period. */
|
||||||
|
unassigned: number;
|
||||||
breakdown: DashboardBreakdownRow[];
|
breakdown: DashboardBreakdownRow[];
|
||||||
statusDistribution: DashboardStatusBucketRow[];
|
statusDistribution: DashboardStatusBucketRow[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { ALL_WIZARD_STATUSES } from "@/domain/work-orders/types/work-order-wizard";
|
import { ALL_WIZARD_STATUSES } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
import { WO_TYPES, type WOType } from "@/domain/work-orders/types/work-order-wizard";
|
import { WO_TYPES, type WOType } from "@/domain/work-orders/types/work-order-wizard";
|
||||||
import type { DashboardRangeKey } from "@/domain/dashboard/types/dashboard-range";
|
import type { DashboardRangeKey } from "@/domain/dashboard/types/dashboard-range";
|
||||||
|
import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
|
||||||
import {
|
import {
|
||||||
addDaysIso,
|
addDaysIso,
|
||||||
businessTodayIso,
|
businessTodayIso,
|
||||||
|
|
@ -77,6 +78,18 @@ export function workOrderRegionDrilldownSearch(
|
||||||
return search;
|
return search;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Unassigned tile's list: open WOs with no dispatcher in the
|
||||||
|
* Dashboard period, the same set the backend counts. All time sends no dates,
|
||||||
|
* so the board searches every week, undated WOs included.
|
||||||
|
*/
|
||||||
|
export function unassignedDrilldownSearch(range: DashboardRangeKey): URLSearchParams {
|
||||||
|
const search = workOrderRangeDrilldownSearch(range);
|
||||||
|
search.set("dispatchers", ASSIGNEE_FILTER_UNASSIGNED);
|
||||||
|
search.set("statuses", OPEN_WIZARD_STATUSES.join(","));
|
||||||
|
return search;
|
||||||
|
}
|
||||||
|
|
||||||
export function scheduledTomorrowDrilldownSearch(
|
export function scheduledTomorrowDrilldownSearch(
|
||||||
today: string = businessTodayIso(),
|
today: string = businessTodayIso(),
|
||||||
): URLSearchParams {
|
): URLSearchParams {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,11 @@ import {
|
||||||
type WorkOrdersListParams,
|
type WorkOrdersListParams,
|
||||||
type WorkOrdersListResult,
|
type WorkOrdersListResult,
|
||||||
} from "@/domain/work-orders/types/work-order-list";
|
} from "@/domain/work-orders/types/work-order-list";
|
||||||
import { buildBoardSearchParamsRecord } from "@/domain/work-orders/utils/board-query-params";
|
import {
|
||||||
|
ADVANCED_SEARCH_ALL_WEEKS_FROM,
|
||||||
|
ADVANCED_SEARCH_ALL_WEEKS_TO,
|
||||||
|
buildBoardSearchParamsRecord,
|
||||||
|
} from "@/domain/work-orders/utils/board-query-params";
|
||||||
|
|
||||||
function buildSearchParams(params: WorkOrdersListParams): Record<string, string | number> {
|
function buildSearchParams(params: WorkOrdersListParams): Record<string, string | number> {
|
||||||
const searchParams: Record<string, string | number> = {
|
const searchParams: Record<string, string | number> = {
|
||||||
|
|
@ -223,8 +227,13 @@ export const workOrdersApi = {
|
||||||
"Unable to verify the work order number: search result set exceeded the safety page limit.",
|
"Unable to verify the work order number: search result set exceeded the safety page limit.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Every week plus undated rows: a WO# must be unique regardless of schedule.
|
||||||
const result = await workOrdersApi.searchBoard({
|
const result = await workOrdersApi.searchBoard({
|
||||||
search: normalized,
|
search: normalized,
|
||||||
|
datePreset: "Custom",
|
||||||
|
dateFrom: ADVANCED_SEARCH_ALL_WEEKS_FROM,
|
||||||
|
dateTo: ADVANCED_SEARCH_ALL_WEEKS_TO,
|
||||||
|
includeDateless: true,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ export type DateRangeKey =
|
||||||
export type CompletionDocFilter = "No" | "Yes" | "NN";
|
export type CompletionDocFilter = "No" | "Yes" | "NN";
|
||||||
|
|
||||||
export interface WorkOrderAdvancedFilters {
|
export interface WorkOrderAdvancedFilters {
|
||||||
dateRange: DateRangeKey;
|
/** `null` = no date range selected: search every week. */
|
||||||
|
dateRange: DateRangeKey | null;
|
||||||
customFrom: string;
|
customFrom: string;
|
||||||
customTo: string;
|
customTo: string;
|
||||||
/** invariant: independent of DateRangeKey; never sent as datePreset=Unscheduled. */
|
/** invariant: independent of DateRangeKey; never sent as datePreset=Unscheduled. */
|
||||||
|
|
@ -48,7 +49,7 @@ export interface WorkOrderAdvancedFilters {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EMPTY_ADVANCED_FILTERS: WorkOrderAdvancedFilters = {
|
export const EMPTY_ADVANCED_FILTERS: WorkOrderAdvancedFilters = {
|
||||||
dateRange: "this-week",
|
dateRange: null,
|
||||||
customFrom: "",
|
customFrom: "",
|
||||||
customTo: "",
|
customTo: "",
|
||||||
unscheduledOnly: false,
|
unscheduledOnly: false,
|
||||||
|
|
|
||||||
|
|
@ -318,6 +318,11 @@ export interface WorkOrderBoardSearchQueryParams {
|
||||||
* legacy wire behavior.
|
* legacy wire behavior.
|
||||||
*/
|
*/
|
||||||
unscheduledOnly?: boolean;
|
unscheduledOnly?: boolean;
|
||||||
|
/**
|
||||||
|
* `true` adds open rows with no date to the range. Sent only when no
|
||||||
|
* date range is selected, so an explicit range strictly narrows the results.
|
||||||
|
*/
|
||||||
|
includeDateless?: boolean;
|
||||||
hasUplift?: boolean;
|
hasUplift?: boolean;
|
||||||
upliftStatuses?: string[];
|
upliftStatuses?: string[];
|
||||||
myWorkOrders?: boolean;
|
myWorkOrders?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ function severityLabel(level: WorkOrderSeverity): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendDateChip(chips: AdvancedFilterChip[], filters: WorkOrderAdvancedFilters) {
|
export function appendDateChip(chips: AdvancedFilterChip[], filters: WorkOrderAdvancedFilters) {
|
||||||
if (filters.dateRange !== "this-week") {
|
if (filters.dateRange !== null) {
|
||||||
const value =
|
const value =
|
||||||
filters.dateRange === "custom"
|
filters.dateRange === "custom"
|
||||||
? `${fmtChipDate(filters.customFrom) || "Any"} – ${fmtChipDate(filters.customTo) || "Any"}`
|
? `${fmtChipDate(filters.customFrom) || "Any"} – ${fmtChipDate(filters.customTo) || "Any"}`
|
||||||
|
|
@ -71,7 +71,7 @@ export function appendDateChip(chips: AdvancedFilterChip[], filters: WorkOrderAd
|
||||||
value,
|
value,
|
||||||
clear: (current) => ({
|
clear: (current) => ({
|
||||||
...current,
|
...current,
|
||||||
dateRange: "this-week",
|
dateRange: null,
|
||||||
customFrom: "",
|
customFrom: "",
|
||||||
customTo: "",
|
customTo: "",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,44 @@ import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-o
|
||||||
import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
|
import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
|
||||||
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
import type { WorkOrderTableRow } from "@/domain/work-orders/types/work-order-table-row";
|
||||||
import { dateRangeFor } from "@/domain/work-orders/utils/advanced-filters-utils";
|
import { dateRangeFor } from "@/domain/work-orders/utils/advanced-filters-utils";
|
||||||
|
import {
|
||||||
|
ADVANCED_SEARCH_ALL_WEEKS_FROM,
|
||||||
|
ADVANCED_SEARCH_ALL_WEEKS_TO,
|
||||||
|
} from "@/domain/work-orders/utils/board-query-params";
|
||||||
|
import { isTerminalCarryOverStatus } from "@/domain/work-orders/utils/carry-over-display";
|
||||||
|
import { addDaysIso } from "@/domain/work-orders/utils/wizard-date-utils";
|
||||||
|
|
||||||
type AdvancedFilterPredicate = (
|
type AdvancedFilterPredicate = (
|
||||||
row: WorkOrderTableRow,
|
row: WorkOrderTableRow,
|
||||||
filters: WorkOrderAdvancedFilters,
|
filters: WorkOrderAdvancedFilters,
|
||||||
) => boolean;
|
) => boolean;
|
||||||
|
|
||||||
|
const isOpenUndated = (row: WorkOrderTableRow): boolean =>
|
||||||
|
!row.scheduledOn && !isTerminalCarryOverStatus(row.status);
|
||||||
|
|
||||||
|
// Mirrors the board search (ApplyDateRangeFilter and ApplyUnscheduledOnlyFilter)
|
||||||
|
// so the page and /board/search keep the same rows: a row is in a range
|
||||||
|
// when its date is, or when it is week-only and its target week overlaps. With
|
||||||
|
// no range, every week is searched and open undated rows are added. Closed
|
||||||
|
// undated rows are never unscheduled.
|
||||||
const matchesDateRange: AdvancedFilterPredicate = (row, filters) => {
|
const matchesDateRange: AdvancedFilterPredicate = (row, filters) => {
|
||||||
if (filters.unscheduledOnly) {
|
if (filters.unscheduledOnly) {
|
||||||
return !row.scheduledOn;
|
return isOpenUndated(row);
|
||||||
}
|
}
|
||||||
const [from, to] = dateRangeFor(filters);
|
const [from, to] = dateRangeFor(filters) ?? [
|
||||||
return !row.scheduledOn || (row.scheduledOn >= from && row.scheduledOn <= to);
|
ADVANCED_SEARCH_ALL_WEEKS_FROM,
|
||||||
|
ADVANCED_SEARCH_ALL_WEEKS_TO,
|
||||||
|
];
|
||||||
|
if (row.scheduledOn && row.scheduledOn >= from && row.scheduledOn <= to) return true;
|
||||||
|
if (
|
||||||
|
row.scheduleWeekOnly &&
|
||||||
|
Boolean(row.targetWeek) &&
|
||||||
|
row.targetWeek <= to &&
|
||||||
|
addDaysIso(row.targetWeek, 6) >= from
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return filters.dateRange === null && isOpenUndated(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
const matchesSite: AdvancedFilterPredicate = (row, filters) =>
|
const matchesSite: AdvancedFilterPredicate = (row, filters) =>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ function cloneFilters(filters: WorkOrderAdvancedFilters): WorkOrderAdvancedFilte
|
||||||
}
|
}
|
||||||
|
|
||||||
const ADVANCED_FILTER_ACTIVE_SIGNALS = (filters: WorkOrderAdvancedFilters): boolean[] => [
|
const ADVANCED_FILTER_ACTIVE_SIGNALS = (filters: WorkOrderAdvancedFilters): boolean[] => [
|
||||||
filters.dateRange !== "this-week",
|
filters.dateRange !== null,
|
||||||
Boolean(filters.sites?.size),
|
Boolean(filters.sites?.size),
|
||||||
Boolean(filters.types?.size),
|
Boolean(filters.types?.size),
|
||||||
Boolean(filters.severities?.size),
|
Boolean(filters.severities?.size),
|
||||||
|
|
@ -70,7 +70,9 @@ function monthEnd(y: number, m: number): string {
|
||||||
return `${y}-${pad2(m + 1)}-${pad2(new Date(y, m + 1, 0).getDate())}`;
|
return `${y}-${pad2(m + 1)}-${pad2(new Date(y, m + 1, 0).getDate())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dateRangeFor(filters: WorkOrderAdvancedFilters): [string, string] {
|
/** Inclusive [from, to] for the selected range; `null` when no range is selected. */
|
||||||
|
export function dateRangeFor(filters: WorkOrderAdvancedFilters): [string, string] | null {
|
||||||
|
if (filters.dateRange === null) return null;
|
||||||
const today = todayIso();
|
const today = todayIso();
|
||||||
const ty = Number(today.slice(0, 4));
|
const ty = Number(today.slice(0, 4));
|
||||||
const tm = Number(today.slice(5, 7)) - 1;
|
const tm = Number(today.slice(5, 7)) - 1;
|
||||||
|
|
@ -144,6 +146,6 @@ export function cloneAdvancedFilters(filters: WorkOrderAdvancedFilters): WorkOrd
|
||||||
return cloneFilters(filters);
|
return cloneFilters(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDefaultDateRange(dateRange: DateRangeKey): boolean {
|
export function isDefaultDateRange(dateRange: DateRangeKey | null): boolean {
|
||||||
return dateRange === "this-week";
|
return dateRange === null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
|
import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
|
||||||
import {
|
import type { WorkOrderAdvancedFilters } from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||||
EMPTY_ADVANCED_FILTERS,
|
|
||||||
type WorkOrderAdvancedFilters,
|
|
||||||
} from "@/domain/work-orders/types/work-order-advanced-filters";
|
|
||||||
import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order-list-filters";
|
import type { WorkOrderBarFilters } from "@/domain/work-orders/types/work-order-list-filters";
|
||||||
import {
|
import {
|
||||||
BOARD_DISPATCHER_UNASSIGNED,
|
BOARD_DISPATCHER_UNASSIGNED,
|
||||||
|
|
@ -158,17 +155,21 @@ function resolveAdvancedSearchDate(filters: WorkOrderAdvancedFilters): {
|
||||||
datePreset?: WorkOrderBoardSearchQueryParams["datePreset"];
|
datePreset?: WorkOrderBoardSearchQueryParams["datePreset"];
|
||||||
dateFrom?: string;
|
dateFrom?: string;
|
||||||
dateTo?: string;
|
dateTo?: string;
|
||||||
|
includeDateless?: boolean;
|
||||||
} {
|
} {
|
||||||
// SH-338: server-side unscheduledOnly owns the date-range exemption; the
|
// SH-338: server-side unscheduledOnly owns the date-range exemption; the
|
||||||
// client must not fake a Custom window and sends no date params.
|
// client must not fake a Custom window and sends no date params.
|
||||||
if (filters.unscheduledOnly) {
|
if (filters.unscheduledOnly) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
if (filters.dateRange === EMPTY_ADVANCED_FILTERS.dateRange) {
|
// No range selected searches every week, undated rows included. Any
|
||||||
|
// selected range (This week included) strictly narrows the results.
|
||||||
|
if (filters.dateRange === null) {
|
||||||
return {
|
return {
|
||||||
datePreset: "Custom",
|
datePreset: "Custom",
|
||||||
dateFrom: ADVANCED_SEARCH_ALL_WEEKS_FROM,
|
dateFrom: ADVANCED_SEARCH_ALL_WEEKS_FROM,
|
||||||
dateTo: ADVANCED_SEARCH_ALL_WEEKS_TO,
|
dateTo: ADVANCED_SEARCH_ALL_WEEKS_TO,
|
||||||
|
includeDateless: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,6 +214,7 @@ export function toBoardSearchQueryParams(input: {
|
||||||
datePreset: date.datePreset,
|
datePreset: date.datePreset,
|
||||||
dateFrom: date.dateFrom,
|
dateFrom: date.dateFrom,
|
||||||
dateTo: date.dateTo,
|
dateTo: date.dateTo,
|
||||||
|
includeDateless: date.includeDateless,
|
||||||
sites: filters.sites.size > 0 ? [...filters.sites] : undefined,
|
sites: filters.sites.size > 0 ? [...filters.sites] : undefined,
|
||||||
types: typeFilters.types,
|
types: typeFilters.types,
|
||||||
overdue: typeFilters.overdue,
|
overdue: typeFilters.overdue,
|
||||||
|
|
@ -267,5 +269,6 @@ export function toUnassignedQueueQueryParams(
|
||||||
datePreset: "Custom",
|
datePreset: "Custom",
|
||||||
dateFrom: UNASSIGNED_QUEUE_DATE_FROM,
|
dateFrom: UNASSIGNED_QUEUE_DATE_FROM,
|
||||||
dateTo: UNASSIGNED_QUEUE_DATE_TO,
|
dateTo: UNASSIGNED_QUEUE_DATE_TO,
|
||||||
|
includeDateless: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ const statsData = {
|
||||||
scheduledTomorrow: 3,
|
scheduledTomorrow: 3,
|
||||||
pendingUplifts: 5,
|
pendingUplifts: 5,
|
||||||
avetaPending: 0,
|
avetaPending: 0,
|
||||||
|
unassigned: 4,
|
||||||
breakdown: [
|
breakdown: [
|
||||||
{ status: "PM", count: 7 },
|
{ status: "PM", count: 7 },
|
||||||
{ status: "Completed", count: 0 },
|
{ status: "Completed", count: 0 },
|
||||||
|
|
@ -88,6 +89,11 @@ function renderDashboard() {
|
||||||
return renderWithProviders(<DashboardPage />);
|
return renderWithProviders(<DashboardPage />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function lastNavigation(): string {
|
||||||
|
const calls = navigate.mock.calls;
|
||||||
|
return String(calls[calls.length - 1]?.[0]);
|
||||||
|
}
|
||||||
|
|
||||||
describe("DashboardPage", () => {
|
describe("DashboardPage", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
authUser.current = { id: "99", userRoles: "Admin" };
|
authUser.current = { id: "99", userRoles: "Admin" };
|
||||||
|
|
@ -120,6 +126,7 @@ describe("DashboardPage", () => {
|
||||||
"Scheduled Tomorrow",
|
"Scheduled Tomorrow",
|
||||||
"Pending Uplifts",
|
"Pending Uplifts",
|
||||||
"Aveta Pending",
|
"Aveta Pending",
|
||||||
|
"Unassigned",
|
||||||
"Work Order Breakdown",
|
"Work Order Breakdown",
|
||||||
"Status Distribution",
|
"Status Distribution",
|
||||||
"Dispatcher Workload",
|
"Dispatcher Workload",
|
||||||
|
|
@ -257,6 +264,29 @@ describe("DashboardPage", () => {
|
||||||
expect(lastRequestedPage).toBe(1);
|
expect(lastRequestedPage).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows unassigned WOs for the selected period and drills into that list (SH-392)", () => {
|
||||||
|
renderDashboard();
|
||||||
|
|
||||||
|
const card = screen.getByRole("button", { name: /Unassigned/ });
|
||||||
|
expect(card).toHaveTextContent("4");
|
||||||
|
fireEvent.click(card);
|
||||||
|
|
||||||
|
const weekUrl = new URL(lastNavigation(), "http://x");
|
||||||
|
expect(weekUrl.pathname).toBe("/workorders");
|
||||||
|
expect(weekUrl.searchParams.get("dispatchers")).toBe("__unassigned");
|
||||||
|
expect(weekUrl.searchParams.get("statuses")).not.toContain("Completed");
|
||||||
|
expect(weekUrl.searchParams.get("dateFrom")).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||||
|
expect(weekUrl.searchParams.get("dateTo")).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "All time" }));
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /Unassigned/ }));
|
||||||
|
|
||||||
|
const allTimeUrl = new URL(lastNavigation(), "http://x");
|
||||||
|
expect(allTimeUrl.searchParams.get("dispatchers")).toBe("__unassigned");
|
||||||
|
expect(allTimeUrl.searchParams.has("dateFrom")).toBe(false);
|
||||||
|
expect(allTimeUrl.searchParams.has("dateTo")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps drilldown dates deterministic on the business clock", () => {
|
it("keeps drilldown dates deterministic on the business clock", () => {
|
||||||
const today = businessTodayIso();
|
const today = businessTodayIso();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { fireEvent, screen } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { AdvDateRangeSection } from "@/app/(protected)/workorders/_components/list/advanced-search/adv-date-range-section";
|
||||||
|
import {
|
||||||
|
EMPTY_ADVANCED_FILTERS,
|
||||||
|
type WorkOrderAdvancedFilters,
|
||||||
|
} from "@/domain/work-orders/types/work-order-advanced-filters";
|
||||||
|
import { cloneAdvancedFilters } from "@/domain/work-orders/utils/advanced-filters-utils";
|
||||||
|
import { toBoardSearchQueryParams } from "@/domain/work-orders/utils/board-query-params";
|
||||||
|
import { renderWithProviders } from "@/test/test-utils";
|
||||||
|
|
||||||
|
const onDraft = vi.fn<(draft: WorkOrderAdvancedFilters) => void>();
|
||||||
|
|
||||||
|
function latestDraft(): WorkOrderAdvancedFilters {
|
||||||
|
return onDraft.mock.calls[onDraft.mock.calls.length - 1][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Harness() {
|
||||||
|
const [draft, setDraft] = useState(() => cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS));
|
||||||
|
useEffect(() => {
|
||||||
|
onDraft(draft);
|
||||||
|
}, [draft]);
|
||||||
|
return <AdvDateRangeSection draft={draft} setDraft={setDraft} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSection() {
|
||||||
|
onDraft.mockClear();
|
||||||
|
renderWithProviders(<Harness />);
|
||||||
|
const header = screen.queryByRole("button", { name: /Date range/i });
|
||||||
|
if (header && !screen.queryByRole("button", { name: /^This week$/i })) {
|
||||||
|
fireEvent.click(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Advanced Filters date range (SH-391)", () => {
|
||||||
|
it("starts with no range selected, so the search spans every week", () => {
|
||||||
|
openSection();
|
||||||
|
|
||||||
|
for (const label of ["This week", "Last week", "Last 3 months", "Custom"]) {
|
||||||
|
expect(screen.getByRole("button", { name: new RegExp(`^${label}$`, "i") })).toHaveAttribute(
|
||||||
|
"aria-pressed",
|
||||||
|
"false",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(toBoardSearchQueryParams({ filters: latestDraft() }).includeDateless).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("narrows to This week once picked, and clicking it again clears the range", () => {
|
||||||
|
openSection();
|
||||||
|
const thisWeek = screen.getByRole("button", { name: /^This week$/i });
|
||||||
|
|
||||||
|
fireEvent.click(thisWeek);
|
||||||
|
expect(thisWeek).toHaveAttribute("aria-pressed", "true");
|
||||||
|
const narrowed = toBoardSearchQueryParams({ filters: latestDraft() });
|
||||||
|
expect(narrowed.datePreset).toBe("ThisWeek");
|
||||||
|
expect(narrowed.includeDateless).toBeUndefined();
|
||||||
|
|
||||||
|
fireEvent.click(thisWeek);
|
||||||
|
expect(thisWeek).toHaveAttribute("aria-pressed", "false");
|
||||||
|
expect(latestDraft().dateRange).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -367,7 +367,7 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => {
|
||||||
expect(unscheduled).toHaveAttribute("aria-pressed", "true");
|
expect(unscheduled).toHaveAttribute("aria-pressed", "true");
|
||||||
expect(within(sheet).getByRole("button", { name: /^This week$/i })).toHaveAttribute(
|
expect(within(sheet).getByRole("button", { name: /^This week$/i })).toHaveAttribute(
|
||||||
"aria-pressed",
|
"aria-pressed",
|
||||||
"true",
|
"false",
|
||||||
);
|
);
|
||||||
expect(within(sheet).queryByLabelText(/^From$/i)).not.toBeInTheDocument();
|
expect(within(sheet).queryByLabelText(/^From$/i)).not.toBeInTheDocument();
|
||||||
expect(within(sheet).queryByLabelText(/^To$/i)).not.toBeInTheDocument();
|
expect(within(sheet).queryByLabelText(/^To$/i)).not.toBeInTheDocument();
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ describe("dashboardApi", () => {
|
||||||
ScheduledTomorrow: "3",
|
ScheduledTomorrow: "3",
|
||||||
PendingUplifts: 2,
|
PendingUplifts: 2,
|
||||||
AvetaPending: "1",
|
AvetaPending: "1",
|
||||||
|
Unassigned: "6",
|
||||||
Breakdown: [
|
Breakdown: [
|
||||||
{ Status: "Scheduled", Count: "7" },
|
{ Status: "Scheduled", Count: "7" },
|
||||||
{ Status: "Completed", Count: 4 },
|
{ Status: "Completed", Count: 4 },
|
||||||
|
|
@ -52,6 +53,7 @@ describe("dashboardApi", () => {
|
||||||
scheduledTomorrow: 3,
|
scheduledTomorrow: 3,
|
||||||
pendingUplifts: 2,
|
pendingUplifts: 2,
|
||||||
avetaPending: 1,
|
avetaPending: 1,
|
||||||
|
unassigned: 6,
|
||||||
breakdown: [
|
breakdown: [
|
||||||
{ status: "Scheduled", count: 7 },
|
{ status: "Scheduled", count: 7 },
|
||||||
{ status: "Completed", count: 4 },
|
{ status: "Completed", count: 4 },
|
||||||
|
|
|
||||||
|
|
@ -420,6 +420,14 @@ describe("workOrdersApi.searchBoard", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** A WO# lookup spans every week and undated rows (SH-391). */
|
||||||
|
const ALL_WEEKS_SEARCH = {
|
||||||
|
datePreset: "Custom",
|
||||||
|
dateFrom: "2000-01-01",
|
||||||
|
dateTo: "2099-12-31",
|
||||||
|
includeDateless: "true",
|
||||||
|
};
|
||||||
|
|
||||||
describe("workOrdersApi.findByWoNumber", () => {
|
describe("workOrdersApi.findByWoNumber", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
apiGet.mockReset();
|
apiGet.mockReset();
|
||||||
|
|
@ -450,6 +458,7 @@ describe("workOrdersApi.findByWoNumber", () => {
|
||||||
const searchParams = apiGet.mock.calls[0]?.[1]?.searchParams as URLSearchParams;
|
const searchParams = apiGet.mock.calls[0]?.[1]?.searchParams as URLSearchParams;
|
||||||
expect(Object.fromEntries(searchParams)).toEqual({
|
expect(Object.fromEntries(searchParams)).toEqual({
|
||||||
search: "10000001235",
|
search: "10000001235",
|
||||||
|
...ALL_WEEKS_SEARCH,
|
||||||
page: "0",
|
page: "0",
|
||||||
pageSize: "100",
|
pageSize: "100",
|
||||||
});
|
});
|
||||||
|
|
@ -519,6 +528,7 @@ describe("workOrdersApi.findByWoNumber", () => {
|
||||||
const searchParams = apiGet.mock.calls[0]?.[1]?.searchParams as URLSearchParams;
|
const searchParams = apiGet.mock.calls[0]?.[1]?.searchParams as URLSearchParams;
|
||||||
expect(Object.fromEntries(searchParams)).toEqual({
|
expect(Object.fromEntries(searchParams)).toEqual({
|
||||||
search: "9",
|
search: "9",
|
||||||
|
...ALL_WEEKS_SEARCH,
|
||||||
page: "0",
|
page: "0",
|
||||||
pageSize: "100",
|
pageSize: "100",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { parseDashboardDrilldownParams } from "@/domain/work-orders/utils/dashboard-drilldown-params";
|
import { parseDashboardDrilldownParams } from "@/domain/work-orders/utils/dashboard-drilldown-params";
|
||||||
import { avetaPendingDrilldownSearch } from "@/domain/dashboard/utils/work-order-drilldown-links";
|
import {
|
||||||
|
avetaPendingDrilldownSearch,
|
||||||
|
unassignedDrilldownSearch,
|
||||||
|
} from "@/domain/dashboard/utils/work-order-drilldown-links";
|
||||||
|
import { NON_TERMINAL_LIFECYCLE_STATUSES } from "@/domain/work-orders/types/work-order-board";
|
||||||
|
import { ASSIGNEE_FILTER_UNASSIGNED } from "@/domain/work-orders/types/work-order-list";
|
||||||
|
import { buildAdvancedFilterChips } from "@/domain/work-orders/utils/advanced-filters-utils";
|
||||||
|
import { toBoardSearchQueryParams } from "@/domain/work-orders/utils/board-query-params";
|
||||||
|
|
||||||
describe("parseDashboardDrilldownParams", () => {
|
describe("parseDashboardDrilldownParams", () => {
|
||||||
it("returns null when no recognized param is present", () => {
|
it("returns null when no recognized param is present", () => {
|
||||||
|
|
@ -60,6 +67,41 @@ describe("parseDashboardDrilldownParams", () => {
|
||||||
expect(filters?.regions).toEqual(new Set(["California"]));
|
expect(filters?.regions).toEqual(new Set(["California"]));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("turns the Unassigned tile link into the board search the Dashboard counts (SH-392)", () => {
|
||||||
|
const allTime = parseDashboardDrilldownParams(unassignedDrilldownSearch("all-time"));
|
||||||
|
expect(allTime).not.toBeNull();
|
||||||
|
expect(toBoardSearchQueryParams({ filters: allTime! })).toMatchObject({
|
||||||
|
dispatchers: ["__unassigned__"],
|
||||||
|
statuses: [...NON_TERMINAL_LIFECYCLE_STATUSES],
|
||||||
|
datePreset: "Custom",
|
||||||
|
dateFrom: "2000-01-01",
|
||||||
|
dateTo: "2099-12-31",
|
||||||
|
includeDateless: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const week = parseDashboardDrilldownParams(unassignedDrilldownSearch("this-week"));
|
||||||
|
const weekParams = toBoardSearchQueryParams({ filters: week! });
|
||||||
|
expect(week?.dateRange).toBe("custom");
|
||||||
|
expect(weekParams).toMatchObject({
|
||||||
|
dispatchers: ["__unassigned__"],
|
||||||
|
statuses: [...NON_TERMINAL_LIFECYCLE_STATUSES],
|
||||||
|
datePreset: "Custom",
|
||||||
|
});
|
||||||
|
expect(weekParams.includeDateless).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("opens the Unassigned tile link as the Unassigned option of Assigned to", () => {
|
||||||
|
const filters = parseDashboardDrilldownParams(unassignedDrilldownSearch("this-week"));
|
||||||
|
expect(filters?.dispatchers).toEqual(new Set([ASSIGNEE_FILTER_UNASSIGNED]));
|
||||||
|
|
||||||
|
const chips = buildAdvancedFilterChips(filters!, { dispatcherNameOf: (id) => id });
|
||||||
|
const assignedTo = chips.filter((chip) => chip.group === "Assigned to");
|
||||||
|
expect(assignedTo.map((chip) => chip.value)).toEqual(["Unassigned"]);
|
||||||
|
|
||||||
|
const cleared = assignedTo[0]!.clear(filters!);
|
||||||
|
expect(cleared.dispatchers.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects partial or malformed date ranges", () => {
|
it("rejects partial or malformed date ranges", () => {
|
||||||
expect(
|
expect(
|
||||||
parseDashboardDrilldownParams(
|
parseDashboardDrilldownParams(
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,12 @@ describe("countAdvancedFilters", () => {
|
||||||
expect(countAdvancedFilters(filters)).toBe(3);
|
expect(countAdvancedFilters(filters)).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("counts Unscheduled +1 with the this-week default still in place", () => {
|
it("counts Unscheduled +1 with no date range selected", () => {
|
||||||
const filters = {
|
const filters = {
|
||||||
...cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS),
|
...cloneAdvancedFilters(EMPTY_ADVANCED_FILTERS),
|
||||||
unscheduledOnly: true,
|
unscheduledOnly: true,
|
||||||
};
|
};
|
||||||
expect(filters.dateRange).toBe("this-week");
|
expect(filters.dateRange).toBeNull();
|
||||||
expect(countAdvancedFilters(filters)).toBe(1);
|
expect(countAdvancedFilters(filters)).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,9 +130,43 @@ describe("toBoardSearchQueryParams", () => {
|
||||||
customTo: "2026-06-30",
|
customTo: "2026-06-30",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(thisWeek.datePreset).toBe("Custom");
|
expect(thisWeek.datePreset).toBe("ThisWeek");
|
||||||
expect(thisWeek.dateFrom).toBe("2000-01-01");
|
expect(thisWeek.dateFrom).toBeUndefined();
|
||||||
expect(thisWeek.dateTo).toBe("2099-12-31");
|
expect(thisWeek.dateTo).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("narrows strictly for every selected range, This week included (SH-391)", () => {
|
||||||
|
for (const dateRange of ["this-week", "last-week", "last-3-months", "custom"] as const) {
|
||||||
|
const params = toBoardSearchQueryParams({
|
||||||
|
filters: {
|
||||||
|
...EMPTY_ADVANCED_FILTERS,
|
||||||
|
dateRange,
|
||||||
|
customFrom: "2026-09-21",
|
||||||
|
customTo: "2026-09-25",
|
||||||
|
dispatchers: new Set(["__unassigned__"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(params.includeDateless).toBeUndefined();
|
||||||
|
expect(params.dateFrom).not.toBe("2000-01-01");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("searches every week, undated rows included, when no range is selected (SH-391)", () => {
|
||||||
|
const params = toBoardSearchQueryParams({
|
||||||
|
filters: { ...EMPTY_ADVANCED_FILTERS, dispatchers: new Set(["__unassigned__"]) },
|
||||||
|
});
|
||||||
|
expect(EMPTY_ADVANCED_FILTERS.dateRange).toBeNull();
|
||||||
|
expect(params).toMatchObject({
|
||||||
|
datePreset: "Custom",
|
||||||
|
dateFrom: "2000-01-01",
|
||||||
|
dateTo: "2099-12-31",
|
||||||
|
includeDateless: true,
|
||||||
|
dispatchers: ["__unassigned__"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps undated rows in the pinned Unassigned queue (SH-391)", () => {
|
||||||
|
expect(toUnassignedQueueQueryParams().includeDateless).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends unscheduledOnly=true with no faked Custom date window (SH-338)", () => {
|
it("sends unscheduledOnly=true with no faked Custom date window (SH-338)", () => {
|
||||||
|
|
|
||||||
|
|
@ -454,6 +454,56 @@ describe("matchesAdvancedFilters", () => {
|
||||||
expect(matchesAdvancedFilters(weekOnly, filters)).toBe(true);
|
expect(matchesAdvancedFilters(weekOnly, filters)).toBe(true);
|
||||||
expect(matchesAdvancedFilters(undatedAssigned, filters)).toBe(true);
|
expect(matchesAdvancedFilters(undatedAssigned, filters)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("matches a range the same way the board search does (SH-391)", () => {
|
||||||
|
const filters = {
|
||||||
|
...EMPTY_ADVANCED_FILTERS,
|
||||||
|
dateRange: "custom" as const,
|
||||||
|
customFrom: "2026-09-23",
|
||||||
|
customTo: "2026-09-25",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Week-only: a target week overlapping the range matches, whatever date the
|
||||||
|
// row still carries.
|
||||||
|
const weekOnlyStaleDate = makeRow({
|
||||||
|
scheduledOn: "2026-10-02",
|
||||||
|
targetWeek: "2026-09-21",
|
||||||
|
scheduleWeekOnly: true,
|
||||||
|
});
|
||||||
|
const weekOnlyPreviousWeek = makeRow({
|
||||||
|
scheduledOn: "",
|
||||||
|
targetWeek: "2026-09-14",
|
||||||
|
scheduleWeekOnly: true,
|
||||||
|
});
|
||||||
|
const weekOnlyOutside = makeRow({
|
||||||
|
scheduledOn: "",
|
||||||
|
targetWeek: "2026-09-28",
|
||||||
|
scheduleWeekOnly: true,
|
||||||
|
});
|
||||||
|
const datedOutsideWithWeek = makeRow({ scheduledOn: "2026-10-02", targetWeek: "2026-09-21" });
|
||||||
|
const undated = makeRow({ scheduledOn: "", targetWeek: "" });
|
||||||
|
|
||||||
|
expect(matchesAdvancedFilters(weekOnlyStaleDate, filters)).toBe(true);
|
||||||
|
expect(matchesAdvancedFilters(weekOnlyPreviousWeek, filters)).toBe(false);
|
||||||
|
expect(matchesAdvancedFilters(weekOnlyOutside, filters)).toBe(false);
|
||||||
|
expect(matchesAdvancedFilters(datedOutsideWithWeek, filters)).toBe(false);
|
||||||
|
expect(matchesAdvancedFilters(undated, filters)).toBe(false);
|
||||||
|
// No range: every week, plus open undated rows. Closed undated rows are
|
||||||
|
// never unscheduled, with or without the Unscheduled filter.
|
||||||
|
const noRange = { ...filters, dateRange: null };
|
||||||
|
const unscheduledOnly = { ...filters, unscheduledOnly: true };
|
||||||
|
const completedUndated = makeRow({ scheduledOn: "", targetWeek: "", status: "Completed" });
|
||||||
|
const canceledUndated = makeRow({ scheduledOn: "", targetWeek: "", status: "Canceled" });
|
||||||
|
|
||||||
|
expect(matchesAdvancedFilters(undated, noRange)).toBe(true);
|
||||||
|
expect(matchesAdvancedFilters(datedOutsideWithWeek, noRange)).toBe(true);
|
||||||
|
expect(matchesAdvancedFilters(weekOnlyOutside, noRange)).toBe(true);
|
||||||
|
expect(matchesAdvancedFilters(completedUndated, noRange)).toBe(false);
|
||||||
|
expect(matchesAdvancedFilters(canceledUndated, noRange)).toBe(false);
|
||||||
|
expect(matchesAdvancedFilters(undated, unscheduledOnly)).toBe(true);
|
||||||
|
expect(matchesAdvancedFilters(completedUndated, unscheduledOnly)).toBe(false);
|
||||||
|
expect(matchesAdvancedFilters(canceledUndated, unscheduledOnly)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("computeIsPastDue", () => {
|
describe("computeIsPastDue", () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue