diff --git a/SOLUTION_COMPLETE_PRODUCTION.md b/SOLUTION_COMPLETE_PRODUCTION.md new file mode 100644 index 0000000..082a8e8 --- /dev/null +++ b/SOLUTION_COMPLETE_PRODUCTION.md @@ -0,0 +1,646 @@ +# 🚀 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 + +