Enhanced Ticketing Reports
Provide enhanced ticketing reports to a custom field or HTML Document.
Last updated
Provide enhanced ticketing reports to a custom field or HTML Document.
Last updated
This script will populate a custom field into each Organization that has logged tickets in the configured time range. It will provide data on Technician Time Tracked, Technicians Comments made and then break these up by Tickets, Technicians and Tags per Organization. It will also produce a company wide report showing the information per Organization into the Global Overview company.
There is also an option which will output the report into an HTML document in the location you configure in the script.
Make sure the Getting Started guide has been followed
Core RMM
NinjaOne Ticketing
Once per day / Once per month depending on time interval settings and your requirements
None
Ticketing Reports
ticketingReports
WYSIWYG
Technician: Read Only Automations: None
API: Read/Write
Organization
Displays the ticketing reports
By default the script will use the All Tickets Board please make sure you add the following columns to the board:
Ticket ID
Last Updated
Tracked Time
If you wish to use your own board with custom filters it is possible by updating the board ID, but please make sure it also has the above columns.
This script has several settings you can configure.
There are several different modes you can configure for the report to configure the date range you would like to generate reports for.
LASTDAYS - This mode lets you configure an additional $LastDays settings with the number of days you would like to configure in the past from the date the report is run.
LAST30 - This will generate the reports for the last 30 days.
LASTCALENDARMONTH - This will generate the reports for the previous calendar month.
LAST90 - This will generate the reports for the last 90 days.
LAST3CALENDARMONTHS - This will generate the reports for the previous 3 calendar months.
CUSTOM - This lets you set an additional $StartDate and $EndDate with the exact date range you would like to generate reports for.
There are two modes you can enable for output the first is $OutputToCustomField which will generate reports into the specified custom field. You can also configure the name of the Global Overview company if you wish to change it.
The second option is $OutputToFile which will generate an HTML file with all the reports to the specified directory.
try {
$NinjaOneInstance = Ninja-Property-Get ninjaoneInstance
$NinjaOneClientID = Ninja-Property-Get ninjaoneClientId
$NinjaOneClientSecret = Ninja-Property-Get ninjaoneClientSecret
###### Configure mode #######
# LASTDAYS - Will generate a report for the last x days
#$Mode = 'LASTDAYS'
#$Lastdays = 360
# LAST30 - Will generate reports for the last 30 days
$Mode = 'LAST30'
# LASTCALENDARMONTH - Will Generate a reports for the last calendar month
# $Mode = 'LASTCALENDARMONTH'
# LAST90DAYS - Will generate reports using the last 90 days.
# $Mode = 'LAST90DAYS'
# LAST3CALENDARMONTHS - Will generate reports for the last 3 calendar months.
# $Mode = 'LAST3CALENDARMONTHS'
# CUSTOM - Lets you specify a custom date range
# $Mode = 'CUSTOM'
# $StartDate = '2023-12-01'
# $EndDate = '2023-12-21'
####### Settings #######
# Set the board ID to fetch tickets from. By default 2 is the All Tickets Board.
# Please make sure the board you select has Ticket ID, Last Updated, and Tracked Time fields enabled.
$Script:BoardID = 2
# Set if you want reports to be generated to a custom field.
$OutputToCustomField = $True
$CustomFieldName = 'ticketingReports'
# Set the name of the organization you would like global reports populated to.
$GlobalReportOrg = 'Global Overview'
# Set the location for the ticketing report to be saved. By default it will be saved to the folder where
# the script is run, with the current date appended.
$OutputToFile = $False
$Date = Get-Date -Format "yyyy-MM-dd"
$Output_File = "C:\Temp\$($Date)_Ticketing_Report.html"
####### End Settings #######
$ScriptStart = Get-Date
# Disable progress bars to increase speed.
$ProgressPreference = 'SilentlyContinue'
function Get-NinjaTickets ($LastCursor, $PageSize) {
$Script:Ticketsearch = @"
{
"sortBy": [
{
"field": "lastUpdated",
"direction": "DESC"
}
],
"pageSize": $PageSize,
"lastCursorId": $LastCursor
}
"@
$AllTickets = Invoke-NinjaOneRequest -Path "ticketing/trigger/board/$Script:BoardID/run" -Method POST -Body $Script:Ticketsearch
Return $AllTickets
}
Function Get-NinjaOneTickets($FromUnix, $ToUnix) {
$Found = $False
$LastCursor = 0
[System.Collections.Generic.List[PSCustomObject]]$Script:TicketsList = do {
$FetchedTickets = Get-NinjaTickets -LastCursor $LastCursor -PageSize 1000
if (($FetchedTickets.data[-1].lastUpdated -lt $FromUnix) -or (($FetchedTickets.data | Measure-Object).count -eq 0)) {
$Found = $True
} else {
$LastCursor = $FetchedTickets.metadata.lastCursorId
}
$FetchedTickets.data
} while ($Found -eq $False)
$Script:TicketsListFiltered = $Script:TicketsList | Where-Object { $_.lastUpdated -ge $FromUnix -and $_.lastUpdated -le $ToUnix }
$TotalFilteredTickets = ($Script:TicketsListFiltered | Measure-Object).count
Write-Host "Total Tickets to Process: $TotalFilteredTickets"
$ProcessedTicketCount = 0
[System.Collections.Generic.List[PSCustomObject]]$Script:Tickets = foreach ($TicketItem in $Script:TicketsListFiltered) {
$ProcessedTicketCount++
$Ticket = Invoke-NinjaOneRequest -Path "ticketing/ticket/$($TicketItem.id)" -Method GET
if ($TicketItem.totalTimeTracked -gt 0) {
$TicketLogs = Invoke-NinjaOneRequest -Path "ticketing/ticket/$($TicketItem.id)/log-entry" -Method GET
} else {
$TicketLogs = $null
}
[PSCustomObject]@{
TicketID = '<a href="https://' + $NinjaOneInstance + '/#/ticketing/ticket/' + $Ticket.ID + '">' + $Ticket.ID + '</a>'
nodeID = $Ticket.nodeID
clientID = $Ticket.clientID
assignedAppUserID = $Ticket.assignedAppUserId
requestorUid = $Ticket.requesterUid
Subject = $Ticket.Subject
Status = $Ticket.Status.displayName
Priority = $Ticket.Priority
Severity = $Ticket.Severity
Form = $Ticket.ticketFormID
source = $Ticket.Source
tag = $Ticket.Tags
createTime = $Ticket.createTime
Ticket = $Ticket
Logs = $TicketLogs | Where-Object { $null -ne $_.appUserContactUid -and $_.type -in @('COMMENT', 'DESCRIPTION') -and $null -ne $_.timeTracked -and $_.createTime -ge $FromUnix -and $_.createTime -le $ToUnix } | Select-Object * , @{n = 'ticketID'; e = { $Ticket.ID } }
TotalTime = [math]::Round((($TicketLogs | Where-Object { $_.timeTracked -gt 0 -and $_.createTime -ge $FromUnix -and $_.createTime -le $ToUnix }).timeTracked | Measure-Object -sum).sum / 60 / 60, 2)
}
#Write-Progress -PercentComplete (($ProcessedTicketCount / $TotalFilteredTickets) * 100) -Status "Processing" -Activity "$ProcessedTicketCount / $TotalFilteredTickets Tickets Complete"
}
Write-Host "Processing Tickets Complete"
}
function Get-NinjaOneColours ($NumColours) {
# Step 1: Preset 10 colors
[System.Collections.Generic.List[String]]$presetColors = @(
'#007F8F',
'#7949CE',
'#5B1139',
'#3733C8',
'#567C19',
'#042552',
'#A13FA4',
'#7A6D81',
'#004858',
'#895C07',
'#0198AC',
'#9969ED',
'#862D5D',
'#5753F2',
'#729D2D',
'#00357E',
'#D552D9',
'#9A8CA1',
'#005E73',
'#A9710A',
'#A7CDD2',
'#C8BAE2',
'#D3B5C5',
'#B5B3E2',
'#BBCD9D',
'#ADBED6',
'#D5B1D6',
'#C6C2C8',
'#A2CAD3',
'#D7C8AD'
)
# If the number of requested colors is less than or equal to the preset colors count
if ($NumColours -le $presetColors.count) {
return $presetColors[0..($NumColours - 1)]
}
# Step 2: Generate random colors
$allColors = $presetColors
for ($i = 0; $i -lt ($NumColours - $presetColors.count); $i++) {
$R = Get-Random -Minimum 0 -Maximum 256
$G = Get-Random -Minimum 0 -Maximum 256
$B = Get-Random -Minimum 0 -Maximum 256
$randomColor = "#{0:X2}{1:X2}{2:X2}" -f $R, $G, $B
$allColors.add($randomColor)
}
# Step 3: Output the array of colors
return $allColors
}
function Get-ChartData {
param (
$Data,
$LabelColumn,
$AmountColumn
)
[System.Collections.Generic.List[PSCustomObject]]$ReturnData = @()
$Colours = Get-NinjaOneColours -NumColours ($Data | Measure-Object).count
$Index = 0
foreach ($Item in $Data) {
$ReturnData.add([PSCustomObject]@{
Label = $Item."$LabelColumn"
Amount = $Item."$AmountColumn"
Colour = $Colours[$Index]
})
$Index++
}
Return $ReturnData
}
Function Get-NinjaOneTechnicianReport {
[System.Collections.Generic.List[PSCustomObject]]$Script:TechTime = $( Foreach ($Technician in ($Users | Where-Object { $_.userType -eq 'TECHNICIAN' })) {
[PSCustomObject]@{
Name = "$($Technician.FirstName) $($Technician.LastName)"
'Total Time Tracked' = [math]::Round((($Script:Tickets.Logs | Where-Object { $_.appUserContactID -eq $Technician.id -and $_.appUserContactType -eq 'TECHNICIAN' }).timeTracked | Measure-Object -Sum).sum / 60 / 60, 2)
'Total Created Tickets Assigned' = ($Script:Tickets | Where-object { $_.assignedAppUserID -eq $Technician.id } | Measure-Object).count
'Total Comments Made' = ($Script:Tickets.Logs | where-object { $_.appUserContactId -eq $Technician.ID -and $_.appUserContactType -eq 'TECHNICIAN' } | measure-object).count
}
} ) | Where-Object { $_.'Total Time Tracked' -gt 0 -or $_.'Total Tickets Created' -gt 0 -or $_.'Total Comments by Technicians' }
$TechTimeData = Get-ChartData -Data $Script:TechTime -LabelColumn 'Name' -AmountColumn 'Total Time Tracked'
$TechCommentData = Get-ChartData -Data $Script:TechTime -LabelColumn 'Name' -AmountColumn 'Total Comments Made'
$TechTimeChart = Get-NinjaBarGraph -Data $TechTimeData -Title 'Technician Time Tracked' -NoKey -Icon 'far fa-clock'
$TechCommentChart = Get-NinjaBarGraph -Data $TechCommentData -Title 'Technician Total Comments' -NoKey 'far fa-comments'
$TechTimeHTML = $TechTime | Sort-Object 'Total Time Tracked' -Descending | ConvertTo-HTML -AS Table -Fragment
$TechnicianReportHTML = @"
<div>$TechTimeChart</div>
<div>$TechCommentChart</div>
<div>$TechTimeHTML</div>
"@
return $TechnicianReportHTML
}
Function Get-NinjaOneOrganizationReport {
# Time Per Organization
# Time By Techncian Report
[System.Collections.Generic.List[PSCustomObject]]$Script:OrganizationTime = Foreach ($Org in $Script:Tickets.clientID | Select-Object -unique) {
$MatchedOrg = $Script:Organizations | Where-Object { $_.id -eq $Org }
[PSCustomObject]@{
#Name = '<a href="#client-' + $Org + '">' + $MatchedOrg.Name + '</a>'
Name = '<a href="https://' + $NinjaOneInstance + '/#/customerDashboard/' + $Org + '/customFields">' + $MatchedOrg.Name + '</a>'
'Total Time Tracked' = [math]::Round((($Script:Tickets | Where-Object { $_.clientID -eq $Org }).logs.timetracked | Measure-Object -Sum).sum / 60 / 60 , 2)
'Total Tickets Created' = ($Script:Tickets | Where-object { $_.clientID -eq $Org } | Measure-Object).count
'Total Comments by Technicians' = (($Script:Tickets | Where-object { $_.clientID -eq $Org }).Logs | Where-Object { $_.appUserContactId -in $Users.ID -and $_.appUserContactType -eq 'TECHNICIAN' } | Measure-Object).count
}
$OrgTickets = ($Script:Tickets | Where-Object { $_.clientID -eq $Org }) | Sort-Object 'TotalTime' -Descending
# Org Tech Time
[System.Collections.Generic.List[PSCustomObject]]$Script:OrgTechTime = $(Foreach ($Technician in ($Users | Where-Object { $_.userType -eq 'TECHNICIAN' })) {
[PSCustomObject]@{
Name = "$($Technician.FirstName) $($Technician.LastName)"
'Total Time Tracked' = [math]::Round(((($Script:Tickets | Where-object { $_.clientID -eq $Org }).Logs | Where-Object { $_.appUserContactId -eq $Technician.ID -and $_.appUserContactType -eq 'TECHNICIAN' }).timeTracked | Measure-Object -Sum).sum / 60 / 60, 2)
'Total Created Tickets Assigned' = ($Script:Tickets | Where-object { $_.assignedAppUserID -eq $Technician.id -and $_.clientID -eq $Org } | Measure-Object).count
'Total Comments Made' = (($Script:Tickets | Where-Object { $_.clientID -eq $Org }).Logs | where-object { $_.appUserContactId -eq $Technician.ID -and $_.appUserContactType -eq 'TECHNICIAN' } | measure-object).count
}
} ) | Where-Object { $_.'Total Time Tracked' -gt 0 -or $_.'Total Tickets Created' -gt 0 -or $_.'Total Comments by Technicians' }
# Time Per Tag
[System.Collections.Generic.List[PSCustomObject]]$Script:OrgTagsTime = $(Foreach ($Tag in $Script:Tickets.Tag | Select-Object -unique) {
[PSCustomObject]@{
Name = $Tag
'Total Time Tracked' = [math]::Round((($Script:Tickets | Where-Object { $Tag -in $_.tag -and $_.clientID -eq $Org }).logs.timetracked | Measure-Object -Sum).sum / 60 / 60 , 2)
'Total Tickets Created' = ($Script:Tickets | Where-object { $Tag -in $_.tag -and $_.clientID -eq $Org } | Measure-Object).count
'Total Comments by Technicians' = (($Script:Tickets | Where-object { $Tag -in $_.tag -and $_.clientID -eq $Org }).Logs | Where-Object { $_.appUserContactId -in $Users.ID -and $_.appUserContactType -eq 'TECHNICIAN' } | Measure-Object).count
}
} ) | Where-Object { $_.'Total Time Tracked' -gt 0 -or $_.'Total Tickets Created' -gt 0 -or $_.'Total Comments by Technicians' }
$TopTicketsData = Get-ChartData -Data ($OrgTickets | Select-Object -First 15) -AmountColumn 'TotalTime' -LabelColumn 'TicketID'
$TopTechTimeData = Get-ChartData -Data ($OrgTechTime | Sort-Object Name) -AmountColumn 'Total Time Tracked' -LabelColumn 'Name'
$TopTechCommentsData = Get-ChartData -Data ($OrgTechTime | Sort-Object Name) -AmountColumn 'Total Comments Made' -LabelColumn 'Name'
$TopTagTimeData = Get-ChartData -Data ($OrgTagsTime | Sort-Object Name) -AmountColumn 'Total Time Tracked' -LabelColumn 'Name'
$TopTagCommentData = Get-ChartData -Data ($OrgTagsTime | Sort-Object Name) -AmountColumn 'Total Comments by Technicians' -LabelColumn 'Name'
$TopTicketsChart = Get-NinjaInLineBarGraph -Data $TopTicketsData -Title 'Top 15 Tickets by Time' -KeyInLine -Icon 'far fa-clock'
$TopTechTimeChart = Get-NinjaBarGraph -Data $TopTechTimeData -Title 'Technician Time Tracked' -NoKey -Icon 'far fa-clock'
$TopTechCommentsChart = Get-NinjaBarGraph -Data $TopTechCommentsData -Title 'Technician Comments Made' -NoKey -Icon 'far fa-comments'
$TopTagTimeChart = Get-NinjaBarGraph -Data $TopTagTimeData -Title 'Tags Time Tracked' -NoKey -Icon 'far fa-clock'
$TopTagCommentChart = Get-NinjaBarGraph -Data $TopTagCommentData -Title 'Tags Commments Made' -NoKey -Icon 'far fa-comments'
$TopTicketsHTML = Get-NinjaOneCard -Title 'Top Tickets Summary' -Icon 'fas fa-ticket' -Body "<div>$TopTicketsChart</div><div>$($OrgTickets | Select-Object TicketID, Subject, Status, TotalTime -First 15 | ConvertTo-Html -As Table -Fragment)</div>"
$TechHTML = Get-NinjaOneCard -Title 'Technican Summary' -Icon 'fas fa-users' -Body "<div>$TopTechTimeChart</div><div>$TopTechCommentsChart</div><div>$($OrgTechTime | Sort-Object 'Total Time Tracked' -Descending | select-object -First 10 | ConvertTo-Html -As Table -Fragment)</div>"
$TagsHTML = Get-NinjaOneCard -Title 'Tag Summary' -Icon 'fas fa-tags' -Body "<div>$TopTagTimeChart</div><div>$TopTagCommentChart</div><div>$($OrgTagsTime | Sort-Object 'Total Time Tracked' -Descending | Select-object -First 10 | ConvertTo-Html -As Table -Fragment)</div>"
[System.Collections.Generic.List[PSCustomObject]]$WidgetData = @()
$WidgetData.add([PSCustomObject]@{
Value = ($OrgTickets | Measure-Object).count
Description = 'Total Tickets'
Colour = '#337AB7'
})
$WidgetData.add([PSCustomObject]@{
Value = [math]::Round((($OrgTickets).logs.timetracked | Measure-Object -Sum).sum / 60 / 60 , 2)
Description = 'Total Tech Hours'
Colour = '#337AB7'
})
$WidgetData.add([PSCustomObject]@{
Value = (($OrgTickets).Logs | where-object { $_.appUserContactType -eq 'TECHNICIAN' } | measure-object).count
Description = 'Total Tech Comments'
Colour = '#337AB7'
})
$OrgTicketSummaryWidgetsHTML = (Get-NinjaOneWidgetCard -Data $WidgetData -SmallCols 2 -MedCols 2 -LargeCols 3 -XLCols 3 -NoCard)
$OrgSummaryHTML = Get-NinjaOneCard -Title "Ticketing Report - $Start to $End" -Icon 'fas fa-ticket' -Body $OrgTicketSummaryWidgetsHTML
$OrgHTML = @"
<div class="row g-3">
<div class="col-12 d-flex">$OrgSummaryHTML</div>
<div class="col-12 d-flex">$TopTicketsHTML</div>
<div class="col-12 d-flex">$TechHTML</div>
<div class="col-12 d-flex">$TagsHTML</div>
</div>
"@
if ($OutputToFile -eq $True) {
$OrgCard = Get-NinjaOneCard -Title "$($MatchedOrg.Name) Report" -Icon 'fas fa-building' -Body $OrgHTML
$Script:OrganizationHTML.add($OrgCard)
}
if ($OutputToCustomField -eq $True) {
$UpdateBody = @{
"$CustomFieldName" = @{ 'html' = [System.Web.HttpUtility]::HtmlDecode($OrgHTML) }
}
$Null = Invoke-NinjaOneRequest -Method PATCH -Path "organization/$($Org)/custom-fields" -InputObject $UpdateBody
}
}
$OrgTimeTrackedData = Get-ChartData -Data ($Script:OrganizationTime | Sort-Object Name) -AmountColumn 'Total Time Tracked' -LabelColumn 'Name'
$OrgCommentsTrackedData = Get-ChartData -Data ($Script:OrganizationTime | Sort-Object Name) -AmountColumn 'Total Comments by Technicians' -LabelColumn 'Name'
$OrgTimeTrackedChart = Get-NinjaInLineBarGraph -Data $OrgTimeTrackedData -Title 'Organizations by Time Tracked' -KeyInLine -Icon 'far fa-clock'
$OrgCommentsTrackedChart = Get-NinjaInLineBarGraph -Data $OrgCommentsTrackedData -Title 'Organizations by Comments' -KeyInLine -Icon 'far fa-comments'
$GlobalOrgTimeHTML = $OrganizationTime | Sort-Object 'Total Time Tracked' -Descending | ConvertTo-HTML -AS Table -Fragment
$GlobalOrgHTML = @"
<div>$OrgTimeTrackedChart</div>
<div>$OrgCommentsTrackedChart</div>
<div>$GlobalOrgTimeHTML</div>
"@
return $GlobalOrgHTML
}
Function Get-NinjaOneTagReport {
# Time Per Tag
[System.Collections.Generic.List[PSCustomObject]]$Script:TagsTime = $(Foreach ($Tag in $Script:Tickets.Tag | Select-Object -unique) {
[PSCustomObject]@{
Name = $Tag
'Total Time Tracked' = [math]::Round((($Script:Tickets | Where-Object { $Tag -in $_.tag }).logs.timetracked | Measure-Object -Sum).sum / 60 / 60 , 2)
'Total Tickets Created' = ($Script:Tickets | Where-object { $Tag -in $_.tag -and $_.createTime -ge $TargetUnix } | Measure-Object).count
'Total Comments by Technicians' = (($Script:Tickets | Where-object { $Tag -in $_.tag }).Logs | Where-Object { $_.appUserContactId -in $Users.ID -and $_.appUserContactType -eq 'TECHNICIAN' } | Measure-Object).count
}
}) | Where-Object { $_.'Total Time Tracked' -gt 0 -or $_.'Total Tickets Created' -gt 0 -or $_.'Total Comments by Technicians' }
$TagTimeTrackedData = Get-ChartData -Data ($Script:TagsTime | Sort-Object Name) -AmountColumn 'Total Time Tracked' -LabelColumn 'Name'
$TagCommentsTrackedData = Get-ChartData -Data ($Script:TagsTime | Sort-Object Name) -AmountColumn 'Total Comments by Technicians' -LabelColumn 'Name'
$TagTimeTrackedChart = Get-NinjaBarGraph -Data $TagTimeTrackedData -Title 'Tags by Time Tracked' -NoKey -Icon 'far fa-clock'
$TagCommentsTrackedChart = Get-NinjaBarGraph -Data $TagCommentsTrackedData -Title 'Tags by Comments' -NoKey -Icon 'far fa-comments'
$TagsTimeHTML = $TagsTime | Sort-Object 'Total Time Tracked' -Descending | ConvertTo-HTML -AS Table -Fragment
$TagsReportHTML = @"
<div>$TagTimeTrackedChart</div>
<div>$TagCommentsTrackedChart</div>
<div>$TagsTimeHTML</div>
"@
return $TagsReportHTML
}
$moduleName = "NinjaOneDocs"
if (-not (Get-Module -ListAvailable -Name $moduleName)) {
Install-Module -Name $moduleName -Force -AllowClobber
} else {
$latestVersion = (Find-Module -Name $moduleName).Version
$installedVersion = (Get-Module -ListAvailable -Name $moduleName).Version | Sort-Object -Descending | Select-Object -First 1
if ($installedVersion -ne $latestVersion) {
Update-Module -Name $moduleName -Force
}
}
Import-Module $moduleName
Switch ($Mode) {
'LASTDAYS' {
$StartUnix = Get-NinjaOneTime -Seconds -Date ((Get-Date).AddDays(-$Lastdays))
$EndUnix = Get-NinjaOneTime -Seconds -Date (Get-Date)
}
'LAST30' {
$StartUnix = Get-NinjaOneTime -Seconds -Date ((Get-Date).AddDays(-30))
$EndUnix = Get-NinjaOneTime -Seconds -Date (Get-Date)
}
'LASTCALENDARMONTH' {
$StartUnix = Get-NinjaOneTime -Seconds -Date ((Get-Date).AddMonths(-1).Date.AddDays( - (Get-Date).Day + 1))
$EndUnix = Get-NinjaOneTime -Seconds -Date ((Get-Date).Date.AddDays( - (Get-Date).Day))
}
'LAST90DAYS' {
$StartUnix = Get-NinjaOneTime -Seconds -Date ((Get-Date).AddDays(-90))
$EndUnix = Get-NinjaOneTime -Seconds -Date (Get-Date)
}
'LAST3CALENDARMONTHS' {
$StartUnix = Get-NinjaOneTime -Seconds -Date ((Get-Date).AddMonths(-3).Date.AddDays( - (Get-Date).Day + 1))
$EndUnix = Get-NinjaOneTime -Seconds -Date ((Get-Date).Date.AddDays( - (Get-Date).Day))
}
'CUSTOM' {
$StartUnix = Get-NinjaOneTime -Seconds -Date ((Get-Date($StartDate)))
$EndUnix = Get-NinjaOneTime -Seconds -Date (Get-Date($EndDate))
}
default {
Throw 'Unknown Mode'
}
}
$Start = (Get-TimeFromNinjaOne -Date $StartUnix -Seconds).ToString("yyyy-MM-dd")
$End = (Get-TimeFromNinjaOne -Date $EndUnix -Seconds).ToString("yyyy-MM-dd")
Connect-NinjaOne -NinjaOneInstance $NinjaOneInstance -NinjaOneClientID $NinjaOneClientID -NinjaOneClientSecret $NinjaOneClientSecret
$Users = Invoke-NinjaOneRequest -Path 'users' -Method GET -Paginate
$Script:Organizations = Invoke-NinjaOneRequest -Path 'organizations' -Method GET -Paginate
Get-NinjaOneTickets -FromUnix $StartUnix -ToUnix $EndUnix
$TechReportHTML = Get-NinjaOneTechnicianReport
$TechnicianHTML = Get-NinjaOneCard -Title 'Technician Summary' -Icon 'fas fa-users' -Body $TechReportHTML
[System.Collections.Generic.List[string]]$Script:OrganizationHTML = @()
$GlobalOrgReportHTML = Get-NinjaOneOrganizationReport
$GlobalOrgHTML = Get-NinjaOneCard -Title 'Organization Summary' -Icon 'fas fa-building' -Body $GlobalOrgReportHTML
$TagsReportHTML = Get-NinjaOneTagReport
$TagsHTML = Get-NinjaOneCard -Title 'Tags Summary' -Icon 'fas fa-tags' -Body $TagsReportHTML
$GlobalOrg = $Script:Organizations | where-object { $_.name -eq $GlobalReportOrg }
[System.Collections.Generic.List[PSCustomObject]]$WidgetData = @()
$WidgetData.add([PSCustomObject]@{
Value = ($Tickets | Measure-Object).count
Description = 'Total Tickets'
Colour = '#337AB7'
})
$WidgetData.add([PSCustomObject]@{
Value = [math]::Round((($Script:Tickets).logs.timetracked | Measure-Object -Sum).sum / 60 / 60 , 2)
Description = 'Total Tech Hours'
Colour = '#337AB7'
})
$WidgetData.add([PSCustomObject]@{
Value = (($Script:Tickets).Logs | where-object { $_.appUserContactType -eq 'TECHNICIAN' } | measure-object).count
Description = 'Total Tech Comments'
Colour = '#337AB7'
})
$TicketSummaryWidgetsHTML = (Get-NinjaOneWidgetCard -Data $WidgetData -SmallCols 2 -MedCols 2 -LargeCols 3 -XLCols 3 -NoCard)
$SummaryHTML = Get-NinjaOneCard -Title "Ticketing Report - $Start to $End" -Icon 'fas fa-ticket' -Body $TicketSummaryWidgetsHTML
$OutputHTML = [System.Web.HttpUtility]::HtmlDecode('<div class="row g-3"><div class="col-12 d-flex">' + $SummaryHTML + '</div><div class="col-12 d-flex">' + $TechnicianHTML + '</div><div class="col-12 d-flex">' + $GlobalOrgHTML + '</div><div class="col-12 d-flex">' + $TagsHTML + '</div></div>')
if ($OutputToCustomField -eq $True) {
if (($GlobalOrg | Measure-Object).count -eq 1) {
$UpdateBody = @{
"$CustomFieldName" = @{ 'html' = $OutputHTML }
}
$Null = Invoke-NinjaOneRequest -Method PATCH -Path "organization/$($GlobalOrg.id)/custom-fields" -InputObject $UpdateBody
} else {
Write-Error "Could not match $($GlobalReportOrg) to a single organization"
}
}
$FileHtml = @"
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.1/css/bootstrap-grid.min.css" integrity="sha512-2cWcZ9cbPMZFm2inlFOhwsBVbNMmNxKBtVXqL8OY9tXCZahhnIfXMxPCzpKqiHF2I2mOiNHNXEDUDglwd+4uYw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<title>Ticketing Report</title>
<style>
body {
margin: 0;
background-color: transparent;
word-break: break-word;
font-family: inter, sans-serif;
}
*,
::after,
::before {
box-sizing: border-box;
}
img {
max-width: 100%;
height: auto;
}
h1,
h2 {
margin-top: 10px;
}
blockquote {
padding: 10px 20px;
margin: 0 0 10px;
border-left: 5px solid #EEEEEE;
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: break-word;
}
ol,
ul {
list-style-type: revert;
}
p,
ul,
li {
color: #151617;
font-size: 14px;
font-weight: 400;
word-wrap: break-word;
}
ul.unstyled {
list-style-type: none;
padding: 0;
margin: 0;
}
h1 {
color: #151617;
font-size: 24px;
font-weight: 600;
word-wrap: break-word;
}
h2 {
color: #151617;
font-size: 20px;
font-weight: 500;
word-wrap: break-word;
}
h3 {
color: #151617;
font-size: 16px;
font-weight: 500;
word-wrap: break-word;
}
h4 {
color: #5B666C;
font-size: 14px;
font-weight: 400;
word-wrap: break-word;
}
h5 {
color: #5B666C;
font-size: 12px;
font-weight: 400;
word-wrap: break-word;
}
strong {
color: #151617;
font-size: 14px;
font-weight: 600;
word-wrap: break-word;
}
a {
color: #337AB7;
text-decoration: none;
}
a:hover {
color: #23527c;
}
a:active {
color: #23527c;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
text-align: left;
padding: 8px;
border-bottom: 0.5px solid #CAD0D6;
}
th {
color: #151617;
font-size: 14px;
font-weight: 500;
line-height: 21px;
word-wrap: break-word;
}
td {
color: #363B3E;
font-size: 14px;
font-weight: 400;
line-height: 21px;
word-wrap: break-word;
}
tbody tr:hover {
background-color: #EFF1F3;
}
tr.danger {
padding: 7px 8px;
border-left: 6px #D53948 solid;
}
tr.warning {
padding: 7px 8px;
border-left: 6px #FAC905 solid;
}
tr.success {
padding: 7px 8px;
border-left: 6px #007644 solid;
}
tr.unknown {
padding: 7px 8px;
border-left: 6px #949597 solid;
}
tr.other {
padding: 7px 8px;
border-left: 6px #337AB7 solid;
}
.field-container {
justify-content: center;
align-items: center;
max-width: 100%;
gap: 10px;
overflow: auto;
}
.card {
padding: 24px;
background: #FFFFFF;
border-radius: 4px;
border: 0.5px #CAD0D6 solid;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 8px;
display: inline-flex;
}
.card-title {
color: #151617;
font-size: 16px;
font-weight: 500;
line-height: 24px;
word-wrap: break-word;
}
.card-title-box {
align-self: stretch;
justify-content: space-between;
align-items: center;
gap: 149px;
display: inline-flex;
}
.card-link-box {
border-radius: 4px;
justify-content: center;
align-items: center;
gap: 8px;
display: flex;
}
.card-link {
color: #337AB7;
font-size: 14px;
font-weight: 500;
line-height: 14px;
word-wrap: break-word;
}
.card-body {
color: #151617;
font-size: 14px;
font-weight: 400;
line-height: 24px;
word-wrap: break-word;
width: 100%;
}
.stat-card {
width: 100%;
padding: 24px;
border-radius: 4px;
border: 0.5px #CAD0D6 solid;
flex-direction: column;
gap: 8px;
display: inline-flex;
justify-content: center;
align-items: center;
margin: 0px;
padding-top: 36px;
padding-bottom: 36px;
text-align: Center;
margin-bottom: 24px;
height: 148px;
}
.stat-value {
height: 50%;
font-size: 40px;
color: #cccccc;
margin-bottom: 10px;
}
.stat-desc {
height: 50%;
white-space: nowrap;
}
.btn {
padding: 12px;
background: #337AB7;
border-radius: 4px;
justify-content: center;
align-items: center;
display: inline-flex;
color: #FFFFFF;
font-size: 14px;
font-weight: 500;
line-height: 14px;
word-wrap: break-word;
text-decoration: none;
border: 1px solid transparent;
transition: background-color 0.3s ease, border-color 0.3s ease;
outline: none;
}
.btn:hover {
background: #115D9F;
}
.btn:focus {
border: 1px solid #337AB7;
}
.btn.secondary {
background: #FFFFFF;
color: #337AB7;
padding: 12.5px;
border: 0.5px solid #CAD0D6;
}
.btn.secondary:hover {
background: #EFF1F3;
}
.btn.secondary:focus {
border-color: 1px solid #337AB7;
}
.btn.danger {
background: #C6313A;
color: #FFFFFF;
border: 0.5px solid transparent;
}
.btn.danger:hover {
background: #A71C25;
}
.btn.danger:focus {
border-color: 1px solid #337AB7;
}
.info-card {
width: 100%;
padding: 12px;
background: #EBF2F8;
border-radius: 4px;
justify-content: flex-start;
align-items: flex-start;
gap: 8px;
display: inline-flex;
margin-bottom: 10px;
}
.info-icon {
text-align: center;
color: #337AB7;
font-size: 14px;
font-weight: 900;
word-wrap: break-word;
}
.info-text {
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 8px;
display: inline-flex;
}
.info-title {
color: #151617;
font-size: 14px;
font-weight: 600;
word-wrap: break-word;
}
.info-description {
color: #151617;
font-size: 14px;
font-weight: 400;
word-wrap: break-word;
}
.info-card.error {
background-color: #FBEBED;
}
.info-card.error .info-icon {
color: #C6313A;
}
.info-card.warning {
background-color: #FBEBED;
}
.info-card.warning .info-icon {
color: #FAC905;
}
.info-card.success {
background-color: #E6F2E5;
}
.info-card.success .info-icon {
color: #007644;
}
.tag {
padding: 2px 8px;
background: #018200;
border-radius: 2px;
justify-content: center;
align-items: center;
gap: 8px;
display: inline-flex;
color: #FFFFFF;
font-size: 14px;
font-weight: 400;
word-wrap: break-word;
}
.tag.disabled {
background: #E8E8EA;
color: #6E6D7A;
}
.tag.expired {
background: #E8E8EA;
color: #211F33;
}
.close {
position: absolute;
top: 24px;
right: 27px;
color: #211F33;
text-decoration: none;
font-size: 24px;
font-weight: 300;
}
.nowrap {
white-space: nowrap;
}
.linechart {
width: 100%;
height: 50px;
display: flex;
}
.chart-key {
display: inline-block;
width: 20px;
height: 20px;
margin-right: 10px;
}
</style></head>
<body>
$OutputHTML
$($Script:OrganizationHTML)
</body>
</html>
"@
if ($OutputToFile -eq $True) {
[System.Web.HttpUtility]::HtmlDecode($FileHTML) | Out-File $Output_File
}
Write-Output "$(Get-Date): Complete Total Runtime: $((New-TimeSpan -Start $ScriptStart -End (Get-Date)).TotalSeconds) seconds"
} catch {
Write-Output "Failed to Generate Ticketing Reports. Linenumber: $($_.InvocationInfo.ScriptLineNumber) Error: $($_.Exception.message)"
exit 1
}