From f210d4f8013df4296fe626270ae5d452ddb7de56 Mon Sep 17 00:00:00 2001 From: pr-daaif Date: Sun, 2 Aug 2026 11:35:55 +0100 Subject: [PATCH] =?UTF-8?q?feat(r6):=20R6.3=20=E2=80=94=20Ressources=20sur?= =?UTF-8?q?=20mobile=20(Stock,=20Tiers,=20Fichiers)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api/ressources.ts (usePartners, useParts/usePart, useCreatePurchaseOrder, useDocuments). Écran Stock (sous-seuil en tête) + fiche pièce + "Commander" pré-rempli en une ligne (fournisseur figé, quantité = manquant jusqu'au seuil, prix = dernier connu — BC multi-lignes détaillé réservé au web, D4). Tiers en lecture seule (création/édition réservées au web). Fichiers en métadonnées seules — l'ouverture demande expo-sharing (dépendance native absente, donc un nouveau build natif) : différée explicitement plutôt qu'ajoutée à la légère au milieu de cette passe. Menu : Stock & achats / Tiers / Fichiers routent réellement. Typecheck propre, 17 tests Jest, lint 5/5 paquets. Contrat non touché, toutes les opérations utilisées existaient déjà depuis R3. Co-Authored-By: Claude Sonnet 5 --- CLAUDE.md | 3 +- apps/mobile/app/(tabs)/menu.tsx | 6 +- apps/mobile/app/bibliotheque/index.tsx | 80 +++++++++++++++++++ apps/mobile/app/stock/[id].tsx | 65 ++++++++++++++++ apps/mobile/app/stock/[id]/commander.tsx | 99 ++++++++++++++++++++++++ apps/mobile/app/stock/index.tsx | 90 +++++++++++++++++++++ apps/mobile/app/tiers/index.tsx | 67 ++++++++++++++++ apps/mobile/src/api/ressources.ts | 47 +++++++++++ docs/journal/journal.md | 36 +++++++++ 9 files changed, 489 insertions(+), 4 deletions(-) create mode 100644 apps/mobile/app/bibliotheque/index.tsx create mode 100644 apps/mobile/app/stock/[id].tsx create mode 100644 apps/mobile/app/stock/[id]/commander.tsx create mode 100644 apps/mobile/app/stock/index.tsx create mode 100644 apps/mobile/app/tiers/index.tsx create mode 100644 apps/mobile/src/api/ressources.ts diff --git a/CLAUDE.md b/CLAUDE.md index bd4040d..ae28b0e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,5 +73,6 @@ pnpm + Turborepo. `apps/api` : NestJS, Prisma, PostgreSQL (pgvector + PostGIS), - ✅ **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. - ✅ **R6.2 — Parc (Sites, Ascenseurs)** : `useLocations()` mobile ; écran Sites (premier niveau, `parentId===null`) ; fiche site (identité, zones, ascenseurs du site — consultation seule, D4, pas de carte ni d'édition sur mobile) ; écran Ascenseurs (parc complet déjà préchargé, D1) ouvrant la fiche appareil R4 telle quelle (générique, aucune modification nécessaire). Menu : Ascenseurs/Sites routent réellement, Catégories reste à venir (admin, hors périmètre). Typecheck propre, 17 tests Jest, lint 5/5 — exécution directe de la maquette déjà validée, pas de nouveau tour de design. -- 🔄 **Reprise ici** : R6.3 (Ressources — Stock, Tiers, Fichiers), puis R6.4 (Pilotage — Statistiques, Personnes, Assistant), sur le modèle des sous-releases R4.1→R4.3. Confirmation du référent sur iPhone (nav adaptative R6.1 + correctif déconnexion) toujours en attente. Restes non bloquants inchangés : redéploiement Dokploy de l'instance ENSET (`AI_SERVICE_TOKEN` à créer — runbook §2 — puis « Réindexer tout »), recette Android sur appareil physique, calibrage `AI_SEUIL_*` et qualité darija sur corpus SPELEV réel, secret `DOKPLOY_WEBHOOK_URL`, production client SPELEV (attend les accès serveur du partenaire). +- ✅ **R6.3 — Ressources (Stock, Tiers, Fichiers)** : `api/ressources.ts` (usePartners/useParts/usePart/useCreatePurchaseOrder/useDocuments) ; écran Stock (sous-seuil en tête) + fiche pièce + « Commander » pré-rempli en une ligne (fournisseur figé, quantité = manquant jusqu'au seuil, prix = dernier connu — le BC multi-lignes détaillé reste au web, D4) ; Tiers en lecture seule (création/édition réservées au web) ; Fichiers en métadonnées seules — l'ouverture/téléchargement demande `expo-sharing` (dépendance native absente, donc un nouveau build) et est explicitement différée plutôt qu'ajoutée à la légère. Menu branché. Typecheck propre, 17 tests Jest, lint 5/5, contrat non touché (toutes les opérations existaient déjà depuis R3). +- 🔄 **Reprise ici** : R6.4 (Pilotage — Statistiques, Personnes, Assistant), sur le modèle des sous-releases R4.1→R4.3. Confirmation du référent sur iPhone (nav adaptative R6.1 + correctif déconnexion) toujours en attente. 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), `expo-sharing` pour l'ouverture de documents mobile. - Détail quotidien : `docs/journal/journal.md`. Dépôt : `siop-spelev/siop2` (privé), jalons R0→R5 (v1) + R6 en cours. diff --git a/apps/mobile/app/(tabs)/menu.tsx b/apps/mobile/app/(tabs)/menu.tsx index 917a6ad..575b159 100644 --- a/apps/mobile/app/(tabs)/menu.tsx +++ b/apps/mobile/app/(tabs)/menu.tsx @@ -45,9 +45,9 @@ const GROUPES: { titre: string; liens: LienMenu[] }[] = [ { 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.' }, + { libelle: 'Stock & achats', ico: '◔', permission: ['PARTS', 'view'], route: '/stock' }, + { libelle: 'Tiers', ico: '⇄', permission: ['PURCHASE_ORDERS', 'view'], route: '/tiers' }, + { libelle: 'Fichiers', ico: '▧', permission: ['ASSETS', 'view'], route: '/bibliotheque' }, ], }, { diff --git a/apps/mobile/app/bibliotheque/index.tsx b/apps/mobile/app/bibliotheque/index.tsx new file mode 100644 index 0000000..9335fd3 --- /dev/null +++ b/apps/mobile/app/bibliotheque/index.tsx @@ -0,0 +1,80 @@ +import { FlatList, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useDocuments } from '@/api/ressources'; +import { EnteteFiche } from '@/composants/ui'; +import { useTokens } from '@/theme/tokens'; + +const ICONE_TYPE: Record = { + NOTICE: '📘', + CERTIFICATE: '📜', + PHOTO: '🖼', + OTHER: '📄', +}; + +function taille(octets: number): string { + if (octets < 1024) return `${octets} o`; + if (octets < 1024 * 1024) return `${Math.round(octets / 1024)} Ko`; + return `${(octets / (1024 * 1024)).toFixed(1)} Mo`; +} + +/** Fichiers — R6.3 (Ressources). Consultation des métadonnées seulement + * (D4) : l'ouverture/téléchargement d'un document sur mobile demande un + * flux authentifié (l'API le streame, MinIO n'est jamais exposé) suivi + * d'un partage natif — nécessite `expo-sharing`, pas encore une dépendance + * du projet ; ajoutée dans une prochaine étape plutôt que d'introduire une + * nouvelle dépendance native (et donc un nouveau build) dans cette passe. + * En attendant : ouvrir depuis le web. */ +export default function PageBibliotheque() { + const t = useTokens(); + const { data: documents } = useDocuments(); + const fichiers = [...(documents ?? [])].sort( + (a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt), + ); + + return ( + + + + + Consultation seulement — l'ouverture se fait depuis le web pour l'instant. + + d.id} + contentContainerStyle={{ gap: 8, paddingBottom: 12 }} + ListEmptyComponent={ + + Aucun document accessible. + + } + renderItem={({ item: d }) => ( + + {ICONE_TYPE[d.kind] ?? '📄'} + + + {d.fileName} + + + {[d.assetReference && `Asc. ${d.assetReference}`, d.workOrderReference, taille(d.size)] + .filter(Boolean) + .join(' · ')} + + + + )} + /> + + + ); +} diff --git a/apps/mobile/app/stock/[id].tsx b/apps/mobile/app/stock/[id].tsx new file mode 100644 index 0000000..e4d7fec --- /dev/null +++ b/apps/mobile/app/stock/[id].tsx @@ -0,0 +1,65 @@ +import { router, useLocalSearchParams } from 'expo-router'; +import { ScrollView, Text } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { usePart } from '@/api/ressources'; +import { usePermissions } from '@/auth/use-permissions'; +import { BoutonTel, Carte, EnteteFiche, LigneInfo } from '@/composants/ui'; +import { useTokens } from '@/theme/tokens'; + +const fmt = new Intl.NumberFormat('fr-FR'); +const fmtMAD = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'MAD' }); + +/** Fiche pièce — R6.3. Consultation + une action courante (D4) : commander, + * pré-rempli depuis cette fiche, comme la maquette (écran 6). Les + * mouvements de stock (entrée manuelle, ajustement) restent au web. */ +export default function PageFichePiece() { + const t = useTokens(); + const { id } = useLocalSearchParams<{ id: string }>(); + const { data: piece } = usePart(id); + const { can } = usePermissions(); + + if (!piece) return null; + + const peutCommander = can('PURCHASE_ORDERS', 'create') && !!piece.supplierId; + + return ( + + + + + + + {fmt.format(piece.stock)} + + } + /> + + + + {piece.compatible ? : null} + + + {peutCommander ? ( + router.push(`/stock/${piece.id}/commander`)} /> + ) : null} + {!piece.supplierId && can('PURCHASE_ORDERS', 'create') ? ( + + Aucun fournisseur associé — commande depuis le web. + + ) : null} + + + ); +} diff --git a/apps/mobile/app/stock/[id]/commander.tsx b/apps/mobile/app/stock/[id]/commander.tsx new file mode 100644 index 0000000..2cd4d1a --- /dev/null +++ b/apps/mobile/app/stock/[id]/commander.tsx @@ -0,0 +1,99 @@ +import { router, useLocalSearchParams } from 'expo-router'; +import { useState } from 'react'; +import { ScrollView, Text, TextInput, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useCreatePurchaseOrder, usePart } from '@/api/ressources'; +import { BoutonTel, Carte, EnteteFiche, LigneInfo } from '@/composants/ui'; +import { useTokens } from '@/theme/tokens'; + +/** Nouveau BC pré-rempli depuis l'alerte stock — maquette écran 6. Une + * seule ligne (cette pièce) : le bon de commande multi-lignes détaillé + * reste au web (D4). */ +export default function PageCommander() { + const t = useTokens(); + const { id } = useLocalSearchParams<{ id: string }>(); + const { data: piece } = usePart(id); + const creation = useCreatePurchaseOrder(); + const manquant = piece ? Math.max(piece.threshold - piece.stock, 1) : 1; + const [quantite, setQuantite] = useState(String(manquant)); + const [prix, setPrix] = useState(''); + + if (!piece || !piece.supplierId) return null; + const prixDefaut = piece.lastUnitPrice != null ? String(piece.lastUnitPrice) : ''; + const prixSaisi = prix || prixDefaut; + const qte = Number(quantite); + const pu = Number(prixSaisi); + const valide = qte > 0 && pu >= 0 && Number.isFinite(qte) && Number.isFinite(pu); + + return ( + + + + + + + + + Quantité + + + + + Prix unitaire (MAD) + + + + {creation.isError ? ( + + {creation.error.message} + + ) : null} + + creation.mutate( + { + supplierId: piece.supplierId!, + lines: [{ partId: piece.id, quantity: qte, unitPrice: pu }], + }, + { onSuccess: () => (router.canGoBack() ? router.back() : router.replace('/stock')) }, + ) + } + /> + + + ); +} diff --git a/apps/mobile/app/stock/index.tsx b/apps/mobile/app/stock/index.tsx new file mode 100644 index 0000000..264b5b8 --- /dev/null +++ b/apps/mobile/app/stock/index.tsx @@ -0,0 +1,90 @@ +import { router } from 'expo-router'; +import { FlatList, Pressable, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useParts } from '@/api/ressources'; +import { EnteteFiche } from '@/composants/ui'; +import { useTokens } from '@/theme/tokens'; + +const fmt = new Intl.NumberFormat('fr-FR'); + +/** Stock — R6.3 (Ressources). Sous-seuil en tête, comme le web (stock.tsx) — + * ce qui demande une action passe avant le reste. */ +export default function PageStock() { + const t = useTokens(); + const { data: parts } = useParts(); + const pieces = [...(parts ?? [])].sort((a, b) => { + if (a.belowThreshold !== b.belowThreshold) return a.belowThreshold ? -1 : 1; + return a.designation.localeCompare(b.designation); + }); + const sousSeuil = pieces.filter((p) => p.belowThreshold).length; + + return ( + + + + {sousSeuil} sous seuil + + ) : undefined + } + /> + p.id} + contentContainerStyle={{ gap: 8, paddingBottom: 12 }} + ListEmptyComponent={ + + Aucune pièce accessible. + + } + renderItem={({ item: p }) => ( + router.push(`/stock/${p.id}`)} + style={{ + backgroundColor: t.surface, + borderColor: p.belowThreshold ? t.danger : t.bordure, + borderWidth: p.belowThreshold ? 1.5 : 1, + borderRadius: 12, + padding: 12, + gap: 2, + }} + > + + + {p.designation} + + + {fmt.format(p.stock)} + + + + {p.reference} · seuil {fmt.format(p.threshold)} + {p.supplierName ? ` · ${p.supplierName}` : ''} + + + )} + /> + + + ); +} diff --git a/apps/mobile/app/tiers/index.tsx b/apps/mobile/app/tiers/index.tsx new file mode 100644 index 0000000..cace802 --- /dev/null +++ b/apps/mobile/app/tiers/index.tsx @@ -0,0 +1,67 @@ +import { FlatList, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { usePartners } from '@/api/ressources'; +import { EnteteFiche } from '@/composants/ui'; +import { useTokens } from '@/theme/tokens'; + +/** Tiers — R6.3 (Ressources). Consultation seule sur mobile (D4) : la + * création/édition de fournisseurs et clients reste au web pour l'instant. */ +export default function PageTiers() { + const t = useTokens(); + const { data: partners } = usePartners(); + const tiers = [...(partners ?? [])].sort((a, b) => a.name.localeCompare(b.name)); + + return ( + + + + p.id} + contentContainerStyle={{ gap: 8, paddingBottom: 12 }} + ListEmptyComponent={ + + Aucun tiers accessible. + + } + renderItem={({ item: p }) => ( + + + + {p.name} + + + {p.kind === 'SUPPLIER' ? 'Fournisseur' : 'Client'} + + + + {[p.contactName, p.phone, p.city].filter(Boolean).join(' · ') || '—'} + {p.kind === 'SUPPLIER' && p.openOrders ? ` · ${p.openOrders} BC en cours` : ''} + + + )} + /> + + + ); +} diff --git a/apps/mobile/src/api/ressources.ts b/apps/mobile/src/api/ressources.ts new file mode 100644 index 0000000..b423c2d --- /dev/null +++ b/apps/mobile/src/api/ressources.ts @@ -0,0 +1,47 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { PurchaseOrderCreate } from '@siop/shared'; +import { api, unwrap } from './client'; + +/** Hooks R6.3 — Ressources (Stock, Tiers, Fichiers) : mêmes opérations que + * le web (gestion.ts), périmètre mobile plus étroit (D4 — consultation + + * action courante « commander », pas les flux de gestion les plus denses : + * pas de création/édition de pièce ou de tiers sur mobile pour l'instant). */ + +export function usePartners() { + return useQuery({ + queryKey: ['partners'], + queryFn: async () => (await unwrap(await api.GET('/partners'))).partners, + }); +} + +export function useParts() { + return useQuery({ + queryKey: ['parts'], + queryFn: async () => (await unwrap(await api.GET('/parts'))).parts, + }); +} + +export function usePart(id: string) { + return useQuery({ + queryKey: ['parts', id], + queryFn: async () => unwrap(await api.GET('/parts/{id}', { params: { path: { id } } })), + }); +} + +export function useCreatePurchaseOrder() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body: PurchaseOrderCreate) => + unwrap(await api.POST('/purchase-orders', { body })), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['purchase-orders'] }), + }); +} + +/** Bibliothèque complète (sans filtre appareil/OT — voir aussi + * `useDocumentsOT` dans exploitation.ts, scopée à un OT). */ +export function useDocuments() { + return useQuery({ + queryKey: ['documents'], + queryFn: async () => (await unwrap(await api.GET('/documents'))).documents, + }); +} diff --git a/docs/journal/journal.md b/docs/journal/journal.md index 39788b2..54b8541 100644 --- a/docs/journal/journal.md +++ b/docs/journal/journal.md @@ -4,6 +4,42 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook** --- +## 2026-08-02 — Pr. Daaif (+ Claude) — R6.3 : Ressources sur mobile (Stock, Tiers, Fichiers) + +**Actions** + +- `apps/mobile/src/api/ressources.ts` (nouveau fichier, mêmes opérations que `gestion.ts` côté + web) : `usePartners`, `useParts`/`usePart`, `useCreatePurchaseOrder`, `useDocuments` (bibliothèque + complète, sans filtre — distincte de `useDocumentsOT` déjà scopée à un OT). +- Écran **Stock** (`app/stock/index.tsx`) : pièces sous seuil en tête (même logique que `stock.tsx` + web), badge compteur. **Fiche pièce** (`app/stock/[id].tsx`) : identité + « Commander » si + `PURCHASE_ORDERS.create` ET fournisseur associé — sinon message explicite plutôt qu'un bouton + mort. **Nouveau BC** (`app/stock/[id]/commander.tsx`) : une seule ligne pré-remplie depuis + l'alerte (fournisseur figé, quantité par défaut = manquant jusqu'au seuil, prix par défaut = + dernier prix connu) — le bon de commande multi-lignes détaillé reste au web (D4). +- Écran **Tiers** (`app/tiers/index.tsx`) : liste fournisseurs/clients, **lecture seule** sur + mobile pour cette passe (D4 — création/édition réservées au web). +- Écran **Fichiers** (`app/bibliotheque/index.tsx`) : métadonnées seulement (nom, taille, + rattachement), **pas d'ouverture/téléchargement** — ce flux demande `expo-sharing` (dépendance + native absente du projet) et donc un nouveau cycle de build natif ; reporté plutôt + qu'ajouté à la légère au milieu de cette passe. Le bandeau de l'écran le dit explicitement. +- Menu : Stock & achats / Tiers / Fichiers routent réellement. +- Vérifié : typecheck propre, 17 tests Jest, lint 5/5 paquets. Contrat (`schema.d.ts`) non touché + — toutes les opérations utilisées existaient déjà (R3). + +**Décisions** + +- Aucune nouvelle — Ressources exécute la maquette R6 déjà validée (écran 6, D4) ; Tiers/Fichiers + n'y étaient pas maquettés en détail mais suivent la même logique de consultation déjà actée, + avec les patrons visuels déjà validés (Carte/LigneInfo/EnteteFiche). +- L'ouverture de document sur mobile est explicitement différée à une prochaine étape (nouvelle + dépendance `expo-sharing` = nouveau build natif à valider, pas à empiler sur cette passe). + +**Prochaine étape** : R6.4 (Pilotage — Statistiques, Personnes, Assistant) ; confirmation du +référent sur iPhone pour R6.1/logout, toujours en attente. + +--- + ## 2026-08-02 — Pr. Daaif (+ Claude) — R6.2 : Parc sur mobile (Sites, Ascenseurs) **Actions**