Files
lcbp3/specs/06-tasks/TASK-FE-010-admin-panel.md

681 lines
19 KiB
Markdown

# TASK-FE-010: Admin Panel & Settings UI
**ID:** TASK-FE-010
**Title:** Admin Panel for User & Master Data Management
**Category:** Administration
**Priority:** P2 (Medium)
**Effort:** 5-7 days
**Dependencies:** TASK-FE-002, TASK-FE-005, TASK-BE-012, TASK-BE-013
**Assigned To:** Frontend Developer
---
## 📋 Overview
Build comprehensive Admin Panel for managing users, roles, master data (organizations, projects, contracts, disciplines, document types), system settings, and viewing audit logs.
---
## 🎯 Objectives
1. Create admin layout with separate navigation
2. Build User Management UI (CRUD users, assign roles)
3. Implement Master Data Management screens
4. Create System Settings interface
5. Build Audit Logs viewer
6. Add bulk operations and data import/export
---
## ✅ Acceptance Criteria
- [ ] Admin area accessible only to admins
- [ ] User management (create/edit/delete/deactivate)
- [ ] Role assignment with permission preview
- [ ] Master data CRUD (Organizations, Projects, etc.)
- [ ] Audit logs searchable and filterable
- [ ] System settings editable
- [ ] CSV import/export for bulk operations
---
## 🔧 Implementation Steps
### Step 1: Admin Layout
```typescript
// File: src/app/(admin)/layout.tsx
import { AdminSidebar } from '@/components/admin/sidebar';
import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth';
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getServerSession();
// Check if user has admin role
if (!session?.user?.roles?.some((r) => r.role_name === 'ADMIN')) {
redirect('/');
}
return (
<div className="flex h-screen">
<AdminSidebar />
<div className="flex-1 overflow-auto">{children}</div>
</div>
);
}
```
### Step 2: User Management Page
```typescript
// File: src/app/(admin)/admin/users/page.tsx
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { DataTable } from '@/components/common/data-table';
import { UserDialog } from '@/components/admin/user-dialog';
import { ColumnDef } from '@tanstack/react-table';
import { Badge } from '@/components/ui/badge';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { MoreHorizontal, Plus } from 'lucide-react';
export default function UsersPage() {
const [users, setUsers] = useState([]);
const [dialogOpen, setDialogOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState(null);
const columns: ColumnDef<any>[] = [
{
accessorKey: 'username',
header: 'Username',
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'first_name',
header: 'Name',
cell: ({ row }) => `${row.original.first_name} ${row.original.last_name}`,
},
{
accessorKey: 'is_active',
header: 'Status',
cell: ({ row }) => (
<Badge variant={row.original.is_active ? 'success' : 'secondary'}>
{row.original.is_active ? 'Active' : 'Inactive'}
</Badge>
),
},
{
id: 'roles',
header: 'Roles',
cell: ({ row }) => (
<div className="flex gap-1">
{row.original.roles?.map((role: any) => (
<Badge key={role.user_role_id} variant="outline">
{role.role_name}
</Badge>
))}
</div>
),
},
{
id: 'actions',
cell: ({ row }) => (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setSelectedUser(row.original);
setDialogOpen(true);
}}
>
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDeactivate(row.original.user_id)}
>
{row.original.is_active ? 'Deactivate' : 'Activate'}
</DropdownMenuItem>
<DropdownMenuItem className="text-red-600">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
),
},
];
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">User Management</h1>
<p className="text-gray-600 mt-1">
Manage system users and their roles
</p>
</div>
<Button
onClick={() => {
setSelectedUser(null);
setDialogOpen(true);
}}
>
<Plus className="mr-2 h-4 w-4" />
Add User
</Button>
</div>
<DataTable columns={columns} data={users} />
<UserDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
user={selectedUser}
/>
</div>
);
}
```
### Step 3: User Create/Edit Dialog
```typescript
// File: src/components/admin/user-dialog.tsx
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
const userSchema = z.object({
username: z.string().min(3),
email: z.string().email(),
first_name: z.string().min(1),
last_name: z.string().min(1),
password: z.string().min(6).optional(),
is_active: z.boolean().default(true),
roles: z.array(z.number()),
});
type UserFormData = z.infer<typeof userSchema>;
interface UserDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
user?: any;
}
export function UserDialog({ open, onOpenChange, user }: UserDialogProps) {
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<UserFormData>({
resolver: zodResolver(userSchema),
defaultValues: user || {},
});
const availableRoles = [
{ role_id: 1, role_name: 'ADMIN', description: 'System Administrator' },
{ role_id: 2, role_name: 'USER', description: 'Regular User' },
{ role_id: 3, role_name: 'APPROVER', description: 'Document Approver' },
];
const selectedRoles = watch('roles') || [];
const onSubmit = async (data: UserFormData) => {
// Call API to create/update user
console.log(data);
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{user ? 'Edit User' : 'Create New User'}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Username *</Label>
<Input {...register('username')} />
{errors.username && (
<p className="text-sm text-red-600 mt-1">
{errors.username.message}
</p>
)}
</div>
<div>
<Label>Email *</Label>
<Input type="email" {...register('email')} />
{errors.email && (
<p className="text-sm text-red-600 mt-1">
{errors.email.message}
</p>
)}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>First Name *</Label>
<Input {...register('first_name')} />
</div>
<div>
<Label>Last Name *</Label>
<Input {...register('last_name')} />
</div>
</div>
{!user && (
<div>
<Label>Password *</Label>
<Input type="password" {...register('password')} />
{errors.password && (
<p className="text-sm text-red-600 mt-1">
{errors.password.message}
</p>
)}
</div>
)}
<div>
<Label className="mb-3 block">Roles</Label>
<div className="space-y-2">
{availableRoles.map((role) => (
<label
key={role.role_id}
className="flex items-start gap-3 p-3 border rounded hover:bg-gray-50"
>
<Checkbox
checked={selectedRoles.includes(role.role_id)}
onCheckedChange={(checked) => {
const newRoles = checked
? [...selectedRoles, role.role_id]
: selectedRoles.filter((id) => id !== role.role_id);
setValue('roles', newRoles);
}}
/>
<div>
<div className="font-medium">{role.role_name}</div>
<div className="text-sm text-gray-600">
{role.description}
</div>
</div>
</label>
))}
</div>
</div>
<div className="flex items-center gap-2">
<Checkbox {...register('is_active')} defaultChecked />
<Label>Active</Label>
</div>
<div className="flex justify-end gap-3 pt-4">
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
<Button type="submit">
{user ? 'Update User' : 'Create User'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}
```
### Step 4: Master Data Management (Organizations)
```typescript
// File: src/app/(admin)/admin/organizations/page.tsx
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { DataTable } from '@/components/common/data-table';
import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
export default function OrganizationsPage() {
const [organizations, setOrganizations] = useState([]);
const [dialogOpen, setDialogOpen] = useState(false);
const [formData, setFormData] = useState({
org_code: '',
org_name: '',
org_name_th: '',
description: '',
});
const columns = [
{ accessorKey: 'org_code', header: 'Code' },
{ accessorKey: 'org_name', header: 'Name (EN)' },
{ accessorKey: 'org_name_th', header: 'Name (TH)' },
{ accessorKey: 'description', header: 'Description' },
];
const handleSubmit = async () => {
// Call API to create organization
console.log(formData);
setDialogOpen(false);
};
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Organizations</h1>
<p className="text-gray-600 mt-1">Manage project organizations</p>
</div>
<Button onClick={() => setDialogOpen(true)}>Add Organization</Button>
</div>
<DataTable columns={columns} data={organizations} />
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Organization</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Organization Code *</Label>
<Input
value={formData.org_code}
onChange={(e) =>
setFormData({ ...formData, org_code: e.target.value })
}
placeholder="e.g., กทท."
/>
</div>
<div>
<Label>Name (English) *</Label>
<Input
value={formData.org_name}
onChange={(e) =>
setFormData({ ...formData, org_name: e.target.value })
}
/>
</div>
<div>
<Label>Name (Thai)</Label>
<Input
value={formData.org_name_th}
onChange={(e) =>
setFormData({ ...formData, org_name_th: e.target.value })
}
/>
</div>
<div>
<Label>Description</Label>
<Input
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
/>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setDialogOpen(false)}>
Cancel
</Button>
<Button onClick={handleSubmit}>Create</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
}
```
### Step 5: Audit Logs Viewer
```typescript
// File: src/app/(admin)/admin/audit-logs/page.tsx
'use client';
import { useState } from 'react';
import { Card } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { formatDistanceToNow } from 'date-fns';
export default function AuditLogsPage() {
const [logs, setLogs] = useState([]);
const [filters, setFilters] = useState({
user: '',
action: '',
entity: '',
});
return (
<div className="p-6 space-y-6">
<div>
<h1 className="text-3xl font-bold">Audit Logs</h1>
<p className="text-gray-600 mt-1">View system activity and changes</p>
</div>
{/* Filters */}
<Card className="p-4">
<div className="grid grid-cols-4 gap-4">
<div>
<Input placeholder="Search user..." />
</div>
<div>
<Select>
<SelectTrigger>
<SelectValue placeholder="Action" />
</SelectTrigger>
<SelectContent>
<SelectItem value="CREATE">Create</SelectItem>
<SelectItem value="UPDATE">Update</SelectItem>
<SelectItem value="DELETE">Delete</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Select>
<SelectTrigger>
<SelectValue placeholder="Entity Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="correspondence">Correspondence</SelectItem>
<SelectItem value="rfa">RFA</SelectItem>
<SelectItem value="user">User</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</Card>
{/* Logs List */}
<div className="space-y-2">
{logs.map((log: any) => (
<Card key={log.audit_log_id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<span className="font-medium">{log.user_name}</span>
<Badge>{log.action}</Badge>
<Badge variant="outline">{log.entity_type}</Badge>
</div>
<p className="text-sm text-gray-600">{log.description}</p>
<p className="text-xs text-gray-500 mt-2">
{formatDistanceToNow(new Date(log.created_at), {
addSuffix: true,
})}
</p>
</div>
{log.ip_address && (
<span className="text-xs text-gray-500">
IP: {log.ip_address}
</span>
)}
</div>
</Card>
))}
</div>
</div>
);
}
```
### Step 6: Admin Sidebar Navigation
```typescript
// File: src/components/admin/sidebar.tsx
'use client';
import Link from 'link';
import { usePathname } from 'next/navigation';
import { cn } from '@/lib/utils';
import { Users, Building2, Settings, FileText, Activity } from 'lucide-react';
const menuItems = [
{ href: '/admin/users', label: 'Users', icon: Users },
{ href: '/admin/organizations', label: 'Organizations', icon: Building2 },
{ href: '/admin/projects', label: 'Projects', icon: FileText },
{ href: '/admin/settings', label: 'Settings', icon: Settings },
{ href: '/admin/audit-logs', label: 'Audit Logs', icon: Activity },
];
export function AdminSidebar() {
const pathname = usePathname();
return (
<aside className="w-64 border-r bg-gray-50 p-4">
<h2 className="text-lg font-bold mb-6">Admin Panel</h2>
<nav className="space-y-1">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-lg transition-colors',
isActive
? 'bg-primary text-primary-foreground'
: 'hover:bg-gray-100'
)}
>
<Icon className="h-5 w-5" />
<span>{item.label}</span>
</Link>
);
})}
</nav>
</aside>
);
}
```
---
## 📦 Deliverables
- [ ] Admin layout with sidebar navigation
- [ ] User Management (CRUD, roles assignment)
- [ ] Master Data Management screens:
- [ ] Organizations
- [ ] Projects
- [ ] Contracts
- [ ] Disciplines
- [ ] Document Types
- [ ] System Settings interface
- [ ] Audit Logs viewer with filters
- [ ] CSV import/export functionality
---
## 🧪 Testing
### Test Cases
1. **User Management**
- Create new user
- Assign multiple roles
- Deactivate/activate user
- Delete user
2. **Master Data**
- Create organization
- Edit organization details
- Delete organization (check for dependencies)
3. **Audit Logs**
- View all logs
- Filter by user/action/entity
- Search logs
- Export logs
---
## 🔗 Related Documents
- [TASK-BE-012: Master Data Management](./TASK-BE-012-master-data-management.md)
- [TASK-BE-013: User Management](./TASK-BE-013-user-management.md)
- [ADR-004: RBAC Implementation](../../05-decisions/ADR-004-rbac-implementation.md)
---
**Created:** 2025-12-01
**Status:** Ready