393 lines
17 KiB
C#
393 lines
17 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|