fix(allocations): recover from fully filtered empty state
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
||||
import { formatDate } from "~/lib/format.js";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import { AllocationModal } from "./AllocationModal.js";
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
toggleCollapsedAllocationGroup,
|
||||
type CollapsedAllocationGroups,
|
||||
} from "./allocationGroupState.js";
|
||||
import { getAllocationEmptyState, shouldAutoRelaxAllocationFilters } from "./allocationVisibilityState.js";
|
||||
|
||||
/** Left-border color by allocation status for instant visual scanning */
|
||||
const STATUS_LEFT_BORDER: Record<string, string> = {
|
||||
@@ -202,6 +203,7 @@ export function AllocationsClient() {
|
||||
);
|
||||
// Track expanded project sub-groups: key = "resourceId::projectId"
|
||||
const [expandedSubGroups, setExpandedSubGroups] = useState<Set<string>>(new Set());
|
||||
const hasEvaluatedInitialVisibility = useRef(false);
|
||||
|
||||
const toggleViewMode = useCallback(() => {
|
||||
setViewMode((prev) => {
|
||||
@@ -351,6 +353,49 @@ export function AllocationsClient() {
|
||||
...(hideDraftProjects ? [{ label: "Hiding draft projects", onRemove: () => setHideDraftProjects(false) }] : []),
|
||||
];
|
||||
|
||||
const emptyState = getAllocationEmptyState({
|
||||
totalAssignments: assignmentList.length,
|
||||
totalDemands: demandList.length,
|
||||
visibleAssignments: filteredAllocations.length,
|
||||
visibleDemands: filteredDemands.length,
|
||||
hidePastProjects,
|
||||
hideCompletedProjects,
|
||||
hideDraftProjects,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || hasEvaluatedInitialVisibility.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasEvaluatedInitialVisibility.current = true;
|
||||
|
||||
if (
|
||||
shouldAutoRelaxAllocationFilters({
|
||||
totalAssignments: assignmentList.length,
|
||||
totalDemands: demandList.length,
|
||||
visibleAssignments: filteredAllocations.length,
|
||||
visibleDemands: filteredDemands.length,
|
||||
hidePastProjects,
|
||||
hideCompletedProjects,
|
||||
hideDraftProjects,
|
||||
})
|
||||
) {
|
||||
setHidePastProjects(false);
|
||||
setHideCompletedProjects(false);
|
||||
setHideDraftProjects(false);
|
||||
}
|
||||
}, [
|
||||
assignmentList.length,
|
||||
demandList.length,
|
||||
filteredAllocations.length,
|
||||
filteredDemands.length,
|
||||
hideCompletedProjects,
|
||||
hideDraftProjects,
|
||||
hidePastProjects,
|
||||
isLoading,
|
||||
]);
|
||||
|
||||
function formatPeriod(alloc: AllocationWithDetails) {
|
||||
return formatDate(alloc.startDate) + " \u2192 " + formatDate(alloc.endDate);
|
||||
}
|
||||
@@ -633,7 +678,21 @@ export function AllocationsClient() {
|
||||
|
||||
{!isLoading && sorted.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={totalColSpan} className="py-12 text-center text-sm text-gray-500 dark:text-gray-400">No assignments found.</td>
|
||||
<td colSpan={totalColSpan} className="py-12 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="font-medium text-gray-700 dark:text-gray-200">{emptyState.title}</p>
|
||||
<p>{emptyState.detail}</p>
|
||||
{emptyState.showResetAction && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearAll}
|
||||
className="rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-800"
|
||||
>
|
||||
Show all assignments
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getAllocationEmptyState, shouldAutoRelaxAllocationFilters } from "./allocationVisibilityState.js";
|
||||
|
||||
describe("allocationVisibilityState", () => {
|
||||
it("auto-relaxes default project filters when they hide every available row", () => {
|
||||
expect(
|
||||
shouldAutoRelaxAllocationFilters({
|
||||
totalAssignments: 8,
|
||||
totalDemands: 2,
|
||||
visibleAssignments: 0,
|
||||
visibleDemands: 0,
|
||||
hidePastProjects: true,
|
||||
hideCompletedProjects: true,
|
||||
hideDraftProjects: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not auto-relax when there is no underlying allocation data", () => {
|
||||
expect(
|
||||
shouldAutoRelaxAllocationFilters({
|
||||
totalAssignments: 0,
|
||||
totalDemands: 0,
|
||||
visibleAssignments: 0,
|
||||
visibleDemands: 0,
|
||||
hidePastProjects: true,
|
||||
hideCompletedProjects: true,
|
||||
hideDraftProjects: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("builds a filter-aware empty state when rows are hidden", () => {
|
||||
expect(
|
||||
getAllocationEmptyState({
|
||||
totalAssignments: 5,
|
||||
totalDemands: 1,
|
||||
visibleAssignments: 0,
|
||||
visibleDemands: 0,
|
||||
hidePastProjects: true,
|
||||
hideCompletedProjects: true,
|
||||
hideDraftProjects: false,
|
||||
}),
|
||||
).toEqual({
|
||||
title: "No assignments match the active filters.",
|
||||
detail: "5 assignments and 1 demand are hidden by the active project filters.",
|
||||
showResetAction: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the plain empty state when there is genuinely no data", () => {
|
||||
expect(
|
||||
getAllocationEmptyState({
|
||||
totalAssignments: 0,
|
||||
totalDemands: 0,
|
||||
visibleAssignments: 0,
|
||||
visibleDemands: 0,
|
||||
hidePastProjects: false,
|
||||
hideCompletedProjects: false,
|
||||
hideDraftProjects: false,
|
||||
}),
|
||||
).toEqual({
|
||||
title: "No assignments found.",
|
||||
detail: "Create a planning entry or relax the current project filters once data is available.",
|
||||
showResetAction: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
export interface AllocationVisibilityInput {
|
||||
totalAssignments: number;
|
||||
totalDemands: number;
|
||||
visibleAssignments: number;
|
||||
visibleDemands: number;
|
||||
hidePastProjects: boolean;
|
||||
hideCompletedProjects: boolean;
|
||||
hideDraftProjects: boolean;
|
||||
}
|
||||
|
||||
export interface AllocationEmptyState {
|
||||
title: string;
|
||||
detail: string;
|
||||
showResetAction: boolean;
|
||||
}
|
||||
|
||||
export function shouldAutoRelaxAllocationFilters(input: AllocationVisibilityInput): boolean {
|
||||
const totalRows = input.totalAssignments + input.totalDemands;
|
||||
const visibleRows = input.visibleAssignments + input.visibleDemands;
|
||||
const hasActiveProjectFilters = input.hidePastProjects || input.hideCompletedProjects || input.hideDraftProjects;
|
||||
|
||||
return totalRows > 0 && visibleRows === 0 && hasActiveProjectFilters;
|
||||
}
|
||||
|
||||
export function getAllocationEmptyState(input: AllocationVisibilityInput): AllocationEmptyState {
|
||||
const totalRows = input.totalAssignments + input.totalDemands;
|
||||
|
||||
if (shouldAutoRelaxAllocationFilters(input)) {
|
||||
const assignmentLabel = input.totalAssignments === 1 ? "assignment" : "assignments";
|
||||
const demandLabel = input.totalDemands === 1 ? "demand" : "demands";
|
||||
const hiddenSummary =
|
||||
input.totalDemands > 0
|
||||
? `${input.totalAssignments} ${assignmentLabel} and ${input.totalDemands} ${demandLabel} are hidden by the active project filters.`
|
||||
: `${input.totalAssignments} ${assignmentLabel} are hidden by the active project filters.`;
|
||||
|
||||
return {
|
||||
title: "No assignments match the active filters.",
|
||||
detail: hiddenSummary,
|
||||
showResetAction: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (totalRows === 0) {
|
||||
return {
|
||||
title: "No assignments found.",
|
||||
detail: "Create a planning entry or relax the current project filters once data is available.",
|
||||
showResetAction: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: "No assignments found.",
|
||||
detail: "No visible assignment rows remain after the current filters were applied.",
|
||||
showResetAction: false,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user