Files
OpticZ/src/components/employees/EmployeeManagement.tsx

469 lines
16 KiB
TypeScript

'use client'
import { useEffect, useMemo, useState } from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Plus, Search, Shield, Trash2, UserCog, Users } from 'lucide-react'
type RoleEmploye = 'VENDEUR' | 'RESPONSABLE' | 'ADMIN'
interface Employee {
id: string
email: string
nom: string
prenom: string
role: RoleEmploye
actif: boolean
createdAt: string
updatedAt: string
_count?: {
ventes: number
facturesAchat: number
paiements: number
patients: number
}
}
const roleLabels: Record<RoleEmploye, string> = {
VENDEUR: 'Vendeur',
RESPONSABLE: 'Responsable',
ADMIN: 'Administrateur',
}
const emptyForm = {
email: '',
nom: '',
prenom: '',
role: 'VENDEUR' as RoleEmploye,
actif: true,
password: '',
}
export function EmployeeManagement() {
const [employees, setEmployees] = useState<Employee[]>([])
const [searchTerm, setSearchTerm] = useState('')
const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'inactive'>('all')
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [dialogOpen, setDialogOpen] = useState(false)
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null)
const [form, setForm] = useState(emptyForm)
useEffect(() => {
fetchEmployees()
}, [])
const filteredEmployees = useMemo(() => {
const term = searchTerm.toLowerCase()
return employees.filter((employee) => {
const matchesSearch =
!term ||
employee.nom.toLowerCase().includes(term) ||
employee.prenom.toLowerCase().includes(term) ||
employee.email.toLowerCase().includes(term) ||
roleLabels[employee.role].toLowerCase().includes(term)
const matchesStatus =
statusFilter === 'all' ||
(statusFilter === 'active' && employee.actif) ||
(statusFilter === 'inactive' && !employee.actif)
return matchesSearch && matchesStatus
})
}, [employees, searchTerm, statusFilter])
const activeCount = employees.filter((employee) => employee.actif).length
const adminCount = employees.filter((employee) => employee.actif && employee.role === 'ADMIN').length
async function fetchEmployees() {
setLoading(true)
setError('')
try {
const response = await fetch('/api/employes')
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Erreur lors du chargement des employes')
}
setEmployees(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur inconnue')
} finally {
setLoading(false)
}
}
function openCreateDialog() {
setEditingEmployee(null)
setForm(emptyForm)
setError('')
setDialogOpen(true)
}
function openEditDialog(employee: Employee) {
setEditingEmployee(employee)
setForm({
email: employee.email,
nom: employee.nom,
prenom: employee.prenom,
role: employee.role,
actif: employee.actif,
password: '',
})
setError('')
setDialogOpen(true)
}
async function saveEmployee(event: React.FormEvent) {
event.preventDefault()
setSaving(true)
setError('')
try {
const payload: Record<string, unknown> = {
email: form.email,
nom: form.nom,
prenom: form.prenom,
role: form.role,
actif: form.actif,
}
if (form.password) {
payload.password = form.password
}
const response = await fetch(
editingEmployee ? `/api/employes/${editingEmployee.id}` : '/api/employes',
{
method: editingEmployee ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
)
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Enregistrement impossible')
}
setDialogOpen(false)
setEditingEmployee(null)
setForm(emptyForm)
await fetchEmployees()
} catch (err) {
setError(err instanceof Error ? err.message : 'Erreur inconnue')
} finally {
setSaving(false)
}
}
async function deleteEmployee(employee: Employee) {
if (!confirm(`Supprimer ou desactiver ${employee.prenom} ${employee.nom} ?`)) return
setError('')
const response = await fetch(`/api/employes/${employee.id}`, { method: 'DELETE' })
const data = await response.json()
if (!response.ok) {
setError(data.error || 'Suppression impossible')
return
}
await fetchEmployees()
}
return (
<div className="space-y-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-2xl font-bold text-foreground">Utilisateurs et acces</h2>
<p className="text-sm text-muted-foreground">
Gere les employes, leur statut et leur niveau d'acces.
</p>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button onClick={openCreateDialog} className="gap-2">
<Plus className="h-4 w-4" />
Nouvel employe
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{editingEmployee ? 'Modifier un employe' : 'Nouvel employe'}</DialogTitle>
<DialogDescription>
Les administrateurs peuvent gerer les utilisateurs et les acces.
</DialogDescription>
</DialogHeader>
<form onSubmit={saveEmployee} className="space-y-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="prenom">Prenom</Label>
<Input
id="prenom"
value={form.prenom}
onChange={(event) => setForm({ ...form, prenom: event.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="nom">Nom</Label>
<Input
id="nom"
value={form.nom}
onChange={(event) => setForm({ ...form, nom: event.target.value })}
required
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={form.email}
onChange={(event) => setForm({ ...form, email: event.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">
{editingEmployee ? 'Nouveau mot de passe' : 'Mot de passe'}
</Label>
<Input
id="password"
type="password"
value={form.password}
onChange={(event) => setForm({ ...form, password: event.target.value })}
required={!editingEmployee}
placeholder={editingEmployee ? 'Laisser vide pour conserver' : ''}
/>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>Niveau d'acces</Label>
<Select
value={form.role}
onValueChange={(value: RoleEmploye) => setForm({ ...form, role: value })}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="VENDEUR">Vendeur</SelectItem>
<SelectItem value="RESPONSABLE">Responsable</SelectItem>
<SelectItem value="ADMIN">Administrateur</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-end justify-between rounded-md border p-3">
<div>
<Label>Compte actif</Label>
<p className="text-xs text-muted-foreground">Autorise la connexion</p>
</div>
<Switch
checked={form.actif}
onCheckedChange={(checked) => setForm({ ...form, actif: checked })}
/>
</div>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<div className="flex justify-end gap-2">
<Button type="button" variant="outline" onClick={() => setDialogOpen(false)}>
Annuler
</Button>
<Button type="submit" disabled={saving}>
{saving ? 'Enregistrement...' : 'Enregistrer'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<Card>
<CardContent className="flex items-center justify-between pt-6">
<div>
<p className="text-sm text-muted-foreground">Total</p>
<p className="text-2xl font-bold">{employees.length}</p>
</div>
<Users className="h-8 w-8 text-muted-foreground" />
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center justify-between pt-6">
<div>
<p className="text-sm text-muted-foreground">Comptes actifs</p>
<p className="text-2xl font-bold text-emerald-600">{activeCount}</p>
</div>
<UserCog className="h-8 w-8 text-emerald-500" />
</CardContent>
</Card>
<Card>
<CardContent className="flex items-center justify-between pt-6">
<div>
<p className="text-sm text-muted-foreground">Administrateurs</p>
<p className="text-2xl font-bold text-blue-600">{adminCount}</p>
</div>
<Shield className="h-8 w-8 text-blue-500" />
</CardContent>
</Card>
</div>
{error && !dialogOpen && <p className="text-sm text-destructive">{error}</p>}
<Card>
<CardHeader>
<CardTitle className="text-base">Recherche et filtres</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Rechercher par nom, email ou role..."
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
className="pl-10"
/>
</div>
<div className="flex flex-wrap gap-2">
<Button
variant={statusFilter === 'all' ? 'default' : 'outline'}
size="sm"
onClick={() => setStatusFilter('all')}
>
Tous
</Button>
<Button
variant={statusFilter === 'active' ? 'default' : 'outline'}
size="sm"
onClick={() => setStatusFilter('active')}
>
Actifs
</Button>
<Button
variant={statusFilter === 'inactive' ? 'default' : 'outline'}
size="sm"
onClick={() => setStatusFilter('inactive')}
>
Inactifs
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-0">
<ScrollArea className="h-[560px]">
<Table>
<TableHeader>
<TableRow>
<TableHead>Employe</TableHead>
<TableHead>Acces</TableHead>
<TableHead>Statut</TableHead>
<TableHead>Activite liee</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={5} className="py-8 text-center">
Chargement...
</TableCell>
</TableRow>
) : filteredEmployees.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="py-8 text-center text-muted-foreground">
Aucun employe trouve
</TableCell>
</TableRow>
) : (
filteredEmployees.map((employee) => (
<TableRow key={employee.id}>
<TableCell>
<div className="font-medium">
{employee.prenom} {employee.nom}
</div>
<div className="text-sm text-muted-foreground">{employee.email}</div>
</TableCell>
<TableCell>
<Badge variant={employee.role === 'ADMIN' ? 'default' : 'secondary'}>
{roleLabels[employee.role]}
</Badge>
</TableCell>
<TableCell>
<Badge variant={employee.actif ? 'default' : 'secondary'}>
{employee.actif ? 'Actif' : 'Inactif'}
</Badge>
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{(employee._count?.ventes || 0) + (employee._count?.facturesAchat || 0)} operations
</TableCell>
<TableCell>
<div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" onClick={() => openEditDialog(employee)}>
<UserCog className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => deleteEmployee(employee)}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</ScrollArea>
</CardContent>
</Card>
</div>
)
}