# 🚀 SOLUTION COMPLÈTE - MODULE AVOIRS PRODUCTION ## đŸ”„ PROBLÈME RÉSOLU - Erreur `contact_email` supprimĂ©e - Interface React corrigĂ©e pour les champs optionnels - PrĂȘt pour la production ## 📋 ÉTAPES DE DÉPLOIEMENT ### ÉTAPE 1 - Base de donnĂ©es (ExĂ©cuter dans votre console PostgreSQL) ```sql -- Corriger la structure de la table avoirs ALTER TABLE avoirs ALTER COLUMN invoice_reference DROP NOT NULL; ALTER TABLE avoirs ALTER COLUMN amount DROP NOT NULL; -- VĂ©rifier la correction SELECT column_name, is_nullable FROM information_schema.columns WHERE table_name = 'avoirs' AND column_name IN ('invoice_reference', 'amount'); ``` ### ÉTAPE 2 - Fichiers Ă  remplacer sur votre serveur #### A) shared/schema.ts (section avoirs uniquement) Remplacez la section avoirs par : ```typescript export const avoirs = pgTable("avoirs", { id: serial("id").primaryKey(), supplierId: integer("supplier_id").notNull(), groupId: integer("group_id").notNull(), invoiceReference: varchar("invoice_reference"), // OPTIONNEL amount: decimal("amount", { precision: 10, scale: 2 }), // OPTIONNEL comment: text("comment"), commercialProcessed: boolean("commercial_processed").default(false), status: varchar("status").notNull().default("En attente de demande"), webhookSent: boolean("webhook_sent").default(false), nocodbVerified: boolean("nocodb_verified").default(false), nocodbVerifiedAt: timestamp("nocodb_verified_at"), processedAt: timestamp("processed_at"), createdBy: varchar("created_by").notNull(), createdAt: timestamp("created_at").defaultNow(), updatedAt: timestamp("updated_at").defaultNow(), }); export const insertAvoirSchema = createInsertSchema(avoirs).omit({ id: true, createdAt: true, updatedAt: true, webhookSent: true, nocodbVerified: true, nocodbVerifiedAt: true, processedAt: true, }); export type Avoir = typeof avoirs.$inferSelect; export type InsertAvoir = z.infer; ``` #### B) client/src/pages/Avoirs.tsx **FICHIER COMPLET CORRIGÉ - Copiez tout :** ```typescript import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Plus, Search, FileText, CheckCircle, Clock, AlertCircle } from "lucide-react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; // Types interface Avoir { id: number; supplierId: number; groupId: number; invoiceReference?: string; // OPTIONNEL amount?: number; // OPTIONNEL 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; phone?: string; }; group: { id: number; name: string; color: string; }; creator: { id: string; username: string; firstName?: string; lastName?: 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().optional(), amount: z.number().optional(), 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: undefined, comment: "", commercialProcessed: false, }, }); // Handle form submission const onSubmit = (data: AvoirFormData) => { createAvoirMutation.mutate(data); }; // Filter avoirs based on search term - CORRECTION CRITIQUE 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...
; } // Check permissions if ((user as any)?.role === 'employee') { return (

AccÚs refusé

Vous n'avez pas les permissions pour accéder aux avoirs.

); } const canCreate = ['admin', 'directeur', 'manager'].includes((user as any)?.role); const userGroups = (user as any)?.userGroups || []; const availableGroups = (user as any)?.role === 'admin' ? groups : userGroups.map((ug: any) => ug.group); return (
{/* Header */}

Avoirs

Gestion des demandes d'avoirs et suivi des remboursements

{canCreate && ( Créer un nouvel avoir
( Fournisseur * )} /> ( Magasin * )} /> ( Référence facture (optionnel) )} /> ( Montant (optionnel) { const value = e.target.value; field.onChange(value ? parseFloat(value) : undefined); }} value={field.value || ''} /> )} /> ( Commentaire