mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r6): mobile ouvert à tous les rôles — maquette + socle R6.1
R4 avait délibérément fermé le mobile au Technicien. Le référent demande l'ouverture à tous les rôles avec les mêmes droits qu'au web — nouvelle release R6 (R4 est close, R4.1/4.2/4.3 déjà pris par le socle mobile technicien d'origine, pas de réouverture ni de collision de numérotation). Maquette (docs/02-design/maquettes/maquette-mobile-tous-roles.html, 7 écrans) validée par le référent : D1 barre d'onglets adaptative par rôle (Technicien/Technicien limité inchangés + onglet Menu ajouté) ; D2 le Menu reprend à l'identique les 4 groupes du web, même matrice de permissions ; D3 groupe sans lien visible masqué en entier ; D4 le mobile porte les actions courantes par famille, pas les flux de gestion les plus denses ; D5 aucune logique de permission propre au mobile. R6.1 — socle : usePermissions() mobile calqué sur le web ; barre d'onglets adaptative (ongletsVisibles par rôle, href:null masque sans retirer du navigateur) ; Accueil (dashboard réel pour Administrateur/Gestionnaire/ Dispatcher/Vue seule, aucun chiffre inventé) ; OT (liste complète, viewOther déjà géré serveur, réutilise la fiche OT R4 telle quelle) ; Menu (groupes filtrés, vide masqué) ; Demandes (PanneauDemandes — un seul composant pour tous les rôles : création/suivi Demandeur, approbation/ rejet Gestionnaire/Dispatcher/Administrateur, lecture Vue seule). Familles pas encore portées : écran « à venir » plutôt qu'un lien mort. Typecheck propre, 17 tests Jest, lint 5/5 paquets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ Projet **réel** (SPELEV, maintenance d'ascenseurs, Maroc) mené par le Pr. Daai
|
|||||||
|
|
||||||
1. **Design-first** : aucune ligne de code applicatif avant validation par le référent de la charte graphique, des tokens et des maquettes HD (`docs/02-design/`). Chaque release subit une revue « pixel » écrans ↔ maquettes.
|
1. **Design-first** : aucune ligne de code applicatif avant validation par le référent de la charte graphique, des tokens et des maquettes HD (`docs/02-design/`). Chaque release subit une revue « pixel » écrans ↔ maquettes.
|
||||||
2. **Playbook vivant** : chaque phase du cycle de vie a son dossier `docs/0X-*/` (template réutilisable + artefacts réels). Le journal quotidien vit dans `docs/journal/` — **jamais** dans le livre.
|
2. **Playbook vivant** : chaque phase du cycle de vie a son dossier `docs/0X-*/` (template réutilisable + artefacts réels). Le journal quotidien vit dans `docs/journal/` — **jamais** dans le livre.
|
||||||
3. **Périmètre fermé par release** : R0 Fondations → R1 Référentiel → R2 Exploitation → R3 Gestion → R4 Mobile → R5 IA. On n'ouvre pas Rn+1 avant recette **et** déploiement de Rn.
|
3. **Périmètre fermé par release** : R0 Fondations → R1 Référentiel → R2 Exploitation → R3 Gestion → R4 Mobile → R5 IA → R6 Mobile tous rôles. On n'ouvre pas Rn+1 avant recette **et** déploiement de Rn.
|
||||||
4. **Déployer tôt** : chaque release part sur le serveur de production (Dokploy) dès sa recette.
|
4. **Déployer tôt** : chaque release part sur le serveur de production (Dokploy) dès sa recette.
|
||||||
5. **Le développeur est le premier utilisateur** : `DEMO_MODE=true` active un **sélecteur de compte démo** (connexion 1 clic sur les comptes seedés, endpoint `POST /auth/demo-login` strictement absent si l'env ne l'active pas). Critère : changer de rôle en < 3 s sans mot de passe.
|
5. **Le développeur est le premier utilisateur** : `DEMO_MODE=true` active un **sélecteur de compte démo** (connexion 1 clic sur les comptes seedés, endpoint `POST /auth/demo-login` strictement absent si l'env ne l'active pas). Critère : changer de rôle en < 3 s sans mot de passe.
|
||||||
|
|
||||||
@@ -69,5 +69,8 @@ pnpm + Turborepo. `apps/api` : NestJS, Prisma, PostgreSQL (pgvector + PostGIS),
|
|||||||
- ✅ **Recette terrain mobile sur iPhone 15 Pro — 7/7 validés (19/07, ADR-005)** : Expo Go bloqué par le retard d'approbation Apple (SDK 54 vs notre SDK 57) → **décision : builds natifs locaux (Xcode/Android Studio, signature gratuite) pour la vraie recette, Expo Go conservé pour l'aperçu sans installation**. 5 obstacles techniques réels corrigés et documentés (UDID plutôt que nom, ne jamais contourner `expo run:*` par un xcodebuild manuel, modules Expo/RN précompilés SDK 56/57 incompatibles avec la liaison statique du projet, `fetch` global incompatible avec l'upload multipart natif — fix permanent `EXPO_PUBLIC_USE_RN_FETCH=1` dans `apps/mobile/.env`, découverte réseau du dev client peu fiable). Recette iOS : connexion, scan caméra réel (résolution + rejet QR étranger), vrai mode avion, conflit D2 tranché par l'humain avec photo réellement téléversée, persistance à travers fermeture/reconstruction de l'app, suggestions R5 au vrai modèle, purge de sécurité au changement de compte — tout validé. Android : build natif sur émulateur, passage santé complet.
|
- ✅ **Recette terrain mobile sur iPhone 15 Pro — 7/7 validés (19/07, ADR-005)** : Expo Go bloqué par le retard d'approbation Apple (SDK 54 vs notre SDK 57) → **décision : builds natifs locaux (Xcode/Android Studio, signature gratuite) pour la vraie recette, Expo Go conservé pour l'aperçu sans installation**. 5 obstacles techniques réels corrigés et documentés (UDID plutôt que nom, ne jamais contourner `expo run:*` par un xcodebuild manuel, modules Expo/RN précompilés SDK 56/57 incompatibles avec la liaison statique du projet, `fetch` global incompatible avec l'upload multipart natif — fix permanent `EXPO_PUBLIC_USE_RN_FETCH=1` dans `apps/mobile/.env`, découverte réseau du dev client peu fiable). Recette iOS : connexion, scan caméra réel (résolution + rejet QR étranger), vrai mode avion, conflit D2 tranché par l'humain avec photo réellement téléversée, persistance à travers fermeture/reconstruction de l'app, suggestions R5 au vrai modèle, purge de sécurité au changement de compte — tout validé. Android : build natif sur émulateur, passage santé complet.
|
||||||
- ✅ **Dictée implémentée (22/07, ADR-004 §5)** : écran Voix R5 (jamais construit) — `faster-whisper` local (CTranslate2, CPU, opt-in `AI_TRANSCRIPTION=off|locale`, défaut off), audio jamais persisté (purge serveur ET mobile quoi qu'il arrive), transcription → relecture humaine → suggestion immédiate et/ou sauvegarde dans `InterventionReport.note` (champ existant depuis R2, jamais eu d'écran jusqu'ici) → `note` rejoint le corpus à la clôture comme les bilans codés. Mobile : `expo-audio`/`expo-file-system`, bouton dicter sur l'écran de clôture. Docker `siop2-ai` embarque le modèle (1,54→2,19 Go). Vérifié réellement (voix de synthèse → texte fidèle, bout en bout API, conteneur Docker construit, chaîne corpus complète note→clôture→réindexation→recherche). 80 tests API, 29 pytest ai, mobile vert.
|
- ✅ **Dictée implémentée (22/07, ADR-004 §5)** : écran Voix R5 (jamais construit) — `faster-whisper` local (CTranslate2, CPU, opt-in `AI_TRANSCRIPTION=off|locale`, défaut off), audio jamais persisté (purge serveur ET mobile quoi qu'il arrive), transcription → relecture humaine → suggestion immédiate et/ou sauvegarde dans `InterventionReport.note` (champ existant depuis R2, jamais eu d'écran jusqu'ici) → `note` rejoint le corpus à la clôture comme les bilans codés. Mobile : `expo-audio`/`expo-file-system`, bouton dicter sur l'écran de clôture. Docker `siop2-ai` embarque le modèle (1,54→2,19 Go). Vérifié réellement (voix de synthèse → texte fidèle, bout en bout API, conteneur Docker construit, chaîne corpus complète note→clôture→réindexation→recherche). 80 tests API, 29 pytest ai, mobile vert.
|
||||||
- ✅ **Dictée validée sur iPhone physique (01/08, ADR-004 §5)** : le blocage USB du 22/07 était un faux négatif de `system_profiler` (outil de diagnostic en panne, pas le câble). Deux bugs réels trouvés et corrigés : `expo-audio` iOS exige `setAudioModeAsync({ allowsRecording: true })` avant `record()` (absent, jamais détecté hors appareil réel) ; le correctif des modules précompilés (ADR-005) n'avait jamais été rendu permanent dans `apps/mobile/.env` — corrigé. Chaîne complète vérifiée en vrai : micro → dictée → transcription fidèle → note → clôture → réindexation → **retrouvé par la recherche sémantique**.
|
- ✅ **Dictée validée sur iPhone physique (01/08, ADR-004 §5)** : le blocage USB du 22/07 était un faux négatif de `system_profiler` (outil de diagnostic en panne, pas le câble). Deux bugs réels trouvés et corrigés : `expo-audio` iOS exige `setAudioModeAsync({ allowsRecording: true })` avant `record()` (absent, jamais détecté hors appareil réel) ; le correctif des modules précompilés (ADR-005) n'avait jamais été rendu permanent dans `apps/mobile/.env` — corrigé. Chaîne complète vérifiée en vrai : micro → dictée → transcription fidèle → note → clôture → réindexation → **retrouvé par la recherche sémantique**.
|
||||||
- 🔄 **Reprise ici** : redéploiement Dokploy de l'instance ENSET (`AI_SERVICE_TOKEN` à créer — runbook §2 — puis « Réindexer tout »). Restes : 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).
|
- fix(mobile) **déconnexion accessible depuis tous les onglets (02/08)** : bug remonté par le référent (une fois authentifié, aucun moyen de revenir en arrière ou de se déconnecter — seule « Ma journée » portait ce contrôle, via un `onLongPress` non découvrable). `EnteteTabs` (`composants/ui.tsx`) factorise une entête commune aux 4 onglets terrain, tap simple + confirmation, rôle affiché dynamique.
|
||||||
- Détail quotidien : `docs/journal/journal.md`. Dépôt : `siop-spelev/siop2` (privé), jalons R0→R5.
|
- ✅ **R6 — maquette validée (02/08)** : `maquette-mobile-tous-roles.html`, 7 écrans (synthèse par rôle, Accueil adaptatif, Menu groupé, OT côté Dispatcher, Demandes, Stock, Personnes & statistiques) + 5 décisions actées — D1 barre d'onglets adaptative (Technicien/Technicien limité inchangés + onglet Menu) ; D2 le Menu reprend à l'identique les 4 groupes du web (même matrice, aucune règle nouvelle) ; D3 groupe sans lien visible masqué en entier ; D4 le mobile porte les actions courantes par famille, pas les flux de gestion les plus denses (réservés au web) ; D5 aucune logique de permission propre au mobile. Nouvelle release (R4 est close et R4.1/4.2/4.3 déjà pris par le socle mobile technicien d'origine — pas de réouverture, pas de collision de numérotation).
|
||||||
|
- ✅ **R6.1 — socle** : `usePermissions()` mobile (calqué sur le web, lit `me.permissions`) ; barre d'onglets adaptative par rôle (`ongletsVisibles`, `href: null` masque sans retirer du navigateur) ; écrans Accueil (dashboard réel pour Administrateur/Gestionnaire/Dispatcher/Vue seule, aucun chiffre inventé), OT (liste complète `viewOther`, réutilise la fiche OT R4 telle quelle), Menu (groupes filtrés par la matrice, groupe vide masqué), Demandes (`PanneauDemandes` — un seul composant pour tous les rôles : création/suivi pour le Demandeur, approbation/rejet à motif pour Gestionnaire/Dispatcher/Administrateur, lecture seule pour Vue seule ; `GET /assets/options`, déjà ouvert à tout rôle authentifié depuis R2.3, réutilisé pour le sélecteur d'équipement). Familles pas encore portées (Sites, Ascenseurs, Stock, Tiers, Fichiers, Statistiques, Assistant, Personnes) : écran « à venir » honnête plutôt qu'un lien mort. Typecheck propre, 17 tests Jest verts, lint 5/5 paquets — pas de vérification visuelle en navigateur de mon côté (aucun outil de ce type dans cet environnement), à confirmer par le référent sur l'iPhone déjà connecté au serveur Metro.
|
||||||
|
- 🔄 **Reprise ici** : confirmation du référent sur iPhone (nav adaptative R6.1 + correctif déconnexion) ; puis R6.2 (Parc — Sites, Ascenseurs), R6.3 (Ressources — Stock, Tiers, Fichiers), R6.4 (Pilotage — Statistiques, Personnes, Assistant), sur le modèle des sous-releases R4.1→R4.3. 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.
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { Tabs } from 'expo-router';
|
import { Tabs } from 'expo-router';
|
||||||
import { Text, type ColorValue } from 'react-native';
|
import { Text, type ColorValue } from 'react-native';
|
||||||
import { useAssets, useReferenceValues } from '@/api/exploitation';
|
import { useAssets, useReferenceValues } from '@/api/exploitation';
|
||||||
|
import { useMe } from '@/auth/session';
|
||||||
|
import { ongletsVisibles } from '@/auth/roles';
|
||||||
|
import { usePermissions } from '@/auth/use-permissions';
|
||||||
import { useFile } from '@/file/store';
|
import { useFile } from '@/file/store';
|
||||||
import { useTokens } from '@/theme/tokens';
|
import { useTokens } from '@/theme/tokens';
|
||||||
|
|
||||||
/** La tabbar de la maquette R4. Les onglets à venir restent visibles
|
/** Barre d'onglets ADAPTATIVE (maquette « mobile ouvert à tous les rôles »,
|
||||||
* (périmètre annoncé, même patron que la sidebar web) mais mènent à un
|
* D1) : le Technicien/Technicien limité gardent exactement leurs 4 onglets
|
||||||
* écran « disponible en R4.x ». */
|
* terrain R4, inchangés. Les autres rôles reçoivent Accueil/OT/Menu (ou une
|
||||||
|
* variante plus étroite — Demandeur, Vue seule — selon leurs droits,
|
||||||
|
* `ongletsVisibles`). Tous les écrans sont toujours déclarés : `href: null`
|
||||||
|
* masque un onglet de la barre sans le retirer du navigateur (le Menu peut
|
||||||
|
* toujours y pousser directement). */
|
||||||
|
|
||||||
function Pic({ glyphe, couleur }: { glyphe: string; couleur: ColorValue }) {
|
function Pic({ glyphe, couleur }: { glyphe: string; couleur: ColorValue }) {
|
||||||
return <Text style={{ fontSize: 17, color: couleur, lineHeight: 20 }}>{glyphe}</Text>;
|
return <Text style={{ fontSize: 17, color: couleur, lineHeight: 20 }}>{glyphe}</Text>;
|
||||||
@@ -16,10 +23,19 @@ export default function CoquilleTabs() {
|
|||||||
const t = useTokens();
|
const t = useTokens();
|
||||||
const file = useFile();
|
const file = useFile();
|
||||||
const conflits = file.some((s) => s.statut === 'CONFLIT');
|
const conflits = file.some((s) => s.statut === 'CONFLIT');
|
||||||
|
const { data: me } = useMe();
|
||||||
|
const { can } = usePermissions();
|
||||||
|
const visibles = ongletsVisibles(me?.role.name);
|
||||||
// D1 « lecture locale » : le parc (scan D4) et les référentiels du bilan
|
// D1 « lecture locale » : le parc (scan D4) et les référentiels du bilan
|
||||||
// se préchargent dès l'entrée — le sous-sol n'attend pas qu'on y pense.
|
// se préchargent dès l'entrée — le sous-sol n'attend pas qu'on y pense.
|
||||||
useAssets();
|
// Le Demandeur n'a pas ASSETS.view (seulement /assets/options) : inutile
|
||||||
useReferenceValues();
|
// d'appeler /assets pour lui, l'API répondrait 403.
|
||||||
|
const peutAssets = can('ASSETS', 'view');
|
||||||
|
useAssets({ enabled: peutAssets });
|
||||||
|
useReferenceValues({ enabled: peutAssets });
|
||||||
|
|
||||||
|
const cacher = (nom: string) => (visibles.has(nom) ? undefined : null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
@@ -34,6 +50,7 @@ export default function CoquilleTabs() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="journee"
|
name="journee"
|
||||||
options={{
|
options={{
|
||||||
|
href: cacher('journee'),
|
||||||
title: 'Ma journée',
|
title: 'Ma journée',
|
||||||
tabBarIcon: ({ color }) => <Pic glyphe="☰" couleur={color} />,
|
tabBarIcon: ({ color }) => <Pic glyphe="☰" couleur={color} />,
|
||||||
}}
|
}}
|
||||||
@@ -41,6 +58,7 @@ export default function CoquilleTabs() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="scanner"
|
name="scanner"
|
||||||
options={{
|
options={{
|
||||||
|
href: cacher('scanner'),
|
||||||
title: 'Scanner',
|
title: 'Scanner',
|
||||||
tabBarIcon: ({ color }) => <Pic glyphe="▣" couleur={color} />,
|
tabBarIcon: ({ color }) => <Pic glyphe="▣" couleur={color} />,
|
||||||
}}
|
}}
|
||||||
@@ -48,6 +66,7 @@ export default function CoquilleTabs() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="preventif"
|
name="preventif"
|
||||||
options={{
|
options={{
|
||||||
|
href: cacher('preventif'),
|
||||||
title: 'Préventif',
|
title: 'Préventif',
|
||||||
tabBarIcon: ({ color }) => <Pic glyphe="✓" couleur={color} />,
|
tabBarIcon: ({ color }) => <Pic glyphe="✓" couleur={color} />,
|
||||||
}}
|
}}
|
||||||
@@ -55,6 +74,7 @@ export default function CoquilleTabs() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="synchro"
|
name="synchro"
|
||||||
options={{
|
options={{
|
||||||
|
href: cacher('synchro'),
|
||||||
title: 'Synchro',
|
title: 'Synchro',
|
||||||
tabBarIcon: ({ color }) => <Pic glyphe="⇅" couleur={color} />,
|
tabBarIcon: ({ color }) => <Pic glyphe="⇅" couleur={color} />,
|
||||||
tabBarBadge: file.length || undefined,
|
tabBarBadge: file.length || undefined,
|
||||||
@@ -66,6 +86,38 @@ export default function CoquilleTabs() {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="accueil"
|
||||||
|
options={{
|
||||||
|
href: cacher('accueil'),
|
||||||
|
title: 'Accueil',
|
||||||
|
tabBarIcon: ({ color }) => <Pic glyphe="⌂" couleur={color} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="ot"
|
||||||
|
options={{
|
||||||
|
href: cacher('ot'),
|
||||||
|
title: 'OT',
|
||||||
|
tabBarIcon: ({ color }) => <Pic glyphe="☰" couleur={color} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="nouvelle-demande"
|
||||||
|
options={{
|
||||||
|
href: cacher('nouvelle-demande'),
|
||||||
|
title: 'Nouvelle demande',
|
||||||
|
tabBarIcon: ({ color }) => <Pic glyphe="+" couleur={color} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="menu"
|
||||||
|
options={{
|
||||||
|
href: cacher('menu'),
|
||||||
|
title: 'Menu',
|
||||||
|
tabBarIcon: ({ color }) => <Pic glyphe="☷" couleur={color} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
179
apps/mobile/app/(tabs)/accueil.tsx
Normal file
179
apps/mobile/app/(tabs)/accueil.tsx
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { router } from 'expo-router';
|
||||||
|
import { Pressable, Text, View } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { useRequests, useWorkOrders } from '@/api/exploitation';
|
||||||
|
import { useMe } from '@/auth/session';
|
||||||
|
import { usePermissions } from '@/auth/use-permissions';
|
||||||
|
import { EnteteTabs } from '@/composants/ui';
|
||||||
|
import { PanneauDemandes } from '@/composants/panneau-demandes';
|
||||||
|
import { useTokens } from '@/theme/tokens';
|
||||||
|
|
||||||
|
/** Accueil — maquette « mobile ouvert à tous les rôles », écran 2 : le
|
||||||
|
* Demandeur y voit directement ses demandes (son seul geste) ; les rôles
|
||||||
|
* de gestion (Administrateur/Gestionnaire/Dispatcher/Vue seule) y trouvent
|
||||||
|
* leurs signaux réels (urgence, OT en cours, demandes à traiter) — aucun
|
||||||
|
* chiffre inventé : seuls WORK_ORDERS/REQUESTS sont déjà câblés sur mobile,
|
||||||
|
* le reste (stock, préventif du mois…) attend son propre écran (Menu). */
|
||||||
|
export default function PageAccueil() {
|
||||||
|
const { data: me } = useMe();
|
||||||
|
if (me?.role.name === 'Demandeur') {
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
|
||||||
|
<View style={{ flex: 1, padding: 14, gap: 10 }}>
|
||||||
|
<EnteteTabs />
|
||||||
|
<PanneauDemandes />
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <TableauDeBordGestion />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableauDeBordGestion() {
|
||||||
|
const t = useTokens();
|
||||||
|
const { can } = usePermissions();
|
||||||
|
const peutOT = can('WORK_ORDERS', 'view');
|
||||||
|
const peutDemandes = can('REQUESTS', 'view');
|
||||||
|
const { data: workOrders } = useWorkOrders();
|
||||||
|
const { data: requests } = useRequests();
|
||||||
|
|
||||||
|
const ots = workOrders ?? [];
|
||||||
|
const urgences = ots.filter((o) => o.priority === 'PERSON_TRAPPED' && o.status !== 'DONE' && o.status !== 'CANCELLED');
|
||||||
|
const enCours = ots.filter((o) => o.status === 'OPEN' || o.status === 'IN_PROGRESS').length;
|
||||||
|
const aTraiter = (requests ?? []).filter((r) => r.status === 'RECEIVED').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||||||
|
<View style={{ padding: 14, gap: 10, flex: 1 }}>
|
||||||
|
<EnteteTabs />
|
||||||
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
||||||
|
Accueil
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{urgences.length ? (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
onPress={() => router.push(`/ot/${urgences[0]!.id}`)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: t.prioBloqueFond,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 9,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: t.prioBloque }} />
|
||||||
|
<Text style={{ color: t.prioBloque, fontFamily: 'Manrope_800ExtraBold', fontSize: 12 }}>
|
||||||
|
{urgences.length === 1
|
||||||
|
? `1 personne bloquée — ${urgences[0]!.assetReference} · ${urgences[0]!.siteName}`
|
||||||
|
: `${urgences.length} personnes bloquées`}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{peutOT || peutDemandes ? (
|
||||||
|
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||||||
|
{peutOT ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: t.surface,
|
||||||
|
borderColor: t.bordure,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.primaire }}>
|
||||||
|
{enCours}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_600SemiBold', fontSize: 10.5, color: t.encre2 }}>
|
||||||
|
OT en cours
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
{peutDemandes ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: t.surface,
|
||||||
|
borderColor: t.bordure,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
||||||
|
{aTraiter}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_600SemiBold', fontSize: 10.5, color: t.encre2 }}>
|
||||||
|
Demandes reçues
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{peutDemandes && aTraiter > 0 ? (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
onPress={() => router.push('/demandes')}
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
backgroundColor: t.surface,
|
||||||
|
borderColor: t.bordure,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 11,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: t.primaireDoux,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 14, color: t.primaire }}>✎</Text>
|
||||||
|
</View>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_700Bold', fontSize: 12.5, color: t.encre }}>
|
||||||
|
Demandes en attente
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_400Regular', fontSize: 11, color: t.encre2 }}>
|
||||||
|
à approuver ou rejeter
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: 'Manrope_800ExtraBold',
|
||||||
|
fontSize: 10.5,
|
||||||
|
color: t.stAttente,
|
||||||
|
backgroundColor: t.stAttenteFond,
|
||||||
|
paddingHorizontal: 7,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 999,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{aTraiter}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: t.encre3, fontSize: 13 }}>›</Text>
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!peutOT && !peutDemandes ? (
|
||||||
|
<Text style={{ color: t.encre3, fontFamily: 'Manrope_600SemiBold', textAlign: 'center', padding: 24 }}>
|
||||||
|
Rien à afficher ici pour votre rôle — voir le Menu.
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
139
apps/mobile/app/(tabs)/menu.tsx
Normal file
139
apps/mobile/app/(tabs)/menu.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { router } from 'expo-router';
|
||||||
|
import { Pressable, Text, View } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import type { ObjectCategory, PermissionRight } from '@siop/shared';
|
||||||
|
import { useMe } from '@/auth/session';
|
||||||
|
import { estRoleTerrain } from '@/auth/roles';
|
||||||
|
import { usePermissions } from '@/auth/use-permissions';
|
||||||
|
import { EnteteTabs } from '@/composants/ui';
|
||||||
|
import { useTokens, type Tokens } from '@/theme/tokens';
|
||||||
|
|
||||||
|
/** Menu — maquette « mobile ouvert à tous les rôles », écran 3 : reprend à
|
||||||
|
* l'identique les 4 groupes et l'ordre de la sidebar web (Exploitation /
|
||||||
|
* Parc / Ressources / Pilotage, coquille.tsx), filtrés par LA MÊME matrice
|
||||||
|
* (D2 — aucune règle de droit nouvelle). Un groupe sans aucun lien visible
|
||||||
|
* est masqué en entier (D3) : l'écran est petit, un intitulé vide n'aide
|
||||||
|
* personne. Ce que « Ma journée »/« Préventif » couvrent déjà pour le
|
||||||
|
* Technicien n'est pas dupliqué ici. */
|
||||||
|
|
||||||
|
interface LienMenu {
|
||||||
|
libelle: string;
|
||||||
|
permission: [ObjectCategory, PermissionRight];
|
||||||
|
ico: string;
|
||||||
|
cacheEnTerrain?: boolean; // déjà couvert par un onglet terrain dédié
|
||||||
|
route?: string;
|
||||||
|
aVenir?: string; // detail affiché sur l'écran « à venir »
|
||||||
|
}
|
||||||
|
|
||||||
|
const GROUPES: { titre: string; liens: LienMenu[] }[] = [
|
||||||
|
{
|
||||||
|
titre: 'Exploitation',
|
||||||
|
liens: [
|
||||||
|
{ libelle: 'Ordres de travail', ico: '☰', permission: ['WORK_ORDERS', 'view'], cacheEnTerrain: true, route: '/(tabs)/ot' },
|
||||||
|
{ libelle: 'Demandes', ico: '✎', permission: ['REQUESTS', 'view'], route: '/demandes' },
|
||||||
|
{ libelle: 'Préventif', ico: '✓', permission: ['WORK_ORDERS', 'view'], cacheEnTerrain: true, aVenir: 'Les grilles préventives pour les rôles de gestion arrivent dans une prochaine étape — le Technicien les a déjà dans son onglet dédié.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titre: 'Parc',
|
||||||
|
liens: [
|
||||||
|
{ libelle: 'Ascenseurs', ico: '▣', permission: ['ASSETS', 'view'], aVenir: 'La liste du parc en mobilité arrive dans une prochaine étape — en attendant, le Scanner et les fiches OT y mènent déjà.' },
|
||||||
|
{ libelle: 'Sites', ico: '◫', permission: ['LOCATIONS', 'view'], aVenir: 'La liste des sites en mobilité arrive dans une prochaine étape.' },
|
||||||
|
{ libelle: 'Catégories', ico: '▤', permission: ['SETTINGS', 'view'], aVenir: 'L’administration des catégories reste sur le grand écran pour l’instant.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titre: 'Ressources',
|
||||||
|
liens: [
|
||||||
|
{ libelle: 'Stock & achats', ico: '◔', permission: ['PARTS', 'view'], aVenir: 'Le stock et les bons de commande en mobilité arrivent dans une prochaine étape.' },
|
||||||
|
{ libelle: 'Tiers', ico: '⇄', permission: ['PURCHASE_ORDERS', 'view'], aVenir: 'Fournisseurs et clients restent sur le grand écran pour l’instant.' },
|
||||||
|
{ libelle: 'Fichiers', ico: '▧', permission: ['ASSETS', 'view'], aVenir: 'La bibliothèque documentaire reste sur le grand écran pour l’instant.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titre: 'Pilotage',
|
||||||
|
liens: [
|
||||||
|
{ libelle: 'Statistiques', ico: '◈', permission: ['ANALYTICS', 'view'], aVenir: 'Les statistiques en mobilité arrivent dans une prochaine étape.' },
|
||||||
|
{ libelle: 'Assistant', ico: '✦', permission: ['WORK_ORDERS', 'view'], aVenir: 'L’assistant IA reste sur le grand écran et la clôture mobile pour l’instant.' },
|
||||||
|
{ libelle: 'Personnes & équipes', ico: '◎', permission: ['PEOPLE_TEAMS', 'view'], aVenir: 'La gestion des personnes et équipes en mobilité arrive dans une prochaine étape.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PageMenu() {
|
||||||
|
const t = useTokens();
|
||||||
|
const { data: me } = useMe();
|
||||||
|
const { can } = usePermissions();
|
||||||
|
const terrain = estRoleTerrain(me?.role.name);
|
||||||
|
|
||||||
|
const groupesVisibles = GROUPES.map((g) => ({
|
||||||
|
...g,
|
||||||
|
liens: g.liens.filter(
|
||||||
|
({ permission, cacheEnTerrain }) => can(...permission) && !(cacheEnTerrain && terrain),
|
||||||
|
),
|
||||||
|
})).filter((g) => g.liens.length > 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||||||
|
<View style={{ padding: 14, gap: 14, flex: 1 }}>
|
||||||
|
<EnteteTabs />
|
||||||
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>Menu</Text>
|
||||||
|
{groupesVisibles.length === 0 ? (
|
||||||
|
<Text style={{ color: t.encre3, fontFamily: 'Manrope_600SemiBold', textAlign: 'center', padding: 24 }}>
|
||||||
|
Rien d'autre accessible à votre rôle ici.
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
{groupesVisibles.map((g) => (
|
||||||
|
<View key={g.titre} style={{ gap: 6 }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: 'Manrope_800ExtraBold',
|
||||||
|
fontSize: 10.5,
|
||||||
|
letterSpacing: 0.8,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: t.encre3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{g.titre}
|
||||||
|
</Text>
|
||||||
|
{g.liens.map((lien) => (
|
||||||
|
<LigneMenu key={lien.libelle} lien={lien} t={t} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LigneMenu({ lien, t }: { lien: LienMenu; t: Tokens }) {
|
||||||
|
const aller = () => {
|
||||||
|
if (lien.route) {
|
||||||
|
router.push(lien.route as never);
|
||||||
|
} else {
|
||||||
|
router.push({ pathname: '/a-venir', params: { titre: lien.libelle, detail: lien.aVenir ?? '' } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
onPress={aller}
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
backgroundColor: t.surface,
|
||||||
|
borderColor: t.bordure,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 11,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ width: 22, textAlign: 'center', fontSize: 14, color: t.primaire }}>{lien.ico}</Text>
|
||||||
|
<Text style={{ flex: 1, fontFamily: 'Manrope_700Bold', fontSize: 12.5, color: t.encre }}>
|
||||||
|
{lien.libelle}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: t.encre3, fontSize: 13 }}>›</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
apps/mobile/app/(tabs)/nouvelle-demande.tsx
Normal file
24
apps/mobile/app/(tabs)/nouvelle-demande.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { router } from 'expo-router';
|
||||||
|
import { ScrollView, Text } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { FormulaireDemande } from '@/composants/formulaire-demande';
|
||||||
|
import { EnteteTabs } from '@/composants/ui';
|
||||||
|
import { useTokens } from '@/theme/tokens';
|
||||||
|
|
||||||
|
/** Onglet du Demandeur (maquette « mobile ouvert à tous les rôles »,
|
||||||
|
* écran 5) : le geste qu'il fait le plus mérite son propre onglet plutôt
|
||||||
|
* qu'un bouton caché dans l'Accueil. */
|
||||||
|
export default function PageNouvelleDemande() {
|
||||||
|
const t = useTokens();
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||||||
|
<ScrollView contentContainerStyle={{ padding: 14, gap: 10 }}>
|
||||||
|
<EnteteTabs />
|
||||||
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
||||||
|
Nouvelle demande
|
||||||
|
</Text>
|
||||||
|
<FormulaireDemande surSucces={() => router.replace('/(tabs)/accueil')} />
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
154
apps/mobile/app/(tabs)/ot.tsx
Normal file
154
apps/mobile/app/(tabs)/ot.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import { router } from 'expo-router';
|
||||||
|
import { FlatList, Pressable, RefreshControl, Text, View } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import {
|
||||||
|
WORK_ORDER_PRIORITY_LABELS,
|
||||||
|
WORK_ORDER_STATUS_LABELS,
|
||||||
|
WORK_ORDER_TYPE_LABELS,
|
||||||
|
type WorkOrderPriority,
|
||||||
|
type WorkOrderStatus,
|
||||||
|
} from '@siop/shared';
|
||||||
|
import { useWorkOrders } from '@/api/exploitation';
|
||||||
|
import { EnteteTabs } from '@/composants/ui';
|
||||||
|
import { triJournee } from '@/lib/journee';
|
||||||
|
import { useTokens, type Tokens } from '@/theme/tokens';
|
||||||
|
|
||||||
|
/** Ordres de travail — maquette « mobile ouvert à tous les rôles », écran 4 :
|
||||||
|
* à la différence de « Ma journée » (Technicien, ses OT uniquement),
|
||||||
|
* Administrateur/Gestionnaire/Dispatcher ont `viewOther` — l'API renvoie
|
||||||
|
* déjà TOUS les OT (ADR-003, rien à filtrer ici). Même tri priorité puis
|
||||||
|
* échéance que « Ma journée » ; ouvrir un OT réutilise la fiche R4 telle
|
||||||
|
* quelle (elle affiche déjà les actions permises via `allowedTransitions`). */
|
||||||
|
|
||||||
|
const STYLE_STATUT: Record<WorkOrderStatus, (t: Tokens) => [string, string]> = {
|
||||||
|
OPEN: (t) => [t.stOuvert, t.stOuvertFond],
|
||||||
|
IN_PROGRESS: (t) => [t.stEncours, t.stEncoursFond],
|
||||||
|
ON_HOLD: (t) => [t.stAttente, t.stAttenteFond],
|
||||||
|
DONE: (t) => [t.stTermine, t.stTermineFond],
|
||||||
|
CANCELLED: (t) => [t.stAnnule, t.stAnnuleFond],
|
||||||
|
};
|
||||||
|
const STRIE_PRIORITE: Record<WorkOrderPriority, (t: Tokens) => string> = {
|
||||||
|
PERSON_TRAPPED: (t) => t.prioBloque,
|
||||||
|
HIGH: (t) => t.prioHaute,
|
||||||
|
MEDIUM: (t) => t.prioMoyenne,
|
||||||
|
LOW: (t) => t.prioBasse,
|
||||||
|
NONE: () => 'transparent',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PageOT() {
|
||||||
|
const t = useTokens();
|
||||||
|
const { data: workOrders, refetch, isFetching } = useWorkOrders();
|
||||||
|
const ots = triJournee(workOrders ?? []);
|
||||||
|
const urgences = ots.filter((o) => o.priority === 'PERSON_TRAPPED');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||||||
|
<View style={{ padding: 14, gap: 10, flex: 1 }}>
|
||||||
|
<EnteteTabs />
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'baseline' }}>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
||||||
|
Ordres de travail
|
||||||
|
</Text>
|
||||||
|
<Text style={{ marginLeft: 'auto', fontFamily: 'Manrope_600SemiBold', fontSize: 11, color: t.encre3 }}>
|
||||||
|
{ots.length}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{urgences.length ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: t.prioBloqueFond,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 9,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: t.prioBloque }} />
|
||||||
|
<Text style={{ color: t.prioBloque, fontFamily: 'Manrope_800ExtraBold', fontSize: 12 }}>
|
||||||
|
{urgences.length === 1
|
||||||
|
? `1 personne bloquée — ${urgences[0]!.assetReference} · ${urgences[0]!.siteName}`
|
||||||
|
: `${urgences.length} personnes bloquées`}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<FlatList
|
||||||
|
data={ots}
|
||||||
|
keyExtractor={(o) => o.id}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl refreshing={isFetching} onRefresh={() => void refetch()} tintColor={t.primaire} />
|
||||||
|
}
|
||||||
|
contentContainerStyle={{ gap: 8, paddingBottom: 12 }}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<Text style={{ color: t.encre3, fontFamily: 'Manrope_600SemiBold', padding: 16, textAlign: 'center' }}>
|
||||||
|
Aucun OT — tirez pour rafraîchir.
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
renderItem={({ item: o }) => {
|
||||||
|
const [enc, fond] = STYLE_STATUT[o.status](t);
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={`${o.reference} — ${o.title}`}
|
||||||
|
onPress={() => router.push(`/ot/${o.id}`)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: t.surface,
|
||||||
|
borderColor: t.bordure,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 12,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderLeftColor: STRIE_PRIORITE[o.priority](t),
|
||||||
|
padding: 11,
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: 'Manrope_700Bold',
|
||||||
|
fontSize: 10.5,
|
||||||
|
letterSpacing: 0.6,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: t.encre2,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{o.reference} · {WORK_ORDER_TYPE_LABELS[o.type]}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: 'Manrope_700Bold',
|
||||||
|
fontSize: 10.5,
|
||||||
|
color: enc,
|
||||||
|
backgroundColor: fond,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 999,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{WORK_ORDER_STATUS_LABELS[o.status]}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_700Bold', fontSize: 14, color: t.encre }}>
|
||||||
|
{o.title}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_400Regular', fontSize: 12, color: t.encre2 }}>
|
||||||
|
Asc. {o.assetReference} — {o.siteName}
|
||||||
|
{o.dueDate
|
||||||
|
? ` · échéance ${new Intl.DateTimeFormat('fr-FR', { day: 'numeric', month: 'short' }).format(new Date(o.dueDate))}`
|
||||||
|
: ''}
|
||||||
|
{o.priority !== 'NONE' && o.priority !== 'PERSON_TRAPPED'
|
||||||
|
? ` · ${WORK_ORDER_PRIORITY_LABELS[o.priority]}`
|
||||||
|
: ''}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
apps/mobile/app/a-venir.tsx
Normal file
21
apps/mobile/app/a-venir.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useLocalSearchParams } from 'expo-router';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { AVenir } from '@/composants/a-venir';
|
||||||
|
import { EnteteFiche } from '@/composants/ui';
|
||||||
|
import { useTokens } from '@/theme/tokens';
|
||||||
|
|
||||||
|
/** Destination générique du Menu pour les familles pas encore portées sur
|
||||||
|
* mobile (Sites, Stock, Tiers, Personnes, Statistiques, Assistant…) — même
|
||||||
|
* patron que la sidebar web (release annoncée, jamais un cul-de-sac
|
||||||
|
* silencieux). `AVenir` existait déjà (a-venir.tsx) mais n'était encore
|
||||||
|
* câblé nulle part. */
|
||||||
|
export default function PageAVenir() {
|
||||||
|
const t = useTokens();
|
||||||
|
const { titre, detail } = useLocalSearchParams<{ titre: string; detail: string }>();
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||||||
|
<EnteteFiche titre={titre ?? '—'} />
|
||||||
|
<AVenir titre={titre ?? '—'} release="une prochaine étape" detail={detail ?? ''} />
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
apps/mobile/app/demandes/index.tsx
Normal file
19
apps/mobile/app/demandes/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { View } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { PanneauDemandes } from '@/composants/panneau-demandes';
|
||||||
|
import { EnteteFiche } from '@/composants/ui';
|
||||||
|
import { useTokens } from '@/theme/tokens';
|
||||||
|
|
||||||
|
/** Écran poussé depuis le Menu (Administrateur/Gestionnaire/Dispatcher/Vue
|
||||||
|
* seule — pas d'onglet dédié pour eux, contrairement au Demandeur). */
|
||||||
|
export default function PageDemandesPoussee() {
|
||||||
|
const t = useTokens();
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||||||
|
<View style={{ flex: 1, padding: 14, gap: 10 }}>
|
||||||
|
<EnteteFiche titre="Demandes" />
|
||||||
|
<PanneauDemandes />
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
apps/mobile/app/demandes/nouvelle.tsx
Normal file
18
apps/mobile/app/demandes/nouvelle.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { router } from 'expo-router';
|
||||||
|
import { ScrollView } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { FormulaireDemande } from '@/composants/formulaire-demande';
|
||||||
|
import { EnteteFiche } from '@/composants/ui';
|
||||||
|
import { useTokens } from '@/theme/tokens';
|
||||||
|
|
||||||
|
export default function PageNouvelleDemandePoussee() {
|
||||||
|
const t = useTokens();
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={{ flex: 1, backgroundColor: t.fond }} edges={['top']}>
|
||||||
|
<ScrollView contentContainerStyle={{ padding: 14, gap: 10 }}>
|
||||||
|
<EnteteFiche titre="Nouvelle demande" />
|
||||||
|
<FormulaireDemande surSucces={() => (router.canGoBack() ? router.back() : router.replace('/demandes'))} />
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Redirect } from 'expo-router';
|
import { Redirect } from 'expo-router';
|
||||||
import { ActivityIndicator, View } from 'react-native';
|
import { ActivityIndicator, View } from 'react-native';
|
||||||
|
import { ongletAccueil } from '@/auth/roles';
|
||||||
import { useMe } from '@/auth/session';
|
import { useMe } from '@/auth/session';
|
||||||
import { useTokens } from '@/theme/tokens';
|
import { useTokens } from '@/theme/tokens';
|
||||||
|
|
||||||
/** Aiguillage : session valide → Ma journée ; sinon → connexion.
|
/** Aiguillage : session valide → l'onglet d'accueil du rôle (Ma journée
|
||||||
|
* pour le terrain, Accueil pour les autres) ; sinon → connexion.
|
||||||
* (Hors-ligne avec cache persisté, /users/me sort du cache : on entre.) */
|
* (Hors-ligne avec cache persisté, /users/me sort du cache : on entre.) */
|
||||||
export default function Aiguillage() {
|
export default function Aiguillage() {
|
||||||
const t = useTokens();
|
const t = useTokens();
|
||||||
@@ -16,5 +18,9 @@ export default function Aiguillage() {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return me ? <Redirect href="/(tabs)/journee" /> : <Redirect href="/connexion" />;
|
return me ? (
|
||||||
|
<Redirect href={`/(tabs)/${ongletAccueil(me.role.name)}`} />
|
||||||
|
) : (
|
||||||
|
<Redirect href="/connexion" />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import type { ChecklistState, ReportUpsert, WorkOrderStatus } from '@siop/shared';
|
import type {
|
||||||
|
ChecklistState,
|
||||||
|
ReportUpsert,
|
||||||
|
RequestApprove,
|
||||||
|
RequestCreate,
|
||||||
|
RequestReject,
|
||||||
|
WorkOrderStatus,
|
||||||
|
} from '@siop/shared';
|
||||||
import { api, API_URL, unwrap } from './client';
|
import { api, API_URL, unwrap } from './client';
|
||||||
import { lireJeton } from './jeton';
|
import { lireJeton } from './jeton';
|
||||||
|
|
||||||
@@ -21,9 +28,10 @@ export function useWorkOrder(id: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAssets() {
|
export function useAssets(opts?: { enabled?: boolean }) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['assets'],
|
queryKey: ['assets'],
|
||||||
|
enabled: opts?.enabled ?? true,
|
||||||
queryFn: async () => (await unwrap(await api.GET('/assets'))).assets,
|
queryFn: async () => (await unwrap(await api.GET('/assets'))).assets,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -47,14 +55,70 @@ export function useDocumentsOT(workOrderId: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useReferenceValues() {
|
export function useReferenceValues(opts?: { enabled?: boolean }) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['reference-values'],
|
queryKey: ['reference-values'],
|
||||||
|
enabled: opts?.enabled ?? true,
|
||||||
staleTime: 3600_000, // référentiels administrables : stables en journée
|
staleTime: 3600_000, // référentiels administrables : stables en journée
|
||||||
queryFn: async () => (await unwrap(await api.GET('/reference-values'))).referenceValues,
|
queryFn: async () => (await unwrap(await api.GET('/reference-values'))).referenceValues,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ————— Demandes (mobile ouvert à tous les rôles) —————
|
||||||
|
// Même endpoint pour tous : l'API scope déjà « les siennes » (Demandeur) ou
|
||||||
|
// « toutes » (viewOther, ADR-003) — rien à filtrer côté app.
|
||||||
|
|
||||||
|
export function useRequests() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['requests'],
|
||||||
|
queryFn: async () => (await unwrap(await api.GET('/requests'))).requests,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Options minimales (signalement) — accessibles à tout rôle authentifié,
|
||||||
|
* y compris Demandeur qui n'a pas ASSETS.view (même trou corrigé qu'au web). */
|
||||||
|
export function useAssetOptions() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['asset-options'],
|
||||||
|
queryFn: async () => (await unwrap(await api.GET('/assets/options'))).options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function useInvalideRequests() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return () => queryClient.invalidateQueries({ queryKey: ['requests'] });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateRequest() {
|
||||||
|
const invalide = useInvalideRequests();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (body: RequestCreate) => unwrap(await api.POST('/requests', { body })),
|
||||||
|
onSuccess: invalide,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApproveRequest() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id, ...body }: RequestApprove & { id: string }) =>
|
||||||
|
unwrap(await api.POST('/requests/{id}/approve', { params: { path: { id } }, body })),
|
||||||
|
onSuccess: () =>
|
||||||
|
Promise.all([
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['requests'] }),
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['work-orders'] }),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRejectRequest() {
|
||||||
|
const invalide = useInvalideRequests();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id, ...body }: RequestReject & { id: string }) =>
|
||||||
|
unwrap(await api.POST('/requests/{id}/reject', { params: { path: { id } }, body })),
|
||||||
|
onSuccess: invalide,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function useInvalideOT(id: string) {
|
function useInvalideOT(id: string) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return () =>
|
return () =>
|
||||||
|
|||||||
26
apps/mobile/src/auth/roles.ts
Normal file
26
apps/mobile/src/auth/roles.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { RoleName } from '@siop/shared';
|
||||||
|
|
||||||
|
/** Barre d'onglets ADAPTATIVE (maquette « mobile ouvert à tous les rôles »,
|
||||||
|
* D1) : le Technicien/Technicien limité gardent exactement leurs onglets
|
||||||
|
* terrain R4 ; les autres rôles reçoivent Accueil/OT/Menu (ou une variante
|
||||||
|
* plus étroite selon leurs droits). Décidé une fois ici, pas éparpillé. */
|
||||||
|
|
||||||
|
const ROLES_TERRAIN: RoleName[] = ['Technicien', 'Technicien limité'];
|
||||||
|
|
||||||
|
export function estRoleTerrain(role: RoleName | undefined): boolean {
|
||||||
|
return !role || ROLES_TERRAIN.includes(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ongletsVisibles(role: RoleName | undefined): ReadonlySet<string> {
|
||||||
|
if (estRoleTerrain(role)) {
|
||||||
|
return new Set(['journee', 'scanner', 'preventif', 'synchro', 'menu']);
|
||||||
|
}
|
||||||
|
if (role === 'Demandeur') return new Set(['accueil', 'nouvelle-demande']);
|
||||||
|
if (role === 'Vue seule') return new Set(['accueil', 'menu']);
|
||||||
|
return new Set(['accueil', 'ot', 'menu']); // Administrateur, Gestionnaire, Dispatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Onglet sur lequel router.replace() après connexion (voir app/index.tsx). */
|
||||||
|
export function ongletAccueil(role: RoleName | undefined): 'journee' | 'accueil' {
|
||||||
|
return estRoleTerrain(role) ? 'journee' : 'accueil';
|
||||||
|
}
|
||||||
26
apps/mobile/src/auth/use-permissions.ts
Normal file
26
apps/mobile/src/auth/use-permissions.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { ObjectCategory, PermissionRight } from '@siop/shared';
|
||||||
|
import { useMe } from './session';
|
||||||
|
|
||||||
|
/** Lecture UI de la matrice — même patron que le web (use-permissions.ts).
|
||||||
|
* L'API re-vérifie chaque requête (ADR-003) : ceci ne fait QUE piloter
|
||||||
|
* l'affichage (onglets, groupes du Menu, boutons d'action). */
|
||||||
|
export function usePermissions() {
|
||||||
|
const { data: me } = useMe();
|
||||||
|
const can = (category: ObjectCategory, right: PermissionRight): boolean => {
|
||||||
|
const entry = me?.permissions.find((p) => p.objectCategory === category);
|
||||||
|
if (!entry) return false;
|
||||||
|
switch (right) {
|
||||||
|
case 'view':
|
||||||
|
return entry.canView;
|
||||||
|
case 'viewOther':
|
||||||
|
return entry.canViewOther;
|
||||||
|
case 'create':
|
||||||
|
return entry.canCreate;
|
||||||
|
case 'edit':
|
||||||
|
return entry.canEdit;
|
||||||
|
case 'delete':
|
||||||
|
return entry.canDelete;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return { can };
|
||||||
|
}
|
||||||
105
apps/mobile/src/composants/formulaire-demande.tsx
Normal file
105
apps/mobile/src/composants/formulaire-demande.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Pressable, Text, TextInput, View } from 'react-native';
|
||||||
|
import { useAssetOptions, useCreateRequest } from '@/api/exploitation';
|
||||||
|
import { useTokens } from '@/theme/tokens';
|
||||||
|
import { BoutonTel, ChoixTel } from './ui';
|
||||||
|
|
||||||
|
/** Formulaire « Nouvelle demande » — mêmes champs que la modale de
|
||||||
|
* signalement du web (asset via `/assets/options`, accessible même sans
|
||||||
|
* ASSETS.view ; description ; personne bloquée). Utilisé à la fois par
|
||||||
|
* l'onglet du Demandeur et par le Menu des rôles gestion. */
|
||||||
|
export function FormulaireDemande({ surSucces }: { surSucces: (id: string) => void }) {
|
||||||
|
const t = useTokens();
|
||||||
|
const { data: options } = useAssetOptions();
|
||||||
|
const creation = useCreateRequest();
|
||||||
|
const [assetId, setAssetId] = useState<string | null>(null);
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [personneBloquee, setPersonneBloquee] = useState(false);
|
||||||
|
|
||||||
|
const asset = (options ?? []).find((a) => a.id === assetId) ?? null;
|
||||||
|
const valide = !!assetId && description.trim().length >= 3;
|
||||||
|
|
||||||
|
const envoyer = () => {
|
||||||
|
if (!assetId) return;
|
||||||
|
creation.mutate(
|
||||||
|
{ assetId, description: description.trim(), isPersonTrapped: personneBloquee },
|
||||||
|
{ onSuccess: (r) => surSucces(r.id) },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ gap: 12 }}>
|
||||||
|
<ChoixTel
|
||||||
|
libelle="Équipement"
|
||||||
|
requis
|
||||||
|
valeur={asset ? { id: asset.id, label: `${asset.reference} — ${asset.siteName}` } : null}
|
||||||
|
options={(options ?? []).map((a) => ({ id: a.id, label: `${a.reference} — ${a.siteName}` }))}
|
||||||
|
surChoix={setAssetId}
|
||||||
|
/>
|
||||||
|
<View style={{ gap: 4 }}>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_700Bold', fontSize: 11, color: t.encre2 }}>
|
||||||
|
Description <Text style={{ color: t.danger }}>*</Text>
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
accessibilityLabel="Description du problème"
|
||||||
|
placeholder="Ex. : la porte ne se ferme plus au 3ᵉ étage"
|
||||||
|
placeholderTextColor={t.encre3}
|
||||||
|
multiline
|
||||||
|
value={description}
|
||||||
|
onChangeText={setDescription}
|
||||||
|
style={{
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: t.bordureForte,
|
||||||
|
borderRadius: 9,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 9,
|
||||||
|
minHeight: 64,
|
||||||
|
color: t.encre,
|
||||||
|
fontFamily: 'Manrope_600SemiBold',
|
||||||
|
fontSize: 13,
|
||||||
|
textAlignVertical: 'top',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="switch"
|
||||||
|
accessibilityLabel="Une personne est-elle bloquée ?"
|
||||||
|
accessibilityState={{ checked: personneBloquee }}
|
||||||
|
onPress={() => setPersonneBloquee((v) => !v)}
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
backgroundColor: personneBloquee ? t.prioBloqueFond : t.surface,
|
||||||
|
borderColor: personneBloquee ? t.danger : t.bordure,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: 'Manrope_700Bold',
|
||||||
|
fontSize: 13,
|
||||||
|
color: personneBloquee ? t.danger : t.encre,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Une personne est-elle bloquée ?
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 13, color: personneBloquee ? t.danger : t.encre3 }}>
|
||||||
|
{personneBloquee ? 'OUI' : 'NON'}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
{creation.isError ? (
|
||||||
|
<Text style={{ color: t.danger, fontFamily: 'Manrope_600SemiBold', fontSize: 12.5 }}>
|
||||||
|
{creation.error.message}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<BoutonTel
|
||||||
|
libelle="Envoyer la demande"
|
||||||
|
desactive={!valide || creation.isPending}
|
||||||
|
surAppui={envoyer}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
185
apps/mobile/src/composants/panneau-demandes.tsx
Normal file
185
apps/mobile/src/composants/panneau-demandes.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { router } from 'expo-router';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { FlatList, Text, TextInput, View } from 'react-native';
|
||||||
|
import type { RequestSummary } from '@siop/shared';
|
||||||
|
import {
|
||||||
|
useApproveRequest,
|
||||||
|
useRejectRequest,
|
||||||
|
useRequests,
|
||||||
|
} from '@/api/exploitation';
|
||||||
|
import { usePermissions } from '@/auth/use-permissions';
|
||||||
|
import { useTokens, type Tokens } from '@/theme/tokens';
|
||||||
|
import { BoutonTel } 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
|
||||||
|
* suit SES demandes ; Gestionnaire/Dispatcher/Administrateur y approuvent
|
||||||
|
* ou rejettent (motif obligatoire, comme au web) ; Vue seule consulte sans
|
||||||
|
* bouton. L'API renvoie déjà la liste correctement scopée (ADR-003) — ce
|
||||||
|
* composant affiche ce qu'on lui donne et ne propose que les actions
|
||||||
|
* permises par `can(...)`. */
|
||||||
|
export function PanneauDemandes() {
|
||||||
|
const t = useTokens();
|
||||||
|
const { can } = usePermissions();
|
||||||
|
const { data: requests } = useRequests();
|
||||||
|
const peutTraiter = can('REQUESTS', 'edit');
|
||||||
|
const peutCreer = can('REQUESTS', 'create');
|
||||||
|
const toutes = requests ?? [];
|
||||||
|
const aTraiter = toutes.filter((r) => r.status === 'RECEIVED').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, gap: 10 }}>
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'baseline' }}>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_800ExtraBold', fontSize: 19, color: t.encre }}>
|
||||||
|
Demandes
|
||||||
|
</Text>
|
||||||
|
<Text style={{ marginLeft: 'auto', fontFamily: 'Manrope_600SemiBold', fontSize: 11, color: t.encre3 }}>
|
||||||
|
{aTraiter} à traiter · {toutes.length} au total
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{peutCreer ? (
|
||||||
|
<BoutonTel libelle="+ Nouvelle demande" surAppui={() => router.push('/demandes/nouvelle')} />
|
||||||
|
) : null}
|
||||||
|
<FlatList
|
||||||
|
data={toutes}
|
||||||
|
keyExtractor={(r) => r.id}
|
||||||
|
contentContainerStyle={{ gap: 8, paddingBottom: 12 }}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<Text style={{ color: t.encre3, fontFamily: 'Manrope_600SemiBold', padding: 16, textAlign: 'center' }}>
|
||||||
|
Aucune demande pour l'instant.
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
renderItem={({ item: r }) => <CarteDemande demande={r} peutTraiter={peutTraiter} />}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const STYLE_STATUT: Record<RequestSummary['status'], (t: Tokens) => [string, string]> = {
|
||||||
|
RECEIVED: (t) => [t.stAttente, t.stAttenteFond],
|
||||||
|
APPROVED: (t) => [t.stTermine, t.stTermineFond],
|
||||||
|
REJECTED: (t) => [t.stAnnule, t.stAnnuleFond],
|
||||||
|
};
|
||||||
|
const LABEL_STATUT: Record<RequestSummary['status'], string> = {
|
||||||
|
RECEIVED: 'Reçue',
|
||||||
|
APPROVED: 'Approuvée',
|
||||||
|
REJECTED: 'Rejetée',
|
||||||
|
};
|
||||||
|
|
||||||
|
function CarteDemande({ demande: r, peutTraiter }: { demande: RequestSummary; peutTraiter: boolean }) {
|
||||||
|
const t = useTokens();
|
||||||
|
const approbation = useApproveRequest();
|
||||||
|
const rejet = useRejectRequest();
|
||||||
|
const [motif, setMotif] = useState<string | null>(null); // null = fermé
|
||||||
|
|
||||||
|
const [enc, fond] = STYLE_STATUT[r.status](t);
|
||||||
|
const enCours = r.status === 'RECEIVED';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: t.surface,
|
||||||
|
borderColor: r.isPersonTrapped && enCours ? t.danger : t.bordure,
|
||||||
|
borderWidth: r.isPersonTrapped && enCours ? 1.5 : 1,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 12,
|
||||||
|
gap: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
fontFamily: 'Manrope_700Bold',
|
||||||
|
fontSize: 10.5,
|
||||||
|
letterSpacing: 0.6,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
color: t.encre2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{r.reference}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontFamily: 'Manrope_700Bold',
|
||||||
|
fontSize: 10.5,
|
||||||
|
color: enc,
|
||||||
|
backgroundColor: fond,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 999,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{LABEL_STATUT[r.status]}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_700Bold', fontSize: 14, color: t.encre }}>
|
||||||
|
{r.isPersonTrapped ? '⚠ ' : ''}
|
||||||
|
{r.description}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ fontFamily: 'Manrope_400Regular', fontSize: 12, color: t.encre2 }}>
|
||||||
|
Asc. {r.assetReference} — {r.siteName} · {r.requesterLabel}
|
||||||
|
</Text>
|
||||||
|
{r.rejectionReason ? (
|
||||||
|
<Text style={{ fontFamily: 'Manrope_600SemiBold', fontSize: 12, color: t.danger }}>
|
||||||
|
Motif du rejet : {r.rejectionReason}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{peutTraiter && enCours ? (
|
||||||
|
motif === null ? (
|
||||||
|
<View style={{ flexDirection: 'row', gap: 8, marginTop: 4 }}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<BoutonTel
|
||||||
|
libelle="Approuver → OT"
|
||||||
|
variante="vert"
|
||||||
|
surAppui={() =>
|
||||||
|
approbation.mutate({
|
||||||
|
id: r.id,
|
||||||
|
priority: r.isPersonTrapped ? 'PERSON_TRAPPED' : 'HIGH',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<BoutonTel libelle="Rejeter" variante="gris" surAppui={() => setMotif('')} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={{ gap: 6, marginTop: 4 }}>
|
||||||
|
<TextInput
|
||||||
|
accessibilityLabel="Motif du rejet"
|
||||||
|
placeholder="Motif (lisible par le demandeur) *"
|
||||||
|
placeholderTextColor={t.encre3}
|
||||||
|
value={motif}
|
||||||
|
onChangeText={setMotif}
|
||||||
|
style={{
|
||||||
|
borderWidth: 1.5,
|
||||||
|
borderColor: t.bordureForte,
|
||||||
|
borderRadius: 9,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 9,
|
||||||
|
color: t.encre,
|
||||||
|
fontFamily: 'Manrope_600SemiBold',
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View style={{ flexDirection: 'row', gap: 8 }}>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<BoutonTel libelle="Annuler" variante="contour" surAppui={() => setMotif(null)} />
|
||||||
|
</View>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<BoutonTel
|
||||||
|
libelle="Confirmer le rejet"
|
||||||
|
desactive={motif.trim().length < 3}
|
||||||
|
surAppui={() => rejet.mutate({ id: r.id, reason: motif.trim() }, { onSuccess: () => setMotif(null) })}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
764
docs/02-design/maquettes/maquette-mobile-tous-roles.html
Normal file
764
docs/02-design/maquettes/maquette-mobile-tous-roles.html
Normal file
@@ -0,0 +1,764 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>SIOP V2 — Maquettes HD · Mobile ouvert à tous les rôles</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!--
|
||||||
|
SIOP V2 — Maquette HD « Mobile ouvert à tous les rôles » (design-first).
|
||||||
|
À VALIDER PAR LE RÉFÉRENT AVANT TOUT CODE EXPO (principe n°1).
|
||||||
|
R4 avait délibérément fermé le mobile au Technicien (périmètre R4 D3). Ici :
|
||||||
|
ouvrir l'app à TOUS les rôles, avec les MÊMES droits qu'au web (aucune
|
||||||
|
nouvelle logique de permission — le mobile lit la même matrice, ADR-003).
|
||||||
|
Tokens et gabarit téléphone repris tels quels de maquette-r4.html.
|
||||||
|
Les 4 écrans terrain déjà livrés et recettés (Ma journée, Scanner, Préventif,
|
||||||
|
Synchro) restent INCHANGÉS pour le Technicien — cette maquette ajoute une
|
||||||
|
couche autour, elle ne les remplace pas.
|
||||||
|
|
||||||
|
Décisions proposées à validation (récapitulées aussi dans le journal) :
|
||||||
|
D1 — Barre d'onglets ADAPTATIVE selon le rôle connecté (pas un seul jeu
|
||||||
|
d'onglets pour tout le monde) ; Technicien/Technicien limité gardent
|
||||||
|
exactement leurs onglets terrain actuels + un nouvel onglet « Menu ».
|
||||||
|
D2 — Le Menu mobile reprend EXACTEMENT les 4 groupes du web (Exploitation /
|
||||||
|
Parc / Ressources / Pilotage), dans le même ordre, filtrés par la même
|
||||||
|
matrice de permissions déjà en base — aucune règle de droit nouvelle.
|
||||||
|
D3 — Contrairement au web (le groupe reste affiché même vide), un groupe
|
||||||
|
SANS AUCUN lien visible est masqué en entier sur mobile — l'écran est
|
||||||
|
petit, un intitulé de groupe vide n'aide personne.
|
||||||
|
D4 — Le mobile porte les actions COURANTES de chaque famille (consulter,
|
||||||
|
créer une pièce/BC simple, approuver/rejeter, inviter) mais pas les
|
||||||
|
flux de gestion les plus denses (BC multi-lignes détaillé, revue pixel
|
||||||
|
de document) qui restent réservés au grand écran pour l'instant.
|
||||||
|
D5 — Le mobile n'acquiert AUCUNE logique de permission propre : un hook
|
||||||
|
`usePermissions()` calqué sur celui du web, lisant `me.permissions`
|
||||||
|
(déjà renvoyé par `/users/me`, déjà mis en cache par l'app).
|
||||||
|
-->
|
||||||
|
<style>
|
||||||
|
/* ═══ Tokens (répliques de ../tokens.css) ═══ */
|
||||||
|
:root{
|
||||||
|
--primaire:#1f4fb8;--primaire-actif:#1a439c;--primaire-doux:#eaf0fb;
|
||||||
|
--safran:#dd8a0b;--safran-doux:#fdf3e3;
|
||||||
|
--fond:#f5f7fa;--surface:#ffffff;--surface-2:#eef2f7;
|
||||||
|
--bordure:#dce3ec;--bordure-forte:#b9c4d4;
|
||||||
|
--encre:#1b2534;--encre-2:#55647a;--encre-3:#8494ab;
|
||||||
|
--st-ouvert:#3d6fe0;--st-ouvert-fond:#e9effc;--st-encours:#6d5bd8;--st-encours-fond:#efecfa;
|
||||||
|
--st-attente:#b96f07;--st-attente-fond:#fbf1df;--st-termine:#178a50;--st-termine-fond:#e6f5ec;
|
||||||
|
--st-annule:#68788f;--st-annule-fond:#edf0f4;
|
||||||
|
--prio-bloque:#d92626;--prio-bloque-fond:#fdeaea;--prio-haute:#d92626;
|
||||||
|
--prio-moyenne:#b96f07;--prio-basse:#178a50;--prio-aucune:#8494ab;
|
||||||
|
--succes:#178a50;--alerte:#b96f07;--danger:#d92626;--info:#3d6fe0;
|
||||||
|
--graf-1:#3b69d6;--graf-2:#dd8a0b;--graf-3:#1f9e8e;--graf-4:#8a63d2;--graf-grille:#e6ebf2;
|
||||||
|
--nav-fond:#16233b;--nav-encre:#c6d2e4;--nav-encre-vif:#ffffff;--nav-bordure:#24344f;
|
||||||
|
--police-ui:"Manrope","Avenir Next","Segoe UI Variable",system-ui,sans-serif;
|
||||||
|
--rayon:10px;--rayon-controle:8px;--ombre-menu:0 8px 24px rgb(16 30 54/.14);
|
||||||
|
--transition:140ms ease-out;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme:dark){:root{
|
||||||
|
--primaire:#6e92e8;--primaire-actif:#8facf0;--primaire-doux:#1d2c4c;
|
||||||
|
--safran:#e89a1f;--safran-doux:#33270f;
|
||||||
|
--fond:#0f1622;--surface:#182234;--surface-2:#1e2a40;
|
||||||
|
--bordure:#2b3850;--bordure-forte:#3d4d6b;
|
||||||
|
--encre:#e8edf5;--encre-2:#a7b4c8;--encre-3:#6d7d96;
|
||||||
|
--st-ouvert:#7da2ee;--st-ouvert-fond:#1c2a47;--st-encours:#a394ec;--st-encours-fond:#262040;
|
||||||
|
--st-attente:#e0a33c;--st-attente-fond:#322510;--st-termine:#4bc084;--st-termine-fond:#12301f;
|
||||||
|
--st-annule:#93a3ba;--st-annule-fond:#222d3f;
|
||||||
|
--prio-bloque:#f26d6d;--prio-bloque-fond:#3a1414;--prio-haute:#f26d6d;
|
||||||
|
--prio-moyenne:#e0a33c;--prio-basse:#4bc084;--prio-aucune:#6d7d96;
|
||||||
|
--succes:#4bc084;--alerte:#e0a33c;--danger:#f26d6d;--info:#7da2ee;
|
||||||
|
--graf-1:#5580e4;--graf-2:#c7820a;--graf-3:#26a896;--graf-4:#9673de;--graf-grille:#24304a;
|
||||||
|
--nav-fond:#0c1524;--nav-encre:#a7b4c8;--nav-encre-vif:#f2f6fc;--nav-bordure:#1e2c44;
|
||||||
|
--ombre-menu:0 8px 24px rgb(0 0 0/.45);
|
||||||
|
}}
|
||||||
|
:root[data-theme="light"]{
|
||||||
|
--primaire:#1f4fb8;--primaire-actif:#1a439c;--primaire-doux:#eaf0fb;
|
||||||
|
--safran:#dd8a0b;--safran-doux:#fdf3e3;
|
||||||
|
--fond:#f5f7fa;--surface:#ffffff;--surface-2:#eef2f7;
|
||||||
|
--bordure:#dce3ec;--bordure-forte:#b9c4d4;
|
||||||
|
--encre:#1b2534;--encre-2:#55647a;--encre-3:#8494ab;
|
||||||
|
--st-ouvert:#3d6fe0;--st-ouvert-fond:#e9effc;--st-encours:#6d5bd8;--st-encours-fond:#efecfa;
|
||||||
|
--st-attente:#b96f07;--st-attente-fond:#fbf1df;--st-termine:#178a50;--st-termine-fond:#e6f5ec;
|
||||||
|
--st-annule:#68788f;--st-annule-fond:#edf0f4;
|
||||||
|
--prio-bloque:#d92626;--prio-bloque-fond:#fdeaea;--prio-haute:#d92626;
|
||||||
|
--prio-moyenne:#b96f07;--prio-basse:#178a50;--prio-aucune:#8494ab;
|
||||||
|
--succes:#178a50;--alerte:#b96f07;--danger:#d92626;--info:#3d6fe0;
|
||||||
|
--graf-1:#3b69d6;--graf-2:#dd8a0b;--graf-3:#1f9e8e;--graf-4:#8a63d2;--graf-grille:#e6ebf2;
|
||||||
|
--nav-fond:#16233b;--nav-encre:#c6d2e4;--nav-encre-vif:#ffffff;--nav-bordure:#24344f;
|
||||||
|
--ombre-menu:0 8px 24px rgb(16 30 54/.14);
|
||||||
|
}
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
--primaire:#6e92e8;--primaire-actif:#8facf0;--primaire-doux:#1d2c4c;
|
||||||
|
--safran:#e89a1f;--safran-doux:#33270f;
|
||||||
|
--fond:#0f1622;--surface:#182234;--surface-2:#1e2a40;
|
||||||
|
--bordure:#2b3850;--bordure-forte:#3d4d6b;
|
||||||
|
--encre:#e8edf5;--encre-2:#a7b4c8;--encre-3:#6d7d96;
|
||||||
|
--st-ouvert:#7da2ee;--st-ouvert-fond:#1c2a47;--st-encours:#a394ec;--st-encours-fond:#262040;
|
||||||
|
--st-attente:#e0a33c;--st-attente-fond:#322510;--st-termine:#4bc084;--st-termine-fond:#12301f;
|
||||||
|
--st-annule:#93a3ba;--st-annule-fond:#222d3f;
|
||||||
|
--prio-bloque:#f26d6d;--prio-bloque-fond:#3a1414;--prio-haute:#f26d6d;
|
||||||
|
--prio-moyenne:#e0a33c;--prio-basse:#4bc084;--prio-aucune:#6d7d96;
|
||||||
|
--succes:#4bc084;--alerte:#e0a33c;--danger:#f26d6d;--info:#7da2ee;
|
||||||
|
--graf-1:#5580e4;--graf-2:#c7820a;--graf-3:#26a896;--graf-4:#9673de;--graf-grille:#24304a;
|
||||||
|
--nav-fond:#0c1524;--nav-encre:#a7b4c8;--nav-encre-vif:#f2f6fc;--nav-bordure:#1e2c44;
|
||||||
|
--ombre-menu:0 8px 24px rgb(0 0 0/.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ Base ═══ */
|
||||||
|
*{box-sizing:border-box;margin:0}
|
||||||
|
html,body{background:var(--fond)}
|
||||||
|
body{font-family:var(--police-ui);color:var(--encre);font-size:14px;line-height:1.5}
|
||||||
|
button{font:inherit;cursor:pointer;border:0;background:none;color:inherit}
|
||||||
|
button:focus-visible,a:focus-visible{outline:2px solid var(--primaire);outline-offset:2px;border-radius:4px}
|
||||||
|
.num{font-variant-numeric:tabular-nums}
|
||||||
|
|
||||||
|
/* ═══ Barre de revue (outil maquette, pas l'app) ═══ */
|
||||||
|
.revue{position:sticky;top:0;z-index:50;display:flex;align-items:center;gap:12px;flex-wrap:wrap;
|
||||||
|
padding:10px 16px;background:var(--nav-fond);color:var(--nav-encre);font-size:13px}
|
||||||
|
.revue b{color:var(--nav-encre-vif);letter-spacing:.02em}
|
||||||
|
.revue .onglets{display:flex;gap:4px;flex-wrap:wrap}
|
||||||
|
.revue .onglets button{padding:6px 10px;border-radius:6px;color:var(--nav-encre)}
|
||||||
|
.revue .onglets button:hover{background:rgb(255 255 255/.08);color:var(--nav-encre-vif)}
|
||||||
|
.revue .onglets button[aria-selected="true"]{background:rgb(255 255 255/.12);color:var(--nav-encre-vif);
|
||||||
|
box-shadow:inset 3px 0 0 var(--safran)}
|
||||||
|
.revue .droite{margin-left:auto;display:flex;align-items:center;gap:8px}
|
||||||
|
.revue .theme{padding:6px 10px;border:1px solid var(--nav-bordure);border-radius:6px}
|
||||||
|
.note-ecran{max-width:1120px;margin:14px auto 0;padding:0 20px;color:var(--encre-2);font-size:13px}
|
||||||
|
.note-ecran b{color:var(--encre)}
|
||||||
|
|
||||||
|
/* ═══ Écrans ═══ */
|
||||||
|
.ecran{display:none;max-width:1120px;margin:18px auto 44px;padding:0 20px}
|
||||||
|
.ecran.actif{display:block}
|
||||||
|
.tels{display:flex;gap:28px;justify-content:center;flex-wrap:wrap}
|
||||||
|
.tel{width:310px;border:1px solid var(--bordure-forte);border-radius:28px;overflow:hidden;background:var(--fond);
|
||||||
|
box-shadow:var(--ombre-menu);display:flex;flex-direction:column}
|
||||||
|
.tel .encoche{height:26px;background:var(--nav-fond);display:flex;justify-content:center;align-items:center;flex:none}
|
||||||
|
.tel .encoche i{width:70px;height:7px;border-radius:999px;background:rgb(255 255 255/.18)}
|
||||||
|
.tel .legende{flex:none;text-align:center;font-size:11.5px;font-weight:700;letter-spacing:.05em;
|
||||||
|
text-transform:uppercase;color:var(--encre-3);padding:8px;background:var(--surface-2)}
|
||||||
|
.ecran-tel{padding:14px;display:flex;flex-direction:column;gap:10px;min-height:560px}
|
||||||
|
|
||||||
|
/* Entête app */
|
||||||
|
.entete-tel{display:flex;align-items:center;gap:8px}
|
||||||
|
.entete-tel .flamme{color:var(--safran);font-size:16px}
|
||||||
|
.entete-tel b{font-size:16px;letter-spacing:.03em}
|
||||||
|
.entete-tel small{color:var(--encre-3);font-size:10.5px;margin-top:3px}
|
||||||
|
.entete-tel .fin{margin-left:auto;display:flex;align-items:center;gap:6px}
|
||||||
|
.avatar-tel{width:26px;height:26px;border-radius:50%;background:var(--st-encours);color:#fff;
|
||||||
|
display:flex;align-items:center;justify-content:center;font-size:10.5px;font-weight:700}
|
||||||
|
.sync{font-size:10.5px;font-weight:700;color:var(--st-termine);background:var(--st-termine-fond);
|
||||||
|
padding:2px 8px;border-radius:999px;white-space:nowrap}
|
||||||
|
.sync.off{color:var(--st-attente);background:var(--st-attente-fond)}
|
||||||
|
.titre-tel{font-size:17px;font-weight:800;display:flex;align-items:center;gap:8px}
|
||||||
|
.titre-tel .filet{margin-left:auto;font-size:11px;color:var(--encre-3);font-weight:600}
|
||||||
|
|
||||||
|
/* Bandeaux */
|
||||||
|
.bandeau-off{display:flex;align-items:center;gap:8px;background:var(--st-attente-fond);color:var(--st-attente);
|
||||||
|
border:1px solid currentColor;border-radius:10px;padding:8px 10px;font-size:12px;font-weight:700}
|
||||||
|
.bandeau-urgence{display:flex;align-items:center;gap:8px;background:var(--prio-bloque-fond);color:var(--prio-bloque);
|
||||||
|
border-radius:10px;padding:8px 10px;font-size:12px;font-weight:800}
|
||||||
|
.point-pulse{width:8px;height:8px;border-radius:50%;background:currentColor;flex:none}
|
||||||
|
|
||||||
|
/* Cartes OT */
|
||||||
|
.carte-ot-tel{background:var(--surface);border:1px solid var(--bordure);border-radius:12px;padding:11px 12px;
|
||||||
|
border-left:4px solid transparent}
|
||||||
|
.carte-ot-tel.bloque{border-left-color:var(--prio-bloque)}
|
||||||
|
.carte-ot-tel.haute{border-left-color:var(--prio-haute)}
|
||||||
|
.carte-ot-tel.moyenne{border-left-color:var(--prio-moyenne)}
|
||||||
|
.carte-ot-tel.basse{border-left-color:var(--prio-basse)}
|
||||||
|
.carte-ot-tel .type-t{font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--encre-2);
|
||||||
|
font-weight:700;display:flex;justify-content:space-between;align-items:center;gap:6px}
|
||||||
|
.carte-ot-tel b{display:block;margin:3px 0 2px;font-size:14px}
|
||||||
|
.carte-ot-tel .ou{color:var(--encre-2);font-size:12px}
|
||||||
|
.st{font-size:10.5px;font-weight:700;padding:2px 8px;border-radius:999px;white-space:nowrap}
|
||||||
|
.st.ouvert{color:var(--st-ouvert);background:var(--st-ouvert-fond)}
|
||||||
|
.st.encours{color:var(--st-encours);background:var(--st-encours-fond)}
|
||||||
|
.st.attente{color:var(--st-attente);background:var(--st-attente-fond)}
|
||||||
|
.st.termine{color:var(--st-termine);background:var(--st-termine-fond)}
|
||||||
|
.st.annule{color:var(--st-annule);background:var(--st-annule-fond)}
|
||||||
|
.pastille-file{font-size:10.5px;font-weight:700;color:var(--st-attente);background:var(--st-attente-fond);
|
||||||
|
padding:2px 8px;border-radius:999px}
|
||||||
|
|
||||||
|
/* Boutons */
|
||||||
|
.btn-tel{display:flex;justify-content:center;align-items:center;gap:8px;padding:12px;border-radius:10px;
|
||||||
|
font-weight:700;font-size:14.5px;background:var(--primaire);color:#fff;width:100%}
|
||||||
|
.btn-tel.vert{background:var(--succes)}
|
||||||
|
.btn-tel.rouge{background:var(--danger)}
|
||||||
|
.btn-tel.gris{background:var(--bordure-forte);color:var(--surface)}
|
||||||
|
.btn-tel.contour{background:var(--surface);color:var(--primaire);border:1.5px solid var(--primaire)}
|
||||||
|
.paire-boutons{display:flex;gap:8px}
|
||||||
|
.paire-boutons .btn-tel{padding:10px;font-size:13px}
|
||||||
|
|
||||||
|
/* Fiche : cartes d'info */
|
||||||
|
.carte-tel{background:var(--surface);border:1px solid var(--bordure);border-radius:12px;padding:12px;
|
||||||
|
display:flex;flex-direction:column;gap:8px}
|
||||||
|
.carte-tel h4{font-size:10.5px;letter-spacing:.07em;text-transform:uppercase;color:var(--encre-2)}
|
||||||
|
.ligne-info{display:flex;justify-content:space-between;gap:10px;font-size:12.5px}
|
||||||
|
.ligne-info span:first-child{color:var(--encre-2)}
|
||||||
|
.ligne-info b{text-align:right}
|
||||||
|
.garde{font-size:11.5px;color:var(--alerte);font-weight:600}
|
||||||
|
|
||||||
|
/* Tabbar */
|
||||||
|
.tabbar{margin-top:auto;display:flex;border-top:1px solid var(--bordure);background:var(--surface);
|
||||||
|
padding:6px 4px 8px}
|
||||||
|
.tabbar a{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;font-size:9.5px;
|
||||||
|
font-weight:700;color:var(--encre-3);text-decoration:none;position:relative}
|
||||||
|
.tabbar a.actif{color:var(--primaire)}
|
||||||
|
.tabbar a .pic{font-size:17px;line-height:1.2}
|
||||||
|
.tabbar .badge-tab{position:absolute;top:-3px;right:calc(50% - 16px);background:var(--st-attente);
|
||||||
|
color:#fff;font-size:9px;font-weight:800;padding:0 5px;border-radius:999px}
|
||||||
|
.tabbar .badge-tab.rouge{background:var(--prio-bloque)}
|
||||||
|
|
||||||
|
/* ═══ Nouveautés propres à cette maquette ═══ */
|
||||||
|
|
||||||
|
/* KPI mini-cartes (Accueil / Statistiques) */
|
||||||
|
.kpis-tel{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||||
|
.kpi-tel{background:var(--surface);border:1px solid var(--bordure);border-radius:10px;padding:9px 10px}
|
||||||
|
.kpi-tel .val{font-size:19px;font-weight:800;color:var(--encre)}
|
||||||
|
.kpi-tel .lbl{font-size:10.5px;color:var(--encre-2);font-weight:600}
|
||||||
|
.kpi-tel.accent .val{color:var(--primaire)}
|
||||||
|
|
||||||
|
/* Raccourcis (Accueil) */
|
||||||
|
.raccourci-tel{display:flex;align-items:center;gap:10px;background:var(--surface);border:1px solid var(--bordure);
|
||||||
|
border-radius:10px;padding:10px 11px}
|
||||||
|
.raccourci-tel .ico{width:28px;height:28px;border-radius:8px;background:var(--primaire-doux);color:var(--primaire);
|
||||||
|
display:flex;align-items:center;justify-content:center;font-size:14px;flex:none}
|
||||||
|
.raccourci-tel .txt{flex:1;display:flex;flex-direction:column}
|
||||||
|
.raccourci-tel b{font-size:12.5px}
|
||||||
|
.raccourci-tel small{color:var(--encre-2);font-size:11px}
|
||||||
|
.raccourci-tel .chev{color:var(--encre-3)}
|
||||||
|
.raccourci-tel .num{font-size:10.5px;font-weight:800;color:var(--st-attente);background:var(--st-attente-fond);
|
||||||
|
padding:2px 7px;border-radius:999px}
|
||||||
|
|
||||||
|
/* Menu groupé (mirroir de la nav web) */
|
||||||
|
.menu-groupe{display:flex;flex-direction:column;gap:6px}
|
||||||
|
.menu-groupe .titre-groupe{font-size:10.5px;font-weight:800;letter-spacing:.07em;text-transform:uppercase;
|
||||||
|
color:var(--encre-3);padding:4px 2px 0}
|
||||||
|
.menu-item{display:flex;align-items:center;gap:10px;background:var(--surface);border:1px solid var(--bordure);
|
||||||
|
border-radius:10px;padding:10px 11px}
|
||||||
|
.menu-item .ico{width:24px;text-align:center;font-size:14px;color:var(--primaire);flex:none}
|
||||||
|
.menu-item b{flex:1;font-size:12.5px;font-weight:700}
|
||||||
|
.menu-item .chev{color:var(--encre-3);font-size:13px}
|
||||||
|
.menu-item .num{font-size:10.5px;font-weight:800;color:var(--st-attente);background:var(--st-attente-fond);
|
||||||
|
padding:2px 7px;border-radius:999px}
|
||||||
|
.menu-vide{color:var(--encre-3);font-size:12px;text-align:center;padding:20px 8px;font-weight:600}
|
||||||
|
|
||||||
|
/* Formulaire mobile */
|
||||||
|
.champ-tel{display:flex;flex-direction:column;gap:4px}
|
||||||
|
.champ-tel label{font-size:11px;font-weight:700;color:var(--encre-2)}
|
||||||
|
.champ-tel .valeur-champ{border:1.5px solid var(--bordure-forte);border-radius:9px;background:var(--surface);
|
||||||
|
padding:9px 10px;font-size:13px;font-weight:600;color:var(--encre)}
|
||||||
|
.champ-tel textarea.valeur-champ{min-height:56px}
|
||||||
|
|
||||||
|
/* Tableau de synthèse (hors gabarit téléphone) */
|
||||||
|
.table-roles{width:100%;border-collapse:collapse;background:var(--surface);border:1px solid var(--bordure);
|
||||||
|
border-radius:12px;overflow:hidden;font-size:12.5px}
|
||||||
|
.table-roles th,.table-roles td{padding:10px 12px;text-align:left;border-bottom:1px solid var(--bordure)}
|
||||||
|
.table-roles thead th{background:var(--surface-2);font-size:10.5px;letter-spacing:.05em;text-transform:uppercase;
|
||||||
|
color:var(--encre-2)}
|
||||||
|
.table-roles tbody tr:last-child td{border-bottom:0}
|
||||||
|
.table-roles td.role{font-weight:800}
|
||||||
|
.table-roles .oui{color:var(--st-termine);font-weight:800}
|
||||||
|
.table-roles .non{color:var(--encre-3)}
|
||||||
|
.chip-onglet{display:inline-block;background:var(--primaire-doux);color:var(--primaire);font-weight:700;
|
||||||
|
font-size:11px;padding:2px 8px;border-radius:999px;margin:2px 3px 2px 0}
|
||||||
|
.chip-onglet.terrain{background:var(--safran-doux);color:var(--safran)}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<header class="revue">
|
||||||
|
<b>SIOP · Maquette — Mobile ouvert à tous les rôles</b>
|
||||||
|
<nav class="onglets" role="tablist" aria-label="Écrans">
|
||||||
|
<button role="tab" aria-selected="true" data-cible="synthese">1 · Synthèse par rôle</button>
|
||||||
|
<button role="tab" aria-selected="false" data-cible="accueil">2 · Accueil adaptatif</button>
|
||||||
|
<button role="tab" aria-selected="false" data-cible="menu">3 · Menu groupé</button>
|
||||||
|
<button role="tab" aria-selected="false" data-cible="ot-dispatch">4 · OT — Dispatcher</button>
|
||||||
|
<button role="tab" aria-selected="false" data-cible="demandes">5 · Demandes</button>
|
||||||
|
<button role="tab" aria-selected="false" data-cible="stock">6 · Stock & achats</button>
|
||||||
|
<button role="tab" aria-selected="false" data-cible="pilotage">7 · Personnes & statistiques</button>
|
||||||
|
</nav>
|
||||||
|
<div class="droite"><button class="theme" id="btn-theme">Thème : auto</button></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- ═══ 1 · SYNTHÈSE PAR RÔLE ═══ -->
|
||||||
|
<p class="note-ecran" data-note="synthese"><b>Intention :</b> avant le détail écran par écran,
|
||||||
|
le tableau ci-dessous résume la <b>décision D1/D2</b> pour les 7 rôles : quels onglets la barre
|
||||||
|
adaptative affiche, et quels groupes du Menu restent visibles (mêmes droits qu'au web, rien de
|
||||||
|
nouveau dans la matrice). Les 4 onglets terrain du Technicien (encadré safran) sont ceux déjà
|
||||||
|
livrés en R4 — inchangés.</p>
|
||||||
|
<section class="ecran actif" id="synthese">
|
||||||
|
<div style="overflow-x:auto">
|
||||||
|
<table class="table-roles">
|
||||||
|
<thead><tr><th>Rôle (compte démo)</th><th>Onglets de la barre</th><th>Groupes du Menu visibles</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="role">Administrateur<br><small style="font-weight:500;color:var(--encre-3)">Yasmine Alaoui</small></td>
|
||||||
|
<td><span class="chip-onglet">Accueil</span><span class="chip-onglet">OT</span><span class="chip-onglet">Menu</span></td>
|
||||||
|
<td>Exploitation · Parc · Ressources · Pilotage <span style="color:var(--encre-3)">(tout)</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="role">Gestionnaire<br><small style="font-weight:500;color:var(--encre-3)">Nadia Berrada</small></td>
|
||||||
|
<td><span class="chip-onglet">Accueil</span><span class="chip-onglet">OT</span><span class="chip-onglet">Menu</span></td>
|
||||||
|
<td>Exploitation · Parc · Ressources · Pilotage <span style="color:var(--encre-3)">(stats en lecture)</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="role">Dispatcher<br><small style="font-weight:500;color:var(--encre-3)">Salma Idrissi</small></td>
|
||||||
|
<td><span class="chip-onglet">Accueil</span><span class="chip-onglet">OT</span><span class="chip-onglet">Menu</span></td>
|
||||||
|
<td>Exploitation · Parc (lecture) <span style="color:var(--encre-3)">— Ressources/Pilotage restreints</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="box-shadow:inset 3px 0 0 var(--safran)">
|
||||||
|
<td class="role">Technicien<br><small style="font-weight:500;color:var(--encre-3)">Ahmed Benali</small></td>
|
||||||
|
<td><span class="chip-onglet terrain">Ma journée</span><span class="chip-onglet terrain">Scanner</span><span class="chip-onglet terrain">Préventif</span><span class="chip-onglet terrain">Synchro</span><span class="chip-onglet">Menu</span></td>
|
||||||
|
<td>Menu quasi vide (ses OT + parc en lecture) — <b>inchangé depuis R4</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="box-shadow:inset 3px 0 0 var(--safran)">
|
||||||
|
<td class="role">Technicien limité<br><small style="font-weight:500;color:var(--encre-3)">Youssef Tazi</small></td>
|
||||||
|
<td><span class="chip-onglet terrain">Ma journée</span><span class="chip-onglet terrain">Scanner</span><span class="chip-onglet terrain">Préventif</span><span class="chip-onglet terrain">Synchro</span><span class="chip-onglet">Menu</span></td>
|
||||||
|
<td>Menu quasi vide (ses OT uniquement) — <b>inchangé depuis R4</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="role">Demandeur<br><small style="font-weight:500;color:var(--encre-3)">Karim Doukkali</small></td>
|
||||||
|
<td><span class="chip-onglet">Accueil</span><span class="chip-onglet">Nouvelle demande</span></td>
|
||||||
|
<td>Exploitation → Demandes uniquement <span style="color:var(--encre-3)">(Menu quasi vide, D3)</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="role">Vue seule<br><small style="font-weight:500;color:var(--encre-3)">Rachid Bennis</small></td>
|
||||||
|
<td><span class="chip-onglet">Accueil</span><span class="chip-onglet">Menu</span></td>
|
||||||
|
<td>Exploitation · Parc · Ressources · Pilotage <span style="color:var(--encre-3)">(lecture seule, aucun bouton d'action)</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ 2 · ACCUEIL ADAPTATIF ═══ -->
|
||||||
|
<p class="note-ecran" data-note="accueil" hidden><b>Intention :</b> le Technicien garde « Ma
|
||||||
|
journée » telle quelle (rien à revalider). Les autres rôles reçoivent un <b>Accueil</b> propre à
|
||||||
|
leur travail : le Gestionnaire voit ses signaux de pilotage et des raccourcis vers ce qui demande
|
||||||
|
une action ; le Demandeur voit directement ses demandes et le geste qu'il fait le plus (en
|
||||||
|
créer une nouvelle) — jamais un écran vide générique.</p>
|
||||||
|
<section class="ecran" id="accueil">
|
||||||
|
<div class="tels">
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Technicien — inchangé (R4)</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Technicien</small>
|
||||||
|
<span class="fin"><span class="sync">Synchro à jour</span><span class="avatar-tel">AB</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Ma journée <span class="filet">jeudi 1 août</span></div>
|
||||||
|
<div class="bandeau-urgence"><span class="point-pulse"></span>1 personne bloquée — B2 · Tour Atlas</div>
|
||||||
|
<div class="carte-ot-tel bloque">
|
||||||
|
<div class="type-t">OT-2026-0342 · Dépannage <span class="st ouvert">Ouvert</span></div>
|
||||||
|
<b>Personne bloquée en cabine</b>
|
||||||
|
<div class="ou">Asc. B2 — Tour Atlas · Aïn Diab</div>
|
||||||
|
</div>
|
||||||
|
<div class="carte-ot-tel haute">
|
||||||
|
<div class="type-t">OT-2026-0341 · Dépannage <span class="st encours">En cours</span></div>
|
||||||
|
<b>Bruit anormal en gaine</b>
|
||||||
|
<div class="ou">Asc. A1 — Résidence Al Manar</div>
|
||||||
|
</div>
|
||||||
|
<nav class="tabbar">
|
||||||
|
<a class="actif" href="#"><span class="pic">☰</span>Ma journée</a>
|
||||||
|
<a href="#"><span class="pic">▣</span>Scanner</a>
|
||||||
|
<a href="#"><span class="pic">✓</span>Préventif</a>
|
||||||
|
<a href="#"><span class="pic">⇅</span>Synchro</a>
|
||||||
|
<a href="#"><span class="pic">☷</span>Menu</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Gestionnaire — nouveau</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Gestionnaire</small>
|
||||||
|
<span class="fin"><span class="sync">Synchro à jour</span><span class="avatar-tel">NB</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Accueil <span class="filet">jeudi 1 août</span></div>
|
||||||
|
<div class="kpis-tel">
|
||||||
|
<div class="kpi-tel accent"><div class="val">6</div><div class="lbl">OT en cours</div></div>
|
||||||
|
<div class="kpi-tel"><div class="val">3</div><div class="lbl">Stock sous seuil</div></div>
|
||||||
|
<div class="kpi-tel"><div class="val">78 %</div><div class="lbl">Préventif du mois</div></div>
|
||||||
|
<div class="kpi-tel"><div class="val">2</div><div class="lbl">Demandes reçues</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="raccourci-tel"><span class="ico">◫</span>
|
||||||
|
<span class="txt"><b>Stock sous seuil</b><small>3 pièces à commander</small></span>
|
||||||
|
<span class="num">3</span><span class="chev">›</span></div>
|
||||||
|
<div class="raccourci-tel"><span class="ico">✎</span>
|
||||||
|
<span class="txt"><b>Demandes reçues</b><small>en attente d'approbation</small></span>
|
||||||
|
<span class="num">2</span><span class="chev">›</span></div>
|
||||||
|
<div class="raccourci-tel"><span class="ico">◔</span>
|
||||||
|
<span class="txt"><b>Invitations en attente</b><small>lien envoyé, pas encore activé</small></span>
|
||||||
|
<span class="chev">›</span></div>
|
||||||
|
<nav class="tabbar">
|
||||||
|
<a class="actif" href="#"><span class="pic">⌂</span>Accueil</a>
|
||||||
|
<a href="#"><span class="pic">☰</span>OT</a>
|
||||||
|
<a href="#"><span class="pic">☷</span>Menu</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Demandeur — nouveau</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Demandeur</small>
|
||||||
|
<span class="fin"><span class="sync">Synchro à jour</span><span class="avatar-tel">KD</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Mes demandes</div>
|
||||||
|
<div class="carte-ot-tel">
|
||||||
|
<div class="type-t">DEM-2026-0089 <span class="st attente">Reçue</span></div>
|
||||||
|
<b>Éclairage cabine défaillant</b>
|
||||||
|
<div class="ou">Asc. A2 — Résidence Al Manar</div>
|
||||||
|
</div>
|
||||||
|
<div class="carte-ot-tel">
|
||||||
|
<div class="type-t">DEM-2026-0081 <span class="st termine">Traitée</span></div>
|
||||||
|
<b>Bruit au démarrage</b>
|
||||||
|
<div class="ou">Asc. A2 — Résidence Al Manar → OT-2026-0336</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:auto">
|
||||||
|
<button class="btn-tel">+ Nouvelle demande</button>
|
||||||
|
</div>
|
||||||
|
<nav class="tabbar">
|
||||||
|
<a class="actif" href="#"><span class="pic">⌂</span>Accueil</a>
|
||||||
|
<a href="#"><span class="pic">+</span>Nouvelle demande</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ 3 · MENU GROUPÉ ═══ -->
|
||||||
|
<p class="note-ecran" data-note="menu" hidden><b>Intention :</b> le Menu reprend, à l'identique,
|
||||||
|
les 4 groupes et l'ordre du sidebar web — <b>Exploitation / Parc / Ressources / Pilotage</b> — et
|
||||||
|
les mêmes permissions (`can(catégorie, 'view')`). Un rôle qui n'a droit à rien dans un groupe ne
|
||||||
|
voit pas le groupe du tout (D3) : comparez Administrateur (tout) et Demandeur (presque rien) sur
|
||||||
|
le MÊME écran, sans aucune règle de plus dans le code.</p>
|
||||||
|
<section class="ecran" id="menu">
|
||||||
|
<div class="tels">
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Administrateur — tout visible</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Administrateur</small>
|
||||||
|
<span class="fin"><span class="avatar-tel">YA</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Menu</div>
|
||||||
|
<div class="menu-groupe">
|
||||||
|
<div class="titre-groupe">Exploitation</div>
|
||||||
|
<div class="menu-item"><span class="ico">▦</span><b>Tableau de bord</b><span class="chev">›</span></div>
|
||||||
|
<div class="menu-item"><span class="ico">☰</span><b>Ordres de travail</b><span class="num">1</span><span class="chev">›</span></div>
|
||||||
|
<div class="menu-item"><span class="ico">✎</span><b>Demandes</b><span class="num">2</span><span class="chev">›</span></div>
|
||||||
|
<div class="menu-item"><span class="ico">✓</span><b>Préventif</b><span class="chev">›</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-groupe">
|
||||||
|
<div class="titre-groupe">Parc</div>
|
||||||
|
<div class="menu-item"><span class="ico">▣</span><b>Ascenseurs</b><span class="chev">›</span></div>
|
||||||
|
<div class="menu-item"><span class="ico">◫</span><b>Sites</b><span class="chev">›</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-groupe">
|
||||||
|
<div class="titre-groupe">Ressources</div>
|
||||||
|
<div class="menu-item"><span class="ico">◔</span><b>Stock & achats</b><span class="num">3</span><span class="chev">›</span></div>
|
||||||
|
<div class="menu-item"><span class="ico">⇄</span><b>Tiers</b><span class="chev">›</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-groupe">
|
||||||
|
<div class="titre-groupe">Pilotage</div>
|
||||||
|
<div class="menu-item"><span class="ico">◈</span><b>Statistiques</b><span class="chev">›</span></div>
|
||||||
|
<div class="menu-item"><span class="ico">◎</span><b>Personnes & équipes</b><span class="chev">›</span></div>
|
||||||
|
</div>
|
||||||
|
<nav class="tabbar">
|
||||||
|
<a href="#"><span class="pic">⌂</span>Accueil</a>
|
||||||
|
<a href="#"><span class="pic">☰</span>OT</a>
|
||||||
|
<a class="actif" href="#"><span class="pic">☷</span>Menu</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Demandeur — presque vide (D3)</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Demandeur</small>
|
||||||
|
<span class="fin"><span class="avatar-tel">KD</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Menu</div>
|
||||||
|
<div class="menu-groupe">
|
||||||
|
<div class="titre-groupe">Exploitation</div>
|
||||||
|
<div class="menu-item"><span class="ico">✎</span><b>Mes demandes</b><span class="chev">›</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="menu-vide">Parc, Ressources et Pilotage ne sont pas accessibles à ce rôle —<br>groupes masqués, pas de section vide affichée.</div>
|
||||||
|
<nav class="tabbar">
|
||||||
|
<a class="actif" href="#"><span class="pic">⌂</span>Accueil</a>
|
||||||
|
<a href="#"><span class="pic">+</span>Nouvelle demande</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ 4 · OT — DISPATCHER ═══ -->
|
||||||
|
<p class="note-ecran" data-note="ot-dispatch" hidden><b>Intention :</b> contrairement au
|
||||||
|
Technicien (qui ne voit que SES OT), le Dispatcher a `viewOther` — il voit tous les OT et peut en
|
||||||
|
créer/assigner. Le mobile porte cette différence : liste complète, bouton « + » et une action
|
||||||
|
rapide « Assigner » par carte (bottom-sheet, même pattern que le sélecteur de bilan codé de R4).</p>
|
||||||
|
<section class="ecran" id="ot-dispatch">
|
||||||
|
<div class="tels">
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Dispatcher — tous les OT</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Dispatcher</small>
|
||||||
|
<span class="fin"><span class="sync">Synchro à jour</span><span class="avatar-tel">SI</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Ordres de travail <span class="filet">14 en cours</span></div>
|
||||||
|
<div class="carte-ot-tel bloque">
|
||||||
|
<div class="type-t">OT-2026-0342 · Dépannage <span class="st ouvert">Ouvert</span></div>
|
||||||
|
<b>Personne bloquée en cabine</b>
|
||||||
|
<div class="ou">Asc. B2 — Tour Atlas · non assigné</div>
|
||||||
|
</div>
|
||||||
|
<div class="carte-ot-tel haute">
|
||||||
|
<div class="type-t">OT-2026-0341 · Dépannage <span class="st encours">En cours</span></div>
|
||||||
|
<b>Bruit anormal en gaine</b>
|
||||||
|
<div class="ou">Asc. A1 — Résidence Al Manar · Ahmed Benali</div>
|
||||||
|
</div>
|
||||||
|
<div class="carte-ot-tel moyenne">
|
||||||
|
<div class="type-t">OT-2026-0336 · Préventif <span class="st ouvert">Ouvert</span></div>
|
||||||
|
<b>Contrôle mensuel — juillet</b>
|
||||||
|
<div class="ou">Asc. A2 — Résidence Al Manar · non assigné</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:auto">
|
||||||
|
<button class="btn-tel">+ Nouvel OT</button>
|
||||||
|
</div>
|
||||||
|
<nav class="tabbar">
|
||||||
|
<a href="#"><span class="pic">⌂</span>Accueil</a>
|
||||||
|
<a class="actif" href="#"><span class="pic">☰</span>OT</a>
|
||||||
|
<a href="#"><span class="pic">☷</span>Menu</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Assigner — feuille au pouce</div>
|
||||||
|
<div class="ecran-tel" style="justify-content:flex-end;padding:0">
|
||||||
|
<div style="flex:1;background:rgb(9 14 22/.45)"></div>
|
||||||
|
<div style="background:var(--surface);border-radius:16px 16px 0 0;padding:14px;display:flex;flex-direction:column;gap:10px">
|
||||||
|
<div style="font-weight:800;font-size:15px">Assigner OT-2026-0342</div>
|
||||||
|
<div class="champ-tel">
|
||||||
|
<label>Technicien</label>
|
||||||
|
<div class="valeur-champ">Ahmed Benali ▾</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-tel">Assigner</button>
|
||||||
|
<button class="btn-tel contour">Annuler</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ 5 · DEMANDES ═══ -->
|
||||||
|
<p class="note-ecran" data-note="demandes" hidden><b>Intention :</b> le Demandeur crée sa demande
|
||||||
|
en mobilité (site, ascenseur si connu, description, urgence) exactement comme au web mais sur un
|
||||||
|
formulaire au pouce ; le Gestionnaire/Dispatcher approuve ou rejette (motif obligatoire au rejet,
|
||||||
|
comme au web) directement depuis la carte.</p>
|
||||||
|
<section class="ecran" id="demandes">
|
||||||
|
<div class="tels">
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Demandeur — nouvelle demande</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Demandeur</small>
|
||||||
|
<span class="fin"><span class="avatar-tel">KD</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Nouvelle demande</div>
|
||||||
|
<div class="champ-tel"><label>Site</label><div class="valeur-champ">Résidence Al Manar ▾</div></div>
|
||||||
|
<div class="champ-tel"><label>Ascenseur (si connu)</label><div class="valeur-champ">Asc. A2 ▾</div></div>
|
||||||
|
<div class="champ-tel"><label>Description</label>
|
||||||
|
<div class="valeur-champ" style="min-height:56px">Odeur de brûlé au démarrage, depuis ce matin.</div>
|
||||||
|
</div>
|
||||||
|
<div class="champ-tel"><label>Urgence</label><div class="valeur-champ">Personne bloquée — Non</div></div>
|
||||||
|
<div style="margin-top:auto">
|
||||||
|
<button class="btn-tel">Envoyer la demande</button>
|
||||||
|
</div>
|
||||||
|
<nav class="tabbar">
|
||||||
|
<a href="#"><span class="pic">⌂</span>Accueil</a>
|
||||||
|
<a class="actif" href="#"><span class="pic">+</span>Nouvelle demande</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Gestionnaire — approbation</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Gestionnaire</small>
|
||||||
|
<span class="fin"><span class="avatar-tel">NB</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Demandes reçues <span class="filet">2</span></div>
|
||||||
|
<div class="carte-ot-tel">
|
||||||
|
<div class="type-t">DEM-2026-0089 <span class="st attente">Reçue</span></div>
|
||||||
|
<b>Éclairage cabine défaillant</b>
|
||||||
|
<div class="ou">Asc. A2 — Résidence Al Manar · Karim Doukkali</div>
|
||||||
|
<div class="paire-boutons" style="margin-top:8px">
|
||||||
|
<button class="btn-tel vert">Approuver</button>
|
||||||
|
<button class="btn-tel rouge">Rejeter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="carte-ot-tel">
|
||||||
|
<div class="type-t">DEM-2026-0090 <span class="st attente">Reçue</span></div>
|
||||||
|
<b>Porte palière qui force</b>
|
||||||
|
<div class="ou">Asc. C1 — Anfa Place · Karim Doukkali</div>
|
||||||
|
<div class="paire-boutons" style="margin-top:8px">
|
||||||
|
<button class="btn-tel vert">Approuver</button>
|
||||||
|
<button class="btn-tel rouge">Rejeter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="tabbar">
|
||||||
|
<a href="#"><span class="pic">⌂</span>Accueil</a>
|
||||||
|
<a class="actif" href="#"><span class="pic">☰</span>OT</a>
|
||||||
|
<a href="#"><span class="pic">☷</span>Menu</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ 6 · STOCK & ACHATS ═══ -->
|
||||||
|
<p class="note-ecran" data-note="stock" hidden><b>Intention :</b> le Gestionnaire consulte le
|
||||||
|
stock en mobilité et déclenche l'action courante — commander une pièce sous seuil — sans passer
|
||||||
|
par le grand écran. La fiche pièce reprend les lignes déjà connues (quantité, seuil, dernier
|
||||||
|
prix, fournisseur). Le bon de commande détaillé multi-lignes reste au web (D4).</p>
|
||||||
|
<section class="ecran" id="stock">
|
||||||
|
<div class="tels">
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Gestionnaire — stock</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Gestionnaire</small>
|
||||||
|
<span class="fin"><span class="avatar-tel">NB</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Stock & achats <span class="filet">3 sous seuil</span></div>
|
||||||
|
<div class="carte-tel">
|
||||||
|
<h4>Câble de traction 8 mm</h4>
|
||||||
|
<div class="ligne-info"><span>Quantité</span><b style="color:var(--danger)">2</b></div>
|
||||||
|
<div class="ligne-info"><span>Seuil</span><b>5</b></div>
|
||||||
|
<div class="ligne-info"><span>Fournisseur</span><b>Ascentech Maroc</b></div>
|
||||||
|
<button class="btn-tel" style="margin-top:4px">Commander</button>
|
||||||
|
</div>
|
||||||
|
<div class="carte-tel">
|
||||||
|
<h4>Galet de guidage</h4>
|
||||||
|
<div class="ligne-info"><span>Quantité</span><b style="color:var(--danger)">1</b></div>
|
||||||
|
<div class="ligne-info"><span>Seuil</span><b>4</b></div>
|
||||||
|
<div class="ligne-info"><span>Fournisseur</span><b>SchindlerParts</b></div>
|
||||||
|
<button class="btn-tel" style="margin-top:4px">Commander</button>
|
||||||
|
</div>
|
||||||
|
<nav class="tabbar">
|
||||||
|
<a href="#"><span class="pic">⌂</span>Accueil</a>
|
||||||
|
<a href="#"><span class="pic">☰</span>OT</a>
|
||||||
|
<a class="actif" href="#"><span class="pic">☷</span>Menu</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">BC pré-rempli depuis l'alerte</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Gestionnaire</small>
|
||||||
|
<span class="fin"><span class="avatar-tel">NB</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Nouveau BC</div>
|
||||||
|
<div class="champ-tel"><label>Fournisseur</label><div class="valeur-champ">Ascentech Maroc</div></div>
|
||||||
|
<div class="champ-tel"><label>Pièce</label><div class="valeur-champ">Câble de traction 8 mm</div></div>
|
||||||
|
<div class="champ-tel"><label>Quantité</label><div class="valeur-champ">10</div></div>
|
||||||
|
<div class="garde">Le détail multi-lignes se termine sur le grand écran (D4).</div>
|
||||||
|
<div style="margin-top:auto">
|
||||||
|
<button class="btn-tel">Créer le BC</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══ 7 · PERSONNES & STATISTIQUES ═══ -->
|
||||||
|
<p class="note-ecran" data-note="pilotage" hidden><b>Intention :</b> deux écrans « Pilotage » côte
|
||||||
|
à côte — les Statistiques en lecture (mêmes chiffres que `/analytics/summary`, en cartes plutôt
|
||||||
|
qu'en graphes denses) et Personnes & équipes, avec l'action d'invitation déjà connue du web
|
||||||
|
(lien 7 jours) portée sur mobile.</p>
|
||||||
|
<section class="ecran" id="pilotage">
|
||||||
|
<div class="tels">
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Administrateur — statistiques</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Administrateur</small>
|
||||||
|
<span class="fin"><span class="avatar-tel">YA</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Statistiques <span class="filet">12 derniers mois</span></div>
|
||||||
|
<div class="kpis-tel">
|
||||||
|
<div class="kpi-tel accent"><div class="val">505 MAD</div><div class="lbl">Coût du mois</div></div>
|
||||||
|
<div class="kpi-tel"><div class="val">78 %</div><div class="lbl">Taux préventif</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="carte-tel">
|
||||||
|
<h4>Pannes par organe — top 3</h4>
|
||||||
|
<div class="ligne-info"><span>Portes palières</span><b>9</b></div>
|
||||||
|
<div class="ligne-info"><span>Câblage</span><b>6</b></div>
|
||||||
|
<div class="ligne-info"><span>Éclairage cabine</span><b>4</b></div>
|
||||||
|
</div>
|
||||||
|
<nav class="tabbar">
|
||||||
|
<a href="#"><span class="pic">⌂</span>Accueil</a>
|
||||||
|
<a href="#"><span class="pic">☰</span>OT</a>
|
||||||
|
<a class="actif" href="#"><span class="pic">☷</span>Menu</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tel">
|
||||||
|
<div class="encoche"><i></i></div>
|
||||||
|
<div class="legende">Gestionnaire — personnes & équipes</div>
|
||||||
|
<div class="ecran-tel">
|
||||||
|
<div class="entete-tel">
|
||||||
|
<span class="flamme">♦</span><b>SIOP</b><small>Gestionnaire</small>
|
||||||
|
<span class="fin"><span class="avatar-tel">NB</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="titre-tel">Personnes <span class="filet">12</span></div>
|
||||||
|
<div class="menu-item"><span class="ico">◎</span><b>Ahmed Benali</b><span style="color:var(--encre-3);font-size:11px">Technicien</span></div>
|
||||||
|
<div class="menu-item"><span class="ico">◎</span><b>Youssef Tazi</b><span style="color:var(--encre-3);font-size:11px">Technicien limité</span></div>
|
||||||
|
<div class="menu-item"><span class="ico">◎</span><b>Salma Idrissi</b><span style="color:var(--encre-3);font-size:11px">Dispatcher</span></div>
|
||||||
|
<div style="margin-top:auto">
|
||||||
|
<button class="btn-tel">+ Inviter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var onglets=document.querySelectorAll('.revue .onglets button');
|
||||||
|
var ecrans=document.querySelectorAll('.ecran');
|
||||||
|
var notes=document.querySelectorAll('.note-ecran');
|
||||||
|
onglets.forEach(function(b){b.addEventListener('click',function(){
|
||||||
|
onglets.forEach(function(x){x.setAttribute('aria-selected','false')});
|
||||||
|
b.setAttribute('aria-selected','true');
|
||||||
|
var cible=b.dataset.cible;
|
||||||
|
ecrans.forEach(function(e){e.classList.toggle('actif',e.id===cible)});
|
||||||
|
notes.forEach(function(n){n.hidden=(n.dataset.note!==cible)});
|
||||||
|
window.scrollTo({top:0});
|
||||||
|
})});
|
||||||
|
var ordre=['auto','light','dark'];var i=0;
|
||||||
|
var bt=document.getElementById('btn-theme');
|
||||||
|
bt.addEventListener('click',function(){
|
||||||
|
i=(i+1)%3;var m=ordre[i];
|
||||||
|
if(m==='auto'){document.documentElement.removeAttribute('data-theme')}
|
||||||
|
else{document.documentElement.setAttribute('data-theme',m)}
|
||||||
|
bt.textContent='Thème : '+(m==='auto'?'auto':(m==='light'?'clair':'sombre'));
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -4,6 +4,65 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook**
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-08-02 — Pr. Daaif (+ Claude) — R6 lancée : mobile ouvert à tous les rôles
|
||||||
|
|
||||||
|
**Contexte** — deux retours du référent en test réel sur iPhone : (1) une fois authentifié,
|
||||||
|
impossible de revenir en arrière ou de se déconnecter ; (2) l'app mobile devrait être accessible
|
||||||
|
à tous les rôles, pas seulement au Technicien, avec les mêmes actions que le web. Le premier est
|
||||||
|
un bug, corrigé dans la foulée ; le second est un changement de fond — R4 avait délibérément
|
||||||
|
fermé le périmètre au Technicien (décision actée dans ses maquettes). Ouvrir à tous les rôles
|
||||||
|
veut dire porter sur mobile une bonne partie de ce que fait le web (référentiel, ressources,
|
||||||
|
personnes, statistiques…) : plus gros que R4 dans son ensemble, donc une **nouvelle release,
|
||||||
|
R6** (R4 est close depuis le 17/07, R4.1/R4.2/R4.3 déjà pris par le socle mobile technicien
|
||||||
|
d'origine — pas question de les réutiliser et de brouiller l'historique).
|
||||||
|
|
||||||
|
**Actions**
|
||||||
|
|
||||||
|
- **Correctif déconnexion** : `EnteteTabs` (`composants/ui.tsx`) factorise une entête commune aux
|
||||||
|
4 onglets terrain — badge de compte + confirmation `Alert` au tap simple (l'ancien geste,
|
||||||
|
`onLongPress` sur « Ma journée » seulement, n'était pas découvrable). Le rôle affiché vient du
|
||||||
|
compte connecté, plus de libellé « Technicien » figé.
|
||||||
|
- **Maquette R6** (`docs/02-design/maquettes/maquette-mobile-tous-roles.html`, 7 écrans) rédigée
|
||||||
|
et validée par le référent le jour même : synthèse par rôle, Accueil adaptatif, Menu groupé
|
||||||
|
filtré par la matrice, OT côté Dispatcher, Demandes (création + approbation), Stock, Personnes
|
||||||
|
& statistiques. Cinq décisions actées : D1 barre d'onglets adaptative (Technicien/Technicien
|
||||||
|
limité inchangés + onglet Menu ajouté) ; D2 le Menu reprend à l'identique les 4 groupes du web
|
||||||
|
(Exploitation/Parc/Ressources/Pilotage), même matrice, aucune règle de droit nouvelle ; D3 un
|
||||||
|
groupe sans lien visible est masqué en entier (contrairement au web) ; D4 le mobile porte les
|
||||||
|
actions courantes par famille, pas les flux de gestion les plus denses (réservés au web pour
|
||||||
|
l'instant) ; D5 aucune logique de permission propre au mobile — un hook `usePermissions()`
|
||||||
|
calqué sur celui du web.
|
||||||
|
- **R6.1 — socle** : `usePermissions()` mobile (lit `me.permissions`, déjà renvoyé par
|
||||||
|
`/users/me`) ; barre d'onglets adaptative (`ongletsVisibles` par rôle, `href: null` pour masquer
|
||||||
|
sans retirer du navigateur — le Menu peut toujours y pousser) ; écrans Accueil (dashboard réel
|
||||||
|
pour Administrateur/Gestionnaire/Dispatcher/Vue seule — aucun chiffre inventé, seuls
|
||||||
|
WORK_ORDERS/REQUESTS sont câblés), OT (liste complète, `viewOther` déjà géré serveur, réutilise
|
||||||
|
la fiche OT R4 telle quelle — elle dérive déjà ses actions de `allowedTransitions`), Menu
|
||||||
|
(groupes filtrés, groupe vide masqué), Demandes (`PanneauDemandes`, un seul composant pour tous
|
||||||
|
les rôles : création + suivi pour le Demandeur, approbation/rejet à motif pour
|
||||||
|
Gestionnaire/Dispatcher/Administrateur, lecture seule pour Vue seule — même patron que le web,
|
||||||
|
découverte au passage : `GET /assets/options` déjà ouvert à tout rôle authentifié, réutilisé
|
||||||
|
tel quel). Familles pas encore portées (Sites, Ascenseurs, Stock, Tiers, Fichiers, Statistiques,
|
||||||
|
Assistant, Personnes) : écran « à venir » honnête (`composants/a-venir.tsx`, déjà présent mais
|
||||||
|
jamais câblé jusqu'ici) plutôt qu'un lien mort — chacune mérite son propre passage, sur le
|
||||||
|
modèle R4.1→R4.3.
|
||||||
|
- Vérifié : typecheck propre, 17 tests Jest verts, lint 5/5 paquets. Pas de vérification visuelle
|
||||||
|
en navigateur de mon côté (aucun outil de ce type dans cet environnement) — à confirmer par le
|
||||||
|
référent sur l'iPhone déjà connecté au serveur Metro.
|
||||||
|
|
||||||
|
**Décisions**
|
||||||
|
|
||||||
|
- Nouvelle release **R6** (pas R4.1/4.2/4.3, déjà pris ; pas de réouverture de R4, clos) pour tout
|
||||||
|
le chantier « mobile ouvert à tous les rôles ». Sous-étapes à venir : R6.2 (Parc — Sites,
|
||||||
|
Ascenseurs), R6.3 (Ressources — Stock, Tiers, Fichiers), R6.4 (Pilotage — Statistiques,
|
||||||
|
Personnes, Assistant), sur le modèle des sous-releases R4.1→R4.3.
|
||||||
|
|
||||||
|
**Prochaine étape** : R6.2 (Parc sur mobile) ; confirmation du référent sur iPhone (nav adaptative
|
||||||
|
et correctif déconnexion) ; restes non bloquants inchangés (recette Android physique,
|
||||||
|
redéploiement Dokploy, calibrage `AI_SEUIL_*`/darija, `DOKPLOY_WEBHOOK_URL`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-08-01 — Pr. Daaif (+ Claude) — Dictée validée sur iPhone physique (ADR-004 §5)
|
## 2026-08-01 — Pr. Daaif (+ Claude) — Dictée validée sur iPhone physique (ADR-004 §5)
|
||||||
|
|
||||||
**Actions**
|
**Actions**
|
||||||
|
|||||||
Reference in New Issue
Block a user