Send-SNSMandatoryMomentReport.ps1
|
<#
.SYNOPSIS Gets a named value from a dictionary or object. .DESCRIPTION Reads a key from an IDictionary or a property from another PowerShell object. Returns null when the object is null or does not contain the requested name. .PARAMETER Object Dictionary or object to read from. .PARAMETER Name Key or property name to retrieve. .OUTPUTS System.Object The value associated with the name, or null when it is not present. #> function Get-SNSMandatoryMomentValue { param ( [Parameter()] $Object, [Parameter(Mandatory)] [string]$Name ) if ($null -eq $Object) { return $null } if ($Object -is [System.Collections.IDictionary]) { if ($Object.Contains($Name)) { return $Object[$Name] } return $null } $property = $Object.PSObject.Properties[$Name] if ($null -ne $property) { return $property.Value } return $null } <# .SYNOPSIS Gets role names from nested Scoutnet role data. .DESCRIPTION Recursively searches dictionaries and collections for role_name values and writes each discovered role name to the pipeline. .PARAMETER RoleData Nested role information from a Scoutnet member record. .OUTPUTS System.String The role names found in the supplied role data. #> function Get-SNSMandatoryMomentRoleName { param ( [Parameter()] $RoleData ) if ($null -eq $RoleData) { return } $roleName = Get-SNSMandatoryMomentValue -Object $RoleData -Name 'role_name' if (-not [string]::IsNullOrWhiteSpace([string]$roleName)) { Write-Output $roleName return } if ($RoleData -is [System.Collections.IDictionary]) { foreach ($value in $RoleData.Values) { Get-SNSMandatoryMomentRoleName -RoleData $value } } elseif ($RoleData -is [System.Collections.IEnumerable] -and $RoleData -isnot [string]) { foreach ($value in $RoleData) { Get-SNSMandatoryMomentRoleName -RoleData $value } } } <# .SYNOPSIS Converts a Scoutnet date field to a date-only value. .DESCRIPTION Reads the value member of a Scoutnet field object and parses it using invariant culture. Empty values return null. Invalid dates cause an error that identifies the member field. .PARAMETER Value Scoutnet field object containing a value property. .PARAMETER FieldName Member field name, used in an error if the date cannot be parsed. .OUTPUTS System.DateTime The parsed date with its time component removed, or null for an empty value. .EXAMPLE ConvertTo-SNSMandatoryMomentDate -Value @{ value = '2025-03-01' } -FieldName 'pc_course_89' #> function ConvertTo-SNSMandatoryMomentDate { param ( [Parameter()] $Value, [Parameter(Mandatory)] [string]$FieldName ) $dateValue = Get-SNSMandatoryMomentValue -Object $Value -Name 'value' if ([string]::IsNullOrWhiteSpace([string]$dateValue)) { return $null } $date = [datetime]::MinValue $parsed = [datetime]::TryParse( [string]$dateValue, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::None, [ref]$date ) if (-not $parsed) { throw "Could not parse '$dateValue' in member field '$FieldName' as a date." } return $date.Date } <# .SYNOPSIS Creates the HTML report for Scoutnet mandatory moments. .DESCRIPTION Builds separate tables for Trygga möten and Registerutdrag. Includes members older than the configured minimum age who hold at least one non-excluded group or troop role and whose moment date is missing or older than the configured validity period. Each table is preceded by an explanation of these inclusion criteria. .PARAMETER Memberlist Scoutnet member list returned by Get-SNSApiGroupMemberlist. .PARAMETER MinimumAge Minimum age threshold. Members must be older than this age. .PARAMETER ValidityYears Number of years a completed mandatory moment remains valid. .PARAMETER CourseDescription Configured description displayed above the Trygga möten table. .PARAMETER PoliceCheckDescription Configured description displayed above the Registerutdrag table. .PARAMETER ExcludedRoles Role names that do not qualify a member for inclusion in the report. A member with at least one other role is still included. .PARAMETER AsOfDate Date used for age and validity calculations. Defaults to today. .OUTPUTS System.String Complete HTML document for the mandatory moment report. .EXAMPLE $html = ConvertTo-SNSMandatoryMomentReportHtml -Memberlist $memberlist ` -MinimumAge 15 -ValidityYears 3 ` -CourseDescription $conf.MandatoryMomentCourseDescription ` -PoliceCheckDescription $conf.MandatoryMomentPoliceCheckDescription #> function ConvertTo-SNSMandatoryMomentReportHtml { param ( [Parameter(Mandatory)] [System.Collections.IDictionary]$Memberlist, [Parameter(Mandatory)] [int]$MinimumAge, [Parameter(Mandatory)] [int]$ValidityYears, [Parameter()] [string]$CourseDescription, [Parameter()] [string]$PoliceCheckDescription, [Parameter()] [string[]]$ExcludedRoles = @(), [Parameter()] [datetime]$AsOfDate = [datetime]::Today ) if ($null -eq $Memberlist.data -or $Memberlist.data -isnot [System.Collections.IDictionary]) { throw "The member list does not contain the expected 'data' dictionary." } $members = @($Memberlist.data.Values) $unitNamesById = @{} foreach ($member in $members) { $unit = Get-SNSMandatoryMomentValue -Object $member -Name 'unit' $unitId = Get-SNSMandatoryMomentValue -Object $unit -Name 'raw_value' $unitName = Get-SNSMandatoryMomentValue -Object $unit -Name 'value' if (-not [string]::IsNullOrWhiteSpace([string]$unitId) -and -not [string]::IsNullOrWhiteSpace([string]$unitName)) { $unitNamesById[[string]$unitId] = [string]$unitName } } $cutoffDate = $AsOfDate.Date.AddYears(-$ValidityYears) $moments = @( @{ Field = 'pc_course_89'; Name = 'Trygga möten'; Description = $CourseDescription } @{ Field = 'latest_police_check'; Name = 'Registerutdrag'; Description = $PoliceCheckDescription } ) $html = [System.Text.StringBuilder]::new() [void]$html.AppendLine('<!doctype html><html lang="sv"><head><meta charset="utf-8">') [void]$html.AppendLine('<meta name="viewport" content="width=device-width, initial-scale=1">') [void]$html.AppendLine('<style>body{font-family:Arial,sans-serif;color:#222}table{border-collapse:collapse;width:100%;margin-bottom:2em}th,td{border:1px solid #bbb;padding:6px;text-align:left}th{background:#eee}section{margin-bottom:2em}</style>') [void]$html.AppendLine('</head><body>') $inclusionReason = "En medlem är med i listan om medlemmen är äldre än $MinimumAge år, har minst en kår- eller avdelningsroll och det var minst $ValidityYears år sedan momentet gjordes." $inclusionReason = [System.Net.WebUtility]::HtmlEncode($inclusionReason) [void]$html.AppendLine("<p>$inclusionReason</p>") foreach ($moment in $moments) { $description = [System.Net.WebUtility]::HtmlEncode($moment.Description) [void]$html.AppendLine("<section><h2>$($moment.Name)</h2><p>$description</p>") [void]$html.AppendLine('<table><thead><tr><th>Efternamn</th><th>Förnamn</th><th>Datum när momentet gjordes</th><th>Roller i kåren</th><th>Roller på avdelning</th></tr></thead><tbody>') foreach ($member in $members) { $birthDate = ConvertTo-SNSMandatoryMomentDate -Value $member.date_of_birth -FieldName 'date_of_birth' if ($null -eq $birthDate) { continue } $age = $AsOfDate.Year - $birthDate.Year if ($birthDate.Date -gt $AsOfDate.Date.AddYears(-$age)) { $age-- } if ($age -le $MinimumAge) { continue } $roles = Get-SNSMandatoryMomentValue -Object $member.roles -Name 'value' $groupRoles = @(Get-SNSMandatoryMomentRoleName -RoleData (Get-SNSMandatoryMomentValue -Object $roles -Name 'group') | Sort-Object -Unique) $troopRoles = Get-SNSMandatoryMomentValue -Object $roles -Name 'troop' $unitRoleDescriptions = @( if ($troopRoles -is [System.Collections.IDictionary]) { foreach ($troopEntry in $troopRoles.GetEnumerator()) { $troopRoleNames = @(Get-SNSMandatoryMomentRoleName -RoleData $troopEntry.Value | Sort-Object -Unique) if ($troopRoleNames.Count -gt 0) { $troopId = [string]$troopEntry.Key $troopName = $unitNamesById[$troopId] if ([string]::IsNullOrWhiteSpace($troopName)) { $troopName = "Avdelning $troopId" } "$troopName`: $($troopRoleNames -join ', ')" } } } ) $unitRoles = @(Get-SNSMandatoryMomentRoleName -RoleData $troopRoles | Sort-Object -Unique) $qualifyingGroupRoles = @($groupRoles | Where-Object { $_ -notin $ExcludedRoles }) $qualifyingUnitRoles = @($unitRoles | Where-Object { $_ -notin $ExcludedRoles }) if ($qualifyingGroupRoles.Count -eq 0 -and $qualifyingUnitRoles.Count -eq 0) { continue } $completedDate = ConvertTo-SNSMandatoryMomentDate -Value $member[$moment.Field] -FieldName $moment.Field if ($null -ne $completedDate -and $completedDate -gt $cutoffDate) { continue } $lastName = [System.Net.WebUtility]::HtmlEncode([string](Get-SNSMandatoryMomentValue -Object $member.last_name -Name 'value')) $firstName = [System.Net.WebUtility]::HtmlEncode([string](Get-SNSMandatoryMomentValue -Object $member.first_name -Name 'value')) $dateText = if ($null -eq $completedDate) { 'Aldrig utfört' } else { $completedDate.ToString('yyyy-MM-dd') } $dateText = [System.Net.WebUtility]::HtmlEncode($dateText) $groupRoleText = [System.Net.WebUtility]::HtmlEncode(($groupRoles -join ', ')) $unitRoleText = [System.Net.WebUtility]::HtmlEncode(($unitRoleDescriptions -join '; ')) [void]$html.AppendLine("<tr><td>$lastName</td><td>$firstName</td><td>$dateText</td><td>$groupRoleText</td><td>$unitRoleText</td></tr>") } [void]$html.AppendLine('</tbody></table></section>') } [void]$html.AppendLine('</body></html>') return $html.ToString() } <# .SYNOPSIS Determines whether the mandatory moment report is due. .DESCRIPTION Checks whether the configured daily, weekly, or monthly reporting period has started and whether a report has already been sent in that period. .PARAMETER Frequency Reporting cadence: Daily, Weekly (Monday), or Monthly (the first day). .PARAMETER LastSentDate Date and time of the last successful report. DateTime.MinValue means no report has previously been sent. .PARAMETER AsOfDate Date used to determine the current reporting period. Defaults to today. .OUTPUTS System.Boolean True when a report is due; otherwise, false. .EXAMPLE Test-SNSMandatoryMomentReportDue -Frequency Monthly -AsOfDate (Get-Date) #> function Test-SNSMandatoryMomentReportDue { param ( [Parameter(Mandatory)] [ValidateSet('Monthly', 'Weekly', 'Daily')] [string]$Frequency, [Parameter()] [datetime]$LastSentDate = [datetime]::MinValue, [Parameter()] [datetime]$AsOfDate = [datetime]::Today ) $today = $AsOfDate.Date switch ($Frequency) { 'Monthly' { return $today.Day -eq 1 -and ($LastSentDate -eq [datetime]::MinValue -or $LastSentDate.Year -ne $today.Year -or $LastSentDate.Month -ne $today.Month) } 'Weekly' { $weekStart = $today.AddDays(-(([int]$today.DayOfWeek + 6) % 7)) $lastSentWeekStart = $LastSentDate.Date.AddDays(-(([int]$LastSentDate.DayOfWeek + 6) % 7)) return $today.DayOfWeek -eq [DayOfWeek]::Monday -and ($LastSentDate -eq [datetime]::MinValue -or $lastSentWeekStart -ne $weekStart) } 'Daily' { return $LastSentDate.Date -lt $today } } } <# .SYNOPSIS Creates and emails the mandatory moment report. .DESCRIPTION Validates the report configuration and saved last-sent value, checks whether the configured reporting period is due, generates the HTML report, and sends it to the configured recipients using Microsoft Graph. Returns false when the report is not due or there are no recipients; returns true after a successful send. If Memberlist is not supplied, it is fetched from Scoutnet. .PARAMETER Configuration Report configuration. Defaults to the current SNS configuration. .PARAMETER Memberlist Optional Scoutnet member list. When omitted, the member list is fetched using the configured member-list credential. .PARAMETER LastSentValue Raw value of the MandatoryMomentReportLastSent Automation variable. Empty means that no prior report date is available. .PARAMETER AsOfDate Date used for reporting cadence, member ages, and moment validity. Defaults to today. .OUTPUTS System.Boolean True when the report is sent; false when no send is due or no recipients are configured. .EXAMPLE $lastSent = Get-AutomationVariable -Name 'MandatoryMomentReportLastSent' $sent = Send-SNSMandatoryMomentReport -Configuration $conf -LastSentValue $lastSent if ($sent) { Set-AutomationVariable -Name 'MandatoryMomentReportLastSent' -Value (Get-Date -Format 'o') } #> function Send-SNSMandatoryMomentReport { [CmdletBinding()] [OutputType([bool])] param ( [Parameter()] [SNSConfiguration]$Configuration = (Get-SNSConfiguration), [Parameter()] [System.Collections.IDictionary]$Memberlist, [Parameter()] [object]$LastSentValue, [Parameter()] [datetime]$AsOfDate = [datetime]::Today ) $lastSentDate = [datetime]::MinValue if ($null -ne $LastSentValue -and -not [string]::IsNullOrWhiteSpace([string]$LastSentValue)) { $parsedLastSentDate = [datetime]::MinValue if (-not [datetime]::TryParse( [string]$LastSentValue, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::RoundtripKind, [ref]$parsedLastSentDate )) { Write-SNSLog -Level "Warn" "Automation variable 'MandatoryMomentReportLastSent' does not contain a valid date; using today's date instead." $lastSentDate = $AsOfDate.Date } else { $lastSentDate = $parsedLastSentDate } } if ($Configuration.MandatoryMomentReportFrequency -notin @('Monthly', 'Weekly', 'Daily')) { throw "MandatoryMomentReportFrequency must be 'Monthly', 'Weekly', or 'Daily'." } if ($Configuration.MandatoryMomentMinimumAge -lt 0) { throw 'MandatoryMomentMinimumAge must be zero or greater.' } if ($Configuration.MandatoryMomentValidityYears -lt 1) { throw 'MandatoryMomentValidityYears must be at least one.' } if (-not (Test-SNSMandatoryMomentReportDue ` -Frequency $Configuration.MandatoryMomentReportFrequency ` -LastSentDate $lastSentDate ` -AsOfDate $AsOfDate)) { Write-SNSLog "Mandatory moment report is not due ($($Configuration.MandatoryMomentReportFrequency))." return $false } $recipients = @($Configuration.MandatoryMomentReportRecipients | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) if ($recipients.Count -eq 0) { Write-SNSLog 'Mandatory moment report not sent because no recipients are configured.' return $false } if ([string]::IsNullOrWhiteSpace($Configuration.EmailFromAddress)) { throw 'EmailFromAddress must be configured to send the mandatory moment report.' } if ($null -eq $Memberlist) { $Memberlist = Get-SNSApiGroupMemberlist -Credential $Configuration.CredentialMemberlist } $html = ConvertTo-SNSMandatoryMomentReportHtml ` -Memberlist $Memberlist ` -MinimumAge $Configuration.MandatoryMomentMinimumAge ` -ValidityYears $Configuration.MandatoryMomentValidityYears ` -CourseDescription $Configuration.MandatoryMomentCourseDescription ` -PoliceCheckDescription $Configuration.MandatoryMomentPoliceCheckDescription ` -ExcludedRoles $Configuration.MandatoryMomentExcludedRoles ` -AsOfDate $AsOfDate $params = @{ Message = @{ Subject = 'Rapport: obligatoriska moment' Body = @{ ContentType = 'HTML' Content = $html } ToRecipients = @( foreach ($recipient in $recipients) { @{ EmailAddress = @{ Address = $recipient } } } ) } SaveToSentItems = 'false' } Send-MgUserMail -UserId $Configuration.EmailFromAddress -BodyParameter $params -ErrorAction Stop | Out-Null Write-SNSLog "Mandatory moment report sent to $($recipients -join ', ')." return $true } |