import { useState, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Plus, Search, FileText, CheckCircle, AlertCircle, Clock, Edit, Trash2, UserCheck, Send, Upload, XCircle, MessageSquare } from "lucide-react"; import { useStore } from "@/components/Layout"; 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, DialogFooter } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-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; nocodbTableName?: string; nocodbConfigId?: number; }; creator: { id: string; firstName?: string; lastName?: string; username: string; }; } interface Supplier { id: number; name: string; } interface Group { id: number; name: string; color: string; nocodbTableName?: string; nocodbConfigId?: number; } // Schema for form validation const avoirSchema = z.object({ supplierId: z.coerce.number().min(1, "Veuillez sélectionner un fournisseur"), groupId: z.coerce.number().min(1, "Veuillez sélectionner un magasin"), invoiceReference: z.string().optional(), amount: z.union([ z.null().transform(() => undefined), z.undefined().transform(() => undefined), z.literal("").transform(() => undefined), z.number().transform((num) => { // Permettre tous les nombres existants (positifs, négatifs, zéro) return num; }), z.string().transform((val) => { if (val.trim() === '') return undefined; // Permettre les champs vides const num = parseFloat(val); if (isNaN(num)) { throw new Error("Montant invalide"); } // Supprimer la restriction > 0 pour permettre zéro et les montants négatifs (avoirs) return num; }) ]).optional(), comment: z.string().optional(), commercialProcessed: z.boolean().default(false), status: z.enum(["En attente de demande", "Demandé", "Reçu"]).optional(), }); type AvoirFormData = z.infer; // Type étendu pour les mises à jour incluant les champs de validation type AvoirUpdateData = AvoirFormData & { nocodbVerified?: boolean; nocodbVerifiedAt?: Date | null; }; export default function Avoirs() { const [searchTerm, setSearchTerm] = useState(""); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedAvoir, setSelectedAvoir] = useState(null); // État pour le modal d'envoi d'avoir const [showAvoirModal, setShowAvoirModal] = useState(false); const [selectedAvoirForUpload, setSelectedAvoirForUpload] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [isUploading, setIsUploading] = useState(false); // État pour le modal d'attente du webhook const [showWaitingModal, setShowWaitingModal] = useState(false); const [processingSeconds, setProcessingSeconds] = useState(0); const [processingTimeout, setProcessingTimeout] = useState(null); // États pour le système de vérification de facture const [avoirVerificationResults, setAvoirVerificationResults] = useState>({}); const [verifyingAvoirs, setVerifyingAvoirs] = useState>(new Set()); // ✅ État local pour le champ montant d'édition pour éviter les conflits avec React Hook Form const [editAmountValue, setEditAmountValue] = useState(""); // État pour le modal de commentaire const [showCommentModal, setShowCommentModal] = useState(false); const [selectedComment, setSelectedComment] = useState(""); // Utiliser le contexte global du magasin const { selectedStoreId } = useStore(); 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 avec filtrage par groupe (comme Orders/Deliveries) const avoirsUrl = `/api/avoirs${selectedStoreId ? `?storeId=${selectedStoreId}` : ''}`; const { data: avoirs = [], isLoading } = useQuery({ queryKey: [avoirsUrl, selectedStoreId, (user as any)?.role], queryFn: async () => { console.log('💰 Fetching avoirs from:', avoirsUrl); const response = await fetch(avoirsUrl, { credentials: 'include' }); if (!response.ok) { throw new Error('Failed to fetch avoirs'); } const data = await response.json(); console.log('💰 Avoirs received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items'); // 🔍 Debug: Vérifier les champs nocodbVerified if (Array.isArray(data) && data.length > 0) { console.log('🔍 Premiers avoirs avec nocodbVerified:', data.map(a => ({ id: a.id, invoiceReference: a.invoiceReference, nocodbVerified: a.nocodbVerified, nocodbVerifiedAt: a.nocodbVerifiedAt }))); } return Array.isArray(data) ? data : []; }, enabled: !!user, }); // 🔄 Charger les vérifications depuis le cache serveur au démarrage useEffect(() => { const loadCachedVerifications = async () => { console.log('🔍 LoadCache - Début vérification:', { hasAvoirs: !!avoirs, avoirCount: avoirs?.length || 0 }); if (!avoirs || avoirs.length === 0) return; const cachedResults: Record = {}; // Charger les vérifications depuis le cache serveur pour TOUS les avoirs avec référence for (const avoir of avoirs) { console.log('🔍 LoadCache - Avoir analysé:', { id: avoir.id, hasInvoiceRef: !!avoir.invoiceReference?.trim(), invoiceReference: avoir.invoiceReference, nocodbVerified: avoir.nocodbVerified }); // Si l'avoir a une référence de facture, vérifier le cache serveur if (avoir.invoiceReference?.trim()) { try { const result = await apiRequest(`/api/avoirs/${avoir.id}/verify-invoice`, 'POST', { invoiceReference: avoir.invoiceReference, forceRefresh: false // Utiliser le cache si disponible }); console.log('🔍 LoadCache - Résultat cache serveur pour avoir', avoir.id, ':', result); if (result.exists !== undefined) { cachedResults[avoir.id] = result; console.log('✅ LoadCache - Avoir ajouté au cache depuis serveur:', avoir.id); } } catch (error) { console.log('⚠️ LoadCache - Erreur cache serveur pour avoir', avoir.id, ':', error); // Continuer sans erreur si le cache échoue } } } console.log('🔍 LoadCache - Résultats finaux:', { cachedCount: Object.keys(cachedResults).length, cachedResults }); if (Object.keys(cachedResults).length > 0) { setAvoirVerificationResults(prev => ({ ...prev, ...cachedResults })); console.log('✅ Vérifications chargées depuis le serveur:', cachedResults); } }; loadCachedVerifications(); }, [avoirs]); // 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: () => { // ✅ FIX: Invalider toutes les variations de queryKey avoirs queryClient.invalidateQueries({ predicate: (query) => { return query.queryKey[0]?.toString().includes('/api/avoirs') || false; } }); 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", }); } }); // Edit avoir mutation const editAvoirMutation = useMutation({ mutationFn: async ({ id, data }: { id: number, data: AvoirUpdateData }) => { const response = await fetch(`/api/avoirs/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), credentials: 'include', }); if (!response.ok) { throw new Error('Failed to edit avoir'); } return response.json(); }, onSuccess: () => { // ✅ FIX: Invalider toutes les variations de queryKey avoirs queryClient.invalidateQueries({ predicate: (query) => { return query.queryKey[0]?.toString().includes('/api/avoirs') || false; } }); setIsEditDialogOpen(false); setSelectedAvoir(null); toast({ title: "Avoir modifié", description: "L'avoir a été modifié avec succès.", }); }, onError: (error: any) => { toast({ title: "Erreur", description: error.message || "Erreur lors de la modification", variant: "destructive", }); } }); // Delete avoir mutation const deleteAvoirMutation = useMutation({ mutationFn: async (id: number) => { const response = await fetch(`/api/avoirs/${id}`, { method: 'DELETE', credentials: 'include', }); if (!response.ok) { throw new Error('Failed to delete avoir'); } return response.json(); }, onSuccess: () => { // ✅ FIX: Invalider toutes les variations de queryKey avoirs queryClient.invalidateQueries({ predicate: (query) => { return query.queryKey[0]?.toString().includes('/api/avoirs') || false; } }); setIsDeleteDialogOpen(false); setSelectedAvoir(null); toast({ title: "Avoir supprimé", description: "L'avoir a été supprimé avec succès.", }); }, onError: (error: any) => { toast({ title: "Erreur", description: error.message || "Erreur lors de la suppression", variant: "destructive", }); } }); // Initialize create form const form = useForm({ resolver: zodResolver(avoirSchema), defaultValues: { supplierId: 0, groupId: 0, invoiceReference: "", amount: undefined, comment: "", commercialProcessed: false, status: "En attente de demande", }, }); // Initialize edit form const editForm = useForm({ resolver: zodResolver(avoirSchema), defaultValues: { supplierId: 0, groupId: 0, invoiceReference: "", amount: undefined, comment: "", commercialProcessed: false, status: "En attente de demande", }, }); // Handle form submission const onSubmit = (data: AvoirFormData) => { createAvoirMutation.mutate(data); }; // Handle edit form submission const onEditSubmit = (data: AvoirFormData) => { if (selectedAvoir) { // ✅ Utiliser la valeur de l'état local pour le montant const finalAmount = editAmountValue === "" ? undefined : parseFloat(editAmountValue); const finalData = { ...data, amount: finalAmount }; console.log('💰 Editing avoir - Form data sent:', { originalCommercial: selectedAvoir.commercialProcessed, formCommercial: finalData.commercialProcessed, editAmountValue, finalAmount, fullData: finalData }); editAvoirMutation.mutate({ id: selectedAvoir.id, data: finalData }); } }; // Handle edit action const handleEdit = (avoir: Avoir) => { if (!avoir || !avoir.id) return; setSelectedAvoir(avoir); // ✅ Initialiser l'état local du montant setEditAmountValue(avoir.amount !== null && avoir.amount !== undefined ? avoir.amount.toString() : ""); editForm.reset({ supplierId: avoir.supplierId || 0, groupId: avoir.groupId || 0, invoiceReference: avoir.invoiceReference || "", amount: avoir.amount ?? undefined, comment: avoir.comment || "", commercialProcessed: avoir.commercialProcessed || false, status: avoir.status || "En attente de demande", }); setIsEditDialogOpen(true); }; // Handle delete action const handleDelete = (avoir: Avoir) => { if (!avoir || !avoir.id) return; setSelectedAvoir(avoir); setIsDeleteDialogOpen(true); }; // Handle comment view const handleViewComment = (comment: string) => { setSelectedComment(comment); setShowCommentModal(true); }; // Handle status change const handleStatusChange = (avoirId: number, newStatus: string) => { const avoir = avoirs?.find(a => a?.id === avoirId); if (avoir && avoir.supplierId && avoir.groupId) { editAvoirMutation.mutate({ id: avoirId, data: { supplierId: avoir.supplierId, groupId: avoir.groupId, invoiceReference: avoir.invoiceReference || "", amount: avoir.amount, comment: avoir.comment || "", commercialProcessed: avoir.commercialProcessed, status: newStatus as "En attente de demande" | "Demandé" | "Reçu", } }); } }; // Handle validation/devalidation const handleValidateAvoir = async (avoirId: number) => { try { // Utiliser la route spécifique pour la vérification NocoDB await apiRequest(`/api/avoirs/${avoirId}/nocodb-verification`, 'PUT', { verified: true }); // Marquer le cache comme réconcilié (permanent) const avoir = avoirs.find(a => a.id === avoirId); if (avoir?.invoiceReference) { try { await apiRequest('/api/cache/mark-reconciled', 'POST', { invoiceReference: avoir.invoiceReference, groupId: avoir.groupId }); } catch (cacheError) { console.warn('Cache marking failed but verification succeeded:', cacheError); } } // ✅ Mettre à jour l'état local immédiatement pour affichage instantané setAvoirVerificationResults(prev => ({ ...prev, [avoirId]: { exists: true, fromCache: true, permanent: true, validated: true } })); console.log('✅ Avoir validé - État local mis à jour:', avoirId); // Recharger les avoirs pour voir les changements en base queryClient.invalidateQueries({ queryKey: ['/api/avoirs'] }); toast({ title: "Avoir validé", description: "L'avoir a été marqué comme validé avec succès", }); } catch (error) { toast({ title: "Erreur de validation", description: error instanceof Error ? error.message : "Erreur lors de la validation", variant: "destructive", }); } }; const handleDevalidateAvoir = async (avoirId: number) => { try { // Utiliser la route spécifique pour la vérification NocoDB await apiRequest(`/api/avoirs/${avoirId}/nocodb-verification`, 'PUT', { verified: false }); // ✅ Retirer de l'état local immédiatement setAvoirVerificationResults(prev => { const updated = { ...prev }; delete updated[avoirId]; // Supprimer complètement l'entrée return updated; }); console.log('✅ Avoir dévalidé - État local mis à jour:', avoirId); // Recharger les avoirs pour voir les changements en base queryClient.invalidateQueries({ queryKey: ['/api/avoirs'] }); toast({ title: "Avoir dévalidé", description: "L'avoir a été marqué comme non-validé avec succès", }); } catch (error) { toast({ title: "Erreur de dévalidation", description: error instanceof Error ? error.message : "Erreur lors de la dévalidation", variant: "destructive", }); } }; // 🔍 FONCTION DE VÉRIFICATION DE FACTURE (comme rapprochement) const verifyAvoirInvoiceMutation = useMutation({ mutationFn: async ({ avoirId, invoiceReference, forceRefresh }: { avoirId: number; invoiceReference?: string; forceRefresh?: boolean }) => { try { const result = await apiRequest(`/api/avoirs/${avoirId}/verify-invoice`, 'POST', { invoiceReference, forceRefresh: forceRefresh || false }); return result; } catch (error: any) { console.error('Erreur API vérification avoir:', error); throw new Error(error.message || 'Erreur de vérification'); } }, onSuccess: (result, variables) => { setAvoirVerificationResults(prev => ({ ...prev, [variables.avoirId]: result })); // Auto-remplissage sécurisé si facture trouvée et montant disponible if (result?.exists === true && result?.invoiceAmount !== undefined && result?.invoiceAmount !== null) { const avoir = avoirs?.find(a => a?.id === variables.avoirId); if (avoir && avoir.supplierId && avoir.groupId) { try { console.log('🔄 Auto-remplissage montant:', { avoirId: variables.avoirId, amount: result.invoiceAmount }); editAvoirMutation.mutate({ id: variables.avoirId, data: { supplierId: avoir.supplierId, groupId: avoir.groupId, invoiceReference: avoir.invoiceReference || "", amount: result.invoiceAmount, comment: avoir.comment || "", commercialProcessed: avoir.commercialProcessed || false, status: avoir.status as "En attente de demande" | "Demandé" | "Reçu", } }); } catch (autoFillError) { console.error('❌ Erreur auto-remplissage:', autoFillError); } } } setVerifyingAvoirs(prev => { const newSet = new Set(prev); newSet.delete(variables.avoirId); return newSet; }); }, onError: (error, variables) => { console.error('Erreur vérification facture avoir:', error); setAvoirVerificationResults(prev => ({ ...prev, [variables.avoirId]: { exists: false, matchType: 'none', errorMessage: error instanceof Error ? error.message : 'Erreur inconnue' } })); setVerifyingAvoirs(prev => { const newSet = new Set(prev); newSet.delete(variables.avoirId); return newSet; }); toast({ title: "Erreur de vérification", description: error instanceof Error ? error.message : 'Erreur inconnue', variant: "destructive", }); } }); // Fonction pour déclencher la vérification d'un avoir const handleVerifyAvoirInvoice = (avoir: Avoir, forceRefresh: boolean = false) => { const hasInvoiceRef = avoir.invoiceReference?.trim(); if (!hasInvoiceRef) { toast({ title: "Référence manquante", description: "Veuillez saisir une référence de facture avant la vérification", variant: "destructive", }); return; } if (!avoir.group?.nocodbTableName && !avoir.group?.nocodbConfigId) { toast({ title: "Vérification non disponible", description: "Ce magasin n'a pas de configuration NocoDB", variant: "destructive", }); return; } console.log('🔍 Déclenchement vérification avoir:', { avoirId: avoir.id, invoiceReference: avoir.invoiceReference, supplier: avoir.supplier?.name, group: avoir.group?.name }); setVerifyingAvoirs(prev => new Set(prev).add(avoir.id)); verifyAvoirInvoiceMutation.mutate({ avoirId: avoir.id, invoiceReference: avoir.invoiceReference, forceRefresh }); }; // Fonction pour vérifier tous les avoirs avec une référence facture const handleVerifyAllAvoirInvoices = () => { const avoirsToVerify = avoirs.filter(avoir => avoir.invoiceReference?.trim() && (avoir.group?.nocodbTableName || avoir.group?.nocodbConfigId) ); if (avoirsToVerify.length === 0) { toast({ title: "Aucun avoir à vérifier", description: "Aucun avoir avec référence de facture trouvé", }); return; } avoirsToVerify.forEach((avoir, index) => { // Délai échelonné pour éviter la surcharge setTimeout(() => { handleVerifyAvoirInvoice(avoir, true); // Force refresh pour tous }, index * 200); // 200ms entre chaque vérification }); toast({ title: "Vérification lancée", description: `Vérification de ${avoirsToVerify.length} avoir(s) en cours...`, }); }; // ✅ SUPPRIMÉ : Le chargement automatique causait des crashes JavaScript // Les vérifications se font maintenant uniquement à la demande utilisateur // 🔥 FONCTIONS WEBHOOK MODAL (comme rapprochement) const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file && file.type === 'application/pdf') { setSelectedFile(file); } else { toast({ title: "Erreur", description: "Veuillez sélectionner un fichier PDF", variant: "destructive", }); } }; const handleOpenAvoirModal = (avoir: Avoir) => { setSelectedAvoirForUpload(avoir); setSelectedFile(null); setShowAvoirModal(true); }; const handleCloseAvoirModal = () => { setShowAvoirModal(false); setSelectedAvoirForUpload(null); setSelectedFile(null); setIsUploading(false); }; const startProcessingTimer = () => { setProcessingSeconds(0); const interval = setInterval(() => { setProcessingSeconds(prev => prev + 1); }, 1000); setProcessingTimeout(interval); }; const handleCloseWaitingModal = () => { setShowWaitingModal(false); setIsUploading(false); setProcessingSeconds(0); if (processingTimeout) { clearInterval(processingTimeout); setProcessingTimeout(null); } }; const handleSendAvoir = async () => { if (!selectedFile || !selectedAvoirForUpload) { toast({ title: "Erreur", description: "Veuillez sélectionner un fichier PDF", variant: "destructive", }); return; } // Utiliser groupe par défaut (1) pour admin si pas de groupe sélectionné const groupId = selectedAvoirForUpload.groupId || ((user as any)?.role === 'admin' ? 1 : selectedAvoirForUpload.groupId); const groups = await queryClient.fetchQuery({ queryKey: ['/api/groups'] }); const group = (groups as any[]).find(g => g.id === groupId); if (!group?.webhookUrl) { toast({ title: "Erreur", description: "Aucun webhook configuré pour ce magasin", variant: "destructive", }); return; } // Fermer le modal de sélection et ouvrir le modal d'attente setShowAvoirModal(false); setShowWaitingModal(true); setIsUploading(true); startProcessingTimer(); try { const formData = new FormData(); formData.append('file', selectedFile); formData.append('supplier', selectedAvoirForUpload.supplier?.name || ''); formData.append('invoiceReference', selectedAvoirForUpload.invoiceReference || ''); formData.append('type', 'Avoir'); // Créer un AbortController pour gérer le timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 secondes const response = await fetch(group.webhookUrl, { method: 'POST', body: formData, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`Erreur ${response.status}: ${response.statusText}`); } handleCloseWaitingModal(); toast({ title: "Succès", description: "Avoir traité avec succès via le webhook", }); // Vérification automatique de la facture après envoi réussi if (selectedAvoirForUpload.invoiceReference?.trim()) { setTimeout(() => { handleVerifyAvoirInvoice(selectedAvoirForUpload, true); }, 1000); // Délai de 1 seconde pour laisser le temps aux données de se synchroniser } // Reset des données setSelectedAvoirForUpload(null); setSelectedFile(null); // Invalider les caches pour recharger queryClient.invalidateQueries({ predicate: (query) => { return query.queryKey[0]?.toString().includes('/api/avoirs') || false; } }); } catch (error: any) { handleCloseWaitingModal(); let errorMessage = "Impossible d'envoyer l'avoir"; if (error.name === 'AbortError') { errorMessage = "Le traitement a pris trop de temps (timeout de 60 secondes)"; } else if (error.message) { errorMessage = error.message; } toast({ title: "Erreur d'envoi", description: errorMessage, variant: "destructive", }); // Rouvrir le modal de sélection en cas d'erreur setShowAvoirModal(true); } }; // Confirm delete const confirmDelete = () => { if (selectedAvoir) { deleteAvoirMutation.mutate(selectedAvoir.id); } }; // 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

{/* Bouton Vérifier tous si des avoirs avec référence existent */} {avoirs.some(avoir => avoir.invoiceReference?.trim()) && ( )}
Créer un nouvel avoir Remplissez les informations pour créer une nouvelle demande d'avoir.
( Fournisseur )} /> ( Magasin )} /> ( Référence facture (optionnel) )} /> ( Montant (€) (optionnel) field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)} /> )} /> ( Commentaire