187 lines
7.7 KiB
TypeScript
187 lines
7.7 KiB
TypeScript
import { Head, router } from '@inertiajs/react';
|
|
import { Save } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { PageHeader } from '@/components/page-header';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import { update } from '@/routes/admin/developer/roles';
|
|
import type {
|
|
PermissionGroups,
|
|
RoleWithPermissions,
|
|
} from '@/types/role-permission';
|
|
import { formatRoleLabel } from '@/types/role-permission';
|
|
|
|
type Props = {
|
|
roles: RoleWithPermissions[];
|
|
permissionGroups: PermissionGroups;
|
|
};
|
|
|
|
export default function RolePermissionIndex({
|
|
roles,
|
|
permissionGroups,
|
|
}: Props) {
|
|
const [selectedRoleId, setSelectedRoleId] = useState<number | undefined>(
|
|
roles[0]?.id,
|
|
);
|
|
const selectedRole = roles.find((role) => role.id === selectedRoleId);
|
|
const [selected, setSelected] = useState<string[]>(
|
|
selectedRole?.permissions ?? [],
|
|
);
|
|
const [processing, setProcessing] = useState(false);
|
|
|
|
function handleSelectRole(id: string) {
|
|
const role = roles.find((r) => r.id === Number(id));
|
|
setSelectedRoleId(role?.id);
|
|
setSelected(role?.permissions ?? []);
|
|
}
|
|
|
|
function toggle(name: string, checked: boolean) {
|
|
setSelected((prev) =>
|
|
checked ? [...prev, name] : prev.filter((p) => p !== name),
|
|
);
|
|
}
|
|
|
|
function toggleGroup(names: string[], checked: boolean) {
|
|
setSelected((prev) => {
|
|
const withoutGroup = prev.filter((p) => !names.includes(p));
|
|
|
|
return checked ? [...withoutGroup, ...names] : withoutGroup;
|
|
});
|
|
}
|
|
|
|
function handleSave() {
|
|
if (!selectedRole) {
|
|
return;
|
|
}
|
|
|
|
setProcessing(true);
|
|
router.put(
|
|
update.url(selectedRole.id),
|
|
{ permissions: selected },
|
|
{
|
|
preserveScroll: true,
|
|
onFinish: () => setProcessing(false),
|
|
},
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Head title="Role & Permission" />
|
|
|
|
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
|
<PageHeader
|
|
title="Role & Permission"
|
|
actions={
|
|
selectedRole && (
|
|
<Button onClick={handleSave} disabled={processing}>
|
|
<Save className="h-4 w-4" />
|
|
Simpan
|
|
</Button>
|
|
)
|
|
}
|
|
/>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<Select
|
|
value={
|
|
selectedRoleId ? String(selectedRoleId) : undefined
|
|
}
|
|
onValueChange={handleSelectRole}
|
|
>
|
|
<SelectTrigger className="w-full sm:w-72">
|
|
<SelectValue placeholder="Pilih role" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{roles.map((role) => (
|
|
<SelectItem
|
|
key={role.id}
|
|
value={String(role.id)}
|
|
>
|
|
{formatRoleLabel(role.name)}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{selectedRole && (
|
|
<Badge variant="secondary">
|
|
{selected.length} permission
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{selectedRole && (
|
|
<div className="grid gap-4">
|
|
{Object.entries(permissionGroups).map(
|
|
([group, names]) => {
|
|
const allChecked = names.every((name) =>
|
|
selected.includes(name),
|
|
);
|
|
|
|
return (
|
|
<Card key={group}>
|
|
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
|
<CardTitle className="text-base">
|
|
{group}
|
|
</CardTitle>
|
|
<label className="flex items-center gap-2 text-sm font-normal text-muted-foreground">
|
|
<Checkbox
|
|
checked={allChecked}
|
|
onCheckedChange={(
|
|
checked,
|
|
) =>
|
|
toggleGroup(
|
|
names,
|
|
checked === true,
|
|
)
|
|
}
|
|
/>
|
|
Pilih Semua
|
|
</label>
|
|
</CardHeader>
|
|
<CardContent className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
{names.map((name) => (
|
|
<label
|
|
key={name}
|
|
className="flex items-center gap-2 rounded-md border p-2 text-sm hover:bg-muted"
|
|
>
|
|
<Checkbox
|
|
checked={selected.includes(
|
|
name,
|
|
)}
|
|
onCheckedChange={(
|
|
checked,
|
|
) =>
|
|
toggle(
|
|
name,
|
|
checked ===
|
|
true,
|
|
)
|
|
}
|
|
/>
|
|
<span className="font-mono text-xs">
|
|
{name}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
},
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|