An interactive demonstration of administrative access control boards. Compare checkbox grids, dual transfer columns, visual chip grids, and dense switches.
User Module Assignment Laboratory
Compare administrative access control screens: permission grid matrices, side-by-side selection lists, chip toggle grids, or dense switches.
12 users x 10 modules
User / Module | Billing 7/12 | Scheduling 10/12 | Reporting 6/12 | Inventory 5/12 | CRM 9/12 | HR 4/12 | Payroll 4/12 | Dispatch 8/12 | Fleet Management 5/12 | Quality Control 4/12 |
|---|---|---|---|---|---|---|---|---|---|---|
Alice Johnson Admin 10/10 | ||||||||||
Bob Martinez Standard 4/10 | ||||||||||
Carol Chen Admin 8/10 | ||||||||||
David Kim Standard 3/10 | ||||||||||
Eva Patel Read Only 2/10 | ||||||||||
Frank Wilson Standard 5/10 | ||||||||||
Grace Lee Admin 10/10 | ||||||||||
Henry Brown Standard 3/10 | ||||||||||
Irene Davis Read Only 1/10 | ||||||||||
Jack Thompson Standard 3/10 | ||||||||||
Karen White Admin 10/10 | ||||||||||
Leo Garcia Standard 3/10 |
Total assignments: 62
Variant1_CheckboxMatrix.tsx (Widget Implementation)
import { useState, useMemo } from "react";
import {
Box,
Checkbox,
TextInput,
Button,
Group,
Text,
Badge,
ScrollArea,
Tooltip,
ActionIcon,
} from "@mantine/core";
import { HiMagnifyingGlass, HiXMark } from "react-icons/hi2";
import type { UserModuleAssignmentProps } from "./types";
export function Variant1_CheckboxMatrix({
users,
modules,
assignments,
onAssignmentChange,
}: UserModuleAssignmentProps) {
const [userSearch, setUserSearch] = useState("");
const [moduleSearch, setModuleSearch] = useState("");
const filteredUsers = useMemo(
() =>
users.filter(
(u) =>
u.name.toLowerCase().includes(userSearch.toLowerCase()) ||
u.systemAccess.toLowerCase().includes(userSearch.toLowerCase()),
),
[users, userSearch],
);
const filteredModules = useMemo(
() => modules.filter((m) => m.name.toLowerCase().includes(moduleSearch.toLowerCase())),
[modules, moduleSearch],
);
const isAssigned = (userId: string, moduleId: string) =>
assignments[userId]?.includes(moduleId) ?? false;
const toggleAssignment = (userId: string, moduleId: string) => {
const current = assignments[userId] ?? [];
const next = current.includes(moduleId)
? current.filter((id) => id !== moduleId)
: [...current, moduleId];
onAssignmentChange({ ...assignments, [userId]: next });
};
const toggleAllModulesForUser = (userId: string) => {
const current = assignments[userId] ?? [];
const allFilteredIds = filteredModules.map((m) => m.id);
const allAssigned = allFilteredIds.every((id) => current.includes(id));
if (allAssigned) {
// Remove only the filtered modules
const next = current.filter((id) => !allFilteredIds.includes(id));
onAssignmentChange({ ...assignments, [userId]: next });
} else {
// Add all filtered modules (keep existing ones from other filters)
const next = [...new Set([...current, ...allFilteredIds])];
onAssignmentChange({ ...assignments, [userId]: next });
}
};
const toggleAllUsersForModule = (moduleId: string) => {
const filteredUserIds = filteredUsers.map((u) => u.id);
const allAssigned = filteredUserIds.every((uid) => (assignments[uid] ?? []).includes(moduleId));
const next = { ...assignments };
for (const uid of filteredUserIds) {
const current = next[uid] ?? [];
if (allAssigned) {
next[uid] = current.filter((id) => id !== moduleId);
} else if (!current.includes(moduleId)) {
next[uid] = [...current, moduleId];
}
}
onAssignmentChange(next);
};
const clearAllUsersForModule = (moduleId: string) => {
const next = { ...assignments };
for (const uid of filteredUsers.map((u) => u.id)) {
next[uid] = (next[uid] ?? []).filter((id) => id !== moduleId);
}
onAssignmentChange(next);
};
const isAllModulesForUser = (userId: string) => {
const current = assignments[userId] ?? [];
return filteredModules.length > 0 && filteredModules.every((m) => current.includes(m.id));
};
const isSomeModulesForUser = (userId: string) => {
const current = assignments[userId] ?? [];
return (
filteredModules.some((m) => current.includes(m.id)) &&
!filteredModules.every((m) => current.includes(m.id))
);
};
const isAllUsersForModule = (moduleId: string) =>
filteredUsers.length > 0 &&
filteredUsers.every((u) => (assignments[u.id] ?? []).includes(moduleId));
const isSomeUsersForModule = (moduleId: string) =>
filteredUsers.some((u) => (assignments[u.id] ?? []).includes(moduleId)) &&
!filteredUsers.every((u) => (assignments[u.id] ?? []).includes(moduleId));
const countForModule = (moduleId: string) =>
filteredUsers.filter((u) => (assignments[u.id] ?? []).includes(moduleId)).length;
const countForUser = (userId: string) => {
const current = assignments[userId] ?? [];
return filteredModules.filter((m) => current.includes(m.id)).length;
};
return (
<Box p="md" style={{ height: "100%", display: "flex", flexDirection: "column" }}>
{/* Search bars */}
<Group mb="md" gap="md">
<TextInput
placeholder="Search users..."
leftSection={<HiMagnifyingGlass size={14} />}
value={userSearch}
onChange={(e) => setUserSearch(e.currentTarget.value)}
style={{ width: 250 }}
rightSection={
userSearch ? (
<ActionIcon size="sm" variant="subtle" onClick={() => setUserSearch("")}>
<HiXMark size={14} />
</ActionIcon>
) : null
}
/>
<TextInput
placeholder="Search modules..."
leftSection={<HiMagnifyingGlass size={14} />}
value={moduleSearch}
onChange={(e) => setModuleSearch(e.currentTarget.value)}
style={{ width: 250 }}
rightSection={
moduleSearch ? (
<ActionIcon size="sm" variant="subtle" onClick={() => setModuleSearch("")}>
<HiXMark size={14} />
</ActionIcon>
) : null
}
/>
<Text size="sm" c="dimmed">
{filteredUsers.length} users x {filteredModules.length} modules
</Text>
</Group>
{/* Matrix */}
<ScrollArea style={{ flex: 1 }} type="auto">
<table
style={{
borderCollapse: "collapse",
width: "max-content",
minWidth: "100%",
}}
>
<thead>
<tr>
{/* Top-left corner: empty cell */}
<th
style={{
position: "sticky",
left: 0,
top: 0,
zIndex: 3,
background: "#f8f9fa",
borderBottom: "2px solid #dee2e6",
borderRight: "2px solid #dee2e6",
padding: "8px 12px",
minWidth: 220,
}}
>
<Text size="xs" c="dimmed" fw={600}>
User / Module
</Text>
</th>
{/* Module column headers */}
{filteredModules.map((mod) => (
<th
key={mod.id}
style={{
position: "sticky",
top: 0,
zIndex: 2,
background: "#f8f9fa",
borderBottom: "2px solid #dee2e6",
padding: "8px 6px",
minWidth: 110,
textAlign: "center",
verticalAlign: "bottom",
}}
>
<Text size="xs" fw={600} mb={4}>
{mod.name}
</Text>
<Group gap={4} justify="center" wrap="nowrap">
<Tooltip label="Toggle all users">
<Checkbox
size="xs"
checked={isAllUsersForModule(mod.id)}
indeterminate={isSomeUsersForModule(mod.id)}
onChange={() => toggleAllUsersForModule(mod.id)}
/>
</Tooltip>
<Tooltip label="Unassign all users">
<ActionIcon
size="xs"
variant="subtle"
color="red"
onClick={() => clearAllUsersForModule(mod.id)}
>
<HiXMark size={12} />
</ActionIcon>
</Tooltip>
</Group>
<Text size="xs" c="dimmed" mt={2}>
{countForModule(mod.id)}/{filteredUsers.length}
</Text>
</th>
))}
</tr>
</thead>
<tbody>
{filteredUsers.map((user, idx) => (
<tr
key={user.id}
style={{
background: idx % 2 === 0 ? "#fff" : "#f8f9fa",
}}
>
{/* User row header */}
<td
style={{
position: "sticky",
left: 0,
zIndex: 1,
background: idx % 2 === 0 ? "#fff" : "#f8f9fa",
borderRight: "2px solid #dee2e6",
borderBottom: "1px solid #eee",
padding: "6px 12px",
}}
>
<Group gap="sm" wrap="nowrap">
<Tooltip label="Toggle all modules">
<Checkbox
size="xs"
checked={isAllModulesForUser(user.id)}
indeterminate={isSomeModulesForUser(user.id)}
onChange={() => toggleAllModulesForUser(user.id)}
/>
</Tooltip>
<div>
<Text size="sm" fw={500} style={{ whiteSpace: "nowrap" }}>
{user.name}
</Text>
<Group gap={4}>
<Badge size="xs" variant="light">
{user.systemAccess}
</Badge>
<Text size="xs" c="dimmed">
{countForUser(user.id)}/{filteredModules.length}
</Text>
</Group>
</div>
</Group>
</td>
{/* Checkboxes */}
{filteredModules.map((mod) => (
<td
key={mod.id}
style={{
textAlign: "center",
borderBottom: "1px solid #eee",
padding: "6px",
}}
>
<Checkbox
size="sm"
checked={isAssigned(user.id, mod.id)}
onChange={() => toggleAssignment(user.id, mod.id)}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</ScrollArea>
{/* Footer summary */}
<Group mt="md" justify="space-between">
<Text size="sm" c="dimmed">
Total assignments: {Object.values(assignments).reduce((sum, arr) => sum + arr.length, 0)}
</Text>
<Group gap="xs">
<Button
size="xs"
variant="light"
onClick={() => {
const next: Record<string, string[]> = {};
for (const u of users) {
next[u.id] = modules.map((m) => m.id);
}
onAssignmentChange(next);
}}
>
Assign All
</Button>
<Button
size="xs"
variant="light"
color="red"
onClick={() => {
const next: Record<string, string[]> = {};
for (const u of users) {
next[u.id] = [];
}
onAssignmentChange(next);
}}
>
Clear All
</Button>
</Group>
</Group>
</Box>
);
}