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:
2026-04-11 08:49:50 +02:00
parent e3551fb78f
commit 17f2de5f48
8 changed files with 1257 additions and 953 deletions
@@ -0,0 +1,143 @@
import type { AllocationWithDetails, ColumnDef } from "@capakraken/shared";
import { ALLOCATION_STATUS_BADGE as STATUS_BADGE } from "~/lib/status-styles.js";
const STATUS_LEFT_BORDER: Record<string, string> = {
ACTIVE: "border-l-green-500",
PROPOSED: "border-l-amber-500",
CONFIRMED: "border-l-blue-500",
COMPLETED: "border-l-gray-400",
CANCELLED: "border-l-red-500",
};
type AllocationRowProps = {
alloc: AllocationWithDetails;
visibleColumns: ColumnDef[];
isGrouped?: boolean;
rowIndex?: number;
isSelected: boolean;
onToggleSelection: () => void;
onEdit: () => void;
onRequestDelete: () => void;
deleteDisabled: boolean;
formatPeriod: (alloc: AllocationWithDetails) => string;
};
export function AllocationRow({
alloc,
visibleColumns,
isGrouped = false,
rowIndex = 0,
isSelected,
onToggleSelection,
onEdit,
onRequestDelete,
deleteDisabled,
formatPeriod,
}: AllocationRowProps) {
const leftBorder = STATUS_LEFT_BORDER[alloc.status] ?? "border-l-gray-300";
return (
<tr
key={alloc.id}
data-testid="allocation-row"
className={`border-l-[3px] transition-colors hover:bg-gray-50/80 dark:hover:bg-gray-900/60 animate-row-enter ${leftBorder} ${isSelected ? "bg-brand-50 dark:bg-brand-900/20" : ""}`}
style={{ animationDelay: `${Math.min(rowIndex * 15, 300)}ms` }}
>
<td className="px-4 py-3">
<input
type="checkbox"
checked={isSelected}
onChange={onToggleSelection}
className="rounded border-gray-300 dark:border-gray-600"
/>
</td>
{visibleColumns.map((col) => {
switch (col.key) {
case "resource":
return (
<td
key={col.key}
className="px-4 py-3 text-sm font-medium text-gray-900 dark:text-gray-100"
>
{isGrouped ? (
<span className="text-gray-400 dark:text-gray-500"></span>
) : (
(alloc.resource?.displayName ?? "—")
)}
</td>
);
case "project":
return (
<td key={col.key} className="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">
{alloc.project ? (
<>
<span className="font-mono text-xs">{alloc.project.shortCode}</span>{" "}
{alloc.project.name}
</>
) : (
"—"
)}
</td>
);
case "role":
return (
<td key={col.key} className="px-4 py-3 text-sm text-gray-600 dark:text-gray-300">
{alloc.role}
</td>
);
case "dates":
return (
<td
key={col.key}
className="whitespace-nowrap px-4 py-3 text-xs text-gray-500 dark:text-gray-400"
>
{formatPeriod(alloc)}
</td>
);
case "hoursPerDay":
return (
<td key={col.key} className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{alloc.hoursPerDay}h
</td>
);
case "cost":
return (
<td key={col.key} className="px-4 py-3 text-sm text-gray-900 dark:text-gray-100">
{(alloc.dailyCostCents / 100).toFixed(0)}
</td>
);
case "status":
return (
<td key={col.key} className="px-4 py-3">
<span
className={`inline-block px-2 py-0.5 text-xs rounded-full font-medium ${STATUS_BADGE[alloc.status] ?? "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400"}`}
>
{alloc.status}
</span>
</td>
);
default:
return (
<td key={col.key} className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400">
</td>
);
}
})}
<td className="px-4 py-3">
<div className="flex items-center justify-end gap-2">
<button type="button" onClick={onEdit} className="app-action-edit">
Edit
</button>
<button
type="button"
onClick={onRequestDelete}
disabled={deleteDisabled}
className="app-action-delete disabled:opacity-50"
>
Delete
</button>
</div>
</td>
</tr>
);
}