Public/Export-M365PPlan.ps1

function Export-M365PPlan {
    <#
    .SYNOPSIS
        Exports a complete Microsoft 365 Planner plan to JSON or XML format.
 
    .DESCRIPTION
        The Export-M365PPlan function exports a complete Planner plan including all metadata,
        buckets, tasks, assignments, and checklist items to a structured JSON or XML file.
        This is useful for backup, documentation, migration, or version control purposes.
 
    .PARAMETER PlanId
        The unique identifier (GUID) of the plan to export.
 
    .PARAMETER OutputPath
        The file path where the exported plan will be saved. The file extension (.json or .xml)
        will be added automatically based on the Format parameter if not provided.
 
    .PARAMETER Format
        The output format for the exported plan. Valid values are 'JSON' (default) or 'XML'.
 
    .PARAMETER IncludeCompleted
        If specified, completed tasks will be included in the export. By default, only
        incomplete tasks are exported.
 
    .PARAMETER Force
        If specified, overwrites the output file if it already exists without prompting.
 
    .EXAMPLE
        Export-M365PPlan -PlanId "abc123def456" -OutputPath ".\backup.json"
 
        Exports the specified plan to a JSON file named backup.json in the current directory.
 
    .EXAMPLE
        Export-M365PPlan -PlanId "abc123def456" -OutputPath ".\backup" -Format XML -IncludeCompleted
 
        Exports the plan including completed tasks to an XML file named backup.xml.
 
    .EXAMPLE
        Get-MgGroupPlannerPlan -GroupId "group123" | Export-M365PPlan -OutputPath ".\exports" -Format JSON
 
        Exports all plans from a specific group to separate JSON files in the exports directory.
 
    .NOTES
        Author: Sergio Cánovas Cardona
        Version: 1.1.0
        Requires: Microsoft.Graph.Planner module
        The exported file contains complete plan structure that can be imported with Import-M365PPlan.
 
    .LINK
        Import-M365PPlan
        Copy-M365PPlan
    #>


    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Id')]
        [string]$PlanId,

        [Parameter(Mandatory = $true)]
        [string]$OutputPath,

        [Parameter(Mandatory = $false)]
        [ValidateSet('JSON', 'XML')]
        [string]$Format = 'JSON',

        [Parameter(Mandatory = $false)]
        [switch]$IncludeCompleted,

        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    begin {
        Write-Verbose "Starting plan export process..."
        
        # Ensure output directory exists
        $outputDir = Split-Path -Path $OutputPath -Parent
        if ($outputDir -and -not (Test-Path -Path $outputDir)) {
            Write-Verbose "Creating output directory: $outputDir"
            New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
        }
    }

    process {
        try {
            # Step 1: Get plan details
            Write-Verbose "Retrieving plan details for Plan ID: $PlanId"
            $plan = Get-MgPlannerPlan -PlannerPlanId $PlanId -ErrorAction Stop
            
            if (-not $plan) {
                throw "Plan with ID '$PlanId' not found."
            }

            Write-Verbose "Plan found: $($plan.Title)"

            # Step 2: Get all buckets
            Write-Verbose "Retrieving buckets..."
            $buckets = Get-MgPlannerPlanBucket -PlannerPlanId $PlanId -All -ErrorAction Stop
            Write-Verbose "Found $($buckets.Count) bucket(s)"

            # Step 3: Get all tasks
            Write-Verbose "Retrieving tasks..."
            $allTasks = Get-MgPlannerPlanTask -PlannerPlanId $PlanId -All -ErrorAction Stop
            
            # Filter completed tasks if not included
            if (-not $IncludeCompleted) {
                $tasks = $allTasks | Where-Object { $_.PercentComplete -ne 100 }
                Write-Verbose "Found $($tasks.Count) incomplete task(s) (total: $($allTasks.Count))"
            } else {
                $tasks = $allTasks
                Write-Verbose "Found $($tasks.Count) task(s)"
            }

            # Step 4: Get task details (including checklist items)
            Write-Verbose "Retrieving detailed task information..."
            $taskDetails = @()
            foreach ($task in $tasks) {
                try {
                    $taskDetail = Get-MgPlannerTaskDetail -PlannerTaskId $task.Id -ErrorAction Stop
                    
                    $taskDetails += @{
                        Task = $task
                        Details = $taskDetail
                    }
                } catch {
                    Write-Warning "Could not retrieve details for task '$($task.Title)': $_"
                }
            }

            # Step 5: Build export structure
            $exportData = [PSCustomObject]@{
                ExportedAt = Get-Date -Format 'o'
                ExportedBy = (Get-MgContext).Account
                ModuleVersion = '1.1.0'
                Plan = @{
                    Id = $plan.Id
                    Title = $plan.Title
                    CreatedDateTime = $plan.CreatedDateTime
                    Owner = $plan.Owner
                    Container = $plan.Container
                }
                Buckets = @($buckets | ForEach-Object {
                    @{
                        Id = $_.Id
                        Name = $_.Name
                        OrderHint = $_.OrderHint
                        PlanId = $_.PlanId
                    }
                })
                Tasks = @($taskDetails | ForEach-Object {
                    @{
                        Id = $_.Task.Id
                        Title = $_.Task.Title
                        BucketId = $_.Task.BucketId
                        PlanId = $_.Task.PlanId
                        PercentComplete = $_.Task.PercentComplete
                        Priority = $_.Task.Priority
                        StartDateTime = $_.Task.StartDateTime
                        DueDateTime = $_.Task.DueDateTime
                        CompletedDateTime = $_.Task.CompletedDateTime
                        Assignments = $_.Task.Assignments.AdditionalProperties
                        OrderHint = $_.Task.OrderHint
                        Description = $_.Details.Description
                        Checklist = $_.Details.Checklist.AdditionalProperties
                        References = $_.Details.References.AdditionalProperties
                        PreviewType = $_.Details.PreviewType
                    }
                })
                Statistics = @{
                    TotalBuckets = $buckets.Count
                    TotalTasks = $tasks.Count
                    CompletedTasksIncluded = $IncludeCompleted.IsPresent
                    TotalTasksInPlan = $allTasks.Count
                }
            }

            # Step 6: Determine output file path
            $extension = if ($Format -eq 'JSON') { '.json' } else { '.xml' }
            if (-not $OutputPath.EndsWith($extension)) {
                $OutputPath = $OutputPath -replace '\.(json|xml)$', ''
                $OutputPath = "$OutputPath$extension"
            }

            # Check if file exists
            if ((Test-Path -Path $OutputPath) -and -not $Force) {
                $shouldContinue = $PSCmdlet.ShouldContinue(
                    "File '$OutputPath' already exists. Overwrite?",
                    "Confirm Overwrite"
                )
                if (-not $shouldContinue) {
                    Write-Warning "Export cancelled by user."
                    return
                }
            }

            # Step 7: Export to file
            if ($PSCmdlet.ShouldProcess($OutputPath, "Export plan '$($plan.Title)' as $Format")) {
                Write-Verbose "Exporting to: $OutputPath"
                
                if ($Format -eq 'JSON') {
                    $exportData | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding UTF8 -Force
                } else {
                    # Convert to XML
                    $xmlDoc = New-Object System.Xml.XmlDocument
                    $declaration = $xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", $null)
                    $xmlDoc.AppendChild($declaration) | Out-Null
                    
                    function ConvertTo-XmlElement {
                        param($Object, $ElementName, $XmlDoc, $ParentElement)
                        
                        $element = $XmlDoc.CreateElement($ElementName)
                        
                        if ($Object -is [System.Collections.IEnumerable] -and $Object -isnot [string]) {
                            foreach ($item in $Object) {
                                ConvertTo-XmlElement -Object $item -ElementName 'Item' -XmlDoc $XmlDoc -ParentElement $element
                            }
                        } elseif ($Object -is [System.Collections.IDictionary] -or $Object.PSObject.Properties) {
                            $props = if ($Object -is [System.Collections.IDictionary]) { $Object.Keys } else { $Object.PSObject.Properties.Name }
                            foreach ($prop in $props) {
                                $value = if ($Object -is [System.Collections.IDictionary]) { $Object[$prop] } else { $Object.$prop }
                                if ($null -ne $value) {
                                    if ($value -is [System.Collections.IEnumerable] -and $value -isnot [string]) {
                                        $childElement = ConvertTo-XmlElement -Object $value -ElementName $prop -XmlDoc $XmlDoc -ParentElement $element
                                        $element.AppendChild($childElement) | Out-Null
                                    } elseif ($value -is [System.Collections.IDictionary] -or $value.PSObject.Properties.Count -gt 0) {
                                        $childElement = ConvertTo-XmlElement -Object $value -ElementName $prop -XmlDoc $XmlDoc -ParentElement $element
                                        $element.AppendChild($childElement) | Out-Null
                                    } else {
                                        $element.SetAttribute($prop, $value.ToString())
                                    }
                                }
                            }
                        } else {
                            $element.InnerText = $Object.ToString()
                        }
                        
                        return $element
                    }
                    
                    $rootElement = ConvertTo-XmlElement -Object $exportData -ElementName 'PlannerPlanExport' -XmlDoc $xmlDoc -ParentElement $xmlDoc
                    $xmlDoc.AppendChild($rootElement) | Out-Null
                    $xmlDoc.Save($OutputPath)
                }

                Write-Verbose "Export completed successfully"
                Write-Host "Plan '$($plan.Title)' exported to: $OutputPath" -ForegroundColor Green
                Write-Host " - Buckets: $($buckets.Count)" -ForegroundColor Cyan
                Write-Host " - Tasks: $($tasks.Count)" -ForegroundColor Cyan
                Write-Host " - Format: $Format" -ForegroundColor Cyan

                # Return file info
                Get-Item -Path $OutputPath
            }

        } catch {
            Write-Error "Failed to export plan: $_"
            throw
        }
    }

    end {
        Write-Verbose "Export process completed"
    }
}