"use client"; import { createPortal } from "react-dom"; import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { clsx } from "clsx"; 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"; import { SkillTagInput } from "~/components/ui/SkillTagInput.js"; import { usePermissions } from "~/hooks/usePermissions.js"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { formatCents, toDateInputValue } from "~/lib/format.js"; import { ConfettiBurst } from "~/components/ui/ConfettiBurst.js"; import { SuccessToast } from "~/components/ui/SuccessToast.js"; import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js"; // ─── Constants ──────────────────────────────────────────────────────────────── const ORDER_TYPE_OPTIONS = [ { value: "BD", label: "BD" }, { value: "CHARGEABLE", label: "Chargeable" }, { value: "INTERNAL", label: "Internal" }, { value: "OVERHEAD", label: "Overhead" }, ] as const; const ALLOCATION_TYPE_OPTIONS = [ { value: "INT", label: "INT" }, { value: "EXT", label: "EXT" }, ] as const; const INPUT_CLS = "px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm w-full"; const SELECT_CLS = "px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm w-full bg-white"; const LABEL_CLS = "block text-xs font-medium text-gray-600 mb-1"; const BTN_PRIMARY = "px-5 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50 transition-colors"; const BTN_SECONDARY = "px-5 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm font-medium transition-colors"; const BTN_DANGER = "px-2 py-1 text-red-500 hover:text-red-700 hover:bg-red-50 rounded text-sm transition-colors"; // ─── Types ──────────────────────────────────────────────────────────────────── interface Assignment { requirementId: string; resourceId: string; resourceName: string; role: string; } interface WizardState { blueprintId: string | null; shortCode: string; name: string; orderType: string; allocationType: string; startDate: string; endDate: string; budgetEur: string; winProbability: number; responsiblePerson: string; staffingReqs: StaffingRequirement[]; assignments: Assignment[]; saveAsDraft: boolean; dynamicFields: Record; blueprintName: string | null; blueprintFieldDefs: BlueprintFieldDefinition[]; } function makeDefaultState(): WizardState { const today = toDateInputValue(new Date()); return { blueprintId: null, shortCode: "", name: "", orderType: "CHARGEABLE", allocationType: "INT", startDate: today, endDate: today, budgetEur: "", winProbability: 100, responsiblePerson: "", staffingReqs: [], assignments: [], saveAsDraft: true, dynamicFields: {}, blueprintName: null, blueprintFieldDefs: [], }; } function makeReq(): StaffingRequirement { return { id: uuid(), role: "", requiredSkills: [], preferredSkills: [], hoursPerDay: 8, headcount: 1, }; } // ─── Step indicators ───────────────────────────────────────────────────────── const STEPS = [ "Blueprint & Identity", "Timeline & Budget", "Staffing Demand", "Suggestions", "Review & Create", ]; function StepBar({ current }: { current: number }) { return (
{STEPS.map((label, idx) => (
{idx < current ? "✓" : idx + 1}
{label}
{idx < STEPS.length - 1 && (
)}
))}
); } // ─── 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 (