From f3565df01f18a5cd1e8a988f9b8bf7737c778810 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Mon, 8 Sep 2025 13:48:14 +0000 Subject: [PATCH] Add functionality to manage and track customer credit notes Introduces the "Avoirs" feature, including database schema, API endpoints for CRUD operations, and frontend components for viewing and managing credit notes. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869/0C3uBiZ --- client/src/components/RouterProduction.tsx | 2 + client/src/components/Sidebar.tsx | 9 +- client/src/pages/Avoirs.tsx | 530 +++++++++++++++++++++ server/routes.ts | 253 +++++++++- server/storage.ts | 228 ++++++++- sql/create_avoirs_table.sql | 50 ++ 6 files changed, 1069 insertions(+), 3 deletions(-) create mode 100644 client/src/pages/Avoirs.tsx create mode 100644 sql/create_avoirs_table.sql 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 + +