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