Public/Import-M365PTasksFromCsv.ps1

function Import-M365PTasksFromCsv {
    <#
    .SYNOPSIS
        Imports tasks in bulk from a CSV file into a Microsoft Planner plan.
 
    .DESCRIPTION
        This function reads a CSV file containing task information and creates tasks
        in the specified Planner plan. It automatically creates buckets if they don't exist
        and can assign tasks to users by their email addresses.
 
    .PARAMETER PlanId
        The ID of the Planner plan where tasks will be imported.
 
    .PARAMETER CsvPath
        The path to the CSV file containing task data. Required columns:
        - Title: Task title (required)
        - BucketName: Name of the bucket (required)
        - StartDate: Task start date (optional, format: yyyy-MM-dd)
        - DueDate: Task due date (optional, format: yyyy-MM-dd)
        - AssignedUser: User email address to assign the task (optional)
 
    .PARAMETER GroupId
        The ID of the Microsoft 365 Group that owns the plan. Required for user assignment lookups.
 
    .EXAMPLE
        Import-M365PTasksFromCsv -PlanId "plan123" -CsvPath "C:\tasks.csv" -GroupId "group456"
         
        Imports all tasks from the CSV file into the specified plan.
 
    .EXAMPLE
        Get-Content tasks.csv | Import-M365PTasksFromCsv -PlanId "plan123" -GroupId "group456"
         
        Imports tasks from pipeline input.
 
    .NOTES
        Requires Microsoft.Graph.Planner module and appropriate permissions:
        - Tasks.ReadWrite
        - User.Read.All (if using AssignedUser column)
         
        CSV Format Example:
        Title,BucketName,StartDate,DueDate,AssignedUser
        "Setup environment","Sprint 1","2026-02-01","2026-02-05","user@contoso.com"
        "Code review","Sprint 1","2026-02-06","2026-02-10",""
    #>

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

        [Parameter(Mandatory = $true, ValueFromPipeline = $false)]
        [ValidateScript({
            if (-not (Test-Path $_)) {
                throw "CSV file not found: $_"
            }
            if ($_ -notmatch '\.csv$') {
                throw "File must be a CSV file"
            }
            return $true
        })]
        [string]$CsvPath,

        [Parameter(Mandatory = $true)]
        [string]$GroupId
    )

    begin {
        Write-Verbose "Starting bulk task import"
        Write-Verbose "Plan ID: $PlanId"
        Write-Verbose "CSV Path: $CsvPath"
        Write-Verbose "Group ID: $GroupId"

        # Initialize bucket cache
        $script:bucketCache = @{}
        
        # Initialize user cache for assignments
        $script:userCache = @{}
    }

    process {
        try {
            # Import CSV file
            Write-Verbose "Reading CSV file..."
            $tasks = Import-Csv -Path $CsvPath

            if ($tasks.Count -eq 0) {
                Write-Warning "No tasks found in CSV file"
                return
            }

            # Validate required columns
            $requiredColumns = @('Title', 'BucketName')
            $csvColumns = $tasks[0].PSObject.Properties.Name
            
            foreach ($column in $requiredColumns) {
                if ($column -notin $csvColumns) {
                    throw "CSV file missing required column: $column"
                }
            }

            Write-Verbose "Found $($tasks.Count) tasks to import"

            # Get existing buckets for the plan
            Write-Verbose "Loading existing buckets..."
            $existingBuckets = Get-MgPlannerPlanBucket -PlannerPlanId $PlanId -All
            foreach ($bucket in $existingBuckets) {
                $script:bucketCache[$bucket.Name] = $bucket.Id
            }
            Write-Verbose "Loaded $($existingBuckets.Count) existing buckets"

            # Process each task
            $successCount = 0
            $errorCount = 0

            for ($i = 0; $i -lt $tasks.Count; $i++) {
                $task = $tasks[$i]
                $taskNumber = $i + 1
                
                try {
                    Write-Verbose "[$taskNumber/$($tasks.Count)] Processing: $($task.Title)"

                    # Get or create bucket
                    if (-not $script:bucketCache.ContainsKey($task.BucketName)) {
                        Write-Verbose "Creating new bucket: $($task.BucketName)"
                        $newBucket = New-MgPlannerBucket -Name $task.BucketName -PlanId $PlanId
                        $script:bucketCache[$task.BucketName] = $newBucket.Id
                    }

                    $bucketId = $script:bucketCache[$task.BucketName]

                    # Build task parameters
                    $taskParams = @{
                        PlanId = $PlanId
                        BucketId = $bucketId
                        Title = $task.Title
                    }

                    # Add optional dates if provided
                    if ($task.PSObject.Properties['StartDate'] -and $task.StartDate) {
                        try {
                            $startDate = [DateTime]::Parse($task.StartDate)
                            $taskParams['StartDateTime'] = $startDate.ToString('yyyy-MM-ddTHH:mm:ssZ')
                        }
                        catch {
                            Write-Warning "Invalid StartDate format for task '$($task.Title)': $($task.StartDate)"
                        }
                    }

                    if ($task.PSObject.Properties['DueDate'] -and $task.DueDate) {
                        try {
                            $dueDate = [DateTime]::Parse($task.DueDate)
                            $taskParams['DueDateTime'] = $dueDate.ToString('yyyy-MM-ddTHH:mm:ssZ')
                        }
                        catch {
                            Write-Warning "Invalid DueDate format for task '$($task.Title)': $($task.DueDate)"
                        }
                    }

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

                    # Handle user assignment if specified
                    if ($task.PSObject.Properties['AssignedUser'] -and $task.AssignedUser) {
                        try {
                            Write-Verbose "Assigning task to: $($task.AssignedUser)"
                            
                            # Get or cache user ID
                            if (-not $script:userCache.ContainsKey($task.AssignedUser)) {
                                $user = Get-MgUser -Filter "mail eq '$($task.AssignedUser)' or userPrincipalName eq '$($task.AssignedUser)'" -Top 1
                                if ($user) {
                                    $script:userCache[$task.AssignedUser] = $user.Id
                                }
                                else {
                                    Write-Warning "User not found: $($task.AssignedUser)"
                                    $successCount++
                                    continue
                                }
                            }

                            $userId = $script:userCache[$task.AssignedUser]

                            # Get task for ETag
                            $taskForUpdate = Get-MgPlannerTask -PlannerTaskId $newTask.Id
                            
                            # Build assignments hashtable
                            $assignments = @{
                                $userId = @{
                                    "@odata.type" = "#microsoft.graph.plannerAssignment"
                                    "orderHint" = " !"
                                }
                            }

                            # Update task with assignment
                            Update-MgPlannerTask -PlannerTaskId $newTask.Id `
                                                 -Assignments $assignments `
                                                 -IfMatch $taskForUpdate.AdditionalProperties['@odata.etag']
                            
                            Write-Verbose "Task assigned successfully"
                        }
                        catch {
                            Write-Warning "Failed to assign task '$($task.Title)' to user: $_"
                        }
                    }

                    $successCount++
                }
                catch {
                    $errorCount++
                    Write-Error "Failed to import task '$($task.Title)': $_"
                }
            }

            # Summary
            Write-Verbose "Import completed: $successCount succeeded, $errorCount failed"
            
            [PSCustomObject]@{
                TotalTasks = $tasks.Count
                Succeeded = $successCount
                Failed = $errorCount
                PlanId = $PlanId
            }
        }
        catch {
            Write-Error "Failed to import tasks: $_"
            throw
        }
    }

    end {
        Write-Verbose "Import-M365PTasksFromCsv completed"
    }
}