refactor(web): decompose AllocationsClient and UsersClient into focused subcomponents
AllocationsClient (1364→962 lines): extracted AllocationRow, AllocationGroupedBody, OpenDemandsPanel, and AllocationBatchDialogs. UsersClient (1338→895 lines): extracted UserEditModal and UserCreateModal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import { SystemRole } from "@capakraken/shared";
|
||||
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
||||
|
||||
const SYSTEM_ROLE_LABELS: Record<SystemRole, string> = {
|
||||
[SystemRole.ADMIN]: "Admin",
|
||||
[SystemRole.MANAGER]: "Manager",
|
||||
[SystemRole.CONTROLLER]: "Controller",
|
||||
[SystemRole.USER]: "User",
|
||||
[SystemRole.VIEWER]: "Viewer",
|
||||
};
|
||||
|
||||
export type CreateState = {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
systemRole: SystemRole;
|
||||
};
|
||||
|
||||
type UserCreateModalProps = {
|
||||
state: CreateState;
|
||||
actionError: string | null;
|
||||
isPending: boolean;
|
||||
createPending: boolean;
|
||||
onChange: (state: CreateState) => void;
|
||||
onSubmit: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function UserCreateModal({
|
||||
state,
|
||||
actionError,
|
||||
isPending,
|
||||
createPending,
|
||||
onChange,
|
||||
onSubmit,
|
||||
onClose,
|
||||
}: UserCreateModalProps) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-full max-w-md mx-4 flex flex-col">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Create User</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 text-2xl leading-none"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-5 space-y-4">
|
||||
{actionError && (
|
||||
<div className="rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 px-3 py-2 text-sm text-red-700 dark:text-red-400">
|
||||
{actionError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Name <InfoTooltip content="The display name for this user account." />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={state.name}
|
||||
onChange={(e) => onChange({ ...state, name: e.target.value })}
|
||||
placeholder="Max Mustermann"
|
||||
className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Email{" "}
|
||||
<InfoTooltip content="Login email address. Also used to auto-link the user to a resource record." />
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={state.email}
|
||||
onChange={(e) => onChange({ ...state, email: e.target.value })}
|
||||
placeholder="user@example.com"
|
||||
className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Password{" "}
|
||||
<InfoTooltip content="Minimum 8 characters. Stored securely using Argon2 hashing." />
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={state.password}
|
||||
onChange={(e) => onChange({ ...state, password: e.target.value })}
|
||||
placeholder="Min. 8 characters"
|
||||
className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Role{" "}
|
||||
<InfoTooltip content="ADMIN: full system access. MANAGER: manage resources, projects, allocations. CONTROLLER: read + export financial data. USER: standard access. VIEWER: read-only." />
|
||||
</label>
|
||||
<select
|
||||
value={state.systemRole}
|
||||
onChange={(e) => onChange({ ...state, systemRole: e.target.value as SystemRole })}
|
||||
className="border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2 text-sm w-full bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400"
|
||||
>
|
||||
{Object.values(SystemRole).map((role) => (
|
||||
<option key={role} value={role}>
|
||||
{SYSTEM_ROLE_LABELS[role]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSubmit}
|
||||
disabled={
|
||||
isPending || !state.name.trim() || !state.email.trim() || state.password.length < 8
|
||||
}
|
||||
className="px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{createPending ? "Creating..." : "Create User"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user