# Avoirs
_41 constats vérifiés — 8 haute, 21 moyenne, 12 basse._
## Pages analysées
### `/avoirs (bureau)`
**Rôle :** Suivre les demandes d'avoir fournisseurs d'un magasin : créer une demande, la faire passer de 'En attente de demande' à 'Demandé' puis à 'Reçu', vérifier que la facture d'avoir existe en comptabilité (NocoDB), valider l'avoir, envoyer le PDF au système externe (webhook) et consulter l'historique des avoirs finalisés.
**Tâches principales de l'utilisateur :**
- Créer une demande d'avoir (fournisseur, magasin, référence, montant, commentaire)
- Changer le statut d'un avoir (En attente de demande, Demandé, Reçu)
- Vérifier la présence de la facture d'avoir en comptabilité (loupe ou bouton 'Vérifier toutes')
- Valider ou dévalider un avoir (admin/directeur)
- Envoyer le PDF de l'avoir reçu
- Modifier ou supprimer un avoir (admin/directeur)
- Rechercher un avoir et consulter les onglets En cours / Finalisés
**Appels API :**
| Endpoint | Déclencheur | Handler serveur | Notes |
|---|---|---|---|
| GET /api/user | au montage (useQuery ['/api/user']) | server/localAuth.production.ts:246 (lit req.user en session, sans requête BD) | En production, ce cache react-query n'est jamais rempli par useAuthUnified (enabled: isDevelopment, useAuthUnified.ts:43). La requête est donc refaite, et toutes les autres requêtes de la page l'attendent (enabled: !!user). |
| GET /api/groups | au montage, après /api/user ; puis queryClient.fetchQuery à l'envoi du PDF | server/routes.ts:970 → storage.getUserWithGroups (storage.ts:371, 2 requêtes) + storage.getGroups (storage.ts:432, select * avec le logo base64) pour admin ; sinon userGroups[].group (id, name, color seulement) | N'est utile qu'aux formulaires création/édition. Pour un non-admin, la réponse ne contient pas webhookUrl, ce qui casse l'envoi du PDF. |
| GET /api/suppliers | au montage, après /api/user | server/routes.ts:1141 → storage.getUser + storage.getSuppliers | Liste complète des fournisseurs, utilisée seulement dans les modales. |
| GET /api/avoirs?storeId=X | au montage (après /api/user), puis refetch après chaque création, modification ou suppression (invalidation par prédicat) | server/routes.ts:3711 → storage.getUserWithGroups + storage.getAvoirs (storage.ts:2528 : 3 LEFT JOIN, groups.* complet, aucune limite) | queryKey [avoirsUrl, selectedStoreId, role] différente de celle du mobile. Charge tout l'historique, y compris le logo du magasin répété dans chaque ligne. |
| POST /api/avoirs/:id/verify-invoice | boucle séquentielle dans un useEffect à chaque changement de 'avoirs' (montage et chaque refetch) ; clic sur la loupe ; 'Vérifier toutes' (forceRefresh, toutes les 200 ms) ; 1 s après l'envoi du PDF | server/routes.ts:3982 → getUserWithGroups + storage.getAvoir (storage.ts:2575) + invoiceVerificationService.verifyInvoice (invoiceVerification.ts:249 → getInvoiceVerificationCache, getGroup, getActiveNocodbConfig, appel HTTP NocoDB, saveInvoiceVerificationCache) | N appels séquentiels à chaque affichage. Si le cache a expiré (6 h ou 12 h), chaque appel va jusqu'à NocoDB. |
| POST /api/avoirs | clic 'Créer' | server/routes.ts:3774 → getUserWithGroups, createAvoir (storage.ts:2619), getGroup, fetch webhook attendu sans timeout, updateAvoirWebhookStatus | Réponse bloquée tant que le webhook externe n'a pas répondu. supplierName codé en dur à 'Unknown'. |
| PUT /api/avoirs/:id | modale Modifier ; changement de statut dans le menu de la ligne ; remplissage automatique du montant après une vérification réussie | server/routes.ts:3851 → getUserWithGroups, getAvoir, updateAvoir (storage.ts:2624), puis getGroup et webhook si passage à 'Reçu' | 3 logs JSON complets par appel. Les champs non envoyés sont remis à null. |
| DELETE /api/avoirs/:id | confirmation de suppression | server/routes.ts:3945 → getUserWithGroups, getAvoir, deleteAvoir (storage.ts:2633) | Réservé à admin et directeur. |
| PUT /api/avoirs/:id/nocodb-verification | clic Valider / Dévalider | server/routes.ts:4083 → getUserWithGroups, updateAvoirNocodbVerification (storage.ts:2644), getAvoir, updateCacheAsReconciled | Réservé à admin et directeur, mais le bouton est affiché à tous les rôles. L'invalidation côté client ne correspond pas à la queryKey quand un magasin est sélectionné. |
| POST /api/cache/mark-reconciled | juste après un Valider réussi | server/routes.ts:4126 → invoiceVerificationService.updateCacheAsReconciled | Redondant : la route nocodb-verification marque déjà le cache comme réconcilié. |
| POST {group.webhookUrl} (URL externe) | clic 'Envoyer' dans la modale d'envoi du PDF | aucun : appel direct depuis le navigateur | Dépend du CORS du service externe. La page BLReconciliation passe, elle, par le proxy /api/reconciliation/send-invoice (routes.ts:883). |
**Lisibilité / simplicité :** La page est dense et ambiguë pour un utilisateur non technicien. Le tableau a 8 colonnes. Chaque ligne contient un menu de statut modifiable directement, sans confirmation, alors qu'un passage à 'Reçu' déclenche un webhook externe, et la même ligne affiche parfois 'Lecture seule'. La colonne Actions aligne jusqu'à 6 boutons-icônes de couleurs différentes, sans texte (seulement un title au survol), et la colonne Vérification se réduit à une loupe de 16 px. Le badge vert 'Validé' apparaît dès que la facture est trouvée, alors que l'avoir n'est pas validé et que le bouton 'Valider' est toujours proposé, ce qui rend le flux principal contradictoire. Tout l'onglet Finalisés est grisé (opacity-75), donc peu lisible. Les compteurs sont affichés trois fois (en-tête, onglets, carte). Plusieurs messages emploient du jargon (NocoDB, webhook) ou sont en anglais ('Failed to create avoir', '403: {"message":...}'). Formats peu soignés : '#Sans référence', '12.50 €' avec un point. Le formulaire de création n'est pas réinitialisé après envoi et ignore le magasin sélectionné en haut. Le PDF ne peut pas être envoyé par un directeur ou un employé. Côté performance, chaque affichage déclenche une cascade (/api/user, puis 3 requêtes, puis N POST de vérification séquentiels). 'Vérifier toutes' produit O(N²) requêtes et N toasts. De nombreux console.log de debug restent actifs en production.
### `/avoirs (mobile)`
**Rôle :** Version téléphone du suivi des avoirs : voir la liste des avoirs du magasin sélectionné, en créer un rapidement et changer leur statut ou les supprimer.
**Tâches principales de l'utilisateur :**
- Consulter les avoirs du magasin sélectionné
- Rechercher par fournisseur ou référence de facture
- Créer une demande d'avoir (feuille du bas)
- Changer le statut via le menu '⋮'
- Supprimer un avoir
**Appels API :**
| Endpoint | Déclencheur | Handler serveur | Notes |
|---|---|---|---|
| GET /api/user | au montage, via useAuthUnified dans la page et dans MobileLayout (fetch direct no-cache en production) | server/localAuth.production.ts:246 | 2 requêtes supplémentaires à chaque ouverture de la page. La liste attend l'une d'elles (enabled: !!user). |
| GET /api/avoirs?storeId=X | au montage, seulement si un magasin est sélectionné et l'utilisateur chargé | server/routes.ts:3711 → getUserWithGroups + storage.getAvoirs (storage.ts:2528) | Avec 'Tous les magasins', la requête n'est pas lancée et la page affiche 'Aucun avoir trouvé'. queryKey ['/api/avoirs', storeId] différente de celle du bureau. |
| GET /api/suppliers | au montage | server/routes.ts:1141 → storage.getUser + storage.getSuppliers | Utile seulement quand la feuille de création est ouverte. |
| GET /api/groups | au montage | server/routes.ts:970 → getUserWithGroups (+ getGroups pour admin) | Même clé que MobileApp, donc dédoublonnée, mais inutile : StoreContext fournit déjà 'stores'. |
| POST /api/avoirs | clic 'Créer la demande' | server/routes.ts:3774 → createAvoir + webhook synchrone | Montant envoyé à 0 par défaut. |
| PUT /api/avoirs/:id | menu '⋮' > Marquer En attente / Demandé / Reçu | server/routes.ts:3851 → getAvoir, updateAvoir, webhook si 'Reçu' | Envoie l'objet avoir complet, relations comprises (group avec le logo base64). Aucun onError. |
| DELETE /api/avoirs/:id | menu '⋮' > Supprimer (sans confirmation) | server/routes.ts:3945 → deleteAvoir | Erreur 403 pour manager et employé, avalée sans message. |
**Lisibilité / simplicité :** La présentation en cartes est plus lisible que le tableau du bureau. En revanche : la liste est vide avec le message trompeur 'Aucun avoir trouvé' quand 'Tous les magasins' est sélectionné. La suppression se fait d'un tap, sans confirmation, et est proposée à tous les rôles. Les échecs (403) sont silencieux. Le menu de statut propose toujours les 3 états, sans indiquer l'état courant et sans les restrictions du bureau (manager vers 'Reçu', avoirs validés). Le formulaire n'affiche aucune erreur de validation et force le montant à 0 €. Les libellés, couleurs et statuts diffèrent du bureau ('Suivi' contre 'Gestion', indigo contre bleu, 'En attente' contre 'En attente de demande', 'Vérifié Compta' contre 'Validé'). Il n'y a pas de séparation En cours / Finalisés, la date s'affiche sans l'année, et le chargement utilise un spinner alors que le bureau utilise un skeleton.
## Constats
| ID | Sév. | Catégorie | Effort | Titre | Fichier |
|---|---|---|---|---|---|
| [AVOIRS-01](#avoirs-01) | haute | perf-api | M | Boucle séquentielle de N appels POST verify-invoice à chaque chargement de la liste | `client/src/pages/Avoirs.tsx:192` |
| [AVOIRS-02](#avoirs-02) | haute | perf-api | M | « Vérifier toutes » déclenche une cascade O(N²) de requêtes, N toasts et ferme la modale d'édition | `client/src/pages/Avoirs.tsx:689` |
| [AVOIRS-03](#avoirs-03) | haute | bug | S | Après Valider ou Dévalider, la liste n'est pas rafraîchie quand un magasin est sélectionné | `client/src/pages/Avoirs.tsx:528` |
| [AVOIRS-04](#avoirs-04) | haute | bug | M | Envoi du PDF impossible pour directeur et employé, et appel direct au webhook externe depuis le navigateur | `client/src/pages/Avoirs.tsx:776` |
| [AVOIRS-05](#avoirs-05) | haute | ux-simplicite | S | Badge « Validé » affiché alors que l'avoir n'est pas validé (la facture a seulement été trouvée) | `client/src/pages/Avoirs.tsx:1037` |
| [AVOIRS-API-01](#avoirs-api-01) | haute | perf-api | S | La liste renvoie la ligne complète du magasin (logo base64, SMTP, webhook) dans chaque avoir | `server/storage.ts:2546` |
| [MOB-AVOIRS-01](#mob-avoirs-01) | haute | bug | S | Liste vide « Aucun avoir trouvé » quand « Tous les magasins » est sélectionné | `client/src/pages/mobile/AvoirsPage.tsx:83` |
| [MOB-AVOIRS-02](#mob-avoirs-02) | haute | ux-simplicite | S | Suppression sans confirmation, proposée à tous les rôles, avec échec silencieux | `client/src/pages/mobile/AvoirsPage.tsx:257` |
| [AVOIRS-06](#avoirs-06) | moyenne | ux-simplicite | M | Statut modifié immédiatement par un menu déroulant dans chaque ligne, sans confirmation, avec effet externe | `client/src/pages/Avoirs.tsx:994` |
| [AVOIRS-07](#avoirs-07) | moyenne | ux-simplicite | M | Jusqu'à 6 boutons-icônes sans texte par ligne et une loupe de 16 px | `client/src/pages/Avoirs.tsx:1060` |
| [AVOIRS-08](#avoirs-08) | moyenne | lisibilite | S | Onglet « Finalisés » entièrement grisé et semi-transparent | `client/src/pages/Avoirs.tsx:989` |
| [AVOIRS-09](#avoirs-09) | moyenne | lisibilite | S | Messages techniques (NocoDB, webhook) et erreurs brutes en anglais affichés à l'utilisateur | `client/src/pages/Avoirs.tsx:666` |
| [AVOIRS-10](#avoirs-10) | moyenne | bug | S | Bouton « Valider » proposé à des rôles que le serveur refuse | `client/src/pages/Avoirs.tsx:1076` |
| [AVOIRS-11](#avoirs-11) | moyenne | perf-client | S | Requête /api/user redondante qui bloque toutes les autres (cascade) | `client/src/pages/Avoirs.tsx:147` |
| [AVOIRS-12](#avoirs-12) | moyenne | bug | S | Formulaire de création non réinitialisé, et magasin par défaut qui ignore le magasin sélectionné | `client/src/pages/Avoirs.tsx:263` |
| [AVOIRS-13](#avoirs-13) | moyenne | perf-client | S | Filtrages non mémoïsés et code mort exécuté à chaque frappe, avec un risque de plantage | `client/src/pages/Avoirs.tsx:871` |
| [AVOIRS-14](#avoirs-14) | moyenne | perf-client | M | Aucune pagination : tout l'historique est rendu avec un Select Radix par ligne | `client/src/pages/Avoirs.tsx:988` |
| [AVOIRS-15](#avoirs-15) | moyenne | dette-code | S | console.log de débogage actifs en production dans les chemins chauds | `client/src/pages/Avoirs.tsx:168` |
| [AVOIRS-API-02](#avoirs-api-02) | moyenne | perf-api | M | Ni pagination ni filtre de statut en SQL : la séparation En cours / Finalisés se fait en JavaScript | `server/storage.ts:2561` |
| [AVOIRS-API-03](#avoirs-api-03) | moyenne | perf-serveur | S | Logs JSON volumineux à chaque création, modification et vérification | `server/routes.ts:3874` |
| [AVOIRS-API-04](#avoirs-api-04) | moyenne | perf-api | S | Création et modification bloquées par l'appel au webhook externe, sans timeout | `server/routes.ts:3821` |
| [AVOIRS-API-05](#avoirs-api-05) | moyenne | bug | S | Le webhook avoir est envoyé avec un nom de fournisseur factice | `server/routes.ts:3813` |
| [AVOIRS-API-06](#avoirs-api-06) | moyenne | bug | S | Le PUT efface montant, référence et commentaire s'ils ne sont pas envoyés, ce qui oblige à renvoyer tout l'objet | `server/routes.ts:3879` |
| [AVOIRS-API-07](#avoirs-api-07) | moyenne | bug | S | Règles métier appliquées seulement côté client (statut « Reçu », avoirs validés, changement de magasin) | `server/routes.ts:3866` |
| [MOB-AVOIRS-03](#mob-avoirs-03) | moyenne | ux-simplicite | S | Menu de statut sans indication de l'état courant ni restrictions | `client/src/pages/mobile/AvoirsPage.tsx:239` |
| [MOB-AVOIRS-04](#mob-avoirs-04) | moyenne | perf-api | S | Le changement de statut envoie l'objet avoir complet avec ses relations | `client/src/pages/mobile/AvoirsPage.tsx:241` |
| [MOB-AVOIRS-05](#mob-avoirs-05) | moyenne | bug | S | Montant forcé à 0 € lors d'une création sur mobile | `client/src/pages/mobile/AvoirsPage.tsx:135` |
| [MOB-AVOIRS-06](#mob-avoirs-06) | moyenne | ux-simplicite | S | Erreurs de validation invisibles dans le formulaire mobile | `client/src/pages/mobile/AvoirsPage.tsx:46` |
| [MOB-AVOIRS-07](#mob-avoirs-07) | moyenne | perf-client | M | Requêtes /api/user répétées et cascade avant le chargement des avoirs | `client/src/pages/mobile/AvoirsPage.tsx:72` |
| [AVOIRS-16](#avoirs-16) | basse | ux-simplicite | S | Compteurs affichés trois fois et deux composants Tabs séparés | `client/src/pages/Avoirs.tsx:1168` |
| [AVOIRS-17](#avoirs-17) | basse | lisibilite | S | Formats incohérents : « #Sans référence », montants « 12.50 € », couleur CSS invalide | `client/src/pages/Avoirs.tsx:1012` |
| [AVOIRS-18](#avoirs-18) | basse | bug | S | La règle « un admin peut modifier un avoir validé » est inaccessible depuis le menu de statut | `client/src/pages/Avoirs.tsx:997` |
| [AVOIRS-19](#avoirs-19) | basse | perf-api | S | Double marquage du cache lors de la validation, et validation hors useMutation | `client/src/pages/Avoirs.tsx:510` |
| [AVOIRS-20](#avoirs-20) | basse | bug | S | Timer du modal d'attente jamais nettoyé, et modal fermable pendant l'envoi | `client/src/pages/Avoirs.tsx:746` |
| [AVOIRS-21](#avoirs-21) | basse | dette-code | S | Code mort, imports inutilisés et typage faible | `client/src/pages/Avoirs.tsx:882` |
| [AVOIRS-22](#avoirs-22) | basse | perf-bundle | S | Pages Avoirs (bureau et mobile) chargées dans le bundle initial | `client/src/components/RouterProduction.tsx:26` |
| [AVOIRS-API-08](#avoirs-api-08) | basse | bug | S | Périmètre du directeur incohérent d'une route avoirs à l'autre | `server/routes.ts:3721` |
| [AVOIRS-API-09](#avoirs-api-09) | basse | perf-serveur | S | verify-invoice recharge des données déjà disponibles et ne met pas en cache les résultats négatifs | `server/invoiceVerification.ts:285` |
| [MOB-AVOIRS-08](#mob-avoirs-08) | basse | perf-client | S | Données de référence chargées inutilement et tri redondant | `client/src/pages/mobile/AvoirsPage.tsx:91` |
| [MOB-AVOIRS-09](#mob-avoirs-09) | basse | coherence-design | M | Libellés, couleurs et statuts différents de la version bureau | `client/src/pages/mobile/AvoirsPage.tsx:180` |
| [MOB-AVOIRS-10](#mob-avoirs-10) | basse | dette-code | S | Imports inutilisés, typage any et schéma dupliqué | `client/src/pages/mobile/AvoirsPage.tsx:19` |
### AVOIRS-01
**Boucle séquentielle de N appels POST verify-invoice à chaque chargement de la liste** — perf-api, sévérité haute, effort M
- **Fichier :** `client/src/pages/Avoirs.tsx:192`
- **Constat :** useEffect(() => { const loadCachedVerifications = async () => { ... for (const avoir of avoirs) { if (avoir.invoiceReference?.trim()) { const result = await apiRequest(`/api/avoirs/${avoir.id}/verify-invoice`, 'POST', { invoiceReference: avoir.invoiceReference, forceRefresh: false }); ... } } }; loadCachedVerifications(); }, [avoirs]); (l.192-245). Le commentaire de la l.716 ('✅ SUPPRIMÉ : Le chargement automatique causait des crashes JavaScript') est contredit : la boucle est toujours active. Les avoirs déjà validés (nocodbVerified) ne sont pas exclus. Côté serveur, chaque appel exécute getUserWithGroups (2 requêtes), getAvoir (3 JOIN) et une lecture du cache. Le cache expire au bout de 6 h ou 12 h (invoiceVerification.ts:101-108), après quoi l'appel va jusqu'à NocoDB. Les magasins sans configuration ne sont jamais mis en cache (invoiceVerification.ts:302-309).
- **Impact :** Avec 100 avoirs ayant une référence : 100 allers-retours successifs (plusieurs secondes à plusieurs dizaines de secondes de trafic) après chaque affichage. Chaque refetch (après toute création, modification ou suppression) relance la boucle entière. Les badges 'Validé' et les boutons 'Valider' apparaissent au compte-gouttes. Le serveur est chargé et le quota NocoDB consommé le matin, à l'expiration du cache.
- **Recommandation :** Supprimer ce useEffect. Renvoyer l'état de vérification avec la liste : dans storage.getAvoirs, ajouter un LEFT JOIN invoice_verification_cache ON cache_key = avoirs.group_id || '_' || lower(trim(avoirs.invoice_reference)) AND expires_at > now(), exposé sous la forme verification {exists, invoiceAmount, isReconciled}. À défaut, créer un endpoint groupé POST /api/avoirs/verifications {ids} qui lit uniquement le cache (un seul SELECT ... WHERE cache_key IN (...)) sans jamais appeler NocoDB, consommé par un useQuery. Exclure les avoirs nocodbVerified.
### AVOIRS-02
**« Vérifier toutes » déclenche une cascade O(N²) de requêtes, N toasts et ferme la modale d'édition** — perf-api, sévérité haute, effort M
- **Fichier :** `client/src/pages/Avoirs.tsx:689`
- **Constat :** avoirsToVerify.forEach((avoir, index) => { setTimeout(() => { handleVerifyAvoirInvoice(avoir, true); }, index * 200); }); (l.703-708). forceRefresh=true contourne le cache, d'où un appel NocoDB par avoir, y compris pour les avoirs déjà validés (le filtre des l.690-693 n'exclut pas nocodbVerified). En cas de succès : editAvoirMutation.mutate({ id: variables.avoirId, data: { ..., amount: result.invoiceAmount, ... } }) (l.601-612). Son onSuccess invalide toutes les clés '/api/avoirs' (l.303-307), affiche le toast 'Avoir modifié' et exécute setIsEditDialogOpen(false); setSelectedAvoir(null) (l.308-313). Le refetch produit un nouveau tableau 'avoirs', ce qui relance la boucle d'AVOIRS-01 (N POST).
- **Impact :** Pour N avoirs : N appels NocoDB, N PUT, N GET /api/avoirs et jusqu'à N×N POST verify-invoice. L'utilisateur reçoit N toasts 'Avoir modifié'. Si une modale d'édition est ouverte, elle se ferme toute seule. Le montant saisi est remplacé sans prévenir par le montant HT de la facture.
- **Recommandation :** Créer un endpoint serveur POST /api/avoirs/verify-all, limité aux groupes accessibles et excluant les avoirs nocodbVerified. Il traite les vérifications en parallèle avec une concurrence limitée (4) et ne remplit le montant côté serveur que s'il est vide. Côté client : une seule mutation suivie d'une seule invalidation. En attendant, utiliser une mutation de remplissage automatique séparée, sans toast ni fermeture de modale, qui met à jour la ligne avec queryClient.setQueryData au lieu d'invalider la liste.
### AVOIRS-03
**Après Valider ou Dévalider, la liste n'est pas rafraîchie quand un magasin est sélectionné** — bug, sévérité haute, effort S
- **Fichier :** `client/src/pages/Avoirs.tsx:528`
- **Constat :** La queryKey est [avoirsUrl, selectedStoreId, (user as any)?.role] avec avoirsUrl = `/api/avoirs${selectedStoreId ? `?storeId=${selectedStoreId}` : ''}` (l.164-166). handleValidateAvoir et handleDevalidateAvoir appellent queryClient.invalidateQueries({ queryKey: ['/api/avoirs'] }) (l.528 et l.560). TanStack compare les clés élément par élément : '/api/avoirs' ≠ '/api/avoirs?storeId=3', donc aucune requête n'est invalidée. Les autres mutations utilisent un prédicat includes('/api/avoirs') (l.265-269), qui fonctionne.
- **Impact :** Dès qu'un magasin est sélectionné (cas le plus courant), un avoir validé reste dans l'onglet 'En cours'. Après 'Dévalider', l'état local est supprimé (l.551-555) mais avoir.nocodbVerified reste à true dans le cache : la ligne affiche toujours 'Validé' et reste dans 'Finalisés' jusqu'au rechargement de la page. L'utilisateur croit que l'action a échoué et recommence.
- **Recommandation :** Correction auto limitée au minimum : remplacer aux l.528 et 560 par queryClient.invalidateQueries({ predicate: q => q.queryKey[0]?.toString().includes('/api/avoirs') || false }). La normalisation de la clé ['/api/avoirs', {storeId}] avec un queryFn dédié (bureau et mobile) se fait à part, sans automatisation.
### AVOIRS-04
**Envoi du PDF impossible pour directeur et employé, et appel direct au webhook externe depuis le navigateur** — bug, sévérité haute, effort M
- **Fichier :** `client/src/pages/Avoirs.tsx:776`
- **Constat :** const groups = await queryClient.fetchQuery({ queryKey: ['/api/groups'] }); const group = (groups as any[]).find(g => g.id === groupId); if (!group?.webhookUrl) { toast({ ... description: 'Aucun webhook configuré pour ce magasin' }) } (l.776-786). Pour un non-admin, GET /api/groups renvoie (user as any).userGroups?.map((ug: any) => ug.group) (server/routes.ts:983). Ces groupes viennent de getUserWithGroups, qui ne sélectionne que g.id, g.name, g.color, created_at et updated_at (server/storage.ts:376-399) : webhookUrl est donc toujours undefined. Le bouton Upload est affiché si role !== 'manager' (l.1063), donc aux directeurs et aux employés. L'envoi se fait ensuite par fetch(group.webhookUrl, { method: 'POST', body: formData }) (l.805) directement depuis le navigateur, alors que BLReconciliation passe par le proxy '/api/reconciliation/send-invoice' (BLReconciliation.tsx:587-592, routes.ts:883), justement pour les « problèmes CORS ».
- **Impact :** Pour un directeur ou un employé, le bouton 'Envoyer fichier avoir' échoue toujours avec un message incompréhensible. Pour un admin, l'envoi dépend du CORS du service externe et l'URL du webhook est visible dans le navigateur.
- **Recommandation :** Créer POST /api/avoirs/:id/send-file. Le serveur vérifie l'accès au groupe de l'avoir, lit webhookUrl via storage.getGroup(avoir.groupId) et relaie le fichier (même parseur multipart que send-invoice). Ne pas réutiliser send-invoice, qui fait confiance à une URL fournie par le client. Décider explicitement si les employés peuvent envoyer ce fichier.
### AVOIRS-05
**Badge « Validé » affiché alors que l'avoir n'est pas validé (la facture a seulement été trouvée)** — ux-simplicite, sévérité haute, effort S
- **Fichier :** `client/src/pages/Avoirs.tsx:1037`
- **Constat :** {(avoirVerificationResults[avoir.id]?.exists === true || avoir.nocodbVerified) ? (
Validé
) (l.1037-1041). Dans la même ligne : {avoirVerificationResults[avoir.id]?.exists === true && !avoir.nocodbVerified && ()} (l.1076-1086).
- **Impact :** L'utilisateur voit 'Validé' en vert et, en même temps, un bouton vert ✓ 'Valider'. Il pense que le travail est fait, et l'avoir ne passe jamais dans 'Finalisés'. C'est le flux principal de la page qui devient ambigu.
- **Recommandation :** Afficher 3 états explicites avec du texte : 'Non vérifié' (gris, avec un bouton 'Vérifier'), 'Facture trouvée – à valider' (bleu ou orange, avec un bouton texte 'Valider'), et 'Validé' (vert) uniquement si avoir.nocodbVerified.
### AVOIRS-API-01
**La liste renvoie la ligne complète du magasin (logo base64, SMTP, webhook) dans chaque avoir** — perf-api, sévérité haute, effort S
- **Fichier :** `server/storage.ts:2546`
- **Constat :** getAvoirs sélectionne supplier: suppliers, group: groups, creator: { id, firstName, lastName, username, email: users.email } (l.2546-2554) avec .leftJoin(groups, eq(avoirs.groupId, groups.id)). La table groups contient logo: text('logo'), // Logo en data URI (data:image/png;base64,...) (shared/schema.ts:67), smtpHost, smtpUser et smtpSenderEmail (l.70-76), webhookUrl (l.63), address et phone. getAvoir fait de même (l.2593-2594) ; il est appelé par chaque PUT, DELETE et verify-invoice. Le middleware global stripSmtpPassword (server/routes.ts:168-174, server/sanitize.ts:13-45) parcourt ensuite récursivement tous les objets de la réponse.
- **Impact :** Le logo (souvent plusieurs dizaines à centaines de Ko) est répété dans chaque avoir : environ 200 avoirs × 100 Ko ≈ 20 Mo de JSON pour une seule liste, soit un chargement très lent, surtout en 4G. Sérialisation et nettoyage récursif consomment du CPU. webhookUrl et la configuration SMTP sont exposés aux employés et aux managers.
- **Recommandation :** Projection : group {id, name, color, nocodbTableName, nocodbConfigId}, supplier {id, name}, creator {id, firstName, lastName, username}, dans getAvoirs et getAvoir. Mettre à jour AvoirWithRelations (ou un type dédié) pour que tsc passe. MemStorage peut rester tel quel.
### MOB-AVOIRS-01
**Liste vide « Aucun avoir trouvé » quand « Tous les magasins » est sélectionné** — bug, sévérité haute, effort S
- **Fichier :** `client/src/pages/mobile/AvoirsPage.tsx:83`
- **Constat :** queryFn: async () => { if (!selectedStoreId) return []; ... }, enabled: !!selectedStoreId && !!user (l.82-88). MobileLayout propose 'Tous les magasins' (MobileLayout.tsx:28, 40-46), et MobileApp ne présélectionne un magasin que s'il n'y en a qu'un (MobileApp.tsx:37). L'état vide affiche
Aucun avoir trouvé
(l.200-204).
- **Impact :** Un admin ou un directeur multi-magasins ouvre la page et lit 'Aucun avoir trouvé' alors que des avoirs existent. Il croit qu'il n'y en a pas.
- **Recommandation :** Appeler /api/avoirs sans storeId quand aucun magasin n'est sélectionné (le serveur filtre déjà selon le rôle, routes.ts:3721-3733). À défaut, afficher 'Choisissez un magasin en haut de l'écran pour voir ses avoirs'.
### MOB-AVOIRS-02
**Suppression sans confirmation, proposée à tous les rôles, avec échec silencieux** — ux-simplicite, sévérité haute, effort S
- **Fichier :** `client/src/pages/mobile/AvoirsPage.tsx:257`
- **Constat :** deleteMutation.mutate(avoir.id)} className='text-red-600'> (l.257-263). deleteMutation et updateStatusMutation n'ont pas d'onError (l.113-127). Le serveur refuse la suppression aux rôles autres qu'admin et directeur (routes.ts:3960-3962).
- **Impact :** Sur téléphone, un tap malheureux supprime définitivement un avoir pour un admin. Pour un manager ou un employé, rien ne se passe et aucun message n'apparaît.
- **Recommandation :** Ajouter une confirmation par AlertDialog. Masquer 'Supprimer' pour les rôles autres qu'admin et directeur. Ajouter un onError qui affiche un toast en français.
### AVOIRS-06
**Statut modifié immédiatement par un menu déroulant dans chaque ligne, sans confirmation, avec effet externe** — ux-simplicite, sévérité moyenne, effort M
- **Fichier :** `client/src/pages/Avoirs.tsx:994`
- **Constat :**