"use client";
import { useState } from "react";
import { FieldType } from "@capakraken/shared";
import type { BlueprintFieldDefinition, FieldOption, StaffingRequirement } from "@capakraken/shared";
import { trpc } from "~/lib/trpc/client.js";
import { RolePresetsEditor } from "./RolePresetsEditor.js";
const FIELD_TYPES: { value: FieldType; label: string }[] = [
{ value: FieldType.TEXT, label: "Text" },
{ value: FieldType.TEXTAREA, label: "Textarea" },
{ value: FieldType.NUMBER, label: "Number" },
{ value: FieldType.BOOLEAN, label: "Boolean" },
{ value: FieldType.DATE, label: "Date" },
{ value: FieldType.SELECT, label: "Select" },
{ value: FieldType.MULTI_SELECT, label: "Multi-Select" },
{ value: FieldType.URL, label: "URL" },
{ value: FieldType.EMAIL, label: "Email" },
];
const BTN_PRIMARY =
"px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50";
const BTN_SECONDARY =
"px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm font-medium";
function makeEmptyField(order: number): BlueprintFieldDefinition {
return {
id: Math.random().toString(36).slice(2),
key: "",
label: "",
type: FieldType.TEXT,
required: false,
order,
};
}
// ---------------------------------------------------------------------------
// OptionsEditor — for SELECT / MULTI_SELECT
// ---------------------------------------------------------------------------
interface OptionsEditorProps {
options: FieldOption[];
onChange: (options: FieldOption[]) => void;
}
function OptionsEditor({ options, onChange }: OptionsEditorProps) {
function addOption() {
onChange([...options, { value: "", label: "" }]);
}
function removeOption(idx: number) {
onChange(options.filter((_, i) => i !== idx));
}
function updateOption(idx: number, field: "value" | "label", val: string) {
const next = options.map((o, i) =>
i === idx ? { ...o, [field]: val } : o,
);
onChange(next);
}
return (
);
}
// ---------------------------------------------------------------------------
// FieldRow — a single field definition row
// ---------------------------------------------------------------------------
interface FieldRowProps {
field: BlueprintFieldDefinition;
onChange: (field: BlueprintFieldDefinition) => void;
onDelete: () => void;
}
function FieldRow({ field, onChange, onDelete }: FieldRowProps) {
const [expanded, setExpanded] = useState(false);
const needsOptions =
field.type === FieldType.SELECT || field.type === FieldType.MULTI_SELECT;
function update(
key: K,
value: BlueprintFieldDefinition[K],
) {
onChange({ ...field, [key]: value });
}
return (
);
}
// ---------------------------------------------------------------------------
// BlueprintFieldEditor — the modal
// ---------------------------------------------------------------------------
interface BlueprintFieldEditorProps {
blueprintId: string;
blueprintName: string;
initialFieldDefs: BlueprintFieldDefinition[];
initialRolePresets?: StaffingRequirement[];
initialTab?: "fields" | "presets";
onClose: () => void;
}
export function BlueprintFieldEditor({
blueprintId,
blueprintName,
initialFieldDefs,
initialRolePresets = [],
initialTab = "fields",
onClose,
}: BlueprintFieldEditorProps) {
const utils = trpc.useUtils();
const [activeTab, setActiveTab] = useState<"fields" | "presets">(initialTab);
const [fields, setFields] = useState(
() =>
[...initialFieldDefs].sort((a, b) => a.order - b.order),
);
const [saveError, setSaveError] = useState(null);
const [presetSaveError, setPresetSaveError] = useState(null);
const updateMutation = trpc.blueprint.update.useMutation();
const presetMutation = trpc.blueprint.updateRolePresets.useMutation();
function addField() {
setFields((prev) => [...prev, makeEmptyField(prev.length)]);
}
function removeField(idx: number) {
setFields((prev) =>
prev
.filter((_, i) => i !== idx)
.map((f, i) => ({ ...f, order: i })),
);
}
function updateField(idx: number, updated: BlueprintFieldDefinition) {
setFields((prev) =>
prev.map((f, i) => (i === idx ? updated : f)),
);
}
function handleSave() {
setSaveError(null);
// Reassign order by current list position
const normalised = fields.map((f, i) => ({ ...f, order: i }));
updateMutation.mutate(
{
id: blueprintId,
data: { fieldDefs: normalised },
},
{
onSuccess: async () => {
await utils.blueprint.list.invalidate();
onClose();
},
onError: (err) => {
setSaveError(err.message);
},
},
);
}
// Close on backdrop click
function handleBackdropClick(e: React.MouseEvent) {
if (e.target === e.currentTarget) onClose();
}
return (
{/* Header */}
Edit Fields:{" "}
{blueprintName}
{/* Tabs */}
{(["fields", "presets"] as const).map((tab) => (
))}
{activeTab === "fields" ? (
<>
{/* Field list */}
{fields.length === 0 && (
No fields yet. Click “+ Add Field” to get started.
)}
{fields.map((field, idx) => (
updateField(idx, updated)}
onDelete={() => removeField(idx)}
/>
))}
{/* Add field button */}
{/* Error */}
{saveError && (
{saveError}
)}
{/* Footer */}
>
) : (
Role presets are auto-loaded in Step 3 of the Project Creation Wizard when this blueprint is selected.
presetMutation.mutate(
{ id: blueprintId, rolePresets: presets },
{
onSuccess: async () => {
await utils.blueprint.list.invalidate();
setPresetSaveError(null);
onClose();
},
onError: (err) => {
setPresetSaveError(err.message);
},
},
)
}
isSaving={presetMutation.isPending}
saveError={presetSaveError}
/>
)}
);
}