# Publicités _35 constats vérifiés — 3 haute, 19 moyenne, 13 basse._ ## Pages analysées ### `/publicities (desktop, >= 768 px, tablette incluse)` **Rôle :** Consulter le planning annuel des campagnes publicitaires (catalogues/promos) et savoir quels magasins y participent. L'admin les crée, les modifie et les supprime. **Tâches principales de l'utilisateur :** - Voir les publicités en cours et à venir de l'année pour son magasin - Visualiser la couverture semaine par semaine (Vue d'ensemble) ou jour par jour (vue calendrier) - Ouvrir le détail d'une publicité (dates, magasins participants, créateur) - Exporter la liste de l'année en CSV - Admin : créer ou modifier une publicité et cocher les magasins participants (PublicityForm en modale) - Admin : supprimer une publicité après confirmation **Appels API :** | Endpoint | Déclencheur | Handler serveur | Notes | |---|---|---|---| | GET /api/ad-campaigns?year=YYYY&storeId=N | au montage, puis à chaque changement d'année ou de magasin (clé ['/api/ad-campaigns', selectedYear, selectedStoreId], fetch() manuel, staleTime par défaut 30 s, sans placeholderData ni enabled) | server/routes.ts:4595 → storage.getUserWithGroups (routes.ts:4597, 2 requêtes, en double avec deserializeUser localAuth.ts:141) → storage.getPublicities (storage.ts:1502 : SELECT publicities WHERE year, puis SELECT participations LEFT JOIN groups) | groupIds calculé par la route mais ignoré par getPublicities, donc pas de filtre magasin ni de cloisonnement par rôle. Ligne groups complète (logo base64, config SMTP/NocoDB) jointe à chaque participation. Créateur jamais joint. Triple tri (SQL, JS serveur, JS client). 2 console.log à chaque appel. Au total 6 requêtes SQL, dont 4 séquentielles avant les données. En cas d'erreur, réponse 500 avec []. | | GET /api/groups | au montage si user (même clé que Layout.tsx:48, donc cache partagé, pas de double appel réseau) | server/routes.ts:970 → storage.getUserWithGroups + storage.getGroups (storage.ts:432, SELECT * FROM groups) pour l'admin, sinon userGroups (id/name/color) | La page n'utilise que id, name et color. Pour l'admin, toutes les colonnes partent (logo en data URI, SMTP, webhook). Doublon de useStore().stores. | | POST /api/ad-campaigns | au clic sur Créer dans PublicityForm (fetch manuel) | server/routes.ts:4629 → getUserWithGroups → insertPublicitySchema.parse → storage.createPublicity (storage.ts:1586) → storage.setPublicityParticipations (storage.ts:1630 : DELETE puis INSERT) → storage.getPublicity (storage.ts:1562) | 5 à 6 requêtes séquentielles hors transaction. Un N° en double renvoie un 500 générique en anglais. La réponse complète est ignorée par le client, qui invalide toute la liste et la recharge. | | PUT /api/ad-campaigns/:id | au clic sur Modifier dans PublicityForm | server/routes.ts:4665 → storage.updatePublicity (storage.ts:1591, req.body brut sans validation) → setPublicityParticipations → getPublicity | Pas de validation Zod, pas de transaction, pas de 404 si l'id n'existe pas. Invalidation large côté client. | | POST /api/ad-campaigns/:id/delete (repli DELETE /api/ad-campaigns/:id sur 404) | au clic sur Supprimer dans la modale de confirmation | server/routes.ts:4737 (copie de routes.ts:4697) → storage.deletePublicity (storage.ts:1600 : DELETE participations RETURNING puis DELETE publicities RETURNING) | Deux handlers copiés-collés avec environ 8 console.log chacun. La suppression manuelle des participations est redondante avec le ON DELETE CASCADE. Le repli DELETE côté client est du code mort. | **Lisibilité / simplicité :** Page dense et peu hiérarchisée. L'en-tête aligne 7 contrôles sur une ligne sans retour à la ligne : bascule Liste/Grille en icônes sans libellé, bouton 'Vue d'ensemble', icône filtre, année, mois, 'Exporter CSV', 'Nouvelle publicité'. Viennent ensuite 3 cartes stats (même icône), puis une grille annuelle de 12 colonnes affichée par défaut avant la liste, qui se retrouve sous la ligne de flottaison. La liste desktop n'a ni recherche ni filtre par statut ou magasin, alors que la version mobile a une recherche. La colonne 'Créé par' affiche toujours 'Utilisateur'. Le sélecteur de magasin n'a aucun effet (bug serveur). Des statuts et la vue calendrier sont faux d'un jour (fuseau horaire), et les numéros de semaine ne sont pas ISO certaines années. Pendant le chargement, les stats affichent 0 et toutes les semaines apparaissent 'sans publicité'. Il n'y a pas d'état d'erreur : une erreur s'affiche comme 'Aucune publicité… Commencez par créer…', y compris pour les non-admins. Les messages d'erreur serveur sont en anglais. Le tableau n'est pas scrollable en tablette (colonne Actions coupée). La modale de création n'a pas de hauteur max. Des console.log s'exécutent à chaque rendu en production. La vue calendrier fait doublon avec la page /calendar. ### `/publicities (mobile, < 768 px)` **Rôle :** Consulter rapidement sur téléphone les publicités de l'année pour le magasin sélectionné. **Tâches principales de l'utilisateur :** - Voir la liste des publicités de l'année pour son magasin - Rechercher par N° ou désignation - Lire le statut (En cours / À venir / Terminée) et les dates **Appels API :** | Endpoint | Déclencheur | Handler serveur | Notes | |---|---|---|---| | GET /api/ad-campaigns?storeId=N&year=YYYY | au montage et au changement d'année ou de magasin, seulement si selectedStoreId (clé ['/api/ad-campaigns', selectedStoreId, selectedYear], ordre inverse de la version desktop) | server/routes.ts:4595 → storage.getUserWithGroups → storage.getPublicities (storage.ts:1502) | storeId est ignoré par le serveur (groupIds non appliqué), donc la liste contient aussi les pubs où le magasin ne participe pas. Le payload contient le logo base64 de chaque magasin participant. Pas d'appel quand aucun magasin n'est sélectionné, ce qui donne une liste vide trompeuse. | **Lisibilité / simplicité :** Liste de cartes simple et lisible, avec recherche et année. En revanche : le bouton flottant '+' est visible pour tous les rôles et ne fait rien (TODO). La liste est vide avec 'Aucune publicité trouvée' quand aucun magasin n'est sélectionné (admin 'Tous les magasins'). Pas de détail ni de magasins participants. 'Créé par' affiche l'identifiant technique (admin_local, manual_...). Le statut passe à 'Terminée' pendant tout le dernier jour. Badges et couleurs différents du desktop (vert plein contre vert pâle, icône violette contre verte). Pas d'état d'erreur. Le même message s'affiche pour une liste vide et une recherche sans résultat. Les pubs en cours ne sont pas mises en avant (tri par N°). ## Constats | ID | Sév. | Catégorie | Effort | Titre | Fichier | |---|---|---|---|---|---| | [PUB-01](#pub-01) | haute | bug | S | getPublicities ignore groupIds : filtre magasin et cloisonnement par rôle inopérants | `server/storage.ts:1502` | | [PUB-02](#pub-02) | haute | perf-api | S | Ligne groups complète (logo base64, config SMTP/NocoDB) jointe à chaque participation | `server/storage.ts:1536` | | [PUB-11](#pub-11) | haute | ux-simplicite | S | Bouton flottant « + » sans action, visible par tous les rôles | `client/src/pages/mobile/PublicitiesPage.tsx:162` | | [PUB-03](#pub-03) | moyenne | bug | S | Créateur jamais joint : « Créé par » affiche « Utilisateur » ou un identifiant technique | `server/storage.ts:1503` | | [PUB-04](#pub-04) | moyenne | perf-serveur | S | Utilisateur rechargé en base dans chaque handler alors que deserializeUser l'a déjà chargé | `server/routes.ts:4597` | | [PUB-06](#pub-06) | moyenne | dette-code | S | Route /api/ad-campaigns/debug exposée et routes de suppression dupliquées | `server/routes.ts:4529` | | [PUB-07](#pub-07) | moyenne | perf-client | S | Deux console.log identiques à chaque rendu, plus des logs dans queryFn et la mutation | `client/src/pages/Publicities.tsx:75` | | [PUB-08](#pub-08) | moyenne | perf-client | M | Vue annuelle, calendrier et stats recalculés à chaque rendu sans useMemo | `client/src/pages/Publicities.tsx:229` | | [PUB-09](#pub-09) | moyenne | bug | S | Fuseau horaire : 1er jour absent du calendrier, « Terminée » le dernier jour | `client/src/pages/Publicities.tsx:624` | | [PUB-10](#pub-10) | moyenne | bug | S | Numéros de semaine non ISO (décalés d'une semaine certaines années) | `client/src/pages/Publicities.tsx:206` | | [PUB-12](#pub-12) | moyenne | ux-simplicite | S | Liste vide trompeuse quand aucun magasin n'est sélectionné | `client/src/pages/mobile/PublicitiesPage.tsx:45` | | [PUB-13](#pub-13) | moyenne | ux-simplicite | S | Aucun état d'erreur : une panne s'affiche comme « aucune publicité » | `client/src/pages/Publicities.tsx:572` | | [PUB-14](#pub-14) | moyenne | perf-client | S | Requête sans keepPreviousData, staleTime ni enabled : flash à 0, double appel au montage | `client/src/pages/Publicities.tsx:42` | | [PUB-17](#pub-17) | moyenne | bug | M | PUT sans validation, écritures non transactionnelles (création, participations, suppression) | `server/routes.ts:4681` | | [PUB-18](#pub-18) | moyenne | ux-simplicite | S | Messages d'erreur génériques en anglais, N° en double non expliqué | `server/routes.ts:4661` | | [PUB-19](#pub-19) | moyenne | ux-simplicite | M | Champ Année manuel, bornes incohérentes, filtrage par année saisie plutôt que par dates | `client/src/components/PublicityForm.tsx:27` | | [PUB-20](#pub-20) | moyenne | ux-simplicite | S | Formulaire : erreurs qui persistent, alerte rouge sur un champ optionnel, watch global | `client/src/components/PublicityForm.tsx:215` | | [PUB-21](#pub-21) | moyenne | lisibilite | M | En-tête surchargé : 7 contrôles sur une ligne, non responsive | `client/src/pages/Publicities.tsx:321` | | [PUB-22](#pub-22) | moyenne | ux-simplicite | S | Tableau 7 colonnes non scrollable : colonne Actions coupée en tablette | `client/src/pages/Publicities.tsx:720` | | [PUB-23](#pub-23) | moyenne | ux-simplicite | M | Liste desktop sans recherche ni filtre de statut, libellés abrégés, lignes non cliquables | `client/src/pages/Publicities.tsx:748` | | [PUB-24](#pub-24) | moyenne | lisibilite | M | Vue d'ensemble affichée par défaut, info uniquement au survol, légende sans noms | `client/src/pages/Publicities.tsx:466` | | [PUB-30](#pub-30) | moyenne | perf-api | S | « Publicités à venir » : 3 appels séquentiels d'années complètes faute de filtre API | `client/src/pages/Dashboard.tsx:197` | | [PUB-05](#pub-05) | basse | perf-serveur | S | console.log dans le chemin chaud de lecture et logs verbeux sur la suppression | `server/storage.ts:1525` | | [PUB-15](#pub-15) | basse | perf-client | S | Triple tri (SQL, serveur JS, client JS) incompatible avec le format de N° suggéré | `client/src/pages/Publicities.tsx:57` | | [PUB-16](#pub-16) | basse | perf-serveur | S | Association participations ↔ publicités en O(N×M) et 2 requêtes séquentielles | `server/storage.ts:1550` | | [PUB-25](#pub-25) | basse | lisibilite | S | Vue calendrier : seul le N° est affiché, pas de navigation mois par mois, doublon avec /calendar | `client/src/pages/Publicities.tsx:651` | | [PUB-26](#pub-26) | basse | coherence-design | S | Icônes, badges de statut, CTA et confirmation incohérents | `client/src/pages/Publicities.tsx:775` | | [PUB-27](#pub-27) | basse | accessibilite | S | Boutons Liste/Grille sans nom accessible ni infobulle | `client/src/pages/Publicities.tsx:324` | | [PUB-28](#pub-28) | basse | dette-code | S | Code mort et requête groups redondante avec StoreContext | `client/src/pages/Publicities.tsx:303` | | [PUB-29](#pub-29) | basse | perf-api | S | Invalidation par préfixe après mutation : la réponse serveur complète est ignorée | `client/src/components/PublicityForm.tsx:84` | | [PUB-31](#pub-31) | basse | ux-simplicite | S | Modale création/édition sans hauteur max : boutons hors écran avec beaucoup de magasins | `client/src/pages/Publicities.tsx:857` | | [PUB-32](#pub-32) | basse | perf-bundle | M | Pages Publicités et formulaire importés statiquement dans le bundle initial | `client/src/components/RouterProduction.tsx:15` | | [PUB-33](#pub-33) | basse | ux-simplicite | M | Mobile : pas de détail, pas de magasins participants, pas de priorisation des pubs en cours | `client/src/pages/mobile/PublicitiesPage.tsx:119` | | [PUB-34](#pub-34) | basse | dette-code | M | Trois queryFn différents et clés incohérentes pour le même endpoint | `client/src/pages/mobile/PublicitiesPage.tsx:43` | | [PUB-35](#pub-35) | basse | perf-serveur | S | Index publicities/participations hors de shared/schema.ts, index group_id non garanti | `shared/schema.ts:166` | ### PUB-01 **getPublicities ignore groupIds : filtre magasin et cloisonnement par rôle inopérants** — bug, sévérité haute, effort S - **Fichier :** `server/storage.ts:1502` - **Constat :** storage.ts:1502 `async getPublicities(year?: number, groupIds?: number[])`. Le seul filtre appliqué est l.1517-1519 `if (year) { query = query.where(eq(publicities.year, year)); }` et `groupIds` n'est jamais lu. Pourtant routes.ts:4605-4615 calcule `groupIds = userGroupIds` pour les non-admins et `[parseInt(storeId)]` pour l'admin. CalendarGrid.tsx:428-442 refiltre côté client pour compenser, Publicities.tsx et PublicitiesPage.tsx ne le font pas. - **Impact :** Employés et managers voient les publicités de tous les magasins. Le sélecteur de magasin ne change ni la liste, ni les stats Total/En cours/À venir, ni l'export CSV. Sur mobile, filtré par magasin, on voit des pubs où le magasin ne participe pas. Le payload est plus gros que nécessaire. - **Recommandation :** Quand groupIds est défini, filtrer en SQL : `and(eq(publicities.year, year), inArray(publicities.id, db.select({ id: publicityParticipations.publicityId }).from(publicityParticipations).where(inArray(publicityParticipations.groupId, groupIds))))`. Il faut garantir l'index group_id (voir PUB-35). Décision produit à prendre : l'admin en « Tous les magasins » continue-t-il de voir les pubs sans magasin ? (comportement actuel) ### PUB-02 **Ligne groups complète (logo base64, config SMTP/NocoDB) jointe à chaque participation** — perf-api, sévérité haute, effort S — vérification : partiellement confirmé - **Fichier :** `server/storage.ts:1536` - **Constat :** storage.ts:1533-1541 `.select({ publicityId: ..., groupId: ..., group: groups })`, idem dans getPublicity l.1570. La table groups (shared/schema.ts:48-79) contient `logo: text("logo") // Logo en data URI (data:image/png;base64,...)`, plus smtpHost, smtpUser, webhookUrl, nocodbTableName, address, phone… Les clients n'utilisent que group.name et group.color (Publicities.tsx:791-793, 939-941 ; CalendarGrid.tsx:659 ; Dashboard.tsx:618-629). Pour l'admin, /api/groups fait aussi `db.select().from(groups)` (storage.ts:432), utilisé ici seulement pour la couleur et le nom. - **Impact :** Le logo est dupliqué pour chaque participation de chaque publicité. Avec 100 pubs × 5 magasins × un logo de 50 Ko, la réponse JSON atteint environ 25 Mo, ce qui ralentit fortement le chargement, surtout en 4G. stripSmtpPassword (sanitize.ts, branché routes.ts:168-174) parcourt récursivement tout ce payload. La configuration SMTP, webhook et NocoDB est en plus exposée aux employés. - **Recommandation :** Limiter la correction à getPublicities et getPublicity : remplacer `group: groups` par `group: { id: groups.id, name: groups.name, color: groups.color }`. Ne pas modifier /api/groups ni getGroups() dans ce lot, car d'autres écrans consomment webhookUrl, logo et smtp*. ### PUB-11 **Bouton flottant « + » sans action, visible par tous les rôles** — ux-simplicite, sévérité haute, effort S - **Fichier :** `client/src/pages/mobile/PublicitiesPage.tsx:162` - **Constat :** l.162-170 : `