using SharepointToolbox.Web.Services.Export; namespace SharepointToolbox.Web.Core.Models; /// How often a scheduled report recurs. public enum ReportFrequency { Daily, Weekly, Monthly } /// /// A recurrence rule. The report fires at on the cadence /// described by . applies to Weekly; /// applies to Monthly (clamped to the last day of short months). /// All times are UTC to keep scheduling unambiguous across DST. /// public class RecurrenceRule { public ReportFrequency Frequency { get; set; } = ReportFrequency.Weekly; /// Time of day to run, UTC, "HH:mm". public string TimeOfDayUtc { get; set; } = "06:00"; /// 0 = Sunday … 6 = Saturday. Used when is Weekly. public DayOfWeek DayOfWeek { get; set; } = DayOfWeek.Monday; /// 1–31. Used when is Monthly. public int DayOfMonth { get; set; } = 1; /// /// Computes the next fire time strictly after . /// public DateTime ComputeNextRunUtc(DateTime afterUtc) { var (hh, mm) = ParseTime(TimeOfDayUtc); switch (Frequency) { case ReportFrequency.Daily: { var candidate = new DateTime(afterUtc.Year, afterUtc.Month, afterUtc.Day, hh, mm, 0, DateTimeKind.Utc); if (candidate <= afterUtc) candidate = candidate.AddDays(1); return candidate; } case ReportFrequency.Weekly: { var candidate = new DateTime(afterUtc.Year, afterUtc.Month, afterUtc.Day, hh, mm, 0, DateTimeKind.Utc); int delta = ((int)DayOfWeek - (int)candidate.DayOfWeek + 7) % 7; candidate = candidate.AddDays(delta); if (candidate <= afterUtc) candidate = candidate.AddDays(7); return candidate; } case ReportFrequency.Monthly: default: { var candidate = BuildMonthly(afterUtc.Year, afterUtc.Month, hh, mm); if (candidate <= afterUtc) { var next = afterUtc.AddMonths(1); candidate = BuildMonthly(next.Year, next.Month, hh, mm); } return candidate; } } } private DateTime BuildMonthly(int year, int month, int hh, int mm) { int day = Math.Min(DayOfMonth, DateTime.DaysInMonth(year, month)); return new DateTime(year, month, day, hh, mm, 0, DateTimeKind.Utc); } private static (int Hour, int Minute) ParseTime(string s) { var parts = (s ?? "06:00").Split(':'); int hh = parts.Length > 0 && int.TryParse(parts[0], out var h) ? Math.Clamp(h, 0, 23) : 6; int mm = parts.Length > 1 && int.TryParse(parts[1], out var m) ? Math.Clamp(m, 0, 59) : 0; return (hh, mm); } } /// /// Flat, serializable bag of the report-generation options used across all report /// types. Only the fields relevant to the chosen /// are honoured; the runner maps them to the concrete option records /// (, , …). /// public class ScheduledReportOptions { // Permissions public bool IncludeInherited { get; set; } public bool ScanFolders { get; set; } = true; // Permissions + Storage public int FolderDepth { get; set; } = 1; public bool IncludeSubsites { get; set; } // Storage public bool PerLibrary { get; set; } = true; public bool IncludeHiddenLibraries { get; set; } = true; public bool IncludePreservationHold { get; set; } = true; public bool IncludeListAttachments { get; set; } = true; public bool IncludeRecycleBin { get; set; } = true; // Duplicates public string DuplicateMode { get; set; } = "Files"; public bool MatchSize { get; set; } = true; public bool MatchCreated { get; set; } public bool MatchModified { get; set; } public bool MatchSubfolderCount { get; set; } public bool MatchFileCount { get; set; } public string? Library { get; set; } // Version cleanup public List LibraryTitles { get; set; } = new(); public int KeepLast { get; set; } = 5; public bool KeepFirst { get; set; } // Search public List Extensions { get; set; } = new(); public string? Regex { get; set; } public int MaxResults { get; set; } = 1000; // User access audit — the logins/emails to report access for (substring matched). public List TargetUserLogins { get; set; } = new(); } /// /// Optional email delivery for a generated report, sent through Graph (app-only, /// Mail.Send). The report file is attached. Body/subject support the /// placeholders {ReportName}, {ClientName}, {ReportType}, {DateUtc} and {FileName}. /// public class ReportEmailSettings { public bool Enabled { get; set; } /// /// Mailbox to send AS (UPN or address). App-only Graph has no signed-in user, so a /// concrete sender mailbox is required — Graph posts to /users/{From}/sendMail. /// public string From { get; set; } = string.Empty; public List To { get; set; } = new(); public List Cc { get; set; } = new(); public string Subject { get; set; } = "{ClientName} — {ReportName}"; /// HTML body. Placeholders are substituted before sending. public string Body { get; set; } = "

Hello,

Please find attached the {ReportType} report \"{ReportName}\" for {ClientName}, generated on {DateUtc} UTC.

"; } /// /// A user-defined schedule that generates a report for a single client (profile) /// on a recurrence. Persisted to schedules.json. /// public class ScheduledReport { public string Id { get; set; } = Guid.NewGuid().ToString(); /// this schedule belongs to. public string ProfileId { get; set; } = string.Empty; /// Human label shown in the UI. public string Name { get; set; } = string.Empty; public ReportType Type { get; set; } public ScheduledReportOptions Options { get; set; } = new(); /// When true, run against every site in the tenant (site discovery); otherwise use . public bool AllSites { get; set; } = true; public List SiteUrls { get; set; } = new(); public ReportMergeMode MergeMode { get; set; } = ReportMergeMode.SingleMerged; public ReportFormat Format { get; set; } = ReportFormat.Html; public RecurrenceRule Recurrence { get; set; } = new(); /// Optional Graph email delivery of the generated report. public ReportEmailSettings Email { get; set; } = new(); public bool Enabled { get; set; } = true; public string CreatedBy { get; set; } = string.Empty; public DateTime CreatedUtc { get; set; } = DateTime.UtcNow; public DateTime? LastRunUtc { get; set; } public DateTime? NextRunUtc { get; set; } }