diff --git a/Custom/Kernel/Modules/AgentTicketSearch.pm b/Custom/Kernel/Modules/AgentTicketSearch.pm
new file mode 100644
index 0000000..ef6154b
--- /dev/null
+++ b/Custom/Kernel/Modules/AgentTicketSearch.pm
@@ -0,0 +1,2682 @@
+# --
+# 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;
diff --git a/Kernel/Config/Files/XML/TicketSearch-IncludeInvalidAgents.xml b/Kernel/Config/Files/XML/TicketSearch-IncludeInvalidAgents.xml
new file mode 100644
index 0000000..deacfb7
--- /dev/null
+++ b/Kernel/Config/Files/XML/TicketSearch-IncludeInvalidAgents.xml
@@ -0,0 +1,10 @@
+
+
+
+ When searching for tickets in agent attributes, include invalid agents.
+ Core::Ticket
+
+ - 0
+
+
+
diff --git a/TicketSearch-IncludeInvalidAgents.sopm b/TicketSearch-IncludeInvalidAgents.sopm
new file mode 100644
index 0000000..e5cf41b
--- /dev/null
+++ b/TicketSearch-IncludeInvalidAgents.sopm
@@ -0,0 +1,14 @@
+
+
+ TicketSearch-IncludeInvalidAgents
+ 10.1.0
+ 10.1.x
+ Rother OSS GmbH
+ https://rother-oss.com/
+ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007
+ This package adds the possibility to include invalid agents in the ticket search.
+
+
+
+
+