From 625a842d89223792b1c85ae77291dec4e9e8cbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sat, 14 Mar 2026 23:29:07 +0100 Subject: [PATCH] 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 --- .../app/(app)/estimates/EstimatesClient.tsx | 239 +++- .../src/app/(app)/projects/ProjectsClient.tsx | 105 +- apps/web/src/app/(app)/projects/page.tsx | 6 +- .../app/(app)/resources/ResourcesClient.tsx | 1110 +++++++++++++-- apps/web/src/app/(app)/staffing/page.tsx | 10 +- apps/web/src/app/(app)/timeline/page.tsx | 10 +- .../src/app/api/reports/allocations/route.ts | 25 +- apps/web/src/app/auth/signin/page.tsx | 139 +- apps/web/src/app/globals.css | 236 +++- apps/web/src/app/layout.tsx | 15 +- .../components/admin/SystemSettingsClient.tsx | 1102 +++++++++------ .../components/dashboard/DashboardClient.tsx | 63 +- .../dashboard/widgets/ChargeabilityWidget.tsx | 332 ++++- .../dashboard/widgets/ProjectTableWidget.tsx | 163 ++- .../dashboard/widgets/ResourceTableWidget.tsx | 229 +++- .../dashboard/widgets/StatCardsWidget.tsx | 25 +- .../components/estimates/ApplyEffortRules.tsx | 2 +- .../estimates/CommercialTermsEditor.tsx | 464 +++++++ apps/web/src/components/layout/AppShell.tsx | 189 ++- .../notifications/NotificationBell.tsx | 9 +- .../reports/ChargeabilityReportClient.tsx | 400 +++--- .../components/resources/ResourceDetail.tsx | 39 +- .../components/resources/ResourceModal.tsx | 41 +- apps/web/src/components/roles/RoleModal.tsx | 78 +- apps/web/src/components/roles/RolesClient.tsx | 137 +- .../src/components/staffing/StaffingPanel.tsx | 293 ++-- .../components/timeline/TimelineFilter.tsx | 249 +++- .../components/timeline/TimelineToolbar.tsx | 50 +- .../src/components/timeline/TimelineView.tsx | 157 ++- apps/web/src/components/ui/FilterBar.tsx | 4 +- apps/web/src/components/ui/NavProgressBar.tsx | 4 +- apps/web/src/lib/trpc/provider.tsx | 18 +- apps/web/tailwind.config.ts | 3 +- docs/imports/departed-sync-2026-03-14.md | 49 + docs/product-roadmap.md | 6 +- .../api/src/__tests__/anonymization.test.ts | 129 ++ .../chargeability-report-router.test.ts | 207 +++ .../src/__tests__/effort-rule-router.test.ts | 540 ++++++++ .../src/__tests__/entitlement-router.test.ts | 443 ++++++ .../api/src/__tests__/estimate-router.test.ts | 1212 +++++++++++++++++ .../experience-multiplier-router.test.ts | 629 +++++++++ .../src/__tests__/notification-router.test.ts | 263 ++++ .../api/src/__tests__/resource-router.test.ts | 523 +++++++ .../api/src/__tests__/staffing-router.test.ts | 297 ++++ .../api/src/__tests__/vacation-router.test.ts | 854 ++++++++++++ packages/api/src/lib/anonymization.ts | 841 ++++++++++++ .../api/src/router/chargeability-report.ts | 15 +- packages/api/src/router/dashboard.ts | 46 +- packages/api/src/router/settings.ts | 20 + packages/application/package.json | 1 + .../src/__tests__/assignment-bookings.test.ts | 22 +- .../commit-dispo-import-batch.test.ts | 157 +++ .../src/__tests__/dashboard.test.ts | 171 +++ .../src/__tests__/dispo-import.test.ts | 5 + packages/application/src/index.ts | 10 + .../allocation/chargeability-bookings.ts | 53 + .../allocation/list-assignment-bookings.ts | 10 +- .../dashboard/get-chargeability-overview.ts | 37 +- .../dispo-import/parse-dispo-matrix.ts | 16 +- .../use-cases/dispo-import/read-workbook.ts | 9 +- .../dispo-import/stage-dispo-projects.ts | 84 +- .../use-cases/dispo-import/tbd-projects.ts | 116 ++ .../src/use-cases/resource/index.ts | 4 + .../recompute-resource-value-scores.ts | 111 ++ packages/db/src/import-dispo-batch.test.ts | 3 + packages/db/src/import-dispo-batch.ts | 10 + .../src/__tests__/commercial-terms.test.ts | 226 +++ .../engine/src/estimate/commercial-terms.ts | 129 ++ packages/engine/src/estimate/index.ts | 1 + packages/shared/src/constants/columns.ts | 2 + .../shared/src/schemas/estimate.schema.ts | 30 + packages/shared/src/types/estimate.ts | 32 + packages/shared/src/types/resource.ts | 1 + pnpm-lock.yaml | 3 + 74 files changed, 11680 insertions(+), 1583 deletions(-) create mode 100644 apps/web/src/components/estimates/CommercialTermsEditor.tsx create mode 100644 docs/imports/departed-sync-2026-03-14.md create mode 100644 packages/api/src/__tests__/anonymization.test.ts create mode 100644 packages/api/src/__tests__/chargeability-report-router.test.ts create mode 100644 packages/api/src/__tests__/effort-rule-router.test.ts create mode 100644 packages/api/src/__tests__/entitlement-router.test.ts create mode 100644 packages/api/src/__tests__/estimate-router.test.ts create mode 100644 packages/api/src/__tests__/experience-multiplier-router.test.ts create mode 100644 packages/api/src/__tests__/notification-router.test.ts create mode 100644 packages/api/src/__tests__/resource-router.test.ts create mode 100644 packages/api/src/__tests__/staffing-router.test.ts create mode 100644 packages/api/src/__tests__/vacation-router.test.ts create mode 100644 packages/api/src/lib/anonymization.ts create mode 100644 packages/application/src/use-cases/allocation/chargeability-bookings.ts create mode 100644 packages/application/src/use-cases/dispo-import/tbd-projects.ts create mode 100644 packages/application/src/use-cases/resource/index.ts create mode 100644 packages/application/src/use-cases/resource/recompute-resource-value-scores.ts create mode 100644 packages/engine/src/__tests__/commercial-terms.test.ts create mode 100644 packages/engine/src/estimate/commercial-terms.ts diff --git a/apps/web/src/app/(app)/estimates/EstimatesClient.tsx b/apps/web/src/app/(app)/estimates/EstimatesClient.tsx index 6ed7780..a145fc1 100644 --- a/apps/web/src/app/(app)/estimates/EstimatesClient.tsx +++ b/apps/web/src/app/(app)/estimates/EstimatesClient.tsx @@ -2,18 +2,71 @@ import { useMemo, useState } from "react"; import Link from "next/link"; -import type { AppRouter } from "@planarchy/api/router"; import { EstimateStatus, type EstimateVersionStatus } from "@planarchy/shared"; -import type { inferRouterOutputs } from "@trpc/server"; import { clsx } from "clsx"; import { EstimateWizard } from "~/components/estimates/EstimateWizard.js"; import { usePermissions } from "~/hooks/usePermissions.js"; import { formatDateLong, formatMoney } from "~/lib/format.js"; import { trpc } from "~/lib/trpc/client.js"; -type RouterOutput = inferRouterOutputs; -type EstimateListItem = RouterOutput["estimate"]["list"][number]; -type EstimateDetail = RouterOutput["estimate"]["getById"]; +type EstimateMetric = { + id: string; + key: string; + label: string; + valueDecimal: number; + valueCents: number | null; + currency: string | null; +}; + +type EstimateScopeItem = { + id: string; + name: string; + description: string | null; + scopeType: string; +}; + +type EstimateDemandLine = { + id: string; + name: string; + hours: number; + costTotalCents: number; + priceTotalCents: number; + currency: string; + chapter: string | null; +}; + +type EstimateVersion = { + versionNumber: number; + label: string | null; + status: EstimateVersionStatus; + notes: string | null; + metrics: EstimateMetric[]; + scopeItems: EstimateScopeItem[]; + demandLines: EstimateDemandLine[]; +}; + +type EstimateProjectRef = { + shortCode: string; + name: string; +}; + +type EstimateListItem = { + id: string; + name: string; + status: EstimateStatus; + opportunityId: string | null; + updatedAt: Date | string; + project: EstimateProjectRef | null; + versions: Array>; +}; + +type EstimateDetail = { + id: string; + name: string; + status: EstimateStatus; + project: EstimateProjectRef | null; + versions: EstimateVersion[]; +}; const STATUS_STYLES: Record = { DRAFT: "bg-slate-100 text-slate-700", @@ -30,7 +83,7 @@ const VERSION_STYLES: Record = { SUPERSEDED: "bg-zinc-200 text-zinc-700", }; -function formatMetricValue(metric: EstimateDetail["versions"][number]["metrics"][number]) { +function formatMetricValue(metric: EstimateMetric) { if (metric.valueCents != null) { return formatMoney(metric.valueCents, metric.currency ?? "EUR"); } @@ -42,7 +95,13 @@ function formatMetricValue(metric: EstimateDetail["versions"][number]["metrics"] function getLatestVersion(estimate: EstimateDetail | null | undefined) { if (!estimate) return null; - return [...estimate.versions].sort((left, right) => right.versionNumber - left.versionNumber)[0] ?? null; + let latest = estimate.versions[0] ?? null; + for (const version of estimate.versions) { + if (!latest || version.versionNumber > latest.versionNumber) { + latest = version; + } + } + return latest; } function EstimateDetailPanel({ @@ -58,16 +117,27 @@ function EstimateDetailPanel({ const latestMetrics = latestVersion?.metrics ?? []; return ( -