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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user