Files
CapaKraken/apps/web/src/components/blueprints/RolePresetsEditor.tsx
T
Hartmut cd78f72f33 chore: full technical rename planarchy → capakraken
Complete rename of all technical identifiers across the codebase:

Package names (11 packages):
- @planarchy/* → @capakraken/* in all package.json, tsconfig, imports

Import statements: 277 files, 548 occurrences replaced

Database & Docker:
- PostgreSQL user/db: planarchy → capakraken
- Docker volumes: planarchy_pgdata → capakraken_pgdata
- Connection strings updated in docker-compose, .env, CI

CI/CD:
- GitHub Actions workflow: all filter commands updated
- Test database credentials updated

Infrastructure:
- Redis channel: planarchy:sse → capakraken:sse
- Logger service name: planarchy-api → capakraken-api
- Anonymization seed updated
- Start/stop/restart scripts updated

Test data:
- Seed emails: @planarchy.dev → @capakraken.dev
- E2E test credentials: all 11 spec files updated
- Email defaults: @planarchy.app → @capakraken.app
- localStorage keys: planarchy_* → capakraken_*

Documentation: 30+ .md files updated

Verification:
- pnpm install: workspace resolution works
- TypeScript: only pre-existing TS2589 (no new errors)
- Engine: 310/310 tests pass
- Staffing: 37/37 tests pass

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-27 13:18:09 +01:00

208 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import type { StaffingRequirement } from "@capakraken/shared";
import { uuid } from "~/lib/uuid.js";
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";
const BTN_DANGER =
"px-2 py-1 text-red-500 hover:text-red-700 hover:bg-red-50 rounded text-sm transition-colors";
function makeEmptyPreset(): StaffingRequirement {
return {
id: uuid(),
role: "",
requiredSkills: [],
preferredSkills: [],
hoursPerDay: 8,
headcount: 1,
};
}
interface PresetRowProps {
preset: StaffingRequirement;
onChange: (preset: StaffingRequirement) => void;
onDelete: () => void;
}
function PresetRow({ preset, onChange, onDelete }: PresetRowProps) {
function update<K extends keyof StaffingRequirement>(key: K, value: StaffingRequirement[K]) {
onChange({ ...preset, [key]: value });
}
return (
<div className="border border-gray-200 rounded-lg p-3 bg-white">
<div className="flex flex-wrap items-center gap-2">
{/* Role */}
<input
type="text"
value={preset.role}
onChange={(e) => update("role", e.target.value)}
placeholder="Role name"
className={`${INPUT_CLS} flex-1 min-w-32`}
aria-label="Role name"
/>
{/* Required Skills */}
<div className="flex flex-col gap-0.5 flex-1 min-w-40">
<label className="text-xs text-gray-400">Required skills (comma-sep)</label>
<input
type="text"
value={preset.requiredSkills.join(", ")}
onChange={(e) =>
update(
"requiredSkills",
e.target.value
.split(",")
.map((s) => s.trim())
.filter(Boolean),
)
}
placeholder="e.g. 3D Modeling, Lighting"
className={INPUT_CLS}
aria-label="Required skills"
/>
</div>
{/* Hours per day */}
<div className="flex flex-col gap-0.5 w-24">
<label className="text-xs text-gray-400">h/day</label>
<input
type="number"
value={preset.hoursPerDay}
min={0}
max={24}
step={0.5}
onChange={(e) => update("hoursPerDay", parseFloat(e.target.value) || 0)}
className={INPUT_CLS}
aria-label="Hours per day"
/>
</div>
{/* Headcount */}
<div className="flex flex-col gap-0.5 w-20">
<label className="text-xs text-gray-400">Count</label>
<input
type="number"
value={preset.headcount}
min={1}
max={20}
onChange={(e) => update("headcount", parseInt(e.target.value, 10) || 1)}
className={INPUT_CLS}
aria-label="Headcount"
/>
</div>
{/* Delete */}
<button
type="button"
onClick={onDelete}
className={`${BTN_DANGER} self-end mb-0.5`}
aria-label="Remove preset"
>
×
</button>
</div>
{/* Preferred skills (secondary row) */}
<div className="mt-2">
<label className="text-xs text-gray-400">Preferred skills (optional)</label>
<input
type="text"
value={(preset.preferredSkills ?? []).join(", ")}
onChange={(e) =>
update(
"preferredSkills",
e.target.value
.split(",")
.map((s) => s.trim())
.filter(Boolean),
)
}
placeholder="e.g. Compositing, Art Direction"
className={`${INPUT_CLS} w-full mt-0.5`}
aria-label="Preferred skills"
/>
</div>
</div>
);
}
interface RolePresetsEditorProps {
initialPresets: StaffingRequirement[];
/** Called with the current presets array when the user clicks Save */
onSave: (presets: StaffingRequirement[]) => void;
isSaving?: boolean;
saveError?: string | null;
}
export function RolePresetsEditor({
initialPresets,
onSave,
isSaving = false,
saveError = null,
}: RolePresetsEditorProps) {
const [presets, setPresets] = useState<StaffingRequirement[]>(initialPresets);
function addPreset() {
setPresets((prev) => [...prev, makeEmptyPreset()]);
}
function removePreset(idx: number) {
setPresets((prev) => prev.filter((_, i) => i !== idx));
}
function updatePreset(idx: number, updated: StaffingRequirement) {
setPresets((prev) => prev.map((p, i) => (i === idx ? updated : p)));
}
return (
<div>
<div className="space-y-3 max-h-[50vh] overflow-y-auto">
{presets.length === 0 && (
<p className="text-sm text-gray-400 text-center py-8">
No role presets yet. Click &ldquo;+ Add Role&rdquo; to define default staffing.
</p>
)}
{presets.map((preset, idx) => (
<PresetRow
key={preset.id}
preset={preset}
onChange={(updated) => updatePreset(idx, updated)}
onDelete={() => removePreset(idx)}
/>
))}
</div>
<div className="mt-3">
<button
type="button"
onClick={addPreset}
className="flex items-center gap-1.5 text-sm text-brand-600 hover:text-brand-800 font-medium"
>
<span className="text-lg leading-none">+</span> Add Role
</button>
</div>
{saveError && (
<div className="mt-2 px-3 py-2 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
{saveError}
</div>
)}
<div className="flex justify-end mt-4">
<button
type="button"
onClick={() => onSave(presets)}
disabled={isSaving}
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50"
>
{isSaving ? "Saving…" : "Save Presets"}
</button>
</div>
</div>
);
}