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" } } |