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:
2026-04-09 13:08:19 +02:00
parent a16c41e739
commit 6831e199c6
9 changed files with 272 additions and 35 deletions
@@ -0,0 +1,111 @@
"use client";
import Link from "next/link";
import { trpc } from "~/lib/trpc/client.js";
import { EmptyState } from "~/components/ui/EmptyState.js";
import { formatDate } from "~/lib/format.js";
const PROJECT_STATUS_BADGE: Record<string, string> = {
ACTIVE: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300",
DRAFT: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400",
ON_HOLD: "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300",
COMPLETED: "bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300",
CANCELLED: "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300",
};
export function ScenariosListClient() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { data, isLoading } = (trpc.project.list.useQuery as any)({ limit: 500 }, { staleTime: 60_000 }) as {
data: { projects: Array<{ id: string; shortCode: string; name: string; status: string; startDate: string | null; endDate: string | null; clientId: string | null }> } | undefined;
isLoading: boolean;
};
const projects = data?.projects ?? [];
return (
<div className="app-page space-y-6">
<div className="app-page-header">
<div>
<h1 className="app-page-title">Scenario Planning</h1>
<p className="app-page-subtitle mt-1">
Explore what-if staffing scenarios for any project without committing changes.
</p>
</div>
</div>
<div className="app-surface overflow-hidden">
{isLoading ? (
<div className="divide-y divide-gray-100 dark:divide-gray-800">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-center gap-4 px-4 py-3">
<div className="h-4 w-20 shimmer-skeleton rounded" />
<div className="h-4 flex-1 shimmer-skeleton rounded" />
<div className="h-4 w-24 shimmer-skeleton rounded" />
</div>
))}
</div>
) : projects.length === 0 ? (
<EmptyState
title="No projects yet"
detail="Create a project first, then return here to plan scenarios."
action={{ label: "Go to Projects", onClick: () => { window.location.href = "/projects"; } }}
/>
) : (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-100 dark:border-gray-800 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
<th className="px-4 py-3 text-left">Code</th>
<th className="px-4 py-3 text-left">Project</th>
<th className="px-4 py-3 text-left">Client</th>
<th className="px-4 py-3 text-left">Status</th>
<th className="px-4 py-3 text-left">Start</th>
<th className="px-4 py-3 text-left">End</th>
<th className="px-4 py-3" />
</tr>
</thead>
<tbody className="divide-y divide-gray-50 dark:divide-gray-800/50">
{projects.map((p) => (
<tr
key={p.id}
className="group hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors"
>
<td className="px-4 py-3 font-mono text-xs text-gray-500 dark:text-gray-400">
{p.shortCode}
</td>
<td className="px-4 py-3 font-medium text-gray-900 dark:text-gray-100">
{p.name}
</td>
<td className="px-4 py-3 text-gray-500 dark:text-gray-400">
{p.clientId ?? "—"}
</td>
<td className="px-4 py-3">
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${PROJECT_STATUS_BADGE[p.status] ?? PROJECT_STATUS_BADGE["DRAFT"]}`}>
{p.status.charAt(0) + p.status.slice(1).toLowerCase().replace("_", " ")}
</span>
</td>
<td className="px-4 py-3 text-gray-500 dark:text-gray-400 tabular-nums">
{p.startDate ? formatDate(new Date(p.startDate)) : "—"}
</td>
<td className="px-4 py-3 text-gray-500 dark:text-gray-400 tabular-nums">
{p.endDate ? formatDate(new Date(p.endDate)) : "—"}
</td>
<td className="px-4 py-3 text-right">
<Link
href={`/projects/${p.id}/scenario`}
className="inline-flex items-center gap-1 rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-1 text-xs font-medium text-gray-600 dark:text-gray-300 opacity-0 group-hover:opacity-100 transition hover:border-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800"
>
Open Scenario
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Link>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</div>
);
}