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

284 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RECETTE ANDROID — AUDIT AVANT MODIFICATION (OBJECTIF 0 / §17)
> Projet : `D:\Avalonia\Omika` — Base OMIKA réelle. Aucun fichier modifié au stade d'audit.
> Ce document est le livrable de l'audit préalable. Les correctifs ne sont appliqués qu'après validation.
## Sommaire
1. [Architecture rappelée](#architecture)
2. [Tableau de synthèse des problèmes](#tableau)
3. [Cause exacte par problème](#causes)
4. [Classification](#classification)
5. [Preuve de non-régression Batik](#batik)
6. [Routes HTTP appelées](#routes)
7. [Requêtes SQL concernées](#sql)
8. [Inventaire des icônes à remplacer](#icones)
9. [Propositions de SVG monochrome](#svg)
<a name="architecture"></a>
## 1. Architecture rappelée
- **Mobile** (`src/Omika.App.Local`) : offline-first sur SQLite, synchronisé avec l'API via `MobileApiClient`.
- **API** (`src/Omika.Api`) : endpoints REST, sélection de base `Batik`/`Omika` via `ApplicationRuntimeContext.Current.DetectedVersion`.
- **Données** (`src/Omika.Data`) : repositories `Adaptive*` qui délèguent à `Batik*Repository` (schéma historique) ou `Omika*Repository` (schéma physique Omika). Les mappers (`Mapping/Omika`, `Mapping/Batik`) produisent le modèle canonique.
- **Identité publique** : GUID. Omika → GUID physique (colonne `GUID`). Batik → GUID synthétique réversible (`MobileEntityIdentityResolver`).
- **UI** (`src/Omika.App`) : Avalonia, thèmes clair/sombre via `DesignTokens.axaml` (clés `DynamicResource`).
<a name="tableau"></a>
## 2. Tableau de synthèse
| # | PROBLÈME | SYMPTÔME | COUCHE | FICHIER(S) | CAUSE EXACTE | SPÉCIFIQUE OMIKA ? | BATIK IMPACTÉ ? | CORRECTION PROPOSÉE |
|---|---|---|---|---|---|---|---|---|
| 1 | Carte OSM bloquée | 403 / "volunteer-run servers" | ANDROID/UI (tuiles) | `Controls/LieuMap.axaml.cs:97` | `OpenStreetMap.CreateTileLayer()` (Mapsui) utilise un HttpClient sans User-Agent identifiant Omika | NON | NON (commun) | Tile source custom avec User-Agent + cache + attribution ; fallback adresse+Itinéraire |
| 2 | Icônes hétérogènes | Emoji/Unicode/couleurs | UI | multiples (cf. §8) | Emoji/Unicode et couleurs codées en dur | NON | NON | Standardiser en Path monochrome + `DynamicResource` |
| 3 | Boutons entête intervention | fond blanc massif | UI | `Views/RendezVousDetailView.axaml:120-139` | `Background=OnPrimaryColorBrush` (blanc) + emoji `⌖ ⚒ ▧` | NON | NON | Style translucide + SVG monochrome |
| 4 | Agenda équipe : icône groupe | bleue/multicolore | UI | `Views/AgendaView.axaml:377` (`👥`) | emoji `👥` utilisé comme icône de groupe (pas dans `AgendaEquipeView` qui n'a pas d'icône) | NON | NON | Remplacer par SVG/Path monochrome |
| 5 | RDV→intervention introuvable | "Intervention introuvable." | OMIKA DATA / API | `OmikaRendezVousRepository`, `MobileSnapshotService`, `OmikaInterventionRepository.GetDetailsAsync` | (voir §3.5) | OUI | NON | Corriger le mapping/guid Omika |
| 6A | Note CreationUser=0 | CreationUser=0 en base | API / OMIKA DATA | `OmikaInterventionRepository.ResolveCreationUserAsync:1560` | token `CollaborateurId` = PK numérique licence ; `ResolveCreationUserAsync` attend un GUID | OUI | NON | Résoudre l'utilisateur depuis l'identité token (UserId/UT_PKID ou PK numérique) |
| 6B | Compteur notes = 0 | "0 note(s)" | LOCAL/API | `InterventionDetailViewModel.RefreshFromApiAsync` | compteur = `Notes.Count` chargé en arrière-plan via API ; échec/guid → vide | OUI (lié #5) | NON | même cause que #5 + retry |
| 7 | Compteur images/documents = 0 | "0 fichier(s)" | API / OMIKA DATA | `MobileDocumentService.GetByInterventionAsync` + `OmikaDocumentRepository` | résolution guid intervention / filtre catégorie | OUI | NON | même cause que #5 + vérif filtre |
| 8 | Écran Images/Documents | titre redondant + bouton coupé | UI | `Views/InterventionResourcesView.axaml:37,73` | `TextBlock "Images"` redondant + `StackPanel Orientation=Horizontal` non responsive | NON | NON | Supprimer titre + WrapPanel |
| 9 | Création email 404 | 404 modèles courriel | API | `MobileApiClient.cs:761` + `ModeleDocumentEndpoints.cs:18` | **Slash final manquant** : mobile appelle `GET /modele-mails`, API enregistre `GET /modele-mails/` (`MapGroup("/modele-mails").MapGet("/")`) | NON (route commune) | NON (corrigé pour les 2) | `MapGet("/")` → `MapGet("")` (ou slash final côté mobile) |
| 10 | Rapports 404 | 404 modèles rapport | API | `MobileApiClient.cs:771` + `ModeleDocumentEndpoints.cs:28` | **Slash final manquant** : `GET /modele-rapports` vs `GET /modele-rapports/` | NON | NON | idem #9 |
| 11 | Bouton téléphone inopérant | rien ne se lance | ANDROID | `Services/PlatformPhoneLauncher.cs` | `TopLevel.Launcher.LaunchUriAsync` (ACTION_VIEW) ≠ ACTION_DIAL | NON | NON | Launcher Android `ACTION_DIAL` (tel:) |
| 12 | Bouton email inopérant | aucun client mail | ANDROID | `Services/PlatformEmailLauncher.cs` | `LaunchUriAsync` générique ≠ ACTION_SENDTO | NON | NON | Launcher Android `ACTION_SENDTO` (mailto:) |
| 13 | Fiche équipe/collaborateurs responsive | email sur plusieurs lignes, cartes hautes | UI | `CollaborateursView`, `CollaborateurDetailView`, `EquipeDetailView` | layout non responsive | NON | NON | Réorganiser grille responsive |
| 14 | Messages d'erreur techniques | "Response status code…404" affiché | UI | `NouveauEmailViewModel`, `RapportChampsViewModel` | `ErrorMessage = ex.Message` brut | NON | NON | Message utilisateur propre + log technique |
<a name="causes"></a>
## 3. Cause exacte par problème
### 3.1 Carte OpenStreetMap bloquée
- `LieuMap.axaml.cs:97` appelle `OpenStreetMap.CreateTileLayer()` (Mapsui). Le `HttpTileSource` par défaut de Mapsui charge `https://tile.openstreetmap.org/{z}/{x}/{y}.png` via un `HttpClient` **sans User-Agent identifiant Omika** → OSM rejette avec 403 (« volunteer-run servers »).
- Le géocodeur `AddressGeocoder` (Nominatim) a déjà un User-Agent correct (`Omika-Mobile/1.0`), mais pas le chargement des tuiles.
- Pas de cache de tuiles configuré, pas d'attribution OSM visible, pas de fallback propre : l'image 403 brute est affichée.
### 3.5 RDV → « Ouvrir l'intervention » → introuvable
Flux tracé (lecture de code) :
1. `RendezVousDetailViewModel.OpenIntervention` → navigation avec `RendezVous.InterventionGuid` (GUID).
2. `AgendaItem.InterventionGuid` ← `AgendaService.MapToAgendaItem` ← `RendezVousDetailsDto.InterventionId`.
3. `RendezVousDetailsDto.InterventionId` ← `MobileSnapshotService.MapRendezVous` ← `MobileIdentityMapper.ResolveIdString(Intervention, dto.InterventionSourceReference)`.
4. `InterventionSourceReference` ← `OmikaRendezVousMapper` : `{ NumericId: RV_IdInter, EntityGuid: ResolveGuid(interventionGuids, RV_IdInter) }`.
5. Pour Omika, `MobileEntityIdentityResolver.CreateOmikaIdentity` retourne `physical.Guid`. Si `EntityGuid` est null (GUID intervention non résolu), `CreateOmikaIdentity` **lève `InvalidOperationException`** (« aucun GUID physique fourni »).
6. Navigation → `InterventionLocalService.GetInterventionByIdAsync(Guid)` → API `GET /interventions/{guid}` → `OmikaInterventionRepository.GetDetailsAsync` (GUID → `GUID IN (...)`).
**Conclusion de la trace statique (chaîne vérifiée de bout en bout)** : la chaîne d'identité est **correcte** pour Omika :
- `OmikaRendezVousMapper` résout `InterventionSourceReference = { NumericId: RV_IdInter, EntityGuid: Intervention.GUID }` en bulk (`LoadInterventionGuidsAsync`, requête `IN_PKID IN (...)`).
- `MobileSnapshotService.MapRendezVous` → `ResolveIdString` → `CreateOmikaIdentity` retourne le GUID physique.
- `AgendaItem.InterventionGuid` = ce GUID ; navigation → API `GET /interventions/{guid}` → `GetDetailsAsync` (GUID) ; repli local sur la table snapshot `Intervention` (peuplée par `SnapshotApplier` à partir de `LoadInterventionsForRendezVousAsync`, clé = GUID canonique).
- `PhysicalGuidParser.GetStorageCandidates` couvre les formes D / N / D-tronquée.
**Déclencheur exact restant = niveau données** (à confirmer par les logs temporaires demandés au §5 de la recette) :
- soit `RV_IdInter` pointe vers une intervention dont la colonne `GUID` est NULL/invalide (→ `EntityGuid` null → `CreateOmikaIdentity` lèverait, OU `InterventionId` null) ;
- soit l'intervention référencée n'existe pas dans `Intervention` (référence orpheline) ;
- soit un écart de format GUID entre `RV_IdInter→GUID` et l'`Intervention.GUID` réel (byte-order HFSQL).
**Logs temporaires à poser (correction #5)** : `RDV_PKID`, `RDV GUID`, `RV_IdInter`, `Intervention.GUID` résolu, `InterventionGuid` envoyé à la navigation (type + valeur), requête SQL utilisée, résultat. Aucun `IN_NumIntervention` ne doit être réintroduit.
Les compteurs #6B et #7 passent par les mêmes endpoints API (`GET /api/interventions/{guid}/notes`, `GET /api/interventions/{guid}/documents`) : si la résolution d'intervention échoue, listes vides → « 0 note(s) » / « 0 fichier(s) ». #7 a en plus un filtre par catégories de document (`BuildAllowedDocumentCategories` + `IsCategoryAllowed`) qui peut vider la liste si le profil n'a pas le droit `A.DOC.*` correspondant.
### 3.6A Note CreationUser = 0
- Endpoint `AddNoteForInterventionAsync` (`InterventionEndpoints.cs:427`) transmet `httpContext.GetMobileIdentity()?.CollaborateurId` à `InterventionService.AddNoteAsync` → `ResolveCreationUserAsync(collaborateurId)`.
- En **production**, `LicenseEndpoints.BuildSession` (`LicenseEndpoints.cs:72`) pose `CollaborateurId = response.CollaborateurId` = `licence.CollaborateurId.ToString()` (PK numérique du collaborateur, cf. `LicenseService.BuildSuccessAsync:404`).
- `OmikaInterventionRepository.ResolveCreationUserAsync` (`:1560`) fait `Guid.TryParse(collaborateurGuid, out Guid)` : une valeur numérique **échoue** → retour `0` → `CreationUser = 0`.
- `BatikInterventionRepository.ResolveCreationUserAsync` (`:1513`) décode un GUID synthétique Batik : une valeur numérique échoue aussi (`Guid.TryParse` → 0).
- **Racine** : incohérence de type entre la claim `CollaborateurId` du token (numérique en prod) et l'API de `ResolveCreationUserAsync` (attend un GUID).
### 3.9 / 3.10 Email / Rapport 404 — CAUSE EXACTE CONFIRMÉE
- **Slash final manquant.** `MobileApiClient.GetModeleMailsAsync` appelle `GET {BaseUrl}/modele-mails` (sans slash), mais l'API enregistre la route `GET /modele-mails/` (avec slash) via `MapGroup("/modele-mails").MapGet("/")` (`ModeleDocumentEndpoints.cs:18`). En ASP.NET Core minimal API, `MapGroup("/x").MapGet("/")` produit `/x/` et **ne matche pas** `/x` → 404.
- Idem `GetModeleRapportsAsync` → `GET /modele-rapports` vs `GET /modele-rapports/` (`ModeleDocumentEndpoints.cs:28`).
- Les autres routes auditées (`/mot-clefs`, `/generation/email`, `/generation/rapport`, `/api/rapports/champs`, `/api/mails/champs`, `/api/mails`, `/api/mails/apercu`, `/api/rapports`) matchent **exactement**. Le préfixe `/api` est cohérent, les routes sont bien enregistrées (`Program.cs:243-244`), et `MobileAuthMiddleware` ne renvoie jamais 404.
- **Correction** : `MapGet("/")` → `MapGet("")` dans `ModeleDocumentEndpoints` (canonique, fait matcher `/modele-mails` sans slash) — sans impact Batik (route schéma-agnostique, corrige les deux schémas). Alternative : ajouter le slash final dans `MobileApiClient` (l.761/771).
- Référence historique `Mail_Modele` / `ModeleRapport WHERE MRP_EnvoiMob=1` → routes REST actuelles (cf. `docs/modeles-email-rapport.md`).
### 3.11 / 3.12 Téléphone / Email inopérants
- `PlatformPhoneLauncher.DialAsync` et `PlatformEmailLauncher.ComposeAsync` utilisent `TopLevel.Launcher.LaunchUriAsync(uri)` (launcher générique Avalonia). Sur Android, ce launcher utilise `ACTION_VIEW`, qui **ne lance pas le composeur natif** (tel:) ni le client mail de façon fiable.
- Aucun launcher Android spécifique n'existe (`src/Omika.App.Android` ne contient pas de Phone/Email launcher).
- Les ViewModels ignorent le retour `bool` : aucun message si aucun handler n'est disponible.
### 3.14 Messages d'erreur techniques
- `NouveauEmailViewModel.LoadAsync` et `RapportChampsViewModel.LoadAsync` font `catch (Exception ex) { ErrorMessage = ex.Message; }` ; `EnsureSuccessStatusCode()` lève `HttpRequestException` dont le message brut (« Response status code does not indicate success: 404 (Not Found). ») est affiché tel quel.
<a name="classification"></a>
## 4. Classification
| # | Classification |
|---|---|
| 1 | ANDROID/UI (commun, non spécifique données) |
| 2 | UI |
| 3 | UI |
| 4 | UI |
| 5 | OMIKA DATA + API (résolution identité intervention) |
| 6A | API + OMIKA DATA (résolution utilisateur) |
| 6B | LOCAL/API (compteur = source API) |
| 7 | API + OMIKA DATA (documents) |
| 8 | UI |
| 9 | API (route/déploiement) |
| 10 | API (route/déploiement) |
| 11 | ANDROID (launcher) |
| 12 | ANDROID (launcher) |
| 13 | UI |
| 14 | UI |
<a name="batik"></a>
## 5. Preuve de non-régression Batik
- Les corrections **UI/Android** (#1, #2, #3, #4, #8, #11, #12, #13, #14) ne touchent **aucune** requête Batik : elles portent sur les `.axaml`, les launchers Android et les messages d'erreur, partagés entre les deux schémas.
- Les corrections **données** (#5, #6A, #7, #9, #10) seront appliquées **uniquement** dans les providers/repositories/mappers **Omika** (`src/Omika.Data/Repositories/Omika/*`, `Mapping/Omika/*`) ou dans la couche d'identité commune, **sans modifier** `Batik*Repository` ni `Batik*Mapper`.
- `ResolveCreationUserAsync` Batik est déjà isolé dans `BatikInterventionRepository` ; sa correction éventuelle (acceptation du PK numérique) doit être faite symétriquement sans casser le décodage du GUID synthétique Batik (test à prévoir).
- Les routes API (#9, #10) sont communes : l'ajout d'un alias `/api/modele-mails` (si retenu) n'affecte pas le comportement Batik, qui utilise la même API.
<a name="routes"></a>
## 6. Routes HTTP actuellement appelées (MobileApiClient → Omika.Api)
| Méthode client | URL appelée | Route API | Match |
|---|---|---|---|
| GetModeleMailsAsync | `GET /modele-mails` | `ModeleDocumentEndpoints` `/modele-mails` | ✅ (préfixe sans `/api`) |
| GetModeleRapportsAsync | `GET /modele-rapports` | `ModeleDocumentEndpoints` `/modele-rapports` | ✅ |
| GetMotClefsAsync | `GET /mot-clefs` | `ModeleDocumentEndpoints` `/mot-clefs` | ✅ |
| GetEquipesAsync | `GET /equipes` | `EquipeEndpoints` `/equipes` | ✅ |
| GetAllEquipesAsync | `GET /equipes/all` | `EquipeEndpoints` `/equipes/all` | ✅ |
| GetCollaborateurByIdAsync | `GET /collaborateurs/{id}` | `CollaborateurEndpoints` | ✅ |
| GetRendezVousByIdAsync | `GET /rendez-vous/{id}` | `RendezVousEndpoints` | ✅ |
| GetInterventionByIdAsync | `GET /interventions/{id}` | `InterventionEndpoints` | ✅ |
| GetDossierByIdAsync | `GET /dossiers/{id}` | `DossierEndpoints` | ✅ |
| GetClientByIdAsync | `GET /clients/{id}` | `ClientEndpoints` | ✅ |
| GetSiteByIdAsync | `GET /sites/{id}` | `SiteEndpoints` | ✅ |
| GetContactByIdAsync | `GET /contacts/{id}` | `ContactEndpoints` | ✅ |
| GetCompteRenduByRendezVousAsync | `GET /rendez-vous/{id}/compte-rendu` | `CompteRenduEndpoints` | ✅ |
| GetNotesByInterventionAsync | `GET /api/interventions/{id}/notes` | `InterventionEndpoints` mobileGroup | ✅ |
| AddNoteByInterventionAsync | `POST /api/interventions/{id}/notes` | `InterventionEndpoints` mobileGroup | ✅ |
| GetDocumentsByInterventionAsync | `GET /api/interventions/{id}/documents` | `InterventionEndpoints` mobileGroup | ✅ |
| GetRendezVousByInterventionAsync | `GET /api/interventions/{id}/rendez-vous` | `InterventionEndpoints` mobileGroup | ✅ |
| GetEmailsByInterventionAsync | `GET /api/interventions/{id}/emails` | `InterventionEndpoints` mobileGroup | ✅ |
| AddEmailByInterventionAsync | `POST /api/interventions/{id}/emails` | `InterventionEndpoints` mobileGroup | ✅ |
| GetSmtpConfigurationsAsync | `GET /api/mails/configurations-smtp` | `RapportChampEndpoints` | ✅ |
| GetRapportChampsAsync | `POST /api/rapports/champs` | `RapportChampEndpoints` | ✅ |
| GetMailChampsAsync | `POST /api/mails/champs` | `RapportChampEndpoints` | ✅ |
| CreateEmailModeleAsync | `POST /api/mails` | `RapportChampEndpoints` | ✅ |
| GetEmailModeleApercuAsync | `POST /api/mails/apercu` | `RapportChampEndpoints` | ✅ |
| CreateRapportAsync | `POST /api/rapports` | `RapportChampEndpoints` | ✅ |
| GenerateEmailAsync | `POST /generation/email` | `ModeleDocumentEndpoints` | ✅ |
| GenerateRapportAsync | `POST /generation/rapport` | `ModeleDocumentEndpoints` | ✅ |
| SearchInterventionsAsync | `POST /api/interventions/search` | `InterventionEndpoints` mobileGroup | ✅ |
| GetInterventionsByIdsAsync | `POST /api/interventions/by-ids` | `InterventionEndpoints` mobileGroup | ✅ |
| SearchDossiersAsync | `GET /api/dossiers` | `DossierEndpoints` mobileGroup | ✅ |
| SearchClientsAsync | `GET /api/clients` | `ClientEndpoints` mobileGroup | ✅ |
| SearchSitesAsync | `GET /api/sites` | `SiteEndpoints` mobileGroup | ✅ |
| SearchCollaborateursAsync | `GET /collaborateurs` | `CollaborateurEndpoints` | ✅ (sans `/api`) |
| GetSuivisTempsAsync | `GET /api/suivis-temps` | `SuiviTempsEndpoints` | ✅ |
| GetBootstrapAsync | `GET /api/mobile/bootstrap` | `MobileBootstrapEndpoints` | ✅ |
| GetSnapshotAsync | `GET /api/sync/snapshot` | `SyncEndpoints` | ✅ |
| SendOperationAsync | `POST /api/sync/operations` | `SyncEndpoints` | ✅ |
| GetStatusAsync | `GET /api/sync/status/{id}` | `SyncEndpoints` | ✅ |
| ActivateAsync | `POST /api/license/activate` | `LicenseEndpoints` | ✅ |
| ValidateAsync | `POST /api/license/validate` | `LicenseEndpoints` | ✅ |
| GetDevelopmentSessionAsync | `POST /api/dev/session` | `DevelopmentAuthEndpoints` (Dev only) | ✅ |
> **Incohérence constatée** : le mobile utilise un mélange de routes avec `/api` et sans `/api` (ex. `/modele-mails` sans préfixe vs `/api/mails`). Les routes « modèle » (`/modele-mails`, `/modele-rapports`, `/mot-clefs`, `/generation/*`) sont les seules sans `/api`. C'est cohérent avec le code API actuel mais fragile et à uniformiser.
<a name="sql"></a>
## 7. Requêtes SQL concernées (côté API, base HFSQL)
| Problème | Requête |
|---|---|
| #5 (intervention par GUID) | `SELECT … FROM Intervention WHERE GUID IN (?)` (formes D/N/tronquée via `PhysicalGuidParser.GetStorageCandidates`) — `OmikaInterventionRepository.GetDetailsAsync` |
| #5 (RDV→intervention) | `SELECT … FROM RDV_Planning … LEFT JOIN Intervention I ON RV.RV_IdInter = I.IN_PKID` (résolution bulk `RV_IdInter → Intervention.GUID`) — `OmikaRendezVousRepository` |
| #6A (user) | `SELECT Utilisateur.UT_PKID FROM Utilisateur INNER JOIN Collaborateur ON Collaborateur.CO_PKID = Utilisateur.UT_IDCollaborateur WHERE Collaborateur.GUID IN (?)` — `OmikaInterventionRepository.ResolveCreationUserAsync` |
| #6A (Batik) | `SELECT UT_PKID FROM Utilisateur WHERE UT_IDCollaborateur = ?` — `BatikInterventionRepository.ResolveCreationUserAsync` |
| #6B/#7 (notes) | `SELECT MI_PKID, GUID, MI_Message, CreationDate, CreationUser FROM MessageInterne WHERE MI_IDIntervention = ? AND (MI_IDTypeMessage IS NULL OR MI_IDTypeMessage <= 10)` — `OmikaInterventionRepository.GetNotesAsync` |
| #7 (documents) | résolution intervention GUID → `IN_PKID` puis requête documents (filtre `DC_IDIntervention` / catégorie / état) — `OmikaDocumentRepository.GetByInterventionAsync` |
| #7 (catégories) | `BuildAllowedDocumentCategories` → filtrage par clés `T.DOC.*` selon droits `A.DOC.*` |
<a name="icones"></a>
## 8. Inventaire des icônes à remplacer (extrait — inventaire complet en cours)
| Fichier | Ligne | Actuel | Fonction | Proposition |
|---|---|---|---|---|
| `Views/RendezVousDetailView.axaml` | 122 | `⌖ Itinéraire` (emoji) | Itinéraire | Path "navigation/itinéraire" monochrome |
| `Views/RendezVousDetailView.axaml` | 134 | `⚒ Ouvrir l'intervention` | Ouvrir intervention | Path "marteau/outil" monochrome |
| `Views/RendezVousDetailView.axaml` | 139 | `▧ Compte rendu` | Compte rendu | Path "document" monochrome |
| `Views/InterventionResourcesView.axaml` | 37/73 | titre texte redondant | (titre) | supprimer |
| `Controls/LieuMap.axaml` | 25 | `⌖` (TextBlock) | épingle | Path "pin" monochrome |
| `ViewModels/NouveauEmailViewModel.cs` | 144 | `✉ Email vierge` | modèle vierge | (libellé, pas d'icône) |
| `ViewModels/NouveauEmailViewModel.cs` | 888 | `📷 {Nom}` | pièce jointe photo | Path "image" |
| `ViewModels/RapportChampsViewModel.cs` | 606 | `✓` / `!` (Indicateur) | état rubrique | Path check/alert monochrome |
| `Views/AgendaEquipeView.axaml` | (filtre Équipe) | icône groupe colorée | filtre équipe | Path "group" monochrome |
| `Views/InterventionDocumentsView.axaml` | 17 | `← Retour` | retour | Path "arrow-left" |
Les contrôles `CallButton.axaml` et `EmailButton.axaml` utilisent **déjà** des `Path` monochromes `Fill="{DynamicResource TextPrimaryBrush}"` : conformes, à conserver comme référence visuelle.
<a name="svg"></a>
## 9. Propositions de SVG monochrome (famille unique)
Famille retenue : **pictogrammes "Line/Outline" sobres** (traits fins, style Lucide/Material Outlined), **un seul style** (trait 1.5–2, pas de remplissage multicolore), couleur portée par `Foreground="{DynamicResource PrimaryBrush}"` ou `Fill="{DynamicResource …}"` — **jamais codée en dur**.
| Pictogramme | Usage | Path (24×24 viewBox, données type Material) |
|---|---|---|
| Téléphone | appel | `M6.62,10.79c1.44,2.83 …` (déjà dans CallButton) |
| Email | courriel | `M20,4H4C2.9,4 …` (déjà dans EmailButton) |
| Itinéraire/GPS | itinéraire | navigation arrow / pin |
| Intervention (marteau/outil) | ouvrir intervention | wrench/hammer |
| Rapport | rapport | document + lignes |
| Note | note | edit/pencil |
| Image | photo | image frame |
| Document | document | file |
| Collaborateurs | personne | person outline |
| Équipe | groupe | people/group outline |
| Calendrier | agenda | calendar |
| Paramètres | paramètres | gear |
| Synchronisation | sync | refresh/circular arrows |
| Retour | retour | arrow-left |
| Ajouter | ajouter | plus |
| Télécharger | télécharger | download arrow |
| Épingle | carte | location pin |
Chaque pictogramme sera matérialisé par un `Path` Avalonia (monochrome) ou un `StreamGeometry`, avec `Fill`/`Foreground` en `DynamicResource` pour rester compatible clair/sombre.
## 8bis. Couleurs codées en dur à remplacer par `DynamicResource`
| Fichier | Ligne | Valeur | Remplacer par |
|---|---|---|---|
| `Views/RendezVousDetailView.axaml` | 143 | `#E53935` / `White` (bouton Annuler) | `DangerBrush` / `OnDanger` |
| `Views/RapportChampsView.axaml` | 108/115/122 | `#22c55e` / `#f59e0b` / `#ef4444` | `SuccessBrush` / `WarningBrush` / `DangerBrush` |
| `Views/RapportChampsView.axaml` | 253 | `#e5e7eb` | `SurfaceTertiaryBrush` |
| `Views/RapportChampsView.axaml` | 138,233,258 | `ThemeForegroundLowBrush` (**clé INEXISTANTE** dans DesignTokens) | `TextMutedBrush` |
| 10 fichiers (badges héros) | — | `#26FFFFFF` | token `OnPrimaryContainer`/`HeroChip` |
| `Controls/ImageEditorControl.axaml` | — | `#E53935…`, `#FFFFFF` | ressources centralisées |
| `Views/*Converter.cs` (`NavItemTextBrushConverter`, `SyncStateToBrushConverter`) | — | `Brushes.Gray` fallback | ressource `TextMutedBrush` |
## 8ter. Correctifs appliqués (lot 1 — compilation Android 0 erreur vérifiée)
| # | Correctif | Fichiers modifiés | Vérification |
|---|---|---|---|
| 9/10 | `MapGet("/")` → `MapGet("")` (route sans slash final) | `src/Omika.Api/Endpoints/ModeleDocumentEndpoints.cs` | Omika.Api 0 erreur |
| 6A | `ResolveCreationUserAsync` accepte PK numérique + GUID (Omika & Batik) | `OmikaInterventionRepository.cs`, `BatikInterventionRepository.cs` | Omika.Data 0 erreur |
| 11/12 | Launchers natifs Android `ACTION_DIAL`/`ACTION_SENDTO` + hook `PlatformLaunchers` | `AndroidPhoneLauncher.cs`, `AndroidEmailLauncher.cs` (nouveaux), `PlatformLaunchers.cs` (nouveau), `AppServices.cs`, `Application.cs` | Android 0 erreur |
| 1 | User-Agent OSM explicite `OmikaMobile/1.0 (+https://www.applilogik.fr)` | `Controls/LieuMap.axaml.cs` | Omika.App 0 erreur |
| 3 | Boutons d'en-tête translucides `heroAction` + icônes Path monochrome | `Views/RendezVousDetailView.axaml`, `Styles/Controls.axaml` | Omika.App 0 erreur |
| 8 | Suppression titre redondant + `WrapPanel` (boutons wrappables) | `Views/InterventionResourcesView.axaml` | Omika.App 0 erreur |
| 14 | Messages utilisateur propres + log technique | `NouveauEmailViewModel.cs`, `RapportChampsViewModel.cs` | Omika.App 0 erreur |
| 5 | Logs temporaires de traçage RDV→intervention | `RendezVousDetailViewModel.cs`, `InterventionLocalService.cs` | Omika.App 0 erreur |
### Lot 2 — Icônes monochromes + couleurs + responsive (compilation 0 erreur)
| # | Correctif | Fichiers |
|---|---|---|
| 2 | Bug clé `ThemeForegroundLowBrush` (inexistante) → `TextMutedBrush` | `RapportChampsView.axaml`, `NouveauEmailView.axaml` |
| 2 | Couleurs sémantiques en dur → `Success/Warning/Danger/SurfaceTertiaryBrush` | `RapportChampsView.axaml` |
| 2 | Icônes de tuiles emoji → Path monochrome `PrimaryTextColorBrush` (✎ ▧ ✉ 🛡 📄) | `InterventionDetailView.axaml` |
| 4 | Icône groupe `👥` → Path « people » monochrome | `AgendaView.axaml` |
| 2 | Icône calendrier `📅` → Path « calendar » monochrome | `Controls/DateField.axaml` |
| 2 | Tokens `HeroChipBrush` / `HeroBorderBrush` ajoutés | `DesignTokens.axaml` |
| 13 | Email non tronqué (`TextWrapping`) fiche collaborateur + liste collaborateurs | `CollaborateurDetailView.axaml`, `CollaborateursView.axaml` |
> Reste (lots suivants) : `#26FFFFFF` → `HeroChipBrush` sur les badges (cosmétique), autres emoji (`↻ ⌖ ⚠ ⓘ 🗑 ✎ 🖼 🔒 ★/☆ ✉ + → ←`), responsive `FilterGrid` agenda équipe, et les correctifs de données `#5/#6B/#7` (résolution intervention + filtre catégories `T.DOC.*`).
> Build Android : `dotnet build src\Omika.App.Android\Omika.App.Android.csproj -c Debug -m:1 -nodeReuse:false` → **0 erreur** (SDK 10.0.302 via `global.json`). Les builds nécessitent `-m:1 -nodeReuse:false` (bug de nœud MSBuild parallèle dans l'environnement) et un accès élargi pour la télémétrie Avalonia (écrit hors workspace).
## 9. État des sous-analyses
- ✅ Route API (#9/#10) : **confirmé** — slash final manquant (`/modele-mails` vs `/modele-mails/`).
- ✅ Icônes + responsive (#2/#3/#4/#8/#13) : **confirmé** — inventaire ~40 emoji/Unicode, aucune couleur codée en dur dans les Path, liste des couleurs en dur à remplacer (dont clé manquante `ThemeForegroundLowBrush`).
- ✅ Trace RDV→intervention (#5/#6B/#7) : chaîne d'identité vérifiée de bout en bout (correcte) ; déclencheur exact = niveau données, à confirmer par les logs temporaires prévus en correction.