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 ( +