SweepAsync(DateTime utcNow, Canc
}
}
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
- catch { result.Errors++; } // Never persist provider exceptions or sensitive content.
+ catch (Exception ex) { result.Errors++; Resgrid.Framework.Logging.LogError($"Checklist reminder sweep failed for department {departmentId}: {ex.GetType().FullName}."); }
}
}
return result;
diff --git a/Core/Resgrid.Services/ChecklistReportDocuments.cs b/Core/Resgrid.Services/ChecklistReportDocuments.cs
new file mode 100644
index 000000000..d89cc31de
--- /dev/null
+++ b/Core/Resgrid.Services/ChecklistReportDocuments.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Resources;
+using System.Security.Cryptography;
+using System.Text;
+using Newtonsoft.Json;
+using Resgrid.Model.Checklists;
+using Resgrid.Model.Providers;
+
+namespace Resgrid.Services
+{
+ /// In-memory exports from already authorized snapshots. No remote images, scripts or user HTML.
+ public static class ChecklistReportDocuments
+ {
+ private static readonly ResourceManager Strings = new("Resgrid.Localization.Areas.User.Checklists.Checklists", typeof(Resgrid.Localization.SupportedLocales).Assembly);
+ public static string Text(string key) => Strings.GetString(key, CultureInfo.CurrentUICulture) ?? key;
+ private static string H(object value) => WebUtility.HtmlEncode(Convert.ToString(value, CultureInfo.CurrentCulture));
+ private static string Cell(object value) => "| " + H(value) + " | ";
+ private static string Head(params string[] keys) => "" + string.Concat(keys.Select(k => "| " + H(Text(k)) + " | ")) + "
";
+ private static string Page(string title, string body) => "" + H(Text(title)) + "" + H(Text(title)) + "
" + body + "";
+ public static string Locked() => Page("ChecklistComplianceReport", "" + H(Text("ScheduledReportProtected")) + "
");
+ public static string Compliance(ChecklistComplianceSummary report, bool missedOnly = false)
+ {
+ var body = new StringBuilder("" + H(report.FromUtc.ToString("u")) + " — " + H(report.UntilUtc.ToString("u")) + "
" + H(Text("ComplianceMethod")) + "
");
+ body.Append("").Append(H(Text("AuthorizedScope"))).Append("
").Append(Head("Target", "ExpectedChecks", "CompletedChecks", "OnTimeChecks", "MissedChecks", "ExcusedChecks", "CompletionRate"));
+ foreach (var group in report.Groups.Where(g => !missedOnly || g.Missed > 0)) body.Append("").Append(Cell(group.Target.Name)).Append(Cell(group.Expected)).Append(Cell(group.Completed)).Append(Cell(group.OnTime)).Append(Cell(group.Missed)).Append(Cell(group.Skipped)).Append(Cell(group.CompletionRate?.ToString("0.##") ?? "—")).Append("
");
+ body.Append("
").Append(H(Text("MissedTrend"))).Append("
").Append(Head("Date", "ExpectedChecks", "MissedChecks"));
+ foreach (var day in report.Trend) body.Append("").Append(Cell(day.DayUtc.ToString("yyyy-MM-dd"))).Append(Cell(day.Expected)).Append(Cell(day.Missed)).Append("
");
+ body.Append("
").Append(Entries(report.Entries.Where(e => !missedOnly || e.Missed))).Append(Unavailable(report.UnavailableSources));
+ return Page(missedOnly ? "ChecklistMissedReport" : "ChecklistComplianceReport", body.ToString());
+ }
+ public static string ComplianceBody(ChecklistComplianceSummary report, bool missedOnly = false)
+ {
+ var html = Compliance(report, missedOnly);
+ return html.Substring(html.IndexOf("", StringComparison.Ordinal) + 6).Replace("