# ============================================================ # Omika Diagnostic Center — Seed du référentiel COMPLET # seed-referentiel-complet.ps1 # ============================================================ # Génère et (optionnellement) applique le référentiel administrable # diagnostics_reference (T24-A) pour : # * les 30 ScreenCode du catalogue applicatif (fenêtres / écrans) ; # * les 3 FeatureCode « action métier » historiques ; # * TOUS les FeatureCode de boutons, dérivés AUTOMATIQUEMENT depuis les # méthodes [RelayCommand] des ViewModels (src\Omika.App\ViewModels). # # PRINCIPE : le code de chaque bouton est dérivé du NOM de la méthode # (même algorithme que DiagnosticFeatureCodes.FromCommandName) puis un # libellé français lui est associé. Le script SCANNE le code source : # re-exécutable après ajout d'un bouton, il reste toujours à jour. # # IDEMPOTENT : INSERT ... ON CONFLICT DO NOTHING. Un libellé déjà modifié # par un Admin (via Diagnostics.Web /Referentiel/Admin) n'est JAMAIS écrasé. # NON DESTRUCTIF : aucun DROP, aucun DELETE, aucune mise à jour. # # Usage : # .\scripts\seed-referentiel-complet.ps1 # génère le .sql uniquement # .\scripts\seed-referentiel-complet.ps1 -Apply # génère + applique via psql # .\scripts\seed-referentiel-complet.ps1 -Apply -Database omika_diagnostics_preprod # .\scripts\seed-referentiel-complet.ps1 -Apply -Server SRV-OMIKA -Username omika_diagnostics_web # .\scripts\seed-referentiel-complet.ps1 -Apply -Force # sans demande de confirmation # # Remarques : # - Nécessite psql (PostgreSQL) uniquement pour -Apply. # - Rôle de connexion : 'postgres' (défaut, superutilisateur, comme 011) ou # 'omika_diagnostics_web' (moindre privilège : INSERT accordé par 011). # 'omika_diagnostics_owner' est NOLOGIN et ne peut PAS servir ici. # - Le mot de passe n'est JAMAIS en clair : saisi en mode sécurisé, ou # fourni via la variable d'environnement PGPASSWORD (jamais en paramètre). # - Les codes de boutons dont le libellé n'est pas encore dans la table # interne $featureLabels sont signalés (libellé de repli « humanisé »). # ============================================================ [CmdletBinding()] param( [string]$Database = 'omika_diagnostics', [string]$Server = 'localhost', [int]$Port = 5432, [string]$Username = 'postgres', [string]$PsqlPath = '', [string]$OutputSql = '', [switch]$Apply, [switch]$Force ) $ErrorActionPreference = 'Stop' # Racine du dépôt = parent du dossier scripts\. $RepoRoot = Split-Path -Parent $PSScriptRoot $ViewModelsDir = Join-Path $RepoRoot 'src\Omika.App\ViewModels' if (-not (Test-Path -LiteralPath $ViewModelsDir)) { throw "Dossier ViewModels introuvable : $ViewModelsDir" } if ([string]::IsNullOrWhiteSpace($OutputSql)) { $OutputSql = Join-Path $RepoRoot 'scripts\dev\seed_referentiel_complet.sql' } # ============================================================ # 1. Portage EXACT de DiagnosticFeatureCodes.FromCommandName # (retire le suffixe Async, PascalCase -> UPPER_SNAKE_CASE). # ============================================================ function ConvertTo-FeatureCode { param([string]$MethodName) $v = $MethodName.Trim() if ([string]::IsNullOrWhiteSpace($v)) { return 'ACTION' } if ($v.EndsWith('Async', [System.StringComparison]::Ordinal)) { $v = $v.Substring(0, $v.Length - 5) } $sb = New-Object System.Text.StringBuilder for ($i = 0; $i -lt $v.Length; $i++) { $c = $v[$i] if ([char]::IsUpper($c)) { $prevLower = ($i -gt 0) -and [char]::IsLower($v[$i - 1]) $prevUpper = ($i -gt 0) -and [char]::IsUpper($v[$i - 1]) $nextLower = ($i + 1 -lt $v.Length) -and [char]::IsLower($v[$i + 1]) if (($prevLower -or ($prevUpper -and $nextLower)) -and $sb.Length -gt 0 -and $sb[$sb.Length - 1] -ne '_') { [void]$sb.Append('_') } [void]$sb.Append([char]::ToUpperInvariant($c)) } elseif ($c -eq '_' -or $c -eq '-' -or $c -eq ' ') { if ($sb.Length -gt 0 -and $sb[$sb.Length - 1] -ne '_') { [void]$sb.Append('_') } } else { [void]$sb.Append([char]::ToUpperInvariant($c)) } } if ($sb.Length -eq 0) { return 'ACTION' } return $sb.ToString() } # ============================================================ # 2. Extraction des noms de méthodes [RelayCommand]. # ============================================================ function Get-RelayCommandMethodNames { param([string]$Directory) $names = New-Object System.Collections.Generic.List[string] $sigPattern = '^\s*(?:public|private|internal|protected)?\s*(?:async\s+)?(?:Task(?:<[^>]+>)?|void|bool|[A-Za-z_][A-Za-z0-9_<>,\.\[\]\? ]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(' foreach ($file in (Get-ChildItem -Path $Directory -Filter *.cs | Sort-Object Name)) { $lines = Get-Content -Path $file.FullName for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '\[RelayCommand') { for ($j = $i + 1; $j -lt [Math]::Min($i + 8, $lines.Count); $j++) { if ($lines[$j] -match $sigPattern) { $names.Add($Matches[1]) break } } } } } return $names } # ============================================================ # 3. Catalogue des 30 ScreenCode (labels = DiagnosticScreenCodeCatalog). # ============================================================ $screenCodes = @( [pscustomobject]@{ Code = 'AGENDA'; Label = 'Agenda' }, [pscustomobject]@{ Code = 'INTERVENTION_SEARCH'; Label = 'Recherche d''interventions' }, [pscustomobject]@{ Code = 'DOSSIER_LIST'; Label = 'Dossiers' }, [pscustomobject]@{ Code = 'TEAM'; Label = 'Équipe' }, [pscustomobject]@{ Code = 'TEAM_DETAIL'; Label = 'Détail d''équipe' }, [pscustomobject]@{ Code = 'CLIENT_LIST'; Label = 'Clients' }, [pscustomobject]@{ Code = 'SITE_LIST'; Label = 'Sites' }, [pscustomobject]@{ Code = 'SETTINGS'; Label = 'Paramètres' }, [pscustomobject]@{ Code = 'RENDEZVOUS_DETAIL'; Label = 'Détail de rendez-vous' }, [pscustomobject]@{ Code = 'AGENDA_TEAM'; Label = 'Agenda d''équipe' }, [pscustomobject]@{ Code = 'INTERVENTION_DETAIL'; Label = 'Détail d''intervention' }, [pscustomobject]@{ Code = 'INTERVENTION_DOCUMENTS'; Label = 'Documents d''intervention' }, [pscustomobject]@{ Code = 'INTERVENTION_NOTES'; Label = 'Notes d''intervention' }, [pscustomobject]@{ Code = 'INTERVENTION_NOTES_ADMIN'; Label = 'Notes administratives' }, [pscustomobject]@{ Code = 'INTERVENTION_EMAILS'; Label = 'Courriels d''intervention' }, [pscustomobject]@{ Code = 'EMAIL'; Label = 'Nouveau courriel' }, [pscustomobject]@{ Code = 'RAPPORT'; Label = 'Rapport' }, [pscustomobject]@{ Code = 'INTERVENTION_RESOURCES'; Label = 'Contenus d''intervention' }, [pscustomobject]@{ Code = 'PHOTO_VIEWER'; Label = 'Visionneuse photo' }, [pscustomobject]@{ Code = 'IMAGE_EDITOR'; Label = 'Éditeur d''image' }, [pscustomobject]@{ Code = 'INTERVENTION_CONTACTS'; Label = 'Contacts d''intervention' }, [pscustomobject]@{ Code = 'CONTACT_DETAIL'; Label = 'Détail de contact' }, [pscustomobject]@{ Code = 'DOSSIER_DETAIL'; Label = 'Détail de dossier' }, [pscustomobject]@{ Code = 'CLIENT_DETAIL'; Label = 'Détail de client' }, [pscustomobject]@{ Code = 'SITE_DETAIL'; Label = 'Détail de site' }, [pscustomobject]@{ Code = 'COLLABORATEUR_DETAIL'; Label = 'Détail de collaborateur' }, [pscustomobject]@{ Code = 'INTERVENTION_EDIT'; Label = 'Formulaire intervention' }, [pscustomobject]@{ Code = 'SUIVI_TEMPS_CREATE'; Label = 'Saisie de temps' }, [pscustomobject]@{ Code = 'COMPTE_RENDU_CREATE'; Label = 'Compte rendu' }, [pscustomobject]@{ Code = 'SYNC_QUEUE'; Label = 'File de synchronisation' } ) # ============================================================ # 4. Libellés français des FeatureCode (boutons + 3 actions métier). # Clé = FeatureCode dérivé ; valeur = libellé métier français. # ============================================================ $semanticFeatureOrder = @('INTERVENTION_SEARCH_RUN', 'REPORT_GENERATE', 'EMAIL_SEND') $featureLabels = @{ # --- Actions métier historiques (semantic success events) --- 'INTERVENTION_SEARCH_RUN' = 'Recherche d''intervention' 'REPORT_GENERATE' = 'Génération de rapport' 'EMAIL_SEND' = 'Envoi de courriel' # --- Boutons (dérivés des commandes [RelayCommand]) --- 'ADD_COLLABORATEUR' = 'Ajouter un collaborateur' 'ADD_DOCUMENT' = 'Ajouter un document' 'ADD_NOTE' = 'Ajouter une note' 'ADD_PHOTO' = 'Ajouter une photo' 'ADD_PHOTO_FROM_CAMERA' = 'Prendre une photo' 'ADD_TIME' = 'Ajouter un temps' 'AJOUTER_DOCUMENT' = 'Ajouter un document' 'AJOUTER_PHOTO' = 'Ajouter une photo' 'ANNULER' = 'Annuler' 'ANNULER_CONFIRMATION_ANNULATION' = 'Annuler la confirmation' 'APPLY_CUSTOM_COLOR' = 'Appliquer la couleur personnalisée' 'APPLY_PRIMARY_COLOR' = 'Appliquer la couleur principale' 'ARCHIVE' = 'Archiver' 'ASSIGN_LICENCE' = 'Attribuer une licence' 'BACK' = 'Retour' 'BASCULER_OPTION' = 'Cocher / décocher l''option' 'BASCULER_SELECTION' = 'Sélectionner la pièce jointe' 'CALL' = 'Appeler' 'CALL_COLLABORATEUR' = 'Appeler le collaborateur' 'CALL_CONTACT' = 'Appeler le contact' 'CALL_PRIMARY' = 'Appeler (contact principal)' 'CALL_SUIVI_PAR' = 'Appeler (suivi par)' 'CANCEL' = 'Annuler' 'CANCEL_ARCHIVE' = 'Annuler l''archivage' 'CANCEL_SCAN' = 'Annuler le scan' 'CLEAR_ALL_DATA' = 'Vider toutes les données' 'CLEAR_DOCUMENTS' = 'Vider les documents' 'CONFIRM_ARCHIVE' = 'Confirmer l''archivage' 'CONFIRMER_ANNULATION' = 'Confirmer l''annulation' 'CONTINUER' = 'Continuer' 'DELETE_DRAFT' = 'Supprimer le brouillon' 'DEMANDER_ANNULATION' = 'Demander l''annulation' 'DOWNLOAD' = 'Télécharger' 'DOWNLOAD_ALL_DOCUMENTS' = 'Télécharger tous les documents' 'DOWNLOAD_ALL_PHOTOS' = 'Télécharger toutes les photos' 'EDIT' = 'Modifier' 'EDIT_DRAFT' = 'Modifier le brouillon' 'EMAIL' = 'Envoyer un courriel' 'EMAIL_COLLABORATEUR' = 'Écrire au collaborateur' 'EMAIL_CONTACT' = 'Écrire au contact' 'EMAIL_PRIMARY' = 'Écrire (contact principal)' 'EMAIL_SUIVI_PAR' = 'Écrire (suivi par)' 'ENREGISTRER' = 'Enregistrer' 'ENROLL' = 'Enrôler' 'ENVOYER' = 'Envoyer' 'ENVOYER_EMAIL' = 'Envoyer par courriel' 'GENERER' = 'Générer' 'GO_TO_TODAY' = 'Aller à aujourd''hui' 'HIDE_EDITOR' = 'Masquer l''éditeur' 'INITIALIZE' = 'Initialiser' 'LOAD' = 'Charger' 'LOAD_AVEC_DESTINATAIRE' = 'Charger avec destinataire' 'LOAD_AVEC_RAPPORT' = 'Charger avec rapport' 'LOAD_EDITION' = 'Charger en édition' 'LOAD_SCOPES' = 'Charger les périmètres' 'LOGOUT' = 'Se déconnecter' 'MODIFIER' = 'Modifier' 'NAVIGATE' = 'Naviguer' 'NEW_INTERVENTION' = 'Nouvelle intervention' 'NEXT' = 'Suivant' 'NEXT_DAY' = 'Jour suivant' 'NEXT_WEEK' = 'Semaine suivante' 'NOUVEAU' = 'Nouveau' 'OPEN' = 'Ouvrir' 'OPEN_CLIENT' = 'Ouvrir le client' 'OPEN_COLLABORATEUR' = 'Ouvrir le collaborateur' 'OPEN_COMPTE_RENDU' = 'Ouvrir le compte rendu' 'OPEN_CONTACT' = 'Ouvrir le contact' 'OPEN_CONTACTS' = 'Ouvrir les contacts' 'OPEN_DOCUMENTS' = 'Ouvrir les documents' 'OPEN_DOSSIER' = 'Ouvrir le dossier' 'OPEN_EMAILS' = 'Ouvrir les courriels' 'OPEN_EQUIPE' = 'Ouvrir l''équipe' 'OPEN_FIRST_RENDEZ_VOUS' = 'Ouvrir le premier rendez-vous' 'OPEN_IMAGES' = 'Ouvrir les images' 'OPEN_IMAGES_ET_DOCUMENTS' = 'Ouvrir images et documents' 'OPEN_INTERVENTION' = 'Ouvrir l''intervention' 'OPEN_ITINERAIRE' = 'Ouvrir l''itinéraire' 'OPEN_NOTES' = 'Ouvrir les notes' 'OPEN_NOTES_ADMINISTRATIVES' = 'Ouvrir les notes administratives' 'OPEN_RAPPORT' = 'Ouvrir le rapport' 'OPEN_RENDEZ_VOUS' = 'Ouvrir le rendez-vous' 'OPEN_SITE' = 'Ouvrir le site' 'OPEN_SYNC_QUEUE' = 'Ouvrir la file de synchronisation' 'OPEN_TEAM_AGENDA' = 'Ouvrir l''agenda d''équipe' 'OUVRIR_BROUILLON' = 'Ouvrir le brouillon' 'OUVRIR_RUBRIQUE' = 'Ouvrir une rubrique' 'PREVIOUS' = 'Précédent' 'PREVIOUS_DAY' = 'Jour précédent' 'PREVIOUS_WEEK' = 'Semaine précédente' 'RELEASE_LICENCE' = 'Libérer la licence' 'REMOVE_COLLABORATEUR' = 'Retirer un collaborateur' 'RESET_APP' = 'Réinitialiser l''application' 'RESET_FILTERS' = 'Réinitialiser les filtres' 'RESET_SEARCH' = 'Réinitialiser la recherche' 'RETOUR' = 'Retour' 'RETOUR_RUBRIQUES' = 'Retour aux rubriques' 'RETRY' = 'Réessayer' 'RUBRIQUE_PRECEDENTE' = 'Rubrique précédente' 'RUBRIQUE_SUIVANTE' = 'Rubrique suivante' 'SAVE' = 'Enregistrer' 'SAVE_EDITOR' = 'Enregistrer l''éditeur' 'SAVE_PAUSE_DEJEUNER' = 'Enregistrer la pause déjeuner' 'SCAN' = 'Scanner' 'SEARCH' = 'Rechercher' 'SELECT_CALENDAR_DAY' = 'Sélectionner un jour' 'SELECT_CLIENTS' = 'Onglet Clients' 'SELECT_CONTACTS' = 'Onglet Contacts' 'SELECT_DAY' = 'Sélectionner le jour' 'SELECT_DOSSIERS' = 'Onglet Dossiers' 'SELECT_INTERVENTIONS' = 'Onglet Interventions' 'SELECT_RENDEZ_VOUS' = 'Onglet Rendez-vous' 'SELECT_SITES' = 'Onglet Sites' 'SELECT_TAB' = 'Changer d''onglet' 'SEND_SUPPORT_EMAIL' = 'Écrire au support' 'SET_PRIMARY_COLOR' = 'Changer la couleur principale' 'SET_THEME_MODE' = 'Changer le thème' 'SHOW_CALENDAR' = 'Afficher le calendrier' 'SHOW_DAY' = 'Afficher le jour' 'SHOW_PINNED' = 'Afficher les épinglés' 'SHOW_RECENT' = 'Afficher les récents' 'SHOW_WEEK' = 'Afficher la semaine' 'SYNC_NOW' = 'Synchroniser maintenant' 'TOGGLE_PINNED' = 'Épingler / désépingler' 'VALIDER' = 'Valider' } # ============================================================ # 5. Dérivation des boutons depuis le code source + contrôle de # couverture (un code non mappé => libellé de repli + avertissement). # ============================================================ function ConvertTo-HumanizedLabel { param([string]$Code) $words = $Code -split '_' $parts = foreach ($w in $words) { if ($w.Length -eq 0) { continue } if ($w.Length -eq 1) { $w.ToUpperInvariant() } else { $w.Substring(0, 1).ToUpperInvariant() + $w.Substring(1).ToLowerInvariant() } } return ($parts -join ' ') } $methodNames = Get-RelayCommandMethodNames -Directory $ViewModelsDir $buttonCodes = $methodNames | ForEach-Object { ConvertTo-FeatureCode $_ } | Sort-Object -Unique $unmapped = @() foreach ($code in $buttonCodes) { if (-not $featureLabels.ContainsKey($code)) { $unmapped += $code $featureLabels[$code] = ConvertTo-HumanizedLabel -Code $code } } if ($unmapped.Count -gt 0) { Write-Warning "Codes de boutons sans libellé explicite (libellé humanisé appliqué) : $($unmapped -join ', ')" } # ============================================================ # 6. Génération du SQL (INSERT ... ON CONFLICT DO NOTHING). # ============================================================ function ConvertTo-SqlLiteral { param([string]$Value) return "'" + ($Value -replace "'", "''") + "'" } $rows = New-Object System.Collections.Generic.List[string] # ScreenCodes : sort_order 10..290 (pas de 10). $screenSort = 10 foreach ($s in $screenCodes) { $rows.Add(" ('SCREEN_CODE', $(ConvertTo-SqlLiteral $s.Code), $(ConvertTo-SqlLiteral $s.Label), NULL, TRUE, $screenSort, now(), now())") $screenSort += 10 } # FeatureCode « actions métier » : sort_order 10/20/30 (aligné 011). $semanticSort = 10 foreach ($code in $semanticFeatureOrder) { if ($featureLabels.ContainsKey($code)) { $rows.Add(" ('FEATURE_CODE', $(ConvertTo-SqlLiteral $code), $(ConvertTo-SqlLiteral $featureLabels[$code]), NULL, TRUE, $semanticSort, now(), now())") } $semanticSort += 10 } # FeatureCode « boutons » : tri alphabétique, sort_order 1000, 1010, ... $buttonSort = 1000 foreach ($code in ($buttonCodes | Sort-Object)) { $rows.Add(" ('FEATURE_CODE', $(ConvertTo-SqlLiteral $code), $(ConvertTo-SqlLiteral $featureLabels[$code]), NULL, TRUE, $buttonSort, now(), now())") $buttonSort += 10 } $sql = @" -- ===================================================================== -- Omika Diagnostic Center — SEED RÉFÉRENTIEL COMPLET (généré) -- Généré par scripts\seed-referentiel-complet.ps1 -- ===================================================================== -- NON DESTRUCTIF : INSERT ... ON CONFLICT DO NOTHING. -- IDEMPOTENT : un libellé modifié par un Admin n'est JAMAIS écrasé. -- Cible : table diagnostics_reference (T24-A, créée par 011_diagnostics_reference.sql). -- Aucun mot de passe, aucune donnée personnelle, aucun secret. -- ===================================================================== INSERT INTO diagnostics_reference (reference_type, code, label, description, enabled, sort_order, created_at_utc, updated_at_utc) VALUES $($rows -join ",`n") ON CONFLICT DO NOTHING; -- ===================================================================== -- FIN. Vérification : -- SELECT reference_type, COUNT(*) FROM diagnostics_reference GROUP BY reference_type; -- ===================================================================== "@ $null = New-Item -ItemType Directory -Path (Split-Path -Parent $OutputSql) -Force # UTF-8 SANS BOM (psql), indépendant de la version PowerShell. $utf8NoBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($OutputSql, $sql, $utf8NoBom) # ============================================================ # 7. Application via psql (optionnel). # ============================================================ if ($Apply) { $psql = $PsqlPath if ([string]::IsNullOrWhiteSpace($psql)) { $cmd = Get-Command psql -ErrorAction SilentlyContinue if ($cmd) { $psql = $cmd.Source } elseif (Test-Path -LiteralPath 'C:\Program Files\PostgreSQL\18\bin\psql.exe') { $psql = 'C:\Program Files\PostgreSQL\18\bin\psql.exe' } else { throw "psql introuvable : précisez -PsqlPath ou ajoutez PostgreSQL\bin au PATH." } } if (-not $Force) { Write-Host '' Write-Host "Application du référentiel :" -ForegroundColor Yellow Write-Host " Fichier : $OutputSql" Write-Host " Serveur : ${Server}:${Port}" Write-Host " Base : $Database" Write-Host " Utilisateur : $Username" $confirm = Read-Host "Confirmer l'application (taper OUI) ?" if ($confirm -cne 'OUI') { Write-Host 'Annulé par l''utilisateur.' -ForegroundColor Yellow exit 0 } } if ([string]::IsNullOrWhiteSpace($env:PGPASSWORD)) { $sec = Read-Host "Mot de passe PostgreSQL pour $Username" -AsSecureString $env:PGPASSWORD = [Runtime.InteropServices.Marshal]::PtrToStringUni( [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec)) } try { & $psql -h $Server -p $Port -U $Username -d $Database -v ON_ERROR_STOP=1 -f $OutputSql if ($LASTEXITCODE -ne 0) { throw "psql a échoué (exit $LASTEXITCODE)." } } finally { $env:PGPASSWORD = $null } } # ============================================================ # 8. Résumé. # ============================================================ Write-Host '' Write-Host '== Référentiel généré ==' -ForegroundColor Cyan Write-Host " Écrans (ScreenCode) : $($screenCodes.Count)" Write-Host " Boutons [RelayCommand] : $($methodNames.Count) méthodes -> $($buttonCodes.Count) codes uniques" Write-Host " Actions métier (histor.) : $($semanticFeatureOrder.Count)" Write-Host " Total lignes SQL : $($rows.Count)" Write-Host " Fichier SQL : $OutputSql" if ($unmapped.Count -gt 0) { Write-Warning "Rappel : $($unmapped.Count) code(s) sans libellé explicite : $($unmapped -join ', ')" } if (-not $Apply) { Write-Host '' Write-Host 'Relancez avec -Apply pour appliquer via psql, ou exécutez :' -ForegroundColor Yellow Write-Host " psql -U $Username -d $Database -v ON_ERROR_STOP=1 -f $OutputSql" }