From d2da7396ce42eaa543dc392a0e11d031bfde74a0 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 12 Nov 2026 07:11:18 -0500 Subject: [PATCH] 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 --- OpenNest.Reporting/NestPdfWriter.cs | 20 +++++++++++++------ OpenNest.Reporting/NestReportSnapshot.cs | 7 +++++++ .../Reporting/NestPdfWriterTests.cs | 20 ++++++++++++++++++- OpenNest.Tests/Reporting/ReportPdf.cs | 2 +- OpenNest/Forms/EditNestForm.cs | 6 +++++- docs/nest-reports.md | 12 +++++++---- 6 files changed, 54 insertions(+), 13 deletions(-) diff --git a/OpenNest.Reporting/NestPdfWriter.cs b/OpenNest.Reporting/NestPdfWriter.cs index e66c5eae..0bbbd565 100644 --- a/OpenNest.Reporting/NestPdfWriter.cs +++ b/OpenNest.Reporting/NestPdfWriter.cs @@ -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}"; diff --git a/OpenNest.Reporting/NestReportSnapshot.cs b/OpenNest.Reporting/NestReportSnapshot.cs index 8ccb110d..46e9435f 100644 --- a/OpenNest.Reporting/NestReportSnapshot.cs +++ b/OpenNest.Reporting/NestReportSnapshot.cs @@ -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)); + /// + /// 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. + /// + public string ThicknessName { get; init; } = ""; + /// "lb" for inch jobs, "kg" for millimeter jobs. public string WeightUnits => Units == "mm" ? "kg" : "lb"; diff --git a/OpenNest.Tests/Reporting/NestPdfWriterTests.cs b/OpenNest.Tests/Reporting/NestPdfWriterTests.cs index a57fdbbd..711be5fb 100644 --- a/OpenNest.Tests/Reporting/NestPdfWriterTests.cs +++ b/OpenNest.Tests/Reporting/NestPdfWriterTests.cs @@ -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() { diff --git a/OpenNest.Tests/Reporting/ReportPdf.cs b/OpenNest.Tests/Reporting/ReportPdf.cs index 4c8268a7..d7598d8b 100644 --- a/OpenNest.Tests/Reporting/ReportPdf.cs +++ b/OpenNest.Tests/Reporting/ReportPdf.cs @@ -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++) { diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index db08a6f3..18b456e7 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -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); } diff --git a/docs/nest-reports.md b/docs/nest-reports.md index a829de3b..4efd42fb 100644 --- a/docs/nest-reports.md +++ b/docs/nest-reports.md @@ -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