Functions/Public/Get-TogglUtilizationReport.ps1

<#
        .Synopsis
        Calculate a utilization report for all pay periods in the specified date range.
        .Description
        This cmdlet uses Toggl's tag functionality to classify time entries.
        The following tags are used:
            Billable
            Holiday
            Non-Billable
            PTO
            Training
            Utilized
        .Example
        Get-TogglUtilizationReport
        Generate a utilization report for the current pay period
        .Example
        Get-TogglUtilizationReport -From (Get-Date).AddMonths(-12) -ExcludeCurrentPeriod | Export-Csv -NoTypeInformation 'c:\temp\TogglUtilizationReport.csv'
        Generate a utilization report for the past year, excluding the current pay period, and outputs to a CSV file
        .Example
        Get-TogglUtilizationReport -From (Get-Date).AddMonths(-12) -ExcludeCurrentPeriod | Out-GridView
        Generate a utilization report for the past year, excluding the current pay period, and outputs to grid view
        .Link
        https://github.com/Dapacruz/Toggl.Functions
#>

function Get-TogglUtilizationReport {
    [CmdletBinding()]
    Param (
        [Parameter(Position=0)]
        [datetime]$From = $(if ((Get-Date).Hour -lt 17) { (Get-Date).AddDays(-1) } else { Get-Date }),
        [Parameter(Position=1)]
        [datetime]$To = $(if ((Get-Date).Hour -lt 17) { (Get-Date).AddDays(-1) } else { Get-Date }),
        [switch]$ExcludeCurrentPeriod
    )

    $work_days = 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'
    $report = @()
    
    # Normalize $From
    if ($From.Day -gt 1 -and $From.Day -le 15) {
        $From = Get-Date $From -Day 1
    } elseif ($From.Day -gt 16) {
        $From = Get-Date $From -Day 16
    }
    
    if ($ExcludeCurrentPeriod) {
        if ($To.Date -ge (Get-Date -Day 16).Date) {
            $To = Get-Date -Day 15
        } elseif ($To.Date -ge (Get-Date -Day 1).Date) {
            $To = (Get-Date -Day 1).AddDays(-1)
        }
    }
    
    while ($From -le $To) {
        $month_first_day = Get-Date $From -Day 1
        $month_last_day = $month_first_day.AddMonths(1).AddDays(-1)
        $normal_hours = 0
        $total_hours = 0
        $billable_hours = 0
        $utilized_hours = 0
        $training_hours = 0
        $pto_hours = 0
        $holiday_hours = 0
        $non_billable_hours = 0
        $overtime_hours = 0
        
        # Determine period start and end
        $period_start = $From.Day
        switch ($period_start) {
            1 {
                if ($From.AddDays(14) -gt $To) {
                    $period_end = $To.Day
                } else {
                    $period_end = $From.AddDays(14).Day
                }
            }
            16 {
                if ($month_last_day -gt $To) {
                    $period_end = $To.Day
                } else {
                    $period_end = $month_last_day.Day
                }
            }
            default {throw "Current Date out of range: $From"}
        }
    
        # Calculate normal working hours for the specified pay period
        for ($x=$period_start; $x -le $period_end; $x++) {
            $day = '{0:yyyy-MM}-{1}' -f $From, $x
            $day_of_week = (Get-Date $day).DayOfWeek
            
            if ($day_of_week -in $work_days) {
                $normal_hours += 8
            }
        }
        
        [system.array]$detailed_report = Get-TogglDetailedReport -From ('{0:yyyy-MM}-{1}' -f $From, $period_start) -To ('{0:yyyy-MM}-{1}' -f $From, $period_end)

        $total_hours = ($detailed_report | Measure-Object -Property 'Duration(Hrs)' -Sum).Sum
        if ($total_hours -eq $null) {
            $total_hours = 0
        }
        $billable_hours = ($detailed_report.Where{$_.WorkType -eq 'Billable'} | Measure-Object -Property 'Duration(Hrs)' -Sum).Sum
        if ($billable_hours -eq $null) {
            $billable_hours = 0
        }
        $utilized_hours = ($detailed_report.Where{$_.WorkType -eq 'Utilized'} | Measure-Object -Property 'Duration(Hrs)' -Sum).Sum
        if ($utilized_hours -eq $null) {
            $utilized_hours = 0
        }
        $training_hours = ($detailed_report.Where{$_.WorkType -eq 'Training'} | Measure-Object -Property 'Duration(Hrs)' -Sum).Sum
        if ($training_hours -eq $null) {
            $training_hours = 0
        }
        $pto_hours = ($detailed_report.Where{$_.WorkType -eq 'PTO'} | Measure-Object -Property 'Duration(Hrs)' -Sum).Sum
        if ($pto_hours -eq $null) {
            $pto_hours = 0
        }
        $holiday_hours = ($detailed_report.Where{$_.WorkType -eq 'Holiday'} | Measure-Object -Property 'Duration(Hrs)' -Sum).Sum
        if ($holiday_hours -eq $null) {
            $holiday_hours = 0
        }
        $non_billable_hours = ($detailed_report.Where{$_.WorkType -eq 'Non-Billable'} | Measure-Object -Property 'Duration(Hrs)' -Sum).Sum
        if ($non_billable_hours -eq $null) {
            $non_billable_hours = 0
        }
        $overtime_hours = $total_hours-$normal_hours
        if ($overtime_hours -lt 0) {
            $overtime_hours = 0
        }
        if ($normal_hours -gt 0) {
            $percent_billable = ($billable_hours/($normal_hours-$pto_hours-$holiday_hours-$training_hours))*100
            $percent_utilized = (($billable_hours+$utilized_hours)/($normal_hours-$pto_hours-$holiday_hours-$training_hours))*100
        } else {
            $percent_billable = 0
            $percent_utilized = 0
        }
        # Output summary totals
        $obj = New-Object -TypeName PSObject
        
        # Insert custom TypeName (defined in $PSScriptRoot\format.ps1xml) to control default display
        $obj.PSTypeNames.Insert(0,'Toggl.Report.Utilization')
        
        Add-Member -InputObject $obj -MemberType NoteProperty -Name PeriodStart -Value ('{0:yyyy-MM-dd}' -f (Get-Date $From -Day $period_start))
        Add-Member -InputObject $obj -MemberType NoteProperty -Name 'Total(hrs)' -Value ('{0:N2}' -f $total_hours)
        Add-Member -InputObject $obj -MemberType NoteProperty -Name 'Normal(hrs)' -Value ('{0:N2}' -f $normal_hours)
        Add-Member -InputObject $obj -MemberType NoteProperty -Name 'Overtime(hrs)' -Value ('{0:N2}' -f $overtime_hours)
        Add-Member -InputObject $obj -MemberType NoteProperty -Name 'Training(hrs)' -Value ('{0:N2}' -f $training_hours)
        Add-Member -InputObject $obj -MemberType NoteProperty -Name 'PTO(hrs)' -Value ('{0:N2}' -f $pto_hours)
        Add-Member -InputObject $obj -MemberType NoteProperty -Name 'Holiday(hrs)' -Value ('{0:N2}' -f $holiday_hours)
        Add-Member -InputObject $obj -MemberType NoteProperty -Name 'Non-Billable(hrs)' -Value ('{0:N2}' -f $non_billable_hours)
        Add-Member -InputObject $obj -MemberType NoteProperty -Name 'Utilized(hrs)' -Value ('{0:N2}' -f $utilized_hours)
        Add-Member -InputObject $obj -MemberType NoteProperty -Name 'Billable(hrs)' -Value ('{0:N2}' -f $billable_hours)
        Add-Member -InputObject $obj -MemberType NoteProperty -Name 'Billable(%)' -Value ('{0:N0}' -f $percent_billable)
        Add-Member -InputObject $obj -MemberType NoteProperty -Name 'Utilized(%)' -Value ('{0:N0}' -f $percent_utilized)

        $report += $obj
        
        switch ($From.Day) {
            1 {$From = $From.AddDays(15)}
            16 {$From = Get-Date $From.AddMonths(1) -Day 1}
            default {throw "Current date out of range: $From"}
        }
    }
    
    Write-Output $report
}