From 3f2660c506e62620df767dfc4f4542cb419e8ec4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 00:27:29 +0000 Subject: [PATCH] =?UTF-8?q?docs(audit):=20analyse=20compl=C3=A8te=20du=20w?= =?UTF-8?q?ebUI=20page=20par=20page=20(620=20constats=20v=C3=A9rifi=C3=A9s?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Annexe générée par un audit multi-agents : pour chaque page, rôle, tâches utilisateur, appels API tracés jusqu'au handler serveur, et constats UX / lisibilité / performance / bugs avec preuve fichier:ligne, vérifiés de façon contradictoire. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MsDJjQrAggcJwbbBtKhgyb --- docs/audit-webui/01-dashboard.md | 569 ++++++++++++++ .../02-calendar-orders-deliveries.md | 733 ++++++++++++++++++ docs/audit-webui/03-reconciliation-payment.md | 463 +++++++++++ docs/audit-webui/04-avoirs.md | 476 ++++++++++++ docs/audit-webui/05-tasks.md | 434 +++++++++++ docs/audit-webui/06-sav.md | 441 +++++++++++ docs/audit-webui/07-customer-orders.md | 492 ++++++++++++ docs/audit-webui/08-dlc.md | 596 ++++++++++++++ docs/audit-webui/09-publicities.md | 401 ++++++++++ docs/audit-webui/10-users-groups.md | 492 ++++++++++++ .../11-suppliers-contacts-analytics.md | 690 +++++++++++++++++ docs/audit-webui/12-admin-utilities-auth.md | 666 ++++++++++++++++ docs/audit-webui/13-app-shell.md | 381 +++++++++ docs/audit-webui/14-server-infra.md | 257 ++++++ docs/audit-webui/15-database.md | 655 ++++++++++++++++ 15 files changed, 7746 insertions(+) create mode 100644 docs/audit-webui/01-dashboard.md create mode 100644 docs/audit-webui/02-calendar-orders-deliveries.md create mode 100644 docs/audit-webui/03-reconciliation-payment.md create mode 100644 docs/audit-webui/04-avoirs.md create mode 100644 docs/audit-webui/05-tasks.md create mode 100644 docs/audit-webui/06-sav.md create mode 100644 docs/audit-webui/07-customer-orders.md create mode 100644 docs/audit-webui/08-dlc.md create mode 100644 docs/audit-webui/09-publicities.md create mode 100644 docs/audit-webui/10-users-groups.md create mode 100644 docs/audit-webui/11-suppliers-contacts-analytics.md create mode 100644 docs/audit-webui/12-admin-utilities-auth.md create mode 100644 docs/audit-webui/13-app-shell.md create mode 100644 docs/audit-webui/14-server-infra.md create mode 100644 docs/audit-webui/15-database.md diff --git a/docs/audit-webui/01-dashboard.md b/docs/audit-webui/01-dashboard.md new file mode 100644 index 0000000..c506ec8 --- /dev/null +++ b/docs/audit-webui/01-dashboard.md @@ -0,0 +1,569 @@ +# Tableau de bord + +_46 constats vérifiés — 9 haute, 23 moyenne, 14 basse._ + +## Pages analysées + +### `/ et /dashboard (desktop, largeur >= 768px)` + +**Rôle :** Page d'accueil après connexion. Elle donne une vue d'ensemble du magasin : alertes DLC, 4 compteurs (livraisons du mois, commandes en attente, délai moyen, palettes), commandes en attente, livraisons prévues, publicités à venir, informations internes et tâches à faire. + +**Tâches principales de l'utilisateur :** +- Voir ce qu'il faut traiter aujourd'hui (produits DLC expirés ou proches, commandes en retard, livraisons attendues) +- Lire les informations / annonces publiées par l'administration +- (Admin) créer, modifier ou supprimer une information +- Marquer un produit DLC comme 'stock épuisé' depuis la fenêtre d'alerte, ou reporter l'alerte de 2 h +- Consulter les tâches ouvertes du magasin + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/user (x5 : RouterProduction, Layout, Sidebar via useAuthSimple, Dashboard, AnnouncementCard) | au montage de chaque composant (fetch dans un useEffect, cache: 'no-cache', non partagé) | server/localAuth.ts:204 (dev) / server/localAuth.production.ts:246 ; req.user vient de deserializeUser -> storage.getUserWithGroups (storage.ts:371) | 5 appels identiques. En production, chaque instance démarre avec user=null, ce qui fausse les requêtes du Dashboard lancées au montage. | +| GET /api/groups | au montage du Layout (enabled: !!user) | server/routes.ts:970 -> storage.getUserWithGroups + storage.getGroups (storage.ts:432) | Renvoie toutes les colonnes de groups (logo base64, config NocoDB/SMTP). | +| GET /api/weather/current | au montage du WeatherWidget (en-tête), puis refetchInterval de 30 min | server/routes.ts:5671 -> getWeatherSettings (storage.ts:2891), getWeatherData x2 (storage.ts:2910), puis API Visual Crossing si la donnée manque | Lectures BD et appels externes séquentiels, sans timeout. Rien n'est mis en cache quand l'historique N-1 échoue. | +| GET /api/stats/monthly?year&month[&storeId si admin] | au montage | server/routes.ts:4155 -> storage.getMonthlyStats (storage.ts:1222), 4 requêtes séquentielles | Résultat 'stats' jamais affiché : requête inutile. | +| GET /api/stats/yearly?year[&storeId si admin] | au montage | server/routes.ts:4192 -> storage.getYearlyStats (storage.ts:1364), 4 requêtes séquentielles | Seul averageDeliveryTime est utilisé. Au montage, la requête part sans storeId (user=null) et le résultat reste en cache. | +| GET /api/orders[?storeId si admin] | au montage, puis une 2e fois quand user est chargé (admin avec magasin sélectionné : la clé change) | server/routes.ts:1343 -> storage.getOrders (storage.ts:678) + loadDeliveriesByOrderIds (storage.ts:522) | Tout l'historique, sans limite, avec les livraisons imbriquées et les objets supplier/group complets. | +| GET /api/deliveries[?storeId si admin] | au montage, puis une 2e fois (même cas que /api/orders) | server/routes.ts:1752 -> storage.getDeliveries (storage.ts:885) + attachOrdersAndCommentCounts | Tout l'historique, alors que la page n'affiche que 2 compteurs et 4 lignes. | +| GET /api/customer-orders[?storeId si admin] | au montage | server/routes.ts:3486 -> storage.getCustomerOrders (storage.ts:1868) | customerOrderStats est calculé mais jamais affiché : requête inutile. | +| GET /api/announcements?recent=true[&storeId] | au montage, après le /api/user propre au Dashboard (enabled: !!user) | server/routes.ts:5770 (requête Drizzle inline + N+1 sur users/groups, l. 5815-5866) | Paramètre 'recent' ignoré par le serveur. Filtre de 2 jours fait côté client. | +| GET /api/announcements[?storeId] | au montage d'AnnouncementCard, après son propre /api/user | server/routes.ts:5770 | Doublon de l'appel précédent sous une autre queryKey. | +| GET /api/ad-campaigns?year=N-1, N, N+1 | au montage, 3 appels séquentiels (boucle for + await) | server/routes.ts:4595 -> storage.getPublicities (storage.ts:1502) | Télécharge 3 années complètes pour en garder 3 lignes ; groupIds est ignoré côté storage. | +| GET /api/dlc-products/stats?storeId | au montage, plus une invalidation forcée à chaque montage (useEffect) | server/routes.ts:2831 -> storage.getDlcStats (storage.ts:2224) | Agrégat SQL correct, mais définitions incohérentes avec la liste. | +| GET /api/dlc-products?status=expires\|expires_soon&storeId | à l'ouverture de la modale DLC (enabled: isOpen && compteur > 0) | server/routes.ts:2797 -> storage.getDlcProducts (storage.ts:1976) | Liste complète sans LIMIT alors que 8 lignes sont affichées. | +| GET /api/tasks[?storeId si admin] | au montage | server/routes.ts:3193 -> storage.getTasks (storage.ts:2267) | Renvoie [] pour tout non-admin sans storeId (routes.ts:3237-3249). Historique complet, tâches terminées comprises. | +| PUT /api/dlc-products/:id/stock-epuise | au clic sur 'Stock épuisé' dans la modale DLC | server/routes.ts:3023 | Invalide /api/dlc-products et /api/dlc-products/stats. | +| POST/PUT/DELETE /api/announcements[/:id] | au clic (admin) dans AnnouncementCard | server/routes.ts:5895 / 5976 / 6027 | La suppression part sans confirmation. L'invalidation ['/api/announcements'] ne touche pas la clé '/api/announcements/recent'. | + +**Lisibilité / simplicité :** Page dense : 2 bandeaux DLC, 4 compteurs, 5 cartes et jusqu'à 2 modales automatiques. Rien n'est cliquable : ni compteurs, ni lignes, ni bandeaux, alors que les cartes ont un effet d'ombre au survol. Il n'y a ni état de chargement ni état d'erreur : pendant le chargement, l'utilisateur voit '0', 'Aucune commande en attente' ou 'Toutes les tâches sont terminées', ce qui est faux. Du texte de debug est visible ('(API: NOT_ARRAY)', '(N livraisons totales)'). Certains libellés sont ambigus ('Total palettes', délai calculé sur l'année sans le dire, numéro de publicité affiché à la place de sa désignation). Le badge de jours en attente peut être négatif. Les badges de priorité ont tous la même couleur. La bordure colorée des lignes est invisible (classe border-l-3 inexistante). La modale DLC ne peut pas se fermer avec la croix : elle se rouvre aussitôt. Elle peut aussi s'empiler sur la modale d'annonce. Pour les managers et directeurs, les données ne suivent pas le magasin sélectionné : tâches toujours vides, commandes du 1er magasin, statistiques agrégées sur tous leurs magasins. + +### `/ et /dashboard (mobile, largeur < 768px)` + +**Rôle :** Accueil simplifié sur téléphone : salutation et date, 4 compteurs (commandes, livraisons, tâches en cours, tâches urgentes), accès rapides et 3 tâches à faire. + +**Tâches principales de l'utilisateur :** +- Voir d'un coup d'œil l'activité du jour +- Aller vite aux commandes, livraisons, tâches ou au calendrier +- Voir les prochaines tâches à faire + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/user (x5 : RouterProduction, MobileApp, DashboardPage, MobileLayout, MobileBottomNav) | au montage, puis 3 de plus à chaque navigation (MobileLayout et MobileBottomNav sont remontés par chaque page) | server/localAuth.ts:204 / localAuth.production.ts:246 -> getUserWithGroups | Les requêtes de la page attendent le /api/user de leur propre instance (enabled: !!user) : une cascade de plus. | +| GET /api/groups | au montage de MobileApp | server/routes.ts:970 | OK | +| GET /api/stats/dashboard?storeId | au montage | aucun : la route n'existe pas dans server/routes.ts, réponse 404 | Appel mort ; 'stats' et 'isLoading' ne sont jamais utilisés. | +| GET /api/tasks?storeId | au montage | server/routes.ts:3193 -> storage.getTasks (storage.ts:2267) | Même queryKey ['/api/tasks', storeId] que le Dashboard desktop et la page Tâches, mais pas la même URL. | +| GET /api/orders?storeId&date=yyyy-MM-dd | au montage | server/routes.ts:1343 (ne lit que startDate/endDate, l. 1351) -> getOrders (storage.ts:678) | Paramètre 'date' ignoré : tout l'historique est téléchargé et affiché comme 'Commandes'. | +| GET /api/deliveries?storeId&date=yyyy-MM-dd | au montage | server/routes.ts:1752 (date ignorée) -> getDeliveries (storage.ts:885) | Idem : total historique affiché comme 'Livraisons'. | + +**Lisibilité / simplicité :** Écran simple et lisible : salutation, grosses tuiles, accès rapides. Mais les chiffres 'Commandes' et 'Livraisons' sont des totaux historiques présentés comme l'activité du jour. Il n'y a pas d'état de chargement : '0' s'affiche d'abord. Il n'y a aucune alerte DLC ni information, contrairement au desktop, alors que les employés sur téléphone en ont besoin. Les accès rapides reprennent 3 des 4 boutons de la barre du bas. Leurs libellés au singulier ('Commande', 'Tâche') laissent croire à une création. 'Agenda' s'appelle 'Calendrier' dans la barre du bas. Les liens rechargent toute l'application. + +### `En-tête desktop (toutes les routes) : WeatherWidget + DateWidget` + +**Rôle :** Affiche dans l'en-tête la ville, la météo du jour comparée à la même date l'an dernier, et la date du jour. + +**Tâches principales de l'utilisateur :** +- Comparer la météo du jour à celle de l'an dernier (prévision d'affluence) +- Voir la date du jour + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/weather/current | au montage du Layout, puis polling toutes les 30 min ; retry: 1 | server/routes.ts:5671 -> getWeatherSettings / getWeatherData / getNearestWeatherData (storage.ts:2891-2941) + weatherService.fetchCurrentWeather / fetchPreviousYearWeather (weatherService.ts:24, 45) | Répond 404 si le service n'est pas configuré, ce qui déclenche un retry inutile. Appels BD et externes séquentiels, sans timeout. | + +**Lisibilité / simplicité :** Bloc compact, mais 3 colonnes (Aujourd'hui / Année dernière / Différence) prennent beaucoup de place dans l'en-tête. Le texte 'Chargement météo...' apparaît puis disparaît si le service n'est pas configuré, ce qui décale l'en-tête. La date est calculée au rendu et n'est jamais rafraîchie après minuit. La traduction des conditions météo est faite deux fois (client et serveur). + +### `/calendar (StatsPanel)` + +**Rôle :** Panneau flottant 'Statistiques du mois' sur le calendrier : commandes, livraisons, palettes, colis, délai moyen. + +**Tâches principales de l'utilisateur :** +- Voir les volumes du mois affiché dans le calendrier + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/stats/monthly?year&month&storeId | au montage et à chaque changement de mois | server/routes.ts:4155 -> storage.getMonthlyStats (storage.ts:1222) | queryKey [statsUrl, storeId] différente de celle du Dashboard ['/api/stats/monthly', storeId] pour la même donnée. Sans placeholderData, un squelette s'affiche à chaque changement de mois. | + +**Lisibilité / simplicité :** Panneau fixé en bas à droite (min-w-80), qu'on ne peut pas replier : il masque une partie du calendrier, surtout sur tablette. Les valeurs 'Palettes' et 'Colis' sont fausses (somme de toutes les unités, et nombre de livraisons). 'Commandes en attente' est un total historique affiché sous le titre 'Statistiques du mois'. + +### `(non routé) RecentTasksCard, ResponsiveDashboard` + +**Rôle :** Composants prévus pour le tableau de bord mais importés nulle part : code mort. + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/tasks?storeId | jamais : le composant n'est pas monté | server/routes.ts:3193 | fetch sans credentials ni contrôle res.ok. | + +**Lisibilité / simplicité :** Pas visible par l'utilisateur. Il entretient seulement de la confusion pour les développeurs : troisième tri et troisième mise en forme des priorités de tâches. + +## Constats + +| ID | Sév. | Catégorie | Effort | Titre | Fichier | +|---|---|---|---|---|---| +| [DASH-01](#dash-01) | haute | bug | S | Pour les non-admins, le magasin sélectionné n'est pas envoyé : tâches toujours vides, données d'un autre magasin | `client/src/pages/Dashboard.tsx:285` | +| [DASH-02](#dash-02) | haute | bug | S | La fenêtre d'alerte DLC se rouvre aussitôt qu'on la ferme (croix, Échap, clic à l'extérieur) | `client/src/pages/Dashboard.tsx:273` | +| [DASH-03](#dash-03) | haute | bug | M | En production, user vaut null au montage du Dashboard : requêtes envoyées avec le mauvais périmètre puis téléchargées deux fois | `client/src/hooks/useAuthUnified.ts:23` | +| [DASH-05](#dash-05) | haute | perf-api | L | L'historique complet des commandes et livraisons est téléchargé pour afficher 4 compteurs et 2 courtes listes | `client/src/pages/Dashboard.tsx:84` | +| [DASH-07](#dash-07) | haute | perf-serveur | M | Chaque ligne embarque l'objet magasin complet, logo base64 et configuration NocoDB/SMTP compris | `server/storage.ts:693` | +| [DASH-08](#dash-08) | haute | perf-serveur | S | Pas de compression HTTP ni de cache long sur les assets, et le middleware de compression maison est faux et inutilisé | `server/index.production.ts:143` | +| [DASH-22](#dash-22) | haute | ux-simplicite | M | Aucun état de chargement ni d'erreur : l'écran affiche de fausses informations pendant le chargement | `client/src/pages/Dashboard.tsx:435` | +| [DASH-28](#dash-28) | haute | bug | S | Mobile : 'Commandes' et 'Livraisons' affichent des totaux historiques (paramètre date ignoré), et un appel part vers une route inexistante | `client/src/pages/mobile/DashboardPage.tsx:101` | +| [DASH-32](#dash-32) | haute | perf-bundle | M | Aucun découpage du code : les 30+ pages desktop et mobile et recharts sont chargées dès l'accueil | `client/src/components/RouterProduction.tsx:5` | +| [DASH-04](#dash-04) | moyenne | perf-api | M | /api/user appelé 5 fois au chargement du tableau de bord, et 3 fois de plus à chaque navigation mobile | `client/src/hooks/useAuthUnified.ts:160` | +| [DASH-06](#dash-06) | moyenne | perf-api | S | Deux requêtes dont le résultat n'est jamais affiché : /api/stats/monthly et /api/customer-orders | `client/src/pages/Dashboard.tsx:28` | +| [DASH-09](#dash-09) | moyenne | perf-serveur | M | Chaque route relit l'utilisateur et ses magasins (2 requêtes) déjà chargés par passport | `server/routes.ts:1345` | +| [DASH-10](#dash-10) | moyenne | perf-serveur | S | Statistiques mensuelles et annuelles : 4 requêtes SQL séquentielles, non mises en cache | `server/storage.ts:1270` | +| [DASH-11](#dash-11) | moyenne | perf-api | S | Publicités à venir : 3 appels séquentiels qui téléchargent 3 années complètes pour en afficher 3 | `client/src/pages/Dashboard.tsx:205` | +| [DASH-12](#dash-12) | moyenne | perf-serveur | S | Informations : requête N+1 (auteur et magasin chargés un par un) et contenu complet écrit dans les logs | `server/routes.ts:5815` | +| [DASH-13](#dash-13) | moyenne | bug | S | Les non-admins voient les informations de tous les magasins, et le paramètre 'recent' est ignoré | `server/routes.ts:5797` | +| [DASH-15](#dash-15) | moyenne | perf-client | S | Changer de magasin relance d'abord les requêtes de l'ancien magasin, puis affiche des zéros | `client/src/components/Layout.tsx:138` | +| [DASH-17](#dash-17) | moyenne | perf-client | S | Calculs lourds refaits à chaque rendu, sans useMemo, et tri qui modifie le cache React Query | `client/src/pages/Dashboard.tsx:293` | +| [DASH-18](#dash-18) | moyenne | bug | S | Le badge 'jours en attente' affiche des valeurs négatives et n'a pas de légende | `client/src/pages/Dashboard.tsx:506` | +| [DASH-19](#dash-19) | moyenne | coherence-design | S | Badges de priorité des tâches : le variant est passé dans className, donc toutes les priorités ont la même couleur | `client/src/pages/Dashboard.tsx:687` | +| [DASH-21](#dash-21) | moyenne | lisibilite | S | Texte de debug technique visible par les utilisateurs | `client/src/pages/Dashboard.tsx:642` | +| [DASH-23](#dash-23) | moyenne | ux-simplicite | M | Rien n'est cliquable alors que tout réagit au survol | `client/src/pages/Dashboard.tsx:430` | +| [DASH-24](#dash-24) | moyenne | ux-simplicite | M | Alertes DLC affichées en double (bandeaux + fenêtre), et deux fenêtres automatiques peuvent s'empiler | `client/src/pages/Dashboard.tsx:409` | +| [DASH-25](#dash-25) | moyenne | lisibilite | S | Libellés ambigus, majuscules à l'anglaise, et numéro de publicité affiché à la place de la désignation | `client/src/pages/Dashboard.tsx:476` | +| [DASH-26](#dash-26) | moyenne | bug | S | Statistiques 'Palettes' et 'Colis' fausses : unités mélangées et nombre de livraisons présenté comme nombre de colis | `server/storage.ts:1310` | +| [DASH-27](#dash-27) | moyenne | bug | S | Compteurs DLC incohérents avec la liste de la fenêtre : produits du jour comptés deux fois, 'stock épuisé' traités différemment | `server/storage.ts:2245` | +| [DASH-29](#dash-29) | moyenne | perf-client | M | Chaque page mobile remonte l'en-tête et la barre du bas, ce qui relance /api/user à chaque navigation | `client/src/pages/mobile/DashboardPage.tsx:136` | +| [DASH-30](#dash-30) | moyenne | perf-client | S | Les liens 'Accès rapide' et le bouton de la fenêtre DLC rechargent toute l'application | `client/src/pages/mobile/DashboardPage.tsx:187` | +| [DASH-31](#dash-31) | moyenne | ux-simplicite | M | Mobile : les accès rapides doublonnent la barre du bas, avec des libellés trompeurs, et les alertes utiles manquent | `client/src/pages/mobile/DashboardPage.tsx:179` | +| [DASH-35](#dash-35) | moyenne | perf-serveur | S | Météo : appels séquentiels sans timeout, et l'API externe est rappelée à chaque requête si l'historique échoue | `server/routes.ts:5683` | +| [DASH-37](#dash-37) | moyenne | ux-simplicite | S | Informations : suppression sans confirmation avec une icône 'X', boutons de 24 px sans libellé, texte non tronqué | `client/src/components/AnnouncementCard.tsx:356` | +| [DASH-38](#dash-38) | moyenne | bug | M | Mêmes queryKeys pour des URL différentes (et l'inverse) : les données affichées dépendent de la page visitée en premier | `client/src/pages/Dashboard.tsx:282` | +| [DASH-14](#dash-14) | basse | perf-api | S | Les informations sont récupérées deux fois sous deux clés différentes, qui ne s'invalident pas ensemble | `client/src/pages/Dashboard.tsx:97` | +| [DASH-16](#dash-16) | basse | perf-client | S | Les requêtes DLC sont invalidées à chaque montage du tableau de bord, ce qui annule le cache | `client/src/pages/Dashboard.tsx:23` | +| [DASH-20](#dash-20) | basse | coherence-design | S | La classe 'border-l-3' n'existe pas dans Tailwind 3 : la barre colorée à gauche des lignes est invisible | `client/src/pages/Dashboard.tsx:510` | +| [DASH-33](#dash-33) | basse | perf-client | S | Script de bannière Replit (domaine tiers) chargé en production | `client/index.html:19` | +| [DASH-34](#dash-34) | basse | accessibilite | S | Page déclarée en anglais et zoom bloqué sur mobile | `client/index.html:5` | +| [DASH-36](#dash-36) | basse | ux-simplicite | S | Météo non configurée : retry inutile puis disparition du bloc, avec décalage de l'en-tête | `client/src/components/WeatherWidget.tsx:121` | +| [DASH-39](#dash-39) | basse | lisibilite | S | Fenêtre DLC : formulations techniques ou alarmistes, et '0 jour(s)' | `client/src/components/DlcAlertModal.tsx:178` | +| [DASH-40](#dash-40) | basse | ux-simplicite | S | 'Livraisons à venir' inclut les livraisons en retard, et les tâches sont triées différemment sur chaque écran | `client/src/pages/Dashboard.tsx:310` | +| [DASH-41](#dash-41) | basse | accessibilite | S | Textes secondaires en gris clair 'text-gray-400' de petite taille, sous le seuil de contraste | `client/src/pages/Dashboard.tsx:568` | +| [DASH-42](#dash-42) | basse | ux-simplicite | S | Panneau de statistiques fixe et non repliable qui masque le calendrier | `client/src/components/StatsPanel.tsx:54` | +| [DASH-43](#dash-43) | basse | bug | S | La date de l'en-tête n'est jamais rafraîchie | `client/src/components/DateWidget.tsx:16` | +| [DASH-44](#dash-44) | basse | perf-serveur | S | Logs volumineux à chaque appel dans les chemins les plus sollicités | `server/routes.ts:3263` | +| [DASH-45](#dash-45) | basse | dette-code | S | Code mort : RecentTasksCard, ResponsiveDashboard, server/cache.ts, imports et variables inutilisés | `client/src/components/tasks/RecentTasksCard.tsx:25` | +| [DASH-46](#dash-46) | basse | dette-code | S | Configurations de priorités et de types d'information dupliquées, avec des correspondances contre-intuitives | `client/src/pages/Dashboard.tsx:139` | + +### DASH-01 + +**Pour les non-admins, le magasin sélectionné n'est pas envoyé : tâches toujours vides, données d'un autre magasin** — bug, sévérité haute, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:285` +- **Constat :** Dashboard.tsx:285 'if (selectedStoreId && user?.role === 'admin') params.append("storeId", ...)'. Même condition l. 37, 62, 79-81. Côté serveur, routes.ts:3237-3249 : non-admin sans storeId -> 'returning empty result' (res.json([])). routes.ts:1410-1420 : manager/directeur sans storeId -> 'groupIds = [userGroupIds[0]]'. routes.ts:4170-4178 : stats -> 'groupIds = userGroupIds' (tous les magasins). Les pages Commandes et Livraisons envoient storeId pour tous les rôles (Orders.tsx:72-73 'CRITICAL FIX'). +- **Impact :** Un manager ou un directeur voit toujours 'Aucune tâche en cours / Toutes les tâches sont terminées'. Les commandes et livraisons affichées sont celles de son 1er magasin, quel que soit le magasin choisi, et les statistiques sont agrégées sur tous ses magasins. L'alerte DLC, elle, suit le magasin sélectionné : la page mélange 3 périmètres différents. +- **Recommandation :** Envoyer storeId pour tous les rôles dès qu'un magasin est sélectionné, comme dans Orders.tsx et Tasks.tsx. Centraliser la construction des URL dans un hook (ex. useStoreScopedUrl('/api/orders')) utilisé par toutes les pages, pour éviter de nouvelles divergences. + +### DASH-02 + +**La fenêtre d'alerte DLC se rouvre aussitôt qu'on la ferme (croix, Échap, clic à l'extérieur)** — bug, sévérité haute, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:273` +- **Constat :** Dashboard.tsx:276-278 'handleCloseDlcAlertModal = () => setShowDlcAlertModal(false)'. L'effet l. 244-273 dépend de showDlcAlertModal : 'if (shouldShowDlcModal && !showDlcAlertModal) { ... } else { setShowDlcAlertModal(true); }' tant qu'aucun snooze n'est présent dans localStorage. Seul 'Traiter plus tard (2h)' (DlcAlertModal.tsx:106-112) écrit ce snooze. +- **Impact :** Un employé ou un manager ne peut pas fermer la fenêtre normalement : elle revient instantanément. Il est bloqué jusqu'à trouver le bouton 'Traiter plus tard (2h)', ce qui est vécu comme un bug. +- **Recommandation :** À la fermeture, mémoriser un rejet pour la session (sessionStorage 'dlcAlertDismissed' ou snooze court). Déclencher l'ouverture automatique une seule fois par chargement (useRef 'alreadyShown'), sans dépendre de showDlcAlertModal. + +### DASH-03 + +**En production, user vaut null au montage du Dashboard : requêtes envoyées avec le mauvais périmètre puis téléchargées deux fois** — bug, sévérité haute, effort M + +- **Fichier :** `client/src/hooks/useAuthUnified.ts:23` +- **Constat :** useAuthUnified.ts:23-24 'const [productionUser, setProductionUser] = useState(null)' : chaque instance refait son fetch('/api/user') (l. 160). Au montage, Dashboard.tsx:79 construit ordersUrl sans storeId car user?.role est undefined, puis avec storeId une fois user chargé : la clé change et l'historique complet est retéléchargé. Pour /api/stats/yearly (l. 55) et /api/tasks (l. 282), la queryKey ne contient pas le rôle : le résultat 'tous magasins' obtenu avec user=null reste en cache sous la clé du magasin sélectionné. +- **Impact :** Pour un admin avec un magasin sélectionné, l'historique de tous les magasins (commandes, livraisons, commandes clients) est téléchargé puis jeté. Le délai moyen et la liste des tâches affichent les chiffres de tous les magasins au lieu du magasin choisi. +- **Recommandation :** Faire de useAuthUnified un simple useQuery(['/api/user'], { staleTime: Infinity }) partagé par le cache, ou un AuthContext fourni par RouterProduction. user est alors disponible dès le 1er rendu. Ajouter 'enabled: !!user' aux requêtes du Dashboard qui dépendent du rôle. + +### DASH-05 + +**L'historique complet des commandes et livraisons est téléchargé pour afficher 4 compteurs et 2 courtes listes** — perf-api, sévérité haute, effort L + +- **Fichier :** `client/src/pages/Dashboard.tsx:84` +- **Constat :** Dashboard.tsx:84-90 'useQuery({ queryKey: [ordersUrl, selectedStoreId] })' sur '/api/orders' et '/api/deliveries' sans filtre de date. Ils servent à calculer côté client deliveredThisMonth (l. 318), totalPalettes (l. 328), pendingOrders (l. 302) et upcomingDeliveries (.slice(0, 4), l. 313). Côté serveur, storage.getOrders (storage.ts:678-723) : pas de LIMIT, livraisons imbriquées via loadDeliveriesByOrderIds. storage.getDeliveries (885-926) : pas de LIMIT, commande imbriquée et nombre de commentaires. getTasks (storage.ts:2267) renvoie aussi toutes les tâches, terminées comprises, pour en afficher 5. +- **Impact :** Le temps de chargement et la mémoire grandissent avec l'historique (des Mo de JSON après quelques années), surtout sur réseau magasin ou 4G. Le serveur sérialise des milliers de lignes à chaque ouverture du tableau de bord. +- **Recommandation :** Créer GET /api/dashboard/summary?storeId=. Il renverrait, calculés en SQL (COUNT/SUM avec WHERE et LIMIT) et lancés en parallèle (Promise.all) : nombre de commandes en attente, livraisons et palettes reçues ce mois, délai moyen, les 10 commandes en attente les plus anciennes, les 4 prochaines livraisons planifiées, les 3 prochaines publicités, les 5 tâches ouvertes prioritaires, les compteurs DLC et la dernière information. Une seule requête légère au lieu de 11. + +### DASH-07 + +**Chaque ligne embarque l'objet magasin complet, logo base64 et configuration NocoDB/SMTP compris** — perf-serveur, sévérité haute, effort M — vérification : partiellement confirmé + +- **Fichier :** `server/storage.ts:693` +- **Constat :** storage.ts:692-693 'supplier: suppliers, group: groups,' dans getOrders. Même chose dans loadDeliveriesByOrderIds (l. 552-553), getDeliveries (l. 912-913), loadOrdersByIds, getCustomerOrders (l. 1868), getDlcProducts (l. 1980), getTasks (l. 2271) et getPublicities (l. 1536). Or shared/schema.ts:67 'logo: text("logo"), // Logo en data URI (data:image/png;base64,...)', l. 63 webhookUrl, l. 74 smtpPassword. Pour masquer smtpPassword, routes.ts:168 copie en profondeur chaque réponse : 'res.json = (body) => originalJson(stripSmtpPassword(body))'. +- **Impact :** Le logo base64 (souvent 10 à 100 Ko) est répété dans chaque commande, chaque livraison imbriquée et chaque produit DLC : la taille des réponses est multipliée et le parse JSON côté navigateur ralentit. Le serveur reparcourt et recopie tout le JSON à chaque réponse. La configuration interne (webhook, NocoDB, hôte et utilisateur SMTP) est exposée à tous les utilisateurs. +- **Recommandation :** Définir des projections par usage. Pour les listes (commandes, tâches, DLC, commandes clients, publicités) : { id, name, color }. Pour les livraisons et avoirs utilisés en rapprochement : ajouter webhookUrl, nocodbConfigId et nocodbTableName. Ne jamais sélectionner logo ni smtp*. Vérifier chaque appelant client par grep sur `.group?.` avant de changer une projection. Garder stripSmtpPassword global comme filet de sécurité. Son coût baissera de lui-même avec des objets plus petits. + +### DASH-08 + +**Pas de compression HTTP ni de cache long sur les assets, et le middleware de compression maison est faux et inutilisé** — perf-serveur, sévérité haute, effort S — vérification : partiellement confirmé + +- **Fichier :** `server/index.production.ts:143` +- **Constat :** server/index.production.ts:27-28 : express.json et urlencoded, aucun middleware compression. l. 143 'app.use('/assets', express.static(join(publicPath, 'assets')))' sans maxAge ni immutable. server/cache.ts:93-100 : setupCompression pose 'Content-Encoding: gzip' sans rien compresser, et n'est importé nulle part (grep vide). Aucun nginx/gzip dans le dépôt. Assets dist/public/assets : index 1,08 Mo + vendor-charts 410 Ko + vendor-react 146 Ko + CSS 121 Ko servis bruts. +- **Impact :** Environ 1,8 Mo de JS/CSS non compressé au premier chargement (environ 450 Ko en gzip). Les assets sont revalidés à chaque visite, et les grosses réponses JSON (commandes, livraisons) circulent 5 à 10 fois plus lourdes que nécessaire. +- **Recommandation :** (1) Mécanique : servir /assets avec express.static(..., { maxAge: '1y', immutable: true }) dans index.production.ts:143, sans toucher au static '/' ni à index.html. Supprimer setupCompression de server/cache.ts, voire tout le fichier (DASH-45). (2) Ajouter la dépendance compression et @types/compression, puis app.use(compression()) avant registerProductionRoutes dans index.production.ts et dans index.ts. Vérifier au préalable la configuration gzip du nginx externe (réseau nginx_default). + +### DASH-22 + +**Aucun état de chargement ni d'erreur : l'écran affiche de fausses informations pendant le chargement** — ux-simplicite, sévérité haute, effort M + +- **Fichier :** `client/src/pages/Dashboard.tsx:435` +- **Constat :** Dashboard.tsx ne lit jamais isLoading ni isError (grep vide). Pendant le chargement : l. 435 '{deliveredThisMonth}' = 0, l. 533 'Aucune commande en attente', l. 701-702 'Aucune tâche en cours / Toutes les tâches sont terminées'. mobile/DashboardPage.tsx:63 récupère isLoading sans s'en servir. Les queryFn qui échouent renvoient [] ou null en silence (DashboardPage.tsx:88, 106, 124). +- **Impact :** L'utilisateur peut croire que tout est traité alors que les données ne sont pas encore arrivées ou que le serveur est en erreur. C'est risqué pour des décisions opérationnelles (DLC, retards). +- **Recommandation :** Afficher un squelette (Skeleton shadcn) par carte tant que isLoading est vrai. En cas d'erreur, afficher un message clair dans la carte ('Impossible de charger les commandes' + bouton 'Réessayer'). Ne montrer les messages d'état vide qu'après un chargement réussi. + +### DASH-28 + +**Mobile : 'Commandes' et 'Livraisons' affichent des totaux historiques (paramètre date ignoré), et un appel part vers une route inexistante** — bug, sévérité haute, effort S + +- **Fichier :** `client/src/pages/mobile/DashboardPage.tsx:101` +- **Constat :** DashboardPage.tsx:101 'params.append('date', format(new Date(), 'yyyy-MM-dd'))' puis 'fetch(`/api/orders?${params}`)'. Le serveur ne lit que startDate/endDate : routes.ts:1351 'const { startDate, endDate, storeId } = req.query;'. Même chose pour /api/deliveries (l. 119-121). DashboardPage.tsx:69 'fetch(`/api/stats/dashboard?...`)' : aucune route de ce nom dans server/routes.ts (404), et 'stats' n'est jamais utilisé. +- **Impact :** Sur téléphone, l'employé voit par exemple '1 254 Commandes' en pensant que c'est l'activité du jour. L'application télécharge tout l'historique juste pour compter les lignes (.length), ce qui est lent en 4G, et fait un appel 404 inutile. +- **Recommandation :** Supprimer l'appel /api/stats/dashboard. Utiliser 'startDate=endDate=aujourd'hui', ou mieux l'endpoint de synthèse (DASH-05) qui renvoie directement les compteurs. Libeller 'Commandes du jour' et 'Livraisons du jour'. + +### DASH-32 + +**Aucun découpage du code : les 30+ pages desktop et mobile et recharts sont chargées dès l'accueil** — perf-bundle, sévérité haute, effort M — vérification : partiellement confirmé + +- **Fichier :** `client/src/components/RouterProduction.tsx:5` +- **Constat :** RouterProduction.tsx:5-44 : imports statiques de toutes les pages (Analytics, BLReconciliation, Avoirs, DatabaseDebug… et les 11 pages mobiles). Aucun React.lazy dans client/src (grep vide). vite.config.ts:41-46 : manualChunks 'vendor-charts': ['recharts'], que index.html précharge (modulepreload). Build : dist/public/assets/index-*.js = 1 081 745 octets, vendor-charts = 410 243 octets. +- **Impact :** Environ 1,7 Mo de JS à télécharger, parser et exécuter avant d'afficher l'accueil, alors que le tableau de bord n'utilise ni graphiques ni la plupart des pages. Premier affichage lent sur les PC de caisse et sur mobile. +- **Recommandation :** Charger les pages secondaires avec React.lazy dans un }>, en gardant Dashboard, AuthPage et la page mobile d'accueil en import direct. Ajouter un gestionnaire d'erreur de chargement de chunk (rechargement unique de la page). Retirer 'vendor-charts' de manualChunks, ou passer à une fonction manualChunks qui n'y place que node_modules/recharts et d3-*. Vérifier après le build que index.html ne précharge plus ce chunk. + +### DASH-04 + +**/api/user appelé 5 fois au chargement du tableau de bord, et 3 fois de plus à chaque navigation mobile** — perf-api, sévérité moyenne, effort M + +- **Fichier :** `client/src/hooks/useAuthUnified.ts:160` +- **Constat :** useAuthUnified.ts:160 'const response = await fetch('/api/user', { credentials: 'include', cache: 'no-cache', ...' dans un useEffect propre à chaque instance. Instances sur le desktop : RouterProduction, Layout, Dashboard, AnnouncementCard, plus useAuthSimple dans Sidebar.tsx:44. Sur mobile : RouterProduction, MobileApp, DashboardPage, MobileLayout, MobileBottomNav. Côté serveur, chaque appel coûte la session + getUserWithGroups dans deserializeUser (localAuth.production.ts:193-196) + de nouveau getUserWithGroups dans localAuth.ts:211. +- **Impact :** Environ 5 allers-retours et 20 à 25 requêtes SQL de plus à chaque ouverture. Sur mobile, les requêtes de la page attendent le /api/user de leur propre instance, ce qui ajoute une étape à la cascade et retarde l'affichage. +- **Recommandation :** Une seule source d'auth (useQuery partagé ou contexte), comme pour DASH-03. Supprimer useAuthSimple au profit du même hook. + +### DASH-06 + +**Deux requêtes dont le résultat n'est jamais affiché : /api/stats/monthly et /api/customer-orders** — perf-api, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:28` +- **Constat :** Dashboard.tsx:28 'const { data: stats } = useQuery({ queryKey: ['/api/stats/monthly', ...' : 'stats' n'est utilisé nulle part dans le JSX. Dashboard.tsx:92 'const { data: customerOrders = [] } = useQuery(...)' : il ne sert qu'à customerOrderStats (l. 380-387), jamais rendu. recentOrders (l. 293-299) et ordersByStatus (l. 372-377) ne sont pas utilisés non plus. +- **Impact :** 2 requêtes HTTP de plus, dont l'historique complet des commandes clients (storage.ts:1868, sans LIMIT), plus 4 requêtes SQL séquentielles pour les stats mensuelles, pour rien. +- **Recommandation :** Supprimer ces deux useQuery et les variables mortes recentOrders, ordersByStatus et customerOrderStats. + +### DASH-09 + +**Chaque route relit l'utilisateur et ses magasins (2 requêtes) déjà chargés par passport** — perf-serveur, sévérité moyenne, effort M + +- **Fichier :** `server/routes.ts:1345` +- **Constat :** routes.ts:1345 'const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);', répété l. 1754, 2799, 2833, 3195, 3488, 4157, 4194, 4597 et 5775. Or localAuth.production.ts:193-196 'passport.deserializeUser(async (id) => { const user = await storage.getUserWithGroups(id); ...' : req.user est déjà un UserWithGroups. Et getUserWithGroups (storage.ts:371-405) fait 2 requêtes séquentielles (getUser puis user_groups JOIN groups). +- **Impact :** Par requête API : session + 2 + 2 requêtes SQL séquentielles avant la logique métier. Sur le tableau de bord (environ 20 appels), cela fait environ 80 requêtes SQL inutiles et autant de latence ajoutée. +- **Recommandation :** Introduire un helper `getCurrentUser(req): UserWithGroups` qui renvoie req.user. L'appliquer d'abord aux routes chaudes du tableau de bord (orders, deliveries, tasks, stats, dlc stats, ad-campaigns, announcements), puis progressivement. Réécrire getUserWithGroups en une requête (users LEFT JOIN user_groups LEFT JOIN groups) en conservant exactement le format { ...user, userGroups: [{ userId, groupId, group: { id, name, color, createdAt, updatedAt } }] }. + +### DASH-10 + +**Statistiques mensuelles et annuelles : 4 requêtes SQL séquentielles, non mises en cache** — perf-serveur, sévérité moyenne, effort S + +- **Fichier :** `server/storage.ts:1270` +- **Constat :** storage.ts:1270 'const ordersResult = await db...', l. 1276 'const deliveriesResult = await db...', l. 1287 'const pendingResult = await db...', l. 1308 'const deliveriesStatsResult = await db...' : 4 await successifs indépendants. Même schéma dans getYearlyStats (l. 1409, 1415, 1426, 1446), dont le Dashboard n'utilise que averageDeliveryTime (Dashboard.tsx:317). +- **Impact :** La latence de l'endpoint vaut la somme de 4 allers-retours BD au lieu du plus long. Les statistiques annuelles, qui bougent peu, sont recalculées à chaque ouverture. +- **Recommandation :** Auto : seulement Promise.all sur les 4 requêtes de getMonthlyStats et de getYearlyStats. Le cache mémoire est à décider séparément, avec invalidation sur création, modification ou validation de commande et de livraison. + +### DASH-11 + +**Publicités à venir : 3 appels séquentiels qui téléchargent 3 années complètes pour en afficher 3** — perf-api, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:205` +- **Constat :** Dashboard.tsx:202-219 'const years = [currentYear - 1, currentYear, currentYear + 1]; for (const year of years) { ... const response = await fetch(`/api/ad-campaigns?${params}`) ...' puis un filtre et un tri côté client (l. 222-224), et '.slice(0, 3)' au rendu (l. 584). Côté serveur, getPublicities (storage.ts:1502-1560) ignore groupIds et associe les participations par 'participations.filter((p) => p.publicityId === publicity.id)' (l. 1552-1553), soit O(n×m). +- **Impact :** 3 allers-retours en cascade (l'année N-1 ne peut pas contenir de publicité future), et des centaines de publicités avec leurs participations (et le magasin complet, cf. DASH-07) transférées pour 3 lignes. +- **Recommandation :** Ajouter GET /api/ad-campaigns/upcoming?limit=3 : WHERE start_date > CURRENT_DATE ORDER BY start_date LIMIT 3 (l'index idx_publicities_start_date existe déjà, migrations/0000_flashy_blur.sql:260). En attendant, faire les appels N et N+1 en Promise.all et supprimer N-1. Côté storage, grouper les participations dans une Map. + +### DASH-12 + +**Informations : requête N+1 (auteur et magasin chargés un par un) et contenu complet écrit dans les logs** — perf-serveur, sévérité moyenne, effort S — vérification : partiellement confirmé + +- **Fichier :** `server/routes.ts:5815` +- **Constat :** routes.ts:5815 'messages.map(async (message) => { ... const [userResult] = await db.select({...}).from(users).where(eq(users.username, message.createdBy));' (l. 5821) puis 'const [groupResult] = await db.select().from(groups).where(eq(groups.id, message.storeId));' (l. 5852). l. 5811 : 'console.log('🔍 [PRODUCTION] Raw messages from DB:', messages.length, 'items:', messages);'. +- **Impact :** Jusqu'à 10 requêtes SQL de plus par appel, et cet appel est fait 2 fois par ouverture (DASH-14). Le select().from(groups) ramène aussi le logo base64. Les logs contiennent le texte intégral des annonces. +- **Recommandation :** Une requête dashboardMessages LEFT JOIN users ON users.username = created_by LEFT JOIN groups ON groups.id = store_id. Sélectionner users.id, username, firstName, lastName, name et groups.id, name. Recalculer `author` (même logique de displayName et même repli) et `group` ({ id, name } ou null), pour garder exactement le format de réponse actuel. Supprimer les console.log l.5773, 5779, 5811, 5820, 5827 et 5865. + +### DASH-13 + +**Les non-admins voient les informations de tous les magasins, et le paramètre 'recent' est ignoré** — bug, sévérité moyenne, effort S + +- **Fichier :** `server/routes.ts:5797` +- **Constat :** routes.ts:5797 'if (user.role === 'admin' && req.query.storeId) { query = query.where(or(eq(dashboardMessages.storeId, storeId), isNull(dashboardMessages.storeId))) }' : aucun filtre pour les autres rôles. Le Dashboard envoie 'recent=true' (Dashboard.tsx:104), que le serveur ne lit jamais, et refiltre sur 2 jours côté client (l. 117-124). +- **Impact :** Un employé du magasin A voit les informations destinées au magasin B, ce qui crée de la confusion sur les consignes à appliquer. Avec la limite de 5, des informations globales peuvent aussi être évincées par celles d'autres magasins. +- **Recommandation :** Pour les non-admins, filtrer 'store_id IS NULL OR store_id IN (userGroupIds)', ou sur le magasin sélectionné s'il est autorisé. Gérer 'recent' côté SQL (created_at >= now() - interval '2 days') ou retirer ce paramètre. + +### DASH-15 + +**Changer de magasin relance d'abord les requêtes de l'ancien magasin, puis affiche des zéros** — perf-client, sévérité moyenne, effort S + +- **Fichier :** `client/src/components/Layout.tsx:138` +- **Constat :** Layout.tsx:138-146 'queryClient.invalidateQueries({ predicate: ... includes('/api/orders') || ... '/api/deliveries' || '/api/stats/monthly' || '/api/tasks' })' est appelé avant 'setSelectedStoreId(newStoreId)' (l. 154). Les requêtes encore actives, qui portent la clé de l'ancien magasin, sont donc refetchées. Toutes ces clés contiennent déjà selectedStoreId, et aucune requête du Dashboard n'a de placeholderData. +- **Impact :** À chaque changement de magasin, l'historique complet des commandes et livraisons de l'ancien magasin est retéléchargé pour rien. Ensuite, l'écran affiche '0' et 'Aucune...' le temps du nouveau chargement. +- **Recommandation :** Auto : supprimer seulement l'appel invalidateQueries de Layout.tsx:138-146. L'ajout de placeholderData: keepPreviousData, avec un indicateur 'Mise à jour…', est à traiter séparément comme décision produit. + +### DASH-17 + +**Calculs lourds refaits à chaque rendu, sans useMemo, et tri qui modifie le cache React Query** — perf-client, sévérité moyenne, effort S — vérification : partiellement confirmé + +- **Fichier :** `client/src/pages/Dashboard.tsx:293` +- **Constat :** Dashboard.tsx:293-294 'const recentOrders = Array.isArray(allOrders) ? allOrders.sort(...)' : .sort() trie sur place le tableau stocké dans le cache, partagé avec la page Commandes pour l'admin. Puis 4 passes .filter sur allOrders (l. 302, 316, 373-375), 2 sur allDeliveries (l. 318, 328) et 5 sur customerOrders (l. 381-385), avec safeDate() à chaque comparaison. Le composant est re-rendu à chaque fin d'une des environ 12 requêtes. +- **Impact :** Sur un historique de plusieurs milliers de lignes, des dizaines de millisecondes de calcul par rendu, répétées une douzaine de fois au chargement : interface saccadée sur les PC de magasin modestes. La mutation du cache peut aussi changer l'ordre d'une autre page. +- **Recommandation :** Regrouper les dérivations dans un useMemo([allOrders, allDeliveries]) en une seule passe, et copier avant de trier ('[...allOrders].sort'). Mieux : déplacer ces agrégats côté serveur (DASH-05). + +### DASH-18 + +**Le badge 'jours en attente' affiche des valeurs négatives et n'a pas de légende** — bug, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:506` +- **Constat :** Dashboard.tsx:505-507 'const orderDate = safeDate(order.plannedDate); const daysPending = Math.floor((new Date().getTime() - orderDate.getTime()) / (1000*60*60*24)); const isOverdue = daysPending > 10;' puis l. 524 '{daysPending} jour{daysPending > 1 ? 's' : ''}'. Une commande 'pending' a généralement une date prévue future. +- **Impact :** L'utilisateur lit '-4 jour' ou '3 jours' sans savoir s'il s'agit d'un retard ou d'un délai restant. Le rouge n'apparaît qu'au-delà de 10 jours, de façon arbitraire, donc une commande en retard d'un jour reste orange. +- **Recommandation :** Si la date prévue est passée, afficher 'En retard de X j' en rouge. Sinon, afficher 'Prévue le 12 oct.' en neutre. Trier les retards en premier et limiter la liste à 5-10 lignes avec un lien 'Voir toutes les commandes en attente'. + +### DASH-19 + +**Badges de priorité des tâches : le variant est passé dans className, donc toutes les priorités ont la même couleur** — coherence-design, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:687` +- **Constat :** Dashboard.tsx:686-688 '' alors que getPriorityConfig (l. 341-368) renvoie color: 'destructive' | 'default' | 'secondary', qui sont des noms de variant et non des classes CSS. RecentTasksCard.tsx:152 fait correctement 'variant={priorityConfig.color}'. +- **Impact :** 'Élevée', 'Moyenne' et 'Faible' s'affichent tous en bleu primaire : l'urgence ne se voit pas d'un coup d'œil. +- **Recommandation :** Remplacer par ``, puis partager la configuration via DASH-46 (et non DASH-34). + +### DASH-21 + +**Texte de debug technique visible par les utilisateurs** — lisibilite, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:642` +- **Constat :** Dashboard.tsx:642 '

(API: {Array.isArray(upcomingPublicities) ? upcomingPublicities.length : 'NOT_ARRAY'})

'. l. 568 '({Array.isArray(allDeliveries) ? allDeliveries.length : 0} livraisons totales)'. Aussi un console.log dans le queryFn (l. 217). +- **Impact :** Des mentions comme '(API: 0)' ou '(API: NOT_ARRAY)' font croire à une panne et nuisent à la confiance dans l'outil. +- **Recommandation :** Auto : supprimer seulement les deux

de debug (l.568 et l.642) en gardant les messages 'Aucune livraison programmée' et 'Aucune publicité à venir'. L'ajout d'un lien d'action est à traiter avec DASH-23. + +### DASH-23 + +**Rien n'est cliquable alors que tout réagit au survol** — ux-simplicite, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Dashboard.tsx:430` +- **Constat :** Dashboard.tsx:430 '' sur les 4 compteurs et les 5 cartes. Les lignes ont 'hover:bg-orange-100' (l. 510), mais il n'y a aucun Link ni onClick (grep 'Link|navigate|href' vide). Les bandeaux DLC (l. 409-425) ne mènent pas non plus à /dlc. +- **Impact :** L'utilisateur voit '12 commandes en attente' mais doit passer par le menu, puis chercher et filtrer lui-même. L'effet de survol laisse croire à un clic qui ne fait rien. +- **Recommandation :** Rendre chaque compteur et chaque ligne cliquables : /orders?status=pending, /deliveries?status=planned, /dlc?status=expires, /tasks, /publicities. Ajouter un lien 'Tout voir' en pied de carte. Retirer l'effet hover-shadow sur ce qui n'est pas cliquable. + +### DASH-24 + +**Alertes DLC affichées en double (bandeaux + fenêtre), et deux fenêtres automatiques peuvent s'empiler** — ux-simplicite, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Dashboard.tsx:409` +- **Constat :** Dashboard.tsx:409-425 : bandeaux jaune et rouge DLC. l. 244-273 : ouverture automatique de DlcAlertModal avec les mêmes chiffres. l. 174-182 : ouverture automatique de la fenêtre 'Nouvelle Annonce'. Les deux Dialog (l. 710 et 773) sont indépendants : rien n'empêche open=true sur les deux à la fois pour un non-admin. +- **Impact :** À la connexion, un employé peut recevoir deux fenêtres superposées en plus de deux bandeaux : surcharge et confusion sur ce qu'il faut faire en premier. +- **Recommandation :** Créer un seul bloc 'À traiter aujourd'hui' en haut de page (DLC expirés, DLC proches, commandes en retard), avec un bouton d'action chacun. Afficher au plus une fenêtre automatique par session, avec une file : l'information d'abord, l'alerte DLC ensuite. + +### DASH-25 + +**Libellés ambigus, majuscules à l'anglaise, et numéro de publicité affiché à la place de la désignation** — lisibilite, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:476` +- **Constat :** l. 476 'Total palettes' : en réalité les palettes livrées ce mois (l. 328-338). l. 462 'Délai commande → livraison' : en réalité la moyenne annuelle (yearlyStats). l. 595-596 : '{publicity.pubNumber}' en gras et '{publicity.designation}' tronquée à max-w-40 (160 px). Titres 'Tableau de Bord', 'Commandes en Attente', 'Livraisons à Venir', 'Publicités à Venir' à côté de 'Tâches à faire'. Sous-titre générique l. 400 'Vue d'ensemble des performances et statistiques'. +- **Impact :** Un employé ne sait pas sur quelle période portent les chiffres, et ne reconnaît pas une opération publicitaire à son numéro interne. +- **Recommandation :** Renommer en 'Palettes reçues ce mois', 'Délai moyen commande → livraison (2026)', 'Livraisons ce mois'. Pour les publicités, afficher la désignation en premier, puis 'Pub n° 2541 · du 12 au 18 oct.'. Écrire les titres en casse française ('Commandes en attente'). + +### DASH-26 + +**Statistiques 'Palettes' et 'Colis' fausses : unités mélangées et nombre de livraisons présenté comme nombre de colis** — bug, sévérité moyenne, effort S + +- **Fichier :** `server/storage.ts:1310` +- **Constat :** storage.ts:1310-1311 'totalPalettes: sql`COALESCE(SUM(CAST(${deliveries.quantity} as INTEGER)), 0)`, totalPackages: sql`COALESCE(COUNT(*), 0)`' sans condition sur deliveries.unit (même chose l. 1448-1449). Affiché dans StatsPanel.tsx:79-87 comme 'Palettes' et 'Colis'. StatsPanel.tsx:101-104 affiche aussi pendingOrdersCount, qui couvre tout l'historique, sous le titre 'Statistiques du mois'. +- **Impact :** Les volumes mensuels sont faux (colis comptés comme palettes, 'Colis' = nombre de livraisons) et peuvent fausser le pilotage du magasin. +- **Recommandation :** Utiliser SUM(CASE WHEN unit='palettes' THEN quantity ELSE 0 END) et SUM(CASE WHEN unit='colis' THEN quantity ELSE 0 END), calculés sans innerJoin orders : seul le délai moyen a besoin de la jointure. Libeller pendingOrdersCount 'Commandes en attente (toutes dates)'. + +### DASH-27 + +**Compteurs DLC incohérents avec la liste de la fenêtre : produits du jour comptés deux fois, 'stock épuisé' traités différemment** — bug, sévérité moyenne, effort S + +- **Fichier :** `server/storage.ts:2245` +- **Constat :** storage.ts:2245 'expiryDate BETWEEN today AND alertDate' (expiringSoon) et l. 2251 'expiryDate <= today' (expired) : un produit qui expire aujourd'hui est compté dans les deux. Les stats excluent stockEpuise=true, mais la liste 'expires' (l. 2019-2028) ne filtre pas stockEpuise, et DlcAlertModal.tsx:196-199 affiche alors un badge 'Stock épuisé'. La date est calculée en UTC : 'today.toISOString().split('T')[0]'. +- **Impact :** Le titre 'Produits Expirés (3)' ne correspond pas au nombre de lignes affichées. Les bandeaux additionnent deux fois les produits du jour. Entre minuit et 2 h, heure de Paris, c'est la date de la veille qui est utilisée. +- **Recommandation :** Définir une seule fois les conditions SQL (expiré : expiry_date < aujourd'hui ; bientôt : entre aujourd'hui et aujourd'hui + 15 ; mêmes exclusions status != 'valides', processed_until_expiry et stock_epuise) et les réutiliser pour les stats et les listes. Pour 'aujourd'hui', utiliser `(now() AT TIME ZONE 'Europe/Paris')::date` plutôt que CURRENT_DATE nu ou toISOString. + +### DASH-29 + +**Chaque page mobile remonte l'en-tête et la barre du bas, ce qui relance /api/user à chaque navigation** — perf-client, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/mobile/DashboardPage.tsx:136` +- **Constat :** DashboardPage.tsx:136 '' est rendu dans la page : 10 pages mobiles font de même (grep ' de RouterProduction.tsx:108-143. Passer le titre de page par contexte ou par prop de route. Utiliser une auth partagée (DASH-04). + +### DASH-30 + +**Les liens 'Accès rapide' et le bouton de la fenêtre DLC rechargent toute l'application** — perf-client, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/mobile/DashboardPage.tsx:187` +- **Constat :** DashboardPage.tsx:187-191 '' (lien HTML natif, pas le de wouter). DlcAlertModal.tsx:141-143 'const handleViewDlcModule = () => { window.location.href = '/dlc'; };'. +- **Impact :** Chaque clic recharge le JS (environ 1,7 Mo non compressé, cf. DASH-08), refait l'authentification et vide le cache React Query. Sur mobile, cela représente plusieurs secondes d'attente pour une simple navigation interne. +- **Recommandation :** Remplacer par '' (wouter) dans DashboardPage, et par 'const [, navigate] = useLocation(); navigate('/dlc'); onClose();' dans DlcAlertModal. + +### DASH-31 + +**Mobile : les accès rapides doublonnent la barre du bas, avec des libellés trompeurs, et les alertes utiles manquent** — ux-simplicite, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/mobile/DashboardPage.tsx:179` +- **Constat :** DashboardPage.tsx:179-184 '{ label: "Commande", path: "/orders" }, { label: "Livraison", ... }, { label: "Tâche", ... }, { label: "Agenda", path: "/calendar" }'. MobileBottomNav.tsx:30-34 contient déjà Commandes, Livraisons et Tâches, et l. 37 nomme '/calendar' 'Calendrier'. La page mobile n'a ni alerte DLC ni informations, contrairement au desktop (Dashboard.tsx:409-425, 652). +- **Impact :** La place précieuse sur téléphone répète la navigation. 'Commande' au singulier laisse croire à un bouton 'Créer', le même écran porte deux noms, et l'employé en rayon ne voit pas les produits DLC à retirer. +- **Recommandation :** Remplacer les accès rapides par un bloc 'À traiter' : DLC expirés et proches (lien /dlc), information du jour, commandes clients à appeler. Ou proposer de vraies actions de création ('+ Nouvelle livraison'). Utiliser le même nom partout ('Calendrier'). + +### DASH-35 + +**Météo : appels séquentiels sans timeout, et l'API externe est rappelée à chaque requête si l'historique échoue** — perf-serveur, sévérité moyenne, effort S + +- **Fichier :** `server/routes.ts:5683` +- **Constat :** routes.ts:5673 'const settings = await storage.getWeatherSettings();', l. 5683 'let currentYearData = await storage.getWeatherData(today, true);', l. 5684 'let previousYearData = await ...' : séquentiel. Puis fetchCurrentWeather et fetchPreviousYearWeather, aussi séquentiels (l. 5687-5720). weatherService.ts:27 et 51 : 'await fetch(url)' sans AbortSignal ni timeout. En cas d'échec de l'historique (quota, 401), rien n'est enregistré : chaque appel suivant rappelle Visual Crossing. +- **Impact :** Si l'API météo est lente, la requête bloque une des 6 connexions HTTP/1.1 du navigateur pendant tout le chargement. Le quota API s'épuise vite, ce qui entretient les échecs. +- **Recommandation :** Auto : paralléliser les deux lectures BD, puis les deux fetch externes (Promise.all), et ajouter `signal: AbortSignal.timeout(3000)` aux deux fetch de weatherService.ts. Le cache mémoire de 30 min et le cache négatif de la journée sont à valider séparément : ils empêchent de réessayer après une panne transitoire. + +### DASH-37 + +**Informations : suppression sans confirmation avec une icône 'X', boutons de 24 px sans libellé, texte non tronqué** — ux-simplicite, sévérité moyenne, effort S + +- **Fichier :** `client/src/components/AnnouncementCard.tsx:356` +- **Constat :** AnnouncementCard.tsx:352-360 '' : pas d'AlertDialog, pas d'aria-label. l. 324 '

{announcement.content}

' : contenu entier. Vocabulaire mélangé : titre 'Informations' (l. 275), bouton 'Nouvelle' (l. 294), fenêtre du Dashboard 'Nouvelle Annonce' (Dashboard.tsx:715), état vide 'Créez la première annonce' (l. 308). +- **Impact :** Un admin qui veut 'fermer' une information la supprime définitivement en un clic. Les cibles de 24 px sont difficiles à viser. Une information longue étire la carte et pousse les tâches hors de l'écran. +- **Recommandation :** Utiliser l'icône Trash2 avec un AlertDialog 'Supprimer cette information ?', des boutons d'au moins 36 px avec aria-label 'Modifier' et 'Supprimer', et line-clamp-3 avec 'Lire la suite'. Choisir un seul terme ('Information') partout. + +### DASH-38 + +**Mêmes queryKeys pour des URL différentes (et l'inverse) : les données affichées dépendent de la page visitée en premier** — bug, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Dashboard.tsx:282` +- **Constat :** ['/api/tasks', selectedStoreId] : Dashboard.tsx:282-285 n'envoie storeId que pour l'admin, Tasks.tsx:327-333 et mobile/DashboardPage.tsx:80-83 l'envoient pour tous. ['/api/dlc-products/stats', selectedStoreId] : DlcPage.tsx:96-99 n'envoie storeId que pour l'admin, Dashboard.tsx:236 pour tous. Inversement, '/api/stats/monthly' est rangé sous ['/api/stats/monthly', id] (Dashboard.tsx:29) et sous [statsUrl, id] (StatsPanel.tsx:21). +- **Impact :** Après un passage par la page Tâches, l'accueil d'un manager affiche ses tâches pendant 30 s, puis la liste se vide au refetch. Les compteurs DLC peuvent différer entre l'accueil et la page DLC selon l'ordre de visite. La même statistique est téléchargée deux fois. +- **Recommandation :** Centraliser les requêtes dans un module (ex. client/src/lib/queries.ts : tasksQuery(storeId), dlcStatsQuery(storeId)…). La queryKey doit être exactement l'URL appelée, construite par une seule fonction partagée par toutes les pages. + +### DASH-14 + +**Les informations sont récupérées deux fois sous deux clés différentes, qui ne s'invalident pas ensemble** — perf-api, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:97` +- **Constat :** Dashboard.tsx:98 'queryKey: ['/api/announcements/recent', selectedStoreId]' appelle '/api/announcements?...&recent=true'. AnnouncementCard.tsx:134 'queryKey: ['/api/announcements', selectedStoreId, user?.role]' appelle la même route. Après création ou suppression, AnnouncementCard.tsx:192 et 226 n'invalident que ['/api/announcements'], préfixe qui ne couvre pas '/api/announcements/recent'. +- **Impact :** Une requête en double (avec son N+1 serveur) à chaque ouverture. Une information supprimée peut encore surgir dans la fenêtre 'Nouvelle Annonce' tant que le cache n'est pas périmé. +- **Recommandation :** Dans Dashboard, utiliser exactement la clé ['/api/announcements', selectedStoreId, user?.role] avec le même queryFn que AnnouncementCard, extrait dans une fonction partagée (même URL, renvoie le tableau). Calculer l'annonce récente (moins de 2 jours, la plus récente) via l'option `select` ou un useMemo. Les invalidations existantes couvriront alors les deux usages. + +### DASH-16 + +**Les requêtes DLC sont invalidées à chaque montage du tableau de bord, ce qui annule le cache** — perf-client, sévérité basse, effort S — vérification : partiellement confirmé + +- **Fichier :** `client/src/pages/Dashboard.tsx:23` +- **Constat :** Dashboard.tsx:23-26 'useEffect(() => { queryClient.invalidateQueries({ queryKey: ["/api/dlc-products"] }); queryClient.invalidateQueries({ queryKey: ["/api/dlc-products/stats"] }); }, [selectedStoreId, queryClient]);'. Même effet dans DlcAlertModal.tsx:45-47. Les clés contiennent déjà selectedStoreId (l. 232). +- **Impact :** Chaque retour sur l'accueil relance les stats DLC (et marque comme périmées les listes DLC de la page DLC), même si elles datent de quelques secondes. +- **Recommandation :** Corriger d'abord DASH-38 : une clé égale à l'URL réellement appelée, construite par une fonction partagée entre Dashboard et DlcPage. Supprimer ensuite les deux useEffect d'invalidation. + +### DASH-20 + +**La classe 'border-l-3' n'existe pas dans Tailwind 3 : la barre colorée à gauche des lignes est invisible** — coherence-design, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:510` +- **Constat :** Dashboard.tsx:510, 548, 590, 676 et AnnouncementCard.tsx:316 utilisent 'border-l-3 border-orange-500' (etc.). tailwind.config.ts n'étend pas borderWidth et index.css ne définit pas .border-l-3. Tailwind 3.4 ne fournit que border-l, -0, -2, -4 et -8. +- **Impact :** Le code couleur prévu (orange en attente, rouge en retard, vert livraisons, violet publicités) n'apparaît pas : les lignes se ressemblent toutes et la lecture rapide en souffre. +- **Recommandation :** Remplacer par 'border-l-4' ou 'border-l-[3px]'. Pour les informations, adapter la couleur au type (rouge Important, orange Attention…). + +### DASH-33 + +**Script de bannière Replit (domaine tiers) chargé en production** — perf-client, sévérité basse, effort S + +- **Fichier :** `client/index.html:19` +- **Constat :** client/index.html:18-19 ' ', présent aussi dans le build dist/public/index.html. +- **Impact :** Une requête DNS + TLS vers un domaine tiers à chaque chargement, avec un script bloquant en fin de body. Exécution de code externe inutile sur un outil interne. +- **Recommandation :** Supprimer cette balise, ou l'injecter seulement en dev via un plugin Vite conditionnel. + +### DASH-34 + +**Page déclarée en anglais et zoom bloqué sur mobile** — accessibilite, sévérité basse, effort S + +- **Fichier :** `client/index.html:5` +- **Constat :** client/index.html:2 '' alors que toute l'interface est en français. l. 5 ''. +- **Impact :** Chrome propose de 'traduire la page', les lecteurs d'écran prononcent le français avec une voix anglaise, et les utilisateurs malvoyants ne peuvent pas zoomer sur téléphone (WCAG 1.4.4). +- **Recommandation :** Auto : passer seulement à . Retirer maximum-scale=1 après avoir vérifié (grep) que les champs (input, select, textarea) affichés sous 768 px ont tous une police d'au moins 16 px. + +### DASH-36 + +**Météo non configurée : retry inutile puis disparition du bloc, avec décalage de l'en-tête** — ux-simplicite, sévérité basse, effort S + +- **Fichier :** `client/src/components/WeatherWidget.tsx:121` +- **Constat :** WeatherWidget.tsx:121-125 'useQuery({ queryKey: ['/api/weather/current'], refetchInterval: 30*60*1000, retry: 1 })'. routes.ts:5675-5677 renvoie 404 'Weather service not configured'. Pendant l'essai et le nouvel essai, l. 137 affiche 'Chargement météo...', puis l. 144-146 'return null'. WeatherWidget.tsx:27-54 retraduit aussi en français des conditions déjà traduites par le serveur (lang=fr, weatherService.translateConditionToFrench). +- **Impact :** Sur chaque ouverture de l'application sans météo configurée, un texte de chargement apparaît environ 1 s puis disparaît, ce qui fait bouger le sélecteur de magasin. Une requête en double est envoyée. +- **Recommandation :** Côté serveur, répondre 200 { configured: false }. Côté client, mettre 'retry: false' et 'staleTime: 30 min', et réserver une largeur fixe ou un squelette discret. Supprimer translateToFrench côté client. + +### DASH-39 + +**Fenêtre DLC : formulations techniques ou alarmistes, et '0 jour(s)'** — lisibilite, sévérité basse, effort S + +- **Fichier :** `client/src/components/DlcAlertModal.tsx:178` +- **Constat :** DlcAlertModal.tsx:151 'Alerte Produits DLC - Action Requise'. l. 178 'Expiré depuis {Math.abs(getDaysUntilExpiry(product.expiryDate))} jour(s)' donne 'Expiré depuis 0 jour(s)' le jour même. l. 244 '{daysLeft <= 3 ? 'URGENT' : `${daysLeft}j`}'. Toasts avec emojis (l. 80 '✅ Produit marqué', l. 89 '❌ Erreur'). l. 285 '💡 Conseil : Ce modal réapparaîtra…'. Le terme 'modal' est un anglicisme technique. +- **Impact :** Les messages paraissent techniques ou anxiogènes. 'Expiré depuis 0 jour(s)' est déroutant et '5j' est peu lisible. +- **Recommandation :** Écrire 'Expire aujourd'hui' ou 'Expiré depuis 2 jours' (fonction de pluriel), 'Dans 5 jours', et 'Urgent' plutôt que 'URGENT'. Titre : 'Produits à retirer ou à surveiller'. Remplacer 'Ce modal' par 'Cette fenêtre'. Retirer les emojis des toasts pour rester cohérent avec les autres pages. + +### DASH-40 + +**'Livraisons à venir' inclut les livraisons en retard, et les tâches sont triées différemment sur chaque écran** — ux-simplicite, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:310` +- **Constat :** Dashboard.tsx:310-313 '.filter((d) => d.status === 'planned').sort(... scheduledDate ...).slice(0, 4)' : une livraison planifiée à une date passée s'affiche en tête sans signalement. Tâches : Dashboard.tsx:665-669 trie par createdAt croissant (les plus anciennes d'abord). mobile/DashboardPage.tsx:205-207 prend les 3 premières de l'ordre serveur (createdAt décroissant). RecentTasksCard.tsx:59-73 trie par priorité puis échéance. +- **Impact :** Une livraison en retard est présentée comme 'à venir'. Les tâches les plus urgentes ou en retard peuvent ne pas apparaître dans le top 5, et l'ordre change d'un écran à l'autre. +- **Recommandation :** Ajouter un badge rouge 'En retard' aux livraisons planifiées dans le passé, ou une section dédiée. Trier les tâches partout de la même façon : en retard, puis priorité haute, puis échéance la plus proche. + +### DASH-41 + +**Textes secondaires en gris clair 'text-gray-400' de petite taille, sous le seuil de contraste** — accessibilite, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:568` +- **Constat :** Dashboard.tsx:568 et 642 '

', l. 702 'text-xs text-gray-400', AnnouncementCard.tsx:307 'text-xs text-gray-400'. #9ca3af sur fond blanc donne un ratio d'environ 2,5:1 (WCAG AA exige 4,5:1). +- **Impact :** Peu lisible sur les écrans de magasin en pleine lumière et pour les utilisateurs âgés. +- **Recommandation :** Utiliser au minimum text-gray-600 pour tout texte informatif, et réserver gray-400 aux éléments décoratifs. + +### DASH-42 + +**Panneau de statistiques fixe et non repliable qui masque le calendrier** — ux-simplicite, sévérité basse, effort S + +- **Fichier :** `client/src/components/StatsPanel.tsx:54` +- **Constat :** StatsPanel.tsx:54 ''. Même chose pour le squelette (l. 37). Aucun bouton pour replier ou fermer. +- **Impact :** Sur tablette ou petit écran, le panneau de 320 px recouvre les derniers jours du mois et leurs livraisons. +- **Recommandation :** L'intégrer dans le flux de la page (bandeau de 4 compteurs au-dessus du calendrier), ou le rendre repliable avec mémorisation de l'état. Ajouter 'placeholderData: keepPreviousData' pour éviter le squelette à chaque changement de mois. + +### DASH-43 + +**La date de l'en-tête n'est jamais rafraîchie** — bug, sévérité basse, effort S — vérification : partiellement confirmé + +- **Fichier :** `client/src/components/DateWidget.tsx:16` +- **Constat :** DateWidget.tsx:16 'const now = new Date();' est calculé seulement au rendu, sans timer. Le Layout reste monté d'une page à l'autre. +- **Impact :** Sur un poste laissé ouvert la nuit (poste de réception, caisse), l'en-tête affiche encore la date de la veille le matin. +- **Recommandation :** Utiliser un petit hook useToday() qui programme un setTimeout jusqu'à minuit et se réarme. + +### DASH-44 + +**Logs volumineux à chaque appel dans les chemins les plus sollicités** — perf-serveur, sévérité basse, effort S + +- **Fichier :** `server/routes.ts:3263` +- **Constat :** routes.ts:1353 'console.log('Orders API called with:', ...)' (plus 'Fetching all orders' et 'Orders returned'). l. 3263-3269 : 'taskGroups: tasks.map(t => ({ id, title, groupId })).slice(0, 3)', qui parcourt toute la liste juste pour un log. storage.ts:721 (getOrders), 1250 (stats), 2303-2313 (getTasks sampleTasks), routes.ts:5811 (contenu des annonces). +- **Impact :** Du CPU et de l'I/O synchrone (stdout) consommés à chaque requête, des logs illisibles qui noient les vraies erreurs, et du contenu métier écrit dans les logs. +- **Recommandation :** Supprimer ces console.log ou les placer derrière un logger à niveau (DEBUG=logiflow:*). Ne garder que les erreurs et le log d'accès existant (index.production.ts:30-38). + +### DASH-45 + +**Code mort : RecentTasksCard, ResponsiveDashboard, server/cache.ts, imports et variables inutilisés** — dette-code, sévérité basse, effort S + +- **Fichier :** `client/src/components/tasks/RecentTasksCard.tsx:25` +- **Constat :** grep : RecentTasksCard et ResponsiveDashboard ne sont importés nulle part. RecentTasksCard.tsx:38 'fetch(`/api/tasks?...`).then(res => res.json())' sans credentials ni contrôle res.ok. server/cache.ts (cacheMiddleware, setupCompression) n'est importé nulle part. Dashboard.tsx:9 importe ShoppingCart, TrendingUp, MapPin, CheckCircle, FileText et Shield, et l. 5 CardDescription, sans les utiliser. Variables recentOrders, ordersByStatus et customerOrderStats inutilisées. Commentaire trompeur l. 649 'Section Rapprochement BL' au-dessus des informations et des tâches. +- **Impact :** Confusion lors de la maintenance (trois implémentations de la carte des tâches), et risque de réutiliser par erreur un composant défectueux. +- **Recommandation :** Supprimer RecentTasksCard.tsx, ResponsiveDashboard.tsx et server/cache.ts (ou le réécrire proprement), ainsi que les imports, variables et commentaires obsolètes du Dashboard. + +### DASH-46 + +**Configurations de priorités et de types d'information dupliquées, avec des correspondances contre-intuitives** — dette-code, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Dashboard.tsx:139` +- **Constat :** Dashboard.tsx:139-171 getAnnouncementPriorityConfig est un copier-coller d'AnnouncementCard.tsx:66-98 ('error' -> 'Nouveauté' violet, 'success' -> 'Important' rouge). getPriorityConfig est dupliqué dans Dashboard.tsx:341-368 et RecentTasksCard.tsx:76-103 (commentaire 'identique au module Tasks'). +- **Impact :** Toute évolution des couleurs ou libellés doit être faite à 3 ou 4 endroits, d'où des incohérences visuelles entre pages. Les correspondances type -> libellé ('error' = Nouveauté) induisent les développeurs en erreur. +- **Recommandation :** Auto : extraire dans client/src/lib/statusConfig.ts les configurations identiques (types d'annonce : Dashboard et AnnouncementCard ; priorités de tâches avec variante Badge : Dashboard, Tasks.tsx, RecentTasksCard s'il est conservé), sans modifier aucun libellé, couleur ni valeur. Ne pas fusionner la variante mobile (classes CSS) et ne pas faire de migration des valeurs de type. diff --git a/docs/audit-webui/02-calendar-orders-deliveries.md b/docs/audit-webui/02-calendar-orders-deliveries.md new file mode 100644 index 0000000..b069ea1 --- /dev/null +++ b/docs/audit-webui/02-calendar-orders-deliveries.md @@ -0,0 +1,733 @@ +# Calendrier, Commandes, Livraisons + +_58 constats vérifiés — 7 haute, 32 moyenne, 19 basse._ + +## Pages analysées + +### `/calendar (desktop)` + +**Rôle :** Vue mensuelle des commandes fournisseurs, des livraisons prévues/reçues et des publicités du magasin sélectionné, avec un panneau de statistiques du mois. C'est le point d'entrée pour planifier et ouvrir la fiche d'une commande ou d'une livraison. + +**Tâches principales de l'utilisateur :** +- Voir ce qui est commandé et livré chaque jour du mois +- Passer au mois précédent ou suivant +- Créer une commande ou une livraison à une date +- Ouvrir la fiche d'une commande ou livraison (modifier, valider, supprimer) +- Lire le commentaire d'une livraison +- Consulter les chiffres du mois (commandes, livraisons, palettes, colis) + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/orders?startDate&endDate&storeId | au montage, à chaque changement de mois ou de magasin (fetch() manuel dans queryFn) | server/routes.ts:1343 -> storage.getOrdersByDateRange (storage.ts:725) + loadDeliveriesByOrderIds (storage.ts:522) | Charge chaque commande avec group complet (logo base64 compris), supplier complet, creator, et toutes les livraisons liées, elles-mêmes avec group/supplier/creator complets. Plage limitée à startOfMonth..endOfMonth alors que la grille affiche 42 jours. | +| GET /api/deliveries?startDate&endDate&storeId | au montage, à chaque changement de mois ou de magasin | server/routes.ts:1752 -> storage.getDeliveriesByDateRange (storage.ts:939) + attachOrdersAndCommentCounts (loadOrdersByIds + loadReconciliationCommentCounts en Promise.all) | Pas de N+1 (regroupement en 2 requêtes), mais colonnes lourdes : group.* (logo), order complet imbriqué. | +| GET /api/ad-campaigns?year&storeId(admin seulement) | au montage et au changement d'année ou de magasin | server/routes.ts:4595 -> storage.getPublicities (storage.ts:1502) | Le paramètre groupIds est ignoré côté storage : toute l'année, tous magasins, participations avec group complet. Le filtrage par magasin et par jour se fait en JS, 42 fois par rendu. | +| GET /api/stats/monthly?year&month&storeId | au montage de StatsPanel et au changement de mois | server/routes.ts:4155 -> storage.getMonthlyStats (storage.ts:1222) | 4 requêtes SQL séquentielles. Libellés 'Colis' et 'Palettes' faux. Données fictives si NODE_ENV=development. Clé de cache [statsUrl, storeId] jamais touchée par les invalidations ['/api/stats/monthly']. | +| GET /api/groups | Layout au montage (clé partagée avec les modales) | server/routes.ts:970 -> storage.getGroups (admin, toutes colonnes dont logo) ou user.userGroups | Rechargé à chaque ouverture de modale une fois passé le staleTime de 30 s. | +| GET /api/suppliers, GET /api/orders (complet) | à l'ouverture de CreateOrderModal ou de CreateDeliveryModal | server/routes.ts:1141 -> storage.getSuppliers ; server/routes.ts:1343 -> storage.getOrders (storage.ts:678) | CreateDeliveryModal télécharge tout l'historique des commandes pour remplir une liste déroulante. | + +**Lisibilité / simplicité :** La page est dense et difficile à lire. Le mois apparaît deux fois (en-tête et barre de navigation). La légende (bleu primaire, vert foncé, gris) ne correspond pas aux couleurs de la grille (bleu clair, jaune pour 'planifié', vert clair, violet pour les pubs). Les éléments sont écrits en 11 px avec des abréviations (P/C) et des icônes de 8 px. Seuls 2 éléments sont visibles par jour, le reste passe par '+N autres', ce qui ouvre des modales imbriquées. Un clic n'importe où dans une case ouvre 'Création rapide', même pour un utilisateur sans droits. Le bouton '+' n'apparaît qu'au survol. Le panneau de statistiques, fixé en bas à droite, recouvre les dernières cases et affiche des chiffres faux. Un spinner plein écran remplace la grille à chaque changement de mois. Les jours des mois voisins sont affichés mais toujours vides. Le passage au mois suivant saute un mois les 29, 30 et 31. Il n'y a aucun état d'erreur : un échec d'API donne un calendrier vide silencieux. + +### `/calendar (mobile)` + +**Rôle :** Calendrier mensuel simplifié pour téléphone : des points de couleur indiquent les jours avec des commandes ou des livraisons, et la liste du jour sélectionné s'affiche dessous. + +**Tâches principales de l'utilisateur :** +- Voir les jours avec commandes et livraisons +- Voir la liste des événements d'un jour + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/orders?startDate&endDate&storeId | au montage et au changement de mois (enabled: !!user) | server/routes.ts:1343 -> storage.getOrdersByDateRange (storage.ts:725) | Clé ['/api/orders', storeId, 'yyyy-MM'] différente de celle du desktop pour la même donnée. Les erreurs sont avalées (return []). | +| GET /api/deliveries?startDate&endDate&storeId | au montage et au changement de mois | server/routes.ts:1752 -> storage.getDeliveriesByDateRange (storage.ts:939) | Même charge utile lourde que sur desktop pour n'afficher qu'un titre et un statut. | + +**Lisibilité / simplicité :** La page est lisible mais en lecture seule : les cartes ne sont pas cliquables, et on ne peut ni ouvrir une fiche, ni valider, ni créer. Le statut s'affiche brut en anglais ('pending', 'planned', 'delivered'). Les titres 'Cmd #12' et 'Liv #34' sont techniques (orderNumber n'existe pas dans le schéma). Les jours des semaines voisines sont affichés sans données. Il n'y a ni état de chargement ni état d'erreur. + +### `/orders (desktop)` + +**Rôle :** Liste de toutes les commandes fournisseurs du magasin, avec recherche, filtre par statut, pagination, création, modification et suppression. + +**Tâches principales de l'utilisateur :** +- Retrouver une commande (fournisseur, statut) +- Créer une commande +- Modifier une commande +- Supprimer une commande +- Voir le détail et les livraisons liées + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/orders?storeId | au montage et au changement de magasin (fetch() manuel, clé [ordersUrl, storeId]) | server/routes.ts:1343 (branche sans dates, 1372/1450) -> storage.getOrders (storage.ts:678) + loadDeliveriesByOrderIds (storage.ts:522) | Tout l'historique sans LIMIT, puis pagination côté client par 20. Pour un admin en 'tous magasins', ce sont toutes les commandes de tous les magasins. | +| GET /api/groups | au montage | server/routes.ts:970 -> storage.getGroups / user.userGroups | Résultat jamais utilisé dans la page. | +| DELETE /api/orders/:id | au clic sur 'Supprimer définitivement' | server/routes.ts:1655 -> storage.getOrder (storage.ts:783, 2 requêtes) + storage.deleteOrder (storage.ts:880) | Après succès, invalidateQueries puis refetchQueries de toutes les variantes /api/orders et /api/deliveries, y compris celles qui sont inactives. | +| PUT /api/orders/:id | à l'enregistrement de EditOrderModal | server/routes.ts:1620 -> storage.getOrder + storage.updateOrder (storage.ts:871) | Jamais atteint : les arguments de apiRequest sont inversés côté client. | +| POST /api/orders, GET /api/suppliers | ouverture et validation de CreateOrderModal | server/routes.ts:1552 -> storage.createOrder ; server/routes.ts:1141 -> storage.getSuppliers | Liste fournisseurs complète, sans recherche. | + +**Lisibilité / simplicité :** Structure claire (en-tête, filtres, tableau), mais plusieurs défauts. Le libellé 'Groupe' apparaît alors que la navigation dit 'Magasin'. L'identifiant technique '#id' est affiché. Les boutons d'action sont des icônes sans libellé ni info-bulle. Le tableau est en min-w 700px dans un conteneur overflow-hidden, donc tronqué sans défilement sur petit écran. Les couleurs de statut diffèrent du calendrier. Il n'y a ni tri ni filtre de période. La pagination revient à la page 1 après une suppression. La modification d'une commande échoue systématiquement (bug apiRequest). La suppression d'une commande liée à des livraisons renvoie un message générique. Les hooks sont appelés après un return anticipé. Les logs de debug s'exécutent par ligne à chaque frappe. + +### `/orders (mobile)` + +**Rôle :** Liste des commandes en cartes avec recherche et onglets Toutes, En attente, Livrées. + +**Tâches principales de l'utilisateur :** +- Rechercher une commande +- Filtrer par statut + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/orders?storeId | au montage (enabled: !!user) | server/routes.ts:1343 -> storage.getOrders (storage.ts:678) | Tout l'historique, tout rendu sans pagination ni virtualisation. Erreurs avalées (return []). | + +**Lisibilité / simplicité :** Les cartes affichent une flèche '>' mais ne sont pas cliquables, et aucune action n'est possible (icône Plus importée mais jamais utilisée). La date est toujours 'Date inconnue' (champ orderDate inexistant). La recherche par numéro ne fonctionne jamais (orderNumber inexistant). Le statut 'planned' s'affiche brut en anglais. L'accès n'est pas restreint pour les employés, alors que la version desktop les bloque. Une erreur serveur s'affiche comme 'Aucune commande trouvée'. + +### `/deliveries (desktop)` + +**Rôle :** Liste des livraisons du magasin : planifier, modifier, valider la réception (saisie du n° de BL), marquer le contrôle qualité, supprimer. + +**Tâches principales de l'utilisateur :** +- Voir les livraisons à recevoir +- Valider une livraison reçue avec le n° de BL +- Marquer le contrôle comme effectué +- Créer ou modifier une livraison et la lier à une commande +- Supprimer une livraison + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/deliveries?storeId | au montage et au changement de magasin (clé ['/api/deliveries', storeId, user.role]) | server/routes.ts:1752 (branche sans dates 1781/1859) -> storage.getDeliveries (storage.ts:885) + attachOrdersAndCommentCounts | Tout l'historique sans LIMIT, pagination côté client. Filtre withBL fait en JS (routes.ts:1864). | +| GET /api/groups | au montage | server/routes.ts:970 | Résultat jamais utilisé. | +| POST /api/deliveries/:id/validate | ValidateDeliveryModal | server/routes.ts:2604 -> storage.getDelivery (storage.ts:1004 : 4-6 requêtes séquentielles dont getOrder) + validateDelivery + updateOrder | Le contrôle d'accès au magasin n'est fait que pour le rôle manager. | +| PUT /api/deliveries/:id/control | au clic sur l'icône CheckCircle orange, sans confirmation | server/routes.ts:2678 -> storage.getDelivery + markDeliveryControlValidated | Aucun contrôle d'appartenance au magasin. | +| DELETE /api/deliveries/:id | ConfirmDeleteModal | server/routes.ts:2136 -> storage.getDelivery + deleteDelivery | Côté client, /api/orders n'est pas invalidé alors que les commandes embarquent leurs livraisons. | +| POST /api/deliveries, PUT /api/deliveries/:id, GET /api/orders (complet), GET /api/suppliers | modales de création et de modification | server/routes.ts:2072 / 1906 / 1343 -> storage.createDelivery / updateDelivery / getOrders | Tout l'historique des commandes est chargé pour la liste 'Commande liée'. | + +**Lisibilité / simplicité :** Le tableau a 8 colonnes, n'est pas responsive (overflow-hidden, en-tête non adapté au mobile) et ses en-têtes ont deux styles différents (xs gris et sm font-black). Jusqu'à 5 icônes d'action sans libellé, dont deux coches proches pour 'valider la livraison' et 'contrôle effectué'. Le contrôle se valide en un clic sans confirmation. Le filtre 'En attente' ne correspond à aucun statut existant (planned/delivered). Les abréviations 'P' et 'C' sont utilisées pour les quantités. Le libellé 'Groupe' est utilisé à la place de 'Magasin'. Le padding est doublé (p-6 imbriqués). On ne peut pas rechercher par n° de BL. Des logs de debug sont présents. + +### `/deliveries (mobile)` + +**Rôle :** Liste des livraisons en cartes avec recherche et onglets Toutes, En attente, Reçues. + +**Tâches principales de l'utilisateur :** +- Voir les livraisons du jour ou en retard +- Filtrer les livraisons reçues + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/deliveries?storeId | au montage | server/routes.ts:1752 -> storage.getDeliveries (storage.ts:885) | Tout l'historique, rendu intégral, erreurs avalées. | + +**Lisibilité / simplicité :** La page est inutilisable en l'état. expectedDate et deliveryNumber n'existent pas : toutes les dates affichent 'Date inconnue' et les badges 'Aujourd'hui' ou 'En retard' n'apparaissent jamais. Le statut 'received' n'existe pas : l'onglet 'Reçues' est toujours vide et 'En attente' contient aussi les livraisons livrées. L'icône de statut est toujours générique. Aucune action n'est possible : impossible de valider une réception depuis le téléphone, alors que c'est la tâche principale en magasin. + +### `Modales partagées (Calendrier / Commandes / Livraisons)` + +**Rôle :** Formulaires de création, modification, validation, détail et confirmation de suppression des commandes et livraisons. + +**Tâches principales de l'utilisateur :** +- Créer ou modifier une commande ou une livraison +- Lier une livraison à une commande +- Valider une livraison (n° de BL) +- Consulter une fiche et supprimer + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/suppliers | montage de chaque modale de formulaire | server/routes.ts:1141 -> storage.getSuppliers (storage.ts:467) | Refetch à chaque ouverture après 30 s. | +| GET /api/groups | montage de chaque modale de formulaire | server/routes.ts:970 | Admin : toutes les colonnes, logo compris. | +| GET /api/orders (sans paramètre) | montage de CreateDeliveryModal et EditDeliveryModal (queryFn par défaut) | server/routes.ts:1343 -> storage.getOrders (storage.ts:678) | Tout l'historique pour filtrer en JS par fournisseur, magasin et statut. | +| PUT /api/orders/:id | EditOrderModal et le bouton 'Valider Commande' de OrderDetailModal | server/routes.ts:1620 | Cassé depuis EditOrderModal (arguments inversés). | +| POST /api/deliveries/:id/validate | ValidateDeliveryModal | server/routes.ts:2604 | Le BL est obligatoire. Invalidation de stats inefficace. | + +**Lisibilité / simplicité :** Les formulaires sont courts et clairs. En revanche, la liste fournisseurs n'a pas de recherche. Un admin en 'tous magasins' ne peut pas choisir le magasin : le premier est imposé, en lecture seule. Le libellé 'Magasin/Groupe' est ambigu. Les boutons de pied de modale sont alignés tantôt à gauche, tantôt à droite, avec des couleurs incohérentes. La fiche détail utilise un en-tête bleu même pour une livraison, une zone max-h-96 et une modale d'édition empilée par-dessus ; après modification, elle affiche des données périmées. Elle a aussi sa propre confirmation de suppression au lieu de ConfirmDeleteModal. EditDeliveryModal masque la commande liée si celle-ci est livrée. La gestion de session expirée ne fonctionne pas (regex jamais vraie, redirection vers GET /api/login inexistant). Les logs s'exécutent à chaque rendu. + +## Constats + +| ID | Sév. | Catégorie | Effort | Titre | Fichier | +|---|---|---|---|---|---| +| [MOB-01](#mob-01) | haute | ux-simplicite | L | Versions mobiles en lecture seule : impossible d'ouvrir une fiche ou de valider une réception | `client/src/pages/mobile/DeliveriesPage.tsx:163` | +| [MOB-02](#mob-02) | haute | bug | S | Pages mobiles basées sur des champs et statuts inexistants : dates, numéros et filtres cassés | `client/src/pages/mobile/DeliveriesPage.tsx:29` | +| [MOD-01](#mod-01) | haute | perf-api | M | Les modales de livraison téléchargent toutes les commandes pour remplir une liste déroulante | `client/src/components/modals/CreateDeliveryModal.tsx:60` | +| [ORD-01](#ord-01) | haute | bug | S | Modification de commande toujours en échec : arguments de apiRequest inversés | `client/src/components/modals/EditOrderModal.tsx:61` | +| [ORD-02](#ord-02) | haute | perf-api | L | Historique complet chargé sans limite, puis pagination côté navigateur | `client/src/pages/Orders.tsx:73` | +| [SRV-01](#srv-01) | haute | perf-api | S | Logo base64 et configuration SMTP/NocoDB du magasin renvoyés dans chaque ligne de commande ou livraison | `server/storage.ts:693` | +| [SRV-03](#srv-03) | haute | perf-api | S | Aucune compression HTTP, et les assets hashés sont servis sans cache long | `server/index.production.ts:27` | +| [BUNDLE-01](#bundle-01) | moyenne | perf-bundle | M | Toutes les pages, desktop et mobile, sont importées statiquement : bundle initial d'environ 1,5 Mo | `client/src/components/RouterProduction.tsx:5` | +| [CAL-01](#cal-01) | moyenne | bug | S | Le bouton 'mois suivant' saute un mois quand on est le 29, 30 ou 31 | `client/src/pages/Calendar.tsx:171` | +| [CAL-02](#cal-02) | moyenne | perf-client | S | Calculs de debug exécutés en production dans la boucle de rendu | `client/src/components/CalendarGrid.tsx:495` | +| [CAL-03](#cal-03) | moyenne | perf-client | M | Filtrage O(42 × éléments) à chaque rendu, sans mémoïsation | `client/src/components/CalendarGrid.tsx:579` | +| [CAL-04](#cal-04) | moyenne | perf-client | S | Spinner plein écran à chaque changement de mois, et la grille attend les publicités | `client/src/pages/Calendar.tsx:204` | +| [CAL-06](#cal-06) | moyenne | coherence-design | S | La légende ne correspond pas aux couleurs utilisées dans la grille | `client/src/pages/Calendar.tsx:224` | +| [CAL-07](#cal-07) | moyenne | lisibilite | M | Éléments du calendrier illisibles : texte de 11 px, icônes de 8 px, abréviations P/C, statut codé par des points | `client/src/components/CalendarGrid.tsx:75` | +| [CAL-08](#cal-08) | moyenne | ux-simplicite | S | Un clic n'importe où dans une case ouvre 'Création rapide', même sans droits, et le '+' n'est visible qu'au survol | `client/src/pages/Calendar.tsx:181` | +| [CAL-10](#cal-10) | moyenne | ux-simplicite | S | Le panneau de statistiques en position fixe masque le bas du calendrier | `client/src/components/StatsPanel.tsx:54` | +| [CAL-11](#cal-11) | moyenne | bug | M | Panneau 'Statistiques du mois' : chiffres Palettes, Colis et 'en attente' faux | `server/storage.ts:1297` | +| [CAL-15](#cal-15) | moyenne | perf-serveur | S | getPublicities ignore le filtre magasin, charge des groupes complets et regroupe en O(P×N) | `server/storage.ts:1502` | +| [CAL-16](#cal-16) | moyenne | dette-code | M | fetch() manuels dupliqués : aucune gestion du 401 et aucun état d'erreur affiché | `client/src/pages/Calendar.tsx:62` | +| [LIV-01](#liv-01) | moyenne | bug | S | Le filtre 'En attente' des livraisons ne correspond à aucun statut existant | `client/src/pages/Deliveries.tsx:325` | +| [LIV-02](#liv-02) | moyenne | ux-simplicite | S | 'Contrôle effectué' validé en un clic, sans confirmation | `client/src/pages/Deliveries.tsx:484` | +| [MCAL-01](#mcal-01) | moyenne | lisibilite | S | Statuts affichés bruts en anglais et titres techniques 'Cmd #' / 'Liv #' | `client/src/pages/mobile/CalendarPage.tsx:293` | +| [MOB-03](#mob-03) | moyenne | ux-simplicite | S | Les erreurs serveur sont présentées comme 'Aucune commande trouvée' | `client/src/pages/mobile/OrdersPage.tsx:51` | +| [MOD-02](#mod-02) | moyenne | bug | S | Gestion de session expirée inopérante : regex jamais vraie et redirection vers une route inexistante | `client/src/lib/authUtils.ts:2` | +| [MOD-03](#mod-03) | moyenne | ux-simplicite | S | Un admin en 'Tous les magasins' ne peut pas choisir le magasin : le premier est imposé | `client/src/components/modals/CreateOrderModal.tsx:84` | +| [MOD-04](#mod-04) | moyenne | ux-simplicite | M | Choix du fournisseur dans une longue liste sans recherche | `client/src/components/modals/CreateOrderModal.tsx:188` | +| [ORD-03](#ord-03) | moyenne | perf-client | S | Log par ligne à chaque frappe et filtrage sans mémoïsation | `client/src/pages/Orders.tsx:169` | +| [ORD-05](#ord-05) | moyenne | bug | M | Invalidations qui ratent leur cible : clés de cache hétérogènes pour les mêmes données | `client/src/pages/Orders.tsx:76` | +| [ORD-06](#ord-06) | moyenne | perf-client | S | La suppression recharge toutes les variantes en cache, y compris les inactives (invalidate puis refetch) | `client/src/pages/Orders.tsx:141` | +| [ORD-10](#ord-10) | moyenne | accessibilite | S | Actions en icônes seules, sans libellé ni info-bulle, et deux coches ambiguës | `client/src/pages/Deliveries.tsx:453` | +| [ORD-11](#ord-11) | moyenne | ux-simplicite | S | Tableaux tronqués sur écrans étroits : pas de défilement horizontal | `client/src/pages/Orders.tsx:316` | +| [ORD-12](#ord-12) | moyenne | coherence-design | M | Couleurs et libellés de statut différents selon les pages, et couleurs des boutons principaux incohérentes | `client/src/pages/Orders.tsx:190` | +| [ORD-13](#ord-13) | moyenne | ux-simplicite | M | La suppression d'une commande liée à des livraisons échoue avec un message générique | `server/storage.ts:880` | +| [PERM-01](#perm-01) | moyenne | dette-code | M | Deux matrices de permissions divergentes entre client et serveur | `client/src/lib/permissions.ts:1` | +| [SRV-02](#srv-02) | moyenne | perf-api | M | Imbrications redondantes : livraisons complètes dans chaque commande et commande complète dans chaque livraison | `server/storage.ts:522` | +| [SRV-04](#srv-04) | moyenne | perf-serveur | M | Utilisateur et magasins rechargés deux fois par requête API | `server/localAuth.production.ts:195` | +| [SRV-05](#srv-05) | moyenne | perf-serveur | S | Toutes les réponses JSON sont clonées récursivement pour retirer smtpPassword | `server/routes.ts:168` | +| [SRV-07](#srv-07) | moyenne | bug | S | Manager ou directeur multi-magasins sans sélection : seul le premier magasin est renvoyé, en silence | `server/routes.ts:1419` | +| [SRV-08](#srv-08) | moyenne | bug | S | Contrôles d'accès par magasin incomplets (validation, contrôle, lecture, statistiques) | `server/routes.ts:2624` | +| [CAL-05](#cal-05) | basse | bug | S | Les jours des mois voisins sont affichés mais toujours vides | `client/src/pages/Calendar.tsx:33` | +| [CAL-12](#cal-12) | basse | perf-serveur | S | Statistiques mensuelles : 4 requêtes séquentielles, sans cache | `server/storage.ts:1270` | +| [CAL-13](#cal-13) | basse | bug | S | La fenêtre '+X autres' affiche la date de création des commandes au lieu de la date prévue | `client/src/components/CalendarGrid.tsx:324` | +| [CAL-14](#cal-14) | basse | ux-simplicite | M | Seulement 2 éléments par jour, et des modales imbriquées avec des contournements de z-index et d'état | `client/src/components/CalendarGrid.tsx:177` | +| [CAL-17](#cal-17) | basse | perf-client | S | Double chargement au démarrage : les requêtes partent avant la sélection automatique du magasin | `client/src/components/Layout.tsx:70` | +| [MOD-05](#mod-05) | basse | perf-client | S | useEffect et logs exécutés à chaque rendu (dépendance recréée) | `client/src/components/modals/CreateOrderModal.tsx:49` | +| [MOD-06](#mod-06) | basse | perf-client | S | Données de référence rechargées à chaque ouverture de modale | `client/src/lib/queryClient.ts:77` | +| [MOD-07](#mod-07) | basse | dette-code | S | Mutation morte, confirmation de suppression dupliquée et rôles codés en dur | `client/src/components/modals/OrderDetailModal.tsx:70` | +| [MOD-08](#mod-08) | basse | ux-simplicite | M | Modale d'édition empilée sur la fiche, données périmées après modification, contenu limité à 384 px | `client/src/components/modals/OrderDetailModal.tsx:437` | +| [MOD-09](#mod-09) | basse | coherence-design | S | Pieds de modale et boutons d'action incohérents | `client/src/components/modals/CreateOrderModal.tsx:245` | +| [MOD-10](#mod-10) | basse | ux-simplicite | S | Le champ 'Commande liée' disparaît si la commande liée est déjà livrée | `client/src/components/modals/EditDeliveryModal.tsx:191` | +| [ORD-04](#ord-04) | basse | perf-client | S | Requête /api/groups inutilisée | `client/src/pages/Orders.tsx:101` | +| [ORD-07](#ord-07) | basse | bug | S | Hooks appelés après un return anticipé (règle des hooks violée) | `client/src/pages/Orders.tsx:41` | +| [ORD-08](#ord-08) | basse | lisibilite | S | 'Groupe' et 'Magasin/Groupe' au lieu de 'Magasin' | `client/src/pages/Orders.tsx:323` | +| [ORD-09](#ord-09) | basse | lisibilite | S | Identifiants techniques affichés sous des formes différentes selon l'écran | `client/src/components/modals/OrderDetailModal.tsx:209` | +| [ORD-14](#ord-14) | basse | ux-simplicite | S | Retour à la page 1 après chaque suppression, et confirmation fermée avant la fin de l'action | `client/src/components/ui/pagination.tsx:157` | +| [ORD-15](#ord-15) | basse | ux-simplicite | M | Pas de tri, pas de filtre de période, recherche sans n° de BL | `client/src/pages/Deliveries.tsx:176` | +| [SRV-06](#srv-06) | basse | perf-serveur | S | Logs console à chaque appel des chemins chauds | `server/routes.ts:1353` | +| [SRV-09](#srv-09) | basse | perf-serveur | S | Le filtre withBL est appliqué en JavaScript après le chargement complet | `server/routes.ts:1864` | + +### MOB-01 + +**Versions mobiles en lecture seule : impossible d'ouvrir une fiche ou de valider une réception** — ux-simplicite, sévérité haute, effort L + +- **Fichier :** `client/src/pages/mobile/DeliveriesPage.tsx:163` +- **Constat :** DeliveriesPage.tsx:163-195 `` sans onClick, mais avec `` (192) qui suggère un lien. OrdersPage.tsx:146-177 idem : `Plus` est importé (16) et jamais utilisé, aucun bouton de création. CalendarPage.tsx:266-300 : cartes d'événement non cliquables. +- **Impact :** L'employé qui réceptionne un camion avec son téléphone ne peut ni valider la livraison ni saisir le BL. Il doit passer sur un poste fixe. La flèche affichée trompe l'utilisateur, qui touche sans résultat. +- **Recommandation :** Rendre les cartes cliquables et ouvrir une fiche plein écran (Sheet) qui réutilise OrderDetailModal. Ajouter sur chaque livraison 'planned' un bouton visible 'Valider la réception' qui ouvre ValidateDeliveryModal. Ajouter un bouton flottant '+' pour créer (selon les permissions). Retirer le chevron tant que la carte n'est pas cliquable. + +### MOB-02 + +**Pages mobiles basées sur des champs et statuts inexistants : dates, numéros et filtres cassés** — bug, sévérité haute, effort S + +- **Fichier :** `client/src/pages/mobile/DeliveriesPage.tsx:29` +- **Constat :** DeliveriesPage.tsx:27-35 type `{ deliveryNumber?, expectedDate, status }`, ligne 185 `delivery.expectedDate && ... : 'Date inconnue'`, lignes 41/63-64 filtre `'received'`. OrdersPage.tsx:25-33 `{ orderNumber?, orderDate }`, lignes 167-170 `order.orderDate ... : 'Date inconnue'`. Or shared/schema.ts:121-133 (orders : plannedDate, aucun orderNumber/orderDate) et 136-146 (deliveries : scheduledDate, status 'planned, delivered'). Aucune occurrence de orderNumber, deliveryNumber ou expectedDate côté serveur (grep). CalendarPage.tsx:105/113 `Cmd #${order.orderNumber || order.id}`. +- **Impact :** Sur téléphone, toutes les commandes et livraisons affichent 'Date inconnue'. Les badges 'Aujourd'hui' et 'En retard' n'apparaissent jamais. L'onglet 'Reçues' est toujours vide et 'En attente' inclut les livraisons déjà livrées. La recherche par numéro ne trouve rien. Le statut 'planned' s'affiche brut. +- **Recommandation :** Correction minimale et mécanique : expectedDate→scheduledDate, orderDate→plannedDate, filtre 'received'→'delivered' (et 'pending'→status==='planned' pour les livraisons), ajout d'un badge 'planned' ('Planifiée'), suppression de orderNumber/deliveryNumber (afficher l'id). Mutualiser les libellés avec le desktop est un chantier à part (voir ORD-12). + +### MOD-01 + +**Les modales de livraison téléchargent toutes les commandes pour remplir une liste déroulante** — perf-api, sévérité haute, effort M + +- **Fichier :** `client/src/components/modals/CreateDeliveryModal.tsx:60` +- **Constat :** CreateDeliveryModal.tsx:60-62 `useQuery({ queryKey: ['/api/orders'] })` passe par la queryFn par défaut, soit GET /api/orders sans storeId ni dates, donc storage.getOrders() complet avec livraisons imbriquées. Le résultat est ensuite filtré en JS (65-73) par fournisseur, magasin et statut. Même chose dans EditDeliveryModal.tsx:50-61. +- **Impact :** L'ouverture de 'Nouvelle livraison' ou 'Modifier livraison' déclenche le plus gros appel de l'application (toutes les commandes, tous magasins pour un admin). Le formulaire est lent et l'appel est refait à chaque ouverture après 30 s. +- **Recommandation :** Ajouter `GET /api/orders?storeId=&supplierId=&status=pending,planned&light=1` qui renvoie {id, plannedDate, status}. Ne lancer la requête qu'une fois le fournisseur choisi (`enabled: !!formData.supplierId`), avec une clé ['/api/orders', 'open', {storeId, supplierId}]. + +### ORD-01 + +**Modification de commande toujours en échec : arguments de apiRequest inversés** — bug, sévérité haute, effort S + +- **Fichier :** `client/src/components/modals/EditOrderModal.tsx:61` +- **Constat :** EditOrderModal.tsx:61 `await apiRequest("PUT", `/api/orders/${order?.id}`, data);` alors que queryClient.ts:10-15 définit `apiRequest(url: string, method: string = 'GET', body?)`. Le résultat est fetch('PUT', { method: '/api/orders/12' }), qui lève un TypeError (méthode HTTP invalide). C'est le seul appel de ce type dans client/src (grep). +- **Impact :** Aucune commande ne peut être modifiée, ni depuis la page Commandes ni depuis la fiche ouverte dans le calendrier. L'utilisateur voit seulement 'Impossible de modifier la commande'. +- **Recommandation :** Remplacer par `await apiRequest(`/api/orders/${order?.id}`, 'PUT', data);`. Si l'on type method, utiliser `type HttpMethod = 'GET'|'POST'|'PUT'|'PATCH'|'DELETE'` pour ne casser aucun appelant existant. + +### ORD-02 + +**Historique complet chargé sans limite, puis pagination côté navigateur** — perf-api, sévérité haute, effort L + +- **Fichier :** `client/src/pages/Orders.tsx:73` +- **Constat :** Orders.tsx:73 `const ordersUrl = `/api/orders${selectedStoreId ? `?storeId=${selectedStoreId}` : ''}`;` appelle routes.ts:1372/1450 `storage.getOrders(groupIds)` (storage.ts:678-723 : aucun LIMIT, plus toutes les livraisons liées). Ensuite, Orders.tsx:180-188 `usePagination(filteredOrders, 20)`. Même chose pour Deliveries.tsx:80 vers getDeliveries (885-937), et pour OrdersPage.tsx:43-48 et DeliveriesPage.tsx:45-50 sur mobile, qui rendent tout sans pagination. +- **Impact :** Le temps de chargement grandit sans fin avec l'historique. Un admin en 'Tous les magasins' télécharge toutes les commandes de tous les magasins pour n'en voir que 20. La mémoire et le rendu mobile se dégradent. +- **Recommandation :** Créer un endpoint paginé `GET /api/orders?storeId&page&limit&status&search&from&to` qui renvoie { items, total }. Faire le filtre statut, la recherche (ILIKE sur suppliers.name, notes, bl_number) et le tri en SQL, avec par défaut les 3 derniers mois. Côté client : useQuery avec placeholderData: keepPreviousData, ou useInfiniteQuery sur mobile. + +### SRV-01 + +**Logo base64 et configuration SMTP/NocoDB du magasin renvoyés dans chaque ligne de commande ou livraison** — perf-api, sévérité haute, effort S — vérification : partiellement confirmé + +- **Fichier :** `server/storage.ts:693` +- **Constat :** storage.ts:693, 740, 913, 967, 553, 599 : `group: groups,` sélectionne toutes les colonnes de groups, dont shared/schema.ts:67 `logo: text("logo"), // Logo en data URI (data:image/png;base64,...)`, smtpHost, smtpUser, smtpSenderEmail, address, phone, nocodb*. Le groupe est répété pour chaque commande (getOrders), pour chaque livraison imbriquée dans une commande (loadDeliveriesByOrderIds:553) et pour chaque commande imbriquée dans une livraison (loadOrdersByIds:599). Même chose pour les participations de getPublicities (1536). Les pages n'utilisent que group.name et group.color, et BLReconciliation.tsx:298-325 utilise nocodbTableName, nocodbConfigId et webhookUrl. +- **Impact :** Avec un logo de quelques dizaines de Ko, la liste des commandes pèse plusieurs Mo, voire des dizaines. Le chargement des pages Calendrier, Commandes et Livraisons, ainsi que des modales de livraison, devient lent, surtout sur mobile. Des données de configuration inutiles sont aussi exposées au navigateur. +- **Recommandation :** Projeter group: {id, name, color, nocodbConfigId, nocodbTableName, webhookUrl} (sans logo ni smtp*) et supplier: {id, name, email, requiresControl, hasDlc, automaticReconciliation}. Créer des types dédiés (GroupSummary, SupplierSummary) dans OrderWithRelations et DeliveryWithRelations, puis vérifier tous les consommateurs (BLReconciliation, ReconciliationModal, Avoirs, CustomerOrders, Dashboard, exports serveur) avant de livrer. + +### SRV-03 + +**Aucune compression HTTP, et les assets hashés sont servis sans cache long** — perf-api, sévérité haute, effort S — vérification : partiellement confirmé + +- **Fichier :** `server/index.production.ts:27` +- **Constat :** index.production.ts:27-28 n'enregistre que `express.json` et `express.urlencoded`. Aucun middleware compression (absent de package.json) et aucune conf gzip dans le dépôt. Ligne 143 : `app.use('/assets', express.static(join(publicPath, 'assets')));` sans maxAge ni immutable. Le build actuel pèse 1 081 745 octets (index-CCl1yENM.js) + 410 243 octets (vendor-charts) + 120 775 octets de CSS. +- **Impact :** Les JSON de listes (déjà gonflés, voir SRV-01 et SRV-02) et environ 1,7 Mo de JS/CSS transitent non compressés. Les assets sont revalidés à chaque visite. Le premier affichage et chaque navigation sont nettement plus lents, surtout en 4G. +- **Recommandation :** Ajouter `compression` (et @types/compression) aux dépendances et `--external:compression` dans la commande esbuild du Dockerfile, puis `app.use(compression())` avant registerRoutes (index.production.ts et index.ts). Servir /assets avec `{ maxAge: '1y', immutable: true }` et envoyer index.html avec `Cache-Control: no-cache`. Vérifier d'abord la configuration gzip du nginx externe. + +### BUNDLE-01 + +**Toutes les pages, desktop et mobile, sont importées statiquement : bundle initial d'environ 1,5 Mo** — perf-bundle, sévérité moyenne, effort M + +- **Fichier :** `client/src/components/RouterProduction.tsx:5` +- **Constat :** RouterProduction.tsx:5-44 : environ 30 imports de pages en tête (Analytics, BackupManager, Utilities, BLReconciliation, DatabaseDebug, NocoDBConfig, pages mobiles…), sans React.lazy. Le build produit dist/public/assets/index-CCl1yENM.js (1 081 745 octets) et vendor-charts-gtSIDI7m.js (410 243 octets, recharts), tous deux chargés avant le premier affichage du Calendrier ou des Commandes. +- **Impact :** Le premier affichage est lent, surtout sur les postes de magasin et en 4G, et aggravé par l'absence de gzip (SRV-03). +- **Recommandation :** React.lazy par route avec }>, en séparant les arbres desktop et mobile, ET ajouter `window.addEventListener('vite:preloadError', () => window.location.reload())` dans main.tsx, avec index.html servi en no-cache (voir SRV-03). + +### CAL-01 + +**Le bouton 'mois suivant' saute un mois quand on est le 29, 30 ou 31** — bug, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Calendar.tsx:171` +- **Constat :** Calendar.tsx:171-179 `const newDate = new Date(currentDate); ... newDate.setMonth(newDate.getMonth() + 1);`. Le 31 octobre, cela donne le 31 novembre, qui n'existe pas, donc le 1er décembre : novembre est sauté. StatsPanel suit le même currentDate. La version mobile utilise correctement addMonths/subMonths (CalendarPage.tsx:148-151). +- **Impact :** Les utilisateurs qui ouvrent le calendrier en fin de mois ne peuvent pas afficher le mois suivant normalement. Les commandes de ce mois semblent absentes. +- **Recommandation :** Remplacer navigateMonth par `setCurrentDate(d => direction === 'prev' ? subMonths(startOfMonth(d), 1) : addMonths(startOfMonth(d), 1))`. Le bouton 'Aujourd'hui' est un ajout produit, à traiter séparément. + +### CAL-02 + +**Calculs de debug exécutés en production dans la boucle de rendu** — perf-client, sévérité moyenne, effort S + +- **Fichier :** `client/src/components/CalendarGrid.tsx:495` +- **Constat :** CalendarGrid.tsx:495-506 `console.log('📅 All orders dates:', orders.map(o => ({...})))` et 519-525 / 542-548 (log pour chaque élément trouvé, avec `format(date,'yyyy-MM-dd')`). vite.config.ts:29 retire l'appel console.log (`pure`) mais pas ses arguments. Le bundle prod contient bien `s.length>0&&v.getDate()===1&&(s.length,s[0],s.map(S=>{...plannedDate...}))`. Il reste aussi des console.warn dans les boucles (385, 402, 410, 511, 534), et des console.log non protégés par DEV aux lignes 444 et 452. +- **Impact :** Des tableaux et des formatages de dates sont créés inutilement à chaque rendu de la grille (42 cases). Le navigateur peut être inondé de warnings si une donnée est incomplète. La console est inutilisable pour le support. +- **Recommandation :** Supprimer ces blocs de debug (et ceux de Calendar.tsx:53-60, 71-73, 102-109, 120-122, 146-153, 164-166). Garder au plus un console.warn unique hors boucle. + +### CAL-03 + +**Filtrage O(42 × éléments) à chaque rendu, sans mémoïsation** — perf-client, sévérité moyenne, effort M + +- **Fichier :** `client/src/components/CalendarGrid.tsx:579` +- **Constat :** CalendarGrid.tsx:575-580 : dans `paddedDays.map`, `getItemsForDate(date)` parcourt et parse (safeDate) toutes les commandes et toutes les livraisons, et `getPublicitiesForDate(date)` parcourt toutes les publicités de l'année. Aucun useMemo. CalendarGrid n'est pas memo, et l'état des modales est dans Calendar.tsx (26-30), donc l'ouverture de 'Création rapide' ou d'une fiche recalcule toute la grille. Même chose sur mobile : CalendarPage.tsx:139-145 et 224. +- **Impact :** Avec quelques centaines d'éléments par mois, plusieurs dizaines de milliers d'opérations par rendu. L'interface rame à l'ouverture des modales et à la navigation, surtout sur les postes de magasin peu puissants. +- **Recommandation :** Construire une seule fois, avec useMemo, une Map<'yyyy-MM-dd', {orders, deliveries, pubs}> (une passe sur chaque liste). Envelopper CalendarGrid dans React.memo et passer des handlers stables (useCallback). + +### CAL-04 + +**Spinner plein écran à chaque changement de mois, et la grille attend les publicités** — perf-client, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Calendar.tsx:204` +- **Constat :** Calendar.tsx:204 `const isLoading = loadingOrders || loadingDeliveries || loadingPublicities;` puis 282-285 : un spinner remplace toute la grille. Chaque mois a une nouvelle clé (38-41, 87-90), sans placeholderData et sans préchargement des mois voisins. +- **Impact :** La grille disparaît puis réapparaît à chaque clic sur les flèches. L'affichage dépend de la requête la plus lente (publicités, sans rapport avec la logistique), ce qui donne une impression de lenteur. +- **Recommandation :** Ajouter `placeholderData: keepPreviousData` sur les 3 requêtes et afficher la grille avec un indicateur discret (barre fine ou opacité). Ne pas bloquer sur loadingPublicities. Précharger mois-1 et mois+1 avec queryClient.prefetchQuery. Pour le premier chargement, utiliser un squelette de grille plutôt qu'un spinner. + +### CAL-06 + +**La légende ne correspond pas aux couleurs utilisées dans la grille** — coherence-design, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Calendar.tsx:224` +- **Constat :** Calendar.tsx:224-237 : la légende utilise `bg-primary` (bleu vif), `bg-secondary` (vert foncé hsl(120,61%,34%)) et `bg-delivered` (gris). La grille utilise CalendarGrid.tsx:57-61 `bg-gray-400` (livré), `bg-yellow-300` (planifié, absent de la légende), `bg-blue-300` (commande), et 102-105 `bg-green-200` (livraison). Les publicités violettes (658) ne figurent pas dans la légende. +- **Impact :** L'utilisateur ne peut pas décoder le calendrier : il ne sait pas ce que signifient le jaune ou le violet. +- **Recommandation :** Générer la légende à partir des mêmes constantes que la grille et ajouter 'Commande planifiée' (jaune) et 'Publicité' (violet). Voir aussi ORD-12 pour un composant de statut unique. + +### CAL-07 + +**Éléments du calendrier illisibles : texte de 11 px, icônes de 8 px, abréviations P/C, statut codé par des points** — lisibilite, sévérité moyenne, effort M — vérification : partiellement confirmé + +- **Fichier :** `client/src/components/CalendarGrid.tsx:75` +- **Constat :** CalendarGrid.tsx:75 `style={{fontSize: '11px', lineHeight: '1.2'}}`. 81-82 `w-4 h-4 ... ` (et 37 pour le squelette). Il est rendu par Calendar.tsx:335. Il n'est ni repliable ni déplaçable. +- **Impact :** Les cases samedi et dimanche des dernières semaines sont recouvertes : les livraisons qui y figurent ne sont pas visibles. +- **Recommandation :** Intégrer les 4 indicateurs dans un bandeau de KPI sous l'en-tête, ou dans un panneau latéral repliable. + +### CAL-11 + +**Panneau 'Statistiques du mois' : chiffres Palettes, Colis et 'en attente' faux** — bug, sévérité moyenne, effort M + +- **Fichier :** `server/storage.ts:1297` +- **Constat :** storage.ts:1297-1313 : `totalPalettes: SUM(CAST(quantity as INTEGER))` sans filtre sur unit, donc palettes et colis sont additionnés. `totalPackages: COUNT(*)`, affiché 'Colis' (StatsPanel.tsx:85-87), est en fait un nombre de livraisons. `.innerJoin(orders, …)` exclut les livraisons sans commande. pendingResult (1281-1290) n'est pas filtré par mois, mais il est affiché sous 'Statistiques du mois' (StatsPanel.tsx:101-105). Les lignes 1230-1240 renvoient des statistiques fictives (12, 8, 45…) si NODE_ENV==='development'. +- **Impact :** Les managers et directeurs lisent des volumes de palettes et de colis erronés. Ils peuvent prendre de mauvaises décisions et perdre confiance dans l'outil. +- **Recommandation :** Calculer `SUM(quantity) FILTER (WHERE unit='palettes')` et `SUM(quantity) FILTER (WHERE unit='colis')` en leftJoin. Filtrer 'en attente' sur le mois, ou le renommer 'Commandes en attente (toutes dates)'. Supprimer le retour de données fictives. + +### CAL-15 + +**getPublicities ignore le filtre magasin, charge des groupes complets et regroupe en O(P×N)** — perf-serveur, sévérité moyenne, effort S + +- **Fichier :** `server/storage.ts:1502` +- **Constat :** storage.ts:1502 `async getPublicities(year?: number, groupIds?: number[])`. groupIds n'est jamais utilisé dans la requête (seul `eq(publicities.year, year)`). 1532-1541 : participations avec `group: groups` (logo). 1551-1553 : `participations.filter(p => p.publicityId === publicity.id)` pour chaque publicité. Côté client, Calendar.tsx:137-141 charge l'année entière, puis CalendarGrid.tsx:399-455 filtre en JS, pour chacun des 42 jours. +- **Impact :** Les publicités de toute l'année et de tous les magasins sont transférées et traitées à chaque affichage du calendrier. Le calcul est inutile côté serveur comme côté navigateur. +- **Recommandation :** Filtrer en SQL : `publicities.end_date >= :from AND start_date <= :to` et `EXISTS (participation WHERE group_id IN groupIds)`. Projeter group {id, name, color}. Regrouper les participations avec une Map. + +### CAL-16 + +**fetch() manuels dupliqués : aucune gestion du 401 et aucun état d'erreur affiché** — dette-code, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Calendar.tsx:62` +- **Constat :** Calendar.tsx:62-68 `const response = await fetch(url, ...); if (!response.ok) { throw new Error('Failed to fetch orders'); }`. Le message ne contient pas '401', donc il n'y a pas de redirection et queryClient.ts:79-88 relance 2 fois. Aucun rendu d'erreur : `data: orders = []`, ce qui donne un calendrier vide. Même schéma dans StatsPanel.tsx:22-31, Orders.tsx:78-87 et Deliveries.tsx:84-95. Le mobile (OrdersPage.tsx:51, DeliveriesPage.tsx:53, CalendarPage.tsx:67-69) fait `return []` sur erreur. +- **Impact :** Quand le serveur ou la session échoue, l'utilisateur voit 'Aucune commande trouvée' ou un calendrier vide, sans message ni bouton Réessayer, avec 3 fois plus d'appels. +- **Recommandation :** Utiliser la queryFn par défaut (getQueryFn), en construisant l'URL depuis la clé, ou un helper `fetchJson(url)` commun qui lève 'status: texte'. Ajouter un composant sur chaque page. + +### LIV-01 + +**Le filtre 'En attente' des livraisons ne correspond à aucun statut existant** — bug, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Deliveries.tsx:325` +- **Constat :** Deliveries.tsx:325 `En attente`. Le schéma deliveries.status vaut 'planned, delivered' (shared/schema.ts:146) et createDelivery force 'planned' (storage.ts:1099-1102). Le badge 'pending' (199) n'est donc jamais utilisé. +- **Impact :** Le filtre renvoie toujours 'Aucune livraison trouvée'. L'utilisateur conclut à tort qu'il n'a rien à recevoir. +- **Recommandation :** Retirer l'option 'pending' du Select de Deliveries.tsx. Renommer 'planned' en 'À recevoir' est un choix de libellé à valider séparément. + +### LIV-02 + +**'Contrôle effectué' validé en un clic, sans confirmation** — ux-simplicite, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Deliveries.tsx:484` +- **Constat :** Deliveries.tsx:480-490 `onClick={() => handleMarkControlValidated(delivery.id)}` lance directement PUT /api/deliveries/:id/control. L'icône CheckCircle se trouve à côté de l'icône Check de validation de livraison (469-478). Il n'y a pas d'annulation possible. +- **Impact :** Un clic involontaire marque le contrôle qualité comme fait, de façon irréversible depuis l'interface. Cela pose un risque de traçabilité. +- **Recommandation :** Utiliser un bouton texte 'Contrôle fait' avec une confirmation courte (ou une annulation via toast avec 'Annuler'), placé dans la colonne Statut à côté du badge 'Contrôle à faire'. + +### MCAL-01 + +**Statuts affichés bruts en anglais et titres techniques 'Cmd #' / 'Liv #'** — lisibilite, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/mobile/CalendarPage.tsx:293` +- **Constat :** CalendarPage.tsx:288-294 `{event.status}` affiche 'pending', 'planned' ou 'delivered'. Lignes 105 et 113 : `title: `Cmd #${order.orderNumber || order.id}`` et `Liv #…`. +- **Impact :** Des libellés anglais et des abréviations sont présentés à des utilisateurs francophones non techniciens. +- **Recommandation :** Partie automatisable : un helper statusLabel(status) qui renvoie pending→'En attente', planned→'Planifiée', delivered→'Livrée', utilisé dans le Badge, et suppression des références à orderNumber et deliveryNumber. La restructuration titre/sous-titre (fournisseur en titre) est à valider côté produit. + +### MOB-03 + +**Les erreurs serveur sont présentées comme 'Aucune commande trouvée'** — ux-simplicite, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/mobile/OrdersPage.tsx:51` +- **Constat :** OrdersPage.tsx:51 `if (!response.ok) return [];`. DeliveriesPage.tsx:53 idem. CalendarPage.tsx:67-69 et 92-94 idem. Le rendu affiche ensuite l'état vide (OrdersPage.tsx:140-144). +- **Impact :** Lors d'une panne ou d'une session expirée, l'employé pense qu'il n'y a aucune livraison prévue. +- **Recommandation :** Lever une erreur, afficher 'Impossible de charger les données' avec un bouton Réessayer, et rediriger vers /auth en cas de 401. + +### MOD-02 + +**Gestion de session expirée inopérante : regex jamais vraie et redirection vers une route inexistante** — bug, sévérité moyenne, effort S + +- **Fichier :** `client/src/lib/authUtils.ts:2` +- **Constat :** authUtils.ts:2 `return /^401: .*Unauthorized/.test(error.message);` alors que le serveur répond `{ message: "Authentification requise" }` (localAuth.ts:250-254), d'où un message '401: {"message":"Authentification requise"}' qui ne correspond jamais. Par ailleurs `window.location.href = "/api/login"` (CreateOrderModal.tsx:139, EditOrderModal.tsx:81, OrderDetailModal.tsx:58/91/137, CreateDeliveryModal.tsx:170, EditDeliveryModal.tsx:101, ValidateDeliveryModal.tsx:82, Orders.tsx:156, Deliveries.tsx:162) pointe vers une route qui n'existe qu'en POST (localAuth.production.ts:203), et le fallback SPA exclut /api/ (index.production.ts:158-161). +- **Impact :** Quand la session expire, l'utilisateur voit 'Impossible de créer la commande' sans comprendre et peut ressaisir plusieurs fois. Si la branche 401 était atteinte, il tomberait sur la page brute 'Cannot GET /api/login'. +- **Recommandation :** Remplacer la regex par `/^401\b/` ET remplacer les 25 occurrences de "/api/login" par "/auth" (y compris Users.tsx, Suppliers.tsx, Groups.tsx), dans le même changement. Centraliser via MutationCache ensuite. + +### MOD-03 + +**Un admin en 'Tous les magasins' ne peut pas choisir le magasin : le premier est imposé** — ux-simplicite, sévérité moyenne, effort S + +- **Fichier :** `client/src/components/modals/CreateOrderModal.tsx:84` +- **Constat :** CreateOrderModal.tsx:79-85 `defaultGroupId = groups[0].id.toString();` quand aucun magasin n'est sélectionné, puis 202-221 : affichage en lecture seule (aucun Select). CreateDeliveryModal.tsx:99-105 et 265-284 : même chose. +- **Impact :** La commande ou la livraison est créée dans le premier magasin alphabétique, sans possibilité de le changer depuis le formulaire. Cela produit des erreurs de saisie difficiles à repérer. +- **Recommandation :** Si l'utilisateur a plusieurs magasins et qu'aucun n'est sélectionné dans l'en-tête, afficher un Select 'Magasin *' obligatoire, sans valeur par défaut. Sinon, afficher le magasin en lecture seule. + +### MOD-04 + +**Choix du fournisseur dans une longue liste sans recherche** — ux-simplicite, sévérité moyenne, effort M + +- **Fichier :** `client/src/components/modals/CreateOrderModal.tsx:188` +- **Constat :** CreateOrderModal.tsx:188-199, CreateDeliveryModal.tsx:227-238, EditOrderModal.tsx:132-143 et EditDeliveryModal.tsx:155-166 utilisent un ` handleStatusChange(avoir.id, newStatus)} disabled={avoir.nocodbVerified}> (l.994-1009) envoie un PUT immédiatement. Le serveur envoie un webhook quand le statut passe à 'Reçu' (server/routes.ts:3891-3928). Pour un rôle autre qu'admin ou directeur, la même ligne affiche {!canEditDelete && avoir?.status !== 'Reçu' && (Lecture seule)} (l.1136-1138) alors que le menu reste modifiable. +- **Impact :** Un clic accidentel change le statut et envoie une notification irréversible au système externe. La mention 'Lecture seule' contredit un menu modifiable. Un Select large dans chaque ligne alourdit le tableau. +- **Recommandation :** Afficher le statut sous forme de badge coloré (gris 'À demander', bleu 'Demandé', vert 'Reçu'). Ajouter un seul bouton texte contextuel ('Marquer comme demandé' puis 'Marquer comme reçu'), avec confirmation pour 'Reçu'. Supprimer la mention 'Lecture seule'. + +### AVOIRS-07 + +**Jusqu'à 6 boutons-icônes sans texte par ligne et une loupe de 16 px** — ux-simplicite, sévérité moyenne, effort M — vérification : partiellement confirmé + +- **Fichier :** `client/src/pages/Avoirs.tsx:1060` +- **Constat :** La colonne Actions contient Upload (bleu), CheckCircle 'Valider' (vert), XCircle 'Dévalider' (orange), MessageSquare 'Voir le commentaire' (violet), Edit et Trash2 (rouge), tous en className='h-8 w-8 p-0' avec seulement un title= (l.1062-1135). La colonne Vérification contient (l.1047-1053). Le commentaire n'est lisible que dans une modale (l.1102-1112), alors que le mobile l'affiche dans la carte (mobile/AvoirsPage.tsx:268-272). +- **Impact :** Un utilisateur non technicien doit survoler chaque icône pour la comprendre, et le title ne fonctionne pas sur écran tactile. Une cible de 16 px est difficile à cliquer. Les 6 couleurs n'ont pas de légende. +- **Recommandation :** Même recommandation, en corrigeant le constat : jusqu'à 5 icônes par ligne. + +### AVOIRS-08 + +**Onglet « Finalisés » entièrement grisé et semi-transparent** — lisibilite, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Avoirs.tsx:989` +- **Constat :** (l.989), alors que completedAvoirs = avoirs.filter(avoir => avoir.status === 'Reçu' && avoir.nocodbVerified) (l.911). Toutes les lignes de cet onglet sont donc à 75 % d'opacité. Les dates sont déjà en text-gray-500 (l.1033). +- **Impact :** Le contraste est insuffisant (gris 500 à 75 % sur gris 100, sous 4,5:1). L'historique, justement consulté pour des vérifications, devient difficile à lire. +- **Recommandation :** Supprimer opacity-75 et bg-gray-100. Le badge 'Validé' suffit à distinguer ces lignes. + +### AVOIRS-09 + +**Messages techniques (NocoDB, webhook) et erreurs brutes en anglais affichés à l'utilisateur** — lisibilite, sévérité moyenne, effort S — vérification : partiellement confirmé + +- **Fichier :** `client/src/pages/Avoirs.tsx:666` +- **Constat :** 'Ce magasin n'a pas de configuration NocoDB' (l.666) ; 'Aucun webhook configuré pour ce magasin' (l.782) ; 'Avoir traité avec succès via le webhook' (l.821) ; 'Envoi du fichier vers le webhook... {processingSeconds}s' (l.1695). Les mutations font throw new Error('Failed to create avoir') (l.259), 'Failed to edit avoir' (l.297) et 'Failed to delete avoir' (l.332), affichées via description: error.message (l.279, 318, 353). apiRequest produit des erreurs de la forme `${res.status}: ${text}` (client/src/lib/queryClient.ts:3-8), d'où un toast du type '403: {"message":"Insufficient permissions"}' (l.537). +- **Impact :** Les messages sont incompréhensibles pour un employé de magasin, qui ne sait pas quoi faire. +- **Recommandation :** Créer getErrorMessage(err) côté client, utilisé seulement au moment d'afficher le toast. Il lit le code HTTP en tête du message et le traduit (401 « Session expirée », 403 « Vous n'avez pas les droits pour cette action », 404 « Avoir introuvable », 400 « Données invalides », 5xx « Erreur serveur, réessayez »). Ne pas modifier le format des erreurs de apiRequest. Reformuler en français simple les textes 'NocoDB' et 'webhook'. + +### AVOIRS-10 + +**Bouton « Valider » proposé à des rôles que le serveur refuse** — bug, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Avoirs.tsx:1076` +- **Constat :** Côté client : {avoirVerificationResults[avoir.id]?.exists === true && !avoir.nocodbVerified && (` sans `title` ni `aria-label` (Modifier a un `title`, l.773). Le sélecteur rapide de rôle (l.758) n'a pas d'aria-label. Groups.tsx:438-452 : boutons Modifier et Supprimer des cartes sans libellé. Groups.tsx:552-561 : pastilles de couleur avec seulement `title`, sans `aria-pressed` pour la couleur choisie. +- **Impact :** Les lecteurs d'écran annoncent « bouton » sans contexte, et rien au survol ne dit ce que fait la poubelle. +- **Recommandation :** Ajouter `aria-label="Supprimer {nom}"` / `aria-label="Modifier {nom}"` et un `title` équivalent, plus `aria-pressed={formData.color === option.value}` sur les pastilles. + +### USERS-24 + +**Chargement affiché par un spinner central au lieu d'un squelette de la liste** — coherence-design, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Users.tsx:642` +- **Constat :** Users.tsx:642-645 et Groups.tsx:391-394 : `

` dans un bloc `h-64`. +- **Impact :** La page change de forme à l'arrivée des données et le chargement semble plus long, surtout que /api/users est lent (USERS-01). +- **Recommandation :** Afficher 5 lignes `Skeleton` (shadcn) avec la même structure que le tableau ou les cartes. diff --git a/docs/audit-webui/11-suppliers-contacts-analytics.md b/docs/audit-webui/11-suppliers-contacts-analytics.md new file mode 100644 index 0000000..5da7652 --- /dev/null +++ b/docs/audit-webui/11-suppliers-contacts-analytics.md @@ -0,0 +1,690 @@ +# Fournisseurs, Contacts, Statistiques + +_56 constats vérifiés — 11 haute, 24 moyenne, 21 basse._ + +## Pages analysées + +### `/suppliers` + +**Rôle :** Référentiel des fournisseurs (nom, contact, téléphone, email, code ffnancy, mode de paiement, options DLC / rapprochement auto / contrôle) avec un compteur de commandes et livraisons par fournisseur. Menu visible uniquement pour l'admin. + +**Tâches principales de l'utilisateur :** +- Rechercher un fournisseur par nom +- Consulter ses coordonnées et options +- Créer / modifier un fournisseur (admin) +- Supprimer un fournisseur (admin) +- Voir le volume de commandes/livraisons d'un fournisseur + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/suppliers | au montage (queryKey ['/api/suppliers'], queryFn par défaut, staleTime global 30 s) | server/routes.ts:1141 -> storage.getSuppliers() server/storage.ts:467 | Re-lit l'utilisateur en base (storage.getUser, routes.ts:1143) alors que deserializeUser l'a déjà chargé ; filtre ?dlc fait en JS (routes.ts:1151). Liste complète sans pagination (acceptable, table petite). | +| GET /api/stats/by-supplier | au montage, en parallèle | server/routes.ts:1504 -> storage.getOrderDeliveryStatsBySupplier() server/storage.ts:1842 | Agrégat SQL GROUP BY déjà optimisé (Promise.all) ; n'envoie pas storeId : ignore le magasin sélectionné dans l'en-tête ; totaux « depuis toujours ». | +| POST /api/suppliers | clic « Créer » dans la modale | server/routes.ts:1165 -> storage.createSupplier() server/storage.ts:471 | ~12 console.log dont JSON.stringify du body à chaque création (logs serveur non retirés en prod). Erreur Zod 400 non exploitée côté client. | +| PUT /api/suppliers/:id | clic « Modifier » (mise à jour optimiste) | server/routes.ts:1239 -> storage.updateSupplier() server/storage.ts:476 | Autorise admin+manager+directeur alors que l'UI réserve à l'admin ; ZodError renvoyée en 500. Double invalidation onSuccess+onSettled. | +| DELETE /api/suppliers/:id | clic corbeille + confirm() natif | server/routes.ts:1256 -> storage.deleteSupplier() server/storage.ts:485 | Échoue (FK orders/deliveries ON DELETE NO ACTION, init.sql:446-455) dès qu'il existe une commande : message générique « Impossible de supprimer ». | + +**Lisibilité / simplicité :** Grille de cartes très « lourdes » (shadow-lg, icône, #id technique) peu adaptée à 100+ fournisseurs : pas de tri, pas de filtre par option (DLC, contrôle), recherche sur le nom seulement. Le code ffnancy est saisi mais jamais affiché, libellé jargon « Code fournisseur API (liaison ffnancy) ». Bon skeleton de chargement mais aucun état d'erreur (une panne affiche « Commencez par créer votre premier fournisseur »). Doublon fonctionnel avec la colonne Fournisseurs de la page Contacts (mêmes coordonnées, édition possible aux deux endroits avec des droits différents). Suppression impossible dès qu'il y a de l'historique, sans explication. Boutons icône sans libellé accessible. Page rendue sans MobileLayout sur mobile. + +### `/contacts` + +**Rôle :** Annuaire téléphonique : coordonnées des fournisseurs (globales) et contacts libres rattachés à un magasin (dépanneurs, commerciaux, etc.), avec liens tel:/mailto:. + +**Tâches principales de l'utilisateur :** +- Trouver rapidement le téléphone/email d'un fournisseur +- Trouver un contact du magasin +- Ajouter / modifier / supprimer un contact (admin, directeur, manager) +- Mettre à jour les coordonnées d'un fournisseur (admin, directeur) + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/suppliers | au montage (même clé que les autres pages, cache partagé) | server/routes.ts:1141 -> storage.getSuppliers() server/storage.ts:467 | Liste globale complète affichée en premier ; aucun état de chargement. | +| GET /api/contacts?groupId=X (ou /api/contacts) | au montage et à chaque changement de magasin ; queryFn manuelle via apiRequest, sans enabled | server/routes.ts:1273 -> storage.getContacts(groupIds) server/storage.ts:490 | Pour un non-admin le paramètre groupId est ignoré (tous ses magasins) ; si l'utilisateur n'a aucun magasin, groupIds=[] et getContacts renvoie TOUS les contacts. Clé de cache '/api/contacts?groupId=X' non invalidée par les mutations. | +| POST /api/contacts | clic « Créer » | server/routes.ts:1293 -> storage.createContact() server/storage.ts:497 | Aucun contrôle que groupId appartient aux magasins de l'utilisateur. | +| PUT /api/contacts/:id | clic « Modifier » | server/routes.ts:1311 -> storage.updateContact() server/storage.ts:502 | Aucun contrôle d'appartenance du contact ; ZodError -> 500 ; le client écrase groupId par le magasin sélectionné. | +| DELETE /api/contacts/:id | clic corbeille + confirm() | server/routes.ts:1327 -> storage.deleteContact() server/storage.ts:511 | Aucun contrôle d'appartenance : un manager peut supprimer le contact d'un autre magasin par son id. | +| PUT /api/suppliers/:id | modale « Coordonnées — fournisseur » | server/routes.ts:1239 -> storage.updateSupplier() server/storage.ts:476 | Envoie {contact, phone, email} ; invalide ['/api/suppliers'] correctement. | +| GET /api/groups | via Layout/StoreContext (déjà en cache) | server/routes.ts:970 -> storage.getGroups() server/storage.ts:432 (admin) / userGroups (autres) | Pour l'admin renvoie toutes les colonnes de groups dont le logo en data URI base64 et la config SMTP. | + +**Lisibilité / simplicité :** Deux colonnes (Fournisseurs / Autres contacts) claires sur desktop mais, sur mobile, la liste complète des fournisseurs passe avant les contacts du magasin (long défilement). Pas d'état de chargement (les états vides « Aucun contact » s'affichent pendant le chargement) ni d'erreur. Sélecteur de magasin admin propre à la page, en doublon du sélecteur global de l'en-tête. Le bouton « Nouveau contact » disparaît en vue « Tous les magasins » sans explication et le sélecteur « Magasin * » de la modale est ignoré. Les employés (pas de sélecteur dans l'en-tête, pas d'auto-sélection) restent bloqués sur « Sélectionnez un magasin ». Après création/modification/suppression, la liste ne se rafraîchit pas (clé de cache non invalidée). Notes saisies sur une ligne. Page très utile sur téléphone (liens tel:) mais absente du menu mobile et rendue sans MobileLayout. + +### `/analytics` + +**Rôle :** Tableau de bord statistiques (menu « Statistiques ») : KPI commandes/livraisons/taux de rapprochement/montant, courbe temporelle, camembert fournisseurs, barres par magasin, top 5 fournisseurs, exports CSV. + +**Tâches principales de l'utilisateur :** +- Choisir une période (raccourcis ou dates) +- Filtrer par fournisseur / magasin +- Lire les KPI et graphiques +- Exporter les données en CSV + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/analytics/summary | au montage + à chaque changement de filtre (fetch manuel) ; polling 30 s si « Temps réel » | server/routes.ts:6218 -> storage.getAnalyticsSummary() server/storage.ts:3166 | 4 requêtes SQL séquentielles ; la 4e (topStores) fait groups LEFT JOIN orders LEFT JOIN deliveries sans filtre de date (produit cartésien) et son résultat n'est jamais affiché ; totalAmount additionne BL + facture (double comptage). | +| GET /api/analytics/timeseries | au montage + filtres + granularité | server/routes.ts:6250 -> storage.getAnalyticsTimeseries() server/storage.ts:3285 | 2 requêtes séquentielles en SQL brut concaténé (sql.raw) ; format semaine 'YYYY-IW' erroné ; pas de remplissage des trous. | +| GET /api/analytics/by-supplier | au montage + filtres | server/routes.ts:6281 -> storage.getAnalyticsBySupplier() server/storage.ts:3361 | Ignore le filtre fournisseur ; pas de LIMIT alors que le client n'affiche que 5 lignes ; doublonne summary.topSuppliers. | +| GET /api/analytics/by-store | au montage + filtres | server/routes.ts:6310 -> storage.getAnalyticsByStore() server/storage.ts:3400 | AUCUN filtrage par rôle ni par groupIds : tout utilisateur voit l'activité de tous les magasins ; erreurs avalées (return []). | +| GET /api/suppliers | au montage | server/routes.ts:1141 -> storage.getSuppliers() server/storage.ts:467 | Pour la liste déroulante fournisseurs. | +| GET /api/groups | au montage (cache partagé avec Layout) | server/routes.ts:970 -> storage.getGroups() server/storage.ts:432 | Seuls id/name sont utilisés ; l'admin reçoit logo base64 + config SMTP de chaque magasin. | +| GET /api/analytics/export?type=summary\|timeseries\|suppliers\|stores | clic sur un bouton d'export | server/routes.ts:6331 -> getAnalyticsSummary/Timeseries/BySupplier/ByStore | CSV non échappé, séparateur ',' sans BOM, en-têtes anglais ; échec silencieux côté client. | + +**Lisibilité / simplicité :** Page la plus problématique du périmètre. Chaque changement de filtre remplace toute la page par un spinner plein écran (perte du contexte). Les filtres « Fournisseurs »/« Magasins » prétendent être multi-sélection mais ne le sont pas, n'agissent pas sur tous les graphiques, et ignorent le magasin choisi dans l'en-tête. Libellés jargon/anglicismes (« Tableau de bord Analytics » alors que le menu dit « Statistiques », « Granularité », « Taux de réconciliation », bouton « Temps réel/Actualisé »), montants non formatés à la française et sans HT/TTC, calendriers en anglais. Par défaut 365 points journaliers illisibles, axe X brut (« 2026-45 »), camembert dont l'infobulle affiche 0,1,2… au lieu du nom. KPI « Montant total » faux (BL + facture). Exports en double, CSV cassé dans Excel FR, échecs silencieux. États vides affichés pendant le chargement. recharts (410 Ko) chargé pour tous les utilisateurs au démarrage car la page n'est pas lazy. + +### `(non routée) SalesAnalysisPage` + +**Rôle :** Afficher dans une iframe un outil externe d'analyse des ventes dont l'URL est configurée dans les utilitaires. + +**Tâches principales de l'utilisateur :** +- Consulter l'outil d'analyse des ventes +- Recharger l'iframe + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/utilities | au montage | server/routes.ts:333 -> storage.getUtilities() server/storage.ts:3015 | Réservé admin/directeur (403 sinon) ; la page interpréterait le 403 comme « URL non configurée ». | + +**Lisibilité / simplicité :** Code mort : le composant n'est importé nulle part, aucune route ni entrée de menu « Analyse Vente » n'existe. S'il était branché : message trompeur pour manager/employé (403 affiché comme « URL non configurée » avec un bouton vers /utilities réservé admin), iframe en h-screen dans le Layout (double barre de défilement). + +### `(non routée) SalesAnalysisConfig` + +**Rôle :** Formulaire de saisie de l'URL de l'outil d'analyse des ventes. + +**Tâches principales de l'utilisateur :** +- Saisir/enregistrer l'URL de l'outil d'analyse + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/utilities | au montage | server/routes.ts:333 -> storage.getUtilities() server/storage.ts:3015 | Même clé de cache que BackupManager. | +| POST /api/utilities | clic « Sauvegarder » | server/routes.ts:354 -> storage.updateUtilities()/createUtilities() | Aucune validation de format d'URL côté client ou serveur. | + +**Lisibilité / simplicité :** Composant jamais importé (ni dans Utilities.tsx ni dans le routeur) : l'admin ne peut pas configurer l'URL depuis l'interface. Le texte renvoie à un menu « Analyse Vente » inexistant. Impossible d'effacer l'URL proprement (le champ n'est pas resynchronisé si config devient null). + +### `/bl-reconciliation (partie « relances fournisseurs » uniquement : /api/supplier-mail-logs + shared/supplierMail.ts)` + +**Rôle :** Afficher sur l'icône mail de chaque livraison la date de la dernière relance fournisseur envoyée, et construire le contenu du mail de relance (objet, texte, HTML). + +**Tâches principales de l'utilisateur :** +- Voir si une livraison a déjà fait l'objet d'une relance +- Envoyer une relance fournisseur + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/supplier-mail-logs?storeId=X | au montage de BLReconciliation (enabled: !!user) | server/routes.ts:2439 -> storage.getSupplierMailLogs() server/storage.ts:2974 | SELECT * jusqu'à 500 lignes alors que le client ne garde que le dernier envoi 'sent' par livraison ; storeId ignoré pour un directeur. | +| POST /api/deliveries/:id/send-supplier-mail | clic sur l'icône mail | server/routes.ts:2330 -> emailService.sendSupplierDocumentRequest + storage.createSupplierMailLog() server/storage.ts:2969 | Invalide ['/api/supplier-mail-logs'] (préfixe correct). | + +**Lisibilité / simplicité :** Fonctionnellement correct ; le contenu du mail est clair et en français. Points faibles : montant du BL au format « 1234.50 € » au lieu de « 1 234,50 € » dans un mail adressé aux fournisseurs, et indicateur de dernière relance pouvant disparaître pour les livraisons anciennes à cause de la limite de 500 lignes. + +## Constats + +| ID | Sév. | Catégorie | Effort | Titre | Fichier | +|---|---|---|---|---|---| +| [ANA-01](#ana-01) | haute | perf-client | S | Spinner plein écran à chaque changement de filtre : toute la page disparaît | `client/src/pages/Analytics.tsx:140` | +| [ANA-02](#ana-02) | haute | perf-bundle | S | recharts (410 Ko) téléchargé au démarrage par tous les utilisateurs | `client/src/components/RouterProduction.tsx:28` | +| [ANA-03](#ana-03) | haute | perf-serveur | S | Requête topStores en produit cartésien, sans filtre de date, dont le résultat n'est jamais affiché | `server/storage.ts:3242` | +| [ANA-04](#ana-04) | haute | bug | S | KPI « Montant total » faux : BL et facture additionnés | `server/storage.ts:3218` | +| [ANA-05](#ana-05) | haute | bug | S | /api/analytics/by-store sans filtrage par rôle ni par magasin | `server/routes.ts:6310` | +| [ANA-06](#ana-06) | haute | bug | S | Contournement du filtrage par magasin quand la liste filtrée devient vide | `server/routes.ts:6235` | +| [API-01](#api-01) | haute | perf-serveur | M | L'utilisateur est relu en base 2 fois par requête (3 à 4 requêtes SQL d'authentification par appel) | `server/routes.ts:1143` | +| [CONT-01](#cont-01) | haute | bug | S | La liste des contacts ne se rafraîchit pas après création / modification / suppression | `client/src/pages/Contacts.tsx:72` | +| [CONT-02](#cont-02) | haute | bug | S | Un utilisateur sans magasin reçoit les contacts de TOUS les magasins | `server/storage.ts:490` | +| [CONT-03](#cont-03) | haute | bug | S | Création / modification / suppression de contacts sans contrôle du magasin | `server/routes.ts:1311` | +| [CONT-04](#cont-04) | haute | ux-simplicite | S | Les employés restent bloqués sur « Sélectionnez un magasin » sans aucun moyen de choisir | `client/src/components/Layout.tsx:131` | +| [ANA-07](#ana-07) | moyenne | perf-serveur | S | Requêtes SQL indépendantes exécutées en séquence | `server/storage.ts:3211` | +| [ANA-08](#ana-08) | moyenne | perf-api | M | 4 appels analytics séparés avec calculs en double | `client/src/pages/Analytics.tsx:56` | +| [ANA-09](#ana-09) | moyenne | bug | S | Décalage d'un jour sur les périodes (fuseau horaire) et cache jamais réutilisé | `client/src/pages/Analytics.tsx:47` | +| [ANA-10](#ana-10) | moyenne | bug | S | Format de semaine ISO erroné ('YYYY-IW' au lieu de 'IYYY-IW') | `server/storage.ts:3294` | +| [ANA-11](#ana-11) | moyenne | lisibilite | M | Courbe illisible par défaut : 365 points journaliers, dates brutes, trous non comblés | `client/src/pages/Analytics.tsx:31` | +| [ANA-12](#ana-12) | moyenne | bug | S | Camembert : l'infobulle affiche 0, 1, 2… au lieu du nom du fournisseur | `client/src/pages/Analytics.tsx:444` | +| [ANA-13](#ana-13) | moyenne | ux-simplicite | M | Filtres trompeurs : fausse multi-sélection et graphiques qui ignorent certains filtres | `client/src/pages/Analytics.tsx:265` | +| [ANA-14](#ana-14) | moyenne | coherence-design | S | Le magasin choisi dans l'en-tête est ignoré par les Statistiques | `client/src/pages/Analytics.tsx:21` | +| [ANA-15](#ana-15) | moyenne | lisibilite | S | Jargon, anglicismes et montants non formatés à la française | `client/src/pages/Analytics.tsx:159` | +| [ANA-17](#ana-17) | moyenne | ux-simplicite | M | Exports CSV : échec silencieux, boutons en double, fichier inexploitable dans Excel FR | `client/src/pages/Analytics.tsx:123` | +| [ANA-18](#ana-18) | moyenne | ux-simplicite | S | États vides affichés pendant le chargement, aucun état d'erreur | `client/src/pages/Analytics.tsx:407` | +| [API-02](#api-02) | moyenne | perf-api | S | Données de référence (fournisseurs, magasins) refetchées toutes les 30 s / à chaque page ; cache serveur inutilisé | `client/src/lib/queryClient.ts:77` | +| [API-03](#api-03) | moyenne | perf-api | S | /api/groups renvoie à l'admin le logo base64 et la configuration SMTP de chaque magasin | `server/storage.ts:432` | +| [CONT-05](#cont-05) | moyenne | bug | S | Le magasin du contact est écrasé silencieusement à l'enregistrement | `client/src/pages/Contacts.tsx:169` | +| [CONT-06](#cont-06) | moyenne | coherence-design | S | Second sélecteur de magasin propre à la page, en doublon du sélecteur global | `client/src/pages/Contacts.tsx:229` | +| [CONT-07](#cont-07) | moyenne | ux-simplicite | S | Aucun état de chargement ni d'erreur : « Aucun contact » s'affiche pendant le chargement | `client/src/pages/Contacts.tsx:68` | +| [CONT-08](#cont-08) | moyenne | ux-simplicite | M | La liste complète des fournisseurs passe avant les contacts du magasin (mobile) | `client/src/pages/Contacts.tsx:254` | +| [CONT-09](#cont-09) | moyenne | ux-simplicite | S | Contacts, Fournisseurs et Statistiques sur mobile : sans MobileLayout et absents du menu mobile | `client/src/components/RouterProduction.tsx:116` | +| [MAIL-01](#mail-01) | moyenne | perf-api | M | Historique des relances : 500 lignes complètes chargées pour n'utiliser que la dernière par livraison | `server/storage.ts:2974` | +| [SUPP-01](#supp-01) | moyenne | ux-simplicite | M | Fonctionnalité dupliquée avec la colonne Fournisseurs de la page Contacts | `client/src/pages/Suppliers.tsx:369` | +| [SUPP-02](#supp-02) | moyenne | coherence-design | M | Droits fournisseurs incohérents entre UI et serveur | `server/routes.ts:1239` | +| [SUPP-03](#supp-03) | moyenne | ux-simplicite | M | Suppression impossible dès qu'il y a un historique, avec un message générique | `client/src/pages/Suppliers.tsx:262` | +| [SUPP-04](#supp-04) | moyenne | ux-simplicite | S | Erreurs de validation non expliquées à l'utilisateur | `client/src/pages/Suppliers.tsx:93` | +| [SUPP-05](#supp-05) | moyenne | ux-simplicite | M | Grille de cartes lourde, recherche limitée au nom, ni tri ni filtres | `client/src/pages/Suppliers.tsx:276` | +| [ANA-16](#ana-16) | basse | lisibilite | S | Calendriers en anglais, popover qui ne se ferme pas, pas de contrôle début ≤ fin | `client/src/pages/Analytics.tsx:221` | +| [ANA-19](#ana-19) | basse | ux-simplicite | S | Bouton « Temps réel » confus et partiel | `client/src/pages/Analytics.tsx:71` | +| [ANA-20](#ana-20) | basse | dette-code | S | fetch() manuels, logs de debug et variables mortes | `client/src/pages/Analytics.tsx:58` | +| [ANA-21](#ana-21) | basse | dette-code | M | SQL brut concaténé et erreurs avalées dans timeseries / by-store | `server/storage.ts:3297` | +| [ANA-22](#ana-22) | basse | ux-simplicite | S | En-tête non responsive et rendu sans navigation sur mobile | `client/src/pages/Analytics.tsx:154` | +| [ANA-23](#ana-23) | basse | bug | S | Taux de rapprochement calculé sur toutes les livraisons, y compris planifiées | `server/storage.ts:3260` | +| [API-04](#api-04) | basse | dette-code | S | Logs serveur verbeux à chaque création de fournisseur | `server/routes.ts:1167` | +| [API-05](#api-05) | basse | bug | S | Erreurs de validation renvoyées en 500 sur PUT fournisseur / contact ; filtre DLC en JS | `server/routes.ts:1250` | +| [CONT-10](#cont-10) | basse | perf-client | S | Requête contacts non conditionnée et queryFn redondante | `client/src/pages/Contacts.tsx:76` | +| [CONT-11](#cont-11) | basse | ux-simplicite | S | Champ « Notes » sur une seule ligne | `client/src/pages/Contacts.tsx:536` | +| [MAIL-02](#mail-02) | basse | lisibilite | S | Montant du BL au format anglo-saxon dans le mail envoyé au fournisseur | `shared/supplierMail.ts:85` | +| [SALES-01](#sales-01) | basse | dette-code | S | Pages Analyse des ventes jamais branchées (code mort) et texte trompeur | `client/src/pages/SalesAnalysisConfig.tsx:75` | +| [SALES-02](#sales-02) | basse | ux-simplicite | S | Si branchée : 403 présenté comme « URL non configurée », iframe en h-screen, URL non validée | `client/src/pages/SalesAnalysisPage.tsx:39` | +| [SUPP-06](#supp-06) | basse | lisibilite | S | ID technique affiché, code ffnancy caché, libellés jargon | `client/src/pages/Suppliers.tsx:380` | +| [SUPP-07](#supp-07) | basse | ux-simplicite | S | Une erreur réseau s'affiche comme « Aucun fournisseur – créez votre premier fournisseur » | `client/src/pages/Suppliers.tsx:344` | +| [SUPP-08](#supp-08) | basse | perf-client | S | Recherche de stats en O(n×m) à chaque frappe, filtres non mémoïsés | `client/src/pages/Suppliers.tsx:280` | +| [SUPP-09](#supp-09) | basse | perf-api | S | Double invalidation après modification d'un fournisseur | `client/src/pages/Suppliers.tsx:156` | +| [SUPP-10](#supp-10) | basse | dette-code | S | Logs de debug et branche 401 morte dans les mutations | `client/src/pages/Suppliers.tsx:67` | +| [SUPP-11](#supp-11) | basse | coherence-design | S | Compteurs Commandes/Livraisons ambigus et indépendants du magasin sélectionné | `client/src/pages/Suppliers.tsx:61` | +| [SUPP-12](#supp-12) | basse | accessibilite | S | Boutons icône sans libellé ni infobulle, petits textes à faible contraste | `client/src/pages/Suppliers.tsx:385` | +| [SUPP-13](#supp-13) | basse | dette-code | S | Casts `as any` inutiles sur email/codefou | `client/src/pages/Suppliers.tsx:242` | + +### ANA-01 + +**Spinner plein écran à chaque changement de filtre : toute la page disparaît** — perf-client, sévérité haute, effort S + +- **Fichier :** `client/src/pages/Analytics.tsx:140` +- **Constat :** Analytics.tsx:57 `queryKey: ['/api/analytics/summary', queryParams]` sans placeholderData ; l.140-149 `if (summaryLoading) { return (
...Chargement des statistiques...` — chaque nouvelle clé (nouvelle période, fournisseur, magasin) repasse isLoading à true. +- **Impact :** Au moindre clic sur « Ce mois » ou un fournisseur, filtres, KPI et graphiques sont remplacés par un spinner sur toute la hauteur : effet de clignotement, perte de repère, popovers fermés. +- **Recommandation :** Ajouter `placeholderData: keepPreviousData` à toutes les requêtes analytics, n'afficher le spinner plein écran qu'au premier chargement (`isLoading && !summary`) et utiliser `isFetching` pour une opacité réduite / skeleton par carte. + +### ANA-02 + +**recharts (410 Ko) téléchargé au démarrage par tous les utilisateurs** — perf-bundle, sévérité haute, effort S — vérification : partiellement confirmé + +- **Fichier :** `client/src/components/RouterProduction.tsx:28` +- **Constat :** RouterProduction.tsx:28 `import Analytics from "@/pages/Analytics";` (import statique, aucun React.lazy dans le routeur) ; Analytics.tsx:14 est le seul import de recharts ; vite.config.ts manualChunks `"vendor-charts": ["recharts"]` ; dist/public/index.html contient `modulepreload ... /assets/vendor-charts-gtSIDI7m.js` (410 243 octets). +- **Impact :** Même un employé qui n'a pas accès aux Statistiques télécharge et parse ~410 Ko de JS au premier chargement (et l'index fait 1,08 Mo) : démarrage plus lent, surtout sur mobile. +- **Recommandation :** `const Analytics = lazy(() => import("@/pages/Analytics"))` + (desktop et mobile) ET corriger vite.config.ts : retirer l'entrée "vendor-charts" de manualChunks (ou ajouter "clsx"/"tailwind-merge" dans un chunk vendor chargé par l'entrée). Vérifier après `vite build` que dist/public/index.html ne contient plus de modulepreload vers le chunk recharts. Généraliser aux autres pages dans un second temps. + +### ANA-03 + +**Requête topStores en produit cartésien, sans filtre de date, dont le résultat n'est jamais affiché** — perf-serveur, sévérité haute, effort S — vérification : partiellement confirmé + +- **Fichier :** `server/storage.ts:3242` +- **Constat :** storage.ts:3242-3258 `db.select({... COUNT(DISTINCT orders.id), COUNT(DISTINCT deliveries.id)}).from(groups).leftJoin(orders, eq(groups.id, orders.groupId)).leftJoin(deliveries, eq(groups.id, deliveries.groupId))` : pour chaque magasin, nb_commandes × nb_livraisons lignes, sans les conditions de date/fournisseur. `grep topStores client/src` : aucune utilisation ; avgDeliveryDelay (l.3220) non affiché non plus. +- **Impact :** C'est la requête la plus coûteuse de la page (millions de lignes intermédiaires dès quelques milliers de commandes par magasin), exécutée à chaque filtre et toutes les 30 s en mode « Temps réel », pour un résultat jeté. +- **Recommandation :** Supprimer topStores (seule requête coûteuse) de getAnalyticsSummary dans IStorage, DatabaseStorage et MemStorage, en acceptant/annonçant la disparition de cette colonne JSON de l'export Résumé (cf. ANA-17) ; ou, si l'export doit la garder, la calculer via des sous-requêtes agrégées filtrées comme getAnalyticsByStore. avgDeliveryDelay peut rester. + +### ANA-04 + +**KPI « Montant total » faux : BL et facture additionnés** — bug, sévérité haute, effort S + +- **Fichier :** `server/storage.ts:3218` +- **Constat :** storage.ts:3218 `totalAmount: sql`COALESCE(SUM(CAST(bl_amount AS NUMERIC)), 0) + COALESCE(SUM(CAST(invoice_amount AS NUMERIC)), 0)`` alors que le top fournisseurs (l.3230) n'utilise que `SUM(blAmount)`. Affiché tel quel Analytics.tsx:387. +- **Impact :** Pour une livraison rapprochée (BL ≈ facture), le montant est compté deux fois : le KPI est environ le double de la réalité et incohérent avec la somme du top fournisseurs. Décisions prises sur un chiffre faux. +- **Recommandation :** Définir le montant de référence (ex. COALESCE(invoice_amount, bl_amount) par ligne) et l'appliquer partout ; indiquer HT/TTC dans le libellé. + +### ANA-05 + +**/api/analytics/by-store sans filtrage par rôle ni par magasin** — bug, sévérité haute, effort S + +- **Fichier :** `server/routes.ts:6310` +- **Constat :** routes.ts:6317-6323 `const filters = { startDate, endDate, supplierIds }; const byStore = await storage.getAnalyticsByStore(filters);` — aucun bloc « Apply role-based filtering », groupIds non lu ; storage.ts:3400-3455 interroge `FROM groups g` sans WHERE sur g.id. +- **Impact :** Un manager voit le nom et l'activité de tous les magasins de l'enseigne ; le filtre « Magasins » de la page n'a aucun effet sur le graphique « Performance par magasin ». +- **Recommandation :** Appliquer le même filtrage par rôle que les autres routes, ajouter `groupIds` à getAnalyticsByStore (`WHERE g.id = ANY(...)`). + +### ANA-06 + +**Contournement du filtrage par magasin quand la liste filtrée devient vide** — bug, sévérité haute, effort S + +- **Fichier :** `server/routes.ts:6235` +- **Constat :** routes.ts:6235-6240 `filters.groupIds = filters.groupIds ? filters.groupIds.filter((id) => userGroupIds.includes(id)) : userGroupIds;` puis storage.ts:3200 `if (filters.groupIds?.length) { ...inArray(...) }`. Si l'utilisateur passe `groupIds=999` (magasin qui n'est pas le sien) ou n'a aucun magasin, le tableau devient [] et AUCUN filtre n'est appliqué. Même motif l.6266 (timeseries), l.6295 (by-supplier), et dans /export. Le directeur est en outre exempté de tout filtrage (l.6235) alors que /api/groups le limite à ses magasins (l.976-985). +- **Impact :** Fuite de données : un manager obtient les statistiques de toute l'enseigne avec un simple paramètre d'URL ; incohérence de périmètre pour les directeurs. +- **Recommandation :** Si la liste filtrée est vide pour un non-admin, renvoyer immédiatement un résultat vide (ou 403) ; factoriser via `resolveStatsGroupIds` (routes.ts:1469) déjà utilisé par /api/stats/* ; décider explicitement du périmètre du directeur. + +### API-01 + +**L'utilisateur est relu en base 2 fois par requête (3 à 4 requêtes SQL d'authentification par appel)** — perf-serveur, sévérité haute, effort M + +- **Fichier :** `server/routes.ts:1143` +- **Constat :** localAuth.ts:141-143 `passport.deserializeUser(async (id) => { const user = await storage.getUserWithGroups(id); ...})` charge déjà l'utilisateur + groupes à chaque requête ; puis chaque handler recommence : routes.ts:1143 `storage.getUser(...)`, 1275, 1506, 6220, 6252, 6283, 6312 `storage.getUserWithGroups(...)`. storage.ts:371-373 getUserWithGroups fait 2 requêtes séquentielles (`await this.getUser(id)` puis le SELECT user_groups JOIN groups). +- **Impact :** La page Statistiques déclenche 6 appels -> ~22 requêtes SQL rien que pour l'authentification avant tout calcul métier ; latence ajoutée sur chaque appel API de l'application. +- **Recommandation :** Remplacer, en tête de handler uniquement, `await storage.getUser(...)` / `await storage.getUserWithGroups(...)` par `req.user` (garder les relectures faites APRÈS une mise à jour de l'utilisateur) ; fusionner getUserWithGroups en une seule requête LEFT JOIN. Ne PAS ajouter de cache mémoire par userId. + +### CONT-01 + +**La liste des contacts ne se rafraîchit pas après création / modification / suppression** — bug, sévérité haute, effort S + +- **Fichier :** `client/src/pages/Contacts.tsx:72` +- **Constat :** Contacts.tsx:72-74 `const contactsQueryKey = effectiveGroupId ? [`/api/contacts?groupId=${effectiveGroupId}`] : ["/api/contacts"];` alors que les mutations font (l.90, 102, 114) `queryClient.invalidateQueries({ queryKey: ["/api/contacts"] });`. TanStack compare élément par élément : '/api/contacts?groupId=3' !== '/api/contacts', donc la requête n'est jamais invalidée dès qu'un magasin est sélectionné (cas de tous les non-admins). +- **Impact :** L'utilisateur crée un contact, voit le toast « Contact créé avec succès » mais le contact n'apparaît pas ; il recrée souvent un doublon ou pense que l'enregistrement a échoué. Idem pour une suppression : la carte reste affichée. +- **Recommandation :** Utiliser une clé structurée `['/api/contacts', { groupId: effectiveGroupId }]` (queryFn construisant l'URL) et garder `invalidateQueries({ queryKey: ['/api/contacts'] })`, qui matchera alors par préfixe. Idéalement mettre aussi à jour le cache via setQueryData dans onSuccess. + +### CONT-02 + +**Un utilisateur sans magasin reçoit les contacts de TOUS les magasins** — bug, sévérité haute, effort S + +- **Fichier :** `server/storage.ts:490` +- **Constat :** routes.ts:1279-1281 `if (user.role !== 'admin') { groupIds = (user.userGroups || []).map((ug: any) => ug.groupId); }` puis storage.ts:490-494 `if (groupIds && groupIds.length > 0) { ...where(inArray(contacts.groupId, groupIds)) } return await db.select().from(contacts).orderBy(contacts.name);` — un tableau vide fait tomber dans la branche « tout renvoyer ». +- **Impact :** Fuite de données : un employé/manager non encore rattaché à un magasin voit l'annuaire complet de toute l'enseigne. +- **Recommandation :** Dans la route, `if (user.role !== 'admin' && groupIds.length === 0) return res.json([])` ; dans les DEUX implémentations de getContacts (DatabaseStorage l.490 et MemStorage l.3719), traiter `groupIds !== undefined && groupIds.length === 0` comme « aucun contact ». + +### CONT-03 + +**Création / modification / suppression de contacts sans contrôle du magasin** — bug, sévérité haute, effort S + +- **Fichier :** `server/routes.ts:1311` +- **Constat :** routes.ts:1293-1338 : POST, PUT et DELETE vérifient seulement le rôle `['admin', 'directeur', 'manager'].includes(user.role)` puis appellent `storage.createContact(data)` / `updateContact(id, data)` / `deleteContact(id)` sans vérifier que `data.groupId` ou le contact ciblé appartient aux magasins de l'utilisateur. +- **Impact :** Un manager du magasin A peut créer, modifier ou supprimer les contacts du magasin B (simple appel avec un id). Risque de perte de données entre magasins. +- **Recommandation :** Charger le contact (getContact(id)) et vérifier `userGroupIds.includes(contact.groupId)` pour PUT/DELETE ; vérifier `data.groupId` pour POST et PUT (sauf admin). Renvoyer 403 sinon. + +### CONT-04 + +**Les employés restent bloqués sur « Sélectionnez un magasin » sans aucun moyen de choisir** — ux-simplicite, sévérité haute, effort S + +- **Fichier :** `client/src/components/Layout.tsx:131` +- **Constat :** Layout.tsx:131 le sélecteur n'existe que pour `user.role === 'admin' || 'directeur' || 'manager'` ; Layout.tsx:70 l'auto-sélection ne concerne que `directeur`/`manager`. Contacts.tsx:41 `effectiveGroupId = isAdmin ? adminGroupId : selectedStoreId` et l.355-359 affichent « Sélectionnez un magasin pour voir les contacts ». Or Sidebar.tsx:323-326 donne l'accès Contacts au rôle `employee`. +- **Impact :** Sur desktop, un employé dont le magasin n'est pas déjà mémorisé dans localStorage ne voit jamais les contacts de son magasin, alors que la page lui est proposée dans le menu. +- **Recommandation :** Auto-sélectionner le magasin pour tout rôle n'ayant qu'un magasin (y compris employee) dans Layout.tsx et MobileApp.tsx ; à défaut, afficher directement les contacts de tous les magasins de l'utilisateur (ce que renvoie déjà le serveur) au lieu du message bloquant. + +### ANA-07 + +**Requêtes SQL indépendantes exécutées en séquence** — perf-serveur, sévérité moyenne, effort S + +- **Fichier :** `server/storage.ts:3211` +- **Constat :** getAnalyticsSummary : storage.ts:3213 `await orderQuery`, 3223 `await deliveryQuery`, 3236 `await supplierQuery`, 3255 `await storeQuery` ; getAnalyticsTimeseries : 3333 `await db.execute(sql.raw(ordersSql))` puis 3344 `await db.execute(sql.raw(deliveriesSql))`. +- **Impact :** Le temps de réponse est la somme des requêtes au lieu du maximum. +- **Recommandation :** `const [[orderCount], [deliveryStats], topSuppliers] = await Promise.all([...])` ; idem pour les deux requêtes de timeseries (pattern déjà utilisé dans getOrderDeliveryStatsBySupplier l.1861). + +### ANA-08 + +**4 appels analytics séparés avec calculs en double** — perf-api, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Analytics.tsx:56` +- **Constat :** Analytics.tsx:56-112 : summary, timeseries, by-supplier, by-store, chacun avec fetch manuel ; summary.topSuppliers (storage.ts:3226-3239) et by-supplier (storage.ts:3361-3397) exécutent la même agrégation deliveries JOIN suppliers ; by-supplier n'a pas de LIMIT alors que le client fait `bySupplier.slice(0, 5)` (l.445). +- **Impact :** 4 allers-retours + 4 ré-authentifications (API-01) et une agrégation calculée deux fois à chaque filtre. +- **Recommandation :** Créer un endpoint unique GET /api/analytics/overview qui exécute les agrégats en Promise.all et renvoie {kpis, timeseries, bySupplier(top 5), byStore} ; supprimer topSuppliers du summary ou by-supplier. + +### ANA-09 + +**Décalage d'un jour sur les périodes (fuseau horaire) et cache jamais réutilisé** — bug, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Analytics.tsx:47` +- **Constat :** Analytics.tsx:38 `startOfMonth(new Date())` (minuit heure locale) puis l.47 `params.append('startDate', dateRange.from.toISOString())` ; serveur storage.ts:3188 `filters.startDate.toISOString().split('T')[0]`. À Paris (UTC+2), le 01/10 00:00 devient '2026-09-30'. « Aujourd'hui » entre 0 h et 2 h interroge la veille. Les raccourcis utilisent `new Date()` avec millisecondes : la clé de cache change à chaque clic. +- **Impact :** « Ce mois » inclut le dernier jour du mois précédent, « Cette semaine » le dimanche précédent ; chiffres légèrement faux et refetch systématique même en recliquant sur la même période. +- **Recommandation :** Envoyer des dates calendaires `format(date, 'yyyy-MM-dd')` (clé de cache stable) et les utiliser telles quelles côté serveur (sans toISOString). + +### ANA-10 + +**Format de semaine ISO erroné ('YYYY-IW' au lieu de 'IYYY-IW')** — bug, sévérité moyenne, effort S + +- **Fichier :** `server/storage.ts:3294` +- **Constat :** storage.ts:3293-3294 `const dateFormat = granularity === 'day' ? 'YYYY-MM-DD' : granularity === 'week' ? 'YYYY-IW' : 'YYYY-MM';` — en PostgreSQL TO_CHAR('2024-12-30','YYYY-IW') donne '2024-01'. +- **Impact :** Les jours de fin décembre sont agrégés dans la « semaine 01 » de la mauvaise année et fusionnés avec début janvier : pic faux sur la courbe. +- **Recommandation :** Utiliser 'IYYY-IW' (et un libellé lisible « Sem. 45 – 2026 » côté client). + +### ANA-11 + +**Courbe illisible par défaut : 365 points journaliers, dates brutes, trous non comblés** — lisibilite, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Analytics.tsx:31` +- **Constat :** Analytics.tsx:25 période par défaut `addDays(new Date(), -365)` et l.31 `granularity` = 'day' ; l.411 `` affiche '2026-03-14' ou '2026-45' ; storage.ts:3347-3357 ne renvoie que les dates ayant des données (pas de zéros), la ligne relie des jours non consécutifs. +- **Impact :** Graphique en « peigne » impossible à lire, axe X technique, tendances trompeuses. +- **Recommandation :** Granularité automatique selon la durée (≤31 j : jour, ≤6 mois : semaine, sinon mois), tickFormatter en français (« 14 mars », « sept. 2026 »), remplissage des périodes vides à 0 (generate_series côté SQL). + +### ANA-12 + +**Camembert : l'infobulle affiche 0, 1, 2… au lieu du nom du fournisseur** — bug, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Analytics.tsx:444` +- **Constat :** Analytics.tsx:444-453 ` entry.supplierName} outerRadius={80} dataKey="deliveries">` sans `nameKey` ; recharts 2.15.4 Pie.js:520 `var name = getValueByDataKey(entry, nameKey, i)` avec nameKey par défaut 'name' -> l'index. Les labels de noms longs débordent d'une carte de 300 px. +- **Impact :** Au survol l'utilisateur lit « 0 : 34 » : impossible de savoir de quel fournisseur il s'agit ; labels qui se chevauchent. +- **Recommandation :** Auto-implémenter uniquement `nameKey="supplierName"` sur le ; la légende, le retrait des labels et le diagramme en barres relèvent de la refonte visuelle. + +### ANA-13 + +**Filtres trompeurs : fausse multi-sélection et graphiques qui ignorent certains filtres** — ux-simplicite, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Analytics.tsx:265` +- **Constat :** Analytics.tsx:266-277 le Select est mono-valeur ; cliquer un autre fournisseur fait `setSelectedSuppliers([supplierId])` (remplacement), la valeur "multiple" n'a aucun SelectItem. l.30 `selectedStatus` n'est jamais modifié. routes.ts:6288-6292 by-supplier ne lit pas supplierIds ; routes.ts:6317-6321 by-store ne lit pas groupIds. +- **Impact :** Le « ✓ » laisse croire à une sélection multiple qui n'existe pas ; en filtrant sur un fournisseur, le camembert continue d'afficher les 5 fournisseurs, en filtrant sur un magasin, le graphique magasins reste inchangé : résultats contradictoires à l'écran. +- **Recommandation :** Soit un vrai multi-select (Popover + Checkbox), soit un Select simple sans « ✓ » ; appliquer tous les filtres à tous les endpoints (ou masquer/griser le graphique non concerné) ; supprimer selectedStatus. + +### ANA-14 + +**Le magasin choisi dans l'en-tête est ignoré par les Statistiques** — coherence-design, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Analytics.tsx:21` +- **Constat :** Analytics.tsx:21 `const { selectedStoreId } = useStore();` n'est jamais utilisé ; l.29 `selectedStores` démarre vide et l.296-327 propose un troisième sélecteur « Magasins ». +- **Impact :** Un manager qui travaille sur « Magasin A » (en-tête) voit des statistiques multi-magasins sans s'en rendre compte. +- **Recommandation :** Initialiser/synchroniser le filtre magasin avec selectedStoreId (ou supprimer le filtre local et utiliser uniquement le sélecteur global, comme les autres pages). + +### ANA-15 + +**Jargon, anglicismes et montants non formatés à la française** — lisibilite, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Analytics.tsx:159` +- **Constat :** Analytics.tsx:159 « Tableau de bord Analytics » alors que le menu dit « Statistiques » (Sidebar.tsx:283) ; l.250 « Granularité » ; l.365 « Taux de réconciliation » (le reste de l'app dit « Rapprochement ») ; l.171 bouton « Temps réel » qui devient « Actualisé » ; l.387 `${summary.totalAmount.toFixed(2)}€` et l.534 `{supplier.amount.toFixed(2)}€` -> « 123456.70€ » ; aucune mention HT/TTC. +- **Impact :** Termes techniques ou anglais, chiffres difficiles à lire pour un employé/manager ; incohérence de vocabulaire entre menu et page. +- **Recommandation :** Titre « Statistiques », « Regrouper par : jour/semaine/mois », « Livraisons rapprochées (%) » ; formater avec `new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' })` et préciser HT ; ajouter une courte aide (icône i) sous chaque KPI. + +### ANA-17 + +**Exports CSV : échec silencieux, boutons en double, fichier inexploitable dans Excel FR** — ux-simplicite, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Analytics.tsx:123` +- **Constat :** Analytics.tsx:123-138 `if (response.ok) {...}` sans else ni toast ; l.173-180 « Export CSV » et l.566 « Export Résumé » appellent tous deux handleExport('summary'). routes.ts:6375-6381 `Object.keys(data[0]).join(',')` / `Object.values(item).map(val => typeof val === 'object' ? JSON.stringify(val) : val).join(',')` : pas d'échappement, séparateur ',' sans BOM UTF-8, en-têtes anglais (totalOrders...), tableau topSuppliers en JSON dans une cellule ; l'export « timeseries » force `granularity: 'day'` (l.6366) quelle que soit l'option choisie. +- **Impact :** Clic sans effet si pas de données (404), accents cassés et colonnes décalées à l'ouverture dans Excel français (séparateur ';'), noms de fournisseurs contenant une virgule qui cassent le fichier. +- **Recommandation :** Un seul menu « Exporter ▾ » ; côté serveur, CSV avec ';', BOM '', guillemets échappés et en-têtes français (ou XLSX) ; respecter la granularité ; toast d'erreur/« aucune donnée » côté client. + +### ANA-18 + +**États vides affichés pendant le chargement, aucun état d'erreur** — ux-simplicite, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Analytics.tsx:407` +- **Constat :** Analytics.tsx:74-112 timeseries/bySupplier/byStore sans isLoading ni isError ; l.407-427 `Array.isArray(timeseries) && timeseries.length > 0 ? ... : « Aucune donnée temporelle – Vérifiez la période sélectionnée »` (idem l.441-469, 483-509). Une erreur summary laisse les KPI à 0 (l.342 `summary?.totalOrders || 0`). +- **Impact :** L'utilisateur lit « Aucune donnée » et change de période alors que les données arrivent ; une panne est présentée comme « 0 commande ». +- **Recommandation :** Skeleton dans chaque carte pendant le chargement, message d'erreur explicite avec « Réessayer », et n'afficher l'état vide qu'une fois la requête réussie. + +### API-02 + +**Données de référence (fournisseurs, magasins) refetchées toutes les 30 s / à chaque page ; cache serveur inutilisé** — perf-api, sévérité moyenne, effort S + +- **Fichier :** `client/src/lib/queryClient.ts:77` +- **Constat :** queryClient.ts:77 `staleTime: 30 * 1000` global avec `refetchOnMount: true` ; ['/api/suppliers'] est utilisé par ~14 composants (Avoirs, SavTickets, BLReconciliation, modales commande/livraison...). server/cache.ts:53 `export function cacheMiddleware(...)` n'est importé nulle part (grep vide dans server/). +- **Impact :** Chaque navigation après 30 s relance /api/suppliers et /api/groups (avec ré-authentification, cf. API-01) pour des données qui changent quelques fois par mois. +- **Recommandation :** Définir `staleTime: 10 * 60 * 1000` sur les clés ['/api/suppliers'] et ['/api/groups'] (via queryClient.setQueryDefaults), les mutations invalidant déjà ces clés ; côté serveur, mettre en cache mémoire getSuppliers() (invalidé sur POST/PUT/DELETE) ou supprimer server/cache.ts s'il n'est pas utilisé. + +### API-03 + +**/api/groups renvoie à l'admin le logo base64 et la configuration SMTP de chaque magasin** — perf-api, sévérité moyenne, effort S + +- **Fichier :** `server/storage.ts:432` +- **Constat :** storage.ts:432-434 `return await db.select().from(groups).orderBy(groups.name);` (toutes colonnes) ; schema.ts:67 `logo: text("logo"), // Logo en data URI (data:image/png;base64,...)` + colonnes smtp*. routes.ts:979-981 renvoie ce résultat tel quel à l'admin ; Analytics.tsx:118-120 et Layout.tsx:47-50 n'utilisent que id/name/color. +- **Impact :** Pour l'admin, chaque chargement de l'application télécharge les logos de tous les magasins (souvent plusieurs dizaines de Ko chacun) ; forme de réponse différente entre admin et non-admin. +- **Recommandation :** Projeter les colonnes nécessaires à la liste (id, name, color) et exposer le détail (logo, SMTP) via GET /api/groups/:id uniquement sur la page Magasins. + +### CONT-05 + +**Le magasin du contact est écrasé silencieusement à l'enregistrement** — bug, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Contacts.tsx:169` +- **Constat :** Contacts.tsx:169 `const payload = { ...formData, groupId: effectiveGroupId ?? formData.groupId };`. Côté serveur routes.ts:1278-1281 le paramètre groupId est ignoré pour un non-admin, qui reçoit les contacts de tous ses magasins. Pour l'admin, le sélecteur « Magasin * » de la modale (l.461-481) n'est jamais pris en compte puisque le bouton « Nouveau contact » n'est visible que si effectiveGroupId est défini (l.220). +- **Impact :** Un manager multi-magasins voit un contact du magasin B en ayant sélectionné A ; s'il le modifie (même juste le téléphone), le contact est déplacé vers A. Pour l'admin, le choix « Magasin » de la modale est un faux choix. +- **Recommandation :** En édition, conserver `selectedContact.groupId` (ne pas l'écraser). Côté serveur, honorer `groupId` pour les non-admins s'il appartient à leurs magasins. Supprimer le sélecteur de magasin de la modale ou l'utiliser réellement (et autoriser la création en vue « Tous les magasins »). + +### CONT-06 + +**Second sélecteur de magasin propre à la page, en doublon du sélecteur global** — coherence-design, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Contacts.tsx:229` +- **Constat :** Contacts.tsx:40-41 `const [adminGroupId, setAdminGroupId] = useState(null); const effectiveGroupId = isAdmin ? adminGroupId : selectedStoreId;` et l.229-251 un bloc bleu « Magasin affiché (admin) ». Le sélecteur global de l'en-tête (Layout.tsx:131-155) est ignoré pour l'admin sur cette page. +- **Impact :** L'admin choisit « Magasin A » dans l'en-tête mais la page Contacts affiche « Tous les magasins » : deux sélecteurs contradictoires, source de confusion. +- **Recommandation :** Supprimer adminGroupId et utiliser `selectedStoreId` du StoreContext pour tous les rôles (null = tous les magasins pour l'admin). + +### CONT-07 + +**Aucun état de chargement ni d'erreur : « Aucun contact » s'affiche pendant le chargement** — ux-simplicite, sévérité moyenne, effort S + +- **Fichier :** `client/src/pages/Contacts.tsx:68` +- **Constat :** Contacts.tsx:68-84 `const { data: suppliers = [] } = useQuery(...)` / `const { data: contacts = [] } = useQuery(...)` sans lecture de isLoading/isError ; l.275-279 « Aucun fournisseur trouvé » et l.364-367 « Aucun contact » s'affichent tant que les données ne sont pas arrivées ou en cas d'erreur. +- **Impact :** L'utilisateur croit que l'annuaire est vide (ou que ses contacts ont disparu) pendant le chargement ou lors d'une panne réseau. +- **Recommandation :** Afficher des skeletons de cartes pendant isLoading (comme Suppliers.tsx:292-305) et un encart d'erreur avec bouton « Réessayer » sur isError. + +### CONT-08 + +**La liste complète des fournisseurs passe avant les contacts du magasin (mobile)** — ux-simplicite, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Contacts.tsx:254` +- **Constat :** Contacts.tsx:254 `grid grid-cols-1 lg:grid-cols-2` : sous 1024 px la colonne Fournisseurs (tous les fournisseurs de l'enseigne, l.281 `filteredSuppliers.map(...)`, sans pagination) est rendue entièrement avant « Autres contacts ». +- **Impact :** Sur téléphone/tablette il faut faire défiler des dizaines de cartes fournisseurs pour atteindre le contact du dépanneur du magasin. +- **Recommandation :** Remplacer les deux colonnes par des onglets « Fournisseurs » / « Contacts du magasin » (ou une seule recherche unifiée), afficher une liste compacte (nom + boutons Appeler / Email) et limiter l'affichage initial. + +### CONT-09 + +**Contacts, Fournisseurs et Statistiques sur mobile : sans MobileLayout et absents du menu mobile** — ux-simplicite, sévérité moyenne, effort S + +- **Fichier :** `client/src/components/RouterProduction.tsx:116` +- **Constat :** RouterProduction.tsx:116-128 `{/* Pages sans version mobile - utiliser version desktop pour l'instant */} ` (idem /suppliers, /analytics) rendus directement dans MobileApp, alors que les pages mobiles s'enveloppent dans MobileLayout (DashboardPage.tsx:136). MobileBottomNav.tsx:30-42 ne liste ni /contacts ni /analytics. +- **Impact :** La page la plus utile sur téléphone (liens tel: et mailto:) est inaccessible depuis le menu mobile ; si on y arrive par URL, il n'y a ni en-tête ni navigation pour revenir. +- **Recommandation :** Envelopper ces pages dans MobileLayout sur mobile et ajouter « Contacts » au menu « Plus » de MobileBottomNav. + +### MAIL-01 + +**Historique des relances : 500 lignes complètes chargées pour n'utiliser que la dernière par livraison** — perf-api, sévérité moyenne, effort M — vérification : partiellement confirmé + +- **Fichier :** `server/storage.ts:2974` +- **Constat :** storage.ts:2984-2990 `db.select().from(supplierMailLogs)...orderBy(desc(supplierMailLogs.createdAt)).limit(500)` (toutes colonnes : subject, errorMessage, messageId...) ; BLReconciliation.tsx:100-108 ne garde que `log.status === 'sent' && !map.has(log.deliveryId)`. Index existants : group_id et delivery_id seuls (migrations.ts:106-107). routes.ts:2448-2455 n'honore storeId que pour l'admin alors que le client l'envoie aussi pour le directeur (BLReconciliation.tsx:87). +- **Impact :** Payload inutilement gros à chaque ouverture du rapprochement ; au-delà de 500 envois, l'indicateur « déjà relancé » disparaît pour les livraisons plus anciennes (risque de relancer deux fois le même fournisseur). +- **Recommandation :** Endpoint dédié `SELECT DISTINCT ON (delivery_id) delivery_id, created_at, sent_by, sent_by_name, sent_to FROM supplier_mail_logs WHERE status = 'sent' AND group_id = ANY($1) ORDER BY delivery_id, created_at DESC` (champs renvoyés en camelCase comme aujourd'hui : deliveryId, createdAt, sentBy, sentByName), index (group_id, delivery_id, created_at DESC), et prise en compte de storeId pour le directeur s'il appartient à ses magasins. + +### SUPP-01 + +**Fonctionnalité dupliquée avec la colonne Fournisseurs de la page Contacts** — ux-simplicite, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Suppliers.tsx:369` +- **Constat :** Suppliers.tsx:405-422 affiche contact/téléphone/email et permet l'édition (modale l.494-640) ; Contacts.tsx:281-330 affiche les mêmes coordonnées et ouvre sa propre modale d'édition (l.566-613) via PUT /api/suppliers/:id. +- **Impact :** Deux écrans pour la même information, avec des droits différents : l'utilisateur ne sait pas où mettre à jour un numéro. +- **Recommandation :** Garder la fiche complète (options, mode de paiement, code ffnancy) dans Fournisseurs (admin) et faire de Contacts un annuaire en lecture + lien « Modifier la fiche » vers Fournisseurs, ou l'inverse ; une seule modale de coordonnées partagée. + +### SUPP-02 + +**Droits fournisseurs incohérents entre UI et serveur** — coherence-design, sévérité moyenne, effort M + +- **Fichier :** `server/routes.ts:1239` +- **Constat :** Suppliers.tsx:216/229/253/320 : création, modification, suppression réservées à `user?.role === 'admin'` ; Contacts.tsx:27 `CAN_EDIT_SUPPLIER = ["admin", "directeur"]` ; routes.ts:1201 POST autorise admin/manager/directeur, routes.ts:1242 PUT admin/manager/directeur, routes.ts:1259 DELETE admin/directeur. +- **Impact :** Un directeur peut modifier un fournisseur depuis Contacts mais pas depuis Fournisseurs ; un manager peut créer/modifier via l'API sans que l'UI le permette. Matrice de droits impossible à expliquer aux utilisateurs. +- **Recommandation :** Définir une seule règle (ex. hasPermission(role,'suppliers','edit') dans server/permissions) et l'utiliser côté serveur et client (hook usePermission). + +### SUPP-03 + +**Suppression impossible dès qu'il y a un historique, avec un message générique** — ux-simplicite, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Suppliers.tsx:262` +- **Constat :** Suppliers.tsx:262 `if (confirm(...)) deleteMutation.mutate(supplier.id)` puis l.189-193 toast « Impossible de supprimer le fournisseur ». init.sql:446-455 : `FOREIGN KEY ("supplier_id") REFERENCES "suppliers"("id") ON DELETE NO ACTION` sur deliveries et orders ; routes.ts:1265-1268 renvoie 500 « Failed to delete supplier ». +- **Impact :** L'admin confirme la suppression puis reçoit une erreur incompréhensible, alors que la carte affiche déjà le nombre de commandes (stats.orders). +- **Recommandation :** Désactiver le bouton (avec infobulle « X commandes liées ») quand stats.orders + stats.deliveries > 0, renvoyer côté serveur un 409 explicite sur violation FK (code 23503), et proposer un statut « Archivé/inactif ». + +### SUPP-04 + +**Erreurs de validation non expliquées à l'utilisateur** — ux-simplicite, sévérité moyenne, effort S — vérification : partiellement confirmé + +- **Fichier :** `client/src/pages/Suppliers.tsx:93` +- **Constat :** routes.ts:1223-1228 renvoie `{ message: "Validation failed", errors: error.errors }` en 400, mais Suppliers.tsx:93-97 affiche toujours « Impossible de créer le fournisseur » (idem modification l.145-149). +- **Impact :** L'utilisateur ne sait pas quel champ corriger. +- **Recommandation :** Extraire le message serveur de l'erreur (`apiRequest` lève `${status}: ${body}`) et l'afficher dans le toast (403 = droits insuffisants, 409/FK = fournisseur utilisé, etc.) ; la validation Zod côté client n'est pas prioritaire. + +### SUPP-05 + +**Grille de cartes lourde, recherche limitée au nom, ni tri ni filtres** — ux-simplicite, sévérité moyenne, effort M + +- **Fichier :** `client/src/pages/Suppliers.tsx:276` +- **Constat :** Suppliers.tsx:276-278 `suppliers.filter(supplier => supplier.name.toLowerCase().includes(searchTerm.toLowerCase()))` ; l.368-372 cartes `shadow-lg hover:shadow-xl ... p-6` en grille de 3 ; aucun filtre sur hasDlc / requiresControl / automaticReconciliation. +- **Impact :** Avec des dizaines de fournisseurs, long défilement et densité faible ; impossible de lister « les fournisseurs à contrôler » ou « avec DLC ». +- **Recommandation :** Passer en tableau compact (Nom, Contact, Téléphone, Email, Options en badges, Commandes) avec tri, recherche sur nom/contact/email/code, et filtres rapides par option. + +### ANA-16 + +**Calendriers en anglais, popover qui ne se ferme pas, pas de contrôle début ≤ fin** — lisibilite, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Analytics.tsx:221` +- **Constat :** Analytics.tsx:221-226 et 238-243 `` sans `locale={fr}` ni `weekStartsOn` (calendar.tsx ne définit aucune locale) ; Popover non contrôlé ; aucune validation from <= to. +- **Impact :** Mois et jours affichés en anglais, semaine commençant le dimanche ; l'utilisateur doit cliquer à côté pour fermer ; une période inversée renvoie des graphiques vides sans explication. +- **Recommandation :** Auto-implémenter uniquement `locale={fr}` sur les deux ; fermeture contrôlée du Popover, validation début ≤ fin et mode="range" à traiter avec la refonte des filtres. + +### ANA-19 + +**Bouton « Temps réel » confus et partiel** — ux-simplicite, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Analytics.tsx:71` +- **Constat :** Analytics.tsx:71 `refetchInterval: isRefreshing ? 30000 : false` uniquement sur summary ; l.164-172 le bouton passe en `animate-pulse` permanent et son libellé devient « Actualisé ». +- **Impact :** Les graphiques ne se mettent pas à jour en « temps réel » (seuls les KPI) ; le libellé suggère une action terminée ; animation clignotante fatigante ; relance aussi la requête coûteuse ANA-03. +- **Recommandation :** Remplacer par un bouton « Actualiser » ponctuel (refetch de toutes les requêtes) avec l'heure de dernière mise à jour ; supprimer le polling. + +### ANA-20 + +**fetch() manuels, logs de debug et variables mortes** — dette-code, sévérité basse, effort S — vérification : partiellement confirmé + +- **Fichier :** `client/src/pages/Analytics.tsx:58` +- **Constat :** Analytics.tsx:58-112 quatre queryFn avec `fetch(...)` dupliquant la queryFn par défaut et `console.log('📊 [ANALYTICS] Fetching summary...')`, `console.log('✅ [ANALYTICS] Summary data received:', data)` ; `console.error` (l.64, 83) conservés en prod (vite.config.ts ne retire que log/debug) ; `user` (l.20), `selectedStoreId` (l.21), `selectedStatus` (l.30) inutilisés. +- **Impact :** Code verbeux et difficile à maintenir ; la gestion 401 centralisée de queryClient.ts est contournée. +- **Recommandation :** Factoriser une seule queryFn `({ queryKey: [url, params] }) => fetch(`${url}?${params}`)` partagée par les 4 requêtes, supprimer les console.log, la variable `user` (et l'import useAuthUnified) ainsi que selectedStatus/setSelectedStatus (jamais modifiés) ; conserver selectedStoreId pour ANA-14. + +### ANA-21 + +**SQL brut concaténé et erreurs avalées dans timeseries / by-store** — dette-code, sévérité basse, effort M + +- **Fichier :** `server/storage.ts:3297` +- **Constat :** storage.ts:3297-3341 conditions construites en chaînes `orderConditions.push(`supplier_id IN (${supplierIds})`)` puis `db.execute(sql.raw(ordersSql))` ; storage.ts:3414-3439 idem avec `${sql.raw(orderDateFilter)}` ; storage.ts:3458-3461 `catch (error) { console.error(...); return []; }`. Un `supplierIds=abc` donne `IN (NaN)` -> erreur SQL 500. +- **Impact :** Fragile (aujourd'hui protégé seulement par Number()/toISOString), les erreurs de by-store sont masquées en « Aucune donnée magasin ». +- **Recommandation :** Réécrire avec les helpers Drizzle (gte, inArray, sql`...` paramétré) comme getAnalyticsBySupplier, valider les paramètres avec Zod dans la route, laisser remonter les erreurs. + +### ANA-22 + +**En-tête non responsive et rendu sans navigation sur mobile** — ux-simplicite, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Analytics.tsx:154` +- **Constat :** Analytics.tsx:154-155 `
` titre long + deux boutons sans flex-wrap ; la route mobile (RouterProduction.tsx:128) rend la page hors MobileLayout. +- **Impact :** Sur téléphone, titre et boutons se compressent/débordent ; aucune navigation pour revenir. +- **Recommandation :** `flex-col sm:flex-row gap-2`, supprimer le hack de marges négatives, envelopper dans MobileLayout sur mobile (cf. CONT-09). + +### ANA-23 + +**Taux de rapprochement calculé sur toutes les livraisons, y compris planifiées** — bug, sévérité basse, effort S + +- **Fichier :** `server/storage.ts:3260` +- **Constat :** storage.ts:3219 `reconciled: COUNT(CASE WHEN reconciled = true THEN 1 END)` et l.3260 `reconciliationRate: deliveryStats.count ? (reconciled / count) * 100 : 0` où count inclut les livraisons au statut 'planned' (pas encore livrables donc pas rapprochables). +- **Impact :** Le taux est mécaniquement sous-estimé dès qu'il y a des livraisons futures dans la période. +- **Recommandation :** Calculer sur les livraisons au statut 'delivered' (`COUNT(*) FILTER (WHERE status = 'delivered')`) et l'indiquer dans le libellé. + +### API-04 + +**Logs serveur verbeux à chaque création de fournisseur** — dette-code, sévérité basse, effort S + +- **Fichier :** `server/routes.ts:1167` +- **Constat :** routes.ts:1167-1213 une douzaine de `console.log` dont `console.log('📋 POST /api/suppliers - Request body:', JSON.stringify(redactBody(req.body), null, 2));` et `console.log('✅ Supplier data validation passed:', data);`. Le serveur est compilé par `tsc` (package.json:8) : rien n'est retiré en production. +- **Impact :** Pollution des logs de production, données métier écrites en clair. +- **Recommandation :** Supprimer ces logs ou les conditionner à `process.env.NODE_ENV !== 'production'`. + +### API-05 + +**Erreurs de validation renvoyées en 500 sur PUT fournisseur / contact ; filtre DLC en JS** — bug, sévérité basse, effort S + +- **Fichier :** `server/routes.ts:1250` +- **Constat :** routes.ts:1246 `insertSupplierSchema.partial().parse(req.body)` et l.1250-1253 `catch (error) { res.status(500).json({ message: "Failed to update supplier" }) }` ; idem routes.ts:1318-1324 pour les contacts. routes.ts:1151 `suppliers.filter(supplier => supplier.hasDlc === true)` après un SELECT complet. +- **Impact :** Une saisie invalide apparaît comme une panne serveur ; filtrage côté Node au lieu d'un WHERE. +- **Recommandation :** Auto-implémenter uniquement le `if (error.name === 'ZodError') return res.status(400).json({ message: 'Validation failed', errors: error.errors })` dans PUT /api/suppliers/:id et PUT /api/contacts/:id ; le paramètre dlcOnly est optionnel et non prioritaire. + +### CONT-10 + +**Requête contacts non conditionnée et queryFn redondante** — perf-client, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Contacts.tsx:76` +- **Constat :** Contacts.tsx:76-84 `useQuery({ queryKey: contactsQueryKey, queryFn: async () => { const url = effectiveGroupId ? ... ; return await apiRequest(url, "GET"); } })` : pas de `enabled`, alors que l.355-360 n'affiche rien si `!effectiveGroupId && !isAdmin`. La queryFn reconstruit l'URL que la queryFn par défaut gère déjà. +- **Impact :** Requête et transfert inutiles quand le résultat n'est pas affiché ; code dupliqué. +- **Recommandation :** Ajouter `enabled: isAdmin || !!effectiveGroupId` et laisser la queryFn par défaut (ou une queryFn unique construisant l'URL depuis la clé structurée de CONT-01). + +### CONT-11 + +**Champ « Notes » sur une seule ligne** — ux-simplicite, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Contacts.tsx:536` +- **Constat :** Contacts.tsx:535-542 `` alors que la colonne est un `text` (schema.ts:115). +- **Impact :** Saisie d'horaires, consignes ou remarques longues peu lisible ; le texte déborde. +- **Recommandation :** Utiliser le composant Textarea (rows=3). + +### MAIL-02 + +**Montant du BL au format anglo-saxon dans le mail envoyé au fournisseur** — lisibilite, sévérité basse, effort S + +- **Fichier :** `shared/supplierMail.ts:85` +- **Constat :** supplierMail.ts:84-86 `details.push(`Montant du BL : ${blAmount.toFixed(2)} €`);` -> « Montant du BL : 12345.50 € ». +- **Impact :** Mail professionnel en français avec un montant au mauvais format (point décimal, pas de séparateur de milliers). +- **Recommandation :** `new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(blAmount)` -> « 12 345,50 € ». + +### SALES-01 + +**Pages Analyse des ventes jamais branchées (code mort) et texte trompeur** — dette-code, sévérité basse, effort S + +- **Fichier :** `client/src/pages/SalesAnalysisConfig.tsx:75` +- **Constat :** `grep -rn "SalesAnalysis" client/src` : aucun import hors des deux fichiers ; RouterProduction.tsx ne déclare aucune route ; SalesAnalysisConfig.tsx:75 « Cette page sera accessible via le menu "Analyse Vente" » alors que Sidebar.tsx n'a pas cette entrée ; Utilities.tsx n'inclut pas le formulaire. +- **Impact :** L'admin ne peut ni configurer ni consulter l'outil d'analyse ; code à maintenir pour rien. +- **Recommandation :** Décision produit : soit ajouter la route /sales-analysis + entrée de menu « Analyse des ventes » et intégrer SalesAnalysisConfig dans Utilitaires, soit supprimer les deux fichiers et la colonne utilities.sales_analysis_url. + +### SALES-02 + +**Si branchée : 403 présenté comme « URL non configurée », iframe en h-screen, URL non validée** — ux-simplicite, sévérité basse, effort S + +- **Fichier :** `client/src/pages/SalesAnalysisPage.tsx:39` +- **Constat :** routes.ts:340-343 /api/utilities réservé admin/directeur ; SalesAnalysisPage.tsx:20-22 ne lit pas isError, l.39-60 affiche « URL non configurée » + bouton vers /utilities (admin seulement) ; l.64 `
` dans le Layout ; SalesAnalysisConfig.tsx:56-58 accepte n'importe quelle chaîne ; l.29-31 le champ n'est pas vidé si l'URL est supprimée. +- **Impact :** Un manager/employé recevrait un message faux et un bouton menant à une page interdite ; double barre de défilement. +- **Recommandation :** Exposer l'URL via un endpoint lisible par tous les rôles autorisés, gérer isError, hauteur `h-[calc(100vh-header)]`, valider l'URL (z.string().url()). + +### SUPP-06 + +**ID technique affiché, code ffnancy caché, libellés jargon** — lisibilite, sévérité basse, effort S — vérification : partiellement confirmé + +- **Fichier :** `client/src/pages/Suppliers.tsx:380` +- **Constat :** Suppliers.tsx:380 `

#{supplier.id}

` ; le champ codefou est saisi (l.543-549 « Code fournisseur API (liaison ffnancy) ») mais jamais affiché sur la carte ; les options n'ont aucune aide (« Fournisseur à contrôler », « Rapprochement automatique BL/Factures »). +- **Impact :** Information inutile pour l'utilisateur, information utile absente, sens des options obscur pour un non-technicien. +- **Recommandation :** Afficher « Code : FOU01 » à la place de #id ; renommer le champ en « Code fournisseur (catalogue articles ffnancy) » ou un libellé validé par le métier, et ajouter une phrase d'aide sous chaque option. + +### SUPP-07 + +**Une erreur réseau s'affiche comme « Aucun fournisseur – créez votre premier fournisseur »** — ux-simplicite, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Suppliers.tsx:344` +- **Constat :** Suppliers.tsx:55-57 `const { data: suppliers = [], isLoading } = useQuery(...)` (isError non lu) ; l.344-355 `filteredSuppliers.length === 0` -> « Commencez par créer votre premier fournisseur ». +- **Impact :** En cas de panne, l'admin peut croire que le référentiel a été effacé et recréer des fournisseurs. +- **Recommandation :** Gérer isError avec un encart d'erreur et un bouton « Réessayer » (refetch). + +### SUPP-08 + +**Recherche de stats en O(n×m) à chaque frappe, filtres non mémoïsés** — perf-client, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Suppliers.tsx:280` +- **Constat :** Suppliers.tsx:280-283 `supplierStats.find(s => s.id === supplierId)` appelé pour chaque carte (l.370) ; l.276-278 filtre recalculé et `searchTerm.toLowerCase()` réévalué pour chaque fournisseur à chaque rendu. +- **Impact :** Re-rendus plus lents à chaque caractère tapé quand la liste grossit. +- **Recommandation :** `const statsById = useMemo(() => new Map(supplierStats.map(s => [s.id, s])), [supplierStats])` et `useMemo` pour filteredSuppliers avec le terme normalisé une fois. + +### SUPP-09 + +**Double invalidation après modification d'un fournisseur** — perf-api, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Suppliers.tsx:156` +- **Constat :** Suppliers.tsx:156 `queryClient.invalidateQueries({ queryKey: ['/api/suppliers'] });` dans onSuccess puis l.161-163 à nouveau dans onSettled. +- **Impact :** Deux refetchs successifs de /api/suppliers (le premier annulé, souvent déjà parti au serveur) après chaque modification. +- **Recommandation :** Ne garder l'invalidation que dans onSettled. + +### SUPP-10 + +**Logs de debug et branche 401 morte dans les mutations** — dette-code, sévérité basse, effort S — vérification : partiellement confirmé + +- **Fichier :** `client/src/pages/Suppliers.tsx:67` +- **Constat :** Suppliers.tsx:67-69, 103-106, 110, 122, 129 `console.log('🚚 Frontend: Creating supplier...')` ; l.82-91 `if (isUnauthorizedError(error)) { ... window.location.href = "/api/login"; }` alors que authUtils.ts:2 teste `/^401: .*Unauthorized/` et que requireAuth renvoie « Authentification requise » (localAuth.ts:252) : la branche ne s'exécute jamais, et GET /api/login n'existe pas (seul POST). +- **Impact :** Bruit dans le code ; si la branche s'exécutait, l'utilisateur atterrirait sur « Cannot GET /api/login ». +- **Recommandation :** Dans Suppliers.tsx uniquement : supprimer les console.log et les trois blocs `if (isUnauthorizedError(error)) {...}` (comportement inchangé, la redirection 401 est déjà assurée par queryClient pour les requêtes). La correction globale de isUnauthorizedError (regex /^401:/) et la redirection vers /auth relèvent d'un chantier séparé. + +### SUPP-11 + +**Compteurs Commandes/Livraisons ambigus et indépendants du magasin sélectionné** — coherence-design, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Suppliers.tsx:61` +- **Constat :** Suppliers.tsx:61-63 `useQuery({ queryKey: ['/api/stats/by-supplier'] })` sans storeId ; routes.ts:1511 `resolveStatsGroupIds(user, req.query.storeId)` -> tous les magasins ; l.465-474 libellés « Commandes » / « Livraisons » sans période. +- **Impact :** L'admin qui a sélectionné un magasin voit des totaux enseigne « depuis toujours » sans le savoir. +- **Recommandation :** Passer `storeId=selectedStoreId` (clé ['/api/stats/by-supplier', selectedStoreId]) et préciser le libellé (« Commandes (total) » ou « 12 derniers mois »). + +### SUPP-12 + +**Boutons icône sans libellé ni infobulle, petits textes à faible contraste** — accessibilite, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Suppliers.tsx:385` +- **Constat :** Suppliers.tsx:385-399 `` (idem corbeille) ; Contacts.tsx:302-304 et 403-413 identiques. Textes `text-xs text-gray-400` (Suppliers.tsx:543, Contacts.tsx:327) et `text-xs text-blue-500` (Contacts.tsx:397) sous le ratio 4,5:1. +- **Impact :** Lecteurs d'écran annoncent « bouton » sans action ; utilisateurs peu à l'aise ne savent pas ce que fait l'icône ; textes difficiles à lire. +- **Recommandation :** Ajouter `aria-label` et `title` (« Modifier », « Supprimer ») ; passer les textes secondaires en text-gray-500/600 et text-blue-700. + +### SUPP-13 + +**Casts `as any` inutiles sur email/codefou** — dette-code, sévérité basse, effort S + +- **Fichier :** `client/src/pages/Suppliers.tsx:242` +- **Constat :** Suppliers.tsx:242-243 `codefou: (supplier as any).codefou`, `email: (supplier as any).email`, l.417-420 ; Contacts.tsx:188, 317-323, 390-391 ; alors que shared/schema.ts:95-97 déclare `codefou`, `email` et contacts.company (l.111). +- **Impact :** Perte du typage : une faute de frappe ou un renommage de colonne ne sera pas détecté à la compilation. +- **Recommandation :** Supprimer les `as any` (le type Supplier/Contact inféré contient déjà ces champs) et typer les mutationFn (InsertSupplier, InsertContact). diff --git a/docs/audit-webui/12-admin-utilities-auth.md b/docs/audit-webui/12-admin-utilities-auth.md new file mode 100644 index 0000000..3b0d7ed --- /dev/null +++ b/docs/audit-webui/12-admin-utilities-auth.md @@ -0,0 +1,666 @@ +# Administration, Utilitaires & Connexion + +_48 constats vérifiés — 4 haute, 25 moyenne, 19 basse._ + +## Pages analysées + +### `/utilities (+ routes de compatibilité /backup, /nocodb-config, /database-debug, /weather-settings ; aussi exposée sur mobile)` + +**Rôle :** Conteneur d'administration (admin uniquement) qui regroupe en 6 onglets : sauvegardes, configuration NocoDB, debug base de données, météo, webhook BAP, exécution SQL. + +**Tâches principales de l'utilisateur :** +- Choisir un outil d'administration via les onglets +- Accéder directement à un onglet via une ancienne URL (/backup, /nocodb-config...) + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/user | au montage (useAuthUnified : état local par instance, fetch cache:'no-cache') | server/localAuth.ts:204 → storage.getUserWithGroups (server/storage.ts:371) ; + passport.deserializeUser localAuth.ts:141 → getUserWithGroups | Refait aussi par RouterProduction, Layout, Sidebar (useAuthSimple) et chaque onglet monté : ≥5 appels par affichage, 4 requêtes SQL chacun. Tant qu'il n'a pas répondu, user=null → carte « Accès refusé » affichée. | + +**Lisibilité / simplicité :** Six onglets plats (grid-cols-6 fixe) mélangent outils quotidiens (sauvegardes, météo) et outils dangereux pour développeur (SQL brut, scan de schéma), avec des libellés jargon (NocoDB, BAP, Debug). Inutilisable sur téléphone (6 colonnes forcées). Double en-tête de page (Utilitaires + titre de chaque onglet), largeurs/titres/états de chargement différents d'un onglet à l'autre, onglet actif non reflété dans l'URL. Flash « Accès refusé » à chaque ouverture à cause du hook d'auth non partagé. Tous les onglets sont importés statiquement dans le bundle principal téléchargé par tous les employés. + +### `/utilities › onglet Sauvegardes` + +**Rôle :** Voir les sauvegardes de la base, en créer une manuellement, les télécharger ou les supprimer, activer/désactiver la sauvegarde automatique nocturne. + +**Tâches principales de l'utilisateur :** +- Vérifier que la dernière sauvegarde est récente et réussie +- Lancer une sauvegarde manuelle +- Télécharger une sauvegarde +- Supprimer une sauvegarde +- Activer/désactiver la sauvegarde automatique + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/backups | au montage (enabled après le /api/user propre au composant) + polling toutes les 30 s | server/routes.ts:5173 → backupService.getBackupList (server/backupService.ts:159, ORDER BY created_at DESC LIMIT 10) | Requête légère mais précédée de getUserWithGroups (2 requêtes) en plus de la désérialisation ; polling inconditionnel. | +| GET /api/utilities | au montage | server/routes.ts:333 → storage.getUser + storage.getUtilities (server/storage.ts:3015) | Lu pour l'interrupteur et la carte « Statut Système ». | +| POST /api/utilities | clic sur l'interrupteur sauvegarde automatique | server/routes.ts:354 → getUtilities puis updateUtilities/createUtilities (server/storage.ts:3020-3031) | La réponse (config à jour) n'est pas réutilisée : invalidation + refetch ; toast calculé sur l'ancienne valeur. | +| POST /api/backups | clic « Sauvegarde Manuelle » | server/routes.ts:5188 → backupService.createBackup (server/backupService.ts:98) | Requête HTTP ouverte pendant tout le pg_dump ; lecture synchrone du dump complet (readFileSync) pour compter les tables ; un échec laisse le statut « creating ». | +| GET /api/backups/:filename/download | clic « Télécharger » (lien
créé dynamiquement) | server/routes.ts:5203 → backupService.downloadBackup (server/backupService.ts:202) | Nom de fichier non validé (path.join avec le paramètre). | +| DELETE /api/backups/:filename | clic sur l'icône corbeille (sans confirmation) | server/routes.ts:5225 → backupService.deleteBackup (server/backupService.ts:173) | Suppression physique immédiate du fichier + enregistrement. | +| GET /api/user | au montage (useAuthUnified) | server/localAuth.ts:204 | Bloque les deux requêtes ci-dessus (enabled: canManageBackups) → cascade. | + +**Lisibilité / simplicité :** Information répétée trois fois (sous-titre, carte interrupteur, carte « Statut Système »), nom de fichier technique en titre de chaque ligne, unités anglaises (Bytes), description serveur franglaise (« Manuel backup du… »). Suppression en un clic sans confirmation via un bouton icône sans libellé. Ligne de liste non responsive (pas de flex-wrap ni troncature) coupée par l'overflow-x-hidden du Layout. Bouton principal orange alors que les autres onglets utilisent le bleu. Sauvegardes ratées affichées « En cours » à vie et comptées comme « Dernière sauvegarde ». Pas de restauration proposée (à décider côté produit). + +### `/utilities › onglet Configuration NocoDB` + +**Rôle :** Gérer les connexions à la base NocoDB externe utilisée pour vérifier les factures lors du rapprochement BL/factures. + +**Tâches principales de l'utilisateur :** +- Créer une configuration NocoDB +- Modifier une configuration (URL, jeton, projet, actif) +- Supprimer une configuration +- Afficher le jeton API + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/nocodb-config | au montage (enabled: user?.role === 'admin', donc après le /api/user propre au composant) | server/routes.ts:5090 → storage.getUserWithGroups + storage.getNocodbConfigs (server/storage.ts:1655) | Renvoie le jeton API déchiffré au navigateur. Même queryKey réutilisée par Groups.tsx:85. | +| POST /api/nocodb-config | soumission de la modale de création | server/routes.ts:5105 → storage.createNocodbConfig (server/storage.ts:1682) | Aucune contrainte « une seule config active ». | +| PUT /api/nocodb-config/:id | soumission de la modale d'édition | server/routes.ts:5124 → storage.updateNocodbConfig (server/storage.ts:1688) | Le jeton en clair est renvoyé dans le formulaire puis ré-envoyé. | +| DELETE /api/nocodb-config/:id | confirmation dans la modale de suppression | server/routes.ts:5141 → storage.deleteNocodbConfig (server/storage.ts:1701) | Pas de vérification des magasins (groups.nocodb_config_id) qui l'utilisent. | +| GET /api/user | au montage (useAuthUnified) | server/localAuth.ts:204 | Flash « Accès restreint » tant qu'il n'a pas répondu. | + +**Lisibilité / simplicité :** Libellés anglais/jargon (« Personal API Token », placeholders xc-token / your-nocodb-instance), pas d'aide ni de bouton « Tester la connexion » (présent sur Météo et BAP). Jeton visible en clair avec un bouton œil. Plusieurs configurations peuvent être « Actif » sans indiquer laquelle sert réellement. Boutons Modifier/Supprimer icône seule, sans libellé ni style destructif ; suppression sans avertissement sur les magasins liés. Spinner géant (128 px) au chargement, h1 text-3xl dans un conteneur différent des autres onglets. Formulaire de création non réinitialisé, formulaires création/édition dupliqués, console.log de debug à chaque rendu. + +### `/utilities › onglet Debug Base de Données` + +**Rôle :** Outil développeur : scanner le schéma de la base de production, l'écrire dans les logs serveur et télécharger un rapport texte. + +**Tâches principales de l'utilisateur :** +- Lancer le scan du schéma +- Télécharger le rapport + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/debug/log-schema | clic « Démarrer le Scan du Schéma » (fetch manuel) | server/routes.ts:4776 → pool.query en boucle (colonnes + COUNT(*) par table, l.4813-4846) | N+1 séquentiel, COUNT(*) en scan complet, centaines de lignes console ; refuse de fonctionner hors production. | +| GET /api/debug/download-schema | clic « Télécharger » (window.open dans un nouvel onglet) | server/routes.ts:4904 → même balayage complet (l.4971, 4984) | Refait intégralement le travail du scan précédent. | +| GET /api/user | au montage (useAuthUnified) | server/localAuth.ts:204 | Flash « Accès refusé » tant qu'il n'a pas répondu. | + +**Lisibilité / simplicité :** Page entièrement orientée développeur (« logs de votre application Node.js », « Relations FK », marqueur de log à chercher), émojis dans les textes, fonctionne uniquement en production, parcours en deux temps (scan puis téléchargement qui relance le scan). N'a pas sa place au même niveau que les Sauvegardes pour un admin non technicien. + +### `/utilities › onglet Météo` + +**Rôle :** Configurer la clé API Visual Crossing et la ville du widget météo affiché dans l'en-tête de toutes les pages. + +**Tâches principales de l'utilisateur :** +- Saisir la clé API et la ville +- Détecter la ville par géolocalisation +- Tester la connexion +- Activer/désactiver le widget + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/weather/settings | au montage | server/routes.ts:5596 → storage.getWeatherSettings (server/storage.ts:2891) | Renvoie la clé API en clair (mise en defaultValue d'un champ password). | +| PUT /api/weather/settings/:id (POST si aucun réglage) | soumission du formulaire (fetch manuel) | server/routes.ts:5630 / 5611 → storage.updateWeatherSettings (server/storage.ts:2901) / createWeatherSettings (2896) | Ne vide pas le cache météo même si la ville change ; le client n'invalide pas /api/weather/current. | +| POST /api/weather/test-connection | clic « Tester la connexion » (fetch manuel) | server/routes.ts:5650 → weatherService.testApiConnection (server/weatherService.ts:220) | Messages serveur en français ; erreur client en anglais « Test connection failed ». | +| POST /api/weather/geolocation | clic sur le bouton localisation | server/routes.ts:6094 → weatherService.getCityFromCoordinates + storage.updateWeatherSettings + storage.clearWeatherCache | Aucun contrôle de rôle admin ; le client met à jour l'input par manipulation DOM. | +| GET /api/weather/current | WeatherWidget du Layout : montage + toutes les 30 min (non rafraîchi par cette page) | server/routes.ts:5671 → getWeatherSettings + getWeatherData×2 séquentiels (server/storage.ts:2910) + getNearestWeatherData (2930) + API externe si absent | Cache BD indexé par date seulement (pas par ville) ; pas de cache mémoire ; console.log à chaque appel. | + +**Lisibilité / simplicité :** Formulaire simple et plutôt clair, mais : après changement manuel de ville le widget continue d'afficher l'ancienne ville jusqu'au lendemain (cache non vidé), messages d'erreur en anglais, champs non contrôlés manipulés via document.getElementById / querySelector('form'), largeur max-w-2xl incohérente avec les autres onglets, texte « À propos » inexact (le widget est à gauche, le sélecteur de magasin à droite). + +### `/utilities › onglet Configuration BAP` + +**Rôle :** Configurer l'URL du webhook n8n qui reçoit les PDF « BAP » envoyés depuis le bouton BAP de la barre latérale. + +**Tâches principales de l'utilisateur :** +- Saisir/modifier l'URL du webhook +- Tester le webhook +- Activer/désactiver l'envoi +- Sauvegarder + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/webhook-bap-config | au montage | server/routes.ts:186 → storage.getUser + storage.getWebhookBapConfig (server/storage.ts:2992) | Si la table manque, renvoie une config factice avec l'URL de production codée en dur. | +| POST /api/webhook-bap-config | clic « Sauvegarder » | server/routes.ts:222 → getWebhookBapConfig puis updateWebhookBapConfig/createWebhookBapConfig (server/storage.ts:2997-3012) | Erreur de validation Zod renvoyée en 500. | +| POST /api/webhook-bap-config/test | clic « Tester le Webhook » (fetch manuel) | server/routes.ts:264 → fetch POST vers l'URL saisie (timeout 10 s) | Teste l'URL saisie, pas celle enregistrée. | + +**Lisibilité / simplicité :** Jargon (webhook, n8n, BAP non expliqué) et prénoms codés en dur (« Laurie et Jeremy ») ; interrupteur « Configuration active » sans explication de l'effet. Bouton Sauvegarder isolé en bas après la carte de test, statut « Connexion réussie » qui reste affiché après modification de l'URL, pas d'indication de modifications non enregistrées. Bouton copier icône seule. Pas de titre de section cohérent avec les autres onglets. + +### `/utilities › onglet Exécution SQL` + +**Rôle :** Exécuter du SQL brut sur la base de production (collage ou fichier .sql). + +**Tâches principales de l'utilisateur :** +- Coller ou charger un script SQL +- Exécuter +- Lire les résultats/logs + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| POST /api/admin/execute-sql | clic « Exécuter SQL » (fetch manuel dans useMutation) | server/routes.ts:6151 → storage.getUser + db.execute(sql.raw(sqlQuery)) (l.6178) | Aucune transaction ni mode lecture seule ; renvoie toutes les lignes (results: result.rows) + un échantillon dupliqué dans logs. | + +**Lisibilité / simplicité :** Outil extrêmement dangereux exposé à un clic, sans confirmation ni sauvegarde préalable. Résultats non bornés rendus comme un bloc JSON géant sans retour à la ligne (whitespace non préservé) dans une zone sans hauteur max. Bouton obsolète « SQL Webhook BAP » qui écrase l'éditeur et contient une URL de production. Message « Début de l'exécution » effacé aussitôt. + +### `/auth (et toute URL quand l'utilisateur n'est pas connecté)` + +**Rôle :** Écran de connexion (identifiant + mot de passe) avec visuel de présentation sur grand écran. + +**Tâches principales de l'utilisateur :** +- Se connecter +- Voir les identifiants par défaut lors de la première installation + +**Appels API :** + +| Endpoint | Déclencheur | Handler serveur | Notes | +|---|---|---|---| +| GET /api/user | au montage, deux fois (RouterProduction puis AuthPage, chacun via useAuthUnified) | server/localAuth.ts:204 → storage.getUserWithGroups (server/storage.ts:371) | Renvoie 401 pour un visiteur ; provoque deux spinners plein écran successifs. | +| GET /api/default-credentials-check | au montage | server/localAuth.ts:234 → storage.getUserByUsername('admin') (server/storage.ts:351) | Endpoint public ; carte admin/admin affichée par défaut avant la réponse. | +| POST /api/login | soumission du formulaire (fetch manuel en « production », useMutation sinon) | server/localAuth.ts:153 → LocalStrategy (getUserByUsername + scrypt) puis await backupService.checkAndPerformDailyBackup (server/backupService.ts:240) | Peut attendre un pg_dump complet avant de répondre ; puis côté client attente artificielle de 500 ms + rechargement complet. | + +**Lisibilité / simplicité :** Formulaire simple et lisible, mais : deux spinners avant l'affichage, chargement initial lourd (~1,7 Mo JS non compressé + script tiers replit.com bloquant, lang=en déclenchant la traduction Chrome, zoom mobile désactivé), champs sans autoComplete/autoCapitalize (majuscule automatique sur mobile → échec de connexion, comparaison sensible à la casse), identifiants admin/admin affichés par défaut, messages d'erreur trompeurs en cas de blocage (rate-limit) ou de CSRF, anglicisme « across tous vos magasins », 500 ms d'attente + rechargement complet après connexion, sessions en mémoire perdues à chaque redémarrage. + +### `(aucune — page non routée) Landing` + +**Rôle :** Ancienne page d'accueil marketing (héritage Replit Auth), jamais importée. + +**Tâches principales de l'utilisateur :** +- Aucune (code mort) + +**Lisibilité / simplicité :** Code mort : non importée par le routeur, ses boutons redirigent vers GET /api/login qui n'existe pas (seul POST existe). Branding « La Foir'Fouille » codé en dur, classes Tailwind personnalisées. À supprimer. + +### `* (404, desktop dans Layout et mobile dans MobileApp)` + +**Rôle :** Page affichée pour une URL inconnue quand l'utilisateur est connecté. + +**Tâches principales de l'utilisateur :** +- Comprendre que la page n'existe pas et revenir à l'application + +**Lisibilité / simplicité :** Message de développeur en anglais (« 404 Page Not Found », « Did you forget to add the page to the router? »), aucun bouton de retour, min-h-screen à l'intérieur du Layout (double hauteur d'écran). + +## Constats + +| ID | Sév. | Catégorie | Effort | Titre | Fichier | +|---|---|---|---|---|---| +| [ADMIN-01](#admin-01) | haute | bug | M | Hook d'auth non partagé : flash « Accès refusé » et cascade de /api/user à chaque onglet | `client/src/hooks/useAuthUnified.ts:23` | +| [AUTH-01](#auth-01) | haute | perf-bundle | S | Script tiers replit.com bloquant en production, lang="en" et zoom désactivé | `client/index.html:19` | +| [BACKUP-01](#backup-01) | haute | ux-simplicite | S | Suppression d'une sauvegarde en un clic, sans confirmation | `client/src/pages/BackupManager.tsx:142` | +| [SQL-01](#sql-01) | haute | ux-simplicite | M | Exécution de SQL brut en production sans confirmation ni filet de sécurité | `client/src/pages/SQLExecutor.tsx:97` | +| [ADMIN-02](#admin-02) | moyenne | perf-api | S | /api/user exécute 4 requêtes SQL alors que req.user est déjà chargé | `server/localAuth.ts:210` | +| [AUTH-02](#auth-02) | moyenne | perf-serveur | S | Fichiers statiques servis sans compression ni cache long | `server/index.production.ts:143` | +| [AUTH-03](#auth-03) | moyenne | perf-client | M | Double vérification d'auth, double spinner, puis 500 ms d'attente et rechargement complet | `client/src/pages/AuthPage.tsx:69` | +| [AUTH-04](#auth-04) | moyenne | bug | S | Identifiants admin/admin affichés par défaut avant (et sans) vérification | `client/src/pages/AuthPage.tsx:25` | +| [AUTH-05](#auth-05) | moyenne | accessibilite | S | Champs de connexion sans autoComplete/autoCapitalize (échecs sur mobile) | `client/src/pages/AuthPage.tsx:224` | +| [AUTH-06](#auth-06) | moyenne | bug | S | Sessions stockées en mémoire en production (fichier d'auth PostgreSQL jamais utilisé) | `server/routes.ts:119` | +| [AUTH-07](#auth-07) | moyenne | bug | S | CSRF activé en production (npm start) mais jamais envoyé par le client | `server/index.ts:40` | +| [BACKUP-02](#backup-02) | moyenne | bug | S | Une sauvegarde ratée reste « En cours » indéfiniment | `server/backupService.ts:153` | +| [BACKUP-03](#backup-03) | moyenne | perf-serveur | S | Le dump complet est relu en mémoire de façon synchrone pour compter les tables | `server/backupService.ts:134` | +| [BACKUP-04](#backup-04) | moyenne | perf-api | S | La connexion attend la vérification (voire l'exécution) de la sauvegarde quotidienne | `server/localAuth.ts:165` | +| [BACKUP-06](#backup-06) | moyenne | lisibilite | S | Informations répétées trois fois et libellés techniques | `client/src/pages/BackupManager.tsx:205` | +| [BACKUP-07](#backup-07) | moyenne | accessibilite | S | Ligne de sauvegarde non responsive et bouton supprimer sans nom accessible | `client/src/pages/BackupManager.tsx:366` | +| [BACKUP-09](#backup-09) | moyenne | bug | S | Téléchargement de sauvegarde vulnérable au path traversal | `server/backupService.ts:203` | +| [BAP-01](#bap-01) | moyenne | lisibilite | S | Jargon technique et prénoms codés en dur | `client/src/pages/WebhookBAPConfig.tsx:166` | +| [DEBUG-01](#debug-01) | moyenne | perf-serveur | S | Scan de schéma en N+1 avec COUNT(*) complet, refait pour le téléchargement | `server/routes.ts:4831` | +| [DEBUG-02](#debug-02) | moyenne | ux-simplicite | S | Outil développeur incompréhensible pour un admin non technicien | `client/src/pages/DatabaseDebug.tsx:73` | +| [ERR-01](#err-01) | moyenne | lisibilite | M | Messages d'erreur bruts (code HTTP + JSON, en anglais) affichés dans les toasts | `client/src/lib/queryClient.ts:6` | +| [NF-01](#nf-01) | moyenne | lisibilite | S | Page 404 en anglais avec message de développeur et sans retour | `client/src/pages/not-found.tsx:11` | +| [NOCO-01](#noco-01) | moyenne | bug | S | Jeton API NocoDB déchiffré envoyé au navigateur et affiché en clair | `server/storage.ts:1657` | +| [NOCO-03](#noco-03) | moyenne | ux-simplicite | M | Plusieurs configurations « Actif » possibles, une seule utilisée au hasard | `server/storage.ts:1665` | +| [SQL-02](#sql-02) | moyenne | perf-api | S | Résultats SQL non bornés, dupliqués et rendus en un bloc illisible | `server/routes.ts:6197` | +| [UTIL-01](#util-01) | moyenne | ux-simplicite | M | Six onglets plats mêlant outils quotidiens et outils dangereux, inutilisables sur mobile | `client/src/pages/Utilities.tsx:81` | +| [UTIL-03](#util-03) | moyenne | perf-bundle | M | Pages admin importées statiquement dans le bundle principal de tous les utilisateurs | `client/src/components/RouterProduction.tsx:17` | +| [WEATHER-01](#weather-01) | moyenne | bug | S | Changer la ville ne met pas à jour le widget météo (cache non vidé, requête non invalidée) | `server/routes.ts:5639` | +| [WEATHER-02](#weather-02) | moyenne | bug | S | Géolocalisation météo modifiable par n'importe quel utilisateur connecté | `server/routes.ts:6096` | +| [ADMIN-03](#admin-03) | basse | perf-serveur | S | Chaque handler admin relit l'utilisateur en BD pour vérifier le rôle | `server/routes.ts:5175` | +| [AUTH-08](#auth-08) | basse | bug | S | Limite de 5 connexions/15 min par IP et message d'erreur trompeur | `client/src/pages/AuthPage.tsx:76` | +| [AUTH-09](#auth-09) | basse | dette-code | S | Deux implémentations de connexion, hook appelé hors rendu, double soumission | `client/src/pages/AuthPage.tsx:171` | +| [BACKUP-05](#backup-05) | basse | perf-client | S | Polling 30 s inconditionnel de la liste des sauvegardes | `client/src/pages/BackupManager.tsx:61` | +| [BACKUP-08](#backup-08) | basse | bug | S | Toast de l'interrupteur calculé sur l'ancienne valeur et bascule différée | `client/src/pages/BackupManager.tsx:78` | +| [BACKUP-10](#backup-10) | basse | coherence-design | S | Double en-tête de page et bouton principal orange | `client/src/pages/BackupManager.tsx:196` | +| [BAP-02](#bap-02) | basse | ux-simplicite | S | Bouton Sauvegarder isolé et statut de test périmé | `client/src/pages/WebhookBAPConfig.tsx:251` | +| [BAP-03](#bap-03) | basse | bug | S | Validation renvoyée en 500 et configuration factice avec URL de production | `server/routes.ts:236` | +| [LAND-01](#land-01) | basse | dette-code | S | Page Landing morte qui pointe vers une route inexistante | `client/src/pages/Landing.tsx:7` | +| [NOCO-02](#noco-02) | basse | dette-code | S | Log de debug à chaque rendu et code défensif opaque | `client/src/pages/NocoDBConfig.tsx:66` | +| [NOCO-04](#noco-04) | basse | accessibilite | S | Boutons Modifier/Supprimer icône seule et suppression sans avertissement d'impact | `client/src/pages/NocoDBConfig.tsx:300` | +| [NOCO-05](#noco-05) | basse | bug | S | Formulaire de création non réinitialisé après succès | `client/src/pages/NocoDBConfig.tsx:84` | +| [NOCO-06](#noco-06) | basse | dette-code | S | Formulaires de création et d'édition dupliqués | `client/src/pages/NocoDBConfig.tsx:371` | +| [NOCO-07](#noco-07) | basse | lisibilite | M | Libellés anglais/jargon et aucun test de connexion | `client/src/pages/NocoDBConfig.tsx:406` | +| [SQL-03](#sql-03) | basse | dette-code | S | Bouton « SQL Webhook BAP » obsolète, état dupliqué et log de démarrage effacé | `client/src/pages/SQLExecutor.tsx:100` | +| [UTIL-02](#util-02) | basse | ux-simplicite | S | Onglet actif non reflété dans l'URL | `client/src/pages/Utilities.tsx:28` | +| [UTIL-04](#util-04) | basse | coherence-design | M | Conteneurs, titres et états de chargement différents dans chaque onglet | `client/src/pages/NocoDBConfig.tsx:231` | +| [WEATHER-03](#weather-03) | basse | dette-code | S | Formulaire non contrôlé manipulé via le DOM, fetch manuels et erreurs en anglais | `client/src/pages/WeatherSettings.tsx:318` | +| [WEATHER-04](#weather-04) | basse | perf-serveur | S | /api/weather/current recalculé à chaque appel sans cache mémoire | `server/routes.ts:5683` | + +### ADMIN-01 + +**Hook d'auth non partagé : flash « Accès refusé » et cascade de /api/user à chaque onglet** — bug, sévérité haute, effort M + +- **Fichier :** `client/src/hooks/useAuthUnified.ts:23` +- **Constat :** useAuthUnified.ts:23-24 `const [productionUser, setProductionUser] = useState(null); const [productionLoading, setProductionLoading] = useState(true);` + useEffect l.148-167 `fetch('/api/user', { credentials: 'include', cache: 'no-cache' ...})` : état local par instance. Utilities.tsx:26 `const { user } = useAuthUnified();` puis l.45 `if (!canManageBackups && !canManageNocoDB && ...) return (... Accès refusé ...)` sans tenir compte d'isLoading ; même schéma BackupManager.tsx:48/54/177, NocoDBConfig.tsx:45/211 (« Accès restreint »), DatabaseDebug.tsx:8/41. Les requêtes attendent ce second appel : BackupManager.tsx:60 `enabled: canManageBackups`, NocoDBConfig.tsx:58 `enabled: user?.role === 'admin'`. +- **Impact :** À l'ouverture de /utilities, l'admin voit une carte « Accès refusé » (Utilities), puis une seconde (onglet), puis un spinner : 3 allers-retours séquentiels (/api/user → /api/user → /api/backups) avant la liste. /api/user est appelé ≥5 fois par affichage (RouterProduction, Layout, Sidebar via useAuthSimple, Utilities, onglet) et de nouveau à chaque changement d'onglet. +- **Recommandation :** Source unique : `useQuery({ queryKey: ['/api/user'], queryFn: getQueryFn({ on401: 'returnNull' }) sans redirection, staleTime: 5*60_000, retry: false })`. Attention : le queryFn par défaut (queryClient.ts:56-61) fait `window.location.href = '/auth'` sur un 401, ce qui changerait le comportement de RouterProduction (il affiche aujourd'hui AuthPage directement sur '/'). Il faut donc un queryFn dédié qui renvoie null sans rediriger. Garder la même forme de retour { user, isLoading, isAuthenticated, refreshAuth, forceAuthRefresh } pour les ~15 appelants, et faire de useAuthSimple un alias. Dans Utilities, afficher un skeleton tant que isLoading. Les contrôles de rôle dans les onglets peuvent ensuite être retirés (Utilities est le seul point d'entrée de ces pages). + +### AUTH-01 + +**Script tiers replit.com bloquant en production, lang="en" et zoom désactivé** — perf-bundle, sévérité haute, effort S — vérification : partiellement confirmé + +- **Fichier :** `client/index.html:19` +- **Constat :** client/index.html:18-19 `` (classique, sans async/defer) présent aussi dans dist/public/index.html ; l.2 `` ; l.5 `content="width=device-width, initial-scale=1.0, maximum-scale=1"`. +- **Impact :** Le module de l'application (différé) ne s'exécute qu'après le téléchargement de ce script tiers : si replit.com est lent ou filtré par le réseau du magasin, l'écran reste blanc jusqu'au timeout. Chrome propose de « traduire depuis l'anglais » une interface française ; zoom bloqué sur mobile (malvoyants). +- **Recommandation :** Partie mécanique : supprimer la balise replit (l.18-19) et passer `lang="fr"`. Le retrait de `maximum-scale=1` est souhaitable pour l'accessibilité, mais il faut d'abord vérifier que les champs natifs ont une police d'au moins 16px sur mobile. À traiter séparément. + +### BACKUP-01 + +**Suppression d'une sauvegarde en un clic, sans confirmation** — ux-simplicite, sévérité haute, effort S + +- **Fichier :** `client/src/pages/BackupManager.tsx:142` +- **Constat :** l.142-145 `const handleDelete = (filename: string) => { setDeletingBackup(filename); deleteBackupMutation.mutate(filename); };` appelé directement par l.388-391 `