diff --git a/client/src/components/RouterProduction.tsx b/client/src/components/RouterProduction.tsx index 16c6445..3081448 100644 --- a/client/src/components/RouterProduction.tsx +++ b/client/src/components/RouterProduction.tsx @@ -21,6 +21,7 @@ import BackupManager from "@/pages/BackupManager"; import Utilities from "@/pages/Utilities"; import Tasks from "@/pages/Tasks"; import SavTickets from "@/pages/SavTickets"; +import Avoirs from "@/pages/Avoirs"; import WeatherSettings from "@/pages/WeatherSettings"; import Layout from "@/components/Layout"; @@ -96,6 +97,7 @@ function RouterProduction() { + {/* Routes de compatibilité - redirection vers utilities */} diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 46f64ea..eb20bfc 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -32,7 +32,8 @@ import { Settings, Wrench, FileUp, - Send + Send, + Receipt } from "lucide-react"; export default function Sidebar() { @@ -284,6 +285,12 @@ export default function Sidebar() { icon: Wrench, roles: ["admin", "directeur", "manager", "employee"] }, + { + path: "/avoirs", + label: "Avoirs", + icon: Receipt, + roles: ["admin", "directeur", "manager"] + }, ]; const adminItems = [ diff --git a/client/src/pages/Avoirs.tsx b/client/src/pages/Avoirs.tsx new file mode 100644 index 0000000..17d3718 --- /dev/null +++ b/client/src/pages/Avoirs.tsx @@ -0,0 +1,530 @@ +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Plus, Search, FileText, CheckCircle, AlertCircle, Clock } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; + +// Types +interface Avoir { + id: number; + supplierId: number; + groupId: number; + invoiceReference: string; + amount: number; + comment?: string; + commercialProcessed: boolean; + status: 'En attente de demande' | 'Demandé' | 'Reçu'; + webhookSent: boolean; + nocodbVerified: boolean; + nocodbVerifiedAt?: Date; + processedAt?: Date; + createdBy: string; + createdAt: Date; + updatedAt: Date; + supplier: { + id: number; + name: string; + contact?: string; + }; + group: { + id: number; + name: string; + color: string; + }; + creator: { + id: string; + firstName?: string; + lastName?: string; + username: string; + }; +} + +interface Supplier { + id: number; + name: string; +} + +interface Group { + id: number; + name: string; + color: string; +} + +// Schema for form validation +const avoirSchema = z.object({ + supplierId: z.number().min(1, "Veuillez sélectionner un fournisseur"), + groupId: z.number().min(1, "Veuillez sélectionner un magasin"), + invoiceReference: z.string().min(1, "La référence facture est requise"), + amount: z.number().min(0.01, "Le montant doit être supérieur à 0"), + comment: z.string().optional(), + commercialProcessed: z.boolean().optional(), +}); + +type AvoirFormData = z.infer; + +export default function Avoirs() { + const [searchTerm, setSearchTerm] = useState(""); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [selectedStoreId, setSelectedStoreId] = useState("all"); + const { toast } = useToast(); + const queryClient = useQueryClient(); + + // Fetch user profile + const { data: user } = useQuery({ + queryKey: ['/api/user'], + }); + + // Fetch groups for store filter + const { data: groups = [] } = useQuery({ + queryKey: ['/api/groups'], + enabled: !!user, + }); + + // Fetch suppliers + const { data: suppliers = [] } = useQuery({ + queryKey: ['/api/suppliers'], + enabled: !!user, + }); + + // Fetch avoirs + const { data: avoirs = [], isLoading } = useQuery({ + queryKey: ['/api/avoirs', selectedStoreId], + queryFn: () => apiRequest(`/api/avoirs${selectedStoreId !== 'all' ? `?storeId=${selectedStoreId}` : ''}`), + enabled: !!user, + }); + + // Create avoir mutation + const createAvoirMutation = useMutation({ + mutationFn: async (data: AvoirFormData) => { + const response = await fetch('/api/avoirs', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + credentials: 'include', + }); + if (!response.ok) { + throw new Error('Failed to create avoir'); + } + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/avoirs'] }); + setIsCreateDialogOpen(false); + toast({ + title: "Avoir créé", + description: "L'avoir a été créé avec succès.", + }); + }, + onError: (error: any) => { + toast({ + title: "Erreur", + description: error.message || "Erreur lors de la création de l'avoir", + variant: "destructive", + }); + } + }); + + // Initialize form + const form = useForm({ + resolver: zodResolver(avoirSchema), + defaultValues: { + supplierId: 0, + groupId: 0, + invoiceReference: "", + amount: 0, + comment: "", + commercialProcessed: false, + }, + }); + + // Handle form submission + const onSubmit = (data: AvoirFormData) => { + createAvoirMutation.mutate(data); + }; + + // Filter avoirs based on search term + const filteredAvoirs = avoirs.filter(avoir => + avoir.invoiceReference.toLowerCase().includes(searchTerm.toLowerCase()) || + avoir.supplier.name.toLowerCase().includes(searchTerm.toLowerCase()) || + avoir.comment?.toLowerCase().includes(searchTerm.toLowerCase()) || + (avoir.creator.firstName && avoir.creator.lastName + ? `${avoir.creator.firstName} ${avoir.creator.lastName}`.toLowerCase().includes(searchTerm.toLowerCase()) + : avoir.creator.username.toLowerCase().includes(searchTerm.toLowerCase()) + ) + ); + + // Status badge variant + const getStatusVariant = (status: string) => { + switch (status) { + case 'Reçu': + return 'default'; + case 'Demandé': + return 'secondary'; + case 'En attente de demande': + return 'outline'; + default: + return 'outline'; + } + }; + + // Status icon + const getStatusIcon = (status: string) => { + switch (status) { + case 'Reçu': + return ; + case 'Demandé': + return ; + case 'En attente de demande': + return ; + default: + return ; + } + }; + + if (!user) { + return
Chargement...
; + } + + return ( +
+
+
+

Avoirs

+

+ Gestion des demandes d'avoirs et de remboursements +

+
+ + + + + + + + Créer un nouvel avoir + + Remplissez les informations pour créer une nouvelle demande d'avoir. + + + +
+ + ( + + Fournisseur + + + + )} + /> + + ( + + Magasin + + + + )} + /> + + ( + + Référence facture + + + + + + )} + /> + + ( + + Montant (€) + + field.onChange(parseFloat(e.target.value) || 0)} + /> + + + + )} + /> + + ( + + Commentaire + +