From ed9833e56d15eac7a5c2db4e2a00e756d1e3c415 Mon Sep 17 00:00:00 2001 From: pr-daaif Date: Sun, 2 Aug 2026 20:44:21 +0100 Subject: [PATCH] =?UTF-8?q?fix(mobile):=20R6.7=20=E2=80=94=20assignation?= =?UTF-8?q?=20=C3=A0=20un=20technicien=20lors=20de=20l'approbation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le Gestionnaire approuvait une demande sur mobile sans pouvoir assigner l'OT à un technicien (l'OT partait non assigné) — remonté en recette : "ce n'est pas à lui d'agir comme un technicien". Le web le fait déjà (PanneauApprobation, demandes.tsx). PanneauDemandes.tsx : "Approuver → OT" ouvre un panneau avec un ChoixTel "Assigner à" (techniciens actifs, même filtre que le web — status active && role.name.startsWith('Technicien')) avant de confirmer. assigneeIds déjà supporté par l'API et le contrat — aucun changement serveur nécessaire, seule l'UI mobile manquait cette étape. Vérifié de bout en bout : demande → approuvée avec technicien assigné → OT confirmé avec le bon assignee. Typecheck/tests/lint verts. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 3 +- .../src/composants/panneau-demandes.tsx | 85 +++++++++++++++---- docs/journal/journal.md | 36 ++++++++ 3 files changed, 107 insertions(+), 17 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b9a5693..6caeeb7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,5 +80,6 @@ pnpm + Turborepo. `apps/api` : NestJS, Prisma, PostgreSQL (pgvector + PostGIS), - 🔄 **Recette R6 en cours (Gestionnaire, partie 1)** — 2 bugs réels trouvés et corrigés sur iPhone physique : Menu non scrollable (le dernier groupe, Pilotage, était strictement inaccessible une fois les 4 groupes pleinement câblés — présent depuis R6.1, révélé seulement maintenant) ; BC créé sans confirmation visible dans l'app (vérifié côté serveur : les BC étaient bien créés, juste aucun retour affiché). Amélioration sur retour direct : Tiers gagne une fiche détail (identité/contact/BC en cours/sites rattachés) — le lecture-seule sans aucune réaction au tap se lisait comme cassé. Trois signalements vérifiés et écartés (faux positifs) : approbation de demande (a fonctionné), bibliothèque vide (confirmé côté serveur — aucun document sur cette instance, pas un bug mobile), période statistiques (transmise et traitée correctement, les indicateurs affichés ne varient juste pas avec ce jeu de données). - ✅ **Assistant mobile — dictée confirmée sur iPhone physique**, après 3 corrections trouvées en conditions réelles (aucune n'aurait été vue par typecheck/tests/lint) : `expo-file-system` deleteAsync déprécié SDK 57 (import `/legacy`, appliqué aussi à `cloture.tsx`) ; texte transcrit pas entièrement visible (zone multiligne pleine largeur, Envoyer en geste séparé) ; bouton Envoyer chevauchant encore le texte (ScrollView du chat sans `style={flex:1}`, TextInput à hauteur fixe plutôt que `maxHeight` seul, pas fiable sur iOS). - ✅ **R6.6 — Demandeur restreint à son site** : trou trouvé en recette — `GET /assets/options` n'avait aucun filtre (web ET mobile touchés, pas seulement mobile). Corrigé à la racine : relation `User↔Location` (`assignedSites`, migration `r6_demandeur_sites`, vide = aucune restriction pour les autres rôles) ; `AssetsService.allowedLocationIds()` réutilisée par `options()` et par `RequestsService.create` (défense en profondeur, 400 si hors périmètre) ; gestion des sites d'un Demandeur côté web (`personnes.tsx`, invitation + modale dédiée) ; raccourci scan QR côté mobile (`formulaire-demande.tsx`, résout uniquement contre les options déjà filtrées, jamais de repli sur le parc complet). Karim Doukkali (démo) rattaché à Tour Atlas. Bug trouvé en vérification avant tout commit (comparaison id-de-site vs id-d'appareil dans `create()`, aurait rejeté à tort tout signalement d'un Demandeur affecté) et corrigé, méthode renommée `allowedAssetIds` → `allowedLocationIds` pour que le nom dise ce qu'elle retourne. 79/80 tests API (le seul échec est le flake `monthCost` déjà connu, sans rapport) ; typecheck/tests/lint verts sur les 4 paquets. -- 🔄 **Reprise ici** : reste de la recette R6 (Sites/Ascenseurs/Fichiers côté Gestionnaire à confirmer explicitement, puis passage Demandeur avec le nouveau périmètre de site) — avant de considérer R6 close au même sens que R0→R5. `expo-sharing` (ouverture de documents) et `expo-clipboard` (copie du lien d'activation) en réserve pour une prochaine étape native. Restes non bloquants inchangés : redéploiement Dokploy de l'instance ENSET (`AI_SERVICE_TOKEN` à créer — runbook §2 — puis « Réindexer tout »), recette Android sur appareil physique, calibrage `AI_SEUIL_*` et qualité darija sur corpus SPELEV réel, secret `DOKPLOY_WEBHOOK_URL`, production client SPELEV (attend les accès serveur du partenaire). +- ✅ **R6.7 — assignation à l'approbation (mobile)** : le Gestionnaire approuvait une demande mobile sans pouvoir assigner de technicien (l'OT partait non assigné) — « ce n'est pas à lui d'agir comme un technicien ». `PanneauDemandes` gagne un panneau d'approbation avec `ChoixTel` « Assigner à » (techniciens actifs, même filtre que le web), avant confirmation. Rien à changer côté API (`assigneeIds` déjà supporté). Vérifié de bout en bout (demande → approuvée avec assigné → OT avec `assignees` correct). Typecheck/tests/lint verts. +- 🔄 **Reprise ici** : reste de la recette R6 (Sites/Ascenseurs/Fichiers côté Gestionnaire à confirmer explicitement, puis passage Demandeur avec le nouveau périmètre de site et le scan QR) — avant de considérer R6 close au même sens que R0→R5. Question ouverte : le Gestionnaire garde par la matrice R2/R3 un accès `WORK_ORDERS` complet (peut techniquement démarrer/clôturer n'importe quel OT comme un technicien) — établi depuis R2/R3, identique au web, pas une régression R6 ; à resserrer seulement si le référent le demande explicitement. `expo-sharing` (ouverture de documents) et `expo-clipboard` (copie du lien d'activation) en réserve pour une prochaine étape native. Restes non bloquants inchangés : redéploiement Dokploy de l'instance ENSET (`AI_SERVICE_TOKEN` à créer — runbook §2 — puis « Réindexer tout »), recette Android sur appareil physique, calibrage `AI_SEUIL_*` et qualité darija sur corpus SPELEV réel, secret `DOKPLOY_WEBHOOK_URL`, production client SPELEV (attend les accès serveur du partenaire). - Détail quotidien : `docs/journal/journal.md`. Dépôt : `siop-spelev/siop2` (privé), jalons R0→R5 (v1) + R6 en cours. diff --git a/apps/mobile/src/composants/panneau-demandes.tsx b/apps/mobile/src/composants/panneau-demandes.tsx index 5c2a8cd..1c8db2d 100644 --- a/apps/mobile/src/composants/panneau-demandes.tsx +++ b/apps/mobile/src/composants/panneau-demandes.tsx @@ -7,9 +7,10 @@ import { useRejectRequest, useRequests, } from '@/api/exploitation'; +import { useUsers } from '@/api/pilotage'; import { usePermissions } from '@/auth/use-permissions'; import { useTokens, type Tokens } from '@/theme/tokens'; -import { BoutonTel } from './ui'; +import { BoutonTel, ChoixTel } from './ui'; /** Panneau Demandes — UN SEUL composant pour tous les rôles (maquette * « mobile ouvert à tous les rôles », écran 5) : le Demandeur y crée et @@ -68,9 +69,20 @@ const LABEL_STATUT: Record = { function CarteDemande({ demande: r, peutTraiter }: { demande: RequestSummary; peutTraiter: boolean }) { const t = useTokens(); + const { data: users } = useUsers(); const approbation = useApproveRequest(); const rejet = useRejectRequest(); - const [motif, setMotif] = useState(null); // null = fermé + // Un seul panneau ouvert à la fois : approbation (avec assignation — ce + // n'est pas au Gestionnaire d'agir comme un technicien, il délègue) ou + // rejet (motif obligatoire). null = fermé, les deux boutons côte à côte. + const [panneau, setPanneau] = useState<'approuver' | 'rejeter' | null>(null); + const [motif, setMotif] = useState(''); + const [assigneId, setAssigneId] = useState(null); + + const techniciens = (users ?? []).filter( + (u) => u.status === 'active' && u.role.name.startsWith('Technicien'), + ); + const technicien = techniciens.find((tt) => tt.id === assigneId) ?? null; const [enc, fond] = STYLE_STATUT[r.status](t); const enCours = r.status === 'RECEIVED'; @@ -128,22 +140,51 @@ function CarteDemande({ demande: r, peutTraiter }: { demande: RequestSummary; pe ) : null} {peutTraiter && enCours ? ( - motif === null ? ( + panneau === null ? ( - - approbation.mutate({ - id: r.id, - priority: r.isPersonTrapped ? 'PERSON_TRAPPED' : 'HIGH', - }) - } - /> + setPanneau('approuver')} /> - setMotif('')} /> + setPanneau('rejeter')} /> + + + ) : panneau === 'approuver' ? ( + + ({ id: tt.id, label: tt.displayName }))} + surChoix={setAssigneId} + /> + + + { + setPanneau(null); + setAssigneId(null); + }} + /> + + + + approbation.mutate( + { + id: r.id, + priority: r.isPersonTrapped ? 'PERSON_TRAPPED' : 'HIGH', + assigneeIds: assigneId ? [assigneId] : undefined, + }, + { onSuccess: () => setPanneau(null) }, + ) + } + /> + ) : ( @@ -167,13 +208,25 @@ function CarteDemande({ demande: r, peutTraiter }: { demande: RequestSummary; pe /> - setMotif(null)} /> + { + setPanneau(null); + setMotif(''); + }} + /> rejet.mutate({ id: r.id, reason: motif.trim() }, { onSuccess: () => setMotif(null) })} + surAppui={() => + rejet.mutate( + { id: r.id, reason: motif.trim() }, + { onSuccess: () => setPanneau(null) }, + ) + } /> diff --git a/docs/journal/journal.md b/docs/journal/journal.md index ae13e88..8ed127b 100644 --- a/docs/journal/journal.md +++ b/docs/journal/journal.md @@ -4,6 +4,42 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook** --- +## 2026-08-02 — Pr. Daaif (+ Claude) — R6.7 : assignation à l'approbation (mobile) + +**Contexte** — en recette, le référent a signalé que le Gestionnaire doit pouvoir affecter un OT +à un technicien au moment où il approuve une demande — « ce n'est pas à lui d'agir comme un +technicien ». Vérification : le web le fait déjà (`PanneauApprobation`, `demandes.tsx` — +sélecteur « Assigner à » parmi les techniciens actifs). Le mobile (`PanneauDemandes`, construit +en R6.1) approuvait directement avec la priorité seule, sans écran d'assignation — l'OT partait +non assigné, obligeant quelqu'un à revenir dessus ensuite pour le confier à un technicien. + +**Actions** + +- `apps/mobile/src/composants/panneau-demandes.tsx` : le bouton « Approuver → OT » ouvre + désormais un panneau (même patron que le rejet à motif) avec un `ChoixTel` « Assigner à », + peuplé des techniciens actifs (`useUsers()` de `api/pilotage.ts`, filtre + `status === 'active' && role.name.startsWith('Technicien')` — identique au web), avant de + confirmer l'approbation (`assigneeIds` transmis à `POST /requests/{id}/approve`, déjà supporté + côté API et par le contrat — rien à changer côté serveur, seule l'UI mobile manquait cette + étape). +- Vérifié de bout en bout : demande créée par Karim → approuvée par Nadia (Gestionnaire) avec + Ahmed (Technicien) assigné → OT confirmé avec `assignees: [Ahmed Benali]`. Typecheck/tests/lint + verts sur les 4 paquets. + +**Décisions** + +- Le périmètre plus large — le Gestionnaire garde par la matrice R2/R3 `WORK_ORDERS` en accès + complet (`view/viewOther/create/edit/delete`), ce qui lui permet TECHNIQUEMENT de démarrer/ + clôturer n'importe quel OT comme le ferait un technicien — n'est pas touché ici : c'est un + choix de matrice établi depuis R2/R3, identique sur le web, pas une régression du mobile R6. + Signalé au référent comme question ouverte séparée si un resserrement est souhaité. + +**Prochaine étape** : reste de la recette R6 (Sites/Ascenseurs/Fichiers, puis passage Demandeur +avec le nouveau périmètre de site et le scan QR) ; question ouverte sur le périmètre d'édition OT +du Gestionnaire à trancher si besoin. + +--- + ## 2026-08-02 — Pr. Daaif (+ Claude) — R6.6 : Demandeur restreint à son site **Contexte** — en recette, le référent a remarqué que Karim Doukkali (Demandeur) voit tout le