feat: dashboard overhaul, chargeability reports, dispo import enhancements, UI polish

Dashboard: expanded chargeability widget, resource/project table widgets
with sorting and filters, stat cards with formatMoney integration.

Chargeability: new report client with filtering, chargeability-bookings
use case, updated dashboard overview logic.

Dispo import: TBD project handling, parse-dispo-matrix improvements,
stage-dispo-projects resource value scores, new tests.

Estimates: CommercialTermsEditor component, commercial-terms engine
module, expanded estimate schemas and types.

UI: AppShell navigation updates, timeline filter/toolbar enhancements,
role management improvements, signin page redesign, Tailwind/globals
polish, SystemSettings SMTP section, anonymization support.

Tests: new router tests (anonymization, chargeability, effort-rule,
entitlement, estimate, experience-multiplier, notification, resource,
staffing, vacation).

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-03-14 23:29:07 +01:00
parent ad0855902b
commit 625a842d89
74 changed files with 11680 additions and 1583 deletions
@@ -1,6 +1,7 @@
"use client";
import { clsx } from "clsx";
import { useRef } from "react";
import { TimelineFilter, type TimelineFilters } from "./TimelineFilter.js";
interface TimelineToolbarProps {
@@ -40,35 +41,40 @@ export function TimelineToolbar({
onUndo,
onRedo,
}: TimelineToolbarProps) {
const activeFilterCount = filters.chapters.length + filters.eids.length + filters.projectIds.length;
const activeFilterCount =
filters.chapters.length + filters.eids.length + filters.projectIds.length;
const filterAnchorRef = useRef<HTMLDivElement | null>(null);
return (
<div className="flex items-center justify-between mb-2 gap-3">
<div className="text-sm text-gray-500">
<div className="app-toolbar flex flex-wrap items-center justify-between gap-3">
<div className="text-sm text-gray-500 dark:text-gray-400">
{viewMode === "resource"
? `${resourceCount} resources · ${totalAllocCount} allocations`
: `${projectCount} projects`}
</div>
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center justify-end gap-2">
{/* Timeline navigation */}
<div className="flex items-center gap-1">
<button
type="button"
onClick={onNavigateBack}
className="px-2.5 py-1.5 text-sm rounded-lg border border-gray-200 text-gray-600 hover:bg-gray-50 transition-colors"
className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800"
title="Previous 4 weeks"
>
</button>
<button
type="button"
onClick={onNavigateToday}
className="px-3 py-1.5 text-sm rounded-lg border border-gray-200 text-gray-600 hover:bg-gray-50 transition-colors"
className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800"
>
Today
</button>
<button
type="button"
onClick={onNavigateForward}
className="px-2.5 py-1.5 text-sm rounded-lg border border-gray-200 text-gray-600 hover:bg-gray-50 transition-colors"
className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800"
title="Next 4 weeks"
>
@@ -79,18 +85,20 @@ export function TimelineToolbar({
{(onUndo ?? onRedo) && (
<div className="flex items-center gap-1">
<button
type="button"
onClick={onUndo}
disabled={!canUndo}
title="Undo (Ctrl+Z)"
className="px-2 py-1.5 text-sm rounded-lg border border-gray-200 text-gray-600 hover:bg-gray-50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800"
>
</button>
<button
type="button"
onClick={onRedo}
disabled={!canRedo}
title="Redo (Ctrl+Shift+Z / Ctrl+Y)"
className="px-2 py-1.5 text-sm rounded-lg border border-gray-200 text-gray-600 hover:bg-gray-50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
className="rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm text-gray-700 transition hover:border-gray-400 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800"
>
</button>
@@ -98,25 +106,27 @@ export function TimelineToolbar({
)}
{/* View mode toggle */}
<div className="flex rounded-lg border border-gray-200 overflow-hidden text-sm">
<div className="flex overflow-hidden rounded-xl border border-gray-300 bg-white text-sm dark:border-gray-600 dark:bg-gray-900">
<button
type="button"
onClick={() => onViewModeChange("resource")}
className={clsx(
"px-3 py-1.5 transition-colors",
"px-3 py-2 transition-colors",
viewMode === "resource"
? "bg-brand-600 text-white"
: "text-gray-600 hover:bg-gray-50",
: "text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800",
)}
>
Resource view
</button>
<button
type="button"
onClick={() => onViewModeChange("project")}
className={clsx(
"px-3 py-1.5 border-l border-gray-200 transition-colors",
"border-l border-gray-300 px-3 py-2 transition-colors dark:border-gray-600",
viewMode === "project"
? "bg-brand-600 text-white"
: "text-gray-600 hover:bg-gray-50",
: "text-gray-700 hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-gray-800",
)}
>
Project view
@@ -124,24 +134,26 @@ export function TimelineToolbar({
</div>
{/* Filter */}
<div className="relative">
<div ref={filterAnchorRef} className="relative">
<button
type="button"
onClick={() => onFilterOpenChange(!filterOpen)}
className={clsx(
"flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg border transition-colors",
"flex items-center gap-2 rounded-xl border px-3 py-2 text-sm transition-colors",
filterOpen || activeFilterCount > 0
? "bg-brand-50 border-brand-300 text-brand-700"
: "border-gray-200 text-gray-600 hover:bg-gray-50",
? "border-brand-300 bg-brand-50 text-brand-700 dark:border-brand-700 dark:bg-brand-950/40 dark:text-brand-200"
: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800",
)}
>
Filter
{activeFilterCount > 0 && (
<span className="w-4 h-4 rounded-full bg-brand-600 text-white text-xs flex items-center justify-center">
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-brand-600 text-xs text-white">
{activeFilterCount}
</span>
)}
</button>
<TimelineFilter
anchorRef={filterAnchorRef}
filters={filters}
onChange={onFiltersChange}
isOpen={filterOpen}