feat(ux): Sprint 1 — quick wins: EmptyState, DateRangePresets, debounce, save feedback, scenarios nav
- EmptyState shared component; replace AllocationsClient inline empty state - DateRangePresets (this month/quarter/3 months/year) integrated into AllocationModal - Debounce conflict-check inputs in AllocationModal (400ms) using existing useDebounce - Dashboard layout save feedback via SuccessToast after DB write completes - Scenarios nav item in Planning sidebar + /scenarios list page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
||||
import { useLocalStorage } from "~/hooks/useLocalStorage.js";
|
||||
import { formatDate } from "~/lib/format.js";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
import { AllocationModal } from "./AllocationModal.js";
|
||||
@@ -22,6 +23,7 @@ import { useViewPrefs } from "~/hooks/useViewPrefs.js";
|
||||
import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js";
|
||||
import { ALLOCATION_STATUS_BADGE as STATUS_BADGE } from "~/lib/status-styles.js";
|
||||
import { SuccessToast } from "~/components/ui/SuccessToast.js";
|
||||
import { EmptyState } from "~/components/ui/EmptyState.js";
|
||||
import {
|
||||
collapseAllAllocationGroups,
|
||||
createInitialCollapsedAllocationGroups,
|
||||
@@ -240,10 +242,7 @@ export function AllocationsClient() {
|
||||
);
|
||||
|
||||
// ─── View mode: grouped (default) vs flat ──────────────────────────────────
|
||||
const [viewMode, setViewMode] = useState<"grouped" | "flat">(() => {
|
||||
if (typeof window === "undefined") return "grouped";
|
||||
return (localStorage.getItem("capakraken:allocations:viewMode") as "grouped" | "flat") ?? "grouped";
|
||||
});
|
||||
const [viewMode, setViewMode] = useLocalStorage<"grouped" | "flat">("capakraken:allocations:viewMode", "grouped");
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<CollapsedAllocationGroups>(
|
||||
() => createInitialCollapsedAllocationGroups(),
|
||||
);
|
||||
@@ -252,12 +251,8 @@ export function AllocationsClient() {
|
||||
const hasEvaluatedInitialVisibility = useRef(false);
|
||||
|
||||
const toggleViewMode = useCallback(() => {
|
||||
setViewMode((prev) => {
|
||||
const next = prev === "grouped" ? "flat" : "grouped";
|
||||
localStorage.setItem("capakraken:allocations:viewMode", next);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
setViewMode((prev) => prev === "grouped" ? "flat" : "grouped");
|
||||
}, [setViewMode]);
|
||||
|
||||
type ProjectSubGroup = {
|
||||
projectId: string;
|
||||
@@ -750,20 +745,21 @@ export function AllocationsClient() {
|
||||
|
||||
{!isLoading && !allocationQueryFailure && sorted.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={totalColSpan} className="py-12 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<div data-testid="allocations-empty-state" 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 colSpan={totalColSpan}>
|
||||
{emptyState.showResetAction ? (
|
||||
<EmptyState
|
||||
testId="allocations-empty-state"
|
||||
title={emptyState.title}
|
||||
detail={emptyState.detail}
|
||||
action={{ label: "Show all assignments", onClick: clearAll }}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState
|
||||
testId="allocations-empty-state"
|
||||
title={emptyState.title}
|
||||
detail={emptyState.detail}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user