From 4a49ec4f05dd11454f54a66b6e1ed84b90595886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Mon, 6 Apr 2026 00:11:12 +0200 Subject: [PATCH] fix(sanity): resolve 15 gaps from sanity check audit (G-01 through G-15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - G-01: ProjectWizard renders blueprint fieldDefs with DynamicFieldInput component - G-02: Blueprint rolePresets validated via RolePresetsSchema in wizard; API keeps loose schema - G-03: ProjectWizard step 2/3 validation (role, hoursPerDay, headcount required) - G-04: EstimateWizard validates baseCurrency and demand line cost rates - G-05: Project lifecycle transition guards with ALLOWED_TRANSITIONS map - G-06: Blueprint validator extended for minLength/maxLength/pattern and DATE range checks - G-07: assertBlueprintDynamicFields merges global blueprint fieldDefs into validation - G-08: (tracked — chapter managed dropdown; deferred to backend ticket) - G-09: JSDoc added to lcrCents/ucrCents clarifying LCR/UCR terminology - G-10: Dispo route redirect already in place — closed as done - G-11: packages/ui empty by design — closed as documented - G-12: @deprecated JSDoc added to CreateAllocationSchema and UpdateAllocationSchema - G-13: ProjectWizard review step enhanced with blueprint name, field values, skills, assignments - G-14: ProjectWizard handleSubmit collects per-item warnings instead of silent swallowing - G-15: Vacation cancel reverses usedDays entitlement for APPROVED ANNUAL/OTHER vacations Tests: all 1575 passing (1 pre-existing failure in insights-summary unrelated to these changes) Co-Authored-By: Claude Sonnet 4.6 --- .../components/estimates/EstimateWizard.tsx | 19 + .../src/components/projects/ProjectWizard.tsx | 326 +++++++++++++-- docs/sanity-check.md | 384 ++++++++++++++++++ ...assistant-tools-admin-crud-test-helpers.ts | 19 +- ...tools-project-admin-create-success.test.ts | 1 + ...nt-tools-vacation-mutation-test-helpers.ts | 6 + .../__tests__/blueprint-validation.test.ts | 1 + .../api/src/__tests__/project-router.test.ts | 2 + .../api/src/__tests__/vacation-router.test.ts | 14 +- .../src/router/blueprint-procedure-support.ts | 2 +- .../api/src/router/blueprint-validation.ts | 56 ++- packages/api/src/router/project-lifecycle.ts | 27 ++ .../router/vacation-management-procedures.ts | 16 + packages/engine/src/blueprint/validator.ts | 36 +- .../shared/src/schemas/allocation.schema.ts | 9 + .../shared/src/schemas/blueprint.schema.ts | 23 ++ packages/shared/src/types/resource.ts | 8 + 17 files changed, 903 insertions(+), 46 deletions(-) create mode 100644 docs/sanity-check.md diff --git a/apps/web/src/components/estimates/EstimateWizard.tsx b/apps/web/src/components/estimates/EstimateWizard.tsx index e1eb1a6..8aa73f7 100644 --- a/apps/web/src/components/estimates/EstimateWizard.tsx +++ b/apps/web/src/components/estimates/EstimateWizard.tsx @@ -296,11 +296,30 @@ export function EstimateWizard({ onClose }: { onClose: () => void }) { } function validateStep(targetStep: number) { + // Moving from step 0 → step 1: require name if (targetStep === 1 && !name.trim()) { setError("Estimate name is required."); return false; } + // Moving from step 0 → step 1: require base currency + if (targetStep === 1 && !baseCurrency.trim()) { + setError("Base currency is required."); + return false; + } + + // Moving from step 3 (Staffing) → step 4 (Review): validate demand lines + if (targetStep === 4) { + const linesWithHours = demandLines.filter((l) => toHours(l.hours) > 0); + const invalid = linesWithHours.find((l) => toCents(l.costRate) <= 0); + if (invalid) { + setError( + `Demand line "${invalid.name || "unnamed"}" has hours but no cost rate. Please enter a cost rate or remove the line.`, + ); + return false; + } + } + setError(null); return true; } diff --git a/apps/web/src/components/projects/ProjectWizard.tsx b/apps/web/src/components/projects/ProjectWizard.tsx index 79aaaf7..23e4945 100644 --- a/apps/web/src/components/projects/ProjectWizard.tsx +++ b/apps/web/src/components/projects/ProjectWizard.tsx @@ -3,8 +3,8 @@ import { createPortal } from "react-dom"; import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { clsx } from "clsx"; -import type { StaffingRequirement } from "@capakraken/shared"; -import { BlueprintTarget, OrderType, AllocationType, ProjectStatus, AllocationStatus } from "@capakraken/shared"; +import type { StaffingRequirement, BlueprintFieldDefinition } from "@capakraken/shared"; +import { BlueprintTarget, FieldType, OrderType, AllocationType, ProjectStatus, AllocationStatus, RolePresetsSchema } from "@capakraken/shared"; import { trpc } from "~/lib/trpc/client.js"; import { uuid } from "~/lib/uuid.js"; import { DateInput } from "~/components/ui/DateInput.js"; @@ -70,6 +70,9 @@ interface WizardState { staffingReqs: StaffingRequirement[]; assignments: Assignment[]; saveAsDraft: boolean; + dynamicFields: Record; + blueprintName: string | null; + blueprintFieldDefs: BlueprintFieldDefinition[]; } function formatDateForInput(date: Date): string { @@ -95,6 +98,9 @@ function makeDefaultState(): WizardState { staffingReqs: [], assignments: [], saveAsDraft: true, + dynamicFields: {}, + blueprintName: null, + blueprintFieldDefs: [], }; } @@ -160,6 +166,117 @@ function StepBar({ current }: { current: number }) { ); } +// ─── Dynamic Field Input ────────────────────────────────────────────────────── + +function DynamicFieldInput({ + field, + value, + onChange, +}: { + field: BlueprintFieldDefinition; + value: unknown; + onChange: (key: string, val: unknown) => void; +}) { + const strVal = value !== undefined && value !== null ? String(value) : ""; + const arrVal = Array.isArray(value) ? (value as string[]) : []; + + switch (field.type) { + case FieldType.TEXTAREA: + return ( +