# -- # Copyright (C) 2001-2019 OTRS AG, https://otrs.com/ # -- # This software comes with ABSOLUTELY NO WARRANTY. For details, see # the enclosed file COPYING for license information (GPL). If you # did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. # -- package Kernel::System::Stats::Dynamic::TicketList; use strict; use warnings; use List::Util qw( first ); use Kernel::System::VariableCheck qw(:all); use Kernel::Language qw(Translatable); our @ObjectDependencies = ( 'Kernel::Config', 'Kernel::Language', 'Kernel::System::DB', 'Kernel::System::DynamicField', 'Kernel::System::DynamicField::Backend', 'Kernel::System::Lock', 'Kernel::System::Log', 'Kernel::System::Priority', 'Kernel::System::Queue', 'Kernel::System::Service', 'Kernel::System::SLA', 'Kernel::System::State', 'Kernel::System::Stats', 'Kernel::System::Ticket', 'Kernel::System::Ticket::Article', 'Kernel::System::DateTime', 'Kernel::System::Type', 'Kernel::System::User', 'Kernel::Output::HTML::Layout', ); sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # get the dynamic fields for ticket object $Self->{DynamicField} = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet( Valid => 1, ObjectType => ['Ticket'], ); return $Self; } sub GetObjectName { my ( $Self, %Param ) = @_; return 'Ticketlist'; } sub GetObjectBehaviours { my ( $Self, %Param ) = @_; my %Behaviours = ( ProvidesDashboardWidget => 0, ); return %Behaviours; } sub GetObjectAttributes { my ( $Self, %Param ) = @_; # get needed objects my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); my $UserObject = $Kernel::OM->Get('Kernel::System::User'); my $QueueObject = $Kernel::OM->Get('Kernel::System::Queue'); my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket'); my $StateObject = $Kernel::OM->Get('Kernel::System::State'); my $PriorityObject = $Kernel::OM->Get('Kernel::System::Priority'); my $LockObject = $Kernel::OM->Get('Kernel::System::Lock'); my $ValidAgent = 0; if ( defined $ConfigObject->Get('Stats::UseInvalidAgentInStats') && ( $ConfigObject->Get('Stats::UseInvalidAgentInStats') == 0 ) ) { $ValidAgent = 1; } # Get user list without the out of office message, because of the caching in the statistics # and not meaningful with a date selection. my %UserList = $UserObject->UserList( Type => 'Long', Valid => $ValidAgent, NoOutOfOffice => 1, ); # get state list my %StateList = $StateObject->StateList( UserID => 1, ); # get state type list my %StateTypeList = $StateObject->StateTypeList( UserID => 1, ); # get queue list my %QueueList = $QueueObject->GetAllQueues(); # get priority list my %PriorityList = $PriorityObject->PriorityList( UserID => 1, ); # get lock list my %LockList = $LockObject->LockList( UserID => 1, ); my %Limit = ( 5 => 5, 10 => 10, 20 => 20, 50 => 50, 100 => 100, unlimited => Translatable('unlimited'), ); my %TicketAttributes = %{ $Self->_TicketAttributes() }; my %OrderBy = map { $_ => $TicketAttributes{$_} } grep { $_ ne 'Number' } keys %TicketAttributes; # get dynamic field backend object my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend'); # remove non sortable (and orderable) Dynamic Fields DYNAMICFIELD: for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); next DYNAMICFIELD if !$DynamicFieldConfig->{Name}; # check if dynamic field is sortable my $IsSortable = $DynamicFieldBackendObject->HasBehavior( DynamicFieldConfig => $DynamicFieldConfig, Behavior => 'IsSortable', ); # remove dynamic fields from the list if is not sortable if ( !$IsSortable ) { delete $OrderBy{ 'DynamicField_' . $DynamicFieldConfig->{Name} }; } } my %SortSequence = ( Up => Translatable('ascending'), Down => Translatable('descending'), ); my @ObjectAttributes = ( { Name => Translatable('Attributes to be printed'), UseAsXvalue => 1, UseAsValueSeries => 0, UseAsRestriction => 0, Element => 'TicketAttributes', Block => 'MultiSelectField', Translation => 1, Values => \%TicketAttributes, Sort => 'IndividualKey', SortIndividual => $Self->_SortedAttributes(), }, { Name => Translatable('Order by'), UseAsXvalue => 0, UseAsValueSeries => 1, UseAsRestriction => 0, Element => 'OrderBy', Block => 'SelectField', Translation => 1, Values => \%OrderBy, Sort => 'IndividualKey', SortIndividual => $Self->_SortedAttributes(), }, { Name => Translatable('Sort sequence'), UseAsXvalue => 0, UseAsValueSeries => 1, UseAsRestriction => 0, Element => 'SortSequence', Block => 'SelectField', Translation => 1, Values => \%SortSequence, }, { Name => Translatable('Limit'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'Limit', Block => 'SelectField', Translation => 1, Values => \%Limit, Sort => 'IndividualKey', SortIndividual => [ '5', '10', '20', '50', '100', 'unlimited', ], }, { Name => Translatable('Queue'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'QueueIDs', Block => 'MultiSelectField', Translation => 0, TreeView => 1, Values => \%QueueList, }, { Name => Translatable('State'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'StateIDs', Block => 'MultiSelectField', Values => \%StateList, }, { Name => Translatable('State Historic'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'StateIDsHistoric', Block => 'MultiSelectField', Values => \%StateList, }, { Name => Translatable('State Type'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'StateTypeIDs', Block => 'MultiSelectField', Values => \%StateTypeList, }, { Name => Translatable('State Type Historic'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'StateTypeIDsHistoric', Block => 'MultiSelectField', Values => \%StateTypeList, }, { Name => Translatable('Priority'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'PriorityIDs', Block => 'MultiSelectField', Values => \%PriorityList, }, { Name => Translatable('Created in Queue'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CreatedQueueIDs', Block => 'MultiSelectField', Translation => 0, TreeView => 1, Values => \%QueueList, }, { Name => Translatable('Created Priority'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CreatedPriorityIDs', Block => 'MultiSelectField', Values => \%PriorityList, }, { Name => Translatable('Created State'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CreatedStateIDs', Block => 'MultiSelectField', Values => \%StateList, }, { Name => Translatable('Lock'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'LockIDs', Block => 'MultiSelectField', Values => \%LockList, }, { Name => Translatable('Title'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'Title', Block => 'InputField', }, { Name => Translatable('From'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'MIMEBase_From', Block => 'InputField', }, { Name => Translatable('To'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'MIMEBase_To', Block => 'InputField', }, { Name => Translatable('Cc'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'MIMEBase_Cc', Block => 'InputField', }, { Name => Translatable('Subject'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'MIMEBase_Subject', Block => 'InputField', }, { Name => Translatable('Text'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'MIMEBase_Body', Block => 'InputField', }, { Name => Translatable('Create Time'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CreateTime', TimePeriodFormat => 'DateInputFormat', # 'DateInputFormatLong', Block => 'Time', Values => { TimeStart => 'TicketCreateTimeNewerDate', TimeStop => 'TicketCreateTimeOlderDate', }, }, { Name => Translatable('Last changed times'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'LastChangeTime', TimePeriodFormat => 'DateInputFormat', # 'DateInputFormatLong', Block => 'Time', Values => { TimeStart => 'TicketLastChangeTimeNewerDate', TimeStop => 'TicketLastChangeTimeOlderDate', }, }, { Name => Translatable('Change times'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'ChangeTime', TimePeriodFormat => 'DateInputFormat', # 'DateInputFormatLong', Block => 'Time', Values => { TimeStart => 'TicketChangeTimeNewerDate', TimeStop => 'TicketChangeTimeOlderDate', }, }, { Name => Translatable('Pending until time'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'PendingUntilTime', TimePeriodFormat => 'DateInputFormat', # 'DateInputFormatLong', Block => 'Time', Values => { TimeStart => 'TicketPendingTimeNewerDate', TimeStop => 'TicketPendingTimeOlderDate', }, }, { Name => Translatable('Close Time'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CloseTime', TimePeriodFormat => 'DateInputFormat', # 'DateInputFormatLong', Block => 'Time', Values => { TimeStart => 'TicketCloseTimeNewerDate', TimeStop => 'TicketCloseTimeOlderDate', }, }, { Name => Translatable('Historic Time Range'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'HistoricTimeRange', TimePeriodFormat => 'DateInputFormat', # 'DateInputFormatLong', Block => 'Time', Values => { TimeStart => 'HistoricTimeRangeTimeNewerDate', TimeStop => 'HistoricTimeRangeTimeOlderDate', }, }, { Name => Translatable('Escalation'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'EscalationTime', TimePeriodFormat => 'DateInputFormatLong', # 'DateInputFormat', Block => 'Time', Values => { TimeStart => 'TicketEscalationTimeNewerDate', TimeStop => 'TicketEscalationTimeOlderDate', }, }, { Name => Translatable('Escalation - First Response Time'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'EscalationResponseTime', TimePeriodFormat => 'DateInputFormatLong', # 'DateInputFormat', Block => 'Time', Values => { TimeStart => 'TicketEscalationResponseTimeNewerDate', TimeStop => 'TicketEscalationResponseTimeOlderDate', }, }, { Name => Translatable('Escalation - Update Time'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'EscalationUpdateTime', TimePeriodFormat => 'DateInputFormatLong', # 'DateInputFormat', Block => 'Time', Values => { TimeStart => 'TicketEscalationUpdateTimeNewerDate', TimeStop => 'TicketEscalationUpdateTimeOlderDate', }, }, { Name => Translatable('Escalation - Solution Time'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'EscalationSolutionTime', TimePeriodFormat => 'DateInputFormatLong', # 'DateInputFormat', Block => 'Time', Values => { TimeStart => 'TicketEscalationSolutionTimeNewerDate', TimeStop => 'TicketEscalationSolutionTimeOlderDate', }, }, ); if ( $ConfigObject->Get('Ticket::Service') ) { # get service list my %Service = $Kernel::OM->Get('Kernel::System::Service')->ServiceList( KeepChildren => $ConfigObject->Get('Ticket::Service::KeepChildren'), UserID => 1, ); # get sla list my %SLA = $Kernel::OM->Get('Kernel::System::SLA')->SLAList( UserID => 1, ); my @ObjectAttributeAdd = ( { Name => Translatable('Service'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'ServiceIDs', Block => 'MultiSelectField', Translation => 0, TreeView => 1, Values => \%Service, }, { Name => Translatable('SLA'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'SLAIDs', Block => 'MultiSelectField', Translation => 0, Values => \%SLA, }, ); unshift @ObjectAttributes, @ObjectAttributeAdd; } if ( $ConfigObject->Get('Ticket::Type') ) { # get ticket type list my %Type = $Kernel::OM->Get('Kernel::System::Type')->TypeList( UserID => 1, ); my %ObjectAttribute1 = ( Name => Translatable('Type'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'TypeIDs', Block => 'MultiSelectField', Translation => 0, Values => \%Type, ); unshift @ObjectAttributes, \%ObjectAttribute1; } if ( $ConfigObject->Get('Stats::UseAgentElementInStats') ) { my @ObjectAttributeAdd = ( { Name => Translatable('Agent/Owner'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'OwnerIDs', Block => 'MultiSelectField', Translation => 0, Values => \%UserList, }, { Name => Translatable('Created by Agent/Owner'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CreatedUserIDs', Block => 'MultiSelectField', Translation => 0, Values => \%UserList, }, { Name => Translatable('Responsible'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'ResponsibleIDs', Block => 'MultiSelectField', Translation => 0, Values => \%UserList, }, ); push @ObjectAttributes, @ObjectAttributeAdd; } my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); if ( $ConfigObject->Get('Stats::CustomerIDAsMultiSelect') ) { # Get CustomerID # (This way also can be the solution for the CustomerUserID) $DBObject->Prepare( SQL => "SELECT DISTINCT customer_id FROM ticket", ); # fetch the result my %CustomerID; while ( my @Row = $DBObject->FetchrowArray() ) { if ( $Row[0] ) { $CustomerID{ $Row[0] } = $Row[0]; } } my %ObjectAttribute = ( Name => Translatable('Customer ID'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CustomerID', Block => 'MultiSelectField', Values => \%CustomerID, ); push @ObjectAttributes, \%ObjectAttribute; } else { my @CustomerIDAttributes = ( { Name => Translatable('CustomerID (complex search)'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CustomerID', Block => 'InputField', }, { Name => Translatable('CustomerID (exact match)'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CustomerIDRaw', Block => 'InputField', }, ); push @ObjectAttributes, @CustomerIDAttributes; } if ( $ConfigObject->Get('Stats::CustomerUserLoginsAsMultiSelect') ) { # Get all CustomerUserLogins which are related to a tiket. $DBObject->Prepare( SQL => "SELECT DISTINCT customer_user_id FROM ticket", ); # fetch the result my %CustomerUserIDs; while ( my @Row = $DBObject->FetchrowArray() ) { if ( $Row[0] ) { $CustomerUserIDs{ $Row[0] } = $Row[0]; } } my %ObjectAttribute = ( Name => Translatable('Assigned to Customer User Login'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CustomerUserLoginRaw', Block => 'MultiSelectField', Values => \%CustomerUserIDs, ); push @ObjectAttributes, \%ObjectAttribute; } else { my @CustomerUserAttributes = ( { Name => Translatable('Assigned to Customer User Login (complex search)'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CustomerUserLogin', Block => 'InputField', }, { Name => Translatable('Assigned to Customer User Login (exact match)'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CustomerUserLoginRaw', Block => 'InputField', CSSClass => 'CustomerAutoCompleteSimple', HTMLDataAttributes => { 'customer-search-type' => 'CustomerUser', }, }, ); push @ObjectAttributes, @CustomerUserAttributes; } # Add always the field for the customer user login accessible tickets as auto complete field. my %ObjectAttribute = ( Name => Translatable('Accessible to Customer User Login (exact match)'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'CustomerUserID', Block => 'InputField', CSSClass => 'CustomerAutoCompleteSimple', HTMLDataAttributes => { 'customer-search-type' => 'CustomerUser', }, ); push @ObjectAttributes, \%ObjectAttribute; if ( $ConfigObject->Get('Ticket::ArchiveSystem') ) { my %ObjectAttribute = ( Name => Translatable('Archive Search'), UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => 'SearchInArchive', Block => 'SelectField', Translation => 1, Values => { ArchivedTickets => Translatable('Archived tickets'), NotArchivedTickets => Translatable('Unarchived tickets'), AllTickets => Translatable('All tickets'), }, ); push @ObjectAttributes, \%ObjectAttribute; } # cycle trough the activated Dynamic Fields for this screen DYNAMICFIELD: for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); # skip all fields not designed to be supported by statistics my $IsStatsCondition = $DynamicFieldBackendObject->HasBehavior( DynamicFieldConfig => $DynamicFieldConfig, Behavior => 'IsStatsCondition', ); next DYNAMICFIELD if !$IsStatsCondition; my $PossibleValuesFilter; my $IsACLReducible = $DynamicFieldBackendObject->HasBehavior( DynamicFieldConfig => $DynamicFieldConfig, Behavior => 'IsACLReducible', ); if ($IsACLReducible) { # get PossibleValues my $PossibleValues = $DynamicFieldBackendObject->PossibleValuesGet( DynamicFieldConfig => $DynamicFieldConfig, ); # convert possible values key => value to key => key for ACLs using a Hash slice my %AclData = %{ $PossibleValues || {} }; @AclData{ keys %AclData } = keys %AclData; # set possible values filter from ACLs my $ACL = $TicketObject->TicketAcl( Action => 'AgentStats', Type => 'DynamicField_' . $DynamicFieldConfig->{Name}, ReturnType => 'Ticket', ReturnSubType => 'DynamicField_' . $DynamicFieldConfig->{Name}, Data => \%AclData || {}, UserID => 1, ); if ($ACL) { my %Filter = $TicketObject->TicketAclData(); # convert Filer key => key back to key => value using map %{$PossibleValuesFilter} = map { $_ => $PossibleValues->{$_} } keys %Filter; } } # get dynamic field stats parameters my $DynamicFieldStatsParameter = $DynamicFieldBackendObject->StatsFieldParameterBuild( DynamicFieldConfig => $DynamicFieldConfig, PossibleValuesFilter => $PossibleValuesFilter, ); if ( IsHashRefWithData($DynamicFieldStatsParameter) ) { # backward compatibility if ( !$DynamicFieldStatsParameter->{Block} ) { $DynamicFieldStatsParameter->{Block} = 'InputField'; if ( IsHashRefWithData( $DynamicFieldStatsParameter->{Values} ) ) { $DynamicFieldStatsParameter->{Block} = 'MultiSelectField'; } } if ( $DynamicFieldStatsParameter->{Block} eq 'Time' ) { # create object attributes (date/time fields) my $TimePeriodFormat = $DynamicFieldStatsParameter->{TimePeriodFormat} || 'DateInputFormatLong'; my %ObjectAttribute = ( Name => $DynamicFieldStatsParameter->{Name}, UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => $DynamicFieldStatsParameter->{Element}, TimePeriodFormat => $TimePeriodFormat, Block => $DynamicFieldStatsParameter->{Block}, TimePeriodFormat => $TimePeriodFormat, Values => { TimeStart => $DynamicFieldStatsParameter->{Element} . '_GreaterThanEquals', TimeStop => $DynamicFieldStatsParameter->{Element} . '_SmallerThanEquals', }, ); push @ObjectAttributes, \%ObjectAttribute; } elsif ( $DynamicFieldStatsParameter->{Block} eq 'MultiSelectField' ) { # create object attributes (multiple values) my %ObjectAttribute = ( Name => $DynamicFieldStatsParameter->{Name}, UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => $DynamicFieldStatsParameter->{Element}, Block => $DynamicFieldStatsParameter->{Block}, Values => $DynamicFieldStatsParameter->{Values}, Translation => $DynamicFieldStatsParameter->{TranslatableValues} || 0, IsDynamicField => 1, ShowAsTree => $DynamicFieldConfig->{Config}->{TreeView} || 0, ); push @ObjectAttributes, \%ObjectAttribute; } else { # create object attributes (text fields) my %ObjectAttribute = ( Name => $DynamicFieldStatsParameter->{Name}, UseAsXvalue => 0, UseAsValueSeries => 0, UseAsRestriction => 1, Element => $DynamicFieldStatsParameter->{Element}, Block => $DynamicFieldStatsParameter->{Block}, ); push @ObjectAttributes, \%ObjectAttribute; } } } return @ObjectAttributes; } sub GetStatTablePreview { my ( $Self, %Param ) = @_; return $Self->GetStatTable( %Param, Preview => 1, ); } sub GetStatTable { my ( $Self, %Param ) = @_; my %TicketAttributes = map { $_ => 1 } @{ $Param{XValue}{SelectedValues} }; my $SortedAttributesRef = $Self->_SortedAttributes(); my $Preview = $Param{Preview}; my $DBObject = $Kernel::OM->Get('Kernel::System::DB'); # check if a enumeration is requested my $AddEnumeration = 0; if ( $TicketAttributes{Number} ) { $AddEnumeration = 1; delete $TicketAttributes{Number}; } # set default values if no sort or order attribute is given my $OrderRef = first { $_->{Element} eq 'OrderBy' } @{ $Param{ValueSeries} }; my $OrderBy = $OrderRef ? $OrderRef->{SelectedValues}[0] : 'Age'; my $SortRef = first { $_->{Element} eq 'SortSequence' } @{ $Param{ValueSeries} }; my $Sort = $SortRef ? $SortRef->{SelectedValues}[0] : 'Down'; my $Limit = $Param{Restrictions}{Limit}; # check if we can use the sort and order function of TicketSearch my $OrderByIsValueOfTicketSearchSort = $Self->_OrderByIsValueOfTicketSearchSort( OrderBy => $OrderBy, ); # escape search attributes for ticket search my %AttributesToEscape = ( 'CustomerID' => 1, 'Title' => 1, ); # Map the CustomerID search parameter to CustomerIDRaw search parameter for the # exact search match, if the 'Stats::CustomerIDAsMultiSelect' is active. if ( $Kernel::OM->Get('Kernel::Config')->Get('Stats::CustomerIDAsMultiSelect') ) { $Param{Restrictions}->{CustomerIDRaw} = $Param{Restrictions}->{CustomerID}; } ATTRIBUTE: for my $Key ( sort keys %{ $Param{Restrictions} } ) { next ATTRIBUTE if !$AttributesToEscape{$Key}; if ( ref $Param{Restrictions}->{$Key} ) { if ( ref $Param{Restrictions}->{$Key} eq 'ARRAY' ) { $Param{Restrictions}->{$Key} = [ map { $DBObject->QueryStringEscape( QueryString => $_ ) } @{ $Param{Restrictions}->{$Key} } ]; } } else { $Param{Restrictions}->{$Key} = $DBObject->QueryStringEscape( QueryString => $Param{Restrictions}->{$Key} ); } } # get dynamic field backend object my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend'); my %DynamicFieldRestrictions; for my $ParameterName ( sort keys %{ $Param{Restrictions} } ) { if ( $ParameterName =~ m{ \A DynamicField_ ( [a-zA-Z\d]+ ) (?: _ ( [a-zA-Z\d]+ ) )? \z }xms ) { my $FieldName = $1; my $Operator = $2; # loop over the dynamic fields configured DYNAMICFIELD: for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); next DYNAMICFIELD if !$DynamicFieldConfig->{Name}; # skip all fields that do not match with current field name # without the 'DynamicField_' prefix next DYNAMICFIELD if $DynamicFieldConfig->{Name} ne $FieldName; # skip all fields not designed to be supported by statistics my $IsStatsCondition = $DynamicFieldBackendObject->HasBehavior( DynamicFieldConfig => $DynamicFieldConfig, Behavior => 'IsStatsCondition', ); next DYNAMICFIELD if !$IsStatsCondition; # get new search parameter my $DynamicFieldStatsSearchParameter = $DynamicFieldBackendObject->StatsSearchFieldParameterBuild( DynamicFieldConfig => $DynamicFieldConfig, Value => $Param{Restrictions}->{$ParameterName}, Operator => $Operator, ); # add new search parameter if ( !IsHashRefWithData( $DynamicFieldRestrictions{"DynamicField_$FieldName"} ) ) { $DynamicFieldRestrictions{"DynamicField_$FieldName"} = $DynamicFieldStatsSearchParameter; } # extend search parameter elsif ( IsHashRefWithData($DynamicFieldStatsSearchParameter) ) { $DynamicFieldRestrictions{"DynamicField_$FieldName"} = { %{ $DynamicFieldRestrictions{"DynamicField_$FieldName"} }, %{$DynamicFieldStatsSearchParameter}, }; } } } } if ($OrderByIsValueOfTicketSearchSort) { # don't be irritated of the mixture OrderBy <> Sort and SortBy <> OrderBy # the meaning is in TicketSearch is different as in common handling $Param{Restrictions}{OrderBy} = $Sort; $Param{Restrictions}{SortBy} = $OrderByIsValueOfTicketSearchSort; $Param{Restrictions}{Limit} = !$Limit || $Limit eq 'unlimited' ? 100_000_000 : $Limit; } else { $Param{Restrictions}{Limit} = 100_000_000; } # get state object my $StateObject = $Kernel::OM->Get('Kernel::System::State'); # OlderTicketsExclude for historic searches # takes tickets that were closed before the # start of the searched time periode my %OlderTicketsExclude; # NewerTicketExclude for historic searches # takes tickets that were created after the # searched time periode my %NewerTicketsExclude; my %StateList = $StateObject->StateList( UserID => 1 ); # get time object my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime'); # UnixTimeStart & End: # The Time periode the historic search is executed # if no time periode has been selected we take # Unixtime 0 as StartTime and SystemTime as EndTime my $UnixTimeStart = 0; my $UnixTimeEnd = $DateTimeObject->ToEpoch(); if ( $Kernel::OM->Get('Kernel::Config')->Get('Ticket::ArchiveSystem') ) { $Param{Restrictions}->{SearchInArchive} ||= ''; if ( $Param{Restrictions}->{SearchInArchive} eq 'AllTickets' ) { $Param{Restrictions}->{ArchiveFlags} = [ 'y', 'n' ]; } elsif ( $Param{Restrictions}->{SearchInArchive} eq 'ArchivedTickets' ) { $Param{Restrictions}->{ArchiveFlags} = ['y']; } else { $Param{Restrictions}->{ArchiveFlags} = ['n']; } } # get ticket object my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket'); if ( !$Preview && $Param{Restrictions}->{HistoricTimeRangeTimeNewerDate} ) { # Find tickets that were closed before the start of our # HistoricTimeRangeTimeNewerDate, these have to be excluded. # In order to reduce it quickly we reformat the result array # to a hash. my @OldToExclude = $TicketObject->TicketSearch( UserID => 1, Result => 'ARRAY', Permission => 'ro', TicketCloseTimeOlderDate => $Param{Restrictions}->{HistoricTimeRangeTimeNewerDate}, ArchiveFlags => $Param{Restrictions}->{ArchiveFlags}, Limit => 100_000_000, ); %OlderTicketsExclude = map { $_ => 1 } @OldToExclude; $DateTimeObject->Set( String => $Param{Restrictions}->{HistoricTimeRangeTimeNewerDate} ); $UnixTimeStart = $DateTimeObject->ToEpoch(); } if ( !$Preview && $Param{Restrictions}->{HistoricTimeRangeTimeOlderDate} ) { # Find tickets that were closed after the end of our # HistoricTimeRangeTimeOlderDate, these have to be excluded # in order to reduce it quickly we reformat the result array # to a hash. my @NewToExclude = $TicketObject->TicketSearch( UserID => 1, Result => 'ARRAY', Permission => 'ro', TicketCreateTimeNewerDate => $Param{Restrictions}->{HistoricTimeRangeTimeOlderDate}, ArchiveFlags => $Param{Restrictions}->{ArchiveFlags}, Limit => 100_000_000, ); %NewerTicketsExclude = map { $_ => 1 } @NewToExclude; $DateTimeObject->Set( String => $Param{Restrictions}->{HistoricTimeRangeTimeOlderDate} ); $UnixTimeEnd = $DateTimeObject->ToEpoch(); } # get the involved tickets my @TicketIDs; if ($Preview) { @TicketIDs = $TicketObject->TicketSearch( UserID => 1, Result => 'ARRAY', Permission => 'ro', Limit => 10, ); } else { @TicketIDs = $TicketObject->TicketSearch( UserID => 1, Result => 'ARRAY', Permission => 'ro', %{ $Param{Restrictions} }, %DynamicFieldRestrictions, ); } # if we had Tickets we need to reduce the found tickets # to those not beeing in %OlderTicketsExclude # as well as not in %NewerTicketsExclude if ( %OlderTicketsExclude || %NewerTicketsExclude ) { @TicketIDs = grep { !defined $OlderTicketsExclude{$_} && !defined $NewerTicketsExclude{$_} } @TicketIDs; } # if we have to deal with history states if ( !$Preview && ( $Param{Restrictions}->{HistoricTimeRangeTimeNewerDate} || $Param{Restrictions}->{HistoricTimeRangeTimeOlderDate} || ( defined $Param{Restrictions}->{StateTypeIDsHistoric} && ref $Param{Restrictions}->{StateTypeIDsHistoric} eq 'ARRAY' ) || ( defined $Param{Restrictions}->{StateIDsHistoric} && ref $Param{Restrictions}->{StateIDsHistoric} eq 'ARRAY' ) ) && @TicketIDs ) { my $SQLTicketIDInCondition = $DBObject->QueryInCondition( Key => 'ticket_id', Values => \@TicketIDs, QuoteType => 'Integer', BindMode => 0, ); # start building the SQL query from back to front # what's fixed is the history_type_id we have to search for # 1 is ticketcreate # 27 is state update my $SQL = $SQLTicketIDInCondition . ' AND history_type_id IN (1,27) ORDER BY ticket_id ASC'; my %StateIDs; # if we have certain state types we have to search for # build a hash holding all ticket StateIDs => StateNames # we are searching for if ( defined $Param{Restrictions}->{StateTypeIDsHistoric} && ref $Param{Restrictions}->{StateTypeIDsHistoric} eq 'ARRAY' ) { # getting the StateListType: # my %ListType = ( # 1 => "new", # 2 => "open", # 3 => "closed", # 4 => "pending reminder", # 5 => "pending auto", # 6 => "removed", # 7 => "merged", # ); my %ListType = $StateObject->StateTypeList( UserID => 1, ); # Takes the Array of StateTypeID's # example: (1, 3, 5, 6, 7) # maps the ID's to the StateTypeNames # results in a Hash containing the StateTypeNames # example: # %StateTypeHash = { # 'closed' => 1, # 'removed' => 1, # 'pending auto' => 1, # 'merged' => 1, # 'new' => 1 # }; my %StateTypeHash = map { $ListType{$_} => 1 } @{ $Param{Restrictions}->{StateTypeIDsHistoric} }; # And now get the StatesByType # Result is a Hash {ID => StateName,} my @StateTypes = keys %StateTypeHash; %StateIDs = $StateObject->StateGetStatesByType( StateType => [ keys %StateTypeHash ], Result => 'HASH', ); } # if we had certain states selected, add them to the # StateIDs Hash if ( defined $Param{Restrictions}->{StateIDsHistoric} && ref $Param{Restrictions}->{StateIDsHistoric} eq 'ARRAY' ) { # Validate the StateIDsHistoric list by # checking if they are in the %StateList hash # then taking all ValidState ID's and return a hash # holding { StateID => Name } my %Tmp = map { $_ => $StateList{$_} } grep { $StateList{$_} } @{ $Param{Restrictions}->{StateIDsHistoric} }; %StateIDs = ( %StateIDs, %Tmp ); } $SQL = 'SELECT ticket_id, state_id, create_time FROM ticket_history WHERE ' . $SQL; $DBObject->Prepare( SQL => $SQL ); # Structure: # Stores the last TicketState: # TicketID => [StateID, CreateTime] my %FoundTickets; # fetch the result while ( my @Row = $DBObject->FetchrowArray() ) { if ( $Row[0] ) { my $TicketID = $Row[0]; my $StateID = $Row[1]; $DateTimeObject->Set( String => $Row[2] ); my $RowTimeUnix = $DateTimeObject->ToEpoch(); # Entries before StartTime if ( $RowTimeUnix < $UnixTimeStart ) { # if the ticket was already stored if ( $FoundTickets{$TicketID} ) { # if the current state is in the searched states # update the record if ( $StateIDs{$StateID} ) { $FoundTickets{$TicketID} = [ $StateID, $RowTimeUnix ]; } # if it is not in the searched states # a state change happend -> # delete the record else { delete $FoundTickets{$TicketID}; } } # if the ticket was NOT already stored # and the state is in the searched states # store the record elsif ( $StateIDs{$StateID} ) { $FoundTickets{$TicketID} = [ $StateID, $RowTimeUnix ]; } } # Entries between Start and EndTime if ( $RowTimeUnix >= $UnixTimeStart && $RowTimeUnix <= $UnixTimeEnd ) { # if we found a record # with the searched states # add it to the FoundTickets if ( $StateIDs{$StateID} ) { $FoundTickets{$TicketID} = [ $StateID, $RowTimeUnix ]; } } } } # if we had tickets that matched our query # use them to get the details for the statistic if (%FoundTickets) { @TicketIDs = sort { $a <=> $b } keys %FoundTickets; } # if no Tickets were remaining, # after reducing the total amount by the ones # that had none of the searched states, # empty @TicketIDs else { @TicketIDs = (); } } # find out if the extended version of TicketGet is needed, my $Extended = $Self->_ExtendedAttributesCheck( TicketAttributes => \%TicketAttributes, ); # find out if dynamic fields are required my $NeedDynamicFields = 0; DYNAMICFIELDSNEEDED: for my $ParameterName ( sort keys %TicketAttributes ) { if ( $ParameterName =~ m{\A DynamicField_ }xms ) { $NeedDynamicFields = 1; last DYNAMICFIELDSNEEDED; } } my $StatsObject = $Kernel::OM->Get('Kernel::System::Stats'); # generate the ticket list my @StatArray; for my $TicketID (@TicketIDs) { my @ResultRow; my %Ticket = $TicketObject->TicketGet( TicketID => $TicketID, UserID => 1, Extended => $Extended, DynamicFields => $NeedDynamicFields, ); # Format Ticket 'Age' param into human readable format. $Ticket{Age} = $StatsObject->_HumanReadableAgeGet( Age => $Ticket{Age}, ); # add the accounted time if needed if ( $TicketAttributes{AccountedTime} ) { $Ticket{AccountedTime} = $TicketObject->TicketAccountedTimeGet( TicketID => $TicketID ); } # add the number of articles if needed if ( $TicketAttributes{NumberOfArticles} ) { $Ticket{NumberOfArticles} = $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleList( TicketID => $TicketID, UserID => 1 ); } $Ticket{Closed} ||= ''; $Ticket{SolutionTime} ||= ''; $Ticket{SolutionDiffInMin} ||= 0; $Ticket{SolutionInMin} ||= 0; $Ticket{SolutionTimeEscalation} ||= 0; $Ticket{FirstResponse} ||= ''; $Ticket{FirstResponseDiffInMin} ||= 0; $Ticket{FirstResponseInMin} ||= 0; $Ticket{FirstResponseTimeEscalation} ||= 0; $Ticket{FirstLock} ||= ''; $Ticket{UpdateTimeDestinationDate} ||= ''; $Ticket{UpdateTimeDestinationTime} ||= 0; $Ticket{UpdateTimeWorkingTime} ||= 0; $Ticket{UpdateTimeEscalation} ||= 0; $Ticket{SolutionTimeDestinationDate} ||= ''; $Ticket{EscalationDestinationIn} ||= ''; $Ticket{EscalationDestinationDate} ||= ''; $Ticket{EscalationTimeWorkingTime} ||= 0; $Ticket{NumberOfArticles} ||= 0; for my $ParameterName ( sort keys %Ticket ) { if ( $ParameterName =~ m{\A DynamicField_ ( [a-zA-Z\d]+ ) \z}xms ) { # loop over the dynamic fields configured DYNAMICFIELD: for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); next DYNAMICFIELD if !$DynamicFieldConfig->{Name}; # skip all fields that does not match with current field name ($1) # without the 'DynamicField_' prefix next DYNAMICFIELD if $DynamicFieldConfig->{Name} ne $1; # prevent unitilization errors if ( !defined $Ticket{$ParameterName} ) { $Ticket{$ParameterName} = ''; next DYNAMICFIELD; } # convert from stored keys to values for certain Dynamic Fields like # Dropdown, Checkbox and Multiselect my $ValueLookup = $DynamicFieldBackendObject->ValueLookup( DynamicFieldConfig => $DynamicFieldConfig, Key => $Ticket{$ParameterName}, ); # get field value in plain text my $ValueStrg = $DynamicFieldBackendObject->ReadableValueRender( DynamicFieldConfig => $DynamicFieldConfig, Value => $ValueLookup, ); if ( $ValueStrg->{Value} ) { # change raw value from ticket to a plain text value $Ticket{$ParameterName} = $ValueStrg->{Value}; ## nofilter(TidyAll::Plugin::OTRS::Perl::LayoutObject) if ( $DynamicFieldConfig->{Name} =~ /ProcessManagementProcessID|ProcessManagementActivityID/ ) { my $DisplayValue = $DynamicFieldBackendObject->DisplayValueRender( DynamicFieldConfig => $DynamicFieldConfig, Value => $ValueStrg->{Value}, ValueMaxChars => 20, LayoutObject => $Kernel::OM->Get('Kernel::Output::HTML::Layout'), HTMLOutput => 0, ); $Ticket{$ParameterName} = $DisplayValue->{Value}; } } } } } ATTRIBUTE: for my $Attribute ( @{$SortedAttributesRef} ) { next ATTRIBUTE if !$TicketAttributes{$Attribute}; # convert from OTRS time zone to given time zone if ( $Param{TimeZone} && $Param{TimeZone} ne Kernel::System::DateTime->OTRSTimeZoneGet() && $Ticket{$Attribute} && $Ticket{$Attribute} =~ /\A(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2}):(\d{2})\z/ ) { $Ticket{$Attribute} = $StatsObject->_FromOTRSTimeZone( String => $Ticket{$Attribute}, TimeZone => $Param{TimeZone}, ); $Ticket{$Attribute} .= " ($Param{TimeZone})"; } if ( $Attribute eq 'Owner' || $Attribute eq 'Responsible' ) { $Ticket{$Attribute} = $Kernel::OM->Get('Kernel::System::User')->UserName( User => $Ticket{$Attribute}, NoOutOfOffice => 1, ); } push @ResultRow, $Ticket{$Attribute}; } push @StatArray, \@ResultRow; } # use a individual sort if the sort mechanism of the TicketSearch is not usable if ( !$OrderByIsValueOfTicketSearchSort ) { @StatArray = $Self->_IndividualResultOrder( StatArray => \@StatArray, OrderBy => $OrderBy, Sort => $Sort, SelectedAttributes => \%TicketAttributes, Limit => $Limit, ); } # add a enumeration in front of each row if ($AddEnumeration) { my $Counter = 0; for my $Row (@StatArray) { unshift @{$Row}, ++$Counter; } } return @StatArray; } sub GetHeaderLine { my ( $Self, %Param ) = @_; my %SelectedAttributes = map { $_ => 1 } @{ $Param{XValue}{SelectedValues} }; my $TicketAttributes = $Self->_TicketAttributes(); my $SortedAttributesRef = $Self->_SortedAttributes(); my @HeaderLine; # get language object my $LanguageObject = $Kernel::OM->Get('Kernel::Language'); ATTRIBUTE: for my $Attribute ( @{$SortedAttributesRef} ) { next ATTRIBUTE if !$SelectedAttributes{$Attribute}; push @HeaderLine, $LanguageObject->Translate( $TicketAttributes->{$Attribute} ); } return \@HeaderLine; } sub ExportWrapper { my ( $Self, %Param ) = @_; # get needed objects my $UserObject = $Kernel::OM->Get('Kernel::System::User'); my $QueueObject = $Kernel::OM->Get('Kernel::System::Queue'); my $StateObject = $Kernel::OM->Get('Kernel::System::State'); my $PriorityObject = $Kernel::OM->Get('Kernel::System::Priority'); # wrap ids to used spelling for my $Use (qw(UseAsValueSeries UseAsRestriction UseAsXvalue)) { ELEMENT: for my $Element ( @{ $Param{$Use} } ) { next ELEMENT if !$Element || !$Element->{SelectedValues}; my $ElementName = $Element->{Element}; my $Values = $Element->{SelectedValues}; if ( $ElementName eq 'QueueIDs' || $ElementName eq 'CreatedQueueIDs' ) { ID: for my $ID ( @{$Values} ) { next ID if !$ID; $ID->{Content} = $QueueObject->QueueLookup( QueueID => $ID->{Content} ); } } elsif ( $ElementName eq 'StateIDs' || $ElementName eq 'CreatedStateIDs' ) { my %StateList = $StateObject->StateList( UserID => 1 ); ID: for my $ID ( @{$Values} ) { next ID if !$ID; $ID->{Content} = $StateList{ $ID->{Content} }; } } elsif ( $ElementName eq 'PriorityIDs' || $ElementName eq 'CreatedPriorityIDs' ) { my %PriorityList = $PriorityObject->PriorityList( UserID => 1 ); ID: for my $ID ( @{$Values} ) { next ID if !$ID; $ID->{Content} = $PriorityList{ $ID->{Content} }; } } elsif ( $ElementName eq 'OwnerIDs' || $ElementName eq 'CreatedUserIDs' || $ElementName eq 'ResponsibleIDs' ) { ID: for my $ID ( @{$Values} ) { next ID if !$ID; $ID->{Content} = $UserObject->UserLookup( UserID => $ID->{Content} ); } } # Locks and statustype don't have to wrap because they are never different } } return \%Param; } sub ImportWrapper { my ( $Self, %Param ) = @_; # get needed objects my $UserObject = $Kernel::OM->Get('Kernel::System::User'); my $QueueObject = $Kernel::OM->Get('Kernel::System::Queue'); my $StateObject = $Kernel::OM->Get('Kernel::System::State'); my $PriorityObject = $Kernel::OM->Get('Kernel::System::Priority'); # wrap used spelling to ids for my $Use (qw(UseAsValueSeries UseAsRestriction UseAsXvalue)) { ELEMENT: for my $Element ( @{ $Param{$Use} } ) { next ELEMENT if !$Element || !$Element->{SelectedValues}; my $ElementName = $Element->{Element}; my $Values = $Element->{SelectedValues}; if ( $ElementName eq 'QueueIDs' || $ElementName eq 'CreatedQueueIDs' ) { ID: for my $ID ( @{$Values} ) { next ID if !$ID; if ( $QueueObject->QueueLookup( Queue => $ID->{Content} ) ) { $ID->{Content} = $QueueObject->QueueLookup( Queue => $ID->{Content} ); } else { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Import: Can' find the queue $ID->{Content}!" ); $ID = undef; } } } elsif ( $ElementName eq 'StateIDs' || $ElementName eq 'CreatedStateIDs' ) { ID: for my $ID ( @{$Values} ) { next ID if !$ID; my %State = $StateObject->StateGet( Name => $ID->{Content}, Cache => 1, ); if ( $State{ID} ) { $ID->{Content} = $State{ID}; } else { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Import: Can' find state $ID->{Content}!" ); $ID = undef; } } } elsif ( $ElementName eq 'PriorityIDs' || $ElementName eq 'CreatedPriorityIDs' ) { my %PriorityList = $PriorityObject->PriorityList( UserID => 1 ); my %PriorityIDs; for my $Key ( sort keys %PriorityList ) { $PriorityIDs{ $PriorityList{$Key} } = $Key; } ID: for my $ID ( @{$Values} ) { next ID if !$ID; if ( $PriorityIDs{ $ID->{Content} } ) { $ID->{Content} = $PriorityIDs{ $ID->{Content} }; } else { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Import: Can' find priority $ID->{Content}!" ); $ID = undef; } } } elsif ( $ElementName eq 'OwnerIDs' || $ElementName eq 'CreatedUserIDs' || $ElementName eq 'ResponsibleIDs' ) { ID: for my $ID ( @{$Values} ) { next ID if !$ID; if ( $UserObject->UserLookup( UserLogin => $ID->{Content} ) ) { $ID->{Content} = $UserObject->UserLookup( UserLogin => $ID->{Content} ); } else { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Import: Can' find user $ID->{Content}!" ); $ID = undef; } } } # Locks and statustype don't have to wrap because they are never different } } return \%Param; } sub _TicketAttributes { my $Self = shift; # get config object my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); my %TicketAttributes = ( Number => Translatable('Number'), # only a counter for a better readability TicketNumber => $ConfigObject->Get('Ticket::Hook'), #TicketID => 'TicketID', Age => 'Age', Title => 'Title', Queue => 'Queue', #QueueID => 'QueueID', State => 'State', #StateID => 'StateID', Priority => 'Priority', #PriorityID => 'PriorityID', CustomerID => 'Customer ID', Changed => Translatable('Last Changed'), Created => 'Created', CustomerUserID => 'Customer User', Lock => 'Lock', #LockID => 'LockID', UnlockTimeout => 'UnlockTimeout', AccountedTime => 'Accounted time', # the same wording is in AgentTicketPrint.tt RealTillTimeNotUsed => 'RealTillTimeNotUsed', NumberOfArticles => 'Number of Articles', #GroupID => 'GroupID', StateType => 'StateType', UntilTime => 'UntilTime', Closed => 'Close Time', FirstLock => 'First Lock', EscalationResponseTime => 'EscalationResponseTime', EscalationUpdateTime => 'EscalationUpdateTime', EscalationSolutionTime => 'EscalationSolutionTime', EscalationDestinationIn => 'EscalationDestinationIn', # EscalationDestinationTime => 'EscalationDestinationTime', EscalationDestinationDate => 'EscalationDestinationDate', EscalationTimeWorkingTime => 'EscalationTimeWorkingTime', EscalationTime => 'EscalationTime', FirstResponse => 'FirstResponse', FirstResponseInMin => 'FirstResponseInMin', FirstResponseDiffInMin => 'FirstResponseDiffInMin', FirstResponseTimeWorkingTime => 'FirstResponseTimeWorkingTime', FirstResponseTimeEscalation => 'FirstResponseTimeEscalation', FirstResponseTimeNotification => 'FirstResponseTimeNotification', FirstResponseTimeDestinationTime => 'FirstResponseTimeDestinationTime', FirstResponseTimeDestinationDate => 'FirstResponseTimeDestinationDate', FirstResponseTime => 'FirstResponseTime', UpdateTimeEscalation => 'UpdateTimeEscalation', UpdateTimeNotification => 'UpdateTimeNotification', UpdateTimeDestinationTime => 'UpdateTimeDestinationTime', UpdateTimeDestinationDate => 'UpdateTimeDestinationDate', UpdateTimeWorkingTime => 'UpdateTimeWorkingTime', UpdateTime => 'UpdateTime', SolutionTime => 'SolutionTime', SolutionInMin => 'SolutionInMin', SolutionDiffInMin => 'SolutionDiffInMin', SolutionTimeWorkingTime => 'SolutionTimeWorkingTime', SolutionTimeEscalation => 'SolutionTimeEscalation', SolutionTimeNotification => 'SolutionTimeNotification', SolutionTimeDestinationTime => 'SolutionTimeDestinationTime', SolutionTimeDestinationDate => 'SolutionTimeDestinationDate', SolutionTimeWorkingTime => 'SolutionTimeWorkingTime', ); if ( $ConfigObject->Get('Ticket::Service') ) { $TicketAttributes{Service} = 'Service'; #$TicketAttributes{ServiceID} = 'ServiceID', $TicketAttributes{SLA} = 'SLA'; $TicketAttributes{SLAID} = 'SLAID'; } if ( $ConfigObject->Get('Ticket::Type') ) { $TicketAttributes{Type} = 'Type'; #$TicketAttributes{TypeID} = 'TypeID'; } if ( $ConfigObject->Get('Stats::UseAgentElementInStats') ) { $TicketAttributes{Owner} = 'Agent/Owner'; #$TicketAttributes{OwnerID} = 'OwnerID'; # ? $TicketAttributes{CreatedUserIDs} = 'Created by Agent/Owner'; $TicketAttributes{Responsible} = 'Responsible'; #$TicketAttributes{ResponsibleID} = 'ResponsibleID'; } DYNAMICFIELD: for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); next DYNAMICFIELD if !$DynamicFieldConfig->{Name}; $TicketAttributes{ 'DynamicField_' . $DynamicFieldConfig->{Name} } = $DynamicFieldConfig->{Label}; } return \%TicketAttributes; } sub _SortedAttributes { my $Self = shift; my @SortedAttributes = qw( Number TicketNumber Age Title Created Changed Closed Queue State Priority CustomerUserID CustomerID Service SLA Type Owner Responsible AccountedTime EscalationDestinationIn EscalationDestinationTime EscalationDestinationDate EscalationTimeWorkingTime EscalationTime FirstResponse FirstResponseInMin FirstResponseDiffInMin FirstResponseTimeWorkingTime FirstResponseTimeEscalation FirstResponseTimeNotification FirstResponseTimeDestinationTime FirstResponseTimeDestinationDate FirstResponseTime UpdateTimeEscalation UpdateTimeNotification UpdateTimeDestinationTime UpdateTimeDestinationDate UpdateTimeWorkingTime UpdateTime SolutionTime SolutionInMin SolutionDiffInMin SolutionTimeWorkingTime SolutionTimeEscalation SolutionTimeNotification SolutionTimeDestinationTime SolutionTimeDestinationDate SolutionTimeWorkingTime FirstLock Lock StateType UntilTime UnlockTimeout EscalationResponseTime EscalationSolutionTime EscalationUpdateTime RealTillTimeNotUsed NumberOfArticles ); # cycle trought the Dynamic Fields DYNAMICFIELD: for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); next DYNAMICFIELD if !$DynamicFieldConfig->{Name}; # add dynamic field attribute push @SortedAttributes, 'DynamicField_' . $DynamicFieldConfig->{Name}; } return \@SortedAttributes; } sub _ExtendedAttributesCheck { my ( $Self, %Param ) = @_; my @ExtendedAttributes = qw( FirstResponse FirstResponseInMin FirstResponseDiffInMin FirstResponseTimeWorkingTime Closed SolutionTime SolutionInMin SolutionDiffInMin SolutionTimeWorkingTime FirstLock ); ATTRIBUTE: for my $Attribute (@ExtendedAttributes) { return 1 if $Param{TicketAttributes}{$Attribute}; } return; } sub _OrderByIsValueOfTicketSearchSort { my ( $Self, %Param ) = @_; my %SortOptions = ( Age => 'Age', Created => 'Age', CustomerID => 'CustomerID', EscalationResponseTime => 'EscalationResponseTime', EscalationSolutionTime => 'EscalationSolutionTime', EscalationTime => 'EscalationTime', EscalationUpdateTime => 'EscalationUpdateTime', Lock => 'Lock', Owner => 'Owner', Priority => 'Priority', Queue => 'Queue', Responsible => 'Responsible', SLA => 'SLA', Service => 'Service', State => 'State', TicketNumber => 'Ticket', TicketEscalation => 'TicketEscalation', Title => 'Title', Type => 'Type', ); # get dynamic field backend object my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend'); # cycle trought the Dynamic Fields DYNAMICFIELD: for my $DynamicFieldConfig ( @{ $Self->{DynamicField} } ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); next DYNAMICFIELD if !$DynamicFieldConfig->{Name}; # get dynamic field sortable condition my $IsSortable = $DynamicFieldBackendObject->HasBehavior( DynamicFieldConfig => $DynamicFieldConfig, Behavior => 'IsSortable', ); # add dynamic field if is sortable if ($IsSortable) { $SortOptions{ 'DynamicField_' . $DynamicFieldConfig->{Name} } = 'DynamicField_' . $DynamicFieldConfig->{Name}; } } return $SortOptions{ $Param{OrderBy} } if $SortOptions{ $Param{OrderBy} }; return; } sub _IndividualResultOrder { my ( $Self, %Param ) = @_; my @Unsorted = @{ $Param{StatArray} }; my @Sorted; # find out the positon of the values which should be # used for the order my $Counter = 0; my $SortedAttributes = $Self->_SortedAttributes(); ATTRIBUTE: for my $Attribute ( @{$SortedAttributes} ) { next ATTRIBUTE if !$Param{SelectedAttributes}{$Attribute}; last ATTRIBUTE if $Attribute eq $Param{OrderBy}; $Counter++; } # order after a individual attribute if ( $Param{OrderBy} eq 'AccountedTime' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'SolutionTime' ) { @Sorted = sort { $a->[$Counter] cmp $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'SolutionDiffInMin' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'SolutionInMin' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'SolutionTimeWorkingTime' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'FirstResponse' ) { @Sorted = sort { $a->[$Counter] cmp $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'FirstResponseDiffInMin' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'FirstResponseInMin' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'FirstResponseTimeWorkingTime' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'FirstLock' ) { @Sorted = sort { $a->[$Counter] cmp $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'StateType' ) { @Sorted = sort { $a->[$Counter] cmp $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'UntilTime' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'UnlockTimeout' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'EscalationResponseTime' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'EscalationUpdateTime' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'EscalationSolutionTime' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'RealTillTimeNotUsed' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'EscalationTimeWorkingTime' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } elsif ( $Param{OrderBy} eq 'NumberOfArticles' ) { @Sorted = sort { $a->[$Counter] <=> $b->[$Counter] } @Unsorted; } else { @Sorted = sort { $a->[$Counter] cmp $b->[$Counter] } @Unsorted; } # make a reverse sort if needed if ( $Param{Sort} eq 'Down' ) { @Sorted = reverse @Sorted; } # take care about the limit if ( $Param{Limit} && $Param{Limit} ne 'unlimited' ) { my $Count = 0; @Sorted = grep { ++$Count <= $Param{Limit} } @Sorted; } return @Sorted; } 1;