cd78f72f33
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>
190 lines
6.3 KiB
TypeScript
190 lines
6.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import type { RoleWithResourceCount } from "@capakraken/shared";
|
|
import { trpc } from "~/lib/trpc/client.js";
|
|
import { AnimatedModal } from "~/components/ui/AnimatedModal.js";
|
|
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
|
|
|
const PRESET_COLORS = [
|
|
"#6366f1",
|
|
"#8b5cf6",
|
|
"#ec4899",
|
|
"#ef4444",
|
|
"#f97316",
|
|
"#eab308",
|
|
"#22c55e",
|
|
"#14b8a6",
|
|
"#06b6d4",
|
|
"#3b82f6",
|
|
];
|
|
|
|
interface RoleModalProps {
|
|
role?: RoleWithResourceCount | null;
|
|
onClose: () => void;
|
|
onSuccess: () => void;
|
|
}
|
|
|
|
export function RoleModal({ role, onClose, onSuccess }: RoleModalProps) {
|
|
const isEditing = Boolean(role);
|
|
|
|
const [name, setName] = useState(role?.name ?? "");
|
|
const [description, setDescription] = useState(role?.description ?? "");
|
|
const [color, setColor] = useState(role?.color ?? PRESET_COLORS[0]!);
|
|
const [serverError, setServerError] = useState<string | null>(null);
|
|
|
|
const utils = trpc.useUtils();
|
|
|
|
const createMutation = trpc.role.create.useMutation({
|
|
onSuccess: async () => {
|
|
await utils.role.list.invalidate();
|
|
onSuccess();
|
|
},
|
|
onError: (err) => setServerError(err.message),
|
|
});
|
|
|
|
const updateMutation = trpc.role.update.useMutation({
|
|
onSuccess: async () => {
|
|
await utils.role.list.invalidate();
|
|
onSuccess();
|
|
},
|
|
onError: (err) => setServerError(err.message),
|
|
});
|
|
|
|
const isPending = createMutation.isPending || updateMutation.isPending;
|
|
|
|
function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setServerError(null);
|
|
|
|
if (!name.trim()) {
|
|
setServerError("Name is required.");
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
name: name.trim(),
|
|
description: description.trim() || undefined,
|
|
color: color || undefined,
|
|
};
|
|
|
|
if (isEditing && role) {
|
|
updateMutation.mutate({ id: role.id, data: payload });
|
|
} else {
|
|
createMutation.mutate(payload);
|
|
}
|
|
}
|
|
|
|
const inputClass = "app-input";
|
|
const labelClass = "app-label";
|
|
|
|
return (
|
|
<AnimatedModal open onClose={onClose} maxWidth="max-w-md">
|
|
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4 dark:border-gray-700">
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
{isEditing ? "Edit Role" : "New Role"}
|
|
</h2>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="text-xl leading-none text-gray-400 transition hover:text-gray-600 dark:hover:text-gray-200"
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="px-6 py-5 space-y-4">
|
|
<div>
|
|
<label className={labelClass}>
|
|
Name <span className="text-red-500">*</span><InfoTooltip content="Role name shown in timelines, allocation pickers, and staffing demands (e.g. 3D Artist, Producer)." />
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => {
|
|
setName(e.target.value);
|
|
setServerError(null);
|
|
}}
|
|
placeholder="e.g. 3D Artist"
|
|
className={inputClass}
|
|
maxLength={100}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className={labelClass}>Description<InfoTooltip content="Optional description of this role's responsibilities." /></label>
|
|
<input
|
|
type="text"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="Optional description"
|
|
className={inputClass}
|
|
maxLength={500}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className={labelClass}>Color<InfoTooltip content="Color used in timeline bars and badge chips for this role." /></label>
|
|
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/70">
|
|
<div className="mb-3 flex items-center gap-3">
|
|
<div
|
|
className="h-8 w-8 flex-shrink-0 rounded-full border-2 border-gray-200 dark:border-gray-600"
|
|
style={{ backgroundColor: color }}
|
|
/>
|
|
<p className="text-sm text-gray-600 dark:text-gray-300">
|
|
Pick a color that stays readable in timelines and chips.
|
|
</p>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2 flex-1">
|
|
{PRESET_COLORS.map((c) => (
|
|
<button
|
|
key={c}
|
|
type="button"
|
|
onClick={() => setColor(c)}
|
|
className="w-6 h-6 rounded-full border-2 transition-transform hover:scale-110"
|
|
style={{
|
|
backgroundColor: c,
|
|
borderColor: color === c ? "#1f2937" : "transparent",
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
<input
|
|
type="color"
|
|
value={color}
|
|
onChange={(e) => setColor(e.target.value)}
|
|
className="mt-3 h-8 w-10 cursor-pointer rounded border border-gray-300 bg-transparent dark:border-gray-600"
|
|
title="Custom color"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{serverError && (
|
|
<div className="rounded-2xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/30 dark:text-red-300">
|
|
{serverError}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-end gap-3 pt-2">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={isPending}
|
|
className="rounded-xl px-4 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-100 hover:text-gray-900 disabled:opacity-50 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isPending}
|
|
className="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700 disabled:opacity-50"
|
|
>
|
|
{isPending ? "Saving…" : "Save"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</AnimatedModal>
|
|
);
|
|
}
|