# SAV _38 constats vérifiés — 8 haute, 17 moyenne, 13 basse._ ## Pages analysées ### `/sav (desktop)` **Rôle :** Suivre les retours SAV (produits défectueux, pièces manquantes...) d'un magasin : créer un ticket, suivre son statut et sa priorité, échanger des commentaires, supprimer un ticket. **Tâches principales de l'utilisateur :** - Créer un ticket SAV (produit, fournisseur, client, problème, priorité) - Retrouver un ticket (recherche texte + filtres statut/priorité/fournisseur) - Ouvrir le suivi d'un ticket et changer son statut ou sa priorité - Ajouter un commentaire de suivi - Modifier un ticket (bouton crayon) - Supprimer un ticket (admin/directeur) **Appels API :** | Endpoint | Déclencheur | Handler serveur | Notes | |---|---|---|---| | GET /api/sav/tickets?status=&priority=&supplierId=&groupId= | au montage puis à chaque changement de filtre statut/priorité/fournisseur (nouvelle queryKey) | server/routes.ts:5242 -> storage.getUserWithGroups (2 requêtes) + storage.getSavTickets (server/storage.ts:2656) | Aucune pagination/LIMIT ; SELECT de lignes complètes savTickets + suppliers + groups (dont logo base64, config SMTP/NocoDB) + users (dont hash du mot de passe) ; Promise.all de getSavTicketHistory par ticket (no-op actuellement) ; 3 console.log par appel. | | GET /api/sav/stats?groupId= | au montage (queryFn fetch manuel, staleTime 2 min) | server/routes.ts:5524 -> storage.getUserWithGroups + storage.getSavTicketStats (server/storage.ts:2818) | 3 requêtes SQL séquentielles (group by status, group by priority, count) + variable baseQuery morte ; 'Critiques' compte aussi les tickets résolus/fermés. | | GET /api/suppliers | au montage (staleTime 10 min) | server/routes.ts:1141 -> storage.getUser + storage.getSuppliers (server/storage.ts:467) | Liste complète de tous les fournisseurs pour 2 Select sans recherche (filtre + formulaire). | | GET /api/groups | au montage (même clé que Layout.tsx:49, donc dédupliqué) | server/routes.ts:970 -> storage.getUserWithGroups + storage.getGroups (server/storage.ts:432) | Utilisé uniquement pour le formulaire de création ; StoreContext expose déjà `stores`. | | GET /api/sav/tickets/:id | ouverture de la modale de suivi (staleTime 0, déclaré 2 fois lignes 142 et 376) | server/routes.ts:5288 -> storage.getUserWithGroups + storage.getSavTicket (server/storage.ts:2720) | Renvoie les mêmes données que la liste (history toujours []), donc requête redondante à chaque ouverture. | | POST /api/sav/tickets | clic 'Créer le ticket' | server/routes.ts:5319 -> getUserWithGroups + getGroups (admin) + storage.createSavTicket (server/storage.ts:2747) | Numéro de ticket généré dans la route puis écrasé par un count(*) annuel dans storage (doublons possibles). | | PATCH /api/sav/tickets/:id | changement de Statut ou Priorité dans 'Actions Rapides' (sauvegarde immédiate) | server/routes.ts:5390 -> getUserWithGroups + getSavTicket (3 jointures) + storage.updateSavTicket (server/storage.ts:2773) + addSavTicketHistory (no-op) | resolvedAt/closedAt jamais renseignés ; changement de statut non historisé. | | POST /api/sav/tickets/:id/history | clic 'Ajouter commentaire' | server/routes.ts:5471 -> getUserWithGroups + getSavTicket + storage.addSavTicketHistory (server/storage.ts:2808, renvoie null sans rien écrire) | Le commentaire est perdu alors que l'UI affiche 'Commentaire ajouté'. | | DELETE /api/sav/tickets/:id | confirmation dans la modale de suppression | server/routes.ts:5443 -> storage.getUser + getSavTicket + storage.deleteSavTicket (server/storage.ts:2783) | Aucun contrôle d'appartenance au magasin pour un directeur. | **Lisibilité / simplicité :** Page lisible dans sa structure (en-tête, 5 compteurs, filtres, grille de cartes) mais plusieurs fonctions visibles ne marchent pas : le bouton crayon ouvre un formulaire complet dont 'Sauvegarder' affiche 'Fonction en développement', les commentaires affichent un succès mais ne sont jamais enregistrés (historique toujours vide), et la liste ne se rafraîchit pas après création/suppression dès qu'un filtre ou un magasin est sélectionné (staleTime 5 min). Des codes techniques sont affichés tels quels ('pieces_manquantes', 'status_change', 'Statut changé de "en_cours" vers "resolu"'), avec 'N/A' et des emojis. Les cartes ont un curseur 'main' mais ne sont pas cliquables ; les actions sont 3 petites icônes sans libellé. La modale de détail (max-w-6xl, 5 cartes à gauche, overflow-hidden) coupe le bloc 'Actions Rapides' sur des écrans de portable. Le badge 'Nouveau' (commentaire récent) se confond avec le statut 'Nouveau', et les couleurs priorité/statut se chevauchent. Les tickets fermés restent mélangés aux tickets ouverts, sans pagination. Les états d'erreur ne sont pas gérés : une panne API affiche 'Aucun ticket trouvé'. La console est inondée de logs à chaque frappe. ### `/sav (mobile)` **Rôle :** Version téléphone du SAV : consulter les tickets du magasin, appeler le client, changer rapidement le statut et créer un ticket depuis la surface de vente. **Tâches principales de l'utilisateur :** - Rechercher un ticket (n°, client, produit) - Appeler le client (bouton 'Appeler') - Changer le statut via le menu ⋮ - Créer un ticket via le bouton flottant + **Appels API :** | Endpoint | Déclencheur | Handler serveur | Notes | |---|---|---|---| | GET /api/sav-tickets?storeId= | au montage (enabled si selectedStoreId) | AUCUN handler (seul /api/sav/tickets existe, server/routes.ts:5242) -> 404 Express en production (index.production.ts:157-160 fait next() pour /api/) | La liste est toujours vide : l'erreur est masquée par l'état vide 'Aucun ticket SAV'. | | GET /api/suppliers | au montage (queryFn apiRequest, staleTime par défaut 30 s) | server/routes.ts:1141 -> storage.getSuppliers | Nécessaire uniquement pour le formulaire de création. | | GET /api/groups | au montage | server/routes.ts:970 -> storage.getGroups / userGroups | Utilisé seulement en repli dans onSubmit (admin sans magasin). | | PATCH /api/sav-tickets/:id/status | clic sur un statut dans le menu ⋮ | AUCUN handler | Valeurs envoyées en anglais ('in_progress', 'waiting_parts', 'resolved', 'closed') hors de l'enum serveur ; pas de onError. | | POST /api/sav-tickets | submit du formulaire 'Créer le ticket' | AUCUN handler | En production : toast d'erreur contenant le HTML brut du 404. | **Lisibilité / simplicité :** Interface mobile simple et bien pensée (cartes compactes, bouton 'Appeler', bouton flottant), mais entièrement non fonctionnelle : elle appelle des endpoints /api/sav-tickets inexistants, donc la liste est toujours vide et la création comme le changement de statut échouent. Les erreurs de validation du formulaire ne s'affichent jamais (FormMessage absent). Le bouton + est visible pour les employés alors que le serveur refuse la création. Il n'y a ni vue détail, ni commentaires, ni confirmation avant 'Fermer'. Les libellés et couleurs divergent du desktop (badge 'Pièces' au lieu de 'Attente pièces', 'attente_echange' affiché en code brut, violet au lieu de bleu), avec du jargon ('Gencode (EAN13)', placeholder 'Scanner...' sans scanner). ## Constats | ID | Sév. | Catégorie | Effort | Titre | Fichier | |---|---|---|---|---|---| | [MSAV-01](#msav-01) | haute | bug | S | La page mobile appelle des endpoints inexistants (/api/sav-tickets) : SAV mobile inutilisable | `client/src/pages/mobile/SavPage.tsx:88` | | [MSAV-02](#msav-02) | haute | bug | S | Valeurs de statut en anglais, hors de l'enum serveur | `client/src/pages/mobile/SavPage.tsx:230` | | [SAV-01](#sav-01) | haute | bug | S | La liste ne se rafraîchit pas après création/modification/suppression (queryKey incompatible avec l'invalidation) | `client/src/pages/SavTickets.tsx:98` | | [SAV-02](#sav-02) | haute | bug | M | Les commentaires et l'historique ne sont jamais enregistrés, mais l'UI affiche un succès | `server/storage.ts:2808` | | [SAV-03](#sav-03) | haute | ux-simplicite | S | Bouton 'Modifier' : formulaire complet dont la sauvegarde n'est pas implémentée | `client/src/pages/SavTickets.tsx:1338` | | [SAV-04](#sav-04) | haute | bug | S | Le hash du mot de passe du créateur est envoyé au navigateur pour chaque ticket | `server/storage.ts:2669` | | [SAV-05](#sav-05) | haute | perf-api | S | Chaque ticket embarque la ligne magasin complète, logo base64 et config SMTP/NocoDB compris | `server/storage.ts:2668` | | [SAV-06](#sav-06) | haute | bug | S | Un utilisateur non admin sans magasin assigné voit les tickets de tous les magasins | `server/routes.ts:5272` | | [MSAV-03](#msav-03) | moyenne | ux-simplicite | S | Les erreurs de validation du formulaire mobile ne s'affichent jamais | `client/src/pages/mobile/SavPage.tsx:283` | | [MSAV-04](#msav-04) | moyenne | ux-simplicite | S | Bouton + et menu de statut visibles pour les employés, qui n'ont pas le droit | `client/src/pages/mobile/SavPage.tsx:397` | | [MSAV-05](#msav-05) | moyenne | ux-simplicite | M | Mobile : ni détail, ni commentaires, ni confirmation de fermeture, ni gestion d'erreur du statut | `client/src/pages/mobile/SavPage.tsx:106` | | [SAV-07](#sav-07) | moyenne | bug | S | Création par un admin : ticket rattaché au premier magasin alphabétique et non au magasin sélectionné | `client/src/pages/SavTickets.tsx:337` | | [SAV-08](#sav-08) | moyenne | bug | S | Le formulaire 'Nouveau Ticket' est pré-rempli avec le dernier ticket ouvert en modification | `client/src/pages/SavTickets.tsx:400` | | [SAV-09](#sav-09) | moyenne | bug | S | DELETE : aucun contrôle d'appartenance au magasin | `server/routes.ts:5446` | | [SAV-10](#sav-10) | moyenne | perf-serveur | S | Chaque route SAV recharge l'utilisateur et ses magasins déjà fournis par la session | `server/routes.ts:5245` | | [SAV-11](#sav-11) | moyenne | perf-serveur | S | Statistiques SAV : 3 requêtes séquentielles au lieu d'une seule | `server/storage.ts:2832` | | [SAV-12](#sav-12) | moyenne | perf-serveur | M | Schéma N+1 : un appel getSavTicketHistory par ticket de la liste | `server/storage.ts:2704` | | [SAV-13](#sav-13) | moyenne | perf-api | M | Liste sans pagination ni limite, rendue entièrement en cartes | `server/storage.ts:2701` | | [SAV-14](#sav-14) | moyenne | perf-client | S | Chaque changement de filtre vide la liste et affiche un spinner (aucun placeholderData) | `client/src/pages/SavTickets.tsx:97` | | [SAV-15](#sav-15) | moyenne | dette-code | S | console.log de debug exécutés à chaque rendu (chaque frappe clavier) | `client/src/pages/SavTickets.tsx:294` | | [SAV-18](#sav-18) | moyenne | lisibilite | S | Codes techniques, anglais et emojis affichés à l'utilisateur | `client/src/pages/SavTickets.tsx:1034` | | [SAV-19](#sav-19) | moyenne | ux-simplicite | S | Modale de suivi trop chargée : 'Actions Rapides' coupé en bas sur écran de portable | `client/src/pages/SavTickets.tsx:966` | | [SAV-20](#sav-20) | moyenne | accessibilite | S | Cartes non cliquables malgré cursor-pointer ; actions en icônes minuscules sans libellé | `client/src/pages/SavTickets.tsx:846` | | [SAV-22](#sav-22) | moyenne | ux-simplicite | S | Compteurs trompeurs et non cliquables (Critiques inclut les tickets clos, Résolus inclut Fermés) | `server/storage.ts:2882` | | [SAV-23](#sav-23) | moyenne | ux-simplicite | S | Pas d'état d'erreur, état vide sans action, et compteurs qui apparaissent en décalant la page | `client/src/pages/SavTickets.tsx:832` | | [MSAV-06](#msav-06) | basse | coherence-design | S | Libellés et couleurs de statut différents du desktop ; statut 'attente_echange' affiché brut | `client/src/pages/mobile/SavPage.tsx:165` | | [MSAV-07](#msav-07) | basse | lisibilite | S | Jargon 'Gencode (EAN13)' et placeholder 'Scanner...' sans scanner | `client/src/pages/mobile/SavPage.tsx:284` | | [MSAV-08](#msav-08) | basse | dette-code | S | Imports inutilisés et requêtes chargées au montage pour un formulaire fermé | `client/src/pages/mobile/SavPage.tsx:100` | | [MSAV-09](#msav-09) | basse | ux-simplicite | S | Erreur ou absence de magasin sélectionné affichée comme 'Aucun ticket SAV' | `client/src/pages/mobile/SavPage.tsx:202` | | [SAV-16](#sav-16) | basse | perf-serveur | S | Logs console sur chaque requête GET SAV (chemin chaud) | `server/routes.ts:5258` | | [SAV-17](#sav-17) | basse | dette-code | S | Requête de détail déclarée deux fois, plus fetch manuels incohérents | `client/src/pages/SavTickets.tsx:376` | | [SAV-21](#sav-21) | basse | coherence-design | S | Badges ambigus : 'Nouveau' commentaire contre statut 'Nouveau', couleurs priorité/statut qui se chevauchent | `client/src/pages/SavTickets.tsx:856` | | [SAV-24](#sav-24) | basse | ux-simplicite | S | Changement de statut/priorité enregistré immédiatement, sans confirmation, avec un message inexact | `client/src/pages/SavTickets.tsx:1123` | | [SAV-25](#sav-25) | basse | ux-simplicite | S | Les tickets fermés sont mélangés aux tickets à traiter par défaut | `client/src/pages/SavTickets.tsx:59` | | [SAV-26](#sav-26) | basse | bug | S | Numéros de ticket dupliqués possibles (count(*) + 1) et génération en double | `server/storage.ts:2750` | | [SAV-27](#sav-27) | basse | perf-serveur | S | Index SAV absents du schéma Drizzle, pas d'index composite magasin+date ni fournisseur | `shared/schema.ts:869` | | [SAV-28](#sav-28) | basse | ux-simplicite | M | Formulaire de création : fournisseur sans recherche et validation non localisée | `client/src/pages/SavTickets.tsx:536` | | [SAV-29](#sav-29) | basse | perf-bundle | S | Pages SAV desktop et mobile importées statiquement dans le bundle initial | `client/src/components/RouterProduction.tsx:25` | ### MSAV-01 **La page mobile appelle des endpoints inexistants (/api/sav-tickets) : SAV mobile inutilisable** — bug, sévérité haute, effort S - **Fichier :** `client/src/pages/mobile/SavPage.tsx:88` - **Constat :** L88 `fetch(`/api/sav-tickets?storeId=${selectedStoreId}`)`, L108 `apiRequest(`/api/sav-tickets/${id}/status`, 'PATCH', ...)`, L116 `apiRequest('/api/sav-tickets', 'POST', data)`. Aucune route de ce nom dans le serveur (grep 'sav-tickets' dans server/ : 0 résultat). Seuls `/api/sav/tickets` et `/api/sav/tickets/:id` existent (routes.ts:5242-5443). En production, index.production.ts:157-160 fait `next()` pour /api/, d'où un 404. - **Impact :** Sur téléphone, la liste affiche toujours 'Aucun ticket SAV', la création renvoie un toast d'erreur contenant le HTML du 404, et le changement de statut échoue sans message. Les employés en magasin ne peuvent pas utiliser le SAV. - **Recommandation :** Utiliser `/api/sav/tickets` (GET avec `groupId` pour l'admin, comme sur desktop, en partageant la même queryKey), `PATCH /api/sav/tickets/:id` avec `{ status }`, et `POST /api/sav/tickets`. Idéalement, extraire un hook commun `useSavTickets()` partagé entre desktop et mobile. ### MSAV-02 **Valeurs de statut en anglais, hors de l'enum serveur** — bug, sévérité haute, effort S - **Fichier :** `client/src/pages/mobile/SavPage.tsx:230` - **Constat :** L230-239 : `status: 'in_progress'`, `'waiting_parts'`, `'resolved'`, `'closed'`. Le schéma attend `z.enum(["nouveau", "en_cours", "attente_pieces", "attente_echange", "resolu", "ferme"])` (schema.ts:919), et init.sql:356 impose un CHECK sur ces valeurs. Le menu ne propose pas non plus 'Nouveau' ni 'Attente échange'. - **Impact :** Même après correction de l'URL (MSAV-01), chaque changement de statut sera rejeté (400 Validation error), et un ticket en 'Attente échange' ne pourra pas être géré sur mobile. - **Recommandation :** Utiliser les valeurs de l'enum ('en_cours', 'attente_pieces', 'attente_echange', 'resolu', 'ferme') via un statusConfig partagé avec le desktop (à extraire dans un fichier commun). ### SAV-01 **La liste ne se rafraîchit pas après création/modification/suppression (queryKey incompatible avec l'invalidation)** — bug, sévérité haute, effort S - **Fichier :** `client/src/pages/SavTickets.tsx:98` - **Constat :** L95-100 : `const ticketsUrl = `/api/sav/tickets${queryParams...}`` puis `queryKey: [ticketsUrl, selectedStoreId], staleTime: 1000 * 60 * 5`. Les mutations font `queryClient.invalidateQueries({ queryKey: ['/api/sav/tickets'] })` (L170, 209, 240, 268). TanStack compare les éléments du tableau un à un : '/api/sav/tickets' !== '/api/sav/tickets?groupId=3'. Dès qu'un admin a un magasin sélectionné (L91-93 ajoute groupId) ou qu'un filtre est actif, l'invalidation ne correspond à aucune requête. - **Impact :** Un ticket créé n'apparaît pas, un ticket supprimé reste affiché (un second clic sur Supprimer renvoie 'Ticket not found') et un statut modifié reste ancien sur la carte, pendant 5 minutes. L'utilisateur croit que l'action a échoué et la refait (doublons). - **Recommandation :** Partie auto-applicable : clé `['/api/sav/tickets', { status, priority, supplierId, groupId }]` avec une queryFn qui reconstruit l'URL via apiRequest (le message '401: ...' garde la redirection du retry global). Alternative minimale : `invalidateQueries({ predicate: q => String(q.queryKey[0]).startsWith('/api/sav/tickets') })`. Après un commentaire, n'invalider que le détail. Le passage à setQueryData est une optimisation à faire à part, non automatique. ### SAV-02 **Les commentaires et l'historique ne sont jamais enregistrés, mais l'UI affiche un succès** — bug, sévérité haute, effort M - **Fichier :** `server/storage.ts:2808` - **Constat :** storage.ts:2808-2816 `async addSavTicketHistory(...) { // const [history] = await db.insert(savTicketHistory)... return null; }` et 2790-2805 `getSavTicketHistory(...) { // ... return []; }`. La route routes.ts:5508 renvoie 201. Côté client, SavTickets.tsx:214-218 affiche le toast 'Commentaire ajouté', mais la colonne 'Historique et commentaires' (L1154-1177) montre toujours 'Aucun commentaire pour le moment'. Les changements de statut (routes.ts:5424-5430) ne sont pas historisés non plus, et `resolvedAt`/`closedAt` (schema.ts:887-888) ne sont jamais renseignés par le PATCH (routes.ts:5420-5421). - **Impact :** Perte silencieuse des informations saisies par les managers (suivi fournisseur, échanges client). La fonction 'Nouveau' (commentaire < 24 h, L853) ne s'allume jamais. Aucune traçabilité de qui a changé un statut, ni de date de résolution. - **Recommandation :** Recréer la table sav_ticket_history (déjà définie dans migrations/sav_production_fix.sql:41), déclarer `savTicketHistory` dans shared/schema.ts et décommenter les implémentations. Dans updateSavTicket, positionner resolvedAt/closedAt quand le statut passe à 'resolu'/'ferme'. En attendant, masquer le champ commentaire plutôt que d'afficher un faux succès. ### SAV-03 **Bouton 'Modifier' : formulaire complet dont la sauvegarde n'est pas implémentée** — ux-simplicite, sévérité haute, effort S - **Fichier :** `client/src/pages/SavTickets.tsx:1338` - **Constat :** L1337-1344 : `onClick={() => { // TODO: Implement update ticket functionality toast({ title: "Fonction en développement", description: "La modification de tickets sera bientôt disponible." }); }}`. Le bouton crayon est pourtant affiché sur chaque carte pour admin/directeur/manager (L932-941). Le PATCH serveur accepte déjà une mise à jour partielle (routes.ts:5420 `insertSavTicketSchema.partial().parse(req.body)`). - **Impact :** L'utilisateur remplit 9 champs puis découvre que rien n'est enregistré : frustration et perte de temps sur une action visible en permanence. - **Recommandation :** Si on branche la sauvegarde : envoyer seulement les champs éditables (supplierId en entier, produit, problemType, problemDescription, priority, clientName, clientPhone), jamais groupId ni createdBy. Côté serveur, retirer groupId/createdBy/ticketNumber du schéma PATCH (ou vérifier l'accès au nouveau groupId). Sinon, masquer le bouton crayon : c'est une décision produit. ### SAV-04 **Le hash du mot de passe du créateur est envoyé au navigateur pour chaque ticket** — bug, sévérité haute, effort S - **Fichier :** `server/storage.ts:2669` - **Constat :** storage.ts:2665-2669 `.select({ ticket: savTickets, supplier: suppliers, group: groups, creator: users })` et de même 2722-2726 pour getSavTicket. La table users contient `password: varchar("password")` (schema.ts:40). Le middleware de sortie (routes.ts:165-171, sanitize.ts:36) ne retire que smtpPassword. Par contraste, /api/user (localAuth.ts:217-226) choisit ses champs explicitement. - **Impact :** Tout employé qui ouvre la page SAV reçoit les hash bcrypt des managers/admins créateurs de tickets (craquage hors ligne possible). Cela alourdit aussi chaque ticket de colonnes inutiles. - **Recommandation :** Comme proposé. Ajuster aussi le type `SavTicketWithRelations.creator` (schema.ts:939) en `Pick`, sinon getSavTicket ne type-checke plus. MemStorage (4940, 4962) peut faire la même projection. ### SAV-05 **Chaque ticket embarque la ligne magasin complète, logo base64 et config SMTP/NocoDB compris** — perf-api, sévérité haute, effort S - **Fichier :** `server/storage.ts:2668` - **Constat :** storage.ts:2667-2668 `supplier: suppliers, group: groups` (lignes complètes). groups contient `logo: text("logo"), // Logo en data URI (data:image/png;base64,...)` (schema.ts:67), plus smtpHost/smtpUser/webhookUrl/nocodb*. Le client n'utilise que `ticket.supplier?.name` (L911, L1067) et `ticket.group?.name` (L1071). - **Impact :** Avec un logo de 50-200 Ko, 100 tickets donnent 5 à 20 Mo de JSON à télécharger, parser et traverser récursivement (stripSmtpPassword) à chaque chargement et chaque filtre. Le chargement est lent sur le Wi-Fi ou la 4G du magasin, et des données sensibles (hôte/utilisateur SMTP) fuient. - **Recommandation :** Partie auto-applicable : projection `supplier: { id, name }`, `group: { id, name, color }` dans getSavTickets/getSavTicket, et types ajustés (Pick, Pick). Ajouter la compression gzip est une autre décision : nouvelle dépendance, effet global sur toute l'API, et à vérifier avec un éventuel proxy nginx. À traiter à part, pas automatiquement. ### SAV-06 **Un utilisateur non admin sans magasin assigné voit les tickets de tous les magasins** — bug, sévérité haute, effort S - **Fichier :** `server/routes.ts:5272` - **Constat :** routes.ts:5266 `groupIds = userGroups ? userGroups.map(...) : [];` puis 5272 `groupIds: groupIds.length > 0 ? groupIds : undefined`. Dans storage.ts:2678, `if (filters?.groupIds?.length)` : aucun filtre magasin n'est appliqué. Même logique pour les stats (routes.ts:5548, 5552). - **Impact :** Un employé ou un manager fraîchement créé, ou dont l'affectation a été retirée, voit les tickets SAV (noms et téléphones clients) de toute l'enseigne. - **Recommandation :** Pour un non-admin dont groupIds est vide, renvoyer immédiatement `res.json([])` (et des stats à zéro) au lieu de passer `undefined`. ### MSAV-03 **Les erreurs de validation du formulaire mobile ne s'affichent jamais** — ux-simplicite, sévérité moyenne, effort S - **Fichier :** `client/src/pages/mobile/SavPage.tsx:283` - **Constat :** FormMessage est importé (L51), mais aucun FormItem ne le rend, par exemple L283-286 `Gencode (EAN13)`. Le schéma zod définit pourtant 'Gencode requis', 'Désignation requise', 'Fournisseur requis', 'Description requise' (L66-71). supplierId vaut 0 par défaut (L135). - **Impact :** En appuyant sur 'Créer le ticket' avec un champ manquant, rien ne se passe à part un libellé qui devient rouge en haut d'une feuille de 90vh. L'utilisateur pense que le bouton ne marche pas. - **Recommandation :** Ajouter `` dans chaque FormItem et faire défiler jusqu'au premier champ en erreur (`shouldFocusError` est actif par défaut avec les champs enregistrés). ### MSAV-04 **Bouton + et menu de statut visibles pour les employés, qui n'ont pas le droit** — ux-simplicite, sévérité moyenne, effort S - **Fichier :** `client/src/pages/mobile/SavPage.tsx:397` - **Constat :** L397-402 : le FAB `