Premiere commit - Omika
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user