Functions/Public/Reports.ps1

# Report management and widget functions

Function Get-NectarReport {
    <#
        .SYNOPSIS
        Gets basic information about all available reports
         
        .DESCRIPTION
        Gets basic information about all available reports
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarReport
        Returns information about all reports
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(Mandatory=$False)]
        [ValidateSet('name', 'status', 'createdDate', 'lastRun', 'scheduledRun', IgnoreCase=$True)]
        [string]$OrderByField = 'name',
        [Parameter(Mandatory=$False)]
        [ValidateSet('asc', 'desc', IgnoreCase=$True)]
        [string]$OrderDirection = 'asc',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }

            $Params = @{
                'pageNumber'        = 1
                'pageSize'             = $PageSize
                'orderByField'        = $OrderByField
                'orderDirection'    = $OrderDirection
            }

            If ($SearchQuery) { $Params.Add('searchQuery',$SearchQuery) }

            If ($ResultSize) {
                $Params.pageSize = $ResultSize
            } 

            $URI = "https://$Global:NectarCloud/rapi/client/report/config/list?tenant=$TenantName"
            Write-Verbose $URI
            Write-Verbose "** Body Parameters **"
            ForEach($k in $Params.Keys){Write-Verbose "$k`: $($Params[$k])"}

            
            $JSON = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -Body $Params

            $TotalPages = $JSON.totalPages
            
            $JSON.elements
            
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $Params.pageNumber = $PageNum
                    $JSON = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader -Body $Params
                    If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "Error getting report info. $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function New-NectarReport {
    <#
        .SYNOPSIS
        Creates a new report
         
        .DESCRIPTION
        Creates a new report using previously defined widgets from New-NectarReportWidgetConfig
 
        .PARAMETER Name
        The name of the report
 
        .PARAMETER Description
        A description to go with the report
 
        .PARAMETER Type
        The report type (or scope). Select from User or Tenant. User reports are visible only to the creator.
        Tenant reports are visible to all users.
         
        .PARAMETER WidgetList
        An arraylist containing the definitions for all the widgets to put in the report. Widgets are defined
        using New-NectarReportWidget
 
        .PARAMETER ReportTimeZone
        The timezone used for report display
 
        .PARAMETER ScheduleMode
        How often to run the report when on a schedule
 
        .PARAMETER SchedDayOfMonth
        If report is run on a schedule, what day of the month to run the report. Only works with ScheduleMode MONTHLY
 
        .PARAMETER SchedWeekdays
        The days of the week to run the report. Only works with ScheduleMode WEEKLY
 
        .PARAMETER SchedStartDate
        When the report schedule should start
 
        .PARAMETER SchedEndDate
        When the report schedule should end
 
        .PARAMETER SchedStartTime
        The time of day that the report should start processing
 
        .PARAMETER SchedTimeZone
        The timezone the schedule should run on
 
        .PARAMETER SchedNotifyMethod
        How to notify users when the report is ready
 
        .PARAMETER SchedEmail
        One or more email addresses, separated by commas to notify upon report completion.
        Only usable when SchedNotifyMethod is EMAIL
 
        .PARAMETER SchedEmailSubject
        The subject line of the email.
        Only usable when SchedNotifyMethod is EMAIL
 
        .PARAMETER SchedEmailMessage
        The content of the email message.
        Only usable when SchedNotifyMethod is EMAIL
 
        .PARAMETER SchedRepeatEveryNumDays
        How often to repeat a daily schedule
 
        .PARAMETER SchedRepeatEveryNumWeeks
        How often to repeat a weekly schedule
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        New-NectarReport -Name 'Test Report -Description 'This is a test report' -WidgetList $WidgetList -Type User
        Creates a user-level report using a pre-defined set of widgets stored in the $WidgetList variable
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$Name,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Description,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('User','Tenant', IgnoreCase=$True)]
        [string]$Type = 'User',
        [Parameter(Mandatory=$True)]
        [System.Collections.ArrayList]$WidgetList,
        [Parameter(Mandatory=$False)]
        [string]$ReportTimeZone = "Etc/GMT$((Get-TimeZone).BaseUTCOffset.hours)",
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('ONCE','DAILY','WEEKLY','MONTHLY', IgnoreCase=$False)]
        [string]$ScheduleMode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(1,31)]
        [nullable[int]]$SchedDayOfMonth,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('MONDAY','TUESDAY','WEDNESDAY','THURSDAY','FRIDAY','SATURDAY','SUNDAY', IgnoreCase=$False)]
        [string[]]$SchedWeekdays,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SchedStartDate = ((Get-Date).AddDays(1)).ToShortDateString(),
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SchedEndDate,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [datetime]$SchedStartTime = '00:00',
        [Parameter(Mandatory=$False)]
        [string]$SchedTimeZone = "Etc/GMT$((Get-TimeZone).BaseUTCOffset.hours)",
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('NONE','EMAIL', IgnoreCase=$False)]
        [string]$SchedNotifyMethod = 'NONE',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$SchedEmail,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SchedEmailSubject = $Name,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SchedEmailMessage,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [AllowNull()]
        [nullable[int]]$SchedRepeatEveryNumDays,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [AllowNull()]
        [nullable[int]]$SchedRepeatEveryNumWeeks,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
            $TenantName = $Global:NectarTenantName 
        }

        Switch ($Type) {
            'User'            { $ReportTypeID = 1 }
            'Tenant'        { $ReportTypeID = 2 }
        }

        If ($ScheduleMode) {
            If ($SchedNotifyMethod -eq 'EMAIL') {
                $EmailParams = @{
                    'recipients'         = $SchedEmail
                    'subject'            = $SchedEmailSubject  #If (!$SchedEmailSubject) { $Name } Else { $SchedEmailSubject }
                    'message'            = $SchedEmailMessage
                }
            }

            $NotificationParams = @{
                'method'                = $SchedNotifyMethod
                'email'                    = $EmailParams
            }

            $ScheduleParams = @{
                'scheduleMode'            = $ScheduleMode
                'dayOfMonth'             = $SchedDayOfMonth
                'startDate'             = $SchedStartDate
                'startTime'                = Get-Date $SchedStartTime -UFormat %R
                'endDate'                 = If (!$SchedEndDate) { $Null } Else { $SchedEndDate }
                'repeatEveryNumDays'    = $SchedRepeatEveryNumDays
                'repeatEveryNumWeeks'    = $SchedRepeatEveryNumWeeks
                'weekDayOfMonth'        = $SchedWeekDayOfMonth
                'weekDays'                = $SchedWeekDays
                'weekOfMonth'            = $SchedWeekOfMonth
                'timeZone'                = $SchedTimeZone
                'notification'            = $NotificationParams
            }
        }
        
        $Params = @{
            'name'                = $Name
            'description'        = $Description
            'enabled'            = $True
            'schedule'            = $ScheduleParams
            'timeZone'            = $ReportTimeZone
            'visibilityType'    = $ReportTypeID
            'widgets'            = $WidgetList
        }

        $JSONParams = $Params | ConvertTo-Json -Depth 7

        $URI = "https://$Global:NectarCloud/rapi/client/report/config?tenant=$TenantName"
        Write-Verbose $URI
        Write-Verbose $JSONParams

        Invoke-RestMethod_ErrorHandling -Method POST -URI $URI -Headers $Global:NectarAuthHeader -Body $JSONParams -ContentType 'application/json; charset=utf-8'
    }
}


Function New-NectarReportWidget {
    <#
        .SYNOPSIS
        Creates a report widget to be added to a report
         
        .DESCRIPTION
        Creates a report widget to be added to a report. One ore more widgets have to be defined before creating a report
 
        .PARAMETER WidgetType
        The type of widget to create
 
        .PARAMETER WidgetDescription
        A description to go with the widget
 
        .PARAMETER WidgetPosition
        The numeric position to place the widget in the report. Starts at 0
 
        .PARAMETER WidgetGroupVarName
        The name of a arraylist variable to add the widget to.
        If the variable does not exist, it will be created. Makes creating reports a bit easier.
 
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER SessionQualities
        Show sessions that match a given quality rating. Case sensitive. Choose one or more from:
        'GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN'
         
        .PARAMETER NectarScore
        Show sessions that match the given range of NectarScores. Use format aa-xx or aa.bb-xx.yy, where aa is less than xx, and xx is less than 100. Eg. 70-90 or 95.00-95.59
         
        .PARAMETER DurationFrom
        The shortest call length (in seconds) to show session data.
         
        .PARAMETER DurationTo
        The longest call length (in seconds) to show session data.
         
        .PARAMETER Modalities
        Show sessions that match one or more modality types. Not case sensitive. Choose one or more from:
        'AUDIO','VIDEO','APP_SHARING','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN'
         
        .PARAMETER Protocols
        Show sessions that match one or more network protocol types. Case sensitive. Choose one or more from:
        'TCP','UDP','Unknown'
         
        .PARAMETER ResponseCodes
        Show sessions that match one or more SIP response codes. Accepts numbers from 200 to 699
         
        .PARAMETER SessionScenarios
        Show sessions that match one or more session scenarios. Not case sensitive. Choose one or more from:
        'External','Internal','Internal-External','External-Internal','Federated','Internal-Federated','External-Federated','Unknown'
         
        .PARAMETER SessionTypes
        Show sessions that match one or more session scenarios. Case sensitive. Choose one or more from:
        'Conference','Peer To Peer','Peer To Peer (Multimedia)','PSTN/External'
         
        .PARAMETER Codecs
        Show sessions where the selected codec was used by either caller or callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
 
        .PARAMETER CallerCodecs
        Show sessions where the selected codec was used by the caller. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER CalleeCodecs
        Show sessions where the selected codec was used by the callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER Devices
        Show sessions where the selected device was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerDevices
        Show sessions where the selected device was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeDevices
        Show sessions where the selected device was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER DeviceVersions
        Show sessions where the selected device version was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER CallerDeviceVersions
        Show sessions where the selected device version was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
         
        .PARAMETER CalleeDeviceVersions
        Show sessions where the selected device version was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER AgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER CallerAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
         
        .PARAMETER CalleeAgentVersions
        Show sessions where the selected agent version was used by either caller or callee. Can query for multiple agent. Case sensitive. Use Get-NectarAgentVersion for a list of valid agent versions.
 
        .PARAMETER IPAddresses
        Show sessions where the selected IP address was used by either caller or callee. Can query for multiple IPs.
 
        .PARAMETER CallerIPAddresses
        Show sessions where the selected IP address was used by the caller. Can query for multiple IPs.
         
        .PARAMETER CalleeIPAddresses
        Show sessions where the selected IP address was used by the callee. Can query for multiple IPs.
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
         
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER ExtConnectionTypes
        Show sessions that match the caller or callee external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
 
        .PARAMETER CallerExtConnectionTypes
        Show sessions that match the caller's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER CalleeExtConnectionTypes
        Show sessions that match the callee's external connection type (as detected via geolocating the user's external IP address). Can query for multiple types.
         
        .PARAMETER NetworkTypes
        Show sessions where the selected network type was used by either caller or callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER CallerNetworkTypes
        Show sessions where the selected network type was used by the caller. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
         
        .PARAMETER CalleeNetworkTypes
        Show sessions where the selected network type was used by the callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED'
 
        .PARAMETER Platforms
        Show sessions where the selected platform was used by either caller or callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','GENESYS','JABRA','MISCELLANEOUS'
 
        .PARAMETER Servers
        Shows sessions where the selected server was used by either caller or callee. Can query for multiple servers.
        Must follow pattern of PLATFORM:SERVERNAME. Eg. 'CISCO:Server1', 'AVAYA_AURA_CM:Server2'
         
        .PARAMETER EndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CallerEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the caller. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
 
        .PARAMETER CalleeEndpointTypes
        Show sessions where one or more selected Avaya endpoint types have been used by the callee. Choose one or more from: 'medsvr','mgdsp','node','phone','unknown'
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER UserIDs
        Show sessions where the selected user ID was either caller or callee. Can query for multiple user IDs.
 
        .PARAMETER FromUserIDs
        Show sessions where the selected user ID was the caller. Can query for multiple user IDs.
         
        .PARAMETER ToUserIDs
        Show sessions where the selected user ID was the callee. Can query for multiple user IDs.
 
        .PARAMETER ConfOrganizers
        Show sessions hosted by a specified conference organizer. Can query for multiple organizers.
         
        .PARAMETER VPN
        Show sessions where the selected VPN was used by either caller or callee.
 
        .PARAMETER CallerVPN
        Show sessions where the selected VPN was used by the caller.
         
        .PARAMETER CalleeVPN
        Show sessions where the selected VPN was used by the callee.
         
        .PARAMETER ParticipantsMinCount
        Show sessions where the number of participants is greater than or equal to the entered value
 
        .PARAMETER ParticipantsMaxCount
        Show sessions where the number of participants is less than or equal to the entered value
         
        .PARAMETER FeedbackRating
        Show sessions where users provided specific feedback ratings from BAD to EXCELLENT.
        Allowed values are BAD, POOR, FAIR, GOOD, EXCELLENT. Corresponds to ratings from 1 to 5 stars.
         
        .PARAMETER Insights
        Show sessions that match one or more given insights. Choose from NORMAL_SESSION, VOICE_MAIL, HIGH_JITTER, HIGH_PACKET_LOSS, HIGH_ROUNDTRIP_DELAY
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        New-NectarReportWidget -WidgetType 'Call Details, Sessions' -WidgetDescription "Conference Session Summary for New York" -WidgetPosition 0 -WidgetGroupVarName WidgetList -TimePeriod LAST_MONTH -Modalities AUDIO -Platforms TEAMS -SessionTypes CONFERENCE_SESSION -ExtCities 'New York' -ExtCountries 'US'
        Creates a Call Details widget to be used later in New-NectarReport
        .NOTES
        Version 1.0
    #>

    Param (
        [Parameter(Mandatory=$True)]
        [ValidateSet('Call Details, Sessions','Call Details, Quality Audio','Call Details, Quality Video','Call Details, Quality App Sharing','Call Details, Session List','Quality Details, Sessions','Quality Details, Quality Summary','Quality Details, Session List', IgnoreCase=$True)]
        [string]$WidgetType,
        [Parameter(Mandatory=$False)]
        [string]$WidgetDescription,
        [Parameter(Mandatory=$False)]
        [int]$WidgetPosition,
        [Parameter(Mandatory=$False)]
        [string]$WidgetGroupVarName,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('YESTERDAY','LAST_WEEK','LAST_MONTH','LAST_03_MONTHS','LAST_12_MONTHS','MONTH_TO_DATE','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_MONTH',
        [Parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [DateTime]$TimePeriodFrom,
        [Parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [DateTime]$TimePeriodTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [string]$NectarScore,    
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AUDIO','VIDEO','APP_SHARING','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS', IgnoreCase=$True)]
        [string[]]$Modalities,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,608)]
        [string[]]$ResponseCodes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','CONVERSATION_JOURNEY','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN','WEBINAR', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [Parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [Parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$AgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeAgentVersions,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$IPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeIPAddresses,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$ExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CallerExtConnectionTypes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Cable','Dialup','DSL','FTTx','ISDN','Unknown','Wireless', IgnoreCase=$False)]
        [string[]]$CalleeExtConnectionTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('CELLULAR','ENTERPRISE_WIFI','ENTERPRISE_WIRED','EXTERNAL_WIFI','EXTERNAL_WIRED','MOBILE','PPP','UNKNOWN','WIFI','WIRED', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','GENESYS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [Parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','CISCO_WEBEX_CALLING','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','WEB_RTC_AIR_PHONE','WEB_RTC_AMAZON_CONNECT','WEB_RTC_CISCO_WEBEX','WEB_RTC_FIVE9','WEB_RTC_GENESYS_CLOUD','WEB_RTC_GENESYS_MCPE','WEB_RTC_NICE_CXONE','DIAGNOSTICS','GENESYS','JABRA','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [Parameter(Mandatory=$False)]
        [string[]]$Servers,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$EndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CallerEndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('AAEP','AAMS','medsvr','mgdsp','node','phone','SBC','sip','unknown', IgnoreCase=$True)]
        [string[]]$CalleeEndpointTypes,
        [Parameter(Mandatory=$False)]
        [ValidateSet('EXTERNAL','EXTERNAL_FEDERATED','EXTERNAL_INTERNAL','FEDERATED','FEDERATED_EXTERNAL','FEDERATED_INTERNAL','INTERNAL','INTERNAL_EXTERNAL','INTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Scenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CallerScenarios,
        [Parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CalleeScenarios,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$UserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUserIDs,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,        
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [Parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [Parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [Parameter(Mandatory=$False)]
        [ValidateSet('NORMAL_SESSION','VOICE_MAIL','HIGH_JITTER','HIGH_PACKET_LOSS','HIGH_ROUNDTRIP_DELAY','AGENT', IgnoreCase=$False)]
        [string[]]$Insights,
        [Parameter(Mandatory=$False)]
        [ValidateSet('P2P','PING','AUDIO','VIDEO', IgnoreCase=$False)]
        [string[]]$TestTypes,    
        [Parameter(Mandatory=$False)]
        [ValidateSet('PASSED','FAILED','UNKNOWN','INCOMPLETE','INCOMPLETE', IgnoreCase=$False)]
        [string[]]$TestResults,
        [Parameter(Mandatory=$False)]
        [ValidateSet("callerDeviceVersion","calleeDeviceVersion","callerCodec","calleeCodec",
        "confOrganizerOrSpace","callerLocation","calleeLocation","callerDevice",
        "calleeDevice","duration","callerExtConnectionType","calleeExtConnectionType",
        "callerExtCity","calleeExtCity","callerExtCountry","calleeExtCountry",
        "callerExtIpAddress","calleeExtIpAddress","from","fromName","insights",
        "callerIpAddress","calleeIpAddress","callerExtIsp","calleeExtIsp",
        "jabraScore","callerCaptureDevice","calleeCaptureDevice","nectarScore",
        "callerNetworkType","calleeNetworkType","participants","callerPhone",
        "calleePhone","callerPlatform","calleePlatform","protocol","quality",
        "responseCodes","callerScenario","calleeScenario","servers","sessionScenario",
        "sessionType","callerRenderDevice","calleeRenderDevice","startDate","to",
        "toName","callerRating","calleeRating","callerVpn","calleeVpn", IgnoreCase=$False)]
        [string[]]$ViewColumns,
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS','ENDPOINT_CLIENT','CALL_DETAILS_HISTORIC','QUALITY_DETAILS', IgnoreCase=$True)]
        [string]$Scope = 'DEFAULT',
        [switch]$ShowQualityDetails    
    )    
    Begin {
        # Check for the existance of the global widget group variable. Create it if it doesn't exist.
        If ($WidgetGroupVarName) {
            If (Get-Variable $WidgetGroupVarName -Scope Global -ErrorAction SilentlyContinue) {
            }
            Else {
                [System.Collections.ArrayList]$TempVar = @()
                Set-Variable -Name $WidgetGroupVarName -Value (Get-Variable -Name TempVar -ValueOnly) -Scope Global
                Remove-Variable TempVar
            }
        }
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
                $TenantName = $Global:NectarTenantName 
            } ElseIf ($TenantName) {
                If ($TenantName -NotIn $Global:NectarTenantList) {
                    $TList = $Global:NectarTenantList -join ', '
                    Throw "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList. $($_.Exception.Message)"
                }
            }
        
        # Convert to all-caps
        If ($Scope) { $Scope = $Scope.ToUpper() }
        If ($TimePeriod) { $TimePeriod = $TimePeriod.ToUpper() }    
        
        $FilterParams = @{
            'analyticsTimePeriod' = $TimePeriod
        }

        # Map widget names to their corresponding IDs
        Switch ($WidgetType) {
            'Call Details, Sessions'            { $WidgetID = 1 }
            'Call Details, Quality Audio'        { $WidgetID = 5 }
            'Call Details, Quality Video'        { $WidgetID = 6 }
            'Call Details, Quality App Sharing'    { $WidgetID = 7 }
            'Call Details, Session List'        { $WidgetID = 3 }
            'Quality Details, Sessions'            { $WidgetID = 2 }
            'Quality Details, Quality Summary'    { $WidgetID = 8 }
            'Quality Details, Session List'        { $WidgetID = 4 }
        }

    
        # Convert any PowerShell array objects to comma-separated strings to add to the GET querystring
        # Convert multi-string variables into a comma-delimited list and add to the FilterParams array. For variables that need to be in upper-case
        $MultiVarListUpper = 'SessionQualities','SessionScenarios','SessionTypes','Modalities','Platforms','Scenarios',
                            'VPN','CallerVPN','CalleeVPN',
                            'FeedbackRating','Insights','TestTypes','TestResults'
            
        ForEach ($MultiVar in $MultiVarListUpper) {
            If ((Get-Variable $MultiVar).Value) {
                # Make sure all variable names are in camel-case
                $FilterParams.Add($MultiVar.substring(0,1).ToLower()+$MultiVar.substring(1),(Get-Variable $MultiVar).Value)
                Write-Verbose "$($MultiVar.substring(0,1).ToLower()+$MultiVar.substring(1))`: $((Get-Variable $MultiVar).Value)"
            }
        }
        
        # Convert multi-string variables into a comma-delimited list and add to the FilterParams array. For variables that do not need to be in upper-case
        $MultiVarList = 'Protocols','ResponseCodes','Servers',
                        'Codecs','CallerCodecs','CalleeCodecs',
                        'Devices','CallerDevices','CalleeDevices',
                        'DeviceVersions','CallerDeviceVersions','CalleeDeviceVersions',
                        'Locations','CallerLocations','CalleeLocations',
                        'ExtCities','CallerExtCities','CalleeExtCities',
                        'ExtCountries','CallerExtCountries','CalleeExtCountries',
                        'ExtISPs','CallerExtISPs','CalleeExtISPs',
                        'extConnectionTypes','callerExtConnectionTypes','calleeExtConnectionTypes',
                        'NetworkTypes','CallerNetworkTypes','CalleeNetworkTypes',
                        'ipAddresses','callerIpAddresses','calleeIpAddresses',
                        'endpointTypes','callerEndpointTypes','calleeEndpointTypes'
                        
        ForEach ($MultiVar in $MultiVarList) {
            If ((Get-Variable $MultiVar).Value) {
                $MultiVarValue = (Get-Variable $MultiVar).Value

                # Update name of parameters if necessary
                Switch ($MultiVar) {
                    'Servers' { $MultiVar = 'platformServersOrDataCenters' }
                }

                If ($MultiVar -like '*ISPs') { $MultiVar = $MultiVar.Replace('ISPs', 'Isps') }

                # Make sure all variable names are in camel-case
                $FilterParams.Add($MultiVar.substring(0,1).ToLower()+$MultiVar.substring(1),$MultiVarValue)
                Write-Verbose "$($MultiVar.substring(0,1).ToLower()+$MultiVar.substring(1))`: $MultiVarValue)"
            }
        }
        
        # Get user IDs and convert into a comma-delimited list and add to the FilterParams array.
        $UserVarList = 'Users','FromUsers','ToUsers'
        
        ForEach ($UserVar in $UserVarList) {
            If ((Get-Variable $UserVar).Value) {
                $UserList = (Get-Variable $UserVar).Value
                
                $UserIDList = @()
                $PlatformUserNames = @()
                ForEach ($User in $UserList) {
                    Write-Verbose "UserSearch: $User"
                    $UserInfo = Get-NectarUser $User -Scope $Scope -TenantName $TenantName -ResultSize 10000
                    If ($UserInfo.userId) { 
                        $UserIDList += $UserInfo.userId
                    }
                    ElseIf ($UserInfo.platformUserName) {
                        $PlatformUserNames += $UserInfo.platformUserName
                    }
                }
                
                If ($UserIDList) {    
                    $UserIDList | ForEach-Object { $UserIDString += ($(If($UserIDString){","}) + $_) } 
                    $FilterParams.Add($UserVar.substring(0,1).ToLower()+$UserVar.substring(1),$UserIDList)
                    Write-Verbose "UserID $UserVar`: $UserIDList"
                    Remove-Variable UserIDString
                }
                
                If ($PlatformUserNames) { 
                    $PlatformUserNames | ForEach-Object { $PlatformUserNameString += ($(If($PlatformUserNameString){","}) + $_) }
                    $FilterParams.Add($UserVar.Replace('Users','PlatformUserNames'),$PlatformUserNameString)
                    Write-Verbose "Platform $UserVar`: $PlatformUserNameString"
                    Remove-Variable PlatformUserNameString
                }
                
                If (!$UserIDList -And !$PlatformUserNames) { Write-Error 'No matching users found'; Return }
            }
        }

        # Do the same for UserIDs
        $UserIDVarList = 'UserIDs','FromUserIDs','ToUserIDs'
        
        ForEach ($UserIDVar in $UserIDVarList) {
            If ((Get-Variable $UserIDVar -ErrorAction SilentlyContinue).Value) {
                $UserIDList = (Get-Variable $UserIDVar).Value
                
                If ($UserIDList) {    
                    $UserIDList | ForEach-Object { $UserIDString += ($(If($UserIDString){","}) + $_) } 
                    $FilterParams.Add($UserIDVar.substring(0,1).ToLower()+$UserIDVar.substring(1).Replace('ID',''),$UserIDList)  # Convert to camelCase
                    Write-Verbose "UserID $($UserIDVar.substring(0,1).ToLower()+$UserIDVar.substring(1).Replace('ID',''))`: $UserIDString"  
                    Remove-Variable UserIDString
                }
            }
        }

        # Get agent versions and convert into a comma-delimited list and add to the FilterParams array.
        $AgentVarList = 'AgentVersions','CallerAgentVersions','CalleeAgentVersions'
                
        ForEach ($AgentVar in $AgentVarList) {
            If ((Get-Variable $AgentVar).Value) {
                $AgentList = (Get-Variable $AgentVar).Value
                
                # Parse through each entry and search for results
                $FinalAgentList = @()
                ForEach ($Agent in $AgentList) {
                    Write-Verbose "AgentSearch: $Agent"
                    $AgentInfo = Get-NectarAgentVersion -SearchQuery $Agent -TenantName $TenantName
                    If ($AgentInfo) { 
                        $FinalAgentList += $AgentInfo
                    }
                }
                
                # Convert to comma-delimited list and add to FilterParams array
                If ($FinalAgentList) {    
                    $FilterParams.Add($AgentVar.Substring(0,1).ToLower()+$AgentVar.Substring(1,$AgentVar.Length-2),$FinalAgentList)   # Convert to camelCase
                    Write-Verbose "Agent $AgentVar.Substring(0,1).ToLower()+$AgentVar.Substring(1,$AgentVar.Length-2)`: $FinalAgentList"
                }
                
                If (!$FinalAgentList) { Write-Error 'No matching agent versions found'; Return }
            }
        }
        
        # Add single parameter variables to the FilterParams array.
        $VarList = 'NectarScore','DurationFrom','DurationTo','ParticipantsMinCount','ParticipantsMaxCount'
        
        ForEach ($Var in $VarList) {
            If ((Get-Variable $Var).Value) {
                $FilterParams.Add($Var.Substring(0,1).ToLower()+$Var.Substring(1),(Get-Variable $Var).Value)   # Convert to camelCase
                Write-Verbose "$Var`: (Get-Variable $Var).Value"
            }
        }

        # Add time-based parameter variables to the FilterParams array. This converts to UNIX timestamp
        $TimeVarList = 'TimePeriodFrom','TimePeriodTo'

        # Set TimePeriodTo to NOW if not explicitly set
        If ($TimePeriodFrom -And !$TimePeriodTo) {
            [String]$TimePeriodTo = Get-Date
        }
        
        ForEach ($TimeVar in $TimeVarList) {
            If ((Get-Variable $TimeVar).Value) {
                [decimal]$TimePeriodUnix = Get-Date -Date (Get-Variable $TimeVar).Value -UFormat %s
                [long]$TimePeriodUnix = $TimePeriodUnix * 1000
                $FilterParams.Add($TimeVar.Replace('TimePeriod','startDate'),$TimePeriodUnix)
                Write-Verbose "$TimeVar`: $TimePeriodUnix"
            }
        }
        
        If ($ConfOrganizers) {
            $ConfOrganizerIDs = ForEach($Organizer in $ConfOrganizers) {
                (Get-NectarUser $Organizer -Scope $Scope -TenantName $TenantName -ErrorAction:Stop).Userid
            }
            $ConfOrganizerIDs | ForEach-Object { $ConfOrganizerIDsStr += ($(If($ConfOrganizerIDsStr){","}) + $_) }
            $FilterParams.Add('organizersOrSpaces',$ConfOrganizerIDsStr)
        }
        
        If ($ShowQualityDetails) {
            $FilterParams.Add('sessionQualitySources','CDS,CDR_CDS')
            $FilterParams.Add('excludeIncompleteRecords','true')
        }

        $SessionQualitySources = 'CDR','CDR_CDS'
        $FilterParams.Add('sessionQualitySources',$SessionQualitySources)
        $FilterParams.Add('scope', $Scope)
        $FilterParams.Add('excludeAvayaPpm', $True)
        
        # If the widget is the session list type, then set the columns to be displayed
        If ($WidgetType -like '*Session List') {
            $SessionListView = @{
                rowLimit = 50000
                columns = $ViewColumns
                sort = @{
                    field = "startDate"
                    direction = "DESC"
                }
            }
        }
        Else {
            $SessionListView = $Null
        }

        $WidgetConfig = @{
            description        = $WidgetDescription
            exportFormat    = 'pdf'
            filter            = $FilterParams
            view            = $SessionListView
        }

        # Check for widget position. If not present, use the position from the WidgetGroupVar (if available)
        If (!$WidgetPosition) {
            # If the WidgetGroupVar exists, then set the widget position to be the last numeric value
            If ($WidgetGroupVarName) {
                $WidgetGroupData = (Get-Variable $WidgetGroupVarName).Value
                $WidgetPosition = $WidgetGroupData.Count
            }            
        } 

        $WidgetParams = @{
            id                = $NULL
            name            = $WidgetType
            position        = $WidgetPosition
            widgetId        = $WidgetID
            configuration    = $WidgetConfig
        }
        

        If ($WidgetGroupVarName) {
            (Get-Variable $WidgetGroupVarName).Value += $WidgetParams
        }
        Else {
            Return $WidgetParams
        }
    }
}


Function Remove-NectarReport {
    <#
        .SYNOPSIS
        Removes a report from Nectar DXP
         
        .DESCRIPTION
        Removes a report from Nectar DXP
 
        .PARAMETER ID
        The ID of the report to delete. Can be passed from Get-NectarReport
 
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarReport -SearchQuery 'Test' | Remove-NectarReport
        Removes all reports that contain the word 'Test' in the name/description etc
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
            $TenantName = $Global:NectarTenantName 
        }

        Try {
            $URI = "https://$Global:NectarCloud/rapi/client/report/config/$($ID)?tenant=$TenantName"
            Write-Verbose $URI
            
            $Null = Invoke-RestMethod -Method DELETE -URI $URI -Headers $Global:NectarAuthHeader
        }
        Catch {
            Write-Error "Error deleting report #$($ID). $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Start-NectarReport {
    <#
        .SYNOPSIS
        Triggers the processing of a report
         
        .DESCRIPTION
        Triggers the processing of a report
 
        .PARAMETER ID
        The numeric ID of the report to start
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Start-NectarReport -ID 31
        Triggers the execution of report with ID 31
 
        Get-NectarReport Test | Start-NectarReport
        Triggers the execution of any report with 'Test' in the name
         
        .NOTES
        Version 1.0
    #>


    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
            $TenantName = $Global:NectarTenantName 
        }

        Try {
            $URI = "https://$Global:NectarCloud/rapi/client/report/runnow/$($ID)?tenant=$TenantName"
            Write-Verbose $URI
            
            $Null = Invoke-RestMethod -Method PUT -URI $URI -Headers $Global:NectarAuthHeader
        }
        Catch {
            Write-Error "Error running report #$($ID). $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NectarReportPreview {
    <#
        .SYNOPSIS
        Gets information about the report widgets used for a given report
         
        .DESCRIPTION
        Gets information about the report widgets used for a given report
 
        .PARAMETER ID
        The numeric ID of the report to get information about
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarReportPreview -ID 31
        Returns information about the report with ID 31
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ID,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
            $TenantName = $Global:NectarTenantName 
        }

        Try {
            $URI = "https://$Global:NectarCloud/rapi/client/report/config/detail/$($ID)?tenant=$TenantName"
            Write-Verbose $URI
            
            $JSON = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader

            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}

            Return $JSON
        }
        Catch {
            Write-Error "Error getting report info for ID $ID. Does the report exist? $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}


Function Get-NectarReportDetail {
    <#
        .SYNOPSIS
        Returns detail about a given widget within a report
         
        .DESCRIPTION
        Returns detail about a given widget within a report
 
        .PARAMETER ReportID
        The numeric ID of the report that contains the desired widget
         
        .PARAMETER ReportWidgetID
        The numeric ID of the report widget to get details about
 
        .PARAMETER WidgetType
        The type of widget to report on. If not supplied, will be obtained by running Get-NectarReportPreview
         
        .PARAMETER TenantName
        The name of the Nectar DXP tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarReportDetail -ReportID 31 -ReportWidgetID 84
        Returns details about the widget with ID 84 on report ID 31
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ReportID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$ReportWidgetID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('SESSIONS', 'QUALITY_AUDIO', 'QUALITY_VIDEO', 'QUALITY_APPSHARE', 'QUALITY_SUMMARY', 'SESSION_LIST', IgnoreCase=$False)]
        [string]$WidgetType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$WidgetDescription,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$PSBoundParameters.ContainsKey('TenantName')) { 
            $TenantName = $Global:NectarTenantName 
        }

        # Get the widget type if it wasn't entered as a parameter
        If (!$WidgetType) {
            $WidgetType = ((Get-NectarReportPreview -ID $ReportID).Widgets | Where-Object {$_.ReportWidgetID -eq $ReportWidgetID}).widgetType
        }

        Switch ($WidgetType) {
            'GRAPH'             { $URLPath = 'graph' }
            'SESSIONS'            { $URLPath = 'graph' }
            'QUALITY_AUDIO'        { $URLPath = 'session/audio' }
            'QUALITY_VIDEO'        { $URLPath = 'session/video' }
            'QUALITY_APPSHARE'    { $URLPath = 'session/appshare' }
            'QUALITY_SUMMARY'    { $URLPath = 'quality/summary' }
        }

        $URI = "https://$Global:NectarCloud/rapi/client/report/data/$URLPath/$($ReportWidgetID)?tenant=$TenantName"
        Write-Verbose $URI

        Try {
            $JSON = Invoke-RestMethod -Method GET -URI $URI -Headers $Global:NectarAuthHeader
            If ($WidgetDescription) { $JSON | Add-Member -Name 'WidgetDescription' -Value $WidgetDescription -MemberType NoteProperty }
            Return $JSON
        }
        Catch {
            Write-Error "Error getting report widget detail for ID $ReportWidgetID. Does the report widget exist? $($_.Exception.Message)"
            If ($PSCmdlet.MyInvocation.BoundParameters["ErrorAction"] -ne "SilentlyContinue") { Get-JSONErrorStream -JSONResponse $_ }
        }
    }
}
# SIG # Begin signature block
# MIIfjQYJKoZIhvcNAQcCoIIffjCCH3oCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBqtirDACxYwQDL
# D8SZ+1eWDym50sYuRuO2HveOXf0x9qCCDRIwggZyMIIEWqADAgECAghkM1HTxzif
# CDANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx
# EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8G
# A1UEAwwoU1NMLmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTAe
# Fw0xNjA2MjQyMDQ0MzBaFw0zMTA2MjQyMDQ0MzBaMHgxCzAJBgNVBAYTAlVTMQ4w
# DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENv
# cnAxNDAyBgNVBAMMK1NTTC5jb20gQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBD
# QSBSU0EgUjEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCfgxNzqrDG
# bSHL24t6h3TQcdyOl3Ka5LuINLTdgAPGL0WkdJq/Hg9Q6p5tePOf+lEmqT2d0bKU
# Vz77OYkbkStW72fL5gvjDjmMxjX0jD3dJekBrBdCfVgWQNz51ShEHZVkMGE6ZPKX
# 13NMfXsjAm3zdetVPW+qLcSvvnSsXf5qtvzqXHnpD0OctVIFD+8+sbGP0EmtpuNC
# GVQ/8y8Ooct8/hP5IznaJRy4PgBKOm8yMDdkHseudQfYVdIYyQ6KvKNc8HwKp4WB
# wg6vj5lc02AlvINaaRwlE81y9eucgJvcLGfE3ckJmNVz68Qho+Uyjj4vUpjGYDdk
# jLJvSlRyGMwnh/rNdaJjIUy1PWT9K6abVa8mTGC0uVz+q0O9rdATZlAfC9KJpv/X
# gAbxwxECMzNhF/dWH44vO2jnFfF3VkopngPawismYTJboFblSSmNNqf1x1KiVgMg
# Lzh4gL32Bq5BNMuURb2bx4kYHwu6/6muakCZE93vUN8BuvIE1tAx3zQ4XldbyDge
# VtSsSKbt//m4wTvtwiS+RGCnd83VPZhZtEPqqmB9zcLlL/Hr9dQg1Zc0bl0EawUR
# 0tOSjAknRO1PNTFGfnQZBWLsiePqI3CY5NEv1IoTGEaTZeVYc9NMPSd6Ij/D+KNV
# t/nmh4LsRR7Fbjp8sU65q2j3m2PVkUG8qQIDAQABo4H7MIH4MA8GA1UdEwEB/wQF
# MAMBAf8wHwYDVR0jBBgwFoAU3QQJB6L1en1SUxKSle44gCUNplkwMAYIKwYBBQUH
# AQEEJDAiMCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcHMuc3NsLmNvbTARBgNVHSAE
# CjAIMAYGBFUdIAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwOwYDVR0fBDQwMjAwoC6g
# LIYqaHR0cDovL2NybHMuc3NsLmNvbS9zc2wuY29tLXJzYS1Sb290Q0EuY3JsMB0G
# A1UdDgQWBBRUwv4QlQCTzWr158DX2bJLuI8M4zAOBgNVHQ8BAf8EBAMCAYYwDQYJ
# KoZIhvcNAQELBQADggIBAPUPJodwr5miyvXWyfCNZj05gtOII9iCv49UhCe204MH
# 154niU2EjlTRIO5gQ9tXQjzHsJX2vszqoz2OTwbGK1mGf+tzG8rlQCbgPW/M9r1x
# xs19DiBAOdYF0q+UCL9/wlG3K7V7gyHwY9rlnOFpLnUdTsthHvWlM98CnRXZ7WmT
# V7pGRS6AvGW+5xI+3kf/kJwQrfZWsqTU+tb8LryXIbN2g9KR+gZQ0bGAKID+260P
# Z+34fdzZcFt6umi1s0pmF4/n8OdX3Wn+vF7h1YyfE7uVmhX7eSuF1W0+Z0duGwdc
# +1RFDxYRLhHDsLy1bhwzV5Qe/kI0Ro4xUE7bM1eV+jjk5hLbq1guRbfZIsr0WkdJ
# LCjoT4xCPGRo6eZDrBmRqccTgl/8cQo3t51Qezxd96JSgjXktefTCm9r/o35pNfV
# HUvnfWII+NnXrJlJ27WEQRQu9i5gl1NLmv7xiHp0up516eDap8nMLDt7TAp4z5T3
# NmC2gzyKVMtODWgqlBF1JhTqIDfM63kXdlV4cW3iSTgzN9vkbFnHI2LmvM4uVEv9
# XgMqyN0eS3FE0HU+MWJliymm7STheh2ENH+kF3y0rH0/NVjLw78a3Z9UVm1F5VPz
# iIorMaPKPlDRADTsJwjDZ8Zc6Gi/zy4WZbg8Zv87spWrmo2dzJTw7XhQf+xkR6Od
# MIIGmDCCBICgAwIBAgIQIc+kRkn+AQupTThE+j58IjANBgkqhkiG9w0BAQsFADB4
# MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24x
# ETAPBgNVBAoMCFNTTCBDb3JwMTQwMgYDVQQDDCtTU0wuY29tIENvZGUgU2lnbmlu
# ZyBJbnRlcm1lZGlhdGUgQ0EgUlNBIFIxMB4XDTI1MDgwODIwMTMzNFoXDTI2MDgw
# ODIwMTMzNFowfzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMRAwDgYD
# VQQHDAdKZXJpY2hvMR4wHAYDVQQKDBVOZWN0YXIgU2VydmljZXMgQ29ycC4xCzAJ
# BgNVBAsMAklUMR4wHAYDVQQDDBVOZWN0YXIgU2VydmljZXMgQ29ycC4wggGiMA0G
# CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCJXN3SkHk8zvqqnHkfyImA6vDVtGrO
# zTVO6nlzNe85eCoGRBk5ToZ+/uUwcFyjcxSV+jbR29y4O6azNBmwvEw96ukUVKUh
# J+0NIQoH2DJqtkp4v3EppsevtxxwarqC1fvMXMhz2NVBZS6moStIiFyeGTBgyR7P
# gdU+JRqiG1muq5QiSZWjCyBLN6DTDimz5YdX1nhc/64V/oT0g79tYmZm7UEw1rN8
# HZgk46Gezt3IIQGX22ng1nh/3vy1Q46T/8mCT3UANrd3l/jS0XUmYgW8Z91nYlrl
# iNH3nHbONGNFFN8WLPAakt3ITeGmqhZkHyyXmlxKlkLHiR8XewRHn7PpD/DT03x7
# ngKS5Ie0fUM1ZAdXDoghvQ6uNuQ5Q3TAjL2ukJs9u5VmvWyFL1l9ujuKCiNGfy1D
# cS7u1WlcCIXdrX4Hpe2lt/M7fZFkSMeS1TD2gM2+a/7xK5MWwmbV6qK27nKlRpbG
# Q6Yj0VmqJmcgekSrCKPFudNAsDyD6rUYxlUCAwEAAaOCAZUwggGRMAwGA1UdEwEB
# /wQCMAAwHwYDVR0jBBgwFoAUVML+EJUAk81q9efA19myS7iPDOMwegYIKwYBBQUH
# AQEEbjBsMEgGCCsGAQUFBzAChjxodHRwOi8vY2VydC5zc2wuY29tL1NTTGNvbS1T
# dWJDQS1Db2RlU2lnbmluZy1SU0EtNDA5Ni1SMS5jZXIwIAYIKwYBBQUHMAGGFGh0
# dHA6Ly9vY3Nwcy5zc2wuY29tMFEGA1UdIARKMEgwCAYGZ4EMAQQBMDwGDCsGAQQB
# gqkwAQMDATAsMCoGCCsGAQUFBwIBFh5odHRwczovL3d3dy5zc2wuY29tL3JlcG9z
# aXRvcnkwEwYDVR0lBAwwCgYIKwYBBQUHAwMwTQYDVR0fBEYwRDBCoECgPoY8aHR0
# cDovL2NybHMuc3NsLmNvbS9TU0xjb20tU3ViQ0EtQ29kZVNpZ25pbmctUlNBLTQw
# OTYtUjEuY3JsMB0GA1UdDgQWBBQoc8kxtmxEx1bpEE+bhb8JNyGrHTAOBgNVHQ8B
# Af8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBAD1Wlyoz/oOVnzXFQKuv29SQ9J6G
# lqSNMvPzF6204eydcEDLwKh5IpZ7iGo6Km5AoVGNzYwJWyWaTwZpaxOoh+M+NK+E
# /QIOH+mItAaHhKogn96pHL7ZG9oD23s+/EP9jqNpenOZ0HSBWNHc4PO0e7Ys5zyQ
# 0a63PF83kU6hqkny0hca7Mr4gZoFyt6ynEJvv/TIkNCE5tqo/cBW94EpjQBfXTd4
# ycDoQ5Be1gbLboBoEjyrnPl0n+5dQgXSCg25hgf37iTfg+sCXzw3SKeiFauf4s8o
# h3wyXvNGzW4q0ZoE+ZNInk/puRVGnGmGZUr6iKuplqey1vneOOAv5we2hryR8PaA
# eqo6OqxyEw6a+nmRwf3lsp9k6Aml6ary1bdGjRY+ysScAxbeCvC9yS/EzwtaU3Z8
# R+2qLqh7cX8gztflHxrsV7Ql0nE3MrM0ry2bAWYzthbdwYtIuoeGHibng/qxuHOI
# uRLSTzddpXDdLSmw+G116kaxxHCBM+wf6K0nvehm08pXNyzAolNIJrVjsll9YHDT
# wws15kpojCglZsMIDYAlrFzF9MI0dRdiaj7/ttGSdblKZJoDOdfPrWMuBlSmjamk
# 5VxgKKHPQ9mHTP1Q5baEuotRYqJLgTWV1ZzCF+MYH2vmmuyjm4sSEjVfetLaYwIk
# RYAx7I+e9wcfhciSMYIR0TCCEc0CAQEwgYwweDELMAkGA1UEBhMCVVMxDjAMBgNV
# BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29ycDE0
# MDIGA1UEAwwrU1NMLmNvbSBDb2RlIFNpZ25pbmcgSW50ZXJtZWRpYXRlIENBIFJT
# QSBSMQIQIc+kRkn+AQupTThE+j58IjANBglghkgBZQMEAgEFAKB8MBAGCisGAQQB
# gjcCAQwxAjAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcC
# AQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCCx0f+PMxSXrwAmQze4
# lpOM4C8+Jsr/PBtiQWhfxJ3qMDANBgkqhkiG9w0BAQEFAASCAYA/EanJ38lPXUFb
# NRl1j2j14wmam8dxxccHoSbVmM9YQFrc4uVW/EMt2oGYhytwHlnCBEY/KXcp8TKA
# iX5u+Cqp6ffSGJZ4symAa0GuYNTya6OAk82EJi10UBb1hpIPMD1lf7WjYZhS/Mrc
# jpMp3+jtGHFhz4lD00gdDtoSbl9KspuoC8QU2vUC48qfl3Hq7g4yW6+JMvM6nrf6
# lfso1lkqX3xZGDGd6l2tI3PkAjgyUPrS+Pwk35bo21fFa0d9hZHQkdfQSZt0ZMGi
# QteGIDR6Bk3rc2HgbRHaDk+aGXsWlj36DgAfek/fKF8gctq1XpZnYxksefgLWsHS
# dPImWFjCCgHoTIejRBdVgCFzT61BXMnpdgOBHeVA6/mS3TRGx6CcwdUqrRsVeVr0
# v4tfsNt+c5cbVjWUYB8wsJYDAGo/0/lMnd07/1unNDS4suqJ4+BnVKA3KQmmV02g
# pNDFA8NEvymRMWiQfN8NKBvvM7RiJ+dlzapD7YUAFitTVwgML4ehgg8XMIIPEwYK
# KwYBBAGCNwMDATGCDwMwgg7/BgkqhkiG9w0BBwKggg7wMIIO7AIBAzENMAsGCWCG
# SAFlAwQCATB3BgsqhkiG9w0BCRABBKBoBGYwZAIBAQYMKwYBBAGCqTABAwYBMDEw
# DQYJYIZIAWUDBAIBBQAEIOOJFnEN6L1WLFhsChY0qINZxzSzIf4Kr69ldN+iBNfK
# Agh13uFfgedNYhgPMjAyNjA2MDMxNDA5NDhaMAMCAQGgggwAMIIE/DCCAuSgAwIB
# AgIQH2sWYtIuG2xd8cDBoGAOODANBgkqhkiG9w0BAQsFADBzMQswCQYDVQQGEwJV
# UzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xETAPBgNVBAoMCFNT
# TCBDb3JwMS8wLQYDVQQDDCZTU0wuY29tIFRpbWVzdGFtcGluZyBJc3N1aW5nIFJT
# QSBDQSBSMTAeFw0yNTAyMTgxNjMyMDJaFw0zNDExMTIxODUwMDVaMG4xCzAJBgNV
# BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjERMA8GA1UE
# CgwIU1NMIENvcnAxKjAoBgNVBAMMIVNTTC5jb20gVGltZXN0YW1waW5nIFVuaXQg
# MjAyNSBFMTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABBv7UVHHb+ZVluxPXlfE
# 3M6tg0Xnq8dic+O3vPOCRpalUM1vO9A+GRzSVVjyygHhYrBw62XLFh1kv7e+yRd/
# aTajggFaMIIBVjAfBgNVHSMEGDAWgBQMnRAljpqnG5mHQ88IfuG9gZD0zzBRBggr
# BgEFBQcBAQRFMEMwQQYIKwYBBQUHMAKGNWh0dHA6Ly9jZXJ0LnNzbC5jb20vU1NM
# LmNvbS10aW1lU3RhbXBpbmctSS1SU0EtUjEuY2VyMFEGA1UdIARKMEgwPAYMKwYB
# BAGCqTABAwYBMCwwKgYIKwYBBQUHAgEWHmh0dHBzOi8vd3d3LnNzbC5jb20vcmVw
# b3NpdG9yeTAIBgZngQwBBAIwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwRgYDVR0f
# BD8wPTA7oDmgN4Y1aHR0cDovL2NybHMuc3NsLmNvbS9TU0wuY29tLXRpbWVTdGFt
# cGluZy1JLVJTQS1SMS5jcmwwHQYDVR0OBBYEFM582cAEgMUkEGoJ6hyrJT0R/ajS
# MA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAgHN1JIW9ZlNip07F
# fY9r1LJwLKTl1h/y9r1BjDgHMEz7U7J7mAy32JxA1KLyjCCJqNkuRfhDHGFEfZtj
# TOru7AQMDSDiUDvCCc3jaz5py9jGvwIe5npkLbXqXhA+SqCzG719USigwhHkYr7v
# 8NiXoaY0Xu03p0BiZZreZlpngmGB9+86N94FqIFx2I5Z5iuu7d4j7aAOZBNxfU9j
# FDIIGDtWfWeCpF0Q8rLE/3DHDlzJKs5Il3i/VoOl7rTU938oSrxMwww6GDBlKX8r
# GHxvkjgIb9MXkVglJSDHsNAZDeCS/inaxKHA+ChuB8K4OxIxa6k1e+eHBypakAY3
# wuN7w7PlPmhN6k5IdqZn3HZHs2VjoR841Z9wmVPFdGDdkBZ7XOmb5OZUaKufsvtE
# R0VbY2DkzaqAaItt1kc+I+FUz7PiVz2PUzpegkftRRxvrPyIL08blJTnbMQ0XrqD
# N0rEfuTLv4XhhnKyJaXhxCUdjO/cRumMzZ35QTUMItBWy1xGp0iyoFVnAya2pPsV
# MuAI+sy1zxlOS9l5iSxvKJ8gpnpOgjqYTa7u1eyZo+4JlcGoiiR17LthMTrF1q62
# tO5xOpI8txUZ6gtKGtkJV6wdj+vZLcwKZ2M93GqVpOo6UBlGjx1sKsVZ7AIUzJZl
# i4NJ0M2KYcrfNnlebkYKpDnSqWwwggb8MIIE5KADAgECAhBtUhhwh+gjTYVgANCA
# j5NWMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhh
# czEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTEw
# LwYDVQQDDChTU0wuY29tIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNB
# MB4XDTE5MTExMzE4NTAwNVoXDTM0MTExMjE4NTAwNVowczELMAkGA1UEBhMCVVMx
# DjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMREwDwYDVQQKDAhTU0wg
# Q29ycDEvMC0GA1UEAwwmU1NMLmNvbSBUaW1lc3RhbXBpbmcgSXNzdWluZyBSU0Eg
# Q0EgUjEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCuURAT0vk8IKAg
# hd7JUBxkyeH9xek0/wp/MUjoclrFXqhh/fGH91Fc+7fm0MHCE7A+wmOiqBj9ODrJ
# AYGq3rm33jCnHSsCBNWAQYyoauLq8IjqsS1JlXL29qDNMMdwZ8UNzQS7vWZMDJ40
# JSGNphMGTIA2qn2bohGtgRc4p1395ESypUOaGvJ3t0FNL3BuKmb6YctMcQUF2sqo
# oMzd89h0E6ujdvBDo6ZwNnWoxj7YmfWjSXg33A5GuY9ym4QZM5OEVgo8ebz/B+gy
# hyCLNNhh4Mb/4xvCTCMVmNYrBviGgdPZYrym8Zb84TQCmSuX0JlLLa6WK1aO6qlw
# ISbb9bVGh866ekKblC/XRP20gAu1CjvcYciUgNTrGFg8f8AJgQPOCc1/CCdaJSYw
# hJpSdheKOnQgESgNmYZPhFOC6IKaMAUXk5U1tjTcFCgFvvArXtK4azAWUOO1Y3fd
# ldIBL6LjkzLUCYJNkFXqhsBVcPMuB0nUDWvLJfPimstjJ8lF4S6ECxWnlWi7OElV
# wTnt1GtRqeY9ydvvGLntU+FecK7DbqHDUd366UreMkSBtzevAc9aqoZPnjVMjvFq
# V1pYOjzmTiVHZtAc80bAfFe5LLfJzPI6DntNyqobpwTevQpHqPDN9qqNO83r3kaw
# 8A9j+HZiSw2AX5cGdQP0kG0vhzfgBwIDAQABo4IBgTCCAX0wEgYDVR0TAQH/BAgw
# BgEB/wIBADAfBgNVHSMEGDAWgBTdBAkHovV6fVJTEpKV7jiAJQ2mWTCBgwYIKwYB
# BQUHAQEEdzB1MFEGCCsGAQUFBzAChkVodHRwOi8vd3d3LnNzbC5jb20vcmVwb3Np
# dG9yeS9TU0xjb21Sb290Q2VydGlmaWNhdGlvbkF1dGhvcml0eVJTQS5jcnQwIAYI
# KwYBBQUHMAGGFGh0dHA6Ly9vY3Nwcy5zc2wuY29tMD8GA1UdIAQ4MDYwNAYEVR0g
# ADAsMCoGCCsGAQUFBwIBFh5odHRwczovL3d3dy5zc2wuY29tL3JlcG9zaXRvcnkw
# EwYDVR0lBAwwCgYIKwYBBQUHAwgwOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2Ny
# bHMuc3NsLmNvbS9zc2wuY29tLXJzYS1Sb290Q0EuY3JsMB0GA1UdDgQWBBQMnRAl
# jpqnG5mHQ88IfuG9gZD0zzAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQAD
# ggIBAJIZdQ2mWkLPGQfZ8vyU+sCb8BXpRJZaL3Ez3VDlE3uZk3cPxPtybVfLuqac
# i0W6SB22JTMttCiQMnIVOsXWnIuAbD/aFTcUkTLBI3xys+wEajzXaXJYWACDS47B
# RjDtYlDW14gLJxf8W6DQoH3jHDGGy8kGJFOlDKG7/YrK7UGfHtBAEDVe6lyZ+FtC
# srk7dD/IiL/+Q3Q6SFASJLQ2XI89ihFugdYL77CiDNXrI2MFspQGswXEAGpHuaQD
# THUp/LdR3TyrIsLlnzoLskUGswF/KF8+kpWUiKJNC4rPWtNrxlbXYRGgdEdx8SMj
# UTDClldcrknlFxbqHsVmr9xkT2QtFmG+dEq1v5fsIK0vHaHrWjMMmaJ9i+4qGJSD
# 0stYfQ6v0PddT7EpGxGd867Ada6FZyHwbuQSadMb0K0P0OC2r7rwqBUe0BaMqTa6
# LWzWItgBjGcObXeMxmbQqlEz2YtAcErkZvh0WABDDE4U8GyV/32FdaAvJgTfe9Mi
# L2nSBioYe/g5mHUSWAay/Ip1RQmQCvmF9sNfqlhJwkjy/1U1ibUkTIUBX3HgymyQ
# vqQTZLLys6pL2tCdWcjI9YuLw30rgZm8+K387L7ycUvqrmQ3ZJlujHl3r1hgV76s
# 3WwMPgKk1bAEFMj+rRXimSC+Ev30hXZdqyMdl/il5Ksd0vhGMYICWTCCAlUCAQEw
# gYcwczELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3Vz
# dG9uMREwDwYDVQQKDAhTU0wgQ29ycDEvMC0GA1UEAwwmU1NMLmNvbSBUaW1lc3Rh
# bXBpbmcgSXNzdWluZyBSU0EgQ0EgUjECEB9rFmLSLhtsXfHAwaBgDjgwCwYJYIZI
# AWUDBAIBoIIBYTAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcN
# AQkFMQ8XDTI2MDYwMzE0MDk0OFowKAYJKoZIhvcNAQk0MRswGTALBglghkgBZQME
# AgGhCgYIKoZIzj0EAwIwLwYJKoZIhvcNAQkEMSIEIElHGuuKwbtCC+hqhCv3npE9
# hN9mgCGlyaL+QpBGiQzjMIHJBgsqhkiG9w0BCRACLzGBuTCBtjCBszCBsAQgVCr5
# oWqNci5mEUl4iumUwYqaruWmXLNEolSa+Wx5x4swgYswd6R1MHMxCzAJBgNVBAYT
# AlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwI
# U1NMIENvcnAxLzAtBgNVBAMMJlNTTC5jb20gVGltZXN0YW1waW5nIElzc3Vpbmcg
# UlNBIENBIFIxAhAfaxZi0i4bbF3xwMGgYA44MAoGCCqGSM49BAMCBEgwRgIhAOoF
# e0iwfpYXxt5eqNe+Nf7yjKmsFi4o3lqEz9i6Z3R4AiEA1TqgO23psYE0iBSfFigw
# k+9FuG9Yc2GtzYQH48+T0/E=
# SIG # End signature block