Files
HC_APTBS/Services/Impl/PdfService.cs
2026-04-11 12:45:18 +02:00

298 lines
13 KiB
C#

using System;
using System.IO;
using HC_APTBS.Models;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace HC_APTBS.Services.Impl
{
/// <summary>
/// Generates PDF test reports using QuestPDF.
///
/// <para>
/// Report layout:
/// <list type="bullet">
/// <item>Header: company logo, company name, date, operator, client.</item>
/// <item>Pump identification table: ID, serial, model, rotation, lock angle.</item>
/// <item>K-Line ECU data block: model reference, DFI, SW versions, fault codes.</item>
/// <item>Per-test results section: one table per enabled phase showing measured
/// average vs. target ± tolerance and a pass/fail indicator.</item>
/// <item>Footer: page numbers.</item>
/// </list>
/// </para>
/// </summary>
public sealed class PdfService : IPdfService
{
private readonly IConfigurationService _config;
/// <param name="configService">Provides company name, logo path, and report settings.</param>
public PdfService(IConfigurationService configService)
{
_config = configService;
// QuestPDF community licence — required for open-source use.
QuestPDF.Settings.License = LicenseType.Community;
}
// ── IPdfService ───────────────────────────────────────────────────────────
/// <inheritdoc/>
public string GenerateReport(
PumpDefinition pump,
string operatorName,
string clientName,
string outputFolder)
{
Directory.CreateDirectory(outputFolder);
string fileName = SanitiseFileName(
$"{pump.Id}_{pump.SerialNumber}_{clientName}_{DateTime.Now:yyyy-MM-dd_HH-mm}.pdf");
string filePath = Path.Combine(outputFolder, fileName);
var document = Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(25, Unit.Millimetre);
page.DefaultTextStyle(x => x.FontSize(9).FontFamily(Fonts.Arial));
page.Header().Element(c => ComposeHeader(c, pump, operatorName, clientName));
page.Content().Element(c => ComposeContent(c, pump));
page.Footer().AlignCenter().Text(t =>
{
t.Span("Page ");
t.CurrentPageNumber();
t.Span(" of ");
t.TotalPages();
});
});
});
document.GeneratePdf(filePath);
return filePath;
}
// ── Header ────────────────────────────────────────────────────────────────
private void ComposeHeader(
IContainer container, PumpDefinition pump, string operatorName, string clientName)
{
container.Row(row =>
{
// Company logo (optional)
if (File.Exists(_config.Settings.ReportLogoPath))
{
row.ConstantItem(60).Height(40)
.Image(_config.Settings.ReportLogoPath)
.FitArea();
}
row.RelativeItem().PaddingLeft(10).Column(col =>
{
col.Item().Text(_config.Settings.CompanyName)
.Bold().FontSize(14);
col.Item().Text(_config.Settings.CompanyInfo)
.FontSize(8).FontColor(Colors.Grey.Darken1);
});
row.ConstantItem(130).Column(col =>
{
col.Item().Text($"Date: {DateTime.Now:dd/MM/yyyy HH:mm}").FontSize(8);
col.Item().Text($"Operator: {operatorName}").FontSize(8);
col.Item().Text($"Client: {clientName}").FontSize(8);
});
});
}
// ── Content ───────────────────────────────────────────────────────────────
private static void ComposeContent(IContainer container, PumpDefinition pump)
{
container.PaddingTop(10).Column(col =>
{
// ── Pump identification ──────────────────────────────────────────
col.Item().PaddingBottom(8).Element(c => ComposePumpInfoTable(c, pump));
// ── K-Line ECU data ──────────────────────────────────────────────
if (pump.KlineInfo.Count > 0)
col.Item().PaddingBottom(8).Element(c => ComposeKlineTable(c, pump));
// ── Test results — one section per test ──────────────────────────
foreach (var test in pump.Tests)
{
if (!test.HasResults()) continue;
col.Item().PaddingBottom(6).Element(c => ComposeTestSection(c, test));
}
});
}
// ── Pump info table ───────────────────────────────────────────────────────
private static void ComposePumpInfoTable(IContainer container, PumpDefinition pump)
{
container.Table(table =>
{
table.ColumnsDefinition(cols =>
{
cols.RelativeColumn(1);
cols.RelativeColumn(2);
cols.RelativeColumn(1);
cols.RelativeColumn(2);
});
table.Header(header =>
{
header.Cell().ColumnSpan(4)
.Background(Colors.Blue.Darken3)
.Padding(4)
.Text("PUMP IDENTIFICATION")
.FontColor(Colors.White).Bold().FontSize(10);
});
void AddRow(string label1, string value1, string label2, string value2)
{
table.Cell().Padding(3).Text(label1).Bold();
table.Cell().Padding(3).Text(value1);
table.Cell().Padding(3).Text(label2).Bold();
table.Cell().Padding(3).Text(value2);
}
AddRow("Pump ID:", pump.Id, "Model:", pump.Model);
AddRow("Serial No.:", pump.SerialNumber, "Injector:", pump.Injector);
AddRow("Tube:", pump.Tube, "Valve:", pump.Valve);
AddRow("Tension:", pump.Tension, "Rotation:", pump.Rotation);
AddRow("Lock Angle:", $"{pump.LockAngle:F2}°",
"Measured:", $"{pump.LockAngleResult:F2}°");
AddRow("Chaveta:", pump.Chaveta, "Pre-Inj.:", pump.HasPreInjection ? "Yes" : "No");
});
}
// ── K-Line table ──────────────────────────────────────────────────────────
private static void ComposeKlineTable(IContainer container, PumpDefinition pump)
{
container.Table(table =>
{
table.ColumnsDefinition(cols =>
{
cols.RelativeColumn(1);
cols.RelativeColumn(2);
cols.RelativeColumn(1);
cols.RelativeColumn(2);
});
table.Header(header =>
{
header.Cell().ColumnSpan(4)
.Background(Colors.Blue.Darken2)
.Padding(4)
.Text("ECU DATA (K-Line)")
.FontColor(Colors.White).Bold().FontSize(10);
});
void AddKv(string key)
{
if (pump.KlineInfo.TryGetValue(key, out var val))
{
table.Cell().Padding(3).Text(key + ":").Bold();
table.Cell().ColumnSpan(3).Padding(3).Text(val);
}
}
AddKv(KlineKeys.ModelReference);
AddKv(KlineKeys.DataRecord);
AddKv(KlineKeys.SwVersion1);
AddKv(KlineKeys.SwVersion2);
AddKv(KlineKeys.PumpControl);
AddKv(KlineKeys.Dfi);
AddKv(KlineKeys.SerialNumber);
AddKv(KlineKeys.Errors);
});
}
// ── Test results section ──────────────────────────────────────────────────
private static void ComposeTestSection(IContainer container, TestDefinition test)
{
container.Column(col =>
{
col.Item()
.Background(Colors.Grey.Lighten2)
.Padding(4)
.Text($"TEST: {test.Name}")
.Bold().FontSize(10);
col.Item().Table(table =>
{
table.ColumnsDefinition(cols =>
{
cols.RelativeColumn(2); // Phase
cols.RelativeColumn(1); // Parameter
cols.RelativeColumn(1); // Target
cols.RelativeColumn(1); // Tolerance
cols.RelativeColumn(1); // Average
cols.RelativeColumn(1); // Result
});
table.Header(header =>
{
foreach (var h in new[] { "Phase", "Parameter", "Target", "Tolerance ±", "Average", "Result" })
header.Cell()
.Background(Colors.Grey.Darken1)
.Padding(3)
.Text(h).FontColor(Colors.White).Bold().FontSize(8);
});
foreach (var phase in test.Phases)
{
if (!phase.Enabled || phase.Receives == null) continue;
foreach (var tp in phase.Receives)
{
if (tp.Result == null) continue;
bool passed = tp.Result.Passed;
string resultText = passed ? "PASS" : "FAIL";
string bgColor = passed ? Colors.Green.Lighten4 : Colors.Red.Lighten4;
table.Cell().Background(bgColor).Padding(3).Text(phase.Name).FontSize(8);
table.Cell().Background(bgColor).Padding(3).Text(tp.Name).FontSize(8);
table.Cell().Background(bgColor).Padding(3)
.Text(tp.Value.ToString("F2")).FontSize(8);
table.Cell().Background(bgColor).Padding(3)
.Text(tp.Tolerance.ToString("F2")).FontSize(8);
table.Cell().Background(bgColor).Padding(3)
.Text(tp.Result.Average.ToString("F2")).FontSize(8);
table.Cell().Background(bgColor).Padding(3)
.Text(resultText).Bold()
.FontColor(passed ? Colors.Green.Darken2 : Colors.Red.Darken2)
.FontSize(8);
}
// Show any alarm bits that fired during this phase.
if (phase.ErrorBits?.Count > 0)
{
table.Cell().ColumnSpan(6)
.Background(Colors.Orange.Lighten4)
.Padding(3)
.Text($" ⚠ Error bits: {string.Join(", ", phase.ErrorBits)}")
.FontSize(8).FontColor(Colors.Orange.Darken3);
}
}
});
});
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static string SanitiseFileName(string name)
{
foreach (char c in Path.GetInvalidFileNameChars())
name = name.Replace(c, '_');
return name;
}
}
}