refactor(admin): refactor admin roles and users management

This commit is contained in:
2026-06-16 11:16:28 +05:30
parent bea47d29d5
commit 893b49c701
25 changed files with 1615 additions and 825 deletions

View File

@@ -0,0 +1,82 @@
'use client';
import { useMemo } from 'react';
import type { ColumnDef } from '@tanstack/react-table';
import { Edit, RotateCcw } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import type { Role } from '@/types';
interface UseRoleColumnsParams {
onEdit: (role: Role) => void;
onToggleStatus: (role: Role) => void;
isStatusPending: boolean;
}
export function useRoleColumns({
onEdit,
onToggleStatus,
isStatusPending,
}: UseRoleColumnsParams): ColumnDef<Role>[] {
return useMemo(
() => [
{
accessorKey: 'name',
header: 'Name',
cell: ({ row }) => <span className="font-medium">{row.original.name}</span>,
},
{
accessorKey: 'display_name',
header: 'Display Name',
},
{
accessorKey: 'description',
header: 'Description',
cell: ({ row }) => (
<span className="block max-w-sm truncate text-muted-foreground">
{row.original.description || '-'}
</span>
),
},
{
accessorKey: 'user_count',
header: 'Users',
cell: ({ row }) => row.original.user_count ?? 0,
},
{
accessorKey: 'effective_status',
header: 'Status',
cell: ({ row }) => (
<Badge variant={row.original.effective_status ? 'default' : 'secondary'}>
{row.original.effective_status ? 'Active' : 'Inactive'}
</Badge>
),
},
{
id: 'actions',
header: () => <div className="text-right">Actions</div>,
cell: ({ row }) => {
const role = row.original;
return (
<div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={() => onEdit(role)}>
<Edit className="size-4" />
</Button>
<Button
variant="outline"
size="sm"
disabled={isStatusPending}
onClick={() => onToggleStatus(role)}
>
<RotateCcw className="mr-2 size-4" />
{role.effective_status ? 'Deactivate' : 'Activate'}
</Button>
</div>
);
},
},
],
[isStatusPending, onEdit, onToggleStatus],
);
}

View File

@@ -0,0 +1,47 @@
'use client';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { StatusFilter } from '../hooks/useRoleFilters';
interface RoleFiltersProps {
searchTerm: string;
statusFilter: StatusFilter;
onSearchChange: (value: string) => void;
onStatusChange: (value: StatusFilter) => void;
}
export function RoleFilters({
searchTerm,
statusFilter,
onSearchChange,
onStatusChange,
}: RoleFiltersProps) {
return (
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<Input
value={searchTerm}
onChange={(event) => onSearchChange(event.target.value)}
placeholder="Search roles"
className="md:max-w-sm"
/>
<Select value={statusFilter} onValueChange={(value) => onStatusChange(value as StatusFilter)}>
<SelectTrigger className="md:w-44">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All status</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
</div>
);
}

View File

@@ -0,0 +1,125 @@
'use client';
import type { ComponentProps } from 'react';
import type { UseFormRegister } from 'react-hook-form';
import { Loader2 } from 'lucide-react';
import { PermissionTree } from '@/components/permission-tree';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import type { PermissionTreeItem } from '@/types';
import type { RoleFormValues } from '../hooks/useRoleForm';
interface RoleSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
roleId?: number;
register: UseFormRegister<RoleFormValues>;
onSubmit: ComponentProps<'form'>['onSubmit'];
permissionTree: PermissionTreeItem[];
permissionIds: number[];
onPermissionIdsChange: (ids: number[]) => void;
isPermissionsLoading: boolean;
isSaving: boolean;
}
export function RoleSheet({
open,
onOpenChange,
roleId,
register,
onSubmit,
permissionTree,
permissionIds,
onPermissionIdsChange,
isPermissionsLoading,
isSaving,
}: RoleSheetProps) {
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="w-full overflow-y-auto sm:max-w-2xl">
<SheetHeader>
<SheetTitle>{roleId ? 'Edit Role' : 'Create Role'}</SheetTitle>
<SheetDescription>
Assign the role details and permission access for this organization.
</SheetDescription>
</SheetHeader>
<form onSubmit={onSubmit} className="flex flex-1 flex-col gap-5 px-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="role-name">Role Name</Label>
<Input
id="role-name"
placeholder="Enter role name"
{...register('name', { required: true })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="role-display-name">Display Name</Label>
<Input
id="role-display-name"
placeholder="Enter display name"
{...register('display_name', { required: true })}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="role-description">Description</Label>
<textarea
id="role-description"
placeholder="Describe this role"
{...register('description')}
className="min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between gap-3">
<Label>Permissions</Label>
<span className="text-xs text-muted-foreground">{permissionIds.length} selected</span>
</div>
{isPermissionsLoading ? (
<div className="rounded-md border p-6 text-sm text-muted-foreground">
Loading permissions...
</div>
) : (
<PermissionTree
items={permissionTree}
selectedIds={permissionIds}
onChange={onPermissionIdsChange}
/>
)}
</div>
<SheetFooter className="px-0">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSaving}
>
Cancel
</Button>
<Button type="submit" disabled={isSaving}>
{isSaving ? <Loader2 className="mr-2 size-4 animate-spin" /> : null}
{roleId ? 'Update Role' : 'Create Role'}
</Button>
</SheetFooter>
</form>
</SheetContent>
</Sheet>
);
}

View File

@@ -0,0 +1,47 @@
'use client';
import type { ColumnDef } from '@tanstack/react-table';
import type { ReactNode } from 'react';
import { DataTable } from '@/components/data-table';
import type { Role } from '@/types';
interface RoleTableProps {
columns: ColumnDef<Role>[];
roles: Role[];
isLoading: boolean;
toolbar: ReactNode;
skip: number;
limit: number;
total: number;
onPageChange: (skip: number) => void;
}
export function RoleTable({
columns,
roles,
isLoading,
toolbar,
skip,
limit,
total,
onPageChange,
}: RoleTableProps) {
return (
<DataTable
title="Roles"
columns={columns}
data={roles}
isLoading={isLoading}
toolbar={toolbar}
emptyTitle="No roles found."
pagination={{
skip,
limit,
totalItems: total,
onPageChange,
onLimitChange: () => undefined,
}}
/>
);
}