Files
Omika/docs/LOT_MOBILE_0.md
T
2026-10-23 18:46:50 +02:00

2267 lines
102 KiB
Markdown

# 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<Guid>` 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é.