From ad0855902b74be0343683d6feebf7b575f4439d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sat, 14 Mar 2026 23:03:42 +0100 Subject: [PATCH] refactor: complete v2 refactoring plan (Phases 1-5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 — Quick Wins: centralize formatMoney/formatCents, extract findUniqueOrThrow helper (19 routers), shared Prisma select constants, useInvalidatePlanningViews hook, status badge consolidation, composite DB indexes. Phase 2 — Timeline Split: extract TimelineContext, TimelineResourcePanel, TimelineProjectPanel; split 28-dep useMemo into 3 focused memos. TimelineView.tsx reduced from 1,903 to 538 lines. Phase 3 — Query Performance: server-side filtering for getEntriesView, remove availability from timeline resource select, SSE event debouncing (50ms batch window). Phase 4 — Estimate Workspace: extract 7 tab components and 3 editor components. EstimateWorkspaceClient 1,298→306 lines, EstimateWorkspaceDraftEditor 1,205→581 lines. Phase 5 — Package Cleanup: split commit-dispo-import-batch (1,112→573 lines), extract shared pagination helper with 11 tests. All tests pass: 209 API, 254 engine, 67 application. Co-Authored-By: claude-flow --- .../app/(app)/estimates/EstimatesClient.tsx | 10 +- .../src/components/admin/RateCardsClient.tsx | 6 +- .../allocations/AllocationModal.tsx | 13 +- .../allocations/AllocationsClient.tsx | 80 +- .../dashboard/widgets/ProjectTableWidget.tsx | 9 +- .../dashboard/widgets/StatCardsWidget.tsx | 5 +- .../estimates/ApplyExperienceMultipliers.tsx | 10 +- .../components/estimates/EstimateWizard.tsx | 9 +- .../estimates/EstimateWorkspace.types.ts | 2 + .../estimates/EstimateWorkspaceClient.tsx | 1002 +--------- .../EstimateWorkspaceDraftEditor.tsx | 722 +------ .../components/estimates/VersionCompare.tsx | 9 +- .../estimates/editors/AssumptionEditor.tsx | 79 + .../estimates/editors/DemandLineEditor.tsx | 574 ++++++ .../estimates/editors/ScopeItemEditor.tsx | 119 ++ .../estimates/tabs/AssumptionsTab.tsx | 51 + .../components/estimates/tabs/ExportsTab.tsx | 226 +++ .../estimates/tabs/FinancialsTab.tsx | 239 +++ .../components/estimates/tabs/OverviewTab.tsx | 174 ++ .../components/estimates/tabs/ScopeTab.tsx | 52 + .../components/estimates/tabs/StaffingTab.tsx | 185 ++ .../components/estimates/tabs/VersionsTab.tsx | 179 ++ .../components/timeline/TimelineContext.tsx | 441 ++++ .../timeline/TimelineProjectPanel.tsx | 630 ++++++ .../timeline/TimelineResourcePanel.tsx | 830 ++++++++ .../src/components/timeline/TimelineView.tsx | 1780 +++-------------- .../vacations/MyVacationsClient.tsx | 15 +- .../components/vacations/VacationClient.tsx | 15 +- .../src/hooks/useInvalidatePlanningViews.ts | 20 + apps/web/src/lib/format.ts | 18 + apps/web/src/lib/status-styles.ts | 34 + docs/refactor-v2-plan.md | 449 +++++ .../src/__tests__/event-bus-debounce.test.ts | 153 ++ packages/api/src/__tests__/pagination.test.ts | 169 ++ packages/api/src/db/helpers.ts | 12 + packages/api/src/db/pagination.ts | 100 + packages/api/src/db/selects.ts | 3 + packages/api/src/index.ts | 3 +- packages/api/src/router/allocation.ts | 48 +- .../api/src/router/blueprint-validation.ts | 16 +- packages/api/src/router/blueprint.ts | 25 +- packages/api/src/router/client.ts | 33 +- packages/api/src/router/country.ts | 43 +- packages/api/src/router/effort-rule.ts | 101 +- packages/api/src/router/estimate.ts | 157 +- .../api/src/router/experience-multiplier.ts | 97 +- packages/api/src/router/management-level.ts | 49 +- packages/api/src/router/org-unit.ts | 33 +- .../src/router/project-planning-read-model.ts | 20 + packages/api/src/router/project.ts | 84 +- packages/api/src/router/rate-card.ts | 56 +- packages/api/src/router/resource.ts | 427 ++-- packages/api/src/router/role.ts | 47 +- packages/api/src/router/staffing.ts | 49 +- packages/api/src/router/timeline.ts | 114 +- packages/api/src/router/user.ts | 30 +- .../api/src/router/utilization-category.ts | 19 +- packages/api/src/router/vacation.ts | 82 +- packages/api/src/sse/event-bus.ts | 84 +- .../dispo-import/build-dispo-maps.ts | 222 ++ .../dispo-import/commit-dispo-batch-types.ts | 68 + .../dispo-import/commit-dispo-import-batch.ts | 1230 ++++-------- .../dispo-import/determine-placement.ts | 197 ++ .../dispo-import/validate-dispo-batch.ts | 79 + packages/db/prisma/schema.prisma | 11 + 65 files changed, 7108 insertions(+), 4740 deletions(-) create mode 100644 apps/web/src/components/estimates/editors/AssumptionEditor.tsx create mode 100644 apps/web/src/components/estimates/editors/DemandLineEditor.tsx create mode 100644 apps/web/src/components/estimates/editors/ScopeItemEditor.tsx create mode 100644 apps/web/src/components/estimates/tabs/AssumptionsTab.tsx create mode 100644 apps/web/src/components/estimates/tabs/ExportsTab.tsx create mode 100644 apps/web/src/components/estimates/tabs/FinancialsTab.tsx create mode 100644 apps/web/src/components/estimates/tabs/OverviewTab.tsx create mode 100644 apps/web/src/components/estimates/tabs/ScopeTab.tsx create mode 100644 apps/web/src/components/estimates/tabs/StaffingTab.tsx create mode 100644 apps/web/src/components/estimates/tabs/VersionsTab.tsx create mode 100644 apps/web/src/components/timeline/TimelineContext.tsx create mode 100644 apps/web/src/components/timeline/TimelineProjectPanel.tsx create mode 100644 apps/web/src/components/timeline/TimelineResourcePanel.tsx create mode 100644 apps/web/src/hooks/useInvalidatePlanningViews.ts create mode 100644 apps/web/src/lib/status-styles.ts create mode 100644 docs/refactor-v2-plan.md create mode 100644 packages/api/src/__tests__/event-bus-debounce.test.ts create mode 100644 packages/api/src/__tests__/pagination.test.ts create mode 100644 packages/api/src/db/helpers.ts create mode 100644 packages/api/src/db/pagination.ts create mode 100644 packages/api/src/db/selects.ts create mode 100644 packages/application/src/use-cases/dispo-import/build-dispo-maps.ts create mode 100644 packages/application/src/use-cases/dispo-import/commit-dispo-batch-types.ts create mode 100644 packages/application/src/use-cases/dispo-import/determine-placement.ts create mode 100644 packages/application/src/use-cases/dispo-import/validate-dispo-batch.ts diff --git a/apps/web/src/app/(app)/estimates/EstimatesClient.tsx b/apps/web/src/app/(app)/estimates/EstimatesClient.tsx index 32f9dea..6ed7780 100644 --- a/apps/web/src/app/(app)/estimates/EstimatesClient.tsx +++ b/apps/web/src/app/(app)/estimates/EstimatesClient.tsx @@ -8,7 +8,7 @@ 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 } from "~/lib/format.js"; +import { formatDateLong, formatMoney } from "~/lib/format.js"; import { trpc } from "~/lib/trpc/client.js"; type RouterOutput = inferRouterOutputs; @@ -30,14 +30,6 @@ const VERSION_STYLES: Record = { SUPERSEDED: "bg-zinc-200 text-zinc-700", }; -function formatMoney(cents: number | null | undefined, currency = "EUR") { - return new Intl.NumberFormat("de-DE", { - style: "currency", - currency, - maximumFractionDigits: 0, - }).format((cents ?? 0) / 100); -} - function formatMetricValue(metric: EstimateDetail["versions"][number]["metrics"][number]) { if (metric.valueCents != null) { return formatMoney(metric.valueCents, metric.currency ?? "EUR"); diff --git a/apps/web/src/components/admin/RateCardsClient.tsx b/apps/web/src/components/admin/RateCardsClient.tsx index 9da33d8..ffc34da 100644 --- a/apps/web/src/components/admin/RateCardsClient.tsx +++ b/apps/web/src/components/admin/RateCardsClient.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { formatCents } from "~/lib/format.js"; import { trpc } from "~/lib/trpc/client.js"; // ─── Local types ──────────────────────────────────────────────────────────── @@ -86,11 +87,6 @@ const emptyLine: EditingLine = { machineRateCents: 0, }; -function formatCents(cents: number | null | undefined): string { - if (cents == null) return "-"; - return (cents / 100).toFixed(2); -} - function formatDate(d: string | null | undefined): string { if (!d) return "-"; return new Date(d).toLocaleDateString("de-DE"); diff --git a/apps/web/src/components/allocations/AllocationModal.tsx b/apps/web/src/components/allocations/AllocationModal.tsx index b953d58..146e526 100644 --- a/apps/web/src/components/allocations/AllocationModal.tsx +++ b/apps/web/src/components/allocations/AllocationModal.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from "react"; import { useFocusTrap } from "~/hooks/useFocusTrap.js"; +import { useInvalidatePlanningViews } from "~/hooks/useInvalidatePlanningViews.js"; import { AllocationStatus } from "@planarchy/shared"; import type { AllocationWithDetails, RecurrencePattern } from "@planarchy/shared"; import { trpc } from "~/lib/trpc/client.js"; @@ -64,17 +65,7 @@ export function AllocationModal({ allocation, onClose, onSuccess }: AllocationMo { staleTime: 60_000 }, ); - const utils = trpc.useUtils(); - const invalidatePlanningViews = () => { - void utils.allocation.list.invalidate(); - void (utils as { allocation: { listView: { invalidate: () => Promise } } }).allocation.listView.invalidate(); - void utils.allocation.listDemands.invalidate(); - void utils.allocation.listAssignments.invalidate(); - void utils.timeline.getEntries.invalidate(); - void utils.timeline.getEntriesView.invalidate(); - void utils.timeline.getProjectContext.invalidate(); - void utils.timeline.getBudgetStatus.invalidate(); - }; + const invalidatePlanningViews = useInvalidatePlanningViews(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const createDemandMutation = (trpc.allocation.createDemandRequirement.useMutation as any)({ diff --git a/apps/web/src/components/allocations/AllocationsClient.tsx b/apps/web/src/components/allocations/AllocationsClient.tsx index c88d527..38c4ba0 100644 --- a/apps/web/src/components/allocations/AllocationsClient.tsx +++ b/apps/web/src/components/allocations/AllocationsClient.tsx @@ -20,14 +20,7 @@ import { usePermissions } from "~/hooks/usePermissions.js"; import { useColumnConfig } from "~/hooks/useColumnConfig.js"; import { useViewPrefs } from "~/hooks/useViewPrefs.js"; import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js"; - -const STATUS_BADGE: Record = { - ACTIVE: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400", - PROPOSED: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400", - CONFIRMED: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400", - COMPLETED: "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400", - CANCELLED: "bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400", -}; +import { ALLOCATION_STATUS_BADGE as STATUS_BADGE } from "~/lib/status-styles.js"; const ALL_ALLOC_STATUSES = [ { value: "PROPOSED", label: "Proposed" }, @@ -221,12 +214,11 @@ export function AllocationsClient() { const singleDeletePending = deleteDemandMutation.isPending || deleteAssignmentMutation.isPending; return ( -
- {/* Page header */} -
+
+
-

Allocations

-

+

Allocations

+

{isLoading ? "Loading…" : `${filteredAllocations.length} assignment${filteredAllocations.length !== 1 ? "s" : ""}${filteredDemands.length > 0 ? ` · ${filteredDemands.length} open demand${filteredDemands.length !== 1 ? "s" : ""}` : ""}`} @@ -237,28 +229,27 @@ export function AllocationsClient() { href="/api/reports/allocations" target="_blank" rel="noopener noreferrer" - className="px-3 py-1.5 text-sm rounded-lg border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors flex items-center gap-2" + className="inline-flex items-center gap-2 rounded-xl border border-gray-300 bg-white px-3 py-2 text-sm font-medium 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" > ↓ PDF ↓ XLS

- {/* Filters */} setFilterStatus(e.target.value)} - className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm bg-white dark:bg-gray-900 dark:text-gray-100" + className="app-select" > {ALL_ALLOC_STATUSES.map((s) => ( @@ -285,7 +276,7 @@ export function AllocationsClient() { ))} -