Public/New-CloudPCMaintenanceWindow.ps1

function New-CloudPCMaintenanceWindow {
    <#
    .SYNOPSIS
        Creates a Windows 365 Cloud PC maintenance window.

    .DESCRIPTION
        Creates a Cloud PC maintenance window by calling Microsoft Graph beta:
        POST /deviceManagement/virtualEndpoint/maintenanceWindows.

        Use the weekday and weekend parameters for the common Intune portal model,
        or pass one or more schedule objects with -Schedule for newer Graph schedule
        types. Each schedule must be at least two hours long.

        Pass -GroupId to assign the created maintenance window to Microsoft Entra
        groups after creation.

    .PARAMETER DisplayName
        Display name for the maintenance window.

    .PARAMETER Description
        Optional description.

    .PARAMETER NotificationLeadTimeInMinutes
        Number of minutes before the maintenance window opens that users are notified.
        Defaults to 60.

    .PARAMETER WeekdayStartTime
        Start time for the weekday schedule in HH:mm format.

    .PARAMETER WeekdayEndTime
        End time for the weekday schedule in HH:mm format.

    .PARAMETER WeekendStartTime
        Optional start time for the weekend schedule in HH:mm format.
        If omitted, the weekday start time is used for the weekend schedule.

    .PARAMETER WeekendEndTime
        Optional end time for the weekend schedule in HH:mm format.
        If omitted, the weekday end time is used for the weekend schedule.

    .PARAMETER Schedule
        One or more hashtables or objects with scheduleType, startTime, and endTime.
        Times may be HH:mm or Graph time-of-day values such as 01:00:00.0000000.

    .PARAMETER GroupId
        Microsoft Entra group IDs to assign after the maintenance window is created.

    .PARAMETER Force
        Suppress confirmation prompts. Equivalent to -Confirm:$false.

    .EXAMPLE
        New-CloudPCMaintenanceWindow -DisplayName 'Off-Hours Window' -WeekdayStartTime '01:00' -WeekdayEndTime '05:00' -Force

        Creates a maintenance window with matching weekday and weekend schedules.

    .EXAMPLE
        New-CloudPCMaintenanceWindow -DisplayName 'Resize Window' -WeekdayStartTime '01:00' -WeekdayEndTime '05:00' -GroupId '<group-id>' -Force

        Creates a maintenance window and assigns it to a Microsoft Entra group.
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium', DefaultParameterSetName = 'Convenience')]
    [OutputType('WindowsCloudPC.MaintenanceWindowCreateResult')]
    param(
        [Parameter(Mandatory)]
        [string]$DisplayName,

        [string]$Description,

        [ValidateRange(0, 1440)]
        [int]$NotificationLeadTimeInMinutes = 60,

        [Parameter(Mandatory, ParameterSetName = 'Convenience')]
        [ValidatePattern('^\d{2}:\d{2}$')]
        [string]$WeekdayStartTime,

        [Parameter(Mandatory, ParameterSetName = 'Convenience')]
        [ValidatePattern('^\d{2}:\d{2}$')]
        [string]$WeekdayEndTime,

        [Parameter(ParameterSetName = 'Convenience')]
        [ValidatePattern('^\d{2}:\d{2}$')]
        [string]$WeekendStartTime,

        [Parameter(ParameterSetName = 'Convenience')]
        [ValidatePattern('^\d{2}:\d{2}$')]
        [string]$WeekendEndTime,

        [Parameter(Mandatory, ParameterSetName = 'BySchedule')]
        [object[]]$Schedule,

        [string[]]$GroupId = @(),

        [switch]$Force
    )

    begin {
        if ($Force -and -not $PSBoundParameters.ContainsKey('Confirm')) {
            $ConfirmPreference = 'None'
        }

        Connect-CloudPC -AdditionalScopes 'CloudPC.ReadWrite.All' | Out-Null
    }

    process {
        if ([string]::IsNullOrWhiteSpace($DisplayName)) {
            Write-Error 'New-CloudPCMaintenanceWindow: DisplayName is required.'
            return
        }

        if ($PSCmdlet.ParameterSetName -eq 'Convenience') {
            if ($PSBoundParameters.ContainsKey('WeekendStartTime') -xor $PSBoundParameters.ContainsKey('WeekendEndTime')) {
                Write-Error 'New-CloudPCMaintenanceWindow: WeekendStartTime and WeekendEndTime must be specified together.'
                return
            }

            $effectiveWeekendStartTime = if ($PSBoundParameters.ContainsKey('WeekendStartTime')) { $WeekendStartTime } else { $WeekdayStartTime }
            $effectiveWeekendEndTime = if ($PSBoundParameters.ContainsKey('WeekendEndTime')) { $WeekendEndTime } else { $WeekdayEndTime }

            $schedules = @(
                @{
                    scheduleType = 'weekday'
                    startTime    = $WeekdayStartTime
                    endTime      = $WeekdayEndTime
                }
                @{
                    scheduleType = 'weekend'
                    startTime    = $effectiveWeekendStartTime
                    endTime      = $effectiveWeekendEndTime
                }
            )
        }
        else {
            $schedules = @($Schedule)
        }

        $normalizedSchedules = @(
            foreach ($item in $schedules) {
                $scheduleHash = if ($item -is [System.Collections.IDictionary]) {
                    $item
                }
                else {
                    $item | ConvertTo-Json -Depth 10 | ConvertFrom-Json -AsHashtable
                }

                foreach ($requiredProperty in @('scheduleType','startTime','endTime')) {
                    if (-not $scheduleHash.ContainsKey($requiredProperty) -or [string]::IsNullOrWhiteSpace([string]$scheduleHash[$requiredProperty])) {
                        Write-Error "New-CloudPCMaintenanceWindow: each schedule requires $requiredProperty."
                        return
                    }
                }

                $startTime = ConvertTo-CloudPCMaintenanceWindowTime -Value $scheduleHash['startTime'] -PropertyName 'startTime'
                $endTime = ConvertTo-CloudPCMaintenanceWindowTime -Value $scheduleHash['endTime'] -PropertyName 'endTime'
                if (-not $startTime -or -not $endTime) {
                    return
                }

                $duration = $endTime - $startTime
                if ($duration.TotalMinutes -le 0) {
                    $duration = $duration.Add([timespan]::FromDays(1))
                }
                if ($duration.TotalMinutes -lt 120) {
                    Write-Error "New-CloudPCMaintenanceWindow: schedule '$($scheduleHash['scheduleType'])' must be at least two hours long."
                    return
                }

                [ordered]@{
                    scheduleType = [string]$scheduleHash['scheduleType']
                    startTime    = $startTime.ToString('hh\:mm\:ss\.fff')
                    endTime      = $endTime.ToString('hh\:mm\:ss\.fff')
                }
            }
        )

        if ($normalizedSchedules.Count -eq 0) {
            Write-Error 'New-CloudPCMaintenanceWindow: at least one schedule is required.'
            return
        }

        $body = [ordered]@{
            displayName                   = $DisplayName
            description                   = if ($PSBoundParameters.ContainsKey('Description')) { $Description } else { '' }
            notificationLeadTimeInMinutes = $NotificationLeadTimeInMinutes
            schedules                     = @($normalizedSchedules)
        }
        Write-Verbose "New-CloudPCMaintenanceWindow: request body $($body | ConvertTo-Json -Depth 20 -Compress)"

        $created = $null
        $createdId = $null
        $status = 'WhatIf'
        $assignmentStatus = if ($GroupId.Count -gt 0) { 'WhatIf' } else { 'Skipped' }
        $assignmentsApplied = 0
        $errorMessage = $null

        if ($PSCmdlet.ShouldProcess("Cloud PC maintenance window '$DisplayName'", 'Create maintenance window')) {
            try {
                $uri = 'https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/maintenanceWindows'
                $created = Invoke-MgGraphRequest -Method POST -Uri $uri -ContentType 'application/json' -Body ($body | ConvertTo-Json -Depth 20 -Compress)
                $createdId = $created.id
                $status = 'Created'
            }
            catch {
                $status = 'Failed'
                $errorMessage = if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $_.ErrorDetails.Message } else { $_.Exception.Message }
                Write-Error -Message "New-CloudPCMaintenanceWindow: create failed for '$DisplayName'. $errorMessage" -Exception $_.Exception
            }

            if ($status -eq 'Created' -and $GroupId.Count -gt 0) {
                if ([string]::IsNullOrWhiteSpace($createdId)) {
                    $assignmentStatus = 'Failed'
                    $errorMessage = 'Graph create response did not include an id, so assignments could not be applied.'
                    Write-Error "New-CloudPCMaintenanceWindow: $errorMessage"
                }
                else {
                    try {
                        $assignments = @(
                            foreach ($id in $GroupId) {
                                if ([string]::IsNullOrWhiteSpace($id)) { continue }
                                $group = Resolve-CloudPCGroup -GroupId $id
                                @{
                                    target = @{
                                        groupId     = $id
                                        displayName = $group.DisplayName
                                    }
                                }
                            }
                        )

                        if ($assignments.Count -gt 0) {
                            $escapedId = [uri]::EscapeDataString($createdId)
                            $assignUri = "https://graph.microsoft.com/beta/deviceManagement/virtualEndpoint/maintenanceWindows/$escapedId/assign"
                            $assignBody = @{ assignments = @($assignments) } | ConvertTo-Json -Depth 20 -Compress
                            Write-Verbose "New-CloudPCMaintenanceWindow: assignment body $assignBody"
                            Invoke-MgGraphRequest -Method POST -Uri $assignUri -ContentType 'application/json' -Body $assignBody | Out-Null
                            $assignmentStatus = 'Assigned'
                            $assignmentsApplied = $assignments.Count
                        }
                        else {
                            $assignmentStatus = 'Skipped'
                        }
                    }
                    catch {
                        $assignmentStatus = 'Failed'
                        $errorMessage = if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $_.ErrorDetails.Message } else { $_.Exception.Message }
                        Write-Error -Message "New-CloudPCMaintenanceWindow: assignment failed for '$DisplayName'. $errorMessage" -Exception $_.Exception
                    }
                }
            }
        }

        [pscustomobject]@{
            PSTypeName         = 'WindowsCloudPC.MaintenanceWindowCreateResult'
            Id                 = $createdId
            DisplayName        = $DisplayName
            Status             = $status
            AssignmentStatus   = $assignmentStatus
            AssignmentsApplied = $assignmentsApplied
            ErrorMessage       = $errorMessage
            Raw                = $created
        }
    }

    end { }
}