fix(allocations): recover from fully filtered empty state
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
||||||
import { formatDate } from "~/lib/format.js";
|
import { formatDate } from "~/lib/format.js";
|
||||||
import { trpc } from "~/lib/trpc/client.js";
|
import { trpc } from "~/lib/trpc/client.js";
|
||||||
import { AllocationModal } from "./AllocationModal.js";
|
import { AllocationModal } from "./AllocationModal.js";
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
toggleCollapsedAllocationGroup,
|
toggleCollapsedAllocationGroup,
|
||||||
type CollapsedAllocationGroups,
|
type CollapsedAllocationGroups,
|
||||||
} from "./allocationGroupState.js";
|
} from "./allocationGroupState.js";
|
||||||
|
import { getAllocationEmptyState, shouldAutoRelaxAllocationFilters } from "./allocationVisibilityState.js";
|
||||||
|
|
||||||
/** Left-border color by allocation status for instant visual scanning */
|
/** Left-border color by allocation status for instant visual scanning */
|
||||||
const STATUS_LEFT_BORDER: Record<string, string> = {
|
const STATUS_LEFT_BORDER: Record<string, string> = {
|
||||||
@@ -202,6 +203,7 @@ export function AllocationsClient() {
|
|||||||
);
|
);
|
||||||
// Track expanded project sub-groups: key = "resourceId::projectId"
|
// Track expanded project sub-groups: key = "resourceId::projectId"
|
||||||
const [expandedSubGroups, setExpandedSubGroups] = useState<Set<string>>(new Set());
|
const [expandedSubGroups, setExpandedSubGroups] = useState<Set<string>>(new Set());
|
||||||
|
const hasEvaluatedInitialVisibility = useRef(false);
|
||||||
|
|
||||||
const toggleViewMode = useCallback(() => {
|
const toggleViewMode = useCallback(() => {
|
||||||
setViewMode((prev) => {
|
setViewMode((prev) => {
|
||||||
@@ -351,6 +353,49 @@ export function AllocationsClient() {
|
|||||||
...(hideDraftProjects ? [{ label: "Hiding draft projects", onRemove: () => setHideDraftProjects(false) }] : []),
|
...(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) {
|
function formatPeriod(alloc: AllocationWithDetails) {
|
||||||
return formatDate(alloc.startDate) + " \u2192 " + formatDate(alloc.endDate);
|
return formatDate(alloc.startDate) + " \u2192 " + formatDate(alloc.endDate);
|
||||||
}
|
}
|
||||||
@@ -633,7 +678,21 @@ export function AllocationsClient() {
|
|||||||
|
|
||||||
{!isLoading && sorted.length === 0 && (
|
{!isLoading && sorted.length === 0 && (
|
||||||
<tr>
|
<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>
|
</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