fix(allocations): recover from fully filtered empty state

This commit is contained in:
2026-04-01 15:16:57 +02:00
parent 7df751d5eb
commit fd75628e9d
3 changed files with 185 additions and 2 deletions
@@ -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,
};
}