Files
Omika/scripts/seed-referentiel-complet.ps1
2026-10-23 18:46:50 +02:00

462 lines
22 KiB
PowerShell

# ============================================================
# 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"
}