360 lines
15 KiB
TypeScript
360 lines
15 KiB
TypeScript
import { useState } from "react";
|
|
import { trpc } from "@/lib/trpc";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { toast } from "sonner";
|
|
import { Loader2, Plus, Trash2, List, Building2, Calculator, Sparkles } from "lucide-react";
|
|
import DashboardLayout from "@/components/DashboardLayout";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
|
|
export default function ListsAdmin() {
|
|
const { data: departments, isLoading: loadingDepartments } = trpc.departments.getByUser.useQuery();
|
|
const { data: allocations, isLoading: loadingAllocations } = trpc.accountingAllocations.getByUser.useQuery();
|
|
|
|
const createDepartmentMutation = trpc.departments.create.useMutation();
|
|
const deleteDepartmentMutation = trpc.departments.delete.useMutation();
|
|
const createAllocationMutation = trpc.accountingAllocations.create.useMutation();
|
|
const deleteAllocationMutation = trpc.accountingAllocations.delete.useMutation();
|
|
|
|
const utils = trpc.useUtils();
|
|
|
|
const [newDepartment, setNewDepartment] = useState("");
|
|
const [newAllocation, setNewAllocation] = useState("");
|
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
const [itemToDelete, setItemToDelete] = useState<{ type: "department" | "allocation"; id: number; name: string } | null>(null);
|
|
|
|
const handleCreateDepartment = async () => {
|
|
if (!newDepartment.trim()) {
|
|
toast.error("Le nom ne peut pas être vide");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await createDepartmentMutation.mutateAsync({ name: newDepartment.trim() });
|
|
await utils.departments.getByUser.invalidate();
|
|
setNewDepartment("");
|
|
toast.success(`Service "${newDepartment}" ajouté avec succès`);
|
|
} catch (error: any) {
|
|
toast.error(error.message || "Impossible d'ajouter le service");
|
|
}
|
|
};
|
|
|
|
const handleCreateAllocation = async () => {
|
|
if (!newAllocation.trim()) {
|
|
toast.error("Le nom ne peut pas être vide");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await createAllocationMutation.mutateAsync({ name: newAllocation.trim() });
|
|
await utils.accountingAllocations.getByUser.invalidate();
|
|
setNewAllocation("");
|
|
toast.success(`Ventilation "${newAllocation}" ajoutée avec succès`);
|
|
} catch (error: any) {
|
|
toast.error(error.message || "Impossible d'ajouter la ventilation");
|
|
}
|
|
};
|
|
|
|
const confirmDelete = (type: "department" | "allocation", id: number, name: string) => {
|
|
setItemToDelete({ type, id, name });
|
|
setDeleteDialogOpen(true);
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!itemToDelete) return;
|
|
|
|
try {
|
|
if (itemToDelete.type === "department") {
|
|
await deleteDepartmentMutation.mutateAsync({ id: itemToDelete.id });
|
|
await utils.departments.getByUser.invalidate();
|
|
} else {
|
|
await deleteAllocationMutation.mutateAsync({ id: itemToDelete.id });
|
|
await utils.accountingAllocations.getByUser.invalidate();
|
|
}
|
|
toast.success(`"${itemToDelete.name}" supprimé avec succès`);
|
|
} catch (error: any) {
|
|
toast.error(error.message || "Impossible de supprimer");
|
|
} finally {
|
|
setDeleteDialogOpen(false);
|
|
setItemToDelete(null);
|
|
}
|
|
};
|
|
|
|
if (loadingDepartments || loadingAllocations) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="flex flex-col items-center justify-center h-64 gap-4">
|
|
<Loader2 className="w-12 h-12 animate-spin text-primary" />
|
|
<p className="text-muted-foreground">Chargement des listes...</p>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
const departmentCount = departments?.length || 0;
|
|
const allocationCount = allocations?.length || 0;
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="max-w-5xl space-y-8">
|
|
{/* Header */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-3 bg-gradient-to-br from-purple-500 to-pink-600 rounded-xl shadow-lg">
|
|
<List className="w-7 h-7 text-white" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-4xl font-bold bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">
|
|
Administration des listes
|
|
</h1>
|
|
<p className="text-muted-foreground mt-1">
|
|
Gérez les listes de valeurs pour les champs métier des factures
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Statistics */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="p-4 bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-950/20 dark:to-cyan-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-blue-500 rounded-lg">
|
|
<Building2 className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Services</p>
|
|
<p className="text-2xl font-bold text-blue-900 dark:text-blue-100">{departmentCount}</p>
|
|
</div>
|
|
</div>
|
|
<Badge variant="secondary" className="text-sm">
|
|
Actif{departmentCount > 1 ? 's' : ''}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 rounded-lg border border-green-200 dark:border-green-800">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-green-500 rounded-lg">
|
|
<Calculator className="w-5 h-5 text-white" />
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Ventilations</p>
|
|
<p className="text-2xl font-bold text-green-900 dark:text-green-100">{allocationCount}</p>
|
|
</div>
|
|
</div>
|
|
<Badge variant="secondary" className="text-sm">
|
|
Active{allocationCount > 1 ? 's' : ''}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Department List */}
|
|
<Card className="border-2 hover:border-primary/50 transition-colors">
|
|
<CardHeader className="bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-950/20 dark:to-cyan-950/20 border-b">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-blue-500 rounded-lg">
|
|
<Building2 className="w-6 h-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-xl">Service concerné</CardTitle>
|
|
<CardDescription className="mt-1">
|
|
Gérez la liste des services disponibles pour la classification des factures
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6 pt-6">
|
|
<div className="flex gap-3">
|
|
<div className="flex-1">
|
|
<Input
|
|
placeholder="Nouveau service (ex: INFORMATIQUE)"
|
|
value={newDepartment}
|
|
onChange={(e) => setNewDepartment(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
handleCreateDepartment();
|
|
}
|
|
}}
|
|
className="h-11"
|
|
/>
|
|
</div>
|
|
<Button
|
|
onClick={handleCreateDepartment}
|
|
disabled={createDepartmentMutation.isPending || !newDepartment.trim()}
|
|
size="lg"
|
|
className="bg-blue-500 hover:bg-blue-600"
|
|
>
|
|
{createDepartmentMutation.isPending ? (
|
|
<Loader2 className="h-5 w-5 animate-spin" />
|
|
) : (
|
|
<Plus className="h-5 w-5" />
|
|
)}
|
|
<span className="ml-2">Ajouter</span>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{departments && departments.length > 0 ? (
|
|
departments.map((dept) => (
|
|
<div
|
|
key={dept.id}
|
|
className="group flex items-center justify-between p-4 border-2 rounded-lg hover:border-blue-300 hover:bg-blue-50/50 dark:hover:bg-blue-950/20 transition-all"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg group-hover:bg-blue-200 dark:group-hover:bg-blue-900/50 transition-colors">
|
|
<Building2 className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
|
</div>
|
|
<span className="font-semibold text-lg">{dept.name}</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => confirmDelete("department", dept.id, dept.name)}
|
|
disabled={deleteDepartmentMutation.isPending}
|
|
className="opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-100 dark:hover:bg-red-900/30"
|
|
>
|
|
<Trash2 className="h-5 w-5 text-destructive" />
|
|
</Button>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-center py-12 bg-muted/30 rounded-lg border-2 border-dashed">
|
|
<Building2 className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
|
|
<p className="text-muted-foreground font-medium">
|
|
Aucun service configuré
|
|
</p>
|
|
<p className="text-sm text-muted-foreground/70 mt-1">
|
|
Ajoutez-en un pour commencer
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Accounting Allocation List */}
|
|
<Card className="border-2 hover:border-primary/50 transition-colors">
|
|
<CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-b">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-green-500 rounded-lg">
|
|
<Calculator className="w-6 h-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-xl">Ventilation comptable</CardTitle>
|
|
<CardDescription className="mt-1">
|
|
Gérez la liste des ventilations comptables disponibles pour la classification des factures
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6 pt-6">
|
|
<div className="flex gap-3">
|
|
<div className="flex-1">
|
|
<Input
|
|
placeholder="Nouvelle ventilation (ex: MAINTENANCE)"
|
|
value={newAllocation}
|
|
onChange={(e) => setNewAllocation(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
handleCreateAllocation();
|
|
}
|
|
}}
|
|
className="h-11"
|
|
/>
|
|
</div>
|
|
<Button
|
|
onClick={handleCreateAllocation}
|
|
disabled={createAllocationMutation.isPending || !newAllocation.trim()}
|
|
size="lg"
|
|
className="bg-green-500 hover:bg-green-600"
|
|
>
|
|
{createAllocationMutation.isPending ? (
|
|
<Loader2 className="h-5 w-5 animate-spin" />
|
|
) : (
|
|
<Plus className="h-5 w-5" />
|
|
)}
|
|
<span className="ml-2">Ajouter</span>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{allocations && allocations.length > 0 ? (
|
|
allocations.map((alloc) => (
|
|
<div
|
|
key={alloc.id}
|
|
className="group flex items-center justify-between p-4 border-2 rounded-lg hover:border-green-300 hover:bg-green-50/50 dark:hover:bg-green-950/20 transition-all"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg group-hover:bg-green-200 dark:group-hover:bg-green-900/50 transition-colors">
|
|
<Calculator className="h-4 w-4 text-green-600 dark:text-green-400" />
|
|
</div>
|
|
<span className="font-semibold text-lg">{alloc.name}</span>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => confirmDelete("allocation", alloc.id, alloc.name)}
|
|
disabled={deleteAllocationMutation.isPending}
|
|
className="opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-100 dark:hover:bg-red-900/30"
|
|
>
|
|
<Trash2 className="h-5 w-5 text-destructive" />
|
|
</Button>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-center py-12 bg-muted/30 rounded-lg border-2 border-dashed">
|
|
<Calculator className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
|
|
<p className="text-muted-foreground font-medium">
|
|
Aucune ventilation configurée
|
|
</p>
|
|
<p className="text-sm text-muted-foreground/70 mt-1">
|
|
Ajoutez-en une pour commencer
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle className="text-xl">Confirmer la suppression</AlertDialogTitle>
|
|
<AlertDialogDescription className="text-base">
|
|
Êtes-vous sûr de vouloir supprimer <span className="font-semibold">"{itemToDelete?.name}"</span> ?
|
|
<br />
|
|
Cette action est irréversible.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleDelete}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Supprimer
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</DashboardLayout>
|
|
);
|
|
}
|