lib/Classes/Public/TMTask.ps1


class TMTask {

    #region Non-Static Properties

    [System.Int64]$Id

    [System.Int64]$TaskNumber

    [System.String]$Title

    [System.String]$Comment

    [ValidateSet('Hold', 'Planned', 'Ready', 'Pending', 'Started', 'Completed', 'Terminated')]
    [System.String]$Status

    [Nullable[System.DateTime]]$StatusUpdated

    [System.String]$StatusUpdatedElapsed

    [Nullable[System.DateTime]]$LastUpdated

    [System.String]$LastUpdatedElapsed

    [TMTaskAction]$Action

    [TMTaskAsset]$Asset

    [TMReference]$AssignedTo

    [TMReference]$CreatedBy

    [System.String]$Category

    [Nullable[System.DateTime]]$DateCreated

    [ValidateRange(0, 1)]
    [System.Int64]$HardAssigned

    [System.Int64]$EstDurationMinutes

    [String]$EstStart

    [String]$EstFinish

    [System.Int64]$Slack

    [System.Boolean]$IsCriticalPath

    [Nullable[System.DateTime]]$ActStart

    [Nullable[System.DateTime]]$ActFinish

    [System.String]$Team

    [System.Boolean]$IsPublished

    [ValidateRange(0, 100)]
    [System.Int64]$PercentageComplete

    [TMReference]$Project

    [System.Boolean]$IsActionInvocableLocally

    [System.Boolean]$IsActionInvocableRemotely

    [System.Boolean]$IsAutomatic

    [System.Int64]$Duration

    [System.Boolean]$SendNotification

    [ValidateRange(0, 5)]
    [System.Int64]$Priority

    [TMReference]$Event

    [Nullable[System.DateTime]]$DueDate

    [System.String]$InstructionsLink

    [TMTaskDependency[]]$Predecessors

    [TMTaskDependency[]]$Successors

    [System.Int32]$Schema

    [System.String]$Attribute

    [System.Boolean]$AutoGenerated

    [System.String]$DisplayOption

    [System.Object]$Recipe

    [System.Int64]$TaskSpec

    #endregion Non-Static Properties

    #region Static Properties

    # The only valid statuses for a Task
    static [System.String[]]$ValidStatuses = @(
        'Hold',
        'Planned',
        'Ready',
        'Pending',
        'Started',
        'Completed',
        'Terminated'
    )

    # List of field/property names that are used in the TMQL request to TM
    static [System.String[]]$TMQLFetchProperties = @(
        'actFinish'
        'actStart'
        'apiAction.actionType'
        'apiAction.description'
        'apiAction.id'
        'apiAction.isRemote'
        'apiAction.methodParams'
        'apiAction.name'
        'apiActionCompletedAt'
        'apiActionInvokedAt'
        'apiActionSettings'
        'assetEntity.Asset Class'
        'assetEntity.assetType'
        'assetEntity.Bundle'
        'assetEntity.Id'
        'assetEntity.Name'
        'assetEntity.Tags'
        'assignedTo.id'
        'assignedTo.name'
        'attribute'
        'autoGenerated'
        'category'
        'comment'
        'commentType'
        'createdBy.id'
        'createdBy.name'
        'dateCreated'
        'dateResolved'
        'displayOption'
        'dueDate'
        'duration'
        'estFinish'
        'estStart'
        'hardAssigned'
        'id'
        'instructionsLink'
        'isCriticalPath'
        'isPublished'
        'lastUpdated'
        'moveEvent.id'
        'moveEvent.name'
        'percentageComplete'
        'predecessors'
        'priority'
        'project.id'
        'project.name'
        'recipe'
        'role'
        'sendNotification'
        'slack'
        'status'
        'statusUpdated'
        'successors'
        'taskNumber'
        'taskSpec'
    )

    # String that is used in the TMQL query to fetch all of the necessary details about the Task
    static [String]$TMQLFetchString = "fetch '" + ([TMTask]::TMQLFetchProperties -join "', '") + "'"

    #endregion Static Properties

    #region Constructors

    TMTask() {
        $this.addPublicMembers()
    }

    TMTask([Object]$object) {
        $this.Schema = $object.PSObject.Properties.Name -contains 'assetEntity.Tags' ? 2 : 1
        $this.Id = $object.id
        $this.TaskNumber = $object.taskNumber
        $this.Comment = $object.comment ?? $object.title
        $this.Title = $object.title ?? $object.comment
        $this.Status = $object.status
        $this.StatusUpdated = $object.statusUpdated
        $this.StatusUpdatedElapsed = $object.statusUpdatedElapsed ?? ($object.statusUpdated ? (New-TimeSpan -Start $object.statusUpdated -End (Get-Date -AsUTC)) : "")
        $this.LastUpdated = $object.lastUpdated
        $this.LastUpdatedElapsed = $object.lastUpdatedElapsed ?? ($object.statusUpdated ? (New-TimeSpan -Start $object.lastUpdated -End (Get-Date -AsUTC)) : "")
        $this.Action = if ($this.Schema -eq 1) {
            [TMTaskAction]::new(($object.action ?? $object.apiAction))
        } else {
            [TMTaskAction]::new(
                $object.'apiAction.id',
                $object.'apiAction.name',
                $object.'apiAction.isRemote',
                $object.'apiAction.actionType',
                $object.'apiAction.description',
                $object.'apiActionInvokedAt',
                $object.'apiActionCompletedAt',
                $object.'apiAction.methodParams'
            )
        }
        $this.Asset = if ($this.Schema -eq 1) {
            [TMTaskAsset]::new($object.asset)
        } else {
            [TMTaskAsset]::new(
                $object.'assetEntity.Id',
                $object.'assetEntity.Name',
                $object.'assetEntity.Asset Class',
                $object.'assetEntity.assetType',
                @{id = $object.'assetEntity.Bundle.id'; name = $object.'assetEntity.Bundle.name' },
                $object.'assetEntity.tags'
            )
        }
        $this.AssignedTo = $this.Schema -eq 1 ? [TMReference]::new($object.assignedTo) : [TMReference]::new($object.'assignedTo.name', $object.'assignedTo.id')
        $this.CreatedBy = $this.Schema -eq 1 ? [TMReference]::new($object.createdBy) : [TMReference]::new($object.'createdBy.name', $object.'createdBy.id')
        $this.Category = $object.category
        $this.DateCreated = $object.dateCreated
        $this.HardAssigned = $object.hardAssigned
        $this.EstDurationMinutes = $object.estDurationMinutes
        $this.EstStart = $object.estStart
        $this.EstFinish = $object.estFinish
        $this.Slack = $object.slack
        $this.IsCriticalPath = $object.isCriticalPath
        $this.ActStart = $object.actStart
        $this.ActFinish = $object.actFinish
        $this.Team = $object.team ?? $object.role
        $this.IsPublished = $object.isPublished
        $this.PercentageComplete = $object.percentageComplete
        $this.Project = $this.Schema -eq 1 ? [TMReference]::new($object.project) : [TMReference]::new($object.'project.name', $object.'project.id')
        $this.IsActionInvocableLocally = $object.isActionInvocableLocally ?? $this.isActionInvokable($object, 'Local')
        $this.IsActionInvocableRemotely = $object.isActionInvocableRemotely ?? $this.isActionInvokable($object, 'Remote')
        $this.IsAutomatic = $object.isAutomatic ?? ($object.role -eq 'AUTO') -or ($object.'assignedTo.id' -eq 0)
        $this.Duration = $object.duration
        $this.SendNotification = $object.sendNotification
        $this.Priority = $object.priority
        $this.Event = $this.Schema -eq 1 ? [TMReference]::new($object.event) : [TMReference]::new($object.'moveEvent.name', $object.'moveEvent.id')
        $this.DueDate = $object.dueDate
        $this.InstructionsLink = $object.instructionsLink
        $this.Predecessors = $object.predecessors | ForEach-Object { [TMTaskDependency]::new($_) }
        $this.Successors = $object.successors | ForEach-Object { [TMTaskDependency]::new($_) }
        $this.Attribute = $object.attribute
        $this.AutoGenerated = $object.autoGenerated
        $this.DisplayOption = $object.displayOption
        $this.Recipe = $object.recipe
        $this.TaskSpec = $object.taskSpec
        $this.addPublicMembers()
    }

    #endregion Constructors

    #region Non-Static Methods

    <#
        Summary:
            Formats the Task into an object that can be used in the Update-TMTask web services request to TM
        Params:
            UpdateTaskDependencies - Boolean indicating if the Task's dependencies should be updated
        Outputs:
            None
    #>

    [PSCustomObject]GetWSUpdateObject([Boolean]$UpdateTaskDependencies) {
        $returnObject = [PSCustomObject]@{
            id                 = $this.Id
            comment            = $this.Comment ? $this.Comment : $this.Title
            project            = $this.Project.Id
            status             = $this.Status
            assignedTo         = $this.AssignedTo.id
            apiAction          = $this.Action.Id
            apiActionId        = "$($this.Action.Id)"
            category           = $this.Category
            assetEntity        = $this.Asset.Id
            hardAssigned       = $this.HardAssigned
            moveEvent          = $this.Event.id
            priority           = $this.Priority
            role               = $this.Team
            percentageComplete = $this.PercentageComplete
            sendNotification   = $this.SendNotification ? 1 : 0
            instructionsLink   = $this.InstructionsLink
            duration           = $this.Duration
            durationScale      = 'M'
            durationLocked     = 0
        }

        if ($UpdateTaskDependencies) {
            $returnObject | Add-Member -NotePropertyName 'taskDependency' -NotePropertyValue @()
            $returnObject | Add-Member -NotePropertyName 'taskSuccessor' -NotePropertyValue @()

            foreach ($Predecessor in $this.Predecessors) {
                if (-not $Predecessor.Id -or $Predecessor.Id -eq 0) {
                    $Predecessor.Id = -1
                }
                $returnObject.taskDependency += "$($Predecessor.Id)_$($Predecessor.TaskId)"
            }

            foreach ($Successor in $this.Successors) {
                if (-not $Successor.Id -or $Successor.Id -eq 0) {
                    $Successor.Id = -1
                }
                $returnObject.taskSuccessor += "$($Successor.Id)_$($Successor.TaskId)"
            }
        }

        return $returnObject
    }

    <#
        Summary:
            Formats the Task into an object that can be used in the Update-TMTask REST request to TM
        Params:
            Note - A note/comment to be added to the Task
            Status - A new Status to be set on the Task
            UpdateTaskDependencies - Boolean indicating if the Task's dependencies should be updated
        Outputs:
            None
    #>

    [PSCustomObject]GetApiUpdateObject([String]$Note, [String]$Status, [Boolean]$UpdateTaskDependencies) {
        $returnObject = [PSCustomObject]@{
            action             = @{ id = $this.Action.Id }
            asset              = @{ id = $this.Asset.Id }
            event              = @{ id = $this.Event.id }
            assignedTo         = $this.AssignedTo.id
            category           = $this.Category
            comment            = $this.comment ? $this.comment : $this.title
            title              = $this.comment ? $this.comment : $this.title
            hardAssigned       = $this.HardAssigned
            instructionsLink   = $this.InstructionsLink
            percentageComplete = $this.PercentageComplete
            priority           = $this.Priority
            role               = $this.Team
            status             = [String]::IsNullOrWhiteSpace($Status) ? $this.Status : $Status
            currentStatus      = $this.Status
            sendNotification   = $this.SendNotification ? 1 : 0
            project            = $this.Project.Id
            note               = $Note
        }

        if ($UpdateTaskDependencies) {
            $returnObject | Add-Member -NotePropertyName 'predecessors' -NotePropertyValue @()
            $returnObject | Add-Member -NotePropertyName 'successors' -NotePropertyValue @()

            foreach ($Predecessor in $this.Predecessors) {
                if (-not $Predecessor.Id -or $Predecessor.Id -eq 0) {
                    $Predecessor.Id = -1
                }
                $returnObject.predecessors += @{id = $Predecessor.Id; taskId = $Predecessor.TaskId }
            }

            foreach ($Successor in $this.Successors) {
                if (-not $Successor.Id -or $Successor.Id -eq 0) {
                    $Successor.Id = -1
                }
                $returnObject.successors += @{id = $Successor.Id; taskId = $Successor.TaskId }
            }
        }

        return $returnObject
    }

    #endregion Non-Static Methods

    #region Private Methods

    <#
        Summary:
            Copy of the logic from the TM source code to determine if the Action is invokable
        Params:
            object - The object returned from TM representing the Task
            location - The invocation location. Remote or Local
        Outputs:
            None
    #>

    hidden [System.Boolean]isActionInvokable([System.Object]$object, [System.String]$location) {
        $invokable = switch ($location) {
            'Remote' {
                (
                    ($object.'apiAction.id' -gt 0) -and
                    (-not $object.apiActionInvokedAt) -and
                    ($object.'apiAction.isRemote') -and
                    ($object.status -in 'Ready', 'Started')
                )
            }

            'Local' {
                (
                    ($object.'apiAction.id' -gt 0) -and
                    (-not $object.apiActionInvokedAt) -and
                    (-not $object.'apiAction.isRemote') -and
                    ($object.status -in 'Ready', 'Started')
                )
            }

            default { $false }
        }

        return $invokable
    }

    <#
        Summary:
            Adds properties that have a custom getter and or setter script
        Params:
            None
        Outputs:
            None
    #>

    hidden [void]addPublicMembers() {
        # Add a read-only property that calculates the Score of the Task
        $this.PSObject.Properties.Add(
            [PSScriptProperty]::new(
                'Score',
                { # get
                    $score = switch ($this.Status) {
                        'Hold' { 9000000 }
                        'Completed' { $this.StatusUpdated -ge (Get-Date).AddMinutes(-1) ? 8000000 : 3000000 }
                        'Started' { 7000000 }
                        'Ready' { 6000000 }
                        'Pending' { 5000000 }
                        'Planned' { 4000000 }
                        'Terminated' { 2000000 }
                    }

                    return (
                        $score - ($this.Status -in 'Hold', 'Completed', 'Started', 'Terminated' ? (
                            $null -ne $this.StatusUpdated ? (
                                [Math]::Round(([DateTimeOffSet]::UtcNow.ToUnixTimeSeconds() - ([DateTimeOffset]$this.StatusUpdated).ToUnixTimeSeconds()) / 60)
                            ) : 0
                        ) : 0) + (($this.Status -in 'Ready', 'Pending', 'Planned') -and ($null -ne $this.EstStart) ? (
                            [Math]::Floor(
                                [Math]::Sqrt(
                                    10000 * (
                                        (([DateTimeOffset]$this.EstStart).ToUnixTimeSeconds() + $this.Slack) -lt [DateTimeOffSet]::UtcNow.ToUnixTimeSeconds() ? (
                                            [Math]::Round(([DateTimeOffSet]::UtcNow.ToUnixTimeSeconds() - ([DateTimeOffset]$this.EstStart).ToUnixTimeSeconds() - $this.Slack) / 60)
                                        ) : (
                                            [Math]::Round((([DateTimeOffset]$this.EstStart).ToUnixTimeSeconds() + $this.Slack - [DateTimeOffSet]::UtcNow.ToUnixTimeSeconds()) / 60)
                                        )
                                    )
                                ) * ((([DateTimeOffset]$this.EstStart).ToUnixTimeSeconds() + $this.Slack) -lt [DateTimeOffSet]::UtcNow.ToUnixTimeSeconds() ? 1 : -1)
                            )
                        ) : 0) - (($this.Status -ne 'Hold') -and (($this.Team -eq 'AUTO') -or ($this.AssignedTo.Id -eq 5667)) ? 500000 : 0)
                    )
                }
            )
        )
    }

    #endregion Private Methods
}


class TMTaskAction {

    #region Non-Static Properties

    [System.Int64]$Id
    [System.String]$Name
    [System.Boolean]$IsRemote
    [TMReference]$ActionType
    [System.String]$Description
    [Nullable[System.DateTime]]$InvokedAt
    [System.Object]$CompletedAt
    [TMTaskActionMethodParam[]]$MethodParams

    #endregion Non-Static Properties

    #region Constructors

    TMTaskAction() {}

    TMTaskAction(
        [System.Int64]$id,
        [System.String]$name,
        [System.Boolean]$isRemote,
        [System.Object]$actionType,
        [System.String]$description,
        [Nullable[System.DateTime]]$invokedAt,
        [System.Object]$completedAt,
        [System.Object]$params
    ) {
        $this.Id = $id
        $this.Name = $name
        $this.IsRemote = $isRemote
        $this.ActionType = [TMReference]::new($actionType)
        $this.Description = $description
        $this.InvokedAt = $invokedAt
        $this.CompletedAt = $completedAt
        if ($params -is [System.String]) {
            $params = ($params | ConvertFrom-Json -Depth 5)
        }
        $this.MethodParams = $params | ForEach-Object { [TMTaskActionMethodParam]::new($_) }
    }

    TMTaskAction([Object]$object) {
        $this.Id = $object.id
        $this.Name = $object.name
        $this.IsRemote = $object.isRemote
        $this.ActionType = [TMReference]::new($object.actionType)
        $this.Description = $object.description
        $this.InvokedAt = $object.invokedAt
        $this.CompletedAt = $object.completedAt
        if ($object.methodParams -is [System.String]) {
            $object.methodParams = ($object.methodParams | ConvertFrom-Json -Depth 5)
        }
        $this.MethodParams = $object.methodParams | ForEach-Object { [TMTaskActionMethodParam]::new($_) }
    }

    #endregion Constructors

}


class TMTaskActionMethodParam {

    #region Non-Static Properties

    [System.String]$Type
    [System.String]$Value
    [System.String]$Context
    [System.Boolean]$Encoded
    [System.Boolean]$Invalid
    [System.Boolean]$Readonly
    [System.Boolean]$Required
    [System.String]$FieldName
    [System.String]$ParamName
    [System.String]$Description

    #endregion Non-Static Properties

    #region Constructors

    TMTaskActionMethodParam() {}

    TMTaskActionMethodParam([Object]$object) {
        $this.Type = $object.type
        $this.Value = $object.value
        $this.Context = $object.context
        $this.Encoded = $object.encoded
        $this.Invalid = $object.invalid
        $this.Readonly = $object.readonly
        $this.Required = $object.required
        $this.FieldName = $object.fieldName
        $this.ParamName = $object.paramName
        $this.Description = $object.description
    }

    #endregion Constructors

}


class TMTaskAsset {

    #region Non-Static Properties

    [System.Int64]$Id
    [System.String]$Name
    [System.String]$Class
    [System.String]$Type
    [TMReference]$Bundle
    [System.String[]]$Tags

    #endregion Non-Static Properties

    #region Constructors

    TMTaskAsset() {}

    TMTaskAsset(
        [System.Int64]$id,
        [System.String]$name,
        [System.String]$class,
        [System.String]$type,
        [System.Object]$bundle,
        [String[]]$tags
    ) {
        $this.Id = $id
        $this.Name = $name
        $this.Class = $id -eq 0 ? '' : $class
        $this.Type = $type
        $this.Bundle = [TMReference]::new($bundle)
        $this.Tags = $tags
    }

    TMTaskAsset([Object]$object) {
        $this.Id = $object.id
        $this.Name = $object.name
        $this.Class = $object.id -eq 0 ? '' : $object.class
        $this.Type = $object.type
        $this.Bundle = [TMReference]::new($object.bundle)
        $this.Tags = $object.tags
    }

    #endregion Constructors

}


class TMTaskDependency {

    #region Non-Static Properties

    [System.Int64]$Id
    [System.Int64]$TaskId
    [System.Int64]$Number
    [System.String]$Title

    #endregion Non-Static Properties

    #region Static Properties

    # List of field/property names that are used in the TMQL request to TM
    static [System.String[]]$TMQLFetchProperties = @(
        'id'
        'assetComment.id'
        'assetComment.taskNumber'
        'assetComment.comment'
        'predecessor.id'
        'predecessor.taskNumber'
        'predecessor.comment'
    )

    # String that is used in the TMQL query to fetch all of the necessary details about the Task
    static [String]$TMQLFetchString = "fetch '" + ([TMTaskDependency]::TMQLFetchProperties -join "', '") + "'"

    #endregion Static Properties

    #region Constructors

    TMTaskDependency() {}

    TMTaskDependency([System.Int64]$taskId) {
        $this.TaskId = $taskId
    }

    TMTaskDependency([System.Int64]$id, [System.Int64]$taskId, [System.Int64]$number, [System.String]$title) {
        $this.Id = $id
        $this.TaskId = $taskId
        $this.Number = $number
        $this.Title = $title
    }

    TMTaskDependency([Object]$object) {
        $this.Id = $object.id
        $this.TaskId = $object.taskId
        $this.Number = $object.number
        $this.Title = $object.title
    }

    TMTaskDependency([Object]$object, [Boolean]$successor) {
        $this.Id = $object.id
        $this.TaskId = $successor ? $object.'assetComment.id' : $object.'predecessor.id'
        $this.Number = $successor ? $object.'assetComment.taskNumber' : $object.'predecessor.taskNumber'
        $this.Title = $successor ? $object.'assetComment.comment' : $object.'predecessor.comment'
    }

    #endregion Constructors

}