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
@@ -29,7 +29,11 @@ const GridLayout = dynamic(() => import("react-grid-layout").then((m) => m.Respo
ssr: false,
});
function renderWidget(type: DashboardWidgetType, config: DashboardWidgetConfig, onConfigChange: (u: Record<string, unknown>) => void) {
function renderWidget(
type: DashboardWidgetType,
config: DashboardWidgetConfig,
onConfigChange: (u: Record<string, unknown>) => void,
) {
const widget = getWidget(type);
const Component = widget.component;
@@ -73,48 +77,59 @@ export function DashboardClient() {
};
return (
<div className="p-6">
{/* Toolbar */}
<div className="mb-6 flex items-center justify-between">
<div className="app-page space-y-6">
<div className="app-page-header gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="text-sm text-gray-500 mt-1">Drag to rearrange, resize from corners</p>
<h1 className="app-page-title">Dashboard</h1>
<p className="app-page-subtitle mt-1">
Drag widgets to rearrange them and resize from the corners.
</p>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={resetLayout}
className="px-3 py-2 text-sm text-gray-500 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
className="rounded-xl border border-gray-300 px-3 py-2 text-sm font-medium text-gray-600 transition hover:border-gray-400 hover:bg-gray-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
>
Reset
</button>
<button
type="button"
onClick={() => setAddModalOpen(true)}
className="flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium transition-colors"
className="inline-flex items-center gap-2 rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
Add Widget
</button>
</div>
</div>
{/* Grid */}
{config.widgets.length === 0 ? (
<div className="flex flex-col items-center justify-center py-24 text-center border-2 border-dashed border-gray-200 rounded-xl">
<p className="text-gray-400 text-sm mb-4">No widgets yet. Add your first widget to get started.</p>
<div className="app-surface-strong flex flex-col items-center justify-center border-dashed py-24 text-center">
<p className="text-sm font-medium text-gray-700 dark:text-gray-200">
No widgets on this dashboard yet.
</p>
<p className="mt-2 max-w-md text-sm text-gray-500">
Start with a widget and build a view that matches how your team actually plans and
monitors work.
</p>
<button
type="button"
onClick={() => setAddModalOpen(true)}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium"
className="mt-5 rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700"
>
+ Add Widget
Add Widget
</button>
</div>
) : (
<div ref={containerRef}>
<div ref={containerRef} className="app-surface overflow-hidden p-3">
{(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const AnyGridLayout = GridLayout as any;
@@ -128,7 +143,13 @@ export function DashboardClient() {
rowHeight={80}
compactType={null}
preventCollision={false}
onLayoutChange={(_: unknown, allLayouts: Record<string, { i: string; x: number; y: number; w: number; h: number }[]>) => onLayoutChange(allLayouts["lg"] ?? [])}
onLayoutChange={(
_: unknown,
allLayouts: Record<
string,
{ i: string; x: number; y: number; w: number; h: number }[]
>,
) => onLayoutChange(allLayouts["lg"] ?? [])}
draggableHandle=".widget-drag-handle"
margin={[12, 12]}
>
@@ -138,10 +159,8 @@ export function DashboardClient() {
title={widget.title ?? getWidget(widget.type).label}
onRemove={() => removeWidget(widget.id)}
>
{renderWidget(
widget.type,
widget.config,
(update) => updateWidgetConfig(widget.id, update),
{renderWidget(widget.type, widget.config, (update) =>
updateWidgetConfig(widget.id, update),
)}
</WidgetContainer>
</div>
@@ -152,9 +171,7 @@ export function DashboardClient() {
</div>
)}
{addModalOpen && (
<AddWidgetModal onAdd={addWidget} onClose={() => setAddModalOpen(false)} />
)}
{addModalOpen && <AddWidgetModal onAdd={addWidget} onClose={() => setAddModalOpen(false)} />}
</div>
);
}