Unlock progress UI:
- UnlockProgressDialog with dark-themed progress ring, phase indicator, elapsed
time, and cancel/close buttons (non-modal, draggable borderless window)
- UnlockProgressViewModel with event-driven progress tracking via IUnlockService
- Triggers on pump selection (manual or K-Line auto-detect), not test start
UnlockService rewrite:
- Persistent CAN senders that outlive the unlock sequence (StopSenders on pump change)
- Concurrent K-Line fast unlock: awaits session Connected, sends RAM timer shortcut
({02 88 02 03 A8 01 00}), verifies via CAN TestUnlock before skipping wait
- Fix Type 1 verification (Value == 0 means unlocked, was inverted)
K-Line fast unlock support:
- IKwpService.TryFastUnlockAsync / KwpService implementation
Additional features:
- ILocalizationService with ES/EN resource dictionaries and runtime switching
- Safety dialogs: VoltageWarning, OilPumpConfirm, RpmSafetyWarning
- SettingsDialog for app configuration
- BenchService enhancements, ConfigurationService improvements, PDF report updates
- All UI strings localized via DynamicResource
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
551 lines
26 KiB
C#
551 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using HC_APTBS.Models;
|
|
using QuestPDF.Fluent;
|
|
using QuestPDF.Helpers;
|
|
using QuestPDF.Infrastructure;
|
|
|
|
namespace HC_APTBS.Services.Impl
|
|
{
|
|
/// <summary>
|
|
/// Generates professional PDF test reports using QuestPDF.
|
|
///
|
|
/// <para>
|
|
/// Report layout:
|
|
/// <list type="bullet">
|
|
/// <item>Header (every page): company logo, company name, date, operator, client, report title.</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>Overall verdict section: large PASS/FAIL badge with summary statistics.</item>
|
|
/// <item>Per-test results: table with pass/fail rows, followed by measurement charts
|
|
/// showing sample values against target ± tolerance bands.</item>
|
|
/// <item>Footer (every page): software attribution and page numbers.</item>
|
|
/// </list>
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class PdfService : IPdfService
|
|
{
|
|
private readonly IConfigurationService _config;
|
|
private readonly ILocalizationService _loc;
|
|
private readonly byte[]? _defaultLogo;
|
|
|
|
/// <param name="configService">Provides company name, logo path, and report settings.</param>
|
|
/// <param name="localizationService">Provides localised strings for report text.</param>
|
|
public PdfService(IConfigurationService configService, ILocalizationService localizationService)
|
|
{
|
|
_config = configService;
|
|
_loc = localizationService;
|
|
|
|
// QuestPDF community licence — required for open-source use.
|
|
QuestPDF.Settings.License = LicenseType.Community;
|
|
|
|
// Load the embedded default logo as a fallback.
|
|
using var stream = typeof(PdfService).Assembly
|
|
.GetManifestResourceStream("HC_APTBS.Resources.Images.default_logo.png");
|
|
if (stream != null)
|
|
{
|
|
using var ms = new MemoryStream();
|
|
stream.CopyTo(ms);
|
|
_defaultLogo = ms.ToArray();
|
|
}
|
|
}
|
|
|
|
// ── 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 reportDate = DateTime.Now;
|
|
|
|
var document = Document.Create(container =>
|
|
{
|
|
container.Page(page =>
|
|
{
|
|
page.Size(PageSizes.A4);
|
|
page.Margin(25, Unit.Millimetre);
|
|
page.DefaultTextStyle(x => x.FontSize(ReportTheme.BodySize).FontFamily(Fonts.Arial));
|
|
|
|
page.Header().Element(c => ComposeHeader(c, operatorName, clientName, reportDate));
|
|
page.Content().Element(c => ComposeContent(c, pump));
|
|
page.Footer().Element(ComposeFooter);
|
|
});
|
|
});
|
|
|
|
document.GeneratePdf(filePath);
|
|
return filePath;
|
|
}
|
|
|
|
// ── Header ────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Renders the page header: logo, company info, date/operator/client, title.</summary>
|
|
private void ComposeHeader(
|
|
IContainer container, string operatorName, string clientName, DateTime reportDate)
|
|
{
|
|
container.Column(outer =>
|
|
{
|
|
outer.Item().Row(row =>
|
|
{
|
|
// Company logo — prefer configured path, fall back to embedded default.
|
|
if (File.Exists(_config.Settings.ReportLogoPath))
|
|
{
|
|
row.ConstantItem(65).Height(45)
|
|
.Image(_config.Settings.ReportLogoPath)
|
|
.FitArea();
|
|
}
|
|
else if (_defaultLogo != null)
|
|
{
|
|
row.ConstantItem(65).Height(45)
|
|
.Image(_defaultLogo)
|
|
.FitArea();
|
|
}
|
|
|
|
// Company name and info.
|
|
row.RelativeItem().PaddingLeft(10).Column(col =>
|
|
{
|
|
col.Item().Text(_config.Settings.CompanyName)
|
|
.Bold().FontSize(ReportTheme.TitleSize);
|
|
col.Item().Text(_config.Settings.CompanyInfo)
|
|
.FontSize(ReportTheme.CaptionSize)
|
|
.FontColor(ReportTheme.HeaderGrey);
|
|
});
|
|
|
|
// Date / operator / client block.
|
|
row.ConstantItem(140).AlignRight().Column(col =>
|
|
{
|
|
col.Item().Text(string.Format(_loc.GetString("Pdf.Date"), reportDate))
|
|
.FontSize(ReportTheme.CaptionSize + 1);
|
|
col.Item().Text(string.Format(_loc.GetString("Pdf.Operator"), operatorName))
|
|
.FontSize(ReportTheme.CaptionSize + 1);
|
|
col.Item().Text(string.Format(_loc.GetString("Pdf.Client"), clientName))
|
|
.FontSize(ReportTheme.CaptionSize + 1).Bold();
|
|
});
|
|
});
|
|
|
|
// Divider line.
|
|
outer.Item().PaddingTop(4).LineHorizontal(1)
|
|
.LineColor(ReportTheme.DividerLine);
|
|
|
|
// Report title.
|
|
outer.Item().PaddingTop(4).PaddingBottom(2)
|
|
.AlignCenter()
|
|
.Text(_loc.GetString("Pdf.ReportTitle"))
|
|
.Bold().FontSize(ReportTheme.SectionHeaderSize)
|
|
.FontColor(ReportTheme.HeaderNavy);
|
|
});
|
|
}
|
|
|
|
// ── Footer ────────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Renders the page footer: divider, attribution, and page numbers.</summary>
|
|
private void ComposeFooter(IContainer container)
|
|
{
|
|
container.Column(col =>
|
|
{
|
|
col.Item().LineHorizontal(0.5f).LineColor(ReportTheme.DividerLine);
|
|
col.Item().PaddingTop(3).Row(row =>
|
|
{
|
|
row.RelativeItem().Text(_loc.GetString("Pdf.GeneratedBy"))
|
|
.FontSize(ReportTheme.FooterSize)
|
|
.FontColor(ReportTheme.HeaderGrey);
|
|
|
|
row.ConstantItem(100).AlignRight().Text(t =>
|
|
{
|
|
t.DefaultTextStyle(x => x.FontSize(ReportTheme.FooterSize));
|
|
t.Span(_loc.GetString("Pdf.Page"));
|
|
t.CurrentPageNumber();
|
|
t.Span(_loc.GetString("Pdf.Of"));
|
|
t.TotalPages();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── Content ───────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Composes the full report body: pump info, ECU data, verdict, test sections.</summary>
|
|
private void ComposeContent(IContainer container, PumpDefinition pump)
|
|
{
|
|
container.PaddingTop(6).Column(col =>
|
|
{
|
|
// ── Pump identification ──────────────────────────────────────────
|
|
col.Item().PaddingBottom(ReportTheme.SectionGap)
|
|
.Element(c => ComposePumpInfoTable(c, pump));
|
|
|
|
// ── K-Line ECU data ──────────────────────────────────────────────
|
|
if (pump.KlineInfo.Count > 0)
|
|
col.Item().PaddingBottom(ReportTheme.SectionGap)
|
|
.Element(c => ComposeKlineTable(c, pump));
|
|
|
|
// ── Overall verdict ──────────────────────────────────────────────
|
|
col.Item().PaddingBottom(ReportTheme.SectionGap)
|
|
.Element(c => ComposeVerdictSection(c, pump));
|
|
|
|
// ── Test results — one section per test ──────────────────────────
|
|
foreach (var test in pump.Tests)
|
|
{
|
|
if (!test.HasResults()) continue;
|
|
col.Item().PaddingBottom(ReportTheme.SectionGap)
|
|
.Element(c => ComposeTestSection(c, test));
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Pump info table ───────────────────────────────────────────────────────
|
|
|
|
/// <summary>Renders the pump identification table with alternating row stripes.</summary>
|
|
private 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(ReportTheme.HeaderNavy)
|
|
.Padding(5)
|
|
.Text(_loc.GetString("Pdf.PumpIdentification"))
|
|
.FontColor(Colors.White).Bold()
|
|
.FontSize(ReportTheme.SectionHeaderSize);
|
|
});
|
|
|
|
int rowIndex = 0;
|
|
|
|
void AddRow(string label1, string value1, string label2, string value2)
|
|
{
|
|
string bg = (rowIndex++ % 2 == 1) ? ReportTheme.TableAltRow : "#FFFFFF";
|
|
table.Cell().Background(bg).Padding(ReportTheme.CellPad)
|
|
.Text(label1).Bold().FontSize(ReportTheme.BodySize);
|
|
table.Cell().Background(bg).Padding(ReportTheme.CellPad)
|
|
.Text(value1).FontSize(ReportTheme.BodySize);
|
|
table.Cell().Background(bg).Padding(ReportTheme.CellPad)
|
|
.Text(label2).Bold().FontSize(ReportTheme.BodySize);
|
|
table.Cell().Background(bg).Padding(ReportTheme.CellPad)
|
|
.Text(value2).FontSize(ReportTheme.BodySize);
|
|
}
|
|
|
|
AddRow(_loc.GetString("Pdf.PumpId"), pump.Id, _loc.GetString("Pdf.Model"), pump.Model);
|
|
AddRow(_loc.GetString("Pdf.SerialNo"), pump.SerialNumber, _loc.GetString("Pdf.Injector"), pump.Injector);
|
|
AddRow(_loc.GetString("Pdf.Tube"), pump.Tube, _loc.GetString("Pdf.Valve"), pump.Valve);
|
|
AddRow(_loc.GetString("Pdf.Tension"), pump.Tension, _loc.GetString("Pdf.Rotation"), pump.Rotation);
|
|
AddRow(_loc.GetString("Pdf.LockAngle"), $"{pump.LockAngle:F2}\u00B0",
|
|
_loc.GetString("Pdf.Measured"), $"{pump.LockAngleResult:F2}\u00B0");
|
|
AddRow(_loc.GetString("Pdf.Chaveta"), pump.Chaveta, _loc.GetString("Pdf.PreInj"), pump.HasPreInjection ? _loc.GetString("Common.Yes") : _loc.GetString("Common.No"));
|
|
});
|
|
}
|
|
|
|
// ── K-Line table ──────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Renders the K-Line ECU data table with alternating row stripes.</summary>
|
|
private 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(ReportTheme.HeaderNavy)
|
|
.Padding(5)
|
|
.Text(_loc.GetString("Pdf.EcuData"))
|
|
.FontColor(Colors.White).Bold()
|
|
.FontSize(ReportTheme.SectionHeaderSize);
|
|
});
|
|
|
|
int rowIndex = 0;
|
|
|
|
void AddKv(string key)
|
|
{
|
|
if (!pump.KlineInfo.TryGetValue(key, out var val)) return;
|
|
|
|
string bg = (rowIndex++ % 2 == 1) ? ReportTheme.TableAltRow : "#FFFFFF";
|
|
table.Cell().Background(bg).Padding(ReportTheme.CellPad)
|
|
.Text(key + ":").Bold().FontSize(ReportTheme.BodySize);
|
|
table.Cell().ColumnSpan(3).Background(bg).Padding(ReportTheme.CellPad)
|
|
.Text(val).FontSize(ReportTheme.BodySize);
|
|
}
|
|
|
|
AddKv(KlineKeys.ModelReference);
|
|
AddKv(KlineKeys.DataRecord);
|
|
AddKv(KlineKeys.SwVersion1);
|
|
AddKv(KlineKeys.SwVersion2);
|
|
AddKv(KlineKeys.PumpControl);
|
|
AddKv(KlineKeys.Dfi);
|
|
AddKv(KlineKeys.SerialNumber);
|
|
AddKv(KlineKeys.Errors);
|
|
});
|
|
}
|
|
|
|
// ── Verdict section ───────────────────────────────────────────────────────
|
|
|
|
/// <summary>Renders the overall test result badge with summary statistics.</summary>
|
|
private void ComposeVerdictSection(IContainer container, PumpDefinition pump)
|
|
{
|
|
// Compute summary statistics.
|
|
var testsWithResults = pump.Tests.Where(t => t.HasResults()).ToList();
|
|
int totalTests = pump.Tests.Count;
|
|
int testedCount = testsWithResults.Count;
|
|
|
|
int totalPhases = 0;
|
|
int passedPhases = 0;
|
|
foreach (var test in testsWithResults)
|
|
{
|
|
foreach (var phase in test.Phases)
|
|
{
|
|
if (!phase.Enabled || phase.Receives == null) continue;
|
|
foreach (var tp in phase.Receives)
|
|
{
|
|
if (tp.Result == null) continue;
|
|
totalPhases++;
|
|
if (tp.Result.Passed) passedPhases++;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool allPassed = totalPhases > 0 && passedPhases == totalPhases;
|
|
|
|
container.Border(1).BorderColor(ReportTheme.DividerLine)
|
|
.Background(ReportTheme.TableAltRow)
|
|
.Padding(8)
|
|
.Row(row =>
|
|
{
|
|
// PASS/FAIL verdict badge (SVG).
|
|
row.ConstantItem(100).Height(50)
|
|
.Svg(ReportChartRenderer.RenderVerdictBadge(100, 50, allPassed))
|
|
.FitArea();
|
|
|
|
// Summary statistics.
|
|
row.RelativeItem().PaddingLeft(12).Column(col =>
|
|
{
|
|
col.Item().Text(_loc.GetString("Pdf.OverallResult"))
|
|
.Bold().FontSize(ReportTheme.SectionHeaderSize)
|
|
.FontColor(ReportTheme.HeaderNavy);
|
|
|
|
col.Item().PaddingTop(4).Text(
|
|
string.Format(_loc.GetString("Pdf.TestsExecuted"), testedCount, totalTests))
|
|
.FontSize(ReportTheme.BodySize);
|
|
|
|
col.Item().Text(
|
|
string.Format(_loc.GetString("Pdf.ParamsEvaluated"), passedPhases, totalPhases))
|
|
.FontSize(ReportTheme.BodySize);
|
|
|
|
// Per-test mini indicators.
|
|
col.Item().PaddingTop(4).Row(indicators =>
|
|
{
|
|
foreach (var test in pump.Tests)
|
|
{
|
|
bool hasResults = test.HasResults();
|
|
bool testPassed = hasResults && test.Phases
|
|
.Where(p => p.Enabled && p.Receives != null)
|
|
.SelectMany(p => p.Receives)
|
|
.Where(tp => tp.Result != null)
|
|
.All(tp => tp.Result!.Passed);
|
|
|
|
string indicator = !hasResults ? "--"
|
|
: testPassed ? "\u2713" : "\u2717";
|
|
string color = !hasResults ? ReportTheme.HeaderGrey
|
|
: testPassed ? ReportTheme.PassGreen : ReportTheme.FailRed;
|
|
|
|
indicators.AutoItem().PaddingRight(8).Text(t =>
|
|
{
|
|
t.Span($"{test.Name}: ").FontSize(ReportTheme.BodySize);
|
|
t.Span(indicator).Bold().FontSize(ReportTheme.BodySize)
|
|
.FontColor(color);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// ── Test results section ──────────────────────────────────────────────────
|
|
|
|
/// <summary>Renders a single test: results table followed by measurement charts.</summary>
|
|
private void ComposeTestSection(IContainer container, TestDefinition test)
|
|
{
|
|
container.Column(col =>
|
|
{
|
|
// Section header bar.
|
|
col.Item()
|
|
.Background(ReportTheme.HeaderNavy)
|
|
.Padding(5)
|
|
.Text(string.Format(_loc.GetString("Pdf.TestHeader"), test.Name))
|
|
.FontColor(Colors.White).Bold()
|
|
.FontSize(ReportTheme.SectionHeaderSize);
|
|
|
|
// Results table.
|
|
col.Item().Element(c => ComposeResultsTable(c, test));
|
|
|
|
// Charts — one per parameter that has sample data.
|
|
col.Item().PaddingTop(ReportTheme.SubsectionGap)
|
|
.Element(c => ComposeTestCharts(c, test));
|
|
});
|
|
}
|
|
|
|
/// <summary>Renders the pass/fail results table for one test.</summary>
|
|
private void ComposeResultsTable(IContainer container, TestDefinition test)
|
|
{
|
|
container.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[] { _loc.GetString("Pdf.Phase"), _loc.GetString("Pdf.Parameter"), _loc.GetString("Pdf.Target"), _loc.GetString("Pdf.ToleranceHeader"), _loc.GetString("Pdf.Average"), _loc.GetString("Pdf.Result") })
|
|
header.Cell()
|
|
.Background(ReportTheme.AccentBlue)
|
|
.Padding(ReportTheme.CellPad)
|
|
.Text(h).FontColor(Colors.White).Bold()
|
|
.FontSize(ReportTheme.CaptionSize + 1);
|
|
});
|
|
|
|
int rowIndex = 0;
|
|
|
|
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 ? _loc.GetString("Common.Pass") : _loc.GetString("Common.Fail");
|
|
|
|
// Alternating base row colour, tinted by pass/fail.
|
|
string bgColor = passed
|
|
? (rowIndex % 2 == 0 ? ReportTheme.PassGreenLight : "#D7ECD9")
|
|
: (rowIndex % 2 == 0 ? ReportTheme.FailRedLight : "#FFDBDB");
|
|
|
|
table.Cell().Background(bgColor).Padding(ReportTheme.CellPad)
|
|
.Text(phase.Name).FontSize(ReportTheme.CaptionSize + 1);
|
|
table.Cell().Background(bgColor).Padding(ReportTheme.CellPad)
|
|
.Text(tp.Name).FontSize(ReportTheme.CaptionSize + 1);
|
|
table.Cell().Background(bgColor).Padding(ReportTheme.CellPad)
|
|
.Text(tp.Value.ToString("F2")).FontSize(ReportTheme.CaptionSize + 1);
|
|
table.Cell().Background(bgColor).Padding(ReportTheme.CellPad)
|
|
.Text(tp.Tolerance.ToString("F2")).FontSize(ReportTheme.CaptionSize + 1);
|
|
table.Cell().Background(bgColor).Padding(ReportTheme.CellPad)
|
|
.Text(tp.Result.Average.ToString("F2")).FontSize(ReportTheme.CaptionSize + 1);
|
|
table.Cell().Background(bgColor).Padding(ReportTheme.CellPad)
|
|
.Text(resultText).Bold()
|
|
.FontColor(passed ? ReportTheme.PassGreen : ReportTheme.FailRed)
|
|
.FontSize(ReportTheme.CaptionSize + 1);
|
|
|
|
rowIndex++;
|
|
}
|
|
|
|
// Show any alarm bits that fired during this phase.
|
|
if (phase.ErrorBits?.Count > 0)
|
|
{
|
|
table.Cell().ColumnSpan(6)
|
|
.Background(ReportTheme.WarningBg)
|
|
.Padding(ReportTheme.CellPad)
|
|
.Text(string.Format(_loc.GetString("Pdf.ErrorBits"), string.Join(", ", phase.ErrorBits)))
|
|
.FontSize(ReportTheme.CaptionSize + 1)
|
|
.FontColor(ReportTheme.WarningText);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>Renders measurement charts for each parameter that has sample data.</summary>
|
|
private void ComposeTestCharts(IContainer container, TestDefinition test)
|
|
{
|
|
container.Column(col =>
|
|
{
|
|
bool anyChart = false;
|
|
|
|
foreach (var phase in test.Phases)
|
|
{
|
|
if (!phase.Enabled || phase.Receives == null) continue;
|
|
|
|
foreach (var tp in phase.Receives)
|
|
{
|
|
if (tp.Result == null || tp.Result.Samples.Count < 2) continue;
|
|
|
|
anyChart = true;
|
|
string label = $"{phase.Name} \u2014 {tp.Name}";
|
|
|
|
// The chart itself (SVG rendered inline).
|
|
string chartSvg = ReportChartRenderer.RenderMeasurementChart(
|
|
480, ReportTheme.ChartHeight,
|
|
tp.Result.Samples,
|
|
tp.Value,
|
|
tp.Tolerance,
|
|
tp.Result.Average,
|
|
tp.Result.Passed,
|
|
label);
|
|
|
|
col.Item().PaddingBottom(2)
|
|
.Svg(chartSvg)
|
|
.FitWidth();
|
|
|
|
// Chart caption.
|
|
string passFailText = tp.Result.Passed
|
|
? _loc.GetString("Common.Pass")
|
|
: _loc.GetString("Common.Fail");
|
|
col.Item().PaddingBottom(ReportTheme.SubsectionGap)
|
|
.Text(string.Format(_loc.GetString("Pdf.ChartSamples"),
|
|
tp.Result.Samples.Count, tp.Value, tp.Tolerance,
|
|
tp.Result.Average, passFailText))
|
|
.FontSize(ReportTheme.CaptionSize)
|
|
.FontColor(ReportTheme.HeaderGrey);
|
|
}
|
|
}
|
|
|
|
if (!anyChart)
|
|
{
|
|
col.Item().PaddingTop(2).PaddingBottom(4)
|
|
.Text(_loc.GetString("Pdf.NoSampleData"))
|
|
.FontSize(ReportTheme.CaptionSize).Italic()
|
|
.FontColor(ReportTheme.HeaderGrey);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
/// <summary>Sanitises a file name by replacing invalid characters with underscores.</summary>
|
|
private static string SanitiseFileName(string name)
|
|
{
|
|
foreach (char c in Path.GetInvalidFileNameChars())
|
|
name = name.Replace(c, '_');
|
|
return name;
|
|
}
|
|
}
|
|
}
|