116 lines
5.5 KiB
TypeScript
116 lines
5.5 KiB
TypeScript
'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>
|
|
);
|
|
}
|