feat: Add Staff & Access Management Module with RBAC
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } 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';
|
||||
import { getScopesByCategory } from '@/lib/types/staff';
|
||||
import { createDepartment } from '@/lib/actions/staff-management';
|
||||
import { toast } from 'sonner';
|
||||
import { Loader2, Plus } from 'lucide-react';
|
||||
|
||||
const DEPT_COLORS = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4'];
|
||||
|
||||
interface CreateDepartmentDialogProps {
|
||||
onCreated: () => void;
|
||||
}
|
||||
|
||||
export function CreateDepartmentDialog({ onCreated }: CreateDepartmentDialogProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [selectedScopes, setSelectedScopes] = useState<string[]>([]);
|
||||
const [color, setColor] = useState(DEPT_COLORS[0]);
|
||||
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
||||
const scopesByCategory = getScopesByCategory();
|
||||
|
||||
const toggleScope = (scope: string) => {
|
||||
setSelectedScopes(prev => prev.includes(scope) ? prev.filter(s => s !== scope) : [...prev, scope]);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!name.trim()) { toast.error('Department name is required'); return; }
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await createDepartment({ name, slug, baseScopes: selectedScopes, color, description });
|
||||
if (res.success) {
|
||||
toast.success(`Department "${name}" created`);
|
||||
setOpen(false);
|
||||
setName(''); setDescription(''); setSelectedScopes([]);
|
||||
onCreated();
|
||||
}
|
||||
} catch { toast.error('Failed to create department'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Plus className="h-4 w-4 mr-1" /> New Department
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-lg max-h-[85vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Department</DialogTitle>
|
||||
<DialogDescription>Define a new organizational unit with base permissions.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Department Name</Label>
|
||||
<Input value={name} onChange={e => setName(e.target.value)} placeholder="e.g. Customer Support" />
|
||||
{slug && <p className="text-xs text-muted-foreground">Slug: <code>{slug}</code></p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Description</Label>
|
||||
<Input value={description} onChange={e => setDescription(e.target.value)} placeholder="What does this department handle?" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Color</Label>
|
||||
<div className="flex gap-2">
|
||||
{DEPT_COLORS.map(c => (
|
||||
<button key={c} onClick={() => setColor(c)}
|
||||
className={`w-8 h-8 rounded-full border-2 transition-transform ${color === c ? 'border-foreground scale-110' : 'border-transparent'}`}
|
||||
style={{ backgroundColor: c }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label>Base Permissions</Label>
|
||||
<p className="text-xs text-muted-foreground">All members in this department will inherit these permissions.</p>
|
||||
{Object.entries(scopesByCategory).map(([category, scopes]) => (
|
||||
<div key={category} className="space-y-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">{category}</p>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{scopes.map(({ scope, label }) => (
|
||||
<label key={scope} className="flex items-center gap-2 text-sm cursor-pointer p-1.5 rounded hover:bg-muted/50">
|
||||
<Checkbox checked={selectedScopes.includes(scope)} onCheckedChange={() => toggleScope(scope)} />
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleSubmit} disabled={loading}>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Create Department
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user