# LOT MOBILE 0 — Fondation API Mobile + Worker ## Statut Ce document enregistre les décisions d'architecture du LOT MOBILE 0 comme **VALIDÉES**. ## Décisions validées | # | Décision | Statut | |---|----------|--------| | 1 | La base HFSQL n'est **pas modifiable**. Aucune colonne, table ou contrainte ne sera ajoutée/modifiée. | VALIDÉE | | 2 | Le desktop Avalonia continue d'accéder **directement** à HFSQL. Inchangé. | VALIDÉE | | 3 | Le mobile accède **uniquement** via l'API ASP.NET Core. Jamais directement à HFSQL. | VALIDÉE | | 4 | Batik et Omika sont **invisibles** pour le mobile. Le mobile ne connaît jamais la génération de base. | VALIDÉE | | 5 | `OperationId` est présent dans l'enveloppe JSON de synchronisation. GUID de transaction créé par le téléphone. | VALIDÉE | | 6 | `RetryCount` est présent dans l'enveloppe JSON de synchronisation. | VALIDÉE | | 7 | Maximum **30 tentatives** de traitement d'une opération. | VALIDÉE | | 8 | `MobLog` = historique de synchronisation (journal). | VALIDÉE | | 9 | Rétention `MobLog` = **30 jours**. | VALIDÉE | | 10 | Logs uniquement pertinents (réception, succès terminal, rejet terminal, erreur significative, retry significatif, conflit, document introuvable, exception serveur). | VALIDÉE | | 11 | Document identifié publiquement par **GUID** (`DocumentGuid`). Le PK HFSQL reste interne. | VALIDÉE | | 12 | Profil Batik provenant de `ParametreMetier`. | VALIDÉE | | 13 | Profil Omika provenant de `Role`. | VALIDÉE | | 14 | Différences RDV gérées côté backend. Le mobile ne connaît que le modèle commun `RendezVous`. | VALIDÉE | | 15 | Licence offline par défaut = **10 jours**, configurable serveur (`License:OfflineGraceDays`). | VALIDÉE | | 16 | Aucun secret HFSQL communiqué au mobile (adresse, port, login, password, connection string). | VALIDÉE | ## Architecture cible ``` Mobile (Avalonia Android/iOS) │ ▼ API ASP.NET Core (Omika.Api) │ ├── Contrats DTO (Omika.Core.Contracts) ├── Services applicatifs (Omika.Core.Services) ├── Repositories adaptatifs (Omika.Data.Repositories) │ ├── Batik │ └── Omika │ ▼ Tables techniques HFSQL (TOSRV, MobReception, MobLog, Licence, LicenceHisto, ...) │ ▼ Repositories métier existants → Batik ou Omika ``` ## Composants créés ### API Mobile ASP.NET Core - Endpoints Minimal API dans `Omika.Api/Endpoints/Mobile/` - `/health` (existant) - `/api/license/activate` - `/api/license/validate` - `/api/mobile/bootstrap` - `/api/sync/operations` - `/api/sync/status/{operationId}` - `/api/sync/snapshot` - `/api/documents/...` ### Contrats DTO - `Omika.Core.Contracts.Mobile` : licence, bootstrap, synchronisation, snapshot - `Omika.Core.Contracts.Documents` : métadonnées, téléchargement, upload ### Couche applicative mobile/API - `Omika.Core.Services.Mobile` : licence, bootstrap, synchronisation, snapshot - `Omika.Core.Interfaces` : interfaces de services ### Authentification + bootstrap identité/profil/droits - `ITokenService` / `TokenService` : token stateless signé (HMAC-SHA256), lié au client, DeviceId, utilisateur, collaborateur, licence. Aucune table HFSQL supplémentaire. - `MobileAuthMiddleware` : valide le token Bearer et stocke l'identité dans `HttpContext.Items`. - `IMobileIdentityProvider` / `MobileIdentityProvider` : fournisseur d'identité commun (Batik via `ParametreMetier`, Omika via `Role`). - `IMobileIdentityRepository` + `AdaptiveMobileIdentityRepository` + `BatikMobileIdentityRepository` + `OmikaMobileIdentityRepository` : résolution adaptative du profil/droits selon la génération de base. - `IMobileBootstrapService` / `MobileBootstrapService` : construit le contrat commun (utilisateur, collaborateur, profil, droits, configuration, licence). Vérifie que le DeviceId du token correspond à la licence attribuée. - `GET /api/mobile/bootstrap` : endpoint protégé par authentification. ### Infrastructure API - Authentification par token signé (HMAC-SHA256, stateless) - Configuration via `appsettings.json` + variables d'environnement - `ProblemDetails` / contrat d'erreur structuré - `CancellationToken` partout - Fail-fast Production : `MobileAuth:SigningKey` obligatoire en Production (jamais en source) ### Contrat de synchronisation (réception API) - `POST /api/sync/operations` : réception d'une opération de l'Outbox mobile (authentifiée). - Contrat public `SyncOperation` : `OperationId`, `DeviceId`, `EntityType`, `OperationType`, `EntityGuid`, `Payload`. - `OperationId` = GUID de transaction créé par le téléphone. `EntityGuid` = identité de l'objet métier. Jamais interchangeables. - `OperationType` couvre `CREATE`, `UPDATE`, `DELETE`, `ARCHIVE`. - Enveloppe technique interne `SyncEnvelope` persistée dans `TS_JSON` / `MR_JSON` : contient les identités serveur (`ClientId`, `UserId`, `CollaborateurId`) issues du token et le `RetryCount` contrôlé côté serveur. Le mobile ne peut pas imposer ces champs. - Le `DeviceId` de l'opération doit correspondre à l'identité authentifiée. Le `CollaborateurId` fourni par le client n'est jamais utilisé : l'identité provient du token/bootstrap. - La réponse signifie uniquement `RECEIVED` (jamais `SUCCESS`), car l'opération n'est pas encore réellement appliquée. - Idempotence : une répétition du même `OperationId` ne crée pas deux opérations métier. Abstraction `ISyncOperationStatusStore` (stockage terminal `MobLog`). Un verrou par `OperationId` garantit l'idempotence même en cas de requêtes simultanées. - Ordre logique respecté par le futur moteur : `CREATE` → `UPDATE` → `DELETE` / `ARCHIVE`. - `TS_CMD` reçoit une valeur technique interne moderne (`MOB.OP`), jamais le contrat public de l'API. Les anciennes commandes `S.xxx` ne sont pas reproduites publiquement. ### Worker de synchronisation serveur - `Omika.Worker` : worker .NET de synchronisation - Transfert `TOSRV` → `MobReception` - Traitement `MobReception` → métier - Purge `MobLog` (rétention 30 jours) #### Pipeline de traitement (PROMPT 5) - `SyncWorker` : `BackgroundService` qui déclenche périodiquement le traitement. - `SyncProcessor` : logique pure et testable, indépendante du `BackgroundService`. - **Phase 1 — transfert** : `TOSRV` → copie vers `MobReception` → suppression de `TOSRV` uniquement si la copie réussit. - **Phase 2 — traitement** : lit les opérations `MR_Fait = false` arrivées à échéance, parse l'enveloppe JSON (`OperationId`, `RetryCount`, `DeviceId`, `EntityType`, `OperationType`, `EntityGuid`, `Payload`). - **Ordre logique** : `CREATE` → `UPDATE` → `DELETE` / `ARCHIVE`, puis ordre de réception le plus ancien. - `ISyncOperationHandler` / `SyncOperationHandlerRegistry` : handlers métier par `EntityType`, extensibles. Le handler public manipule le contrat commun `SyncOperation` (jamais de différence Batik/Omika visible). - `DefaultSyncOperationHandler` : handler par défaut (traitement métier réel branché ultérieurement via les services/repositories existants). - `SyncOperationResult` : résultat structuré (`Success`, `TransientFailure`, `PermanentFailure`). - `Success` → `SUCCESS` dans MobLog + retrait de la file de travail. - `TransientFailure` → incrément `RetryCount`, réécriture `MR_JSON`, report de `MR_DateHeureRecep`. - `PermanentFailure` → `REJECTED` immédiat (ne consomme pas 30 retries). - Après la 30e tentative impossible → `REJECTED`, plus de retraitement automatique. - `SyncOptions` : `MaxRetryCount = 30`, `RetryDelaySeconds = 120`, `LogRetentionDays = 30` (aucune valeur codée en dur dans les handlers). - `MobLog` : journal structuré (`SyncLogEntry`) avec `OperationId`, statut, `EntityType`, `EntityGuid`, `DeviceId`, `RetryCount`, erreur/code, date. Seuls les événements pertinents sont logués (réception, succès terminal, rejet terminal, erreur significative, retry significatif, conflit, document introuvable, exception serveur). #### Handler Intervention (PROMPT 5 — application métier) - `InterventionSyncPayload` : contrat mobile commun (métadonnées d'intervention, aucun Row HFSQL). - `InterventionSyncOperationHandler` : handler métier Intervention, **commun** (aucune condition Batik/Omika visible). Il délègue le traitement physique à `IInterventionService` (qui sélectionne Batik/Omika via le runtime adaptatif existant) et l'adaptation d'identité publique à `IMobileEntityIdentityResolver`. - **CREATE** : transmet le `EntityGuid` mobile comme `Guid` métier à `CreateInterventionRequest` (rétrocompatible : une création Desktop sans GUID conserve le comportement historique). Idempotent : si l'intervention existe déjà, `SUCCESS` sans recréer. - **UPDATE** : résout l'intervention par identifiant public via le resolver, puis `UpdateAsync`. Absente → `TransientFailure` (`INTERVENTION_NOT_FOUND`). - **ARCHIVE** : `ArchiveAsync` sur l'identifiant public résolu. Absente → `TransientFailure`. - **DELETE** : non supporté pour Intervention → `PermanentFailure` (`INTERVENTION_OPERATION_NOT_SUPPORTED`). - `CreatedEntityId` : identifiant public serveur de l'objet créé (GUID pour Omika, identifiant numérique public pour Batik), jamais le PK HFSQL brut. - Le handler ne dépend que de `IInterventionService` et `IMobileEntityIdentityResolver` (vérifié par test) : aucune dépendance vers un repository physique Batik/Omika. #### Identité publique mobile ↔ identité physique backend (résolveur central) - `IMobileEntityIdentityResolver` / `MobileEntityIdentityResolver` : abstraction Core unique qui adapte l'identité publique mobile à l'identité physique backend selon la génération de base détectée (`ApplicationRuntimeContext`). Aucune condition Batik/Omika dans les handlers. - **Omika** : l'identité publique est le GUID physique. Le resolver le conserve tel quel ; la résolution est directe par GUID. - **Batik** : les tables historiques n'ont pas de GUID physique exploitable. L'identité publique stable est un **GUID synthétique réversible** dérivé du PK numérique (marqueur + version + type d'entité + PK big-endian + checksum CRC32). Il est déterministe, stable, versionné, recalculable sans stockage et décodable vers le PK sans scanner toutes les lignes. Le PK HFSQL n'est jamais exposé au mobile. - `PhysicalEntityIdentity` : contrat interne (GUID physique et/ou PK numérique) porté par `LegacyEntityReference` (`SourceReference`, `[JsonIgnore]` sur le DTO) pour calculer l'identité canonique sans exposer le PK. - `SyncOperationResult` : porte `RequestedEntityGuid` (GUID mobile demandé) et `CanonicalEntityGuid` (GUID canonique stable) lorsqu'ils diffèrent (cas Batik CREATE). Le mobile doit utiliser le GUID canonique pour les UPDATE/ARCHIVE ultérieurs. - **Idempotence par `OperationId` renforcée (reconstitution du résultat terminal)** : le `SyncProcessor` vérifie, avant d'appeler le handler, si l'opération a déjà produit un résultat terminal (`SUCCESS` / `REJECTED`) dans `MobLog` via `ISyncTransferRepository.GetTerminalResultAsync`. Si oui, il **reconstitue** ce résultat sans rappeler le handler métier : un même `OperationId` ne produit jamais deux applications métier (notamment pour un CREATE Batik où le GUID mobile temporaire n'est pas stocké). Le `CanonicalEntityGuid`, le `RequestedEntityGuid` et le `CreatedEntityId` d'origine sont conservés dans `MobLog` (`SyncLogEntry`) et réutilisés lors de la reconstitution. Une protection défensive par `HashSet` empêche aussi tout doublon d'`OperationId` au sein d'un même cycle de traitement. - **Blocage documenté (Batik)** : un GUID mobile temporaire non stocké n'est pas résolvable sans stockage durable. Le mobile doit utiliser le GUID canonique retourné après le CREATE. L'idempotence CREATE Batik par GUID n'est donc pas garantie pour un GUID mobile temporaire (documenté, sans ajout de colonne). L'idempotence par `OperationId` (reconstitution du résultat terminal) reste néanmoins garantie côté serveur. #### Suivi de statut (PROMPT 6) - `GET /api/sync/status/{operationId}` : authentifié. Le résultat appartient au même client/device que l'identité authentifiée. - **Source de vérité** : `MobLog` pour les états terminaux (`SUCCESS` / `REJECTED`) ; `TOSRV` / `MobReception` pour les opérations encore en attente (`PENDING`). Aucun statut `PROCESSING` persistant n'est inventé : il reste `PENDING` entre deux tentatives. - **Contrat public `SyncStatusResponse`** : `OperationId`, `Status`, `RetryCount`, `EntityType`, `OperationType`, `EntityGuid`, `RequestedEntityGuid`, `CanonicalEntityGuid`, `DeviceId`, `ErrorCode`, `ErrorMessage`, `LastUpdatedUtc`, `CompletedAt`. Ne contient jamais de PK HFSQL, d'`IDTOSRV`, de `MR_PKID`, de nom de table technique ni de commande historique `S.xxx` / `W.xxx`. - **Résultat riche pour les états terminaux** : `GetStatusAsync` réutilise `GetTerminalResultAsync` (parsing de l'enveloppe JSON de `MobLog`) pour reconstituer un statut complet, y compris `RequestedEntityGuid` et `CanonicalEntityGuid` (utile pour un CREATE Batik : le mobile récupère le GUID canonique à utiliser pour les UPDATE/ARCHIVE ultérieurs) et `CompletedAt`. - **Isolation par appareil** : si l'opération existe mais appartient à un autre `DeviceId`, `GetStatusAsync` retourne `null` (aucune information métier sensible ne fuit vers un autre appareil). Le endpoint traduit ce cas en `STATUS_NOT_AVAILABLE` avec un HTTP approprié. - **OperationId inconnu / purgé** : retourne `STATUS_NOT_AVAILABLE` avec un HTTP cohérent avec le contrat d'erreur global. #### Purge de MobLog (PROMPT 6) - Rétention `Sync:LogRetentionDays = 30` (aucune valeur codée en dur dans la logique). - **Ciblage strict** : la purge ne supprime QUE les logs structurés appartenant au mécanisme de synchronisation mobile (JSON versionné contenant `"schemaVersion"` et `"operationId"`) **et** dont le statut est terminal (`"status":"Success"` ou `"status":"Rejected"`). Les autres types de logs historiques présents dans `MobLog` ne sont jamais supprimés par cette purge. - **Jamais de purge des opérations non terminées** : une opération `PENDING` n'est jamais supprimée uniquement parce qu'elle est ancienne. - Après purge, un `OperationId` ancien peut retourner `STATUS_NOT_AVAILABLE`. - Le nettoyage est périodique et léger, exécuté par un worker de purge distinct qui ne monopolise pas le worker principal. #### Documents mobile (téléchargement / upload) - `GET /api/documents/{documentGuid:guid}` : téléchargement d'un document par son GUID public (authentifié). Le mobile ne reçoit jamais le chemin physique du serveur. - `POST /api/documents` : upload d'un document. **Un upload = UN document par requête.** Le serveur conserve le `DocumentGuid` fourni par le mobile. - **Identifiant public** : le mobile utilise exclusivement `DocumentGuid` (GUID). Le PK HFSQL (`DC_PKID`) reste strictement interne au backend. - **Résolution d'identité** : le `DocumentGuid` est résolu vers l'identité physique backend via `IMobileEntityIdentityResolver` (GUID physique Omika ou PK numérique Batik), puis le document est chargé par cette identité via `IDocumentRepository.GetByPhysicalIdentityAsync`. - **Stockage physique** : le contenu binaire est lu/écrit via `IDocumentFileStorage` (abstraction anti path traversal, taille maximale configurable). Le `DC_Chemin` stocké en base est un chemin **relatif** à la racine configurée (`Documents:RootPath`). Le chemin serveur n'est jamais exposé au mobile. - **Compatibilité future** : le contrat mobile `DocumentUploadRequest` / `DocumentDownloadResponse` est stable et indépendant de la génération de base (Batik/Omika). Les métadonnées de document sont lues depuis la table `Document` (GUID métier stable si présent, sinon `Guid.Empty` et le resolver produit le GUID public synthétique à partir du PK numérique Batik). Aucune colonne HFSQL n'est ajoutée/modifiée. ## Configuration | Clé | Valeur par défaut | Description | |-----|-------------------|-------------| | `License:OfflineGraceDays` | `10` | Jours de fonctionnement offline | | `Sync:MaxRetryCount` | `30` | Nombre max de tentatives | | `Sync:RetryDelaySeconds` | `120` | Délai entre tentatives | | `Sync:LogRetentionDays` | `30` | Rétention MobLog | | `Auth:Issuer` | — | Émetteur du token | | `Auth:Audience` | — | Audience du token | | `Auth:SigningKey` | — | Clé de signature (secret) | ## Sécurité - Aucun secret HFSQL dans le code source. - Aucune connection string en dur. - Aucun password en dur. - Aucun Row HFSQL exposé publiquement. - Le mobile ne reçoit jamais le chemin physique des fichiers. - Le mobile ne reçoit jamais les adresses/ports/logins/passwords HFSQL. --- # MOBILE UI FOUNDATION ## Statut Socle visuel et de navigation de l'application Avalonia mobile. Ce lot pose le shell responsive, la navigation, le thème clair/sombre, la couleur principale personnalisable, la structure commune des pages et le branchement aux services locaux existants. **Aucun écran métier complet n'est développé ici.** ## Principe maquette UX vs données réelles `docs/maquetteOmika.html` est une **référence UX** (comportement responsive, hiérarchie visuelle, navigation, ergonomie téléphone/tablette), **pas une source de données**. Aucun champ métier, propriété DTO ou donnée fictive n'est créé parce qu'il apparaît dans le HTML. Les modèles/services existants restent la source de vérité métier. Le HTML/CSS n'est pas converti mécaniquement en XAML : l'expérience est reproduite avec des composants Avalonia propres. ## Architecture MVVM ``` View (ShellView, PlaceholderPage, ParametresPage) → ViewModel (ShellViewModel, PlaceholderViewModel, ParametresViewModel) → Service (ThemeService, NavigationService, SyncEngine existant) ``` - Une View n'appelle jamais directement l'API ni SQLite. - Aucune logique métier importante dans les Views. - MVVM via `CommunityToolkit.Mvvm` (déjà présent, aucune nouvelle bibliothèque). ## Shell responsive Le shell principal (`ShellView`) contient : - zone de marque / application - navigation principale (latérale desktop/tablette, barre basse smartphone) - contenu de la page courante - accès au profil (état neutre : aucune identité fictive) - accès synchronisation - état de connectivité/synchronisation non intrusif Aucune donnée utilisateur fictive. Sans identité réelle, un placeholder technique neutre est affiché. ## Navigation Abstraction `INavigationService` / `NavigationService` : - page courante (`CurrentPage`) - éléments disponibles (`Items`) - navigation active (état sélection) - fonctions primaires/secondaires smartphone Le `ShellViewModel` connaît la page courante, les éléments disponibles et la navigation active. Le changement de page placeholder ne déclenche **aucun appel API** : il est purement visuel. La navigation est préparée pour les futurs écrans détail (ex. Agenda → RendezVous → Intervention) sans les implémenter. ### Pages | Page | Type | |------|------| | Home / Agenda | Placeholder | | Interventions | Placeholder | | Dossiers | Placeholder | | Collaborateurs / Équipe | Placeholder | | Clients | Placeholder | | Sites | Placeholder | | Paramètres | Réel minimal (thème, couleur, version, sync) | ## Breakpoints / seuils responsive Centralisés dans `DesignTokens.axaml` (pas de valeurs magiques répétées dans les Views). La logique responsive dépend de la **largeur disponible** (classes adaptatives Avalonia), pas de Android/iOS directement. - **Smartphone** : navigation latérale masquée, barre basse avec 5 accès directs maximum, fonctions secondaires via menu "Plus". - **Tablette** : rail réduit / navigation compacte (transition douce). - **Desktop / grande tablette** : navigation latérale complète. ## Thème clair / sombre `ThemeService` gère `Light`, `Dark` et `System` (si l'architecture le permet). Le changement est dynamique sans redémarrage. Le choix est persisté dans la configuration locale (`UiPreferencesRepository`, table technique `UiPreferences`), jamais dans une table métier snapshot. ## Couleur principale personnalisable Palette basée sur une `PrimaryColor` configurable. Le changement met à jour : - boutons principaux - éléments actifs de navigation - accents - focus - badges / éléments de sélection 8 presets prédéfinis dans `ThemeService.PresetColors`. Pas de sélecteur libre (évite une dépendance inutile). Persistance locale obligatoire. ## Design tokens `DesignTokens.axaml` centralise les ressources visuelles : couleurs, espacements principaux, radius, tailles communes, surfaces, bordures, texte principal / secondaire, success / warning / danger / info. Light et Dark utilisent les mêmes clés de ressources. Aucune valeur couleur hardcodée dans les Views. ## Composants / styles réutilisables `Controls.axaml` définit des styles/ControlThemes pour : PageHeader, Card, boutons principaux/secondaires, badges, champs de saisie, sections, listes, états vides, messages d'erreur, indicateur de chargement. Les contrôles natifs ne sont pas recréés ; les styles apportent une cohérence. ## Persistance des préférences UI `UiPreferences` (table technique `UiPreferences`, ligne unique) stocke : - `ThemeMode` (`System` / `Light` / `Dark`) - `PrimaryColor` (hexadécimal) Persistance via `IUiPreferencesRepository` / `UiPreferencesRepository`. Cette table n'est **jamais** touchée par le refresh snapshot. ## Branchement Synchroniser maintenant `ParametresViewModel.SyncNowCommand` → `ShellViewModel.SyncNowCommand` → `ISyncEngine.SyncAsync` (LE SyncEngine existant, jamais un second moteur). L'UI affiche : sync en cours, succès, erreur significative, hors ligne. Le bouton est désactivé pendant l'opération et un verrou UI (`SemaphoreSlim`) empêche deux clics simultanés de lancer deux cycles UI. ## État de synchronisation Dans le shell, un petit état non intrusif reflète l'état réel du SyncEngine : - synchronisé - synchronisation - hors ligne - erreur / rejet Aucune donnée réseau fictive. Les détails techniques TOSRV/MobReception/MobLog ne sont pas affichés. ## Startup visuel `Loading → Shell` avec un état d'erreur récupérable pendant l'initialisation SQLite, la restauration locale, la vérification bootstrap local et l'éventuelle synchronisation initiale. L'orchestration Startup existante n'est pas réécrite. ## Accessibilité / terrain - zones tactiles suffisamment grandes - lisibilité et contrastes - texte adaptable - navigation utilisable au doigt - pas d'actions critiques uniquement accessibles par hover - focus clavier correct sur desktop/tablette clavier ## Safe area mobile Les safe areas (barre système, encoche, navigation Android/iOS, bottom navigation) sont prises en compte via les APIs Avalonia/platform disponibles. Aucune marge spécifique à un téléphone particulier n'est hardcodée. ## Icônes Réutilisation de la bibliothèque d'icônes déjà présente. Aucun emoji comme icône finale de navigation. Une seule bibliothèque d'icônes. ## Fichiers créés (UI Foundation) - `src/Omika.App/Styles/DesignTokens.axaml` - `src/Omika.App/Styles/Controls.axaml` - `src/Omika.App/Services/ThemeMode.cs` - `src/Omika.App/Services/ThemeService.cs` - `src/Omika.App/Navigation/AppPage.cs` - `src/Omika.App/Navigation/NavigationItem.cs` - `src/Omika.App/Navigation/INavigationService.cs` - `src/Omika.App/Navigation/NavigationService.cs` - `src/Omika.App/Navigation/IPageViewModelFactory.cs` - `src/Omika.App/Navigation/PageViewModelFactory.cs` - `src/Omika.App/ViewModels/ViewModelBase.cs` - `src/Omika.App/ViewModels/SyncUiState.cs` - `src/Omika.App/ViewModels/ShellViewModel.cs` - `src/Omika.App/ViewModels/PlaceholderViewModel.cs` - `src/Omika.App/ViewModels/ParametresViewModel.cs` - `src/Omika.App/Views/ShellView.axaml` (+ code-behind) - `src/Omika.App/Views/PlaceholderPage.axaml` (+ code-behind) - `src/Omika.App/Views/ParametresPage.axaml` (+ code-behind) - `src/Omika.App/Views/MainView.axaml` - `src/Omika.App/Views/NavItem*Converter.cs` - `src/Omika.App/Views/SyncStateToBrushConverter.cs` - `src/Omika.App/AppServices.cs` - `src/Omika.App.Local/Models/UiPreferences.cs` - `src/Omika.App.Local/Abstractions/IUiPreferencesRepository.cs` - `src/Omika.App.Local/Repositories/UiPreferencesRepository.cs` - `src/Omika.App.Local/Services/NetworkConnectivity.cs` - `src/Omika.App.Local/Services/MobileApiClient.cs` ## Fichiers modifiés - `src/Omika.App.Local/Sqlite/SqliteSchema.cs` (table `UiPreferences`) - `src/Omika.App/App.axaml` / `App.axaml.cs` (thème, ressources, DI) - `src/Omika.App/Views/MainView.axaml` (racine Shell) - `tests/Omika.Tests/Omika.Tests.csproj` (référence `Omika.App`) ## Tests ajoutés - `tests/Omika.Tests/Mobile/Local/ThemeServiceTests.cs` : thème clair/sombre, persistance, restauration, couleur principale, fallback config invalide. - `tests/Omika.Tests/Mobile/Local/NavigationServiceTests.cs` : page initiale, sélection, changement de page, fonctions secondaires, aucun appel API. - `tests/Omika.Tests/Mobile/Local/ShellViewModelTests.cs` : navigation, page active, thème, sync UI (appel unique, bouton désactivé, double clic, erreur, offline), aucune identité fictive. --- # DESIGN SYSTEM OMIKA — ORGANISATION DES STYLES ## Principe Le style de l'application est **centralisé** dans deux fichiers de ressources Avalonia. Toute modification visuelle globale (couleur principale, radius, typographie, espacements, formes de boutons) se fait **sans toucher aux Views**. ``` src/Omika.App/Styles/ ├── DesignTokens.axaml → Ressources brutes (couleurs, espacements, radius, │ tailles, breakpoints, typographie) └── Controls.axaml → Styles réutilisables (boutons, cartes, badges, champs, navigation, états, typographie sémantique) ``` ## 1. DesignTokens.axaml — Ressources brutes ### Couleurs sémantiques Light et Dark utilisent les **mêmes clés de ressources**. Le changement de thème est dynamique via `ThemeService` (aucune View à recréer). | Clé | Rôle | |-----|------| | `BackgroundBrush` | Fond de page | | `SurfaceBrush` | Surface de carte / panneau | | `SurfaceSecondaryBrush` | Surface secondaire (fond alterné) | | `SurfaceElevatedBrush` | Surface élevée (cartes avec ombre) | | `SurfaceOverlayBrush` | Surface de survol / overlay | | `TextPrimaryBrush` | Texte principal | | `TextSecondaryBrush` | Texte secondaire | | `TextMutedBrush` | Texte atténué (captions, désactivé) | | `TextOnPrimaryBrush` | Texte sur fond Primary | | `BorderBrush` | Bordure standard | | `BorderStrongBrush` | Bordure forte | | `SuccessBrush` / `SuccessContainerBrush` | Succès | | `WarningBrush` / `WarningContainerBrush` | Avertissement | | `DangerBrush` / `DangerContainerBrush` | Erreur / danger | | `InfoBrush` / `InfoContainerBrush` | Information | | `PrimaryColorBrush` | Couleur principale (pilotée par ThemeService) | | `PrimaryColorDarkBrush` | Primary assombrie (hover) | | `PrimaryColorLightBrush` | Primary éclaircie | | `PrimaryColorContainerBrush` | Conteneur Primary (fond clair) | ### Typographie | Clé | Usage | |-----|-------| | `FontFamilyBase` | Police générale de l'application | | `FontSizeCaption` | 12 — captions, badges | | `FontSizeBodySmall` | 13 — texte secondaire | | `FontSizeBody` | 14 — corps de texte | | `FontSizeButton` | 14 — boutons | | `FontSizeCardTitle` | 16 — titres de cartes | | `FontSizeSectionTitle` | 18 — titres de sections | | `FontSizePageTitle` | 24 — titres de pages | | `FontSizeDisplay` | 28 — affichage / hero | ### Espacements (échelle) | Clé | Valeur | |-----|--------| | `SpaceXXS` | 2 | | `SpaceXS` | 4 | | `SpaceS` | 8 | | `SpaceM` | 12 | | `SpaceL` | 16 | | `SpaceXL` | 24 | | `SpaceXXL` | 32 | Dimensions communes : `ControlHeight` (44), `ControlHeightCompact` (36), `TouchTargetMin` (48), `PagePadding` (24), `CardPadding` (16), `IconSizeS` (16), `IconSizeM` (20), `IconSizeL` (24), `AvatarSize` (36). Thickness réutilisables pour Margin/Padding : `ThicknessXS`, `ThicknessS`, `ThicknessM`, `ThicknessL`, `ThicknessXL`, `ThicknessPage`, `ThicknessCard`. Épaisseurs de bordure : `BorderThicknessThin` (1), `BorderThicknessStrong` (2). ### Radius | Clé | Valeur | Usage | |-----|--------|-------| | `Radius.Small` | 4 | Petits éléments | | `Radius.Control` | 8 | Contrôles (champs, listes) | | `Radius.Button` | 8 | Boutons | | `Radius.Card` | 12 | Cartes | | `Radius.Dialog` | 16 | Dialogues | | `Radius.Full` | 999 | Badges, avatars | ### Breakpoints responsive | Clé | Valeur | |-----|--------| | `BreakpointPhone` | 600 | | `BreakpointTablet` | 960 | La logique responsive dépend de la **largeur disponible** (classes adaptatives Avalonia), pas de Android/iOS directement. Les seuils sont centralisés ici. ## 2. Controls.axaml — Styles réutilisables ### Typographie sémantique | Classe | Usage | |--------|-------| | `TextBlock.pageTitle` | Titre de page | | `TextBlock.sectionTitle` | Titre de section | | `TextBlock.cardTitle` | Titre de carte | | `TextBlock.body` | Corps de texte | | `TextBlock.bodySmall` | Texte secondaire | | `TextBlock.caption` | Caption / texte atténué | | `TextBlock.listTitle` | Titre d'élément de liste (semi-gras) | ### Boutons Variantes : `primary`, `secondary`, `ghost`, `danger`, `icon`. Tailles : `compact` (en plus de la taille normale). | Variante | Usage | |----------|-------| | `Button.primary` | Action principale | | `Button.secondary` | Action secondaire | | `Button.ghost` | Action discrète (navigation) | | `Button.danger` | Action destructive | | `Button.icon` | Bouton icône seule | | `Button.compact` | Taille réduite | ### Autres composants | Classe | Usage | |--------|-------| | `Border.card` | Carte de contenu | | `Border.card.cardElevated` | Carte avec ombre | | `TextBox.field` | Champ de saisie | | `ComboBox.field` | Liste déroulante | | `Border.badge` + `badgeSuccess/Warning/Danger/Info/Primary` | Badge | | `Border.statusBadge` + `statusSuccess/Warning/Danger/Info` | Badge de statut | | `ListBoxItem.listItem` | Élément de liste | | `ListBoxItem.navItem` | Élément de navigation latérale | | `ListBoxItem.bottomNavItem` | Élément de navigation basse | | `StackPanel.pageHeader` | En-tête de page | | `StackPanel.section` | Section de contenu | | `StackPanel.emptyState` | État vide | | `StackPanel.loadingState` | État de chargement | | `Border.errorMessage` | Message d'erreur | | `Border.infoMessage` | Message d'information | ## 3. Règles pour les Views 1. **Toute nouvelle View métier** doit d'abord chercher si un style/composant global existe. 2. **Ne pas répéter** dans les Views des valeurs visuelles telles que `Background="#..."`, `Foreground="#..."`, `FontSize="..."`, `CornerRadius="..."`, `Padding="..."`, `Height="..."` pour les composants standards lorsqu'une ressource ou un style réutilisable peut être utilisé. 3. **Ne créer un style local** que lorsque : - le besoin est réellement spécifique à cet écran ; - aucun style global ne convient ; - la duplication n'est pas justifiée. 4. **Si le même style spécifique apparaît dans deux écrans ou plus** : le déplacer dans les ressources globales. ## 4. DynamicResource Tout ce qui doit changer dynamiquement (Light/Dark, PrimaryColor) utilise `DynamicResource`. Le changement de thème/couleur ne nécessite **pas** de recréer chaque View. ## 5. Validation Une modification centrale simulée de : - `PrimaryColor` (via `ThemeService`) - `Radius.Button` (dans `DesignTokens.axaml`) - `FontSizeBody` (dans `DesignTokens.axaml`) se répercute **sans modification des Views** utilisant les styles correspondants. Les Views ne référencent jamais ces valeurs en dur : elles passent par les styles globaux (`Button.primary`, `TextBlock.body`, etc.) et les `DynamicResource` des design tokens. --- # LOT AGENDA — ÉCRANS MÉTIER RÉELS (Agenda + Détail Rendez-vous) ## Statut Ce lot implémente les **premiers écrans métier réels** de l'application mobile : l'Agenda (liste des rendez-vous d'une journée) et le Détail d'un rendez-vous. Les données proviennent exclusivement du **snapshot local SQLite** (dernier synchronisé) : aucun appel API n'est déclenché par ces écrans. ## Principe - Lecture locale uniquement via `IAgendaService` (abstraction) / `AgendaService` (implémentation SQLite). - Enrichissement des rendez-vous avec les références Client / Site / Intervention disponibles localement. Les relations optionnelles absentes ne provoquent pas d'erreur. - MVVM strict : les Views ne contiennent aucune logique métier ni appel de données ; elles réutilisent exclusivement les styles globaux du Design System (`DesignTokens.axaml` + `Controls.axaml`). ## Navigation avec paramètre `INavigationService.NavigateTo(AppPage page, object? parameter)` a été étendu avec un **paramètre optionnel** (rétrocompatible : l'ancienne surcharge sans paramètre reste disponible). Le paramètre est exposé via `CurrentParameter`. `IPageViewModelFactory` accepte le même paramètre optionnel pour construire le ViewModel de la page cible (ex. `RendezVousDetailViewModel` reçoit le GUID du rendez-vous à afficher). ## Composants créés ### Couche locale (Omika.App.Local) - `Abstractions/IAgendaService.cs` : contrat de lecture de l'agenda. - `Services/AgendaService.cs` : implémentation SQLite (filtrage par journée, tri chronologique, enrichissement Client/Site/Intervention, détail par GUID). - `Models/AgendaItem.cs` : modèle de présentation d'un rendez-vous enrichi. ### Couche présentation (Omika.App) - `ViewModels/AgendaViewModel.cs` : liste du jour, navigation jour précédent / suivant / aujourd'hui, états chargement / vide / erreur, ouverture du détail. - `ViewModels/RendezVousDetailViewModel.cs` : chargement du détail par GUID, retour à l'agenda. - `Views/AgendaView.axaml` (+ code-behind) : liste des rendez-vous du jour. - `Views/RendezVousDetailView.axaml` (+ code-behind) : détail d'un rendez-vous. ### Navigation - `Navigation/AppPage.cs` : ajout de `RendezVousDetail`. - `Navigation/INavigationService.cs` / `NavigationService.cs` : paramètre optionnel de navigation. - `Navigation/IPageViewModelFactory.cs` / `PageViewModelFactory.cs` : paramètre optionnel transmis au ViewModel. - `ViewModels/ShellViewModel.cs` : transmission du paramètre lors de la navigation. ### Injection de dépendances - `AppServices.cs` : enregistrement des repositories `LocalRendezVous`, `LocalClient`, `LocalSite`, `LocalIntervention` et du service `IAgendaService`. ## Fichiers créés (LOT AGENDA) - `src/Omika.App.Local/Abstractions/IAgendaService.cs` - `src/Omika.App.Local/Services/AgendaService.cs` - `src/Omika.App.Local/Models/AgendaItem.cs` - `src/Omika.App/ViewModels/AgendaViewModel.cs` - `src/Omika.App/ViewModels/RendezVousDetailViewModel.cs` - `src/Omika.App/Views/AgendaView.axaml` (+ code-behind) - `src/Omika.App/Views/RendezVousDetailView.axaml` (+ code-behind) ## Fichiers modifiés (LOT AGENDA) - `src/Omika.App/Navigation/AppPage.cs` (ajout `RendezVousDetail`) - `src/Omika.App/Navigation/INavigationService.cs` (paramètre optionnel) - `src/Omika.App/Navigation/NavigationService.cs` (implémentation paramètre) - `src/Omika.App/Navigation/IPageViewModelFactory.cs` (paramètre optionnel) - `src/Omika.App/Navigation/PageViewModelFactory.cs` (paramètre optionnel) - `src/Omika.App/ViewModels/ShellViewModel.cs` (transmission du paramètre) - `src/Omika.App/AppServices.cs` (repositories + service Agenda) - `tests/Omika.Tests/Mobile/Local/LocalTestBase.cs` (exposition `SiteRepository`) ## Tests ajoutés (LOT AGENDA) - `tests/Omika.Tests/Mobile/Local/AgendaServiceTests.cs` : journée sans rendez-vous, filtrage par journée, tri chronologique, enrichissement Client/Site/Intervention, rendez-vous annulé, détail par GUID, introuvable, payload invalide ignoré. - `tests/Omika.Tests/Mobile/Local/AgendaViewModelTests.cs` : chargement, état vide, navigation jour précédent/suivant, retour à aujourd'hui, libellé de date, ouverture du détail (navigation avec paramètre), paramètre null ignoré. ## Conformité Design System Les nouvelles Views (`AgendaView`, `RendezVousDetailView`) réutilisent exclusivement les styles globaux : `pageHeader`, `pageTitle`, `pageSubtitle`, `section`, `sectionTitle`, `body`, `bodySmall`, `caption`, `card`, `badge`, `errorMessage`, `ghost`, `compact`. Aucune valeur visuelle (couleur hex, FontSize, CornerRadius, Padding, Height) n'est répétée dans ces Views : tout passe par les ressources/styles centraux. Une modification centrale de `PrimaryColor`, `Radius.Control` ou de la taille typographique principale se répercute sans toucher à ces écrans. --- # LOT FICHE INTERVENTION — ÉCRAN MÉTIER RÉEL ## Statut Ce lot implémente la **Fiche Intervention** : un écran métier réel qui affiche le détail d'une intervention chargée depuis le **snapshot local SQLite** (aucun appel API). La fiche est enrichie avec les relations Client / Site / Dossier / Rendez-vous / Documents disponibles localement. Les relations optionnelles absentes ne provoquent pas d'erreur. ## Principe - Lecture locale uniquement via `IInterventionLocalService` (abstraction) / `InterventionLocalService` (implémentation SQLite). - Enrichissement de la fiche avec les références Client / Site / Dossier / Rendez-vous / Documents disponibles localement. - MVVM strict : la View ne contient aucune logique métier ni appel de données ; elle réutilise exclusivement les styles globaux du Design System. - Navigation bidirectionnelle : Agenda → Détail Rendez-vous → Fiche Intervention (et retour). Depuis la fiche, un rendez-vous lié peut être rouvert. ## Composants créés ### Couche locale (Omika.App.Local) - `Abstractions/IInterventionLocalService.cs` : contrat de lecture de la fiche. - `Services/InterventionLocalService.cs` : implémentation SQLite (détail par GUID, enrichissement Client/Site/Dossier/Rendez-vous/Documents). - `Models/InterventionDetail.cs` : modèle de présentation d'une intervention enrichie (+ `InterventionRendezVous`, `InterventionDocument`). ### Couche présentation (Omika.App) - `ViewModels/InterventionDetailViewModel.cs` : chargement par GUID, indicateurs de relations (`HasClient`, `HasSite`, `HasDossier`, `HasRendezVous`, `HasDocuments`), ouverture d'un rendez-vous lié, retour à l'agenda. - `Views/InterventionDetailView.axaml` (+ code-behind) : fiche intervention. ### Navigation - `Navigation/AppPage.cs` : ajout de `InterventionDetail`. - `Navigation/PageViewModelFactory.cs` : cas `InterventionDetail` (injection du service + paramètre GUID). - `ViewModels/RendezVousDetailViewModel.cs` : ajout de `HasIntervention`, `InterventionReference` et de la commande `OpenIntervention` (navigation vers la fiche). - `Views/RendezVousDetailView.axaml` : bouton "Ouvrir l'intervention" visible uniquement si le rendez-vous est lié à une intervention. ### Injection de dépendances - `AppServices.cs` : enregistrement des repositories `LocalDossier`, `LocalDocumentMetadata` et du service `IInterventionLocalService`. ## Fichiers créés (LOT FICHE INTERVENTION) - `src/Omika.App.Local/Abstractions/IInterventionLocalService.cs` - `src/Omika.App.Local/Services/InterventionLocalService.cs` - `src/Omika.App.Local/Models/InterventionDetail.cs` - `src/Omika.App/ViewModels/InterventionDetailViewModel.cs` - `src/Omika.App/Views/InterventionDetailView.axaml` (+ code-behind) ## Fichiers modifiés (LOT FICHE INTERVENTION) - `src/Omika.App/Navigation/AppPage.cs` (ajout `InterventionDetail`) - `src/Omika.App/Navigation/PageViewModelFactory.cs` (cas `InterventionDetail`) - `src/Omika.App/ViewModels/RendezVousDetailViewModel.cs` (action ouvrir intervention) - `src/Omika.App/Views/RendezVousDetailView.axaml` (bouton intervention) - `src/Omika.App/AppServices.cs` (repositories + service Intervention) - `tests/Omika.Tests/Mobile/Local/LocalTestBase.cs` (exposition `DossierRepository`, `DocumentMetadataRepository`) - `tests/Omika.Tests/Mobile/Local/ShellViewModelTests.cs` (injection du service Intervention dans la factory) ## Tests ajoutés (LOT FICHE INTERVENTION) - `tests/Omika.Tests/Mobile/Local/InterventionLocalServiceTests.cs` : fiche introuvable, fiche simple, enrichissement Client/Site/Dossier, rendez-vous liés triés chronologiquement, documents liés, relations absentes sans erreur, payload invalide. - `tests/Omika.Tests/Mobile/Local/InterventionDetailViewModelTests.cs` : chargement, état introuvable, indicateurs de relations, ouverture d'un rendez-vous lié (navigation avec paramètre), paramètre null ignoré, retour. - `tests/Omika.Tests/Mobile/Local/RendezVousDetailViewModelTests.cs` : indicateur d'intervention liée, navigation RDV → Fiche Intervention, absence d'intervention sans navigation, retour. ## Conformité Design System La nouvelle View (`InterventionDetailView`) réutilise exclusivement les styles globaux : `pageHeader`, `pageTitle`, `section`, `sectionTitle`, `body`, `bodySmall`, `caption`, `card`, `badge`, `errorMessage`, `ghost`, `secondary`, `compact`. Aucune valeur visuelle (couleur hex, FontSize, CornerRadius, Padding, Height) n'est répétée : tout passe par les ressources/styles centraux. Une modification centrale de `PrimaryColor`, `Radius.Control` ou de la taille typographique principale se répercute sans toucher à cet écran. # LOT DOCUMENTS / PHOTOS MOBILE ## Statut Ce lot ajoute la consultation, le téléchargement à la demande, l'ouverture offline et l'ajout offline-first des documents / photos d'une intervention. ## Métadonnées depuis SQLite L'ouverture de l'écran Documents fonctionne **uniquement** depuis SQLite (snapshot). Aucun appel réseau automatique. Les métadonnées réellement disponibles sont lues depuis `LocalDocumentMetadata` (payload `DocumentSummaryDto`) : `DocumentGuid`, nom, titre, catégorie, MIME type, extension, taille, date de création, `InterventionGuid`. Aucune propriété absente n'est supposée. ## Cache fichier local Le contenu binaire reste dans le filesystem local (répertoire contrôlé par l'application). SQLite stocke uniquement les informations techniques du cache (`DocumentCacheEntry` : chemin local, nom, MIME, taille, état). Le cache survit au refresh snapshot : après un refresh, un document serveur retrouve son cache grâce au `DocumentGuid`. ## Téléchargement à la demande Un téléchargement = un document, strictement à la demande. Ouvrir l'écran ne télécharge rien. Si le fichier est déjà en cache, aucun HTTP. Si hors ligne, aucun HTTP et la métadonnée reste visible. ## Ouverture offline Si le fichier est présent localement, `Ouvrir` ouvre la copie locale via l'abstraction plateforme `IFileLauncher` (aucun code Android/iOS dans le ViewModel). Le ViewModel ne connaît pas le chemin physique. ## Ajout photo / document offline-first Flux obligatoire : capture/sélection → copie fichier locale → création métadonnée locale → création Outbox → UI mise à jour immédiatement. La connexion réseau n'est pas nécessaire. Le fichier est copié dans un espace contrôlé par l'application (aucune référence fragile vers un temp file ou un content URI). ## Outbox L'Outbox existante est réutilisée (aucun mécanisme parallèle). Le payload (`DocumentOutboxPayload`) ne contient **pas** le fichier en Base64 : il contient `DocumentGuid`, `InterventionGuid`, métadonnée utile et une référence locale persistante vers le fichier (survit au redémarrage). ## DocumentGuid vs OperationId `DocumentGuid` = identité métier publique du document. `OperationId` = identité de l'opération de synchronisation. Ils sont **toujours différents**. `OperationId` est généré une seule fois et conservé pendant les retries. ## Comportement SUCCESS Après SUCCESS : métadonnée locale conservée, fichier cache conservé, Outbox mise à jour, aucun fichier supprimé automatiquement, aucun nouveau `DocumentGuid` généré. ## Comportement REJECTED Après REJECTED : document local conservé, fichier local conservé, Outbox conservée avec état Rejected, erreur nettoyée disponible au ViewModel, badge / état visuel possible. Un fichier rejeté n'est jamais supprimé automatiquement. ## Fichier local manquant avant upload Si une opération Outbox Document existe mais que le fichier local a disparu, l'upload n'est pas prétendu réussi : l'opération est mise dans un état d'erreur/rejet local cohérent et un message exploitable est affiché sans exposer de chemin système. ## Purge cache ≠ suppression métier La purge du cache (suppression des fichiers téléchargés) est **distincte** de la suppression du document métier. Aucun DELETE métier n'est émis. Les fichiers nécessaires à une Outbox Pending/Rejetée ne sont jamais supprimés. Le bouton de purge n'est pas branché dans cette tâche tant que la protection n'est pas fiable. ## Design System réutilisé La View Documents réutilise exclusivement les styles globaux : `card`, `listTitle`, `body`, `bodySmall`, `caption`, `badge`, `primary`, `secondary`, `ghost`, `errorMessage`, `emptyState`. Aucune couleur hexadécimale, aucun breakpoint local supplémentaire, aucun FontSize/CornerRadius/Padding standard répété. Les espacements passent par `DynamicResource Space*` et `ThicknessPage`. ## Fichiers créés (LOT DOCUMENTS / PHOTOS) - `src/Omika.App.Local/Abstractions/IDocumentLocalService.cs` - `src/Omika.App.Local/Abstractions/IFileLauncher.cs` - `src/Omika.App.Local/Abstractions/IFilePicker.cs` - `src/Omika.App.Local/Models/InterventionDocumentItem.cs` - `src/Omika.App.Local/Models/DocumentOutboxPayload.cs` - `src/Omika.App.Local/Services/DocumentLocalService.cs` - `src/Omika.App/Services/PlatformFileLauncher.cs` - `src/Omika.App/Services/PlatformFilePicker.cs` - `src/Omika.App/ViewModels/InterventionDocumentsViewModel.cs` - `src/Omika.App/Views/InterventionDocumentsView.axaml` (+ code-behind) - `tests/Omika.Tests/Mobile/Local/DocumentLocalServiceTests.cs` - `tests/Omika.Tests/Mobile/Local/DocumentSyncTests.cs` - `tests/Omika.Tests/Mobile/Local/InterventionDocumentsViewModelTests.cs` - `tests/Omika.Tests/Mobile/Local/DocumentDesignSystemTests.cs` ## Fichiers modifiés (LOT DOCUMENTS / PHOTOS) - `src/Omika.App.Local/Abstractions/IOutboxRepository.cs` (ajout `GetRejectedAsync`) - `src/Omika.App.Local/Repositories/OutboxRepository.cs` (implémentation `GetRejectedAsync`) - `src/Omika.App.Local/Services/DocumentLocalService.cs` (lecture des états Rejected) - `src/Omika.App/Navigation/AppPage.cs` (ajout `InterventionDocuments`) - `src/Omika.App/Navigation/PageViewModelFactory.cs` (cas `InterventionDocuments`) - `src/Omika.App/ViewModels/InterventionDetailViewModel.cs` (navigation vers Documents) - `src/Omika.App/Views/InterventionDetailView.axaml` (accès Documents réel) - `src/Omika.App/AppServices.cs` (enregistrement du service Documents) - `tests/Omika.Tests/Mobile/Local/ShellViewModelTests.cs` (injection du service Documents dans la factory) - `tests/Omika.Tests/Mobile/Local/InterventionDetailViewModelTests.cs` (navigation Intervention → Documents) ## Tests ajoutés (LOT DOCUMENTS / PHOTOS) - `DocumentLocalServiceTests` : liste (documents d'une intervention, aucun document, ordre cohérent, DocumentGuid conservé, metadata invalide ignorée, aucun téléchargement automatique, distinction photo/document), cache (déjà en cache sans HTTP, non cache + online téléchargé, non cache + offline sans HTTP, entrée cache fichier absent invalide, snapshot refresh cache conservé), ouverture offline, ajout (photo/document offline, DocumentGuid généré, OperationId distinct, fichier copié, metadata visible, Outbox créée, redémarrage exploitable, SUCCESS/REJECTED fichier conservé, source temporaire supprimée). - `DocumentSyncTests` : Outbox Document Pending → upload, OperationId conservé au retry, DocumentGuid conservé, fichier manquant sans faux SUCCESS, offline opération conservée, serveur REJECTED état conservé. - `InterventionDocumentsViewModelTests` : chargement, états visuels, actions télécharger/ouvrir/ajouter, navigation. - `DocumentDesignSystemTests` : aucune couleur hexadécimale standard, aucun breakpoint local supplémentaire, réutilisation des styles sémantiques, espacements via DynamicResource. --- # LOT CONTACTS INTERVENTION ## Statut Ce lot implémente la consultation des **contacts réellement liés à une intervention**, entièrement depuis le **snapshot local SQLite** (aucun appel API). Il permet d'ouvrir le détail d'un contact (lecture seule) et de préparer des actions téléphone / email via les abstractions plateforme. **Aucune modification d'écriture** (création / modification / suppression de contact) n'est développée dans ce lot. ## Relation réellement utilisée La relation utilisée est la liste explicite **`ContactIds`** portée par le DTO d'intervention (`InterventionDetailsDto`), résolue vers les Personnes / Contacts stockées localement (table `Personne`, payload `ContactDto`). - **Aucun contact n'est déduit du Client** : seule la relation explicite Intervention → Contact est utilisée. - Un contact du même Client mais non listé dans `ContactIds` n'est **jamais** associé à l'intervention. - Les identités publiques (`ContactIds` sous forme de GUID) sont résolues vers les `LocalPersonne` par leur `Id` (GUID canonique local). ## Source SQLite - Table `Intervention` (payload `InterventionDetailsDto`) : porte `ContactIds`. - Table `Personne` (payload `ContactDto`) : porte les données du contact. ## Identité publique Contact L'identité publique du contact est le **GUID canonique local** (`LocalPersonne.Id`, `ContactDto.Id`). Aucun PK HFSQL n'est utilisé ni transporté dans la navigation. ## Champs réellement disponibles Seules les propriétés réellement alimentables depuis le payload `ContactDto` sont exposées (`InterventionContact`) : - `ContactGuid` (identité publique) - `DisplayName` (nom affiché, sinon construit prénom + nom) - `LastName`, `FirstName`, `Civilite` - `Email`, `Phone`, `Mobile`, `Fax` (si présents dans `Coordonnees`) - `Roles` (libellés des rôles/fonctions, si présents) - `ClientGuid` (identité publique du client parent, si présente) - `Actif`, `EstArchive` Aucune propriété fictive (fonction, service, portable, commentaire) n'est créée. ## Fonctionnement offline L'écran Contacts et le détail Contact fonctionnent **entièrement offline** : navigation Intervention → Contacts → ContactDetail sans aucun HTTP. Les actions Appeler / Email sont des actions plateforme locales et ne modifient pas la stratégie de synchronisation. ## Téléphone / email plateforme - `IPhoneLauncher` / `PlatformPhoneLauncher` : ouvre le composeur système via un URI `tel:` (Avalonia `TopLevel.Launcher`). - `IEmailLauncher` / `PlatformEmailLauncher` : ouvre le client email système via un URI `mailto:`. - Aucun code Android/iOS dans les ViewModels. Aucune donnée personnelle n'est loguée. Le numéro / l'adresse métier n'est jamais modifié. ## Read-only Ce lot est **lecture seule**. Aucune Outbox n'est créée pour modifier, ajouter ou supprimer un contact. Les fonctions d'édition viendront plus tard si le besoin métier est confirmé. ## Donnée manquante éventuelle La relation Intervention → Contact est **explicitement disponible** dans le snapshot (`ContactIds`). Aucune donnée manquante ne bloque ce lot. Les coordonnées d'un contact peuvent être absentes du payload : dans ce cas, les actions téléphone / email sont simplement indisponibles (commandes désactivées). ## Réutilisation Design System Les Views Contacts et Détail Contact réutilisent exclusivement les styles globaux : `card`, `listTitle`, `body`, `bodySmall`, `caption`, `badge`, `badgeInfo`, `primary`, `secondary`, `ghost`, `emptyState`, `errorMessage`, `loadingState`. Aucune couleur hexadécimale, aucun breakpoint local supplémentaire, aucun FontSize / CornerRadius / Padding standard répété. Les espacements passent par `DynamicResource Space*` et `ThicknessPage`. ## Fichiers créés (LOT CONTACTS INTERVENTION) - `src/Omika.App.Local/Abstractions/IInterventionContactsService.cs` - `src/Omika.App.Local/Abstractions/IPhoneLauncher.cs` - `src/Omika.App.Local/Abstractions/IEmailLauncher.cs` - `src/Omika.App.Local/Models/InterventionContact.cs` - `src/Omika.App.Local/Services/InterventionContactsService.cs` - `src/Omika.App/Services/PlatformPhoneLauncher.cs` - `src/Omika.App/Services/PlatformEmailLauncher.cs` - `src/Omika.App/Navigation/ContactDetailNavigation.cs` - `src/Omika.App/ViewModels/InterventionContactsViewModel.cs` - `src/Omika.App/ViewModels/ContactDetailViewModel.cs` - `src/Omika.App/Views/InterventionContactsView.axaml` (+ code-behind) - `src/Omika.App/Views/ContactDetailView.axaml` (+ code-behind) - `tests/Omika.Tests/Mobile/Local/InterventionContactsServiceTests.cs` - `tests/Omika.Tests/Mobile/Local/InterventionContactsViewModelTests.cs` - `tests/Omika.Tests/Mobile/Local/ContactDetailViewModelTests.cs` ## Fichiers modifiés (LOT CONTACTS INTERVENTION) - `src/Omika.App/Navigation/AppPage.cs` (ajout `InterventionContacts`, `ContactDetail`) - `src/Omika.App/Navigation/PageViewModelFactory.cs` (cas `InterventionContacts`, `ContactDetail`) - `src/Omika.App/ViewModels/InterventionDetailViewModel.cs` (navigation vers Contacts) - `src/Omika.App/Views/InterventionDetailView.axaml` (accès Contacts réel) - `src/Omika.App/AppServices.cs` (enregistrement du service Contacts + launchers) - `tests/Omika.Tests/Mobile/Local/LocalTestBase.cs` (exposition `PersonneRepository`) - `tests/Omika.Tests/Mobile/Local/InterventionDetailViewModelTests.cs` (navigation Intervention → Contacts) - `tests/Omika.Tests/Mobile/Local/ShellViewModelTests.cs` (adaptation du constructeur `PageViewModelFactory` aux nouveaux paramètres Contacts/launchers) ## Tests ajoutés (LOT CONTACTS INTERVENTION) - `InterventionContactsServiceTests` : relation (Intervention A retourne uniquement ses contacts, Intervention B ne retourne pas les contacts de A, contact sans relation non inclus, aucune association basée sur le Client, GUID public conservé), service (un contact, plusieurs contacts triés, aucun contact, contact incomplet, téléphone absent, email absent, relation invalide ignorée, intervention inconnue, offline sans appel API, détail par GUID, payload invalide ignoré). - `InterventionContactsViewModelTests` : chargement, liste chargée, état vide, erreur, ouverture du détail (navigation avec paramètre), paramètre null ignoré, retour. - `ContactDetailViewModelTests` : chargement, contact introuvable, commande téléphone disponible / indisponible, commande email disponible / indisponible, retour à la liste des contacts. - `InterventionDetailViewModelTests` : navigation Intervention → Contacts, intervention null sans navigation. --- # LOT NOTES INTERVENTION ## Statut Ce lot visait à développer l'écran **Notes d'une Intervention** (liste, ajout, édition éventuelle) selon le modèle métier réel. Après inspection ciblée du modèle métier, des contrats, du snapshot et du schéma SQLite local, la conclusion est **CAS E** : **aucune donnée Note Intervention exploitable n'existe dans les contrats / snapshot locaux**. Conformément à la règle du CAS E, **l'implémentation métier est stoppée** : aucun écran, aucun service, aucune fausse fonctionnalité n'est créée. Le backend n'est pas modifié. ## Représentation métier réelle d'une Note Intervention Il n'existe **pas** d'entité métier « Note Intervention » autonome exposée au mobile. Les candidats inspectés sont : | Candidat | Existence | Exposé au mobile ? | Conclusion | |----------|-----------|--------------------|------------| | `CompteRendu` (entité canonique + `CompteRenduDto`) | Oui (domaine + contrat) | `InterventionDetailsDto.ComptesRendus` | **Toujours vide** : le repository `GetDetailsAsync` (Omika/Batik) ne peuple jamais `ComptesRendus`. Aucun `AddCompteRendu` dans les repositories. | | `MessageInterne` (table HFSQL) | Oui (HFSQL) | Non | Absent du contrat mobile, du snapshot et du sync. | | `IN_NoteInterne` (colonne Intervention) | Oui (HFSQL) | Non | Marqué « ignoré » dans les mappers (`AddIgnored`). Absent du contrat mobile. | | `DocumentNote` / `NoteDocument` | Oui (HFSQL + domaine) | Non (métadonnées document uniquement) | Concerne **uniquement les Documents** (`DocumentId`). Distinct des notes d'intervention. | ## Différence avec DocumentNote `DocumentNote` / `NoteDocument` est une note **rattachée à un document** (`DocumentId`). Ce n'est **pas** une note d'intervention. Il n'est pas utilisé comme remplacement : le CAS D s'applique (ne pas détourner DocumentNote). ## Relation Note ↔ Intervention Aucune relation exploitable n'existe dans le contrat mobile. La table HFSQL `MessageInterne` porte `MI_IDIntervention` (relation physique), mais elle n'est **pas** exposée dans le snapshot ni dans le sync. `CompteRendu` porte `CR_IDIntervention` mais n'est jamais peuplé par les repositories. ## Champs réellement disponibles Aucun champ de note d'intervention n'est disponible dans le snapshot local. Les champs de `CompteRenduDto` (`Id`, `InterventionId`, `AuteurId`, `Contenu`, `DateCreation`, `DateInterventionDebut/Fin`, `SignataireIds`, `StatutValidation`, `EstArchive`) existent dans le contrat mais restent **toujours vides** car le repository ne les peuple pas. ## Source SQLite Le schéma SQLite local (`SqliteSchema`) ne contient **aucune table Note**. Les tables métier sont : `Client`, `Personne`, `Site`, `Intervention`, `Dossier`, `RendezVous`, `Collaborateur`, `DocumentMetadata`. Aucune table technique ne porte de note d'intervention. ## Lecture offline Non implémentée : aucune donnée Note Intervention n'est disponible dans SQLite. Aucun écran Notes n'est créé. ## CREATE / UPDATE réellement supportés ou non Non supportés. `SyncEntityTypes.Supported` ne contient **pas** de type « Note ». `InterventionSyncPayload` ne porte aucun champ note. Aucun handler Sync Note n'existe. Aucune création / édition offline n'est possible. ## NoteGuid vs OperationId Sans objet : aucune entité Note autonome n'existe. Aucun `NoteGuid` n'est fabriqué artificiellement pour l'UI. ## Outbox Non utilisée pour les notes : aucun type d'entité Note n'est déclaré dans `SyncEntityTypes`. L'Outbox existante reste inchangée. ## Réconciliation avec Intervention Sans objet : aucune Note ne référence une Intervention dans le contrat mobile. ## Comportement Rejected Sans objet : aucune opération Note n'est émise. ## Design System Non utilisé pour un écran Notes (aucun écran créé). Le Design System existant (`DesignTokens.axaml`, `Controls.axaml`) reste inchangé. ## Différences avec DocumentNote - `DocumentNote` / `NoteDocument` : note rattachée à un **Document** (`DocumentId`). - Note Intervention : **n'existe pas** dans le contrat mobile. - `DocumentNote` n'est **jamais** utilisé comme note d'intervention. ## Donnée manquante et évolution backend minimale nécessaire Pour rendre les notes d'intervention exploitables au mobile, il faudrait plus tard (hors périmètre de ce lot, sans modification HFSQL) : 1. **Exposer une entité Note Intervention dans le contrat mobile** : soit peupler réellement `InterventionDetailsDto.ComptesRendus` (ou une collection dédiée) dans les repositories `GetDetailsAsync` (Omika/Batik), soit exposer `MessageInterne` dans le snapshot. 2. **Déclarer un type d'entité** dans `SyncEntityTypes` (ex. `Note`) si la création / édition offline est souhaitée. 3. **Ajouter un handler Sync** pour le type Note (CREATE / UPDATE) et un payload dédié. 4. **Ajouter une table SQLite locale** pour les notes (métier ou technique). Aucune de ces évolutions n'est réalisée dans ce lot : le backend n'est pas modifié. ## Fichiers créés Aucun (CAS E : pas de fausse fonctionnalité). ## Fichiers modifiés - `docs/LOT_MOBILE_0.md` (documentation de ce lot). ## Tests ajoutés Aucun (CAS E : pas de fonctionnalité à tester). La suite unitaire existante reste inchangée. ## Validation - Suite unitaire : inchangée (aucun code ajouté). - Build solution : inchangé. - Build Android : inchangé. - Aucun backend / HFSQL modifié. --- # LOT SUIVI DES TEMPS ## Statut Ce lot visait à développer le **suivi des temps** (saisie, historique, éventuel chronomètre) selon le modèle métier réel. Après inspection ciblée du modèle métier, des contrats, du snapshot, du schéma SQLite local et de la couche Sync, la conclusion est **CAS C** : **il existe une entité métier réelle `SuiviTemps`, mais elle n'est disponible que côté backend et pas dans le snapshot / local SQLite**. Conformément à la règle du CAS C, **l'implémentation mobile est stoppée** : aucun écran, aucun service, aucune fausse fonctionnalité n'est créée. Le backend n'est pas modifié. ## Représentation métier réelle du temps Il existe une **entité métier canonique autonome** `SuiviTemps` (`Omika.Core/Domain/Entities/Planning/SuiviTemps.cs`), avec relations explicites vers Intervention, RendezVous et Collaborateur. C'est structurellement un **CAS A**, mais l'entité n'est **pas exposée au mobile** (CAS C). | Candidat | Existence | Exposé au mobile ? | Conclusion | |----------|-----------|--------------------|------------| | `SuiviTemps` (entité canonique) | Oui (`Omika.Core/Domain/Entities/Planning/SuiviTemps.cs`) | **Non** | Absent du snapshot, du sync, du schéma SQLite local. | | `SuiviTempsDto` (contrat) | Oui (`Omika.Core/Contracts/Planning/SuiviTempsDto.cs`) | **Non** | Jamais retourné par un service ni un endpoint. | | `CreateSuiviTempsRequest` (contrat) | Oui (`Omika.Core/Contracts/Planning/CreateSuiviTempsRequest.cs`) | **Non** | Jamais utilisé par un service ni un endpoint. | | `RendezVousContractMapper.ToSuiviTemps` | Oui (mapper) | **Non** | Défini mais **jamais appelé** par aucun service. | | `SuiviDesTempsRow` (Batik / Omika) | Oui (`Omika.Data/HfSql/Models/`) | **Non** | Modèles physiques HFSQL uniquement. Aucun repository ne les utilise. | | `RendezVous` (entité) | Oui | Oui (snapshot) | Porte `TypeActivite` (type d'activité du RDV), pas un temps saisi. | | `AffectationCollaborateur.TempsValide` | Oui | Oui (snapshot) | Booléen de validation d'affectation, **pas** un temps saisi. | ## Relation Temps ↔ Intervention L'entité `SuiviTemps` porte `InterventionId` (relation explicite). Mais cette relation n'est **pas exploitable au mobile** : `SuiviTemps` n'est pas dans le snapshot, ni dans le sync, ni dans SQLite local. Aucune donnée de temps liée à une Intervention n'est disponible localement. ## Relation Temps ↔ RendezVous L'entité `SuiviTemps` porte `RendezVousId` (relation explicite). Même constat : non exposée au mobile. Aucune donnée de temps liée à un RendezVous n'est disponible localement. ## Relation Temps ↔ Collaborateur L'entité `SuiviTemps` porte `CollaborateurId` (relation explicite). Non exposée au mobile. Le collaborateur authentifié/bootstrap est disponible localement (`LocalBootstrap`), mais il n'y a **aucune donnée de temps** à lui associer. ## Champs réellement disponibles Les champs de l'entité `SuiviTemps` (et du `SuiviTempsDto`) sont : - `Id` (identité publique) - `CollaborateurId` - `InterventionId` - `RendezVousId` - `Date` - `DureeMinutes` (durée en minutes) - `TypeTemps` (paramètre configurable, `ReferenceValue`) - `Description` - `Valide` Ces champs existent dans le contrat mais restent **inaccessibles au mobile** : aucun service ne les retourne, aucun endpoint ne les expose, aucun snapshot ne les transporte. ## Source SQLite Le schéma SQLite local (`SqliteSchema`) ne contient **aucune table SuiviTemps**. Les tables métier sont : `Client`, `Personne`, `Site`, `Intervention`, `Dossier`, `RendezVous`, `Collaborateur`, `DocumentMetadata`. Aucune table technique ne porte de suivi des temps. ## Identité publique L'entité `SuiviTemps` possède une identité publique (`Id`, GUID). Mais comme l'entité n'est pas exposée au mobile, aucun `TimeEntryGuid` n'est fabriqué artificiellement pour l'UI. Aucun `OperationId` de temps n'est généré. ## Lecture offline Non implémentée : aucune donnée de temps n'est disponible dans SQLite. Aucun écran Suivi des Temps n'est créé. ## Création offline supportée ou non Non supportée. `SyncEntityTypes.Supported` (backend et mobile) ne contient **pas** de type « SuiviTemps ». Aucun handler Sync SuiviTemps n'existe (seuls `DefaultSyncOperationHandler` et `InterventionSyncOperationHandler` sont enregistrés). Aucune création offline n'est possible. ## Édition supportée ou non Non supportée. Aucun UPDATE SuiviTemps n'existe dans la couche Sync/métier. ## Suppression / archivage supporté ou non Non supporté. Aucune sémantique de suppression/archivage SuiviTemps n'est exposée au mobile. ## Chronomètre Non développé. Aucun chronomètre n'est implémenté : le modèle métier réel (`SuiviTemps`) n'est pas accessible au mobile, et aucun concept de démarrage/arrêt n'est exposé. ## Identité métier vs OperationId Sans objet : aucune entité SuiviTemps autonome n'est exposée au mobile. Aucun `TimeEntryGuid` ni `OperationId` de temps n'est fabriqué artificiellement. ## Outbox Non utilisée pour le suivi des temps : aucun type d'entité SuiviTemps n'est déclaré dans `SyncEntityTypes`. L'Outbox existante reste inchangée. ## Réconciliation avec Intervention / RendezVous Sans objet : aucune donnée de temps ne référence une Intervention ou un RendezVous dans le contrat mobile. `MobileIdentityReconciliationService` reste inchangé (aucun second mécanisme). ## Comportement SUCCESS Sans objet : aucune opération SuiviTemps n'est émise. ## Comportement REJECTED Sans objet : aucune opération SuiviTemps n'est émise. ## ParametreMetier L'entité `SuiviTemps` référence un `TypeTemps` (paramètre configurable, `ReferenceValue`). Le `ParametreMetier` existe dans le domaine (`Omika.Core/Domain/Entities/Configuration/ParametreMetier.cs`) et est utilisé pour le profil Batik. Mais **aucun type de temps n'est exposé au mobile** : le snapshot ne transporte pas de liste de types de temps, et `SuiviTemps` n'est pas dans le sync. Aucune liste fictive de catégories n'est créée. ## Navigation depuis Intervention / RendezVous Non implémentée : aucune navigation vers un écran Suivi des Temps n'est ajoutée depuis `InterventionDetail` ou `RendezVousDetail`. Aucun `AppPage` SuiviTemps n'est créé. ## Smartphone / Tablette Non implémenté : aucun écran Suivi des Temps n'est créé. ## Design System Non utilisé pour un écran Suivi des Temps (aucun écran créé). Le Design System existant (`DesignTokens.axaml`, `Controls.axaml`) reste inchangé. ## Donnée manquante et évolution backend minimale nécessaire Pour rendre le suivi des temps exploitable au mobile, il faudrait plus tard (hors périmètre de ce lot, sans modification HFSQL) : 1. **Exposer `SuiviTemps` dans le snapshot mobile** : ajouter une collection `SuiviTemps` dans `MobileSnapshotResponse`, la charger dans `MobileSnapshotService` (via un service/repository SuiviTemps à créer), et l'appliquer dans `SnapshotApplier` (table SQLite locale `SuiviTemps`). 2. **Déclarer un type d'entité** dans `SyncEntityTypes` (ex. `SuiviTemps`) si la création / édition offline est souhaitée. 3. **Ajouter un handler Sync** pour le type SuiviTemps (CREATE / UPDATE) et un payload dédié. 4. **Créer un service métier SuiviTemps** (`ISuiviTempsService`) et un repository adaptatif (Batik/Omika) qui peuple réellement les données depuis HFSQL. 5. **Ajouter une table SQLite locale** pour les temps (métier ou technique). Aucune de ces évolutions n'est réalisée dans ce lot : le backend n'est pas modifié. ## Fichiers créés Aucun (CAS C : pas de fausse fonctionnalité). ## Fichiers modifiés - `docs/LOT_MOBILE_0.md` (documentation de ce lot). ## Tests ajoutés Aucun (CAS C : pas de fonctionnalité à tester). La suite unitaire existante reste inchangée. ## Validation - Suite unitaire : inchangée (aucun code ajouté). - Build solution : inchangé. - Build Android : inchangé. - Aucun backend / HFSQL modifié. --- # LOT DOSSIERS MOBILE ## Statut Ce lot développe l'écran **Dossiers** (liste + détail) entièrement depuis les données locales SQLite (dernier snapshot). Le lot est **lecture seule** : aucune création, modification, suppression ou archivage de dossier n'est implémenté. Aucune Outbox Dossier n'est créée. Le backend / HFSQL n'est pas modifié. ## Source SQLite Les données affichées proviennent exclusivement du **snapshot local SQLite** (tables `Dossier`, `Client`, `Intervention`). Aucun appel API n'est déclenché à l'ouverture de `Dossiers`, `DossierDetail` ou `InterventionDetail`. La synchronisation reste exclusivement gérée par le `SyncEngine` existant. ## Champs réellement disponibles Le modèle d'affichage est construit uniquement à partir des données réellement présentes dans le payload `DossierDetailsDto` (snapshot local) : - `ReferenceMetier` (référence métier) - `Titre` (libellé) - `Description` - `ClientId` (relation client, résolue localement) - `Type` / `Statut` / `Priorite` (libellés `ReferenceValueDto`) - `DateOuverture` / `DateCloture` - `EstArchive` Aucun champ n'est créé parce qu'il apparaît dans la maquette. Les relations optionnelles absentes restent nulles/vides sans provoquer d'erreur. ## Identité publique L'identité utilisée est exclusivement le **GUID public** local (`DossierGuid`, `InterventionGuid`, `ClientGuid`). Aucun PK HFSQL n'est utilisé. Navigation : - `Dossiers` → `DossierDetail(DossierGuid)` - `DossierDetail` → `InterventionDetail(InterventionGuid)` ## Relation Dossier → Intervention La relation principale est portée par **`DossierId` sur l'intervention** (table `Intervention`). Aucune relation n'est déduite du Client. Le détail d'un dossier affiche uniquement les interventions dont `DossierId` correspond au dossier courant. Les interventions d'un autre dossier sont exclues. ## Client associé Si le dossier possède un `ClientId`, le client est résolu localement (table `Client`) et son nom / code sont affichés. Un dossier sans client reste un dossier valide (aucune erreur). Aucune grosse fiche Client n'est créée dans ce lot. ## Recherche locale La liste des dossiers propose une recherche **100 % locale**, insensible à la casse, portant sur les champs réellement disponibles : référence, titre et nom du client. Aucun HTTP. Aucun moteur de recherche complexe. ## Filtres Aucun filtre supplémentaire n'est ajouté : le modèle ne porte pas de propriété métier justifiant un filtre dédié (actif/archivé, état, client) exploitable de façon fiable dans ce lot. La recherche locale couvre le besoin. ## Tri Tri déterministe : date d'ouverture (décroissante, si disponible), puis référence, puis titre (insensible à la casse). Aucun ordre métier fictif. ## Fonctionnement offline Tout le lot fonctionne depuis SQLite. Ouvrir `Dossiers`, `DossierDetail` ou `InterventionDetail` ne déclenche aucun HTTP obligatoire. Aucun nouveau mécanisme de refresh n'est créé : la synchronisation reste gérée par le `SyncEngine` existant. ## Read-only Ce lot est **lecture seule**. Aucune Outbox Dossier n'est créée. Aucun CREATE / UPDATE / DELETE / ARCHIVE pour Dossier. ## États UI - **Liste** : Loading, liste, EmptyState, ErrorState. - **Détail** : Loading, Dossier introuvable, détail, ErrorState. Les composants du Design System sont réutilisés (`emptyState`, `errorMessage`, `card`, `listTitle`, `body`, `bodySmall`, `caption`, `badge`, `ghost`). ## Smartphone / Tablette Même ViewModel pour toutes les tailles. Sur smartphone, la recherche est facilement accessible, les cartes sont tactiles, la référence/libellé sont immédiatement lisibles et les interventions liées sont présentées en liste simple. Sur tablette/desktop, l'espace supplémentaire est utilisé pour afficher davantage de détails, sans seconde logique métier. Breakpoints globaux uniquement. ## Design System Les Views Dossiers et Détail Dossier réutilisent exclusivement les styles globaux : `pageHeader`, `pageTitle`, `pageSubtitle`, `section`, `sectionTitle`, `card`, `listTitle`, `body`, `bodySmall`, `caption`, `badge`, `ghost`, `emptyState`, `errorMessage`, `field`. Aucune couleur hexadécimale, aucun breakpoint local, aucun FontSize / CornerRadius / Padding standard répété. Les espacements passent par `DynamicResource Space*` et `ThicknessPage`. ## Fichiers créés (LOT DOSSIERS MOBILE) - `src/Omika.App.Local/Abstractions/IDossierLocalService.cs` - `src/Omika.App.Local/Models/DossierDetail.cs` - `src/Omika.App.Local/Services/DossierLocalService.cs` - `src/Omika.App/ViewModels/DossiersViewModel.cs` - `src/Omika.App/ViewModels/DossierDetailViewModel.cs` - `src/Omika.App/Views/DossiersView.axaml` (+ code-behind) - `src/Omika.App/Views/DossierDetailView.axaml` (+ code-behind) - `tests/Omika.Tests/Mobile/Local/DossierLocalServiceTests.cs` - `tests/Omika.Tests/Mobile/Local/DossiersViewModelTests.cs` - `tests/Omika.Tests/Mobile/Local/DossierDetailViewModelTests.cs` ## Fichiers modifiés (LOT DOSSIERS MOBILE) - `src/Omika.App/Navigation/AppPage.cs` (ajout `DossierDetail`) - `src/Omika.App/Navigation/PageViewModelFactory.cs` (cas `Dossiers`, `DossierDetail`) - `src/Omika.App/Navigation/NavigationService.cs` (item de navigation Dossiers) - `src/Omika.App/AppServices.cs` (enregistrement `IDossierLocalService`) - `docs/LOT_MOBILE_0.md` (documentation de ce lot) ## Tests ajoutés (LOT DOSSIERS MOBILE) - `DossierLocalServiceTests` : liste (aucun dossier, un dossier, plusieurs dossiers, tri déterministe, recherche locale, payload invalide ignoré, offline sans appel API, identité publique conservée, client résolu) et détail (dossier trouvé, inconnu, client résolu, sans client, une intervention liée, plusieurs interventions liées, intervention d'un autre dossier exclue, relation optionnelle absente sans crash, GUID publics conservés). - `DossiersViewModelTests` : loading, liste, empty, erreur, recherche, sélection Dossier, navigation DossierDetail avec DossierGuid, aucun appel API. - `DossierDetailViewModelTests` : chargement, Dossier introuvable, données affichées, interventions liées, navigation vers InterventionDetail, retour, état sans intervention, client résolu / absent. ## Validation - Tests `DossierLocalService` : OK. - Tests `DossiersViewModel` : OK. - Tests `DossierDetailViewModel` : OK. - Tests navigation : OK. - Tests Mobile complets : OK. - Suite unitaire : OK (hors 10 intégrations HFSQL/OLEDB connues). - Build solution `Omika.slnx` : OK. - Build Android : OK. - Aucun backend / HFSQL modifié. --- # LOT CLIENTS MOBILE ## Statut Ce lot est **VALIDÉ**. Il développe la liste et la fiche Client en lecture seule, entièrement depuis le snapshot SQLite local. Aucun backend / HFSQL n'est modifié. ## Source SQLite Les clients sont lus exclusivement depuis la table locale `Client` (dernier snapshot). Le payload JSON est désérialisé en `ClientDetailsDto`. Aucun appel API n'est déclenché lors de la navigation ni du chargement. ## DTO Client réel Le DTO utilisé est `Omika.Core.Contracts.Clients.ClientDetailsDto`. Les champs réellement exploités sont ceux présents dans le snapshot : `Id`, `Code`, `Nom`, `Type`, `Statut`, `FormeJuridique`, `Siren`, `Siret`, `Pays`, `Coordonnees` (Email, Phone, Mobile, Fax), `AdressePrincipale`, `InformationsComplementaires`, `EstArchive`, `Sites`, `Contacts`. ## Champs affichés Liste (`ClientListItem`) : - Nom / raison sociale (`Nom`) - Code (`Code`) - Statut (`Statut`) - Ville (`AdressePrincipale.AdressePostale.City`) - Téléphone (`Coordonnees.Phone` ou `Mobile`) - Email (`Coordonnees.Email`) Fiche (`ClientDetail`) : en-tête (Nom, Code, Statut, Type, FormeJuridique, Siren, Siret, Pays), coordonnées (Email, Phone, Mobile, Fax, adresse principale) et relations. Les champs optionnels absents restent nuls sans erreur. ## Identité publique La navigation utilise exclusivement `ClientGuid` (GUID canonique local). Aucun PK HFSQL n'est transporté. Les relations associées utilisent leurs GUID publics (`ContactGuid`, `DossierGuid`, `InterventionGuid`, `SiteGuid`). ## Recherche Recherche locale insensible à la casse sur les champs réellement disponibles : `Nom`, `Code`, `Ville`, `Statut`. Aucun HTTP. ## Filtres Aucun filtre métier artificiel. Le tri est déterministe : nom (insensible à la casse) puis code. ## Relations réellement disponibles Chaque relation est démontrée localement, jamais déduite par commodité : - **Client → Contacts** : liste `Contacts` embarquée dans le DTO Client. Un contact sans rattachement n'est pas ajouté. Ne pas confondre avec les contacts explicitement liés à une Intervention (contexte distinct). - **Client → Sites** : liste `Sites` embarquée dans le DTO Client. - **Client → Dossiers** : `DossierDetailsDto.ClientId == ClientGuid`. - **Client → Interventions** : `InterventionDetailsDto.ClientId == ClientGuid`. Aucune relation transitive (via Dossier ou Site) n'est utilisée. ## Navigation - Clients → `ClientDetail(ClientGuid)` - Client → Contact : `ContactDetail` via `ClientContactDetailNavigation` (réutilise l'écran Contact existant, retour vers la fiche Client). - Client → Dossier : `DossierDetail(DossierGuid)` (écran existant). - Client → Intervention : `InterventionDetail(InterventionGuid)` (écran existant). - Client → Site : aucun écran Site réel n'existe encore ; le lot Sites viendra ensuite. Les sites sont affichés en lecture seule dans la fiche Client. ## Téléphone / Email Pour le Client lui-même, les launchers génériques existants sont réutilisés (`IPhoneLauncher`, `IEmailLauncher`). La commande est désactivée si la donnée est absente. Aucune donnée personnelle n'est loguée inutilement. ## Adresse / Carte Aucune cartographie n'est développée dans ce lot. Si une adresse réelle existe, elle est affichée. Aucune géolocalisation, itinéraire ou API Maps. ## Fonctionnement offline `ClientsView`, `ClientDetail`, `ContactDetail`, `DossierDetail`, `InterventionDetail` restent utilisables depuis SQLite. Aucun HTTP obligatoire. Le SyncEngine existant reste le seul mécanisme de mise à jour. ## Read-only Aucune Outbox Client. Aucune création, modification, suppression ou archivage de Client. Ce lot est strictement en lecture. ## États UI Liste : Loading, liste, EmptyState, ErrorState. Détail : Loading, Client introuvable, détail, relations vides, ErrorState. Une collection associée vide n'est pas une erreur. ## Smartphone / Tablette Même ViewModel pour toutes les tailles. Sur smartphone, la recherche est facilement accessible, le nom du client immédiatement lisible, les coordonnées essentielles accessibles, les sections empilées et les collections liées compactes. Sur tablette/desktop, l'espace supplémentaire est exploité (coordonnées d'un côté, relations de l'autre) sans seconde logique métier. Breakpoints globaux uniquement. ## Design System Les Views Clients et Détail Client réutilisent exclusivement les styles globaux (`pageHeader`, `pageTitle`, `pageSubtitle`, `section`, `sectionTitle`, `card`, `listTitle`, `body`, `bodySmall`, `caption`, `badge`, `ghost`, `emptyState`, `errorMessage`, `field`). Aucune couleur hexadécimale, aucun breakpoint local, aucun FontSize / CornerRadius / Padding standard répété. Les espacements passent par `DynamicResource Space*` et `ThicknessPage`. ## Fichiers créés (LOT CLIENTS MOBILE) - `src/Omika.App.Local/Abstractions/IClientLocalService.cs` - `src/Omika.App.Local/Models/ClientDetail.cs` - `src/Omika.App.Local/Services/ClientLocalService.cs` - `src/Omika.App/ViewModels/ClientsViewModel.cs` - `src/Omika.App/ViewModels/ClientDetailViewModel.cs` - `src/Omika.App/Views/ClientsView.axaml` (+ code-behind) - `src/Omika.App/Views/ClientDetailView.axaml` (+ code-behind) - `src/Omika.App/Navigation/ClientContactDetailNavigation.cs` - `tests/Omika.Tests/Mobile/Local/ClientLocalServiceTests.cs` - `tests/Omika.Tests/Mobile/Local/ClientsViewModelTests.cs` - `tests/Omika.Tests/Mobile/Local/ClientDetailViewModelTests.cs` ## Fichiers modifiés (LOT CLIENTS MOBILE) - `src/Omika.App/Navigation/AppPage.cs` (ajout `ClientDetail`) - `src/Omika.App/Navigation/PageViewModelFactory.cs` (cas `Clients`, `ClientDetail`) - `src/Omika.App/Navigation/NavigationService.cs` (item de navigation Clients) - `src/Omika.App/AppServices.cs` (enregistrement `IClientLocalService`) - `src/Omika.App/ViewModels/ContactDetailViewModel.cs` (gestion du retour depuis la fiche Client via `ClientContactDetailNavigation`) - `docs/LOT_MOBILE_0.md` (documentation de ce lot) ## Tests ajoutés (LOT CLIENTS MOBILE) - `ClientLocalServiceTests` : liste (aucun client, un client, plusieurs clients, tri déterministe, recherche locale, payload invalide ignoré, identité publique conservée, offline sans appel API) et détail (client trouvé, inconnu, données réelles chargées, champs optionnels absents, aucune relation sans erreur, identité publique conservée). Relations : Client → Contacts (uniquement ses contacts, contact du client B exclu, contact sans client exclu), Client → Dossiers (uniquement ses dossiers, dossier B exclu), Client → Interventions (uniquement ses interventions, intervention B exclue), Client → Sites (sites embarqués, client sans sites). - `ClientsViewModelTests` : loading, liste, empty, erreur, recherche (nom, ville, sans résultat), sélection Client, navigation ClientDetail avec ClientGuid, aucun appel API. - `ClientDetailViewModelTests` : chargement, client introuvable, coordonnées, contacts, dossiers, interventions, sites, relations vides sans erreur, navigation Contact (ClientContactDetailNavigation), navigation Dossier, navigation Intervention, retour, identité publique conservée. ## Validation - Tests `ClientLocalService` : OK. - Tests relations Client : OK. - Tests `ClientsViewModel` : OK. - Tests `ClientDetailViewModel` : OK. - Tests navigation : OK. - Tests Mobile complets : OK. - Suite unitaire : OK (hors 10 intégrations HFSQL/OLEDB connues). - Build solution `Omika.slnx` : OK. - Build Android : OK. - Aucun backend / HFSQL modifié. ## Données manquantes éventuelles - Aucun écran Site réel n'existe encore : les sites sont affichés en lecture seule dans la fiche Client ; le lot Sites viendra ensuite. - Les contacts du Client sont affichés depuis la liste embarquée dans le DTO Client ; le détail Contact réutilise l'écran existant qui lit la table `Personne`. --- # LOT SITES / LIEUX MOBILE ## Statut Ce lot est **VALIDÉ**. Il développe la liste et la fiche des Sites / Lieux, entièrement en lecture seule depuis le snapshot SQLite local. Aucun backend, aucune API, aucun snapshot ni aucune relation artificielle n'a été modifié. ## Terminologie retenue Le mobile manipule le concept métier commun **Site / Lieu**. Les structures physiques historiques (`ClientLieu`, `LieuPersonne`, PK HFSQL, différences Batik/Omika) ne sont **jamais** exposées dans les Views / ViewModels. Le mobile n'utilise que les abstractions locales existantes : `Client`, `Site`, `Contact`, `Intervention`. ## Source de vérité La source métier est exclusivement : - les contrats réels (`SiteDetailsDto`, `ClientDetailsDto`, `ContactDto`, `InterventionDetailsDto`) ; - le snapshot SQLite local ; - les relations explicitement disponibles dans ces contrats. `maquetteOmika.html` sert uniquement de référence UX. Aucun champ ni relation n'a été créé depuis la maquette. ## DTO / modèle local - DTO source : `SiteDetailsDto` (`Omika.Core.Contracts.Sites`). - Modèle de présentation : `SiteListItem` (liste) et `SiteDetail` (détail) dans `Omika.App.Local.Models`. - Payload local : table `Site` (colonne `PayloadJson`), relation `ClientId`. ## Champs affichés Liste (`SiteListItem`) : - `Nom` - `Code` - `Type` (libellé) - `Ville` (depuis l'adresse) - `ClientNom` (si relation résolue) Détail (`SiteDetail`) : - `Nom`, `Code`, `Type` - `InformationsComplementaires`, `EstArchive` - `Adresse` (composée depuis les champs réels du DTO) - `ClientGuid`, `ClientNom`, `ClientCode` - `Contacts` (liste `SiteContact`) - `Interventions` (liste `SiteIntervention`) Aucune propriété UI toujours nulle n'a été créée pour reproduire la maquette. ## Identité publique Navigation et relations utilisent exclusivement les GUID publics locaux : - `Sites` → `SiteDetail(SiteGuid)` - `Site → Client` : `ClientGuid` - `Site → Contact` : `ContactGuid` - `Site → Intervention` : `InterventionGuid` Jamais de PK HFSQL. ## Recherche / filtres - Recherche locale insensible à la casse sur : `Nom`, `Code`, `Ville`, `ClientNom` (propriétés réellement disponibles). - Aucun filtre supplémentaire : aucun filtre n'était justifié par une donnée métier réelle dans ce lot. ## Tri Tri déterministe : `Nom` (insensible à la casse) puis `Code`. ## Relation Site → Client Relation explicite **`SiteDetailsDto.ClientId`** (CAS A). Le détail du site résout le client local correspondant. Cardinalité constatée : un Site peut être lié à un Client via `ClientId` ; le modèle ne suppose pas de correspondance par nom/adresse/code. Si le `ClientId` ne correspond à aucun client local, le client reste absent (pas d'invention). ## Relation Site → Contacts Relation explicite **`SiteDetailsDto.ContactIds`** (liste de GUID), résolue vers les Personnes/Contacts stockées localement. Un Contact du Client n'est **pas** automatiquement un Contact du Site : seuls les contacts listés dans `ContactIds` sont affichés. Si la liste est absente/vide, aucune fausse liste Contacts n'est affichée. ## Relation Site → Interventions Relation directe **`InterventionDetailsDto.SiteId`**. Les interventions du site sont celles dont `SiteId == SiteGuid`. Aucune déduction via Client, Dossier, adresse ou RendezVous. ## Relation Site → RendezVous Aucune section RendezVous n'a été ajoutée dans ce lot : les RendezVous restent accessibles naturellement via la fiche Intervention. Aucune relation directe Site → RendezVous n'est chargée pour éviter de surcharger les relations. ## Navigations - `Site → Client` : `ClientDetail(ClientGuid)` (réutilise l'écran existant). - `Site → Contact` : `ContactDetail(SiteContactDetailNavigation)` (réutilise l'écran existant). - `Site → Intervention` : `InterventionDetail(InterventionGuid)` (réutilise l'écran existant). ## Téléphone / email Le Site ne transporte pas de coordonnées propres dans ce lot (le DTO Site ne porte pas de téléphone/email dédié). Aucune coordonnée du Client n'est déduite comme coordonnée du Site. Les launchers `IPhoneLauncher` / `IEmailLauncher` restent disponibles pour les contacts liés. ## Adresse L'adresse est affichée uniquement depuis les données réelles du DTO Site (`AddressDto`), composée dans `SiteAdresse` (modèle de présentation). La présentation sur une ligne (`OneLine`) n'est pas stockée comme donnée métier. ## Carte / itinéraire Aucune carte ni dépendance Maps n'a été introduite dans ce lot. Affichage texte de l'adresse uniquement. La navigation GPS reste un lot séparé. ## Offline `SitesView`, `SiteDetail`, `ClientDetail`, `ContactDetail` et `InterventionDetail` fonctionnent depuis SQLite. Aucun HTTP obligatoire. `SyncEngine` reste l'unique mécanisme de mise à jour. ## États UI Liste : Loading, liste, EmptyState, ErrorState. Détail : Loading, Site inconnu, détail, collections associées vides, ErrorState. Une relation absente n'est pas une erreur générale. ## Smartphone / Tablette / Desktop Même ViewModel pour toutes les tailles. Smartphone : sections empilées, grandes zones tactiles. Tablette/Desktop : l'espace supplémentaire est utilisé via les sections (coordonnées, client, relations). Breakpoints globaux uniquement. ## Design System Les Views `SitesView` et `SiteDetailView` réutilisent exclusivement les styles globaux : `pageHeader`, `pageTitle`, `pageSubtitle`, `section`, `sectionTitle`, `card`, `listTitle`, `body`, `bodySmall`, `caption`, `badge`, `ghost`, `emptyState`, `errorMessage`, `field`. Aucune couleur hexadécimale, aucun FontSize/CornerRadius/Padding standard répété, aucun breakpoint local. ## Read-only Aucune Outbox Site. Aucun CREATE / UPDATE / DELETE / ARCHIVE pour Site. ## Fichiers créés (LOT SITES / LIEUX MOBILE) - `src/Omika.App.Local/Models/SiteDetail.cs` - `src/Omika.App.Local/Abstractions/ISiteLocalService.cs` - `src/Omika.App.Local/Services/SiteLocalService.cs` - `src/Omika.App/ViewModels/SitesViewModel.cs` - `src/Omika.App/ViewModels/SiteDetailViewModel.cs` - `src/Omika.App/Navigation/SiteContactDetailNavigation.cs` - `src/Omika.App/Views/SitesView.axaml` (+ code-behind) - `src/Omika.App/Views/SiteDetailView.axaml` (+ code-behind) - `tests/Omika.Tests/Mobile/Local/SiteLocalServiceTests.cs` - `tests/Omika.Tests/Mobile/Local/SitesViewModelTests.cs` - `tests/Omika.Tests/Mobile/Local/SiteDetailViewModelTests.cs` ## Fichiers modifiés (LOT SITES / LIEUX MOBILE) - `src/Omika.App/Navigation/AppPage.cs` (ajout `SiteDetail`) - `src/Omika.App/Navigation/PageViewModelFactory.cs` (cas `Sites`, `SiteDetail`, gestion `SiteContactDetailNavigation` dans `CreateContactDetail`) - `src/Omika.App/Navigation/NavigationService.cs` (item de navigation Sites) - `src/Omika.App/AppServices.cs` (enregistrement `ISiteLocalService`) - `src/Omika.App/ViewModels/ContactDetailViewModel.cs` (gestion du retour depuis la fiche Site via `SiteContactDetailNavigation`) - `docs/LOT_MOBILE_0.md` (documentation de ce lot) ## Tests ajoutés (LOT SITES / LIEUX MOBILE) - `SiteLocalServiceTests` : liste (aucun site, un site, plusieurs sites, tri déterministe, recherche locale, payload invalide ignoré, identité publique conservée, offline sans appel API) et détail (site trouvé, inconnu, données réelles chargées, champs optionnels absents, aucune relation sans erreur, identité publique conservée). Relations : Site → Client (bon client, client sans site exclu, aucune correspondance par nom/adresse, site sans client valide), Site → Contacts (uniquement ses contacts, contact du même client mais autre site exclu, contact non relié exclu, GUID public conservé), Site → Interventions (intervention du site incluse, intervention d'un autre site exclue, intervention sans site exclue, aucune déduction via Client, GUID public conservé). - `SitesViewModelTests` : loading, liste, empty, erreur, recherche (nom, ville, sans résultat), sélection Site, navigation SiteDetail avec SiteGuid, aucun appel API. - `SiteDetailViewModelTests` : chargement, site introuvable, coordonnées, client lié, site sans client, contacts, interventions, relations vides sans erreur, navigation Client, navigation Contact (SiteContactDetailNavigation), navigation Intervention, retour, identité publique conservée. ## Validation - Tests `SiteLocalService` : OK. - Tests relations Site : OK. - Tests `SitesViewModel` : OK. - Tests `SiteDetailViewModel` : OK. - Tests navigation : OK. - Tests Mobile complets : OK. - Suite unitaire : OK (hors 10 intégrations HFSQL/OLEDB connues). - Build solution `Omika.slnx` : OK. - Build Android : OK. - Aucun backend / HFSQL modifié. ## Relations éventuellement absentes - Le DTO Site ne transporte pas de téléphone/email dédié : aucune coordonnée propre au Site n'est affichée (les coordonnées du Client ne sont pas déduites). - Aucune relation Site → RendezVous directe n'est chargée dans ce lot. - Les contacts du Site sont affichés uniquement depuis la liste explicite `ContactIds` du DTO Site ; si cette relation n'est pas peuplée dans le snapshot, aucune fausse liste Contacts n'est affichée. --- # LOT ÉQUIPE / COLLABORATEURS — LISTE + DÉTAIL ## Statut Ce lot est **VALIDÉ**. Il développe la section Équipe / Collaborateurs du mobile en lecture seule, entièrement depuis le snapshot SQLite local, sans aucun appel HTTP et sans modification backend/HFSQL. ## DTO réel - `CollaborateurDetailsDto` (`Omika.Core.Contracts.Organisation`) : détail d'un collaborateur. Champs disponibles : `Code`, `Nom`, `Prenom`, `NomAffiche`, `Coordonnees` (`ContactDetailsDto` : `Email`, `Phone`, `Mobile`, `Fax`), `Civilite` (`ReferenceValueDto`), `Statut` (`ReferenceValueDto`), `CouleurPlanning`, `Actif`, `EstArchive`. - `RendezVousDetailsDto` (`Omika.Core.Contracts.Planning`) : porte la relation Collaborateur → RendezVous via la liste `Collaborateurs` (`AffectationCollaborateurDto.CollaborateurId`). - `InterventionDetailsDto` (`Omika.Core.Contracts.Interventions`) : porte la relation Collaborateur → Intervention via la liste `CollaborateurIds`. ## Champs affichés - **Liste** : `DisplayName` (construit dans la couche présentation à partir de `NomAffiche`, sinon `Prenom` + `Nom`, avec fallback propre), `Code`, `Fonction` (libellé `Statut`), `Email`, `Telephone` (fixe sinon mobile), `Actif`, `EstArchive`. - **Détail** : `Code`, `Nom`, `Prenom`, `NomAffiche`, `Civilite`, `Statut`, `CouleurPlanning`, `Actif`, `EstArchive`, coordonnées (`Email`, `Phone`, `Mobile`, `Fax`), relations RendezVous et Interventions. ## Identité publique Navigation exclusivement par `CollaborateurGuid` (GUID canonique local). Aucun PK HFSQL n'est transporté. Aucun DTO de navigation spécialisé (`EquipeCollaborateurNavigation`, `PlanningCollaborateurNavigation`) n'est créé : le mécanisme consolidé `DetailNavigation` / `NavigationContext` est réutilisé. ## Recherche / filtres - Recherche locale insensible à la casse sur `DisplayName`, `Code`, `Fonction`, `Email` (champs réellement disponibles). Aucun HTTP. - Aucun filtre inventé (Disponible / Occupé / En intervention) : seuls les champs réels sont utilisés. ## Relation Collaborateur → RendezVous Relation réellement démontrable via `RendezVousDetailsDto.Collaborateurs` (`AffectationCollaborateurDto.CollaborateurId`). Un rendez-vous apparaît pour chaque collaborateur explicitement associé (cardinalité multiple respectée). Tri chronologique croissant. Tap → `RendezVousDetail` avec `DetailNavigation(RendezVousGuid, NavigationContext(CollaborateurDetail, CollaborateurGuid))`. ## Relation Collaborateur → Intervention Relation réellement démontrable via `InterventionDetailsDto.CollaborateurIds`. Aucune déduction transitive (Collaborateur → RendezVous → Intervention) n'est faite. Tap → `InterventionDetail` avec `DetailNavigation(InterventionGuid, NavigationContext(CollaborateurDetail, CollaborateurGuid))`. ## Navigation contextuelle - Équipe → `CollaborateurDetail(CollaborateurGuid)`. - `CollaborateurDetail` → `RendezVousDetail` → Retour → `CollaborateurDetail`. - `CollaborateurDetail` → `InterventionDetail` → Retour → `CollaborateurDetail`. - Aucun flag `IsFromX` ni DTO de retour spécialisé n'est réintroduit. ## Offline / read-only - `CollaborateursView`, `CollaborateurDetailView`, `RendezVousDetail`, `InterventionDetail` fonctionnent depuis SQLite. Aucun appel HTTP obligatoire. - SyncEngine reste l'unique système de mise à jour. - Aucune Outbox Collaborateur. Aucun CREATE/UPDATE/DELETE/ARCHIVE : lecture seule. ## Design System - Aucune couleur hex standard, aucun breakpoint local, aucun FontSize standard, aucun CornerRadius/Padding standard répété dans les Views. - Styles sémantiques existants réutilisés. Avatar neutre / initiales générées localement (présentation UI, pas donnée métier). `AvatarSize` centralisé. ## Fichiers créés - `src/Omika.App.Local/Models/CollaborateurDetail.cs` (modèles de présentation) - `src/Omika.App.Local/Abstractions/ICollaborateurLocalService.cs` - `src/Omika.App.Local/Services/CollaborateurLocalService.cs` - `src/Omika.App/ViewModels/CollaborateursViewModel.cs` - `src/Omika.App/ViewModels/CollaborateurDetailViewModel.cs` - `src/Omika.App/Views/CollaborateursView.axaml` (+ `.cs`) - `src/Omika.App/Views/CollaborateurDetailView.axaml` (+ `.cs`) - `tests/Omika.Tests/Mobile/Local/CollaborateurLocalServiceTests.cs` - `tests/Omika.Tests/Mobile/Local/CollaborateursViewModelTests.cs` - `tests/Omika.Tests/Mobile/Local/CollaborateurDetailViewModelTests.cs` ## Fichiers modifiés - `src/Omika.App/Navigation/AppPage.cs` (ajout `CollaborateurDetail`) - `src/Omika.App/Navigation/PageViewModelFactory.cs` (cas `Collaborateurs`, `CollaborateurDetail`) - `src/Omika.App/AppServices.cs` (enregistrement `ICollaborateurLocalService`) - `tests/Omika.Tests/Mobile/Local/LocalTestBase.cs` (ajout `CollaborateurRepository`) - `docs/LOT_MOBILE_0.md` (documentation de ce lot) ## Tests ajoutés - `CollaborateurLocalServiceTests` : liste (aucun, un, plusieurs, tri déterministe, recherche locale, payload invalide ignoré, identité publique conservée, offline sans appel API) et détail (trouvé, inconnu, données réelles chargées, champs optionnels absents, identité publique conservée, relations vides sans erreur). Relations : Collaborateur → RendezVous (uniquement ses rendez-vous, rendez-vous d'un autre collaborateur exclu, rendez-vous sans relation exclu, cardinalité multiple respectée, tri chronologique, GUID public conservé), Collaborateur → Interventions (uniquement ses interventions, intervention d'un autre collaborateur exclue, aucune déduction transitive, GUID public conservé). - `CollaborateursViewModelTests` : loading, liste, empty, erreur, recherche (nom, sans résultat, réinitialisation), sélection Collaborateur, navigation CollaborateurDetail avec CollaborateurGuid, aucun appel API. - `CollaborateurDetailViewModelTests` : chargement, collaborateur introuvable, coordonnées, champs optionnels absents, RendezVous, Interventions, relations vides sans erreur, téléphone/email (disponible et absent), navigation RendezVous, navigation Intervention, retour, contexte retour conservé. ## Validation - Tests `CollaborateurLocalService` : OK. - Tests relations Collaborateur : OK. - Tests `CollaborateursViewModel` : OK. - Tests `CollaborateurDetailViewModel` : OK. - Tests navigation contextuelle : OK. - Tests Mobile complets : OK. - Suite unitaire : OK (hors intégrations HFSQL/OLEDB connues). - Build solution `Omika.slnx` : OK. - Build Android : OK. - Aucun backend / HFSQL modifié. ## Relations éventuellement absentes - La relation Collaborateur → RendezVous est portée par `RendezVousDetailsDto.Collaborateurs` ; si cette liste n'est pas peuplée dans le snapshot, aucune fausse liste RendezVous n'est affichée. - La relation Collaborateur → Intervention est portée par `InterventionDetailsDto.CollaborateurIds` ; si cette liste n'est pas peuplée dans le snapshot, aucune fausse liste Interventions n'est affichée. - Aucune relation n'est déduite par transitivité (Collaborateur → RendezVous → Intervention) ni par commodité. - Aucun poste, fonction, équipe, disponibilité, statut, avatar, compétence, téléphone ou email n'est créé si le DTO réel ne le contient pas. --- # LOT INTERVENTIONS — LISTE ## Statut VALIDÉ — lecture seule depuis SQLite, recherche locale, filtre Actif/Archivé, navigation contextuelle vers la fiche Intervention. ## Objectif Afficher la liste des interventions disponibles dans le snapshot local, les rechercher localement, filtrer les archivées et ouvrir la fiche Intervention par identité publique. Fonctionne totalement offline, sans appel API. ## Architecture ``` InterventionsView → InterventionsViewModel → IInterventionLocalService → repositories SQLite ``` - `InterventionsView.axaml` / `.axaml.cs` : vue de la liste. - `InterventionsViewModel` : chargement, recherche, filtre, navigation. - `IInterventionLocalService.GetInterventionsAsync()` : lecture locale. - `InterventionLocalService` : agrégation depuis les repositories SQLite (Intervention, Client, Site, Dossier, Collaborateur). ## DTO / modèle local - DTO source : `InterventionDetailsDto` (Omika.Core.Contracts.Interventions). - Modèle local : `LocalIntervention` (payload JSON + colonnes relationnelles). - Modèle de présentation : `InterventionListItem`. ## Champs affichés Uniquement les champs réellement disponibles dans le DTO : - `Reference` - `Titre` - `Etat` (libellé) - `EstArchive` (filtre) - `DatePrevueDebut` (tri) - Relations résolues localement : `ClientNom`, `SiteNom`, `DossierReference`, `Collaborateurs` (noms des collaborateurs affectés). ## Identité publique - `InterventionGuid` (GUID public) utilisé exclusivement pour la navigation. - Jamais de PK HFSQL. ## Recherche - Recherche locale insensible à la casse sur `Reference` et `Titre`. - Aucun HTTP. ## Filtres - Filtre Actif / Archivé basé sur `EstArchive` (donnée réelle du DTO). - Par défaut, les interventions archivées sont masquées. ## Tri - Tri déterministe par `DatePrevueDebut` puis `Reference`. ## Navigation contextuelle - `InterventionsView` → `InterventionDetail(InterventionGuid)` via `DetailNavigation(InterventionGuid, NavigationContext(AppPage.Interventions, null))`. - Retour contextuel vers la liste Interventions via `NavigationContext`. - Aucun flag `IsFromX` ni DTO de navigation spécialisé. ## Offline - Lecture uniquement depuis SQLite. - Aucun appel HTTP obligatoire. - SyncEngine reste l'unique système de mise à jour. ## Read-only - Aucune Outbox Intervention. - Aucun CREATE / UPDATE / DELETE / ARCHIVE. - Lot strictement lecture. ## Design System - Réutilise les styles sémantiques existants (pageHeader, pageTitle, field, ghost, listTitle, caption, bodySmall, emptyState, errorMessage). - Aucune couleur hex, FontSize, CornerRadius, Padding standard en dur. - Aucun breakpoint local. ## États UI - Loading - Liste - EmptyState - ErrorState ## Tests ajoutés - `InterventionLocalServiceTests` : aucun, un, plusieurs (tri déterministe), payload invalide ignoré, GUID public conservé, relations résolues, flag archivé conservé. - `InterventionsViewModelTests` : loading, liste, empty, erreur (payload invalide), recherche (référence, sans résultat), filtre Actif/Archivé, navigation InterventionDetail avec GUID public et retour contextuel, aucun appel API. - `ShellViewModelTests` : mise à jour du test de navigation pour vérifier que `AppPage.Interventions` retourne un `InterventionsViewModel`. ## Validation - Tests `InterventionLocalService` : OK. - Tests `InterventionsViewModel` : OK. - Tests navigation contextuelle : OK. - Tests Mobile complets : OK. - Suite unitaire : OK (hors intégrations HFSQL/OLEDB connues). - Build solution `Omika.slnx` : OK. - Build Android : OK. - Aucun backend / HFSQL modifié.