feat(reports): move generated time to the footer and name standard thicknesses

The generated date sat among the summary's report details and printed a UTC
offset nobody on the shop floor reads. It now goes in every page's footer,
left of "Page X of Y", in the computer's own time zone without the offset.

The thickness now leads with its gauge or plate fraction ("16 ga - 0.0598 in")
when it matches one, as the thickness box lists it. Reporting references Core
only, so the desktop looks the name up in StandardThicknesses and passes it on
the snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ajandClaude Opus 5.5 committed 2026-11-12 07:25:40 -05:00
1 parent 696e0bdac6
commit d2da7396ce
6 files changed
+54 -13

No files matched your search

+14 -6
View File
@@ -109,7 +109,7 @@ public static class NestPdfWriter
private static void AddSummary(Document document, NestReportSnapshot snapshot)
{
var section = AddSection(document, Orientation.Portrait, PortraitWidth, $"Nest report: {snapshot.Name}");
var section = AddSection(document, snapshot, Orientation.Portrait, PortraitWidth, $"Nest report: {snapshot.Name}");
var title = section.AddParagraph("Nest Report");
title.Format.Font.Size = 14;
title.Format.Font.Bold = true;
@@ -125,7 +125,6 @@ public static class NestPdfWriter
left.Add(("Units:", UnitsName(snapshot)));
var right = new List<(string Key, string Value)>
{
("Generated:", snapshot.GeneratedAt.ToString("yyyy-MM-dd HH:mm zzz", Invariant)),
("Distinct layouts:", snapshot.Plates.Length.ToString(Invariant)),
("Total physical sheets:", snapshot.TotalSheets.ToString(Invariant)),
};
@@ -337,7 +336,7 @@ public static class NestPdfWriter
header.Add(Material(snapshot));
if (snapshot.Thickness > 0)
header.Add(Thickness(snapshot));
var section = AddSection(document, Orientation.Landscape, LandscapeWidth, string.Join(" | ", header));
var section = AddSection(document, snapshot, Orientation.Landscape, LandscapeWidth, string.Join(" | ", header));
var title = section.AddParagraph(heading);
title.Format.Font.Size = 13;
title.Format.Font.Bold = true;
@@ -610,7 +609,8 @@ public static class NestPdfWriter
private static XFont CardFont(bool bold) =>
new(ReportFonts.Family, CardFontSize, bold ? XFontStyleEx.Bold : XFontStyleEx.Regular);
private static Section AddSection(Document document, Orientation orientation, double width, string header)
private static Section AddSection(Document document, NestReportSnapshot snapshot, Orientation orientation,
double width, string header)
{
var section = document.AddSection();
var setup = section.PageSetup;
@@ -633,7 +633,12 @@ public static class NestPdfWriter
top.Format.Font.Color = Colors.DimGray;
AddLines(top, lines);
var footer = section.Footers.Primary.AddParagraph("Page ");
// When it was generated on the left, in this computer's time zone; the page on the right.
var footer = section.Footers.Primary.AddParagraph();
footer.Format.TabStops.AddTabStop(Unit.FromPoint(width), TabAlignment.Right);
footer.AddText("Generated " + snapshot.GeneratedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm", Invariant));
footer.AddTab();
footer.AddText("Page ");
footer.AddPageField();
footer.AddText(" of ");
footer.AddNumPagesField();
@@ -724,8 +729,11 @@ public static class NestPdfWriter
}
// Zero thickness means "unset" in a new nest; never present it as a genuine value.
// A standard gauge or plate fraction leads, as the thickness box lists it: "16 ga - 0.0598 in".
private static string Thickness(NestReportSnapshot snapshot) =>
snapshot.Thickness > 0 ? $"{Number(snapshot.Thickness)} {snapshot.Units}" : "Not specified";
snapshot.Thickness <= 0 ? "Not specified"
: snapshot.ThicknessName.Length > 0 ? $"{snapshot.ThicknessName} - {Number(snapshot.Thickness)} {snapshot.Units}"
: $"{Number(snapshot.Thickness)} {snapshot.Units}";
private static string SheetSize(NestReportSnapshot snapshot, ReportPlate plate) =>
$"{Number(plate.Bounds.Width)} x {Number(plate.Bounds.Length)} {snapshot.Units}";
+7
View File
@@ -62,6 +62,13 @@ public sealed record NestReportSnapshot(string Name, string Customer, string Not
{
public long TotalSheets => Plates.Aggregate(0L, (total, plate) => checked(total + plate.Copies));
/// <summary>
/// The standard gauge or plate fraction the thickness matches ("16 ga", "1/4\""), shown
/// before the decimal; empty when it matches none. Reporting has no thickness table of its
/// own, so the caller supplies it.
/// </summary>
public string ThicknessName { get; init; } = "";
/// <summary>"lb" for inch jobs, "kg" for millimeter jobs.</summary>
public string WeightUnits => Units == "mm" ? "kg" : "lb";
+19 -1
View File
@@ -44,7 +44,12 @@ public sealed class NestPdfWriterTests : IDisposable
Assert.Contains("Test customer", summary);
Assert.Contains("Steel A36", summary);
Assert.Contains("0.125 in", summary);
Assert.Contains("2026-09-29 12:00 +00:00", summary);
// The generation time is in every page's footer, in this computer's time zone, no offset.
var generated = "Generated " + NestReportTestData.GeneratedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
Assert.Contains(generated, summary);
Assert.Contains(generated, plate);
Assert.DoesNotContain("Generated:", summary);
Assert.DoesNotContain("+00:00", summary + plate);
Assert.Matches(@"Distinct layouts:\s+1", summary);
Assert.Matches(@"Total physical sheets:\s+2", summary);
Assert.Matches(@"1\s+24 x 48 in\s+2\s+4\s+\d+\.\d%", summary);
@@ -82,6 +87,19 @@ public sealed class NestPdfWriterTests : IDisposable
+ ReportPdf.Text(Path.Combine(directory, "again.pdf"), 2));
}
[SkippableFact]
public void Write_NamedThicknessLeadsTheDecimalOnSummaryAndPlatePages()
{
var path = Path.Combine(directory, "named-thickness.pdf");
var snapshot = NestReportBuilder.Capture(NestReportTestData.CreateNest(), NestReportTestData.GeneratedAt)
with
{ ThicknessName = "1/8\"" };
NestPdfWriter.Write(snapshot, path);
Assert.Contains("1/8\" - 0.125 in", ReportPdf.Text(path, 1));
Assert.Contains("1/8\" - 0.125 in", ReportPdf.Text(path, 2));
}
[SkippableFact]
public void Write_EmptyAndDemandOnlyJobsProduceExplicitSummaryOnlyReports()
{
+1 -1
View File
@@ -81,7 +81,7 @@ public static class ReportPdf
var excluded = rows.Where(row => Join(row.Words) is var text
&& (text.StartsWith("Weights need", StringComparison.Ordinal)
|| text.StartsWith("Nest report:", StringComparison.Ordinal)
|| Regex.IsMatch(text, @"^Page \d+ of \d+$")))
|| Regex.IsMatch(text, @"^(Generated \S+ \S+ )?Page \d+ of \d+$")))
.Select(row => row.Top).ToList();
for (var column = 0; column < CardsPerRow; column++)
{
+5 -1
View File
@@ -859,7 +859,11 @@ namespace OpenNest.Forms
DateTimeOffset.Now,
SavedCutParameters.Load(targets.Nest.Units),
catalogDensity
);
) with
{
ThicknessName =
Data.StandardThicknesses.Find(targets.Nest.Thickness, targets.Nest.Units)?.Name ?? "",
};
NestPdfWriter.Write(snapshot, destination);
}
+8 -4
View File
@@ -95,8 +95,9 @@ PartPrintoutWriter.Write(snapshot, destinationPath);
An empty/demand-only job, a single plate, or many plates and drawings all
produce one document: a Letter portrait summary followed by one Letter
landscape section per plate, every page carrying a repeated header and
"Page X of Y". Long notes, long drawing names, many drawings and many parts
landscape section per plate, every page carrying a repeated header and a
footer with the generation time (left, in the computer's time zone, no offset)
and "Page X of Y" (right). Long notes, long drawing names, many drawings and many parts
per sheet paginate naturally: MigraDoc continues the Plates table and each
plate's part table across pages with the heading row repeated, the summary's
part cards continue row by row, and notes flow as an ordinary paragraph. No
@@ -110,8 +111,11 @@ beside the ID; complete drawings show no status. Cards without a weight show
"-" with a note under the Parts heading saying what is missing. A card row is
a table row, so a card never splits across pages.
The summary's header lists the job and material on the left and the dates and
totals on the right, to leave room for part cards on the first page.
The summary's header lists the job and material on the left and the totals on
the right, to leave room for part cards on the first page. A thickness that
matches a standard gauge or plate fraction leads with its name, as the
thickness box lists it ("16 ga - 0.0598 in"); the desktop supplies the name,
since Reporting has no thickness table of its own.
Stock sizes read **width x length** in job units. Plate tables use **Quantity**
and **Parts per plate**; the plate quantity multiplies per-plate part counts for