Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
@@ -0,0 +1,392 @@
using System.IO.Compression;
using System.IO.Packaging;
using System.Text;
using System.Text.Json;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
using Microsoft.Extensions.Logging.Abstractions;
using Omika.Core.Contracts.Documents;
using Omika.Core.Interfaces;
using Omika.Core.Services;
using Omika.Data.Configuration;
using Omika.Data.HfSql.Connection;
using Omika.Data.Repositories.Omika;
using WP = DocumentFormat.OpenXml.Drawing.Wordprocessing;
namespace Omika.Tests.Connection;
/// <summary>
/// SONDE DIAGNOSTIC (lecture seule + génération en mémoire) : trace UNE génération
/// de rapport réelle de bout en bout pour expliquer pourquoi une photo sélectionnée
/// n'apparaît pas dans le PDF. Ne modifie JAMAIS la base. Écrit :
/// — artifacts/diag-photo.txt : trace textuelle complète ;
/// — artifacts/diag-modele.docx : modèle DOCX réel (copie) ;
/// — artifacts/diag-rapport.docx : DOCX intermédiaire généré (avant conversion PDF).
/// S'auto-ignore si la configuration locale est absente ou le serveur injoignable.
/// </summary>
public class RapportPhotoDiagnosticTests
{
private const string ConfigPath = @"D:\Avalonia\Omika\config\hfsql-test.local.json";
private const string ArtifactDir = @"D:\Avalonia\Omika\artifacts";
[Fact]
public async Task Diagnostic_Trace_Photo_Reelle()
{
var sb = new StringBuilder();
sb.AppendLine("=== OMIKA PHOTO — DIAGNOSTIC (read-only) ===");
TestConfig? config = LoadConfig();
if (config is null)
{
sb.AppendLine("CONFIG: MISSING (skip)");
await WriteArtifact("diag-photo.txt", sb);
return;
}
var options = new HfSqlConnectionOptions
{
Server = config.Server,
Port = config.Port,
Database = config.Omika.Database,
User = config.Omika.User,
Credential = config.Omika.Credential,
FilePasswords = new HfSqlFilePasswordOptions
{
ProtectionStd = config.FilePasswords.ProtectionStd,
ProtectionPlus = config.FilePasswords.ProtectionPlus
}
};
var factory = new HfSqlOleDbConnectionFactory();
await using var connection = new HfSqlOleDbConnection(factory, options);
try
{
await connection.OpenAsync();
sb.AppendLine($"CONNECTION: OPEN (DB={config.Omika.Database})");
}
catch (Exception ex)
{
sb.AppendLine($"CONNECTION FAILED (skip): {Redact(ex.Message)}");
await WriteArtifact("diag-photo.txt", sb);
return;
}
try
{
// 1. Racine documentaire.
object? rootObj = await connection.ExecuteScalarAsync(
"SELECT PAP_Valeur FROM ParametreApplication WHERE PAP_Code = 'CHEMIN_REPERTOIRE_BASE_DOCUMENTS'");
string racine = rootObj?.ToString() ?? string.Empty;
sb.AppendLine($"DOCUMENT ROOT = {racine}");
var parameters = new FakeApplicationParameters(racine);
var repository = new OmikaRapportChampRepository(
connection, NullLogger<OmikaRapportChampRepository>.Instance, parameters);
// 2. Modèles contenant des rubriques photo.
IReadOnlyList<IReadOnlyDictionary<string, object?>> modeles = await connection.QueryAsync(
"SELECT MRP_PKID, MRP_Nom, MRP_Modele, MRP_Extra FROM ModeleRapport");
sb.AppendLine($"MODELES = {modeles.Count}");
long modelePhoto = 0;
byte[]? modeleDocx = null;
string? modeleTexte = null;
foreach (var m in modeles)
{
byte[]? docx = m.GetValueOrDefault("MRP_Modele") as byte[];
if (docx is null || docx.Length == 0)
{
continue;
}
string texte = DocxTextExtractor.Extraire(docx);
bool contientPhoto = texte.Contains("PHOTO", StringComparison.OrdinalIgnoreCase);
sb.AppendLine($" MODELE pkid={m.GetValueOrDefault("MRP_PKID")} nom={Truncate(m.GetValueOrDefault("MRP_Nom")?.ToString())} photos={contientPhoto}");
if (contientPhoto && modelePhoto == 0)
{
modelePhoto = Convert.ToInt64(m.GetValueOrDefault("MRP_PKID"));
modeleDocx = docx;
modeleTexte = texte;
}
}
if (modelePhoto == 0 || modeleDocx is null)
{
sb.AppendLine("NO PHOTO MODEL FOUND (skip).");
await WriteArtifact("diag-photo.txt", sb);
return;
}
sb.AppendLine();
sb.AppendLine($"CHOSEN MODEL pkid={modelePhoto}");
sb.AppendLine("── MODEL TEXT (photo lines) ──");
foreach (string ligne in (modeleTexte ?? string.Empty).Split('\n'))
{
if (ligne.Contains("PHOTO", StringComparison.OrdinalIgnoreCase)
|| ligne.Contains("IMAGE", StringComparison.OrdinalIgnoreCase))
{
sb.AppendLine($" {ligne.Trim()}");
}
}
await File.WriteAllBytesAsync(Path.Combine(ArtifactDir, "diag-modele.docx"), modeleDocx);
// 3. RubriquesDocument liées aux photos.
IReadOnlyList<IReadOnlyDictionary<string, object?>> rubriques = await connection.QueryAsync(
"SELECT RD_PKID, RD_Nom, RD_Rubrique, RD_TypeMembre, RD_MasqueSaisie, RD_Groupe, RD_MembreMob, RD_IdRubParent, RD_Ordre " +
"FROM RubriquesDocument WHERE RD_Groupe = 'PHOTO_INTER' ORDER BY RD_IdRubParent, RD_Ordre");
sb.AppendLine();
sb.AppendLine($"── RUBRIQUES PHOTO_INTER ({rubriques.Count}) ──");
foreach (var r in rubriques)
{
sb.AppendLine($" rub={r.GetValueOrDefault("RD_Rubrique")} type={r.GetValueOrDefault("RD_TypeMembre")} " +
$"masque={r.GetValueOrDefault("RD_MasqueSaisie")} membre={r.GetValueOrDefault("RD_MembreMob")} parent={r.GetValueOrDefault("RD_IdRubParent")}");
}
// 4. Intervention avec photos (celle qui possède réellement des .jpg).
IReadOnlyList<IReadOnlyDictionary<string, object?>> interventionsAvecPhotos = await connection.QueryAsync(
"SELECT DISTINCT DC_IDIntervention FROM Document WHERE DC_Nom LIKE '%.jpg' OR DC_Nom LIKE '%.png'");
long interventionId = 0;
foreach (var i in interventionsAvecPhotos)
{
interventionId = Convert.ToInt64(i.GetValueOrDefault("DC_IDIntervention"));
break;
}
if (interventionId == 0)
{
IReadOnlyList<IReadOnlyDictionary<string, object?>> toutes = await connection.QueryAsync(
"SELECT IN_PKID FROM Intervention");
foreach (var i in toutes)
{
interventionId = Convert.ToInt64(i.GetValueOrDefault("IN_PKID"));
break;
}
}
IReadOnlyList<IReadOnlyDictionary<string, object?>> photos = await connection.QueryAsync(
"SELECT Document.DC_PKID, Document.GUID, Document.DC_Nom, Document.DC_DateFichier, Document.DC_Chemin, " +
"ParametreMetier.PA_Libelle AS CAT_Libelle " +
"FROM Document LEFT OUTER JOIN ParametreMetier ON ParametreMetier.PA_CLE = Document.DC_CleCategorie " +
"AND Document.DC_CleCategorie <> '' WHERE Document.DC_IDIntervention = ? ORDER BY Document.DC_PKID",
[interventionId]);
sb.AppendLine();
sb.AppendLine($"INTERVENTION pkid={interventionId} photosBrutes={photos.Count}");
foreach (var p in photos.Take(5))
{
sb.AppendLine($" DC_PKID={p.GetValueOrDefault("DC_PKID")} nom={p.GetValueOrDefault("DC_Nom")} cat={p.GetValueOrDefault("CAT_Libelle")}");
}
// 5. Champs complets via le repository (équivalent serveur).
IReadOnlyList<RapportChampDto> champs = await repository.GetChampsAsync(
modelePhoto, interventionId, toutesRub: true);
sb.AppendLine();
sb.AppendLine($"── CHAMPS ({champs.Count}) ──");
foreach (RapportChampDto c in champs)
{
bool photo = c.Rubrique?.Contains("PHOTO", StringComparison.OrdinalIgnoreCase) == true
|| string.Equals(c.Type, "ChpImage", StringComparison.OrdinalIgnoreCase);
if (photo)
{
sb.AppendLine($" rub={c.Rubrique} type={c.Type} typeMembre={c.TypeMembre} membre={c.MembreMob} " +
$"listeMembre={c.ListeMembre.Count} listeDonnesBD={(c.ListeDonnesBD ?? "").Length} chars");
foreach (RapportChampSousElementDto m in c.ListeMembre)
{
sb.AppendLine($" membre: rub={m.Rubrique} type={m.Type} typeMembre={m.TypeMembre} membreMob={m.MembreMob}");
}
if (!string.IsNullOrWhiteSpace(c.ListeDonnesBD))
{
sb.AppendLine($" listeDonnesBD JSON = {Truncate(c.ListeDonnesBD, 800)}");
}
}
}
// 6. Photo sélectionnée (première option photo).
RapportChampDto? photoChamp = champs.FirstOrDefault(c =>
string.Equals(c.Type, "ChpImage", StringComparison.OrdinalIgnoreCase)
|| c.Rubrique?.Contains("PHOTO", StringComparison.OrdinalIgnoreCase) == true);
string? cheminComplet = null;
if (photoChamp is not null)
{
if (photoChamp.ListeMembre is { Count: > 0 })
{
// TABPHOTO : ListeDonnesBD = JSON array.
using JsonDocument doc = JsonDocument.Parse(photoChamp.ListeDonnesBD ?? "[]");
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
foreach (JsonElement item in doc.RootElement.EnumerateArray())
{
if (item.TryGetProperty("cheminComplet", out JsonElement cc) && cc.ValueKind == JsonValueKind.String)
{
cheminComplet = cc.GetString();
break;
}
}
}
}
else
{
cheminComplet = photoChamp.ListeDonnesBD;
}
}
sb.AppendLine();
sb.AppendLine($"PHOTO SELECTED = {(cheminComplet is null ? "NO" : "YES")}");
sb.AppendLine($"CHEMINCOMPLET = {cheminComplet}");
sb.AppendLine($"FILE EXISTS = {(cheminComplet is null ? "N/A" : File.Exists(cheminComplet))}");
if (cheminComplet is not null && File.Exists(cheminComplet))
{
byte[] premiers = new byte[Math.Min(12, new FileInfo(cheminComplet).Length)];
using (FileStream fs = File.OpenRead(cheminComplet))
{
_ = fs.Read(premiers, 0, premiers.Length);
}
sb.AppendLine($"MAGIC BYTES = {BitConverter.ToString(premiers)}");
}
// 7. Génération du DOCX intermédiaire (vraie image fichier).
sb.AppendLine();
sb.AppendLine("── RemplirRubriques ──");
byte[] docxRempli = RapportPdfService.RemplirRubriques(modeleDocx, champs);
await File.WriteAllBytesAsync(Path.Combine(ArtifactDir, "diag-rapport.docx"), docxRempli);
sb.AppendLine($"INTERMEDIATE DOCX = {docxRempli.Length} bytes");
InspecterDocx(sb, docxRempli, modeleTexte ?? string.Empty, champs);
await WriteArtifact("diag-photo.txt", sb);
}
catch (Exception ex)
{
sb.AppendLine();
sb.AppendLine($"ERROR: {ex.GetType().Name}: {ex}");
await WriteArtifact("diag-photo.txt", sb);
}
}
private static void InspecterDocx(
StringBuilder sb,
byte[] docx,
string modeleTexte,
IReadOnlyList<RapportChampDto> champs)
{
using var stream = new MemoryStream(docx);
using WordprocessingDocument document = WordprocessingDocument.Open(stream, false);
MainDocumentPart? main = document.MainDocumentPart;
if (main is null)
{
sb.AppendLine("INSPECT: MainDocumentPart missing");
return;
}
// Images dans le package (sous /word/media/).
sb.AppendLine();
sb.AppendLine("── word/media/* ──");
using (var pkgStream = new MemoryStream(docx))
using (Package package = Package.Open(pkgStream, FileMode.Open, FileAccess.Read))
{
var mediaParts = package.GetParts()
.Where(p => p.Uri.OriginalString.StartsWith("/word/media/", StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var mediaPart in mediaParts)
{
long bytes = 0;
try { using var s = mediaPart.GetStream(); bytes = s.Length; } catch { }
sb.AppendLine($" media: uri={mediaPart.Uri} contentType={mediaPart.ContentType} bytes={bytes}");
}
sb.AppendLine($"IMAGE PARTS = {mediaParts.Count}");
// Relations d'images (document.xml.rels).
sb.AppendLine();
sb.AppendLine("── document.xml.rels (image relationships) ──");
var imageRelations = package
.GetPart(new Uri("/word/document.xml", UriKind.Relative))
.GetRelationshipsByType("http://schemas.openxmlformats.org/officeDocument/2006/relationships/image")
.ToList();
foreach (var rel in imageRelations)
{
sb.AppendLine($" relId={rel.Id} target={rel.TargetUri}");
}
}
sb.AppendLine();
sb.AppendLine("── Drawing OpenXML ──");
int drawings = main.Document.Body!.Descendants<Drawing>().Count();
var extents = main.Document.Body.Descendants<WP.Extent>()
.Select(e => $"{e.Cx!.Value}x{e.Cy!.Value}").ToList();
sb.AppendLine($"DRAWINGS = {drawings}");
foreach (string extent in extents.Take(10))
{
sb.AppendLine($" extent: {extent}");
}
// Placeholder restants.
sb.AppendLine();
sb.AppendLine("── PLACEHOLDER REMAINS ──");
string texte = string.Concat(main.Document.Body.Descendants<Text>().Select(t => t.Text));
foreach (RapportChampDto c in champs)
{
if (c.Rubrique?.Contains("PHOTO", StringComparison.OrdinalIgnoreCase) == true
|| string.Equals(c.Type, "ChpImage", StringComparison.OrdinalIgnoreCase))
{
sb.AppendLine($" rub={c.Rubrique} remains={texte.Contains(c.Rubrique ?? "", StringComparison.Ordinal)}");
}
}
}
private static async Task WriteArtifact(string name, StringBuilder sb)
{
Directory.CreateDirectory(ArtifactDir);
await File.WriteAllTextAsync(Path.Combine(ArtifactDir, name), sb.ToString());
}
private static string Truncate(string? s, int max = 60) => s is null ? "<null>" : (s.Length <= max ? s : s[..max] + "…");
private static string Redact(string msg) => msg.Replace("Password", "[PWD]", StringComparison.OrdinalIgnoreCase);
private sealed class FakeApplicationParameters : IApplicationParameterRepository
{
private readonly string _root;
public FakeApplicationParameters(string root) => _root = root;
public Task<string?> GetValueAsync(string code, CancellationToken ct = default)
=> Task.FromResult<string?>(code == "CHEMIN_REPERTOIRE_BASE_DOCUMENTS" ? _root : null);
}
private sealed class TestConfig
{
public int Port { get; set; }
public string Server { get; set; } = "";
public FilePasswordsConfig FilePasswords { get; set; } = new();
public DatabaseConfig Omika { get; set; } = new();
}
private sealed class FilePasswordsConfig
{
public string? ProtectionStd { get; set; }
public string? ProtectionPlus { get; set; }
}
private sealed class DatabaseConfig
{
public string Credential { get; set; } = "";
public string Database { get; set; } = "";
public string User { get; set; } = "";
}
private static TestConfig? LoadConfig()
{
if (!File.Exists(ConfigPath))
{
return null;
}
try
{
return JsonSerializer.Deserialize<TestConfig>(
File.ReadAllText(ConfigPath),
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch
{
return null;
}
}
}