# -- # OTOBO is a web-based ticketing system for service organisations. # -- # Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ # Copyright (C) 2019-2023 Rother OSS GmbH, https://otobo.de/ # -- # $origin: otobo - a077e914380d1a13d5aa31472ea687353b614622 - Kernel/Modules/AgentTicketSearch.pm # -- # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -- package Kernel::Modules::AgentTicketSearch; use strict; use warnings; use Kernel::System::VariableCheck qw(:all); use Kernel::Language qw(Translatable); our $ObjectManagerDisabled = 1; sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {%Param}; bless( $Self, $Type ); return $Self; } sub Run { my ( $Self, %Param ) = @_; my $Output; # get needed objects my $ParamObject = $Kernel::OM->Get('Kernel::System::Web::Request'); my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket'); my $Config = $ConfigObject->Get("Ticket::Frontend::$Self->{Action}"); # get config data $Self->{StartHit} = int( $ParamObject->GetParam( Param => 'StartHit' ) || 1 ); $Self->{SearchLimit} = $Config->{SearchLimit} || 500; $Self->{SortBy} = $ParamObject->GetParam( Param => 'SortBy' ) || $Config->{'SortBy::Default'} || 'Age'; $Self->{OrderBy} = $ParamObject->GetParam( Param => 'OrderBy' ) || $Config->{'Order::Default'} || 'Down'; $Self->{Profile} = $ParamObject->GetParam( Param => 'Profile' ) || ''; $Self->{SaveProfile} = $ParamObject->GetParam( Param => 'SaveProfile' ) || ''; $Self->{TakeLastSearch} = $ParamObject->GetParam( Param => 'TakeLastSearch' ) || ''; $Self->{SelectTemplate} = $ParamObject->GetParam( Param => 'SelectTemplate' ) || ''; $Self->{EraseTemplate} = $ParamObject->GetParam( Param => 'EraseTemplate' ) || ''; # get list type my $TreeView = 0; if ( $ConfigObject->Get('Ticket::Frontend::ListType') eq 'tree' ) { $TreeView = 1; } # get layout object my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout'); # check request if ( $Self->{Subaction} eq 'OpenSearchDescriptionTicketNumber' ) { my $Output = $LayoutObject->Output( TemplateFile => 'AgentTicketSearchOpenSearchDescriptionTicketNumber', Data => \%Param, ); # TODO: maybe declare as UTF-8 return $LayoutObject->Attachment( Filename => 'OpenSearchDescriptionTicketNumber.xml', ContentType => 'application/opensearchdescription+xml', Content => $Output, Type => 'inline', ); } if ( $Self->{Subaction} eq 'OpenSearchDescriptionFulltext' ) { my $Output = $LayoutObject->Output( TemplateFile => 'AgentTicketSearchOpenSearchDescriptionFulltext', Data => \%Param, ); # TODO: maybe declare as UTF-8 return $LayoutObject->Attachment( Filename => 'OpenSearchDescriptionFulltext.xml', ContentType => 'application/opensearchdescription+xml', Content => $Output, Type => 'inline', ); } # Autocomplete is executed via AJAX request. if ( $Self->{Subaction} eq 'AJAXAutocomplete' ) { $LayoutObject->ChallengeTokenCheck(); my $Skip = $ParamObject->GetParam( Param => 'Skip' ) || ''; my $Search = $ParamObject->GetParam( Param => 'Term' ) || ''; my $Filter = $ParamObject->GetParam( Param => 'Filter' ) || '{}'; my $MaxResults = int( $ParamObject->GetParam( Param => 'MaxResults' ) || 20 ); # Remove leading and trailing spaces from search term. $Search =~ s{ \A \s* ( [^\s]+ ) \s* \z }{$1}xms; # Parse passed search filter. my $SearchFilter = $Kernel::OM->Get('Kernel::System::JSON')->Decode( Data => $Filter, ); # Workaround, all auto completion requests get posted by UTF8 anyway. # Convert any to 8bit string if application is not running in UTF8. $Search = $Kernel::OM->Get('Kernel::System::Encode')->Convert2CharsetInternal( Text => $Search, From => 'utf-8', ); my @TicketIDs; # Search for tickets by: # - Ticket Number # - Ticket Title if ($Search) { @TicketIDs = $TicketObject->TicketSearch( %{$SearchFilter}, TicketNumber => '%' . $Search . '%', Limit => $MaxResults, Result => 'ARRAY', ArchiveFlags => ['n'], UserID => $Self->{UserID}, ); if ( !@TicketIDs ) { @TicketIDs = $TicketObject->TicketSearch( %{$SearchFilter}, Title => '%' . $Search . '%', Limit => $MaxResults, Result => 'ARRAY', ArchiveFlags => ['n'], UserID => $Self->{UserID}, ); } } my @Results; # Include additional ticket information in results. TICKET: for my $TicketID (@TicketIDs) { next TICKET if !$TicketID; next TICKET if $TicketID eq $Skip; my %Ticket = $TicketObject->TicketGet( TicketID => $TicketID, DynamicFields => 0, UserID => $Self->{UserID}, ); next TICKET if !%Ticket; push @Results, { Key => $Ticket{TicketNumber}, Value => $Ticket{TicketNumber} . ' ' . $Ticket{Title}, }; } my $JSON = $LayoutObject->JSONEncode( Data => \@Results || [], ); # Send JSON response. return $LayoutObject->Attachment( ContentType => 'application/json; charset=' . $LayoutObject->{Charset}, Content => $JSON || '', Type => 'inline', NoCache => 1, ); } # check request if ( $ParamObject->GetParam( Param => 'SearchTemplate' ) && $Self->{Profile} ) { my $Profile = $LayoutObject->LinkEncode( $Self->{Profile} ); return $LayoutObject->Redirect( OP => "Action=AgentTicketSearch;Subaction=Search;TakeLastSearch=1;SaveProfile=1;Profile=$Profile" ); } # get single params my %GetParam; # get needed objects my $SearchProfileObject = $Kernel::OM->Get('Kernel::System::SearchProfile'); my $DynamicFieldObject = $Kernel::OM->Get('Kernel::System::DynamicField'); my $BackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend'); # get dynamic field config for frontend module my $DynamicFieldFilter = $Config->{DynamicField}; # get the dynamic fields for ticket object my $DynamicField = $DynamicFieldObject->DynamicFieldListGet( Valid => 1, ObjectType => [ 'Ticket', 'Article' ], FieldFilter => $DynamicFieldFilter || {}, ); # collect all searchable article field definitions and add the fields to the attributes array my %ArticleSearchableFields = $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleSearchableFieldsList(); # load profiles string params (press load profile) if ( ( $Self->{Subaction} eq 'LoadProfile' && $Self->{Profile} ) || $Self->{TakeLastSearch} ) { %GetParam = $SearchProfileObject->SearchProfileGet( Base => 'TicketSearch', Name => $Self->{Profile}, UserLogin => $Self->{UserLogin}, ); # convert attributes if ( $GetParam{ShownAttributes} && ref $GetParam{ShownAttributes} eq 'ARRAY' ) { $GetParam{ShownAttributes} = join ';', @{ $GetParam{ShownAttributes} }; } } # get search string params (get submitted params) else { for my $Key ( sort keys %ArticleSearchableFields, qw( TicketNumber Title CustomerID CustomerIDRaw CustomerUserLogin CustomerUserLoginRaw CustomerUserID StateType Agent ResultForm TimeSearchType ChangeTimeSearchType CloseTimeSearchType LastChangeTimeSearchType EscalationTimeSearchType PendingTimeSearchType UseSubQueues ArticleTimeSearchType SearchInArchive Fulltext ShownAttributes ArticleCreateTimePointFormat ArticleCreateTimePoint ArticleCreateTimePointStart ArticleCreateTimeStart ArticleCreateTimeStartDay ArticleCreateTimeStartMonth ArticleCreateTimeStartYear ArticleCreateTimeStop ArticleCreateTimeStopDay ArticleCreateTimeStopMonth ArticleCreateTimeStopYear TicketCreateTimePointFormat TicketCreateTimePoint TicketCreateTimePointStart TicketCreateTimeStart TicketCreateTimeStartDay TicketCreateTimeStartMonth TicketCreateTimeStartYear TicketCreateTimeStop TicketCreateTimeStopDay TicketCreateTimeStopMonth TicketCreateTimeStopYear TicketChangeTimePointFormat TicketChangeTimePoint TicketChangeTimePointStart TicketChangeTimeStart TicketChangeTimeStartDay TicketChangeTimeStartMonth TicketChangeTimeStartYear TicketChangeTimeStop TicketChangeTimeStopDay TicketChangeTimeStopMonth TicketChangeTimeStopYear TicketLastChangeTimePointFormat TicketLastChangeTimePoint TicketLastChangeTimePointStart TicketLastChangeTimeStart TicketLastChangeTimeStartDay TicketLastChangeTimeStartMonth TicketLastChangeTimeStartYear TicketLastChangeTimeStop TicketLastChangeTimeStopDay TicketLastChangeTimeStopMonth TicketLastChangeTimeStopYear TicketCloseTimePointFormat TicketCloseTimePoint TicketCloseTimePointStart TicketCloseTimeStart TicketCloseTimeStartDay TicketCloseTimeStartMonth TicketCloseTimeStartYear TicketCloseTimeStop TicketCloseTimeStopDay TicketCloseTimeStopMonth TicketCloseTimeStopYear TicketPendingTimePointFormat TicketPendingTimePoint TicketPendingTimePointStart TicketPendingTimeStart TicketPendingTimeStartDay TicketPendingTimeStartMonth TicketPendingTimeStartYear TicketPendingTimeStop TicketPendingTimeStopDay TicketPendingTimeStopMonth TicketPendingTimeStopYear TicketEscalationTimePointFormat TicketEscalationTimePoint TicketEscalationTimePointStart TicketEscalationTimeStart TicketEscalationTimeStartDay TicketEscalationTimeStartMonth TicketEscalationTimeStartYear TicketEscalationTimeStop TicketEscalationTimeStopDay TicketEscalationTimeStopMonth TicketEscalationTimeStopYear TicketCloseTimeNewerDate TicketCloseTimeOlderDate FulltextES ) ) { # get search string params (get submitted params) $GetParam{$Key} = $ParamObject->GetParam( Param => $Key ); # remove white space on the start and end if ( $GetParam{$Key} ) { $GetParam{$Key} =~ s/\s+$//g; $GetParam{$Key} =~ s/^\s+//g; } } # get array params for my $Key ( qw(StateIDs States StateTypeIDs QueueIDs Queues PriorityIDs Priorities OwnerIDs CreatedQueueIDs CreatedUserIDs WatchUserIDs ResponsibleIDs TypeIDs Types ServiceIDs Services SLAIDs SLAs LockIDs Locks) ) { # get search array params (get submitted params) my @Array = $ParamObject->GetArray( Param => $Key ); if (@Array) { $GetParam{$Key} = \@Array; } } # get Dynamic fields from param object # cycle trough the activated Dynamic Fields for this screen DYNAMICFIELD: for my $DynamicFieldConfig ( @{$DynamicField} ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); # get search field preferences my $SearchFieldPreferences = $BackendObject->SearchFieldPreferences( DynamicFieldConfig => $DynamicFieldConfig, ); next DYNAMICFIELD if !IsArrayRefWithData($SearchFieldPreferences); PREFERENCE: for my $Preference ( @{$SearchFieldPreferences} ) { # extract the dynamic field value from the web request my $DynamicFieldValue = $BackendObject->SearchFieldValueGet( DynamicFieldConfig => $DynamicFieldConfig, ParamObject => $ParamObject, ReturnProfileStructure => 1, LayoutObject => $LayoutObject, Type => $Preference->{Type}, ); # set the complete value structure in GetParam to store it later in the search profile if ( IsHashRefWithData($DynamicFieldValue) ) { %GetParam = ( %GetParam, %{$DynamicFieldValue} ); } } } } # get article create time option if ( !$GetParam{ArticleTimeSearchType} ) { $GetParam{'ArticleTimeSearchType::None'} = 1; } elsif ( $GetParam{ArticleTimeSearchType} eq 'TimePoint' ) { $GetParam{'ArticleTimeSearchType::TimePoint'} = 1; } elsif ( $GetParam{ArticleTimeSearchType} eq 'TimeSlot' ) { $GetParam{'ArticleTimeSearchType::TimeSlot'} = 1; } # get create time option if ( !$GetParam{TimeSearchType} ) { $GetParam{'TimeSearchType::None'} = 1; } elsif ( $GetParam{TimeSearchType} eq 'TimePoint' ) { $GetParam{'TimeSearchType::TimePoint'} = 1; } elsif ( $GetParam{TimeSearchType} eq 'TimeSlot' ) { $GetParam{'TimeSearchType::TimeSlot'} = 1; } # get change time option if ( !$GetParam{ChangeTimeSearchType} ) { $GetParam{'ChangeTimeSearchType::None'} = 1; } elsif ( $GetParam{ChangeTimeSearchType} eq 'TimePoint' ) { $GetParam{'ChangeTimeSearchType::TimePoint'} = 1; } elsif ( $GetParam{ChangeTimeSearchType} eq 'TimeSlot' ) { $GetParam{'ChangeTimeSearchType::TimeSlot'} = 1; } # get last change time option if ( !$GetParam{LastChangeTimeSearchType} ) { $GetParam{'LastChangeTimeSearchType::None'} = 1; } elsif ( $GetParam{LastChangeTimeSearchType} eq 'TimePoint' ) { $GetParam{'LastChangeTimeSearchType::TimePoint'} = 1; } elsif ( $GetParam{LastChangeTimeSearchType} eq 'TimeSlot' ) { $GetParam{'LastChangeTimeSearchType::TimeSlot'} = 1; } # get close time option if ( !$GetParam{PendingTimeSearchType} ) { $GetParam{'PendingTimeSearchType::None'} = 1; } elsif ( $GetParam{PendingTimeSearchType} eq 'TimePoint' ) { $GetParam{'PendingTimeSearchType::TimePoint'} = 1; } elsif ( $GetParam{PendingTimeSearchType} eq 'TimeSlot' ) { $GetParam{'PendingTimeSearchType::TimeSlot'} = 1; } # get close time option if ( !$GetParam{CloseTimeSearchType} ) { $GetParam{'CloseTimeSearchType::None'} = 1; } elsif ( $GetParam{CloseTimeSearchType} eq 'TimePoint' ) { $GetParam{'CloseTimeSearchType::TimePoint'} = 1; } elsif ( $GetParam{CloseTimeSearchType} eq 'TimeSlot' ) { $GetParam{'CloseTimeSearchType::TimeSlot'} = 1; } # get escalation time option if ( !$GetParam{EscalationTimeSearchType} ) { $GetParam{'EscalationTimeSearchType::None'} = 1; } elsif ( $GetParam{EscalationTimeSearchType} eq 'TimePoint' ) { $GetParam{'EscalationTimeSearchType::TimePoint'} = 1; } elsif ( $GetParam{EscalationTimeSearchType} eq 'TimeSlot' ) { $GetParam{'EscalationTimeSearchType::TimeSlot'} = 1; } # set result form env if ( !$GetParam{ResultForm} ) { $GetParam{ResultForm} = ''; } # get needed objects my $UserObject = $Kernel::OM->Get('Kernel::System::User'); my $StateObject = $Kernel::OM->Get('Kernel::System::State'); # show result site if ( $Self->{Subaction} eq 'Search' && !$Self->{EraseTemplate} ) { $Self->{ProfileName} = ''; if ( $Self->{Profile} ) { $Self->{ProfileName} = "($Self->{Profile})"; } # fill up profile name (e.g. with last-search) if ( !$Self->{Profile} || !$Self->{SaveProfile} ) { $Self->{Profile} = Translatable('last-search'); } if ( !$Self->{ProfileName} ) { $Self->{ProfileName} = "($Self->{Profile})"; } if ( $Self->{ProfileName} eq '(last-search)' ) { $Self->{ProfileName} = '(' . $LayoutObject->{LanguageObject}->Translate('last-search') . ')'; } # save search profile (under last-search or real profile name) $Self->{SaveProfile} = 1; # remember last search values if ( $Self->{SaveProfile} && $Self->{Profile} ) { # remove old profile stuff $SearchProfileObject->SearchProfileDelete( Base => 'TicketSearch', Name => $Self->{Profile}, UserLogin => $Self->{UserLogin}, ); # convert attributes if ( $GetParam{ShownAttributes} && ref $GetParam{ShownAttributes} eq '' ) { $GetParam{ShownAttributes} = [ split /;/, $GetParam{ShownAttributes} ]; } # replace StateType to StateIDs if ( $GetParam{StateType} ) { my @StateIDs; if ( $GetParam{StateType} eq 'Open' ) { @StateIDs = $StateObject->StateGetStatesByType( Type => 'Viewable', Result => 'ID', ); } elsif ( $GetParam{StateType} eq 'Closed' ) { my %ViewableStateOpenLookup = $StateObject->StateGetStatesByType( Type => 'Viewable', Result => 'HASH', ); my %StateList = $StateObject->StateList( UserID => $Self->{UserID} ); for my $Item ( sort keys %StateList ) { if ( !$ViewableStateOpenLookup{$Item} ) { push @StateIDs, $Item; } } } # current ticket state type else { @StateIDs = $StateObject->StateGetStatesByType( StateType => $GetParam{StateType}, Result => 'ID', ); } # merge with StateIDs if ( @StateIDs && IsArrayRefWithData( $GetParam{StateIDs} ) ) { my %StateIDs = map { $_ => 1 } @StateIDs; @StateIDs = grep { exists $StateIDs{$_} } @{ $GetParam{StateIDs} }; } if (@StateIDs) { $GetParam{StateIDs} = \@StateIDs; } } # insert new profile params KEY: for my $Key ( sort keys %GetParam ) { next KEY if !defined $GetParam{$Key}; next KEY if $Key eq 'StateType'; $SearchProfileObject->SearchProfileAdd( Base => 'TicketSearch', Name => $Self->{Profile}, Key => $Key, Value => $GetParam{$Key}, UserLogin => $Self->{UserLogin}, ); } } my %TimeMap = ( ArticleCreate => 'ArticleTime', TicketCreate => 'Time', TicketChange => 'ChangeTime', TicketLastChange => 'LastChangeTime', TicketClose => 'CloseTime', TicketPending => 'PendingTime', TicketEscalation => 'EscalationTime', ); for my $TimeType ( sort keys %TimeMap ) { # get create time settings if ( !$GetParam{ $TimeMap{$TimeType} . 'SearchType' } ) { # do nothing with time stuff } elsif ( $GetParam{ $TimeMap{$TimeType} . 'SearchType' } eq 'TimeSlot' ) { for my $Key (qw(Month Day)) { $GetParam{ $TimeType . 'TimeStart' . $Key } = sprintf( "%02d", $GetParam{ $TimeType . 'TimeStart' . $Key } ); $GetParam{ $TimeType . 'TimeStop' . $Key } = sprintf( "%02d", $GetParam{ $TimeType . 'TimeStop' . $Key } ); } if ( $GetParam{ $TimeType . 'TimeStartDay' } && $GetParam{ $TimeType . 'TimeStartMonth' } && $GetParam{ $TimeType . 'TimeStartYear' } ) { my $DateTimeObject = $Kernel::OM->Create( 'Kernel::System::DateTime', ObjectParams => { Year => $GetParam{ $TimeType . 'TimeStartYear' }, Month => $GetParam{ $TimeType . 'TimeStartMonth' }, Day => $GetParam{ $TimeType . 'TimeStartDay' }, Hour => 0, # midnight Minute => 0, Second => 0, TimeZone => $Self->{UserTimeZone} || Kernel::System::DateTime->UserDefaultTimeZoneGet(), }, ); # Convert start time to local system time zone. $DateTimeObject->ToOTOBOTimeZone(); $GetParam{ $TimeType . 'TimeNewerDate' } = $DateTimeObject->ToString(); } if ( $GetParam{ $TimeType . 'TimeStopDay' } && $GetParam{ $TimeType . 'TimeStopMonth' } && $GetParam{ $TimeType . 'TimeStopYear' } ) { my $DateTimeObject = $Kernel::OM->Create( 'Kernel::System::DateTime', ObjectParams => { Year => $GetParam{ $TimeType . 'TimeStopYear' }, Month => $GetParam{ $TimeType . 'TimeStopMonth' }, Day => $GetParam{ $TimeType . 'TimeStopDay' }, Hour => 23, # just before midnight Minute => 59, Second => 59, TimeZone => $Self->{UserTimeZone} || Kernel::System::DateTime->UserDefaultTimeZoneGet(), }, ); # Convert stop time to local system time zone. $DateTimeObject->ToOTOBOTimeZone(); $GetParam{ $TimeType . 'TimeOlderDate' } = $DateTimeObject->ToString(); } } elsif ( $GetParam{ $TimeMap{$TimeType} . 'SearchType' } eq 'TimePoint' ) { if ( $GetParam{ $TimeType . 'TimePoint' } && $GetParam{ $TimeType . 'TimePointStart' } && $GetParam{ $TimeType . 'TimePointFormat' } ) { my $Time = 0; if ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'minute' ) { $Time = $GetParam{ $TimeType . 'TimePoint' }; } elsif ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'hour' ) { $Time = $GetParam{ $TimeType . 'TimePoint' } * 60; } elsif ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'day' ) { $Time = $GetParam{ $TimeType . 'TimePoint' } * 60 * 24; } elsif ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'week' ) { $Time = $GetParam{ $TimeType . 'TimePoint' } * 60 * 24 * 7; } elsif ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'month' ) { $Time = $GetParam{ $TimeType . 'TimePoint' } * 60 * 24 * 30; } elsif ( $GetParam{ $TimeType . 'TimePointFormat' } eq 'year' ) { $Time = $GetParam{ $TimeType . 'TimePoint' } * 60 * 24 * 365; } if ( $GetParam{ $TimeType . 'TimePointStart' } eq 'Before' ) { # more than ... ago $GetParam{ $TimeType . 'TimeOlderMinutes' } = $Time; } elsif ( $GetParam{ $TimeType . 'TimePointStart' } eq 'Next' ) { # within next $GetParam{ $TimeType . 'TimeNewerMinutes' } = 0; $GetParam{ $TimeType . 'TimeOlderMinutes' } = -$Time; } elsif ( $GetParam{ $TimeType . 'TimePointStart' } eq 'After' ) { # in more then ... $GetParam{ $TimeType . 'TimeNewerMinutes' } = -$Time; } else { # within last ... $GetParam{ $TimeType . 'TimeOlderMinutes' } = 0; $GetParam{ $TimeType . 'TimeNewerMinutes' } = $Time; } } } } my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article'); # Special behavior for the fulltext search toolbar module: # - Check full text string to see if contents is a ticket number. # - If exists and not in print or CSV mode, redirect to the ticket. # See http://bugs.otrs.org/show_bug.cgi?id=4238 for details. # The original problem was that tickets with customer reply will be # found by a fulltext search (ticket number is in the subjects), but # 'new' tickets will not be found. if ( $GetParam{Fulltext} && $ParamObject->GetParam( Param => 'CheckTicketNumberAndRedirect' ) && $GetParam{ResultForm} ne 'Normal' && $GetParam{ResultForm} ne 'Print' ) { my $TicketID = $TicketObject->TicketIDLookup( TicketNumber => $GetParam{Fulltext}, UserID => $Self->{UserID}, ); if ($TicketID) { return $LayoutObject->Redirect( OP => "Action=AgentTicketZoom;TicketID=$TicketID", ); } } # prepare archive flag if ( $ConfigObject->Get('Ticket::ArchiveSystem') ) { $GetParam{SearchInArchive} ||= ''; if ( $GetParam{SearchInArchive} eq 'AllTickets' ) { $GetParam{ArchiveFlags} = [ 'y', 'n' ]; } elsif ( $GetParam{SearchInArchive} eq 'ArchivedTickets' ) { $GetParam{ArchiveFlags} = ['y']; } else { $GetParam{ArchiveFlags} = ['n']; } } my %AttributeLookup; # create attribute lookup table for my $Attribute ( @{ $GetParam{ShownAttributes} || [] } ) { $AttributeLookup{$Attribute} = 1; } # dynamic fields search parameters for ticket search my %DynamicFieldSearchParameters; # cycle trough the activated Dynamic Fields for this screen DYNAMICFIELD: for my $DynamicFieldConfig ( @{$DynamicField} ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); # get search field preferences my $SearchFieldPreferences = $BackendObject->SearchFieldPreferences( DynamicFieldConfig => $DynamicFieldConfig, ); next DYNAMICFIELD if !IsArrayRefWithData($SearchFieldPreferences); PREFERENCE: for my $Preference ( @{$SearchFieldPreferences} ) { if ( !$AttributeLookup{ 'LabelSearch_DynamicField_' . $DynamicFieldConfig->{Name} . $Preference->{Type} } ) { next PREFERENCE; } # extract the dynamic field value from the profile my $SearchParameter = $BackendObject->SearchFieldParameterBuild( DynamicFieldConfig => $DynamicFieldConfig, Profile => \%GetParam, LayoutObject => $LayoutObject, Type => $Preference->{Type}, ); # set search parameter if ( defined $SearchParameter ) { $DynamicFieldSearchParameters{ 'DynamicField_' . $DynamicFieldConfig->{Name} } = $SearchParameter->{Parameter}; } } } my @ViewableTicketIDs; my $ESActive = $Kernel::OM->Get('Kernel::Config')->Get('Elasticsearch::Active') || 0; # check whether we want to perform a search via Elasticsearch or not # use normal search for sorting, or if ES is not activated if ( $GetParam{FulltextES} && ( !$ESActive || $Self->{TakeLastSearch} ) ) { $GetParam{Fulltext} = $GetParam{Fulltext} || $GetParam{FulltextES}; delete $GetParam{FulltextES}; } # search via DB if FulltextES is empty if ( !$GetParam{FulltextES} ) { local $Kernel::System::DB::UseSlaveDB = 1; # perform ticket search @ViewableTicketIDs = $TicketObject->TicketSearch( Result => 'ARRAY', SortBy => $Self->{SortBy}, OrderBy => $Self->{OrderBy}, Limit => $Self->{SearchLimit}, UserID => $Self->{UserID}, ConditionInline => $Config->{ExtendedSearchCondition}, ContentSearchPrefix => '*', ContentSearchSuffix => '*', FullTextIndex => 1, %GetParam, %DynamicFieldSearchParameters, ); } # if ES is activated else { # get data from cache my $CacheObject = $Kernel::OM->Get('Kernel::System::Cache'); my $CacheData = $CacheObject->Get( Type => 'TicketSearch', Key => ( $GetParam{FulltextES} || '' ) . '_SortBy_' . $Self->{SortBy} . '_OrderBy_' . $Self->{OrderBy}, ); # take ticketids from cache if available, else perform ES search if ( defined $CacheData ) { @ViewableTicketIDs = @{$CacheData}; } else { my $ESObject = $Kernel::OM->Get('Kernel::System::Elasticsearch'); my $Count = $ConfigObject->Get('Elasticsearch::MaxArticleSearch'); # TODO please consider sorting via ES. For fields with type text, mapping for sorting must be the keyword version, e.g. Title.keyword! #my $Field = $Mapping->{ticket}->{mappings}->{properties}->{ $Param{Sort} }->{fields}; #$Param{Sort} .= ( defined $Field ) ? '.' . ( %{$Field} )[0] : ''; if ( ( defined $GetParam{FulltextES} ) && ( !defined $GetParam{Fulltext} ) ) { @ViewableTicketIDs = $ESObject->TicketSearch( Fulltext => $GetParam{FulltextES}, UserID => $Self->{UserID}, UserLogin => $Self->{UserLogin}, CustomerUserID => $Self->{CustomerUserIDLogin} || '', SortBy => $Self->{SortBy}, OrderBy => $Self->{OrderBy}, Limit => $Count, Result => 'ARRAY', ); # set cache for ES results if ($CacheObject) { $CacheObject->Set( Type => 'TicketSearch', Key => $GetParam{FulltextES} . '_SortBy_' . $Self->{SortBy} . '_OrderBy_' . $Self->{OrderBy}, Value => \@ViewableTicketIDs, TTL => $GetParam{CacheTTL} || 60 * 4, ); } } } } # get needed objects my $CustomerUserObject = $Kernel::OM->Get('Kernel::System::CustomerUser'); # get the ticket dynamic fields for CSV display my $CSVDynamicField = $DynamicFieldObject->DynamicFieldListGet( Valid => 1, ObjectType => ['Ticket'], FieldFilter => $Config->{SearchCSVDynamicField} || {}, ); # CSV and Excel output if ( $GetParam{ResultForm} eq 'CSV' || $GetParam{ResultForm} eq 'Excel' ) { # create head (actual head and head for data fill) my @TmpCSVHead = @{ $Config->{SearchCSVData} }; my @CSVHead = @{ $Config->{SearchCSVData} }; # include the selected dynamic fields in CVS results DYNAMICFIELD: for my $DynamicFieldConfig ( @{$CSVDynamicField} ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); next DYNAMICFIELD if !$DynamicFieldConfig->{Name}; next DYNAMICFIELD if $DynamicFieldConfig->{Name} eq ''; push @TmpCSVHead, 'DynamicField_' . $DynamicFieldConfig->{Name}; push @CSVHead, $DynamicFieldConfig->{Label}; } my @CSVData; for my $TicketID (@ViewableTicketIDs) { # Get ticket data. my %Ticket = $TicketObject->TicketGet( TicketID => $TicketID, DynamicFields => 1, Extended => 1, UserID => $Self->{UserID}, ); # Get last customer article. my @Articles = $ArticleObject->ArticleList( TicketID => $TicketID, SenderType => 'customer', OnlyLast => 1, ); # If the ticket has no customer article, get the last agent article. if ( !@Articles ) { @Articles = $ArticleObject->ArticleList( TicketID => $TicketID, SenderType => 'agent', OnlyLast => 1, ); } # Finally, if everything failed, get latest article. if ( !@Articles ) { @Articles = $ArticleObject->ArticleList( TicketID => $TicketID, OnlyLast => 1, ); } my %Article = $ArticleObject->BackendForArticle( %{ $Articles[0] } )->ArticleGet( %{ $Articles[0] }, DynamicFields => 1, ); my %Data; if ( !%Article ) { %Data = %Ticket; # Set missing information. $Data{Subject} = $Ticket{Title} || $LayoutObject->{LanguageObject}->Translate('Untitled'); $Data{Body} = $LayoutObject->{LanguageObject}->Translate('This item has no articles yet.'); $Data{From} = '--'; } else { %Data = ( %Ticket, %Article ); } for my $Key (qw(State Lock)) { $Data{$Key} = $LayoutObject->{LanguageObject}->Translate( $Data{$Key} ); } $Data{Age} = $LayoutObject->CustomerAge( Age => $Data{Age}, Space => ' ', ); # get whole article (if configured!) if ( $Config->{SearchArticleCSVTree} ) { my @Articles = $ArticleObject->ArticleList( TicketID => $TicketID, ); if (@Articles) { for my $Article (@Articles) { my %ArticleData = $ArticleObject->BackendForArticle( %{$Article} )->ArticleGet( TicketID => $TicketID, ArticleID => $Article->{ArticleID}, DynamicFields => 0, ); if ( $ArticleData{Body} ) { $Data{ArticleTree} .= "\n-->" . "||$ArticleData{SenderType}" . "||$ArticleData{From}" . "||$ArticleData{CreateTime}" . "||<--------------\n" . $ArticleData{Body}; } } } else { $Data{ArticleTree} .= $LayoutObject->{LanguageObject}->Translate( 'This item has no articles yet.' ); } } # customer info (customer name) if ( $Data{CustomerUserID} ) { $Data{CustomerName} = $CustomerUserObject->CustomerName( UserLogin => $Data{CustomerUserID}, ); } # user info my %UserInfo = $UserObject->GetUserData( User => $Data{Owner}, ); # merge row data my %Info = ( %Data, %UserInfo, AccountedTime => $TicketObject->TicketAccountedTimeGet( TicketID => $TicketID ), ); # Transform EscalationTime and EscalationTimeWorkingTime to a human readable format. # See bug#13088 (https://bugs.otrs.org/show_bug.cgi?id=13088). $Info{EscalationTime} = $LayoutObject->CustomerAgeInHours( Age => $Info{EscalationTime}, Space => ' ', ); $Info{EscalationTimeWorkingTime} = $LayoutObject->CustomerAgeInHours( Age => $Info{EscalationTimeWorkingTime}, Space => ' ', ); my @Data; for my $Header (@TmpCSVHead) { # check if header is a dynamic field and get the value from dynamic field # backend if ( $Header =~ m{\A DynamicField_ ( [a-zA-Z\d]+ ) \z}xms ) { # loop over the dynamic fields configured for CSV output DYNAMICFIELD: for my $DynamicFieldConfig ( @{$CSVDynamicField} ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); next DYNAMICFIELD if !$DynamicFieldConfig->{Name}; # skip all fields that does not match with current field name ($1) # with out the 'DynamicField_' prefix next DYNAMICFIELD if $DynamicFieldConfig->{Name} ne $1; # get the value as for print (to correctly display) my $ValueStrg = $BackendObject->DisplayValueRender( DynamicFieldConfig => $DynamicFieldConfig, Value => $Info{$Header}, HTMLOutput => 0, LayoutObject => $LayoutObject, ); push @Data, $ValueStrg->{Value}; # terminate the DYNAMICFIELD loop last DYNAMICFIELD; } } # otherwise retrieve data from article else { push @Data, $Info{$Header}; } } push @CSVData, \@Data; } # get Separator from language file my $UserCSVSeparator = $LayoutObject->{LanguageObject}->{Separator}; if ( $ConfigObject->Get('PreferencesGroups')->{CSVSeparator}->{Active} ) { my %UserData = $UserObject->GetUserData( UserID => $Self->{UserID} ); $UserCSVSeparator = $UserData{UserCSVSeparator} if $UserData{UserCSVSeparator}; } my %HeaderMap = ( TicketNumber => Translatable('Ticket Number'), CustomerName => Translatable('Customer Name'), ); my @CSVHeadTranslated = map { $LayoutObject->{LanguageObject}->Translate( $HeaderMap{$_} || $_ ); } @CSVHead; my $CurDateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime'); my $FileName = sprintf( 'ticket_search_%s', $CurDateTimeObject->Format( Format => '%Y-%m-%d_%H-%M' ) ); # get CSV object my $CSVObject = $Kernel::OM->Get('Kernel::System::CSV'); # generate CSV output if ( $GetParam{ResultForm} eq 'CSV' ) { my $CSV = $CSVObject->Array2CSV( Head => \@CSVHeadTranslated, Data => \@CSVData, Separator => $UserCSVSeparator, ); # return csv to download return $LayoutObject->Attachment( Filename => $FileName . '.csv', ContentType => "text/csv; charset=" . $LayoutObject->{UserCharset}, Content => $CSV, ); } # generate Excel output elsif ( $GetParam{ResultForm} eq 'Excel' ) { my $Excel = $CSVObject->Array2CSV( Head => \@CSVHeadTranslated, Data => \@CSVData, Format => 'Excel', ); # return Excel to download return $LayoutObject->Attachment( Filename => $FileName . '.xlsx', ContentType => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", Content => $Excel, ); } } # PDF output elsif ( $GetParam{ResultForm} eq 'Print' ) { # get PDF object my $PDFObject = $Kernel::OM->Get('Kernel::System::PDF'); my @PDFData; for my $TicketID (@ViewableTicketIDs) { # Get ticket data. my %Ticket = $TicketObject->TicketGet( TicketID => $TicketID, DynamicFields => 1, UserID => $Self->{UserID}, ); # Get last customer article. my @Articles = $ArticleObject->ArticleList( TicketID => $TicketID, SenderType => 'customer', OnlyLast => 1, ); # If the ticket has no customer article, get the last agent article. if ( !@Articles ) { @Articles = $ArticleObject->ArticleList( TicketID => $TicketID, SenderType => 'agent', OnlyLast => 1, ); } # Finally, if everything failed, get latest article. if ( !@Articles ) { @Articles = $ArticleObject->ArticleList( TicketID => $TicketID, OnlyLast => 1, ); } my %Article = $ArticleObject->BackendForArticle( %{ $Articles[0] } )->ArticleGet( %{ $Articles[0] }, DynamicFields => 1, ); my %Data; if ( !%Article ) { %Data = %Ticket; # Set missing information. $Data{Subject} = $Data{Title} || Translatable('Untitled'); $Data{From} = '--'; } else { %Data = ( %Ticket, %Article ); } # customer info my %CustomerData; if ( $Data{CustomerUserID} ) { %CustomerData = $CustomerUserObject->CustomerUserDataGet( User => $Data{CustomerUserID}, ); } elsif ( $Data{CustomerID} ) { %CustomerData = $CustomerUserObject->CustomerUserDataGet( CustomerID => $Data{CustomerID}, ); } # customer info (customer name) if ( $CustomerData{UserLogin} ) { $Data{CustomerName} = $CustomerUserObject->CustomerName( UserLogin => $CustomerData{UserLogin}, ); } # user info my %UserInfo = $UserObject->GetUserData( User => $Data{Owner}, ); # customer info string $UserInfo{CustomerName} = '(' . $UserInfo{CustomerName} . ')' if ( $UserInfo{CustomerName} ); my %Info = ( %Data, %UserInfo ); my $Created = $LayoutObject->{LanguageObject}->FormatTimeString( $Data{CreateTime} // $Data{Created}, 'DateFormat', ); my $Owner = "$Info{Owner} ($Info{UserFullname})"; my $Customer = "$Data{CustomerID} $Data{CustomerName}"; my @PDFRow; push @PDFRow, $Data{TicketNumber}; push @PDFRow, $Created; push @PDFRow, $Data{From}; push @PDFRow, $Data{Subject}; push @PDFRow, $Data{State}; push @PDFRow, $Data{Queue}; push @PDFRow, $Owner; push @PDFRow, $Customer; push @PDFData, \@PDFRow; } my $Title = $LayoutObject->{LanguageObject}->Translate('Ticket') . ' ' . $LayoutObject->{LanguageObject}->Translate('Search'); my $PrintedBy = $LayoutObject->{LanguageObject}->Translate('printed by'); my $Page = $LayoutObject->{LanguageObject}->Translate('Page'); my $DateTimeString = $Kernel::OM->Create('Kernel::System::DateTime')->ToString(); my $Time = $LayoutObject->{LanguageObject}->FormatTimeString( $DateTimeString, 'DateFormat', ); # get maximum number of pages my $MaxPages = $ConfigObject->Get('PDF::MaxPages'); if ( !$MaxPages || $MaxPages < 1 || $MaxPages > 1000 ) { $MaxPages = 100; } my $CellData; # verify if there are tickets to show if (@PDFData) { # create the header $CellData->[0]->[0]->{Content} = $ConfigObject->Get('Ticket::Hook'); $CellData->[0]->[0]->{Font} = 'ProportionalBold'; $CellData->[0]->[1]->{Content} = $LayoutObject->{LanguageObject}->Translate('Created'); $CellData->[0]->[1]->{Font} = 'ProportionalBold'; $CellData->[0]->[2]->{Content} = $LayoutObject->{LanguageObject}->Translate('From'); $CellData->[0]->[2]->{Font} = 'ProportionalBold'; $CellData->[0]->[3]->{Content} = $LayoutObject->{LanguageObject}->Translate('Subject'); $CellData->[0]->[3]->{Font} = 'ProportionalBold'; $CellData->[0]->[4]->{Content} = $LayoutObject->{LanguageObject}->Translate('State'); $CellData->[0]->[4]->{Font} = 'ProportionalBold'; $CellData->[0]->[5]->{Content} = $LayoutObject->{LanguageObject}->Translate('Queue'); $CellData->[0]->[5]->{Font} = 'ProportionalBold'; $CellData->[0]->[6]->{Content} = $LayoutObject->{LanguageObject}->Translate('Owner'); $CellData->[0]->[6]->{Font} = 'ProportionalBold'; $CellData->[0]->[7]->{Content} = $LayoutObject->{LanguageObject}->Translate('CustomerID'); $CellData->[0]->[7]->{Font} = 'ProportionalBold'; # create the content array my $CounterRow = 1; for my $Row (@PDFData) { my $CounterColumn = 0; for my $Content ( @{$Row} ) { $CellData->[$CounterRow]->[$CounterColumn]->{Content} = $Content; $CounterColumn++; } $CounterRow++; } } # otherwise, show 'No ticket data found' message else { $CellData->[0]->[0]->{Content} = $LayoutObject->{LanguageObject}->Translate('No ticket data found.'); } # page params my %PageParam; $PageParam{PageOrientation} = 'landscape'; $PageParam{MarginTop} = 30; $PageParam{MarginRight} = 40; $PageParam{MarginBottom} = 40; $PageParam{MarginLeft} = 40; $PageParam{HeaderRight} = $Title; $PageParam{HeadlineLeft} = $Title; # table params my %TableParam; $TableParam{CellData} = $CellData; $TableParam{Type} = 'Cut'; $TableParam{FontSize} = 6; $TableParam{Border} = 0; $TableParam{BackgroundColorEven} = '#DDDDDD'; $TableParam{Padding} = 1; $TableParam{PaddingTop} = 3; $TableParam{PaddingBottom} = 3; # create new pdf document $PDFObject->DocumentNew( Title => $ConfigObject->Get('Product') . ': ' . $Title, Encode => $LayoutObject->{UserCharset}, ); # start table output $PDFObject->PageNew( %PageParam, FooterRight => $Page . ' 1', ); $PDFObject->PositionSet( Move => 'relativ', Y => -6, ); # output title $PDFObject->Text( Text => $Title, FontSize => 13, ); $PDFObject->PositionSet( Move => 'relativ', Y => -6, ); # output "printed by" $PDFObject->Text( Text => $PrintedBy . ' ' . $Self->{UserFullname} . ' (' . $Self->{UserEmail} . ')' . ', ' . $Time, FontSize => 9, ); $PDFObject->PositionSet( Move => 'relativ', Y => -14, ); PAGE: for my $PageNumber ( 2 .. $MaxPages ) { # output table (or a fragment of it) %TableParam = $PDFObject->Table(%TableParam); # stop output or another page if ( $TableParam{State} ) { last PAGE; } else { $PDFObject->PageNew( %PageParam, FooterRight => $Page . ' ' . $PageNumber, ); } } # return the pdf document my $CurDateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime'); my $Filename = sprintf( 'ticket_search_%s.pdf', $CurDateTimeObject->Format( Format => '%Y-%m-%d_%H-%M' ) ); my $PDFString = $PDFObject->DocumentOutput(); return $LayoutObject->Attachment( Filename => $Filename, ContentType => "application/pdf", Content => $PDFString, Type => 'inline', ); } else { # redirect to the ticketzoom if result of the search is only one if ( scalar @ViewableTicketIDs eq 1 && !$Self->{TakeLastSearch} ) { return $LayoutObject->Redirect( OP => "Action=AgentTicketZoom;TicketID=$ViewableTicketIDs[0]", ); } # store last overview screen my $URL = "Action=AgentTicketSearch;Subaction=Search" . ";Profile=" . $LayoutObject->LinkEncode( $Self->{Profile} ) . ";SortBy=" . $LayoutObject->LinkEncode( $Self->{SortBy} ) . ";OrderBy=" . $LayoutObject->LinkEncode( $Self->{OrderBy} ) . ";TakeLastSearch=1;StartHit=" . $LayoutObject->LinkEncode( $Self->{StartHit} ); $Kernel::OM->Get('Kernel::System::AuthSession')->UpdateSessionID( SessionID => $Self->{SessionID}, Key => 'LastScreenOverview', Value => $URL, ); # start HTML page my $Output = $LayoutObject->Header(); $Output .= $LayoutObject->NavigationBar(); # Notify if there are tickets which are not updated. $Output .= $LayoutObject->NotifyNonUpdatedTickets() // ''; $Self->{Filter} = $ParamObject->GetParam( Param => 'Filter' ) || ''; $Self->{View} = $ParamObject->GetParam( Param => 'View' ) || ''; # show tickets my $LinkPage = 'Filter=' . $LayoutObject->LinkEncode( $Self->{Filter} ) . ';View=' . $LayoutObject->LinkEncode( $Self->{View} ) . ';SortBy=' . $LayoutObject->LinkEncode( $Self->{SortBy} ) . ';OrderBy=' . $LayoutObject->LinkEncode( $Self->{OrderBy} ) . ';Profile=' . $LayoutObject->LinkEncode( $Self->{Profile} ) . ';TakeLastSearch=1;Subaction=Search' . ';'; my $LinkSort = 'Filter=' . $LayoutObject->LinkEncode( $Self->{Filter} ) . ';View=' . $LayoutObject->LinkEncode( $Self->{View} ) . ';Profile=' . $LayoutObject->LinkEncode( $Self->{Profile} ) . ';TakeLastSearch=1;Subaction=Search' . ';'; my $LinkFilter = 'TakeLastSearch=1;Subaction=Search;Profile=' . $LayoutObject->LinkEncode( $Self->{Profile} ) . ';'; my $LinkBack = 'Subaction=LoadProfile;Profile=' . $LayoutObject->LinkEncode( $Self->{Profile} ) . ';TakeLastSearch=1&'; my $FilterLink = 'SortBy=' . $LayoutObject->LinkEncode( $Self->{SortBy} ) . ';OrderBy=' . $LayoutObject->LinkEncode( $Self->{OrderBy} ) . ';View=' . $LayoutObject->LinkEncode( $Self->{View} ) . ';Profile=' . $LayoutObject->LinkEncode( $Self->{Profile} ) . ';TakeLastSearch=1;Subaction=Search' . ';'; $Output .= $LayoutObject->TicketListShow( TicketIDs => \@ViewableTicketIDs, Total => scalar @ViewableTicketIDs, View => $Self->{View}, Env => $Self, LinkPage => $LinkPage, LinkSort => $LinkSort, LinkFilter => $LinkFilter, LinkBack => $LinkBack, Profile => $Self->{Profile}, ProfileName => $Self->{ProfileName}, TitleName => Translatable('Search Results'), Bulk => 1, Limit => $Self->{SearchLimit}, Filter => $Self->{Filter}, OrderBy => $Self->{OrderBy}, SortBy => $Self->{SortBy}, RequestedURL => 'Action=' . $Self->{Action} . ';' . $LinkPage, # do not print the result earlier, but return complete content Output => 1, ); # build footer $Output .= $LayoutObject->Footer(); return $Output; } } elsif ( $Self->{Subaction} eq 'AJAXProfileDelete' ) { my $Profile = $ParamObject->GetParam( Param => 'Profile' ); # remove old profile stuff $SearchProfileObject->SearchProfileDelete( Base => 'TicketSearch', Name => $Profile, UserLogin => $Self->{UserLogin}, ); my $Output = $LayoutObject->JSONEncode( Data => 1, ); # TODO: why not application/json return $LayoutObject->Attachment( NoCache => 1, ContentType => 'text/html', Content => $Output, Type => 'inline' ); } elsif ( $Self->{Subaction} eq 'AJAXStopWordCheck' ) { my $StopWordCheckResult = { FoundStopWords => [], }; if ( $Kernel::OM->Get('Kernel::System::Ticket::Article')->SearchStringStopWordsUsageWarningActive() ) { my @ParamNames = $ParamObject->GetParamNames(); my %SearchStrings; SEARCHSTRINGPARAMNAME: for my $SearchStringParamName ( sort @ParamNames ) { next SEARCHSTRINGPARAMNAME if $SearchStringParamName !~ m{\ASearchStrings\[(.*)\]\z}sm; $SearchStrings{$1} = $ParamObject->GetParam( Param => $SearchStringParamName ); } $StopWordCheckResult->{FoundStopWords} = $Kernel::OM->Get('Kernel::System::Ticket::Article')->SearchStringStopWordsFind( SearchStrings => \%SearchStrings, ); } my $Output = $LayoutObject->JSONEncode( Data => $StopWordCheckResult, ); # TODO: why not application/json return $LayoutObject->Attachment( NoCache => 1, ContentType => 'text/html', Charset => $LayoutObject->{UserCharset}, Content => $Output, Type => 'inline' ); } elsif ( $Self->{Subaction} eq 'AJAX' ) { my $Profile = $ParamObject->GetParam( Param => 'Profile' ) || ''; my $EmptySearch = $ParamObject->GetParam( Param => 'EmptySearch' ); if ( !$Profile ) { $EmptySearch = 1; } my %GetParam = $SearchProfileObject->SearchProfileGet( Base => 'TicketSearch', Name => $Profile, UserLogin => $Self->{UserLogin}, ); # convert attributes if ( IsArrayRefWithData( $GetParam{ShownAttributes} ) ) { my @ShowAttributes = grep {defined} @{ $GetParam{ShownAttributes} }; $GetParam{ShownAttributes} = join ';', @ShowAttributes; } # if no profile is used, set default params of default attributes if ( !$Profile ) { if ( $Config->{Defaults} ) { KEY: for my $Key ( sort keys %{ $Config->{Defaults} } ) { next KEY if !$Config->{Defaults}->{$Key}; next KEY if $Key eq 'DynamicField'; if ( $Key =~ /^(Ticket|Article)(Create|Change|Close|Escalation)/ ) { my @Items = split /;/, $Config->{Defaults}->{$Key}; for my $Item (@Items) { my ( $Key, $Value ) = split /=/, $Item; $GetParam{$Key} = $Value; } } else { $GetParam{$Key} = $Config->{Defaults}->{$Key}; } } } } my @Attributes = ( # Main fields { Key => 'TicketNumber', Value => Translatable('Ticket Number'), }, { Key => 'Fulltext', Value => Translatable('Fulltext'), }, { Key => 'Title', Value => Translatable('Title'), }, { Key => '', Value => '-', Disabled => 1, }, ); for my $ArticleFieldKey ( sort keys %ArticleSearchableFields ) { push @Attributes, ( { Key => $ArticleSearchableFields{$ArticleFieldKey}->{Key}, Value => Translatable( $ArticleSearchableFields{$ArticleFieldKey}->{Label} ), }, ); } # Ticket fields push @Attributes, ( { Key => '', Value => '-', Disabled => 1, }, { Key => 'CustomerID', Value => Translatable('CustomerID (complex search)'), }, { Key => 'CustomerIDRaw', Value => Translatable('CustomerID (exact match)'), }, { Key => 'CustomerUserLogin', Value => Translatable('Assigned to Customer User Login (complex search)'), }, { Key => 'CustomerUserLoginRaw', Value => Translatable('Assigned to Customer User Login (exact match)'), }, { Key => 'CustomerUserID', Value => Translatable('Accessible to Customer User Login (exact match)'), }, { Key => 'StateIDs', Value => Translatable('State'), }, { Key => 'PriorityIDs', Value => Translatable('Priority'), }, { Key => 'LockIDs', Value => Translatable('Lock'), }, { Key => 'QueueIDs', Value => Translatable('Queue'), }, { Key => 'CreatedQueueIDs', Value => Translatable('Created in Queue'), }, ); if ( $ConfigObject->Get('Ticket::Type') ) { push @Attributes, ( { Key => 'TypeIDs', Value => Translatable('Type'), }, ); } if ( $ConfigObject->Get('Ticket::Service') ) { push @Attributes, ( { Key => 'ServiceIDs', Value => Translatable('Service'), }, { Key => 'SLAIDs', Value => Translatable('SLA'), }, ); } push @Attributes, ( { Key => 'OwnerIDs', Value => Translatable('Owner'), }, { Key => 'CreatedUserIDs', Value => Translatable('Created by'), }, ); if ( $ConfigObject->Get('Ticket::Watcher') ) { push @Attributes, ( { Key => 'WatchUserIDs', Value => Translatable('Watcher'), }, ); } if ( $ConfigObject->Get('Ticket::Responsible') ) { push @Attributes, ( { Key => 'ResponsibleIDs', Value => Translatable('Responsible'), }, ); } # Time fields push @Attributes, ( { Key => '', Value => '-', Disabled => 1, }, { Key => 'TicketLastChangeTimePoint', Value => Translatable('Ticket Last Change Time (before/after)'), }, { Key => 'TicketLastChangeTimeSlot', Value => Translatable('Ticket Last Change Time (between)'), }, { Key => 'TicketChangeTimePoint', Value => Translatable('Ticket Change Time (before/after)'), }, { Key => 'TicketChangeTimeSlot', Value => Translatable('Ticket Change Time (between)'), }, { Key => 'TicketCloseTimePoint', Value => Translatable('Ticket Close Time (before/after)'), }, { Key => 'TicketCloseTimeSlot', Value => Translatable('Ticket Close Time (between)'), }, { Key => 'TicketCreateTimePoint', Value => Translatable('Ticket Create Time (before/after)'), }, { Key => 'TicketCreateTimeSlot', Value => Translatable('Ticket Create Time (between)'), }, { Key => 'TicketPendingTimePoint', Value => Translatable('Ticket Pending Until Time (before/after)'), }, { Key => 'TicketPendingTimeSlot', Value => Translatable('Ticket Pending Until Time (between)'), }, { Key => 'TicketEscalationTimePoint', Value => Translatable('Ticket Escalation Time (before/after)'), }, { Key => 'TicketEscalationTimeSlot', Value => Translatable('Ticket Escalation Time (between)'), }, { Key => 'ArticleCreateTimePoint', Value => Translatable('Article Create Time (before/after)'), }, { Key => 'ArticleCreateTimeSlot', Value => Translatable('Article Create Time (between)'), }, ); if ( $ConfigObject->Get('Ticket::ArchiveSystem') ) { push @Attributes, ( { Key => 'SearchInArchive', Value => Translatable('Archive Search'), }, ); } # Dynamic fields my $DynamicFieldSeparator = 1; # create dynamic fields search options for attribute select # cycle trough the activated Dynamic Fields for this screen DYNAMICFIELD: for my $DynamicFieldConfig ( @{$DynamicField} ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); next DYNAMICFIELD if !$DynamicFieldConfig->{Name}; next DYNAMICFIELD if $DynamicFieldConfig->{Name} eq ''; # create a separator for dynamic fields attributes if ($DynamicFieldSeparator) { push @Attributes, ( { Key => '', Value => '-', Disabled => 1, }, ); $DynamicFieldSeparator = 0; } # get search field preferences my $SearchFieldPreferences = $BackendObject->SearchFieldPreferences( DynamicFieldConfig => $DynamicFieldConfig, ); next DYNAMICFIELD if !IsArrayRefWithData($SearchFieldPreferences); # translate the dynamic field label my $TranslatedDynamicFieldLabel = $LayoutObject->{LanguageObject}->Translate( $DynamicFieldConfig->{Label}, ); PREFERENCE: for my $Preference ( @{$SearchFieldPreferences} ) { # translate the suffix my $TranslatedSuffix = $LayoutObject->{LanguageObject}->Translate( $Preference->{LabelSuffix}, ) || ''; if ($TranslatedSuffix) { $TranslatedSuffix = ' (' . $TranslatedSuffix . ')'; } push @Attributes, ( { Key => 'Search_DynamicField_' . $DynamicFieldConfig->{Name} . $Preference->{Type}, Value => $TranslatedDynamicFieldLabel . $TranslatedSuffix, }, ); } } # create HTML strings for all dynamic fields my %DynamicFieldHTML; # cycle trough the activated Dynamic Fields for this screen DYNAMICFIELD: for my $DynamicFieldConfig ( @{$DynamicField} ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); my $PossibleValuesFilter; my $IsACLReducible = $BackendObject->HasBehavior( DynamicFieldConfig => $DynamicFieldConfig, Behavior => 'IsACLReducible', ); if ($IsACLReducible) { # get PossibleValues my $PossibleValues = $BackendObject->PossibleValuesGet( DynamicFieldConfig => $DynamicFieldConfig, ); # check if field has PossibleValues property in its configuration if ( IsHashRefWithData($PossibleValues) ) { # get historical values from database my $HistoricalValues = $BackendObject->HistoricalValuesGet( DynamicFieldConfig => $DynamicFieldConfig, ); my $Data = $PossibleValues; # add historic values to current values (if they don't exist anymore) if ( IsHashRefWithData($HistoricalValues) ) { for my $Key ( sort keys %{$HistoricalValues} ) { if ( !$Data->{$Key} ) { $Data->{$Key} = $HistoricalValues->{$Key}; } } } # convert possible values key => value to key => key for ACLs using a Hash slice my %AclData = %{$Data}; @AclData{ keys %AclData } = keys %AclData; # set possible values filter from ACLs my $ACL = $TicketObject->TicketAcl( Action => $Self->{Action}, ReturnType => 'Ticket', ReturnSubType => 'DynamicField_' . $DynamicFieldConfig->{Name}, Data => \%AclData, UserID => $Self->{UserID}, ); if ($ACL) { my %Filter = $TicketObject->TicketAclData(); # convert Filer key => key back to key => value using map %{$PossibleValuesFilter} = map { $_ => $Data->{$_} } keys %Filter; } } } # get search field preferences my $SearchFieldPreferences = $BackendObject->SearchFieldPreferences( DynamicFieldConfig => $DynamicFieldConfig, ); next DYNAMICFIELD if !IsArrayRefWithData($SearchFieldPreferences); PREFERENCE: for my $Preference ( @{$SearchFieldPreferences} ) { # get field HTML $DynamicFieldHTML{ $DynamicFieldConfig->{Name} . $Preference->{Type} } = $BackendObject->SearchFieldRender( DynamicFieldConfig => $DynamicFieldConfig, Profile => \%GetParam, PossibleValuesFilter => $PossibleValuesFilter, DefaultValue => $Config->{Defaults}->{DynamicField} ->{ $DynamicFieldConfig->{Name} }, LayoutObject => $LayoutObject, Type => $Preference->{Type}, ); } } $Param{AttributesStrg} = $LayoutObject->BuildSelection( PossibleNone => 1, Data => \@Attributes, Name => 'Attribute', Multiple => 0, Class => 'Modernize', ); $Param{AttributesOrigStrg} = $LayoutObject->BuildSelection( PossibleNone => 1, Data => \@Attributes, Name => 'AttributeOrig', Multiple => 0, Class => 'Modernize', ); # get all users of own groups my %AllUsers = $UserObject->UserList( Type => 'Long', Valid => 0, ); # Rother OSS / TicketSearch-IncludeInvalidAgents # if ( !$ConfigObject->Get('Ticket::ChangeOwnerToEveryone') ) { if ( !$ConfigObject->Get('Ticket::ChangeOwnerToEveryone') && !$ConfigObject->Get('Ticket::Search')->{IncludeInvalidAgents} ) { # EO TicketSearch-IncludeInvalidAgents my %Involved = $Kernel::OM->Get('Kernel::System::Group')->PermissionUserInvolvedGet( UserID => $Self->{UserID}, Type => 'ro', ); for my $UserID ( sort keys %AllUsers ) { if ( !$Involved{$UserID} ) { delete $AllUsers{$UserID}; } } } my @ShownUsers; my %UsersInvalid; # get valid users of own groups my %ValidUsers = $UserObject->UserList( Type => 'Long', Valid => 1, ); USERID: for my $UserID ( sort { $AllUsers{$a} cmp $AllUsers{$b} } keys %AllUsers ) { if ( !$ValidUsers{$UserID} ) { $UsersInvalid{$UserID} = $AllUsers{$UserID}; next USERID; } push @ShownUsers, { Key => $UserID, Value => $AllUsers{$UserID}, }; } # also show invalid agents (if any) if ( scalar %UsersInvalid ) { push @ShownUsers, { Key => '-', Value => '_____________________', Disabled => 1, }; push @ShownUsers, { Key => '-', Value => $LayoutObject->{LanguageObject}->Translate('Invalid Users'), Disabled => 1, }; push @ShownUsers, { Key => '-', Value => '', Disabled => 1, }; for my $UserID ( sort { $UsersInvalid{$a} cmp $UsersInvalid{$b} } keys %UsersInvalid ) { push @ShownUsers, { Key => $UserID, Value => $UsersInvalid{$UserID}, }; } } $Param{UserStrg} = $LayoutObject->BuildSelection( Data => \@ShownUsers, Name => 'OwnerIDs', Multiple => 1, Size => 5, SelectedID => $GetParam{OwnerIDs}, Class => 'Modernize', ); $Param{CreatedUserStrg} = $LayoutObject->BuildSelection( Data => \@ShownUsers, Name => 'CreatedUserIDs', Multiple => 1, Size => 5, SelectedID => $GetParam{CreatedUserIDs}, Class => 'Modernize', ); if ( $ConfigObject->Get('Ticket::Watcher') ) { $Param{WatchUserStrg} = $LayoutObject->BuildSelection( Data => \@ShownUsers, Name => 'WatchUserIDs', Multiple => 1, Size => 5, SelectedID => $GetParam{WatchUserIDs}, Class => 'Modernize', ); } if ( $ConfigObject->Get('Ticket::Responsible') ) { $Param{ResponsibleStrg} = $LayoutObject->BuildSelection( Data => \@ShownUsers, Name => 'ResponsibleIDs', Multiple => 1, Size => 5, SelectedID => $GetParam{ResponsibleIDs}, Class => 'Modernize', ); } # build service string if ( $ConfigObject->Get('Ticket::Service') ) { my %Service = $Kernel::OM->Get('Kernel::System::Service')->ServiceList( UserID => $Self->{UserID}, KeepChildren => $ConfigObject->Get('Ticket::Service::KeepChildren'), ); $Param{ServicesStrg} = $LayoutObject->BuildSelection( Data => \%Service, Name => 'ServiceIDs', SelectedID => $GetParam{ServiceIDs}, TreeView => $TreeView, Sort => 'TreeView', Size => 5, Multiple => 1, Translation => 0, Max => 200, Class => 'Modernize', ); my %SLA = $Kernel::OM->Get('Kernel::System::SLA')->SLAList( UserID => $Self->{UserID}, ); $Param{SLAsStrg} = $LayoutObject->BuildSelection( Data => \%SLA, Name => 'SLAIDs', SelectedID => $GetParam{SLAIDs}, Sort => 'AlphanumericValue', Size => 5, Multiple => 1, Translation => 0, Max => 200, Class => 'Modernize', ); } $Param{ResultFormStrg} = $LayoutObject->BuildSelection( Data => { Normal => Translatable('Normal'), Print => Translatable('Print'), CSV => Translatable('CSV'), Excel => Translatable('Excel'), }, Name => 'ResultForm', SelectedID => $GetParam{ResultForm} || 'Normal', Class => 'Modernize', ); if ( $ConfigObject->Get('Ticket::ArchiveSystem') ) { $Param{SearchInArchiveStrg} = $LayoutObject->BuildSelection( Data => { ArchivedTickets => Translatable('Archived tickets'), NotArchivedTickets => Translatable('Unarchived tickets'), AllTickets => Translatable('All tickets'), }, Name => 'SearchInArchive', SelectedID => $GetParam{SearchInArchive} || 'NotArchivedTickets', Class => 'Modernize', ); } my %Profiles = $SearchProfileObject->SearchProfileList( Base => 'TicketSearch', UserLogin => $Self->{UserLogin}, ); if ( $Profiles{'last-search'} ) { $Profiles{'last-search'} = $LayoutObject->{LanguageObject}->Translate('last-search'); } $Param{ProfilesStrg} = $LayoutObject->BuildSelection( Data => \%Profiles, Name => 'Profile', ID => 'SearchProfile', SelectedID => $Profile, Class => 'Modernize', Translation => 0, PossibleNone => 1, ); $Param{StatesStrg} = $LayoutObject->BuildSelection( Data => { $StateObject->StateList( UserID => $Self->{UserID}, Action => $Self->{Action}, ), }, Name => 'StateIDs', Multiple => 1, Size => 5, SelectedID => $GetParam{StateIDs}, Class => 'Modernize', ); my %AllQueues = $Kernel::OM->Get('Kernel::System::Queue')->GetAllQueues( UserID => $Self->{UserID}, Type => 'ro', ); $Param{QueuesStrg} = $LayoutObject->AgentQueueListOption( Data => \%AllQueues, Size => 5, Multiple => 1, Name => 'QueueIDs', TreeView => $TreeView, SelectedIDRefArray => $GetParam{QueueIDs}, OnChangeSubmit => 0, Class => 'Modernize', ); $Param{CreatedQueuesStrg} = $LayoutObject->AgentQueueListOption( Data => \%AllQueues, Size => 5, Multiple => 1, Name => 'CreatedQueueIDs', TreeView => $TreeView, SelectedIDRefArray => $GetParam{CreatedQueueIDs}, OnChangeSubmit => 0, Class => 'Modernize', ); $Param{PrioritiesStrg} = $LayoutObject->BuildSelection( Data => { $TicketObject->TicketPriorityList( UserID => $Self->{UserID}, Action => $Self->{Action}, ), }, Name => 'PriorityIDs', Multiple => 1, Size => 5, SelectedID => $GetParam{PriorityIDs}, Class => 'Modernize', ); $Param{LocksStrg} = $LayoutObject->BuildSelection( Data => { $Kernel::OM->Get('Kernel::System::Lock')->LockList( UserID => $Self->{UserID}, Action => $Self->{Action}, ), }, Name => 'LockIDs', Multiple => 1, Size => 5, SelectedID => $GetParam{LockIDs}, Class => 'Modernize', ); $Param{ArticleCreateTimePoint} = $LayoutObject->BuildSelection( Data => [ 1 .. 59 ], Name => 'ArticleCreateTimePoint', SelectedID => $GetParam{ArticleCreateTimePoint}, ); $Param{ArticleCreateTimePointStart} = $LayoutObject->BuildSelection( Data => { 'Last' => Translatable('within the last ...'), 'Before' => Translatable('more than ... ago'), }, Name => 'ArticleCreateTimePointStart', SelectedID => $GetParam{ArticleCreateTimePointStart} || 'Last', ); $Param{ArticleCreateTimePointFormat} = $LayoutObject->BuildSelection( Data => { minute => Translatable('minute(s)'), hour => Translatable('hour(s)'), day => Translatable('day(s)'), week => Translatable('week(s)'), month => Translatable('month(s)'), year => Translatable('year(s)'), }, Name => 'ArticleCreateTimePointFormat', SelectedID => $GetParam{ArticleCreateTimePointFormat}, ); $Param{ArticleCreateTimeStart} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'ArticleCreateTimeStart', Format => 'DateInputFormat', DiffTime => -( ( 60 * 60 * 24 ) * 30 ), Validate => 1, ValidateDateBeforePrefix => 'ArticleCreateTimeStop', ); $Param{ArticleCreateTimeStop} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'ArticleCreateTimeStop', Format => 'DateInputFormat', Validate => 1, ValidateDateAfterPrefix => 'ArticleCreateTimeStart', ); $Param{TicketCreateTimePoint} = $LayoutObject->BuildSelection( Data => [ 1 .. 59 ], Name => 'TicketCreateTimePoint', SelectedID => $GetParam{TicketCreateTimePoint}, ); $Param{TicketCreateTimePointStart} = $LayoutObject->BuildSelection( Data => { 'Last' => Translatable('within the last ...'), 'Before' => Translatable('more than ... ago'), }, Name => 'TicketCreateTimePointStart', SelectedID => $GetParam{TicketCreateTimePointStart} || 'Last', ); $Param{TicketCreateTimePointFormat} = $LayoutObject->BuildSelection( Data => { minute => Translatable('minute(s)'), hour => Translatable('hour(s)'), day => Translatable('day(s)'), week => Translatable('week(s)'), month => Translatable('month(s)'), year => Translatable('year(s)'), }, Name => 'TicketCreateTimePointFormat', SelectedID => $GetParam{TicketCreateTimePointFormat}, ); $Param{TicketCreateTimeStart} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketCreateTimeStart', Format => 'DateInputFormat', DiffTime => -( ( 60 * 60 * 24 ) * 30 ), Validate => 1, ValidateDateBeforePrefix => 'TicketCreateTimeStop', ); $Param{TicketCreateTimeStop} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketCreateTimeStop', Format => 'DateInputFormat', Validate => 1, ValidateDateAfterPrefix => 'TicketCreateTimeStart', ); $Param{TicketPendingTimePoint} = $LayoutObject->BuildSelection( Data => [ 1 .. 59 ], Name => 'TicketPendingTimePoint', SelectedID => $GetParam{TicketPendingTimePoint}, ); $Param{TicketPendingTimePointStart} = $LayoutObject->BuildSelection( Data => { 'Last' => Translatable('within the last ...'), 'Next' => Translatable('within the next ...'), 'Before' => Translatable('more than ... ago'), 'After' => Translatable('in more than ...'), }, Name => 'TicketPendingTimePointStart', SelectedID => $GetParam{TicketPendingTimePointStart} || 'Next', ); $Param{TicketPendingTimePointFormat} = $LayoutObject->BuildSelection( Data => { minute => Translatable('minute(s)'), hour => Translatable('hour(s)'), day => Translatable('day(s)'), week => Translatable('week(s)'), month => Translatable('month(s)'), year => Translatable('year(s)'), }, Name => 'TicketPendingTimePointFormat', SelectedID => $GetParam{TicketPendingTimePointFormat}, ); $Param{TicketPendingTimeStart} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketPendingTimeStart', Format => 'DateInputFormat', DiffTime => -( ( 60 * 60 * 24 ) * 30 ), Validate => 1, ValidateDateBeforePrefix => 'TicketPendingTimeStop', ); $Param{TicketPendingTimeStop} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketPendingTimeStop', Format => 'DateInputFormat', Validate => 1, ValidateDateAfterPrefix => 'TicketPendingTimeStart', ); $Param{TicketChangeTimePoint} = $LayoutObject->BuildSelection( Data => [ 1 .. 59 ], Name => 'TicketChangeTimePoint', SelectedID => $GetParam{TicketChangeTimePoint}, ); $Param{TicketChangeTimePointStart} = $LayoutObject->BuildSelection( Data => { 'Last' => 'within the last ...', 'Before' => 'more than ... ago', }, Name => 'TicketChangeTimePointStart', SelectedID => $GetParam{TicketChangeTimePointStart} || 'Last', ); $Param{TicketChangeTimePointFormat} = $LayoutObject->BuildSelection( Data => { minute => Translatable('minute(s)'), hour => Translatable('hour(s)'), day => Translatable('day(s)'), week => Translatable('week(s)'), month => Translatable('month(s)'), year => Translatable('year(s)'), }, Name => 'TicketChangeTimePointFormat', SelectedID => $GetParam{TicketChangeTimePointFormat}, ); $Param{TicketChangeTimeStart} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketChangeTimeStart', Format => 'DateInputFormat', DiffTime => -( ( 60 * 60 * 24 ) * 30 ), Validate => 1, ValidateDateBeforePrefix => 'TicketChangeTimeStop', ); $Param{TicketChangeTimeStop} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketChangeTimeStop', Format => 'DateInputFormat', Validate => 1, ValidateDateAfterPrefix => 'TicketChangeTimeStart', ); $Param{TicketCloseTimePoint} = $LayoutObject->BuildSelection( Data => [ 1 .. 59 ], Name => 'TicketCloseTimePoint', SelectedID => $GetParam{TicketCloseTimePoint}, ); $Param{TicketCloseTimePointStart} = $LayoutObject->BuildSelection( Data => { 'Last' => Translatable('within the last ...'), 'Before' => Translatable('more than ... ago'), }, Name => 'TicketCloseTimePointStart', SelectedID => $GetParam{TicketCloseTimePointStart} || 'Last', ); $Param{TicketCloseTimePointFormat} = $LayoutObject->BuildSelection( Data => { minute => Translatable('minute(s)'), hour => Translatable('hour(s)'), day => Translatable('day(s)'), week => Translatable('week(s)'), month => Translatable('month(s)'), year => Translatable('year(s)'), }, Name => 'TicketCloseTimePointFormat', SelectedID => $GetParam{TicketCloseTimePointFormat}, ); $Param{TicketCloseTimeStart} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketCloseTimeStart', Format => 'DateInputFormat', DiffTime => -( ( 60 * 60 * 24 ) * 30 ), Validate => 1, ValidateDateBeforePrefix => 'TicketCloseTimeStop', ); $Param{TicketCloseTimeStop} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketCloseTimeStop', Format => 'DateInputFormat', Validate => 1, ValidateDateAfterPrefix => 'TicketCloseTimeStart', ); $Param{TicketLastChangeTimePoint} = $LayoutObject->BuildSelection( Data => [ 1 .. 59 ], Name => 'TicketLastChangeTimePoint', SelectedID => $GetParam{TicketLastChangeTimePoint}, ); $Param{TicketLastChangeTimePointStart} = $LayoutObject->BuildSelection( Data => { 'Last' => Translatable('within the last ...'), 'Before' => Translatable('more than ... ago'), }, Name => 'TicketLastChangeTimePointStart', SelectedID => $GetParam{TicketLastChangeTimePointStart} || 'Last', ); $Param{TicketLastChangeTimePointFormat} = $LayoutObject->BuildSelection( Data => { minute => Translatable('minute(s)'), hour => Translatable('hour(s)'), day => Translatable('day(s)'), week => Translatable('week(s)'), month => Translatable('month(s)'), year => Translatable('year(s)'), }, Name => 'TicketLastChangeTimePointFormat', SelectedID => $GetParam{TicketLastChangeTimePointFormat}, ); $Param{TicketLastChangeTimeStart} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketLastChangeTimeStart', Format => 'DateInputFormat', DiffTime => -( ( 60 * 60 * 24 ) * 30 ), Validate => 1, ValidateDateBeforePrefix => 'TicketLastChangeTimeStop', ); $Param{TicketLastChangeTimeStop} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketLastChangeTimeStop', Format => 'DateInputFormat', Validate => 1, ValidateDateAfterPrefix => 'TicketLastChangeTimeStart', ); $Param{TicketEscalationTimePoint} = $LayoutObject->BuildSelection( Data => [ 1 .. 59 ], Name => 'TicketEscalationTimePoint', SelectedID => $GetParam{TicketEscalationTimePoint}, ); $Param{TicketEscalationTimePointStart} = $LayoutObject->BuildSelection( Data => { 'Last' => Translatable('within the last ...'), 'Next' => Translatable('within the next ...'), 'Before' => Translatable('more than ... ago'), }, Name => 'TicketEscalationTimePointStart', SelectedID => $GetParam{TicketEscalationTimePointStart} || 'Last', ); $Param{TicketEscalationTimePointFormat} = $LayoutObject->BuildSelection( Data => { minute => Translatable('minute(s)'), hour => Translatable('hour(s)'), day => Translatable('day(s)'), week => Translatable('week(s)'), month => Translatable('month(s)'), year => Translatable('year(s)'), }, Name => 'TicketEscalationTimePointFormat', SelectedID => $GetParam{TicketEscalationTimePointFormat}, ); $Param{TicketEscalationTimeStart} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketEscalationTimeStart', Format => 'DateInputFormat', DiffTime => -( ( 60 * 60 * 24 ) * 30 ), Validate => 1, ValidateDateBeforePrefix => 'TicketEscalationTimeStop', ); $Param{TicketEscalationTimeStop} = $LayoutObject->BuildDateSelection( %GetParam, Prefix => 'TicketEscalationTimeStop', Format => 'DateInputFormat', Validate => 1, ValidateDateAfterPrefix => 'TicketEscalationTimeStart', ); my %GetParamBackup = %GetParam; for my $Key ( qw(TicketEscalation TicketClose TicketChange TicketLastChange TicketPending TicketCreate ArticleCreate) ) { for my $SubKey (qw(TimeStart TimeStop TimePoint TimePointStart TimePointFormat)) { delete $GetParam{ $Key . $SubKey }; delete $GetParamBackup{ $Key . $SubKey }; } } # build type string if ( $ConfigObject->Get('Ticket::Type') ) { # get ticket object my %Type = $TicketObject->TicketTypeList( UserID => $Self->{UserID}, Action => $Self->{Action}, ); $Param{TypesStrg} = $LayoutObject->BuildSelection( Data => \%Type, Name => 'TypeIDs', SelectedID => $GetParam{TypeIDs}, Sort => 'AlphanumericValue', Size => 3, Multiple => 1, Translation => 0, Class => 'Modernize', ); } # html search mask output $LayoutObject->Block( Name => 'SearchAJAX', Data => { %Param, %GetParam, EmptySearch => $EmptySearch, }, ); # create the field entries to be displayed in the modal dialog for my $ArticleFieldKey ( sort keys %ArticleSearchableFields ) { $LayoutObject->Block( Name => 'SearchableArticleField', Data => { ArticleFieldLabel => $ArticleSearchableFields{$ArticleFieldKey}->{Label}, ArticleFieldKey => $ArticleSearchableFields{$ArticleFieldKey}->{Key}, ArticleFieldValue => $GetParam{$ArticleFieldKey} // '', }, ); } # output Dynamic fields blocks # cycle trough the activated Dynamic Fields for this screen DYNAMICFIELD: for my $DynamicFieldConfig ( @{$DynamicField} ) { next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig); # get search field preferences my $SearchFieldPreferences = $BackendObject->SearchFieldPreferences( DynamicFieldConfig => $DynamicFieldConfig, ); next DYNAMICFIELD if !IsArrayRefWithData($SearchFieldPreferences); PREFERENCE: for my $Preference ( @{$SearchFieldPreferences} ) { # skip fields that HTML could not be retrieved next PREFERENCE if !IsHashRefWithData( $DynamicFieldHTML{ $DynamicFieldConfig->{Name} . $Preference->{Type} } ); $LayoutObject->Block( Name => 'DynamicField', Data => { Label => $DynamicFieldHTML{ $DynamicFieldConfig->{Name} . $Preference->{Type} } ->{Label}, Field => $DynamicFieldHTML{ $DynamicFieldConfig->{Name} . $Preference->{Type} } ->{Field}, }, ); } } # compat. map for attributes my %Map = ( TimeSearchType => 'TicketCreate', ChangeTimeSearchType => 'TicketChange', CloseTimeSearchType => 'TicketClose', LastChangeTimeSearchType => 'TicketLastChange', PendingTimeSearchType => 'TicketPending', EscalationTimeSearchType => 'TicketEscalation', ArticleTimeSearchType => 'ArticleCreate', ); KEY: for my $Key ( sort keys %Map ) { next KEY if !defined $GetParamBackup{$Key}; if ( $GetParamBackup{$Key} eq 'TimePoint' ) { $GetParamBackup{ $Map{$Key} . 'TimePoint' } = 1; } elsif ( $GetParamBackup{$Key} eq 'TimeSlot' ) { $GetParamBackup{ $Map{$Key} . 'TimeSlot' } = 1; } } # attributes for search my @SearchAttributes; # show attributes my @ShownAttributes; if ( $GetParamBackup{ShownAttributes} ) { @ShownAttributes = split /;/, $GetParamBackup{ShownAttributes}; } my %AlreadyShown; if ($Profile) { ITEM: for my $Item (@Attributes) { my $Key = $Item->{Key}; next ITEM if !$Key; # check if shown if (@ShownAttributes) { my $Show = 0; SHOWN_ATTRIBUTE: for my $ShownAttribute (@ShownAttributes) { if ( 'Label' . $Key eq $ShownAttribute ) { $Show = 1; last SHOWN_ATTRIBUTE; } } next ITEM if !$Show; } else { # Skip undefined next ITEM if !defined $GetParamBackup{$Key}; # Skip empty strings next ITEM if $GetParamBackup{$Key} eq ''; # Skip empty arrays if ( ref $GetParamBackup{$Key} eq 'ARRAY' && !@{ $GetParamBackup{$Key} } ) { next ITEM; } } # show attribute next ITEM if $AlreadyShown{$Key}; $AlreadyShown{$Key} = 1; push @SearchAttributes, $Key; } } # No profile, show default screen else { # Merge regular show/hide settings and the settings for the dynamic fields my %Defaults = %{ $Config->{Defaults} || {} }; for my $DynamicFields ( sort keys %{ $Config->{DynamicField} || {} } ) { if ( $Config->{DynamicField}->{$DynamicFields} == 2 ) { $Defaults{"Search_DynamicField_$DynamicFields"} = 1; } } my @OrderedDefaults; if (%Defaults) { # ordering attributes on the same order like in Attributes for my $Item (@Attributes) { my $KeyAtr = $Item->{Key}; for my $Key ( sort keys %Defaults ) { if ( $Key eq $KeyAtr ) { push @OrderedDefaults, $Key; } } } KEY: for my $Key (@OrderedDefaults) { next KEY if $Key eq 'DynamicField'; # Ignore entry for DF config next KEY if $AlreadyShown{$Key}; $AlreadyShown{$Key} = 1; push @SearchAttributes, $Key; } } # If no attribute is shown, show fulltext search. if ( !keys %AlreadyShown ) { push @SearchAttributes, 'Fulltext'; } } $LayoutObject->AddJSData( Key => 'SearchAttributes', Value => \@SearchAttributes, ); my $Output = $LayoutObject->Output( TemplateFile => 'AgentTicketSearch', Data => \%Param, AJAX => 1, ); return $LayoutObject->Attachment( NoCache => 1, ContentType => 'text/html', Charset => $LayoutObject->{UserCharset}, Content => $Output, Type => 'inline', ); } # show default search screen $Output = $LayoutObject->Header(); $Output .= $LayoutObject->NavigationBar(); # Notify if there are tickets which are not updated. $Output .= $LayoutObject->NotifyNonUpdatedTickets() // ''; $LayoutObject->AddJSData( Key => 'NonAJAXSearch', Value => 1, ); if ( $Self->{Profile} ) { $LayoutObject->AddJSData( Key => 'Profile', Value => $Self->{Profile}, ); } $Output .= $LayoutObject->Output( TemplateFile => 'AgentTicketSearch', Data => \%Param, ); $Output .= $LayoutObject->Footer(); return $Output; } 1;