298 lines
13 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|