102 KiB
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, snapshotOmika.Core.Contracts.Documents: métadonnées, téléchargement, upload
Couche applicative mobile/API
Omika.Core.Services.Mobile: licence, bootstrap, synchronisation, snapshotOmika.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é dansHttpContext.Items.IMobileIdentityProvider/MobileIdentityProvider: fournisseur d'identité commun (Batik viaParametreMetier, Omika viaRole).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éCancellationTokenpartout- Fail-fast Production :
MobileAuth:SigningKeyobligatoire 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.OperationTypecouvreCREATE,UPDATE,DELETE,ARCHIVE.- Enveloppe technique interne
SyncEnvelopepersistée dansTS_JSON/MR_JSON: contient les identités serveur (ClientId,UserId,CollaborateurId) issues du token et leRetryCountcontrôlé côté serveur. Le mobile ne peut pas imposer ces champs. - Le
DeviceIdde l'opération doit correspondre à l'identité authentifiée. LeCollaborateurIdfourni par le client n'est jamais utilisé : l'identité provient du token/bootstrap. - La réponse signifie uniquement
RECEIVED(jamaisSUCCESS), car l'opération n'est pas encore réellement appliquée. - Idempotence : une répétition du même
OperationIdne crée pas deux opérations métier. AbstractionISyncOperationStatusStore(stockage terminalMobLog). Un verrou parOperationIdgarantit l'idempotence même en cas de requêtes simultanées. - Ordre logique respecté par le futur moteur :
CREATE→UPDATE→DELETE/ARCHIVE. TS_CMDreçoit une valeur technique interne moderne (MOB.OP), jamais le contrat public de l'API. Les anciennes commandesS.xxxne 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:BackgroundServicequi déclenche périodiquement le traitement.SyncProcessor: logique pure et testable, indépendante duBackgroundService.- Phase 1 — transfert :
TOSRV→ copie versMobReception→ suppression deTOSRVuniquement si la copie réussit. - Phase 2 — traitement : lit les opérations
MR_Fait = falsearrivé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.
- Phase 1 — transfert :
ISyncOperationHandler/SyncOperationHandlerRegistry: handlers métier parEntityType, extensibles. Le handler public manipule le contrat communSyncOperation(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→SUCCESSdans MobLog + retrait de la file de travail.TransientFailure→ incrémentRetryCount, réécritureMR_JSON, report deMR_DateHeureRecep.PermanentFailure→REJECTEDimmé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) avecOperationId, 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
EntityGuidmobile commeGuidmétier àCreateInterventionRequest(rétrocompatible : une création Desktop sans GUID conserve le comportement historique). Idempotent : si l'intervention existe déjà,SUCCESSsans recréer. - UPDATE : résout l'intervention par identifiant public via le resolver, puis
UpdateAsync. Absente →TransientFailure(INTERVENTION_NOT_FOUND). - ARCHIVE :
ArchiveAsyncsur 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
IInterventionServiceetIMobileEntityIdentityResolver(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é parLegacyEntityReference(SourceReference,[JsonIgnore]sur le DTO) pour calculer l'identité canonique sans exposer le PK.SyncOperationResult: porteRequestedEntityGuid(GUID mobile demandé) etCanonicalEntityGuid(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
OperationIdrenforcée (reconstitution du résultat terminal) : leSyncProcessorvérifie, avant d'appeler le handler, si l'opération a déjà produit un résultat terminal (SUCCESS/REJECTED) dansMobLogviaISyncTransferRepository.GetTerminalResultAsync. Si oui, il reconstitue ce résultat sans rappeler le handler métier : un mêmeOperationIdne produit jamais deux applications métier (notamment pour un CREATE Batik où le GUID mobile temporaire n'est pas stocké). LeCanonicalEntityGuid, leRequestedEntityGuidet leCreatedEntityIdd'origine sont conservés dansMobLog(SyncLogEntry) et réutilisés lors de la reconstitution. Une protection défensive parHashSet<Guid>empêche aussi tout doublon d'OperationIdau 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é :
MobLogpour les états terminaux (SUCCESS/REJECTED) ;TOSRV/MobReceptionpour les opérations encore en attente (PENDING). Aucun statutPROCESSINGpersistant n'est inventé : il restePENDINGentre 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, deMR_PKID, de nom de table technique ni de commande historiqueS.xxx/W.xxx. - Résultat riche pour les états terminaux :
GetStatusAsyncréutiliseGetTerminalResultAsync(parsing de l'enveloppe JSON deMobLog) pour reconstituer un statut complet, y comprisRequestedEntityGuidetCanonicalEntityGuid(utile pour un CREATE Batik : le mobile récupère le GUID canonique à utiliser pour les UPDATE/ARCHIVE ultérieurs) etCompletedAt. - Isolation par appareil : si l'opération existe mais appartient à un autre
DeviceId,GetStatusAsyncretournenull(aucune information métier sensible ne fuit vers un autre appareil). Le endpoint traduit ce cas enSTATUS_NOT_AVAILABLEavec un HTTP approprié. - OperationId inconnu / purgé : retourne
STATUS_NOT_AVAILABLEavec 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 dansMobLogne sont jamais supprimés par cette purge. - Jamais de purge des opérations non terminées : une opération
PENDINGn'est jamais supprimée uniquement parce qu'elle est ancienne. - Après purge, un
OperationIdancien peut retournerSTATUS_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 leDocumentGuidfourni 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
DocumentGuidest résolu vers l'identité physique backend viaIMobileEntityIdentityResolver(GUID physique Omika ou PK numérique Batik), puis le document est chargé par cette identité viaIDocumentRepository.GetByPhysicalIdentityAsync. - Stockage physique : le contenu binaire est lu/écrit via
IDocumentFileStorage(abstraction anti path traversal, taille maximale configurable). LeDC_Cheminstocké 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/DocumentDownloadResponseest stable et indépendant de la génération de base (Batik/Omika). Les métadonnées de document sont lues depuis la tableDocument(GUID métier stable si présent, sinonGuid.Emptyet 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.axamlsrc/Omika.App/Styles/Controls.axamlsrc/Omika.App/Services/ThemeMode.cssrc/Omika.App/Services/ThemeService.cssrc/Omika.App/Navigation/AppPage.cssrc/Omika.App/Navigation/NavigationItem.cssrc/Omika.App/Navigation/INavigationService.cssrc/Omika.App/Navigation/NavigationService.cssrc/Omika.App/Navigation/IPageViewModelFactory.cssrc/Omika.App/Navigation/PageViewModelFactory.cssrc/Omika.App/ViewModels/ViewModelBase.cssrc/Omika.App/ViewModels/SyncUiState.cssrc/Omika.App/ViewModels/ShellViewModel.cssrc/Omika.App/ViewModels/PlaceholderViewModel.cssrc/Omika.App/ViewModels/ParametresViewModel.cssrc/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.axamlsrc/Omika.App/Views/NavItem*Converter.cssrc/Omika.App/Views/SyncStateToBrushConverter.cssrc/Omika.App/AppServices.cssrc/Omika.App.Local/Models/UiPreferences.cssrc/Omika.App.Local/Abstractions/IUiPreferencesRepository.cssrc/Omika.App.Local/Repositories/UiPreferencesRepository.cssrc/Omika.App.Local/Services/NetworkConnectivity.cssrc/Omika.App.Local/Services/MobileApiClient.cs
Fichiers modifiés
src/Omika.App.Local/Sqlite/SqliteSchema.cs(tableUiPreferences)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érenceOmika.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
- Toute nouvelle View métier doit d'abord chercher si un style/composant global existe.
- 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é. - 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.
- 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(viaThemeService)Radius.Button(dansDesignTokens.axaml)FontSizeBody(dansDesignTokens.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 deRendezVousDetail.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 repositoriesLocalRendezVous,LocalClient,LocalSite,LocalInterventionet du serviceIAgendaService.
Fichiers créés (LOT AGENDA)
src/Omika.App.Local/Abstractions/IAgendaService.cssrc/Omika.App.Local/Services/AgendaService.cssrc/Omika.App.Local/Models/AgendaItem.cssrc/Omika.App/ViewModels/AgendaViewModel.cssrc/Omika.App/ViewModels/RendezVousDetailViewModel.cssrc/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(ajoutRendezVousDetail)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(expositionSiteRepository)
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 deInterventionDetail.Navigation/PageViewModelFactory.cs: casInterventionDetail(injection du service + paramètre GUID).ViewModels/RendezVousDetailViewModel.cs: ajout deHasIntervention,InterventionReferenceet de la commandeOpenIntervention(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 repositoriesLocalDossier,LocalDocumentMetadataet du serviceIInterventionLocalService.
Fichiers créés (LOT FICHE INTERVENTION)
src/Omika.App.Local/Abstractions/IInterventionLocalService.cssrc/Omika.App.Local/Services/InterventionLocalService.cssrc/Omika.App.Local/Models/InterventionDetail.cssrc/Omika.App/ViewModels/InterventionDetailViewModel.cssrc/Omika.App/Views/InterventionDetailView.axaml(+ code-behind)
Fichiers modifiés (LOT FICHE INTERVENTION)
src/Omika.App/Navigation/AppPage.cs(ajoutInterventionDetail)src/Omika.App/Navigation/PageViewModelFactory.cs(casInterventionDetail)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(expositionDossierRepository,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.cssrc/Omika.App.Local/Abstractions/IFileLauncher.cssrc/Omika.App.Local/Abstractions/IFilePicker.cssrc/Omika.App.Local/Models/InterventionDocumentItem.cssrc/Omika.App.Local/Models/DocumentOutboxPayload.cssrc/Omika.App.Local/Services/DocumentLocalService.cssrc/Omika.App/Services/PlatformFileLauncher.cssrc/Omika.App/Services/PlatformFilePicker.cssrc/Omika.App/ViewModels/InterventionDocumentsViewModel.cssrc/Omika.App/Views/InterventionDocumentsView.axaml(+ code-behind)tests/Omika.Tests/Mobile/Local/DocumentLocalServiceTests.cstests/Omika.Tests/Mobile/Local/DocumentSyncTests.cstests/Omika.Tests/Mobile/Local/InterventionDocumentsViewModelTests.cstests/Omika.Tests/Mobile/Local/DocumentDesignSystemTests.cs
Fichiers modifiés (LOT DOCUMENTS / PHOTOS)
src/Omika.App.Local/Abstractions/IOutboxRepository.cs(ajoutGetRejectedAsync)src/Omika.App.Local/Repositories/OutboxRepository.cs(implémentationGetRejectedAsync)src/Omika.App.Local/Services/DocumentLocalService.cs(lecture des états Rejected)src/Omika.App/Navigation/AppPage.cs(ajoutInterventionDocuments)src/Omika.App/Navigation/PageViewModelFactory.cs(casInterventionDocuments)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
ContactIdsn'est jamais associé à l'intervention. - Les identités publiques (
ContactIdssous forme de GUID) sont résolues vers lesLocalPersonnepar leurId(GUID canonique local).
Source SQLite
- Table
Intervention(payloadInterventionDetailsDto) : porteContactIds. - Table
Personne(payloadContactDto) : 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,CiviliteEmail,Phone,Mobile,Fax(si présents dansCoordonnees)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 URItel:(AvaloniaTopLevel.Launcher).IEmailLauncher/PlatformEmailLauncher: ouvre le client email système via un URImailto:.- 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.cssrc/Omika.App.Local/Abstractions/IPhoneLauncher.cssrc/Omika.App.Local/Abstractions/IEmailLauncher.cssrc/Omika.App.Local/Models/InterventionContact.cssrc/Omika.App.Local/Services/InterventionContactsService.cssrc/Omika.App/Services/PlatformPhoneLauncher.cssrc/Omika.App/Services/PlatformEmailLauncher.cssrc/Omika.App/Navigation/ContactDetailNavigation.cssrc/Omika.App/ViewModels/InterventionContactsViewModel.cssrc/Omika.App/ViewModels/ContactDetailViewModel.cssrc/Omika.App/Views/InterventionContactsView.axaml(+ code-behind)src/Omika.App/Views/ContactDetailView.axaml(+ code-behind)tests/Omika.Tests/Mobile/Local/InterventionContactsServiceTests.cstests/Omika.Tests/Mobile/Local/InterventionContactsViewModelTests.cstests/Omika.Tests/Mobile/Local/ContactDetailViewModelTests.cs
Fichiers modifiés (LOT CONTACTS INTERVENTION)
src/Omika.App/Navigation/AppPage.cs(ajoutInterventionContacts,ContactDetail)src/Omika.App/Navigation/PageViewModelFactory.cs(casInterventionContacts,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(expositionPersonneRepository)tests/Omika.Tests/Mobile/Local/InterventionDetailViewModelTests.cs(navigation Intervention → Contacts)tests/Omika.Tests/Mobile/Local/ShellViewModelTests.cs(adaptation du constructeurPageViewModelFactoryaux 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.
DocumentNoten'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) :
- Exposer une entité Note Intervention dans le contrat mobile : soit
peupler réellement
InterventionDetailsDto.ComptesRendus(ou une collection dédiée) dans les repositoriesGetDetailsAsync(Omika/Batik), soit exposerMessageInternedans le snapshot. - Déclarer un type d'entité dans
SyncEntityTypes(ex.Note) si la création / édition offline est souhaitée. - Ajouter un handler Sync pour le type Note (CREATE / UPDATE) et un payload dédié.
- 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)CollaborateurIdInterventionIdRendezVousIdDateDureeMinutes(durée en minutes)TypeTemps(paramètre configurable,ReferenceValue)DescriptionValide
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) :
- Exposer
SuiviTempsdans le snapshot mobile : ajouter une collectionSuiviTempsdansMobileSnapshotResponse, la charger dansMobileSnapshotService(via un service/repository SuiviTemps à créer), et l'appliquer dansSnapshotApplier(table SQLite localeSuiviTemps). - Déclarer un type d'entité dans
SyncEntityTypes(ex.SuiviTemps) si la création / édition offline est souhaitée. - Ajouter un handler Sync pour le type SuiviTemps (CREATE / UPDATE) et un payload dédié.
- Créer un service métier SuiviTemps (
ISuiviTempsService) et un repository adaptatif (Batik/Omika) qui peuple réellement les données depuis HFSQL. - 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é)DescriptionClientId(relation client, résolue localement)Type/Statut/Priorite(libellésReferenceValueDto)DateOuverture/DateClotureEstArchive
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.cssrc/Omika.App.Local/Models/DossierDetail.cssrc/Omika.App.Local/Services/DossierLocalService.cssrc/Omika.App/ViewModels/DossiersViewModel.cssrc/Omika.App/ViewModels/DossierDetailViewModel.cssrc/Omika.App/Views/DossiersView.axaml(+ code-behind)src/Omika.App/Views/DossierDetailView.axaml(+ code-behind)tests/Omika.Tests/Mobile/Local/DossierLocalServiceTests.cstests/Omika.Tests/Mobile/Local/DossiersViewModelTests.cstests/Omika.Tests/Mobile/Local/DossierDetailViewModelTests.cs
Fichiers modifiés (LOT DOSSIERS MOBILE)
src/Omika.App/Navigation/AppPage.cs(ajoutDossierDetail)src/Omika.App/Navigation/PageViewModelFactory.cs(casDossiers,DossierDetail)src/Omika.App/Navigation/NavigationService.cs(item de navigation Dossiers)src/Omika.App/AppServices.cs(enregistrementIDossierLocalService)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.PhoneouMobile) - 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
Contactsembarqué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
Sitesembarqué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 :
ContactDetailviaClientContactDetailNavigation(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.cssrc/Omika.App.Local/Models/ClientDetail.cssrc/Omika.App.Local/Services/ClientLocalService.cssrc/Omika.App/ViewModels/ClientsViewModel.cssrc/Omika.App/ViewModels/ClientDetailViewModel.cssrc/Omika.App/Views/ClientsView.axaml(+ code-behind)src/Omika.App/Views/ClientDetailView.axaml(+ code-behind)src/Omika.App/Navigation/ClientContactDetailNavigation.cstests/Omika.Tests/Mobile/Local/ClientLocalServiceTests.cstests/Omika.Tests/Mobile/Local/ClientsViewModelTests.cstests/Omika.Tests/Mobile/Local/ClientDetailViewModelTests.cs
Fichiers modifiés (LOT CLIENTS MOBILE)
src/Omika.App/Navigation/AppPage.cs(ajoutClientDetail)src/Omika.App/Navigation/PageViewModelFactory.cs(casClients,ClientDetail)src/Omika.App/Navigation/NavigationService.cs(item de navigation Clients)src/Omika.App/AppServices.cs(enregistrementIClientLocalService)src/Omika.App/ViewModels/ContactDetailViewModel.cs(gestion du retour depuis la fiche Client viaClientContactDetailNavigation)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) etSiteDetail(détail) dansOmika.App.Local.Models. - Payload local : table
Site(colonnePayloadJson), relationClientId.
Champs affichés
Liste (SiteListItem) :
NomCodeType(libellé)Ville(depuis l'adresse)ClientNom(si relation résolue)
Détail (SiteDetail) :
Nom,Code,TypeInformationsComplementaires,EstArchiveAdresse(composée depuis les champs réels du DTO)ClientGuid,ClientNom,ClientCodeContacts(listeSiteContact)Interventions(listeSiteIntervention)
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:ClientGuidSite → Contact:ContactGuidSite → 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.cssrc/Omika.App.Local/Abstractions/ISiteLocalService.cssrc/Omika.App.Local/Services/SiteLocalService.cssrc/Omika.App/ViewModels/SitesViewModel.cssrc/Omika.App/ViewModels/SiteDetailViewModel.cssrc/Omika.App/Navigation/SiteContactDetailNavigation.cssrc/Omika.App/Views/SitesView.axaml(+ code-behind)src/Omika.App/Views/SiteDetailView.axaml(+ code-behind)tests/Omika.Tests/Mobile/Local/SiteLocalServiceTests.cstests/Omika.Tests/Mobile/Local/SitesViewModelTests.cstests/Omika.Tests/Mobile/Local/SiteDetailViewModelTests.cs
Fichiers modifiés (LOT SITES / LIEUX MOBILE)
src/Omika.App/Navigation/AppPage.cs(ajoutSiteDetail)src/Omika.App/Navigation/PageViewModelFactory.cs(casSites,SiteDetail, gestionSiteContactDetailNavigationdansCreateContactDetail)src/Omika.App/Navigation/NavigationService.cs(item de navigation Sites)src/Omika.App/AppServices.cs(enregistrementISiteLocalService)src/Omika.App/ViewModels/ContactDetailViewModel.cs(gestion du retour depuis la fiche Site viaSiteContactDetailNavigation)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
ContactIdsdu 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 listeCollaborateurs(AffectationCollaborateurDto.CollaborateurId).InterventionDetailsDto(Omika.Core.Contracts.Interventions) : porte la relation Collaborateur → Intervention via la listeCollaborateurIds.
Champs affichés
- Liste :
DisplayName(construit dans la couche présentation à partir deNomAffiche, sinonPrenom+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
IsFromXni DTO de retour spécialisé n'est réintroduit.
Offline / read-only
CollaborateursView,CollaborateurDetailView,RendezVousDetail,InterventionDetailfonctionnent 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).
AvatarSizecentralisé.
Fichiers créés
src/Omika.App.Local/Models/CollaborateurDetail.cs(modèles de présentation)src/Omika.App.Local/Abstractions/ICollaborateurLocalService.cssrc/Omika.App.Local/Services/CollaborateurLocalService.cssrc/Omika.App/ViewModels/CollaborateursViewModel.cssrc/Omika.App/ViewModels/CollaborateurDetailViewModel.cssrc/Omika.App/Views/CollaborateursView.axaml(+.cs)src/Omika.App/Views/CollaborateurDetailView.axaml(+.cs)tests/Omika.Tests/Mobile/Local/CollaborateurLocalServiceTests.cstests/Omika.Tests/Mobile/Local/CollaborateursViewModelTests.cstests/Omika.Tests/Mobile/Local/CollaborateurDetailViewModelTests.cs
Fichiers modifiés
src/Omika.App/Navigation/AppPage.cs(ajoutCollaborateurDetail)src/Omika.App/Navigation/PageViewModelFactory.cs(casCollaborateurs,CollaborateurDetail)src/Omika.App/AppServices.cs(enregistrementICollaborateurLocalService)tests/Omika.Tests/Mobile/Local/LocalTestBase.cs(ajoutCollaborateurRepository)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 :
ReferenceTitreEtat(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
ReferenceetTitre. - 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
DatePrevueDebutpuisReference.
Navigation contextuelle
InterventionsView→InterventionDetail(InterventionGuid)viaDetailNavigation(InterventionGuid, NavigationContext(AppPage.Interventions, null)).- Retour contextuel vers la liste Interventions via
NavigationContext. - Aucun flag
IsFromXni 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 queAppPage.Interventionsretourne unInterventionsViewModel.
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é.