Public/Copy-M365PPlan.ps1

function Copy-M365PPlan {
    <#
    .SYNOPSIS
        Clones a Microsoft Planner plan from one Microsoft 365 Group to another.
 
    .DESCRIPTION
        This function creates a complete copy of a Planner plan including all buckets and task titles.
        The new plan is created in the specified destination group. Task details like assignments,
        descriptions, and checklists are preserved as titles only in this MVP version.
 
    .PARAMETER SourceGroupId
        The ID of the source Microsoft 365 Group containing the plan to copy.
 
    .PARAMETER SourcePlanId
        The ID of the plan to copy.
 
    .PARAMETER DestinationGroupId
        The ID of the destination Microsoft 365 Group where the plan will be copied.
 
    .PARAMETER NewPlanTitle
        Optional. The title for the new plan. If not specified, uses the original plan title with " (Copy)" appended.
 
    .EXAMPLE
        Copy-M365PPlan -SourceGroupId "abc123" -SourcePlanId "plan456" -DestinationGroupId "xyz789"
         
        Copies the specified plan to the destination group with the original title plus " (Copy)".
 
    .EXAMPLE
        Copy-M365PPlan -SourceGroupId "abc123" -SourcePlanId "plan456" -DestinationGroupId "xyz789" -NewPlanTitle "Q1 Project Plan"
         
        Copies the plan with a custom title.
 
    .NOTES
        Requires Microsoft.Graph.Planner module and appropriate permissions:
        - Group.Read.All
        - Tasks.ReadWrite
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$SourceGroupId,

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

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

        [Parameter(Mandatory = $false)]
        [string]$NewPlanTitle
    )

    begin {
        Write-Verbose "Starting plan copy operation"
        Write-Verbose "Source Group: $SourceGroupId"
        Write-Verbose "Source Plan: $SourcePlanId"
        Write-Verbose "Destination Group: $DestinationGroupId"
    }

    process {
        try {
            # Get source plan details
            Write-Verbose "Retrieving source plan details..."
            $sourcePlan = Get-MgGroupPlannerPlan -GroupId $SourceGroupId -PlannerPlanId $SourcePlanId
            
            if (-not $sourcePlan) {
                throw "Source plan not found"
            }

            # Determine new plan title
            if (-not $NewPlanTitle) {
                $NewPlanTitle = "$($sourcePlan.Title) (Copy)"
            }

            Write-Verbose "Creating new plan: $NewPlanTitle"
            
            # Create new plan in destination group
            $newPlan = New-MgGroupPlannerPlan -GroupId $DestinationGroupId -Title $NewPlanTitle
            Write-Verbose "New plan created with ID: $($newPlan.Id)"

            # Get all buckets from source plan
            Write-Verbose "Retrieving buckets from source plan..."
            $sourceBuckets = Get-MgPlannerPlanBucket -PlannerPlanId $SourcePlanId -All
            Write-Verbose "Found $($sourceBuckets.Count) buckets"

            # Create bucket mapping (old ID -> new ID)
            $bucketMapping = @{}

            foreach ($bucket in $sourceBuckets) {
                Write-Verbose "Creating bucket: $($bucket.Name)"
                
                $newBucket = New-MgPlannerBucket -Name $bucket.Name -PlanId $newPlan.Id
                $bucketMapping[$bucket.Id] = $newBucket.Id
                
                Write-Verbose "Bucket created with ID: $($newBucket.Id)"
            }

            # Get all tasks from source plan
            Write-Verbose "Retrieving tasks from source plan..."
            $sourceTasks = Get-MgPlannerPlanTask -PlannerPlanId $SourcePlanId -All
            Write-Verbose "Found $($sourceTasks.Count) tasks"

            # Copy tasks to new buckets
            $taskCount = 0
            foreach ($task in $sourceTasks) {
                $taskCount++
                Write-Verbose "[$taskCount/$($sourceTasks.Count)] Copying task: $($task.Title)"
                
                $newBucketId = $bucketMapping[$task.BucketId]
                
                $taskParams = @{
                    PlanId = $newPlan.Id
                    BucketId = $newBucketId
                    Title = $task.Title
                }

                # Preserve optional properties if they exist
                if ($task.StartDateTime) {
                    $taskParams['StartDateTime'] = $task.StartDateTime
                }
                if ($task.DueDateTime) {
                    $taskParams['DueDateTime'] = $task.DueDateTime
                }
                if ($task.PercentComplete) {
                    $taskParams['PercentComplete'] = 0  # Reset progress for copied tasks
                }

                $newTask = New-MgPlannerTask @taskParams
                Write-Verbose "Task created with ID: $($newTask.Id)"
            }

            Write-Verbose "Plan copy completed successfully"
            
            # Return the new plan object
            return $newPlan

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

    end {
        Write-Verbose "Copy-M365PPlan completed"
    }
}