1# --
2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/
3# --
4# This software comes with ABSOLUTELY NO WARRANTY. For details, see
5# the enclosed file COPYING for license information (GPL). If you
6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
7# --
8
9package Kernel::Modules::AgentTicketZoom;
10
11use strict;
12use warnings;
13use utf8;
14
15our $ObjectManagerDisabled = 1;
16
17use POSIX qw/ceil/;
18use Kernel::System::EmailParser;
19use Kernel::System::VariableCheck qw(:all);
20use Kernel::Language qw(Translatable);
21
22sub new {
23    my ( $Type, %Param ) = @_;
24
25    # allocate new hash for object
26    my $Self = {%Param};
27    bless( $Self, $Type );
28
29    # set debug
30    $Self->{Debug} = 0;
31
32    # get needed objects
33    my $ParamObject  = $Kernel::OM->Get('Kernel::System::Web::Request');
34    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
35    my $UserObject   = $Kernel::OM->Get('Kernel::System::User');
36    my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
37
38    $Self->{ArticleID}      = $ParamObject->GetParam( Param => 'ArticleID' );
39    $Self->{ArticleView}    = $ParamObject->GetParam( Param => 'ArticleView' );
40    $Self->{ZoomExpand}     = $ParamObject->GetParam( Param => 'ZoomExpand' );
41    $Self->{ZoomExpandSort} = $ParamObject->GetParam( Param => 'ZoomExpandSort' );
42    $Self->{ZoomTimeline}   = $ParamObject->GetParam( Param => 'ZoomTimeline' );
43
44    my %UserPreferences = $UserObject->GetPreferences(
45        UserID => $Self->{UserID},
46    );
47
48    # save last used view type in preferences
49    if ( !$Self->{Subaction} ) {
50
51        if (
52            !defined $Self->{ArticleView}
53            && !defined $Self->{ZoomExpand}
54            && !defined $Self->{ZoomTimeline}
55            )
56        {
57            $Self->{ZoomExpand} = $ConfigObject->Get('Ticket::Frontend::AgentZoomExpand');
58            if ( $UserPreferences{UserLastUsedZoomViewType} ) {
59                if ( $UserPreferences{UserLastUsedZoomViewType} eq 'Expand' ) {
60                    $Self->{ZoomExpand} = 1;
61                }
62                elsif ( $UserPreferences{UserLastUsedZoomViewType} eq 'Collapse' ) {
63                    $Self->{ZoomExpand} = 0;
64                }
65                elsif ( $UserPreferences{UserLastUsedZoomViewType} eq 'Timeline' ) {
66                    $Self->{ZoomTimeline} = 1;
67                }
68            }
69        }
70
71        elsif (
72            defined $Self->{ArticleView}
73            || defined $Self->{ZoomExpand}
74            || defined $Self->{ZoomTimeline}
75            )
76        {
77            my $LastUsedZoomViewType = '';
78
79            if ( defined $Self->{ArticleView} ) {
80                $LastUsedZoomViewType = $Self->{ArticleView};
81
82                if ( $Self->{ArticleView} eq 'Expand' ) {
83                    $Self->{ZoomExpand} = 1;
84                }
85                elsif ( $Self->{ArticleView} eq 'Collapse' ) {
86                    $Self->{ZoomExpand} = 0;
87                }
88                elsif ( $Self->{ArticleView} eq 'Timeline' ) {
89                    $Self->{ZoomTimeline} = 1;
90                }
91                else {
92                    $LastUsedZoomViewType = $ConfigObject->Get('Ticket::Frontend::AgentZoomExpand')
93                        ? 'Expand'
94                        : 'Collapse';
95                }
96            }
97            elsif ( defined $Self->{ZoomExpand} && $Self->{ZoomExpand} == 1 ) {
98                $LastUsedZoomViewType = 'Expand';
99            }
100            elsif ( defined $Self->{ZoomExpand} && $Self->{ZoomExpand} == 0 ) {
101                $LastUsedZoomViewType = 'Collapse';
102            }
103            elsif ( defined $Self->{ZoomTimeline} && $Self->{ZoomTimeline} == 1 ) {
104                $LastUsedZoomViewType = 'Timeline';
105            }
106            $UserObject->SetPreferences(
107                UserID => $Self->{UserID},
108                Key    => 'UserLastUsedZoomViewType',
109                Value  => $LastUsedZoomViewType,
110            );
111        }
112    }
113
114    # Please note: ZoomTimeline is an OTRSBusiness feature
115    if ( !$ConfigObject->Get('TimelineViewEnabled') ) {
116        $Self->{ZoomTimeline} = 0;
117    }
118
119    if ( !defined $Self->{DoNotShowBrowserLinkMessage} ) {
120        if ( $UserPreferences{UserAgentDoNotShowBrowserLinkMessage} ) {
121            $Self->{DoNotShowBrowserLinkMessage} = 1;
122        }
123        else {
124            $Self->{DoNotShowBrowserLinkMessage} = 0;
125        }
126    }
127
128    if ( !defined $Self->{ZoomExpandSort} ) {
129        $Self->{ZoomExpandSort} = $ConfigObject->Get('Ticket::Frontend::ZoomExpandSort');
130    }
131
132    $Self->{ArticleFilterActive} = $ConfigObject->Get('Ticket::Frontend::TicketArticleFilter');
133
134    # define if rich text should be used
135    $Self->{RichText} = $ConfigObject->Get('Ticket::Frontend::ZoomRichTextForce')
136        || $LayoutObject->{BrowserRichText}
137        || 0;
138
139    # Always exclude plain text attachment, but exclude HTML body only if rich text is enabled.
140    $Self->{ExcludeAttachments} = {
141        ExcludePlainText => 1,
142        ExcludeHTMLBody  => $Self->{RichText},
143        ExcludeInline    => $Self->{RichText},
144    };
145
146    # get ticket object
147    my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket');
148
149    # ticket id lookup
150    if ( !$Self->{TicketID} && $ParamObject->GetParam( Param => 'TicketNumber' ) ) {
151        $Self->{TicketID} = $TicketObject->TicketIDLookup(
152            TicketNumber => $ParamObject->GetParam( Param => 'TicketNumber' ),
153            UserID       => $Self->{UserID},
154        );
155    }
156
157    # get zoom settings depending on ticket type
158    $Self->{DisplaySettings} = $ConfigObject->Get("Ticket::Frontend::AgentTicketZoom");
159
160    # this is a mapping of history types which is being used
161    # for the timeline view and its event type filter
162    $Self->{HistoryTypeMapping} = {
163        TicketLinkDelete                => Translatable('Link Deleted'),
164        Lock                            => Translatable('Ticket Locked'),
165        SetPendingTime                  => Translatable('Pending Time Set'),
166        TicketDynamicFieldUpdate        => Translatable('Dynamic Field Updated'),
167        EmailAgentInternal              => Translatable('Outgoing Email (internal)'),
168        NewTicket                       => Translatable('Ticket Created'),
169        TypeUpdate                      => Translatable('Type Updated'),
170        EscalationUpdateTimeStart       => Translatable('Escalation Update Time In Effect'),
171        EscalationUpdateTimeStop        => Translatable('Escalation Update Time Stopped'),
172        EscalationFirstResponseTimeStop => Translatable('Escalation First Response Time Stopped'),
173        CustomerUpdate                  => Translatable('Customer Updated'),
174        ChatInternal                    => Translatable('Internal Chat'),
175        SendAutoFollowUp                => Translatable('Automatic Follow-Up Sent'),
176        AddNote                         => Translatable('Note Added'),
177        AddNoteCustomer                 => Translatable('Note Added (Customer)'),
178        AddSMS                          => Translatable('SMS Added'),
179        AddSMSCustomer                  => Translatable('SMS Added (Customer)'),
180        StateUpdate                     => Translatable('State Updated'),
181        SendAnswer                      => Translatable('Outgoing Answer'),
182        ServiceUpdate                   => Translatable('Service Updated'),
183        TicketLinkAdd                   => Translatable('Link Added'),
184        EmailCustomer                   => Translatable('Incoming Customer Email'),
185        WebRequestCustomer              => Translatable('Incoming Web Request'),
186        PriorityUpdate                  => Translatable('Priority Updated'),
187        Unlock                          => Translatable('Ticket Unlocked'),
188        EmailAgent                      => Translatable('Outgoing Email'),
189        TitleUpdate                     => Translatable('Title Updated'),
190        OwnerUpdate                     => Translatable('New Owner'),
191        Merged                          => Translatable('Ticket Merged'),
192        PhoneCallAgent                  => Translatable('Outgoing Phone Call'),
193        Forward                         => Translatable('Forwarded Message'),
194        Unsubscribe                     => Translatable('Removed User Subscription'),
195        TimeAccounting                  => Translatable('Time Accounted'),
196        PhoneCallCustomer               => Translatable('Incoming Phone Call'),
197        SystemRequest                   => Translatable('System Request.'),
198        FollowUp                        => Translatable('Incoming Follow-Up'),
199        SendAutoReply                   => Translatable('Automatic Reply Sent'),
200        SendAutoReject                  => Translatable('Automatic Reject Sent'),
201        ResponsibleUpdate               => Translatable('New Responsible'),
202        EscalationSolutionTimeStart     => Translatable('Escalation Solution Time In Effect'),
203        EscalationSolutionTimeStop      => Translatable('Escalation Solution Time Stopped'),
204        EscalationResponseTimeStart     => Translatable('Escalation Response Time In Effect'),
205        EscalationResponseTimeStop      => Translatable('Escalation Response Time Stopped'),
206        SLAUpdate                       => Translatable('SLA Updated'),
207        ChatExternal                    => Translatable('External Chat'),
208        Move                            => Translatable('Queue Changed'),
209        SendAgentNotification           => Translatable('Notification Was Sent'),
210    };
211
212    # Add custom files to the zoom's frontend module registration on the fly
213    #    to avoid conflicts with other modules.
214    if (
215        defined $ConfigObject->Get('TimelineViewEnabled')
216        && $ConfigObject->Get('TimelineViewEnabled') == 1
217        )
218    {
219        $ConfigObject->Set(
220            Key   => 'Loader::Module::AgentTicketZoom###003-OTRSBusiness',
221            Value => {
222                JavaScript => [
223                    'Core.Agent.TicketZoom.TimelineView.js',
224                ],
225            },
226        );
227    }
228
229    return $Self;
230}
231
232sub Run {
233    my ( $Self, %Param ) = @_;
234
235    # get layout object
236    my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
237
238    # check needed stuff
239    if ( !$Self->{TicketID} ) {
240        return $LayoutObject->ErrorScreen(
241            Message => Translatable('No TicketID is given!'),
242            Comment => Translatable('Please contact the administrator.'),
243        );
244    }
245
246    # get needed objects
247    my $TicketObject  = $Kernel::OM->Get('Kernel::System::Ticket');
248    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
249
250    # check permissions
251    my $Access = $TicketObject->TicketPermission(
252        Type     => 'ro',
253        TicketID => $Self->{TicketID},
254        UserID   => $Self->{UserID}
255    );
256
257    # error screen, don't show ticket
258    return $LayoutObject->NoPermission(
259        Message => Translatable(
260            "This ticket does not exist, or you don't have permissions to access it in its current state."
261        ),
262        WithHeader => $Self->{Subaction} && $Self->{Subaction} eq 'ArticleUpdate' ? 'no' : 'yes',
263    ) if !$Access;
264
265    # get ticket attributes
266    my %Ticket = $TicketObject->TicketGet(
267        TicketID      => $Self->{TicketID},
268        DynamicFields => 1,
269    );
270
271    # get ACL restrictions
272    my %PossibleActions;
273    my $Counter = 0;
274
275    # get config object
276    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
277
278    # get all registered Actions
279    if ( ref $ConfigObject->Get('Frontend::Module') eq 'HASH' ) {
280
281        my %Actions = %{ $ConfigObject->Get('Frontend::Module') };
282
283        # only use those Actions that stats with Agent
284        %PossibleActions = map { ++$Counter => $_ }
285            grep { substr( $_, 0, length 'Agent' ) eq 'Agent' }
286            sort keys %Actions;
287    }
288
289    my $ACL = $TicketObject->TicketAcl(
290        Data          => \%PossibleActions,
291        Action        => $Self->{Action},
292        TicketID      => $Self->{TicketID},
293        ReturnType    => 'Action',
294        ReturnSubType => '-',
295        UserID        => $Self->{UserID},
296    );
297
298    my %AclAction = %PossibleActions;
299    if ($ACL) {
300        %AclAction = $TicketObject->TicketAclActionData();
301    }
302
303    # check if ACL restrictions exist
304    my %AclActionLookup = reverse %AclAction;
305
306    # show error screen if ACL prohibits this action
307    if ( !$AclActionLookup{ $Self->{Action} } ) {
308        return $LayoutObject->NoPermission( WithHeader => 'yes' );
309    }
310
311    # send parameter TicketID to JS
312    $LayoutObject->AddJSData(
313        Key   => 'TicketID',
314        Value => $Self->{TicketID},
315    );
316
317    # mark shown ticket as seen
318    if ( $Self->{Subaction} eq 'TicketMarkAsSeen' ) {
319        my $Success = 1;
320
321        # always show archived tickets as seen
322        if ( $Ticket{ArchiveFlag} ne 'y' ) {
323            $Success = $Self->_TicketItemSeen( TicketID => $Self->{TicketID} );
324        }
325
326        return $LayoutObject->Attachment(
327            ContentType => 'text/html',
328            Content     => $Success,
329            Type        => 'inline',
330            NoCache     => 1,
331        );
332    }
333
334    if ( $Self->{Subaction} eq 'MarkAsImportant' ) {
335
336        # Owner and Responsible can mark articles as important or remove mark
337        if (
338            $Self->{UserID} == $Ticket{OwnerID}
339            || (
340                $ConfigObject->Get('Ticket::Responsible')
341                && $Self->{UserID} == $Ticket{ResponsibleID}
342            )
343            )
344        {
345
346            # Always use user id 1 because other users also have to see the important flag
347            my %ArticleFlag = $ArticleObject->ArticleFlagGet(
348                TicketID  => $Self->{TicketID},
349                ArticleID => $Self->{ArticleID},
350                UserID    => 1,
351            );
352
353            my $ArticleIsImportant = $ArticleFlag{Important};
354            if ($ArticleIsImportant) {
355
356                # Always use user id 1 because other users also have to see the important flag
357                $ArticleObject->ArticleFlagDelete(
358                    TicketID  => $Self->{TicketID},
359                    ArticleID => $Self->{ArticleID},
360                    Key       => 'Important',
361                    UserID    => 1,
362                );
363            }
364            else {
365
366                # Always use user id 1 because other users also have to see the important flag
367                $ArticleObject->ArticleFlagSet(
368                    TicketID  => $Self->{TicketID},
369                    ArticleID => $Self->{ArticleID},
370                    Key       => 'Important',
371                    Value     => 1,
372                    UserID    => 1,
373                );
374            }
375        }
376
377        return $LayoutObject->Redirect(
378            OP => "Action=AgentTicketZoom;TicketID=$Self->{TicketID};ArticleID=$Self->{ArticleID}",
379        );
380    }
381
382    # get required objects
383    my $ParamObject = $Kernel::OM->Get('Kernel::System::Web::Request');
384    my $MainObject  = $Kernel::OM->Get('Kernel::System::Main');
385
386    if ( $Self->{Subaction} eq 'FormDraftDelete' ) {
387        my %Response;
388
389        my $FormDraftID = $ParamObject->GetParam( Param => 'FormDraftID' ) || '';
390        if ($FormDraftID) {
391            $Response{Success} = $Kernel::OM->Get('Kernel::System::FormDraft')->FormDraftDelete(
392                FormDraftID => $FormDraftID,
393                UserID      => $Self->{UserID},
394            );
395        }
396        else {
397            $Response{Error} = $LayoutObject->{LanguageObject}->Translate("Missing FormDraftID!");
398        }
399
400        # build JSON output
401        my $JSON = $LayoutObject->JSONEncode(
402            Data => \%Response,
403        );
404
405        # send JSON response
406        return $LayoutObject->Attachment(
407            ContentType => 'application/json; charset=' . $LayoutObject->{Charset},
408            Content     => $JSON,
409            Type        => 'inline',
410            NoCache     => 1,
411        );
412    }
413
414    if ( $Self->{Subaction} eq 'LoadWidget' ) {
415        my $ElementID = $ParamObject->GetParam( Param => 'ElementID' );
416        my $Config;
417        WIDGET:
418        for my $Key ( sort keys %{ $Self->{DisplaySettings}->{Widgets} // {} } ) {
419            if ( $ElementID eq 'Async_' . $LayoutObject->LinkEncode($Key) ) {
420                $Config = $Self->{DisplaySettings}->{Widgets}->{$Key};
421                last WIDGET;
422            }
423        }
424        if ($Config) {
425            my $Success = eval { $MainObject->Require( $Config->{Module} ) };
426            if ( !$Success ) {
427                $Kernel::OM->Get('Kernel::System::Log')->Log(
428                    Priority => 'error',
429                    Message  => "Cannot load $Config->{Module}: $@",
430                );
431                return $LayoutObject->Attachment(
432                    ContentType => 'text/html',
433                    Content     => '',
434                    Type        => 'inline',
435                    NoCache     => 1,
436                );
437            }
438            my $Module = eval { $Config->{Module}->new( %{$Self} ) };
439            if ( !$Module ) {
440                $Kernel::OM->Get('Kernel::System::Log')->Log(
441                    Priority => 'error',
442                    Message  => "new() of Widget module $Config->{Module} not successful!",
443
444                );
445                return $LayoutObject->Attachment(
446                    ContentType => 'text/html',
447                    Content     => '',
448                    Type        => 'inline',
449                    NoCache     => 1,
450                );
451            }
452            my $WidgetOutput = $Module->Run(
453                Ticket    => \%Ticket,
454                AclAction => \%AclAction,
455                Config    => $Config,
456            );
457
458            return $LayoutObject->Attachment(
459                ContentType => 'text/html',
460                Content     => $WidgetOutput->{Output} // ' ',
461                Type        => 'inline',
462                NoCache     => 1,
463            );
464        }
465        else {
466            $Kernel::OM->Get('Kernel::System::Log')->Log(
467                Priority => 'error',
468                Message  => "Cannot locate module for ElementID $ElementID",
469            );
470            return $LayoutObject->Attachment(
471                ContentType => 'text/html',
472                Content     => '',
473                Type        => 'inline',
474                NoCache     => 1,
475            );
476        }
477    }
478
479    # mark shown article as seen
480    if ( $Self->{Subaction} eq 'MarkAsSeen' ) {
481        my $Success = 1;
482
483        # always show archived tickets as seen
484        if ( $Ticket{ArchiveFlag} ne 'y' ) {
485            $Success = $Self->_ArticleItemSeen(
486                TicketID  => $Self->{TicketID},
487                ArticleID => $Self->{ArticleID},
488            );
489        }
490
491        return $LayoutObject->Attachment(
492            ContentType => 'text/html',
493            Content     => $Success,
494            Type        => 'inline',
495            NoCache     => 1,
496        );
497    }
498
499    # article update
500    elsif ( $Self->{Subaction} eq 'ArticleUpdate' ) {
501        my $Count = $ParamObject->GetParam( Param => 'Count' );
502
503        my $ArticleBackendObject = $ArticleObject->BackendForArticle(
504            TicketID  => $Self->{TicketID},
505            ArticleID => $Self->{ArticleID},
506        );
507
508        my %Article = $ArticleBackendObject->ArticleGet(
509            TicketID      => $Self->{TicketID},
510            ArticleID     => $Self->{ArticleID},
511            RealNames     => 1,
512            DynamicFields => 0,
513        );
514        $Article{Count} = $Count;
515
516        # Get attachment index (excluding body attachments).
517        my %AtmIndex = $ArticleBackendObject->ArticleAttachmentIndex(
518            ArticleID => $Self->{ArticleID},
519            %{ $Self->{ExcludeAttachments} },
520        );
521        $Article{Atms} = \%AtmIndex;
522
523        # fetch all std. templates
524        my %StandardTemplates = $Kernel::OM->Get('Kernel::System::Queue')->QueueStandardTemplateMemberList(
525            QueueID       => $Ticket{QueueID},
526            TemplateTypes => 1,
527            Valid         => 1,
528        );
529
530        my $ArticleWidgetsHTML = $Self->_ArticleItem(
531            Ticket            => \%Ticket,
532            Article           => \%Article,
533            AclAction         => \%AclAction,
534            StandardResponses => $StandardTemplates{Answer},
535            StandardForwards  => $StandardTemplates{Forward},
536            Type              => 'OnLoad',
537        );
538
539        # send data to JS
540        $LayoutObject->AddJSData(
541            Key   => 'ArticleIDs',
542            Value => [ $Self->{ArticleID} ],
543        );
544        $LayoutObject->AddJSData(
545            Key   => 'MenuItems',
546            Value => $Self->{MenuItems},
547        );
548
549        my $Content = $LayoutObject->Output(
550            TemplateFile => 'AgentTicketZoom',
551            Data         => {
552                %Ticket,
553                %Article,
554                %AclAction,
555                ArticleWidgetsHTML => $ArticleWidgetsHTML
556            },
557            AJAX => 1,
558        );
559        if ( !$Content ) {
560            $LayoutObject->FatalError(
561                Message =>
562                    $LayoutObject->{LanguageObject}->Translate( 'Can\'t get for ArticleID %s!', $Self->{ArticleID} ),
563            );
564        }
565        return $LayoutObject->Attachment(
566            ContentType => 'text/html',
567            Charset     => $LayoutObject->{UserCharset},
568            Content     => $Content,
569            Type        => 'inline',
570            NoCache     => 1,
571        );
572    }
573
574    # get needed objects
575    my $UserObject    = $Kernel::OM->Get('Kernel::System::User');
576    my $SessionObject = $Kernel::OM->Get('Kernel::System::AuthSession');
577
578    # write article filter settings to session
579    if ( $Self->{Subaction} eq 'ArticleFilterSet' ) {
580
581        # get params
582        my $TicketID     = $ParamObject->GetParam( Param => 'TicketID' );
583        my $SaveDefaults = $ParamObject->GetParam( Param => 'SaveDefaults' );
584        my @CommunicationChannelFilterIDs = $ParamObject->GetArray( Param => 'CommunicationChannelFilter' );
585        my $CustomerVisibility            = $ParamObject->GetParam( Param => 'CustomerVisibilityFilter' );
586        my @ArticleSenderTypeFilterIDs    = $ParamObject->GetArray( Param => 'ArticleSenderTypeFilter' );
587
588        # build session string
589        my $SessionString = '';
590        if (@CommunicationChannelFilterIDs) {
591            $SessionString .= 'CommunicationChannelFilter<';
592            $SessionString .= join ',', @CommunicationChannelFilterIDs;
593            $SessionString .= '>';
594        }
595        if ( defined $CustomerVisibility && $CustomerVisibility != 2 ) {
596            $SessionString .= "CustomerVisibilityFilter<$CustomerVisibility>";
597        }
598        if (@ArticleSenderTypeFilterIDs) {
599            $SessionString .= 'ArticleSenderTypeFilter<';
600            $SessionString .= join ',', @ArticleSenderTypeFilterIDs;
601            $SessionString .= '>';
602        }
603
604        # write the session
605
606        # save default filter settings to user preferences
607        if ($SaveDefaults) {
608            $UserObject->SetPreferences(
609                UserID => $Self->{UserID},
610                Key    => 'ArticleFilterDefault',
611                Value  => $SessionString,
612            );
613            $SessionObject->UpdateSessionID(
614                SessionID => $Self->{SessionID},
615                Key       => 'ArticleFilterDefault',
616                Value     => $SessionString,
617            );
618        }
619
620        # turn off filter explicitly for this ticket
621        if ( $SessionString eq '' ) {
622            $SessionString = 'off';
623        }
624
625        # update the session
626        my $Update = $SessionObject->UpdateSessionID(
627            SessionID => $Self->{SessionID},
628            Key       => "ArticleFilter$TicketID",
629            Value     => $SessionString,
630        );
631
632        # build JSON output
633        my $JSON = '';
634        if ($Update) {
635            $JSON = $LayoutObject->JSONEncode(
636                Data => {
637                    Message => Translatable('Article filter settings were saved.'),
638                },
639            );
640        }
641
642        # send JSON response
643        return $LayoutObject->Attachment(
644            ContentType => 'application/json; charset=' . $LayoutObject->{Charset},
645            Content     => $JSON,
646            Type        => 'inline',
647            NoCache     => 1,
648        );
649    }
650
651    # write article filter settings to session
652    if ( $Self->{Subaction} eq 'EvenTypeFilterSet' ) {
653
654        # get params
655        my $TicketID     = $ParamObject->GetParam( Param => 'TicketID' );
656        my $SaveDefaults = $ParamObject->GetParam( Param => 'SaveDefaults' );
657        my @EventTypeFilterIDs = $ParamObject->GetArray( Param => 'EventTypeFilter' );
658
659        # build session string
660        my $SessionString = '';
661        if (@EventTypeFilterIDs) {
662            $SessionString .= 'EventTypeFilter<';
663            $SessionString .= join ',', @EventTypeFilterIDs;
664            $SessionString .= '>';
665        }
666
667        # write the session
668
669        # save default filter settings to user preferences
670        if ($SaveDefaults) {
671            $UserObject->SetPreferences(
672                UserID => $Self->{UserID},
673                Key    => 'EventTypeFilterDefault',
674                Value  => $SessionString,
675            );
676            $SessionObject->UpdateSessionID(
677                SessionID => $Self->{SessionID},
678                Key       => 'EventTypeFilterDefault',
679                Value     => $SessionString,
680            );
681        }
682
683        # turn off filter explicitly for this ticket
684        if ( $SessionString eq '' ) {
685            $SessionString = 'off';
686        }
687
688        # update the session
689        my $Update = $SessionObject->UpdateSessionID(
690            SessionID => $Self->{SessionID},
691            Key       => "EventTypeFilter$TicketID",
692            Value     => $SessionString,
693        );
694
695        # build JSON output
696        my $JSON = '';
697        if ($Update) {
698            $JSON = $LayoutObject->JSONEncode(
699                Data => {
700                    Message => Translatable('Event type filter settings were saved.'),
701                },
702            );
703        }
704
705        # send JSON response
706        return $LayoutObject->Attachment(
707            ContentType => 'application/json; charset=' . $LayoutObject->{Charset},
708            Content     => $JSON,
709            Type        => 'inline',
710            NoCache     => 1,
711        );
712    }
713
714    # article filter is activated in sysconfig
715    if ( $Self->{ArticleFilterActive} ) {
716
717        # get article filter settings from session string
718        my $ArticleFilterSessionString = $Self->{ 'ArticleFilter' . $Self->{TicketID} };
719
720        # set article filter for this ticket from user preferences
721        if ( !$ArticleFilterSessionString ) {
722            $ArticleFilterSessionString = $Self->{ArticleFilterDefault};
723        }
724
725        # do not use defaults for this ticket if filter was explicitly turned off
726        elsif ( $ArticleFilterSessionString eq 'off' ) {
727            $ArticleFilterSessionString = '';
728        }
729
730        # extract CommunicationChannels
731        if (
732            $ArticleFilterSessionString
733            && $ArticleFilterSessionString =~ m{ CommunicationChannelFilter < ( [^<>]+ ) > }xms
734            )
735        {
736            my @IDs = split /,/, $1;
737            $Self->{ArticleFilter}->{CommunicationChannelID} = \@IDs;
738        }
739
740        # extract CustomerVisibility
741        if (
742            $ArticleFilterSessionString
743            && $ArticleFilterSessionString =~ m{ CustomerVisibilityFilter < ( [^<>]+ ) > }xms
744            )
745        {
746            $Self->{ArticleFilter}->{CustomerVisibility} = $1;
747        }
748
749        # extract ArticleSenderTypeIDs
750        if (
751            $ArticleFilterSessionString
752            && $ArticleFilterSessionString =~ m{ ArticleSenderTypeFilter < ( [^<>]+ ) > }xms
753            )
754        {
755            my @IDs = split /,/, $1;
756            $Self->{ArticleFilter}->{ArticleSenderTypeID} = \@IDs;
757        }
758
759        # get event type filter settings from session string
760        my $EventTypeFilterSessionString = $Self->{ 'EventTypeFilter' . $Self->{TicketID} };
761
762        # set article filter for this ticket from user preferences
763        if ( !$EventTypeFilterSessionString ) {
764            $EventTypeFilterSessionString = $Self->{EventTypeFilterDefault};
765        }
766
767        # do not use defaults for this ticket if filter was explicitly turned off
768        elsif ( $EventTypeFilterSessionString eq 'off' ) {
769            $EventTypeFilterSessionString = '';
770        }
771
772        # Set article filter with value if it exists.
773        if (
774            $EventTypeFilterSessionString
775            && $EventTypeFilterSessionString =~ m{ EventTypeFilter < ( [^<>]+ ) > }xms
776            )
777        {
778            my @IDs = split /,/, $1;
779            $Self->{EventTypeFilter}->{EventTypeID} = \@IDs;
780        }
781    }
782
783    # return if HTML email
784    if ( $Self->{Subaction} eq 'ShowHTMLeMail' ) {
785
786        # check needed ArticleID
787        if ( !$Self->{ArticleID} ) {
788            return $LayoutObject->ErrorScreen( Message => Translatable('Need ArticleID!') );
789        }
790
791        # get article data
792        my %Article = $ArticleObject->ArticleGet(
793            TicketID      => $Self->{TicketID},
794            ArticleID     => $Self->{ArticleID},
795            DynamicFields => 0,
796        );
797
798        # check if article data exists
799        if ( !%Article ) {
800            return $LayoutObject->ErrorScreen( Message => Translatable('Invalid ArticleID!') );
801        }
802
803        # if it is a HTML email, return here
804        return $LayoutObject->Attachment(
805            Filename => $ConfigObject->Get('Ticket::Hook')
806                . "-$Article{TicketNumber}-$Article{TicketID}-$Article{ArticleID}",
807            Type        => 'inline',
808            ContentType => "$Article{MimeType}; charset=$Article{Charset}",
809            Content     => $Article{Body},
810        );
811    }
812
813    # generate output
814    my $Output = $LayoutObject->Header(
815        Value    => $Ticket{TicketNumber},
816        TicketID => $Ticket{TicketID},
817    );
818    $Output .= $LayoutObject->NavigationBar();
819    $Output .= $Self->MaskAgentZoom(
820        Ticket    => \%Ticket,
821        AclAction => \%AclAction
822    );
823    $Output .= $LayoutObject->Footer();
824    return $Output;
825}
826
827sub MaskAgentZoom {
828    my ( $Self, %Param ) = @_;
829
830    my %Ticket    = %{ $Param{Ticket} };
831    my %AclAction = %{ $Param{AclAction} };
832
833    # get needed objects
834    my $TicketObject  = $Kernel::OM->Get('Kernel::System::Ticket');
835    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
836
837    # Create a list of article sender types for lookup
838    my %ArticleSenderTypeList = $ArticleObject->ArticleSenderTypeList();
839
840    # else show normal ticket zoom view
841    # fetch all move queues
842    my %MoveQueues = $TicketObject->MoveList(
843        TicketID => $Ticket{TicketID},
844        UserID   => $Self->{UserID},
845        Action   => $Self->{Action},
846        Type     => 'move_into',
847    );
848
849    # fetch all std. templates
850    my %StandardTemplates = $Kernel::OM->Get('Kernel::System::Queue')->QueueStandardTemplateMemberList(
851        QueueID       => $Ticket{QueueID},
852        TemplateTypes => 1,
853    );
854
855    # get cofig object
856    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
857
858    # generate shown articles
859    my $Limit = $ConfigObject->Get('Ticket::Frontend::MaxArticlesPerPage');
860
861    my $Order = $Self->{ZoomExpandSort} eq 'reverse' ? 'DESC' : 'ASC';
862    my $Page;
863
864    # get param object
865    my $ParamObject = $Kernel::OM->Get('Kernel::System::Web::Request');
866
867    # get article page
868    my $ArticlePage = $ParamObject->GetParam( Param => 'ArticlePage' );
869
870    my $IsVisibleForCustomer;
871    if ( defined $Self->{ArticleFilter}->{CustomerVisibility} ) {
872        $IsVisibleForCustomer = $Self->{ArticleFilter}->{CustomerVisibility};
873    }
874
875    # Get all articles.
876    my @ArticleBoxAll = $ArticleObject->ArticleList(
877        TicketID             => $Self->{TicketID},
878        IsVisibleForCustomer => $IsVisibleForCustomer,
879    );
880
881    if ( IsArrayRefWithData( $Self->{ArticleFilter}->{CommunicationChannelID} ) ) {
882        my %Filter = map { $_ => 1 } @{ $Self->{ArticleFilter}->{CommunicationChannelID} };
883
884        @ArticleBoxAll = grep { $Filter{ $_->{CommunicationChannelID} } } @ArticleBoxAll;
885    }
886
887    if ( IsArrayRefWithData( $Self->{ArticleFilter}->{ArticleSenderTypeID} ) ) {
888        my %Filter = map { $_ => 1 } @{ $Self->{ArticleFilter}->{ArticleSenderTypeID} };
889
890        @ArticleBoxAll = grep { $Filter{ $_->{SenderTypeID} } } @ArticleBoxAll;
891    }
892
893    if ( $Order eq 'DESC' ) {
894        @ArticleBoxAll = reverse @ArticleBoxAll;
895    }
896
897    my %ArticleFlags = $ArticleObject->ArticleFlagsOfTicketGet(
898        TicketID => $Ticket{TicketID},
899        UserID   => $Self->{UserID},
900    );
901    my $ArticleID;
902
903    if ( $Self->{ArticleID} ) {
904
905        my @ArticleIDs = map { $_->{ArticleID} } @ArticleBoxAll;
906
907        my %ArticleIndex;
908        @ArticleIndex{@ArticleIDs} = ( 0 .. $#ArticleIDs );
909
910        my $Index = $ArticleIndex{ $Self->{ArticleID} };
911        $Index //= 0;
912        $Page = int( $Index / $Limit ) + 1;
913    }
914    elsif ($ArticlePage) {
915        $Page = $ArticlePage;
916    }
917    else {
918
919        # Find latest not seen article.
920        ARTICLE:
921        for my $Article (@ArticleBoxAll) {
922
923            # Ignore system sender type.
924            if (
925                $ConfigObject->Get('Ticket::NewArticleIgnoreSystemSender')
926                && $ArticleSenderTypeList{ $Article->{SenderTypeID} } eq 'system'
927                )
928            {
929                next ARTICLE;
930            }
931
932            next ARTICLE if $ArticleFlags{ $Article->{ArticleID} }->{Seen};
933            $ArticleID = $Article->{ArticleID};
934
935            my @ArticleIDs = map { $_->{ArticleID} } @ArticleBoxAll;
936
937            my %ArticleIndex;
938            @ArticleIndex{@ArticleIDs} = ( 0 .. $#ArticleIDs );
939
940            my $Index = $ArticleIndex{$ArticleID};
941            $Page = int( $Index / $Limit ) + 1;
942
943            last ARTICLE;
944        }
945
946        if ( !$ArticleID ) {
947            $Page = 1;
948        }
949    }
950
951    # We need to find out whether pagination is actually necessary.
952    # The easiest way would be count the articles, but that would slow
953    # down the most common case (fewer articles than $Limit in the ticket).
954    # So instead we use the following trick:
955    # 1) if the $Page > 1, we need pagination
956    # 2) if not, request $Limit + 1 articles. If $Limit + 1 are actually
957    #    returned, pagination is necessary
958    my $Extra = $Page > 1 ? 0 : 1;
959    my $NeedPagination;
960
961    my @ArticleBox = $Self->_ArticleBoxGet(
962        Page          => $Page,
963        ArticleBoxAll => \@ArticleBoxAll,
964        Limit         => $Limit,
965    );
966
967    if ( !@ArticleBox && $Page > 1 ) {
968
969        # If the page argument is past the actual number of pages.
970        # This can happen when a new article filter was added.
971        # Try to get results for the 1st page.
972
973        @ArticleBox = $Self->_ArticleBoxGet(
974            Page          => 1,
975            ArticleBoxAll => \@ArticleBoxAll,
976            Limit         => $Limit,
977        );
978    }
979
980    if ( @ArticleBox > $Limit ) {
981        pop @ArticleBox;
982        $NeedPagination = 1;
983    }
984    elsif ( $Page == 1 && scalar @ArticleBoxAll <= $Limit ) {
985        $NeedPagination = 0;
986    }
987    else {
988        $NeedPagination = 1;
989    }
990
991    $Page ||= 1;
992
993    my $Pages;
994    if ($NeedPagination) {
995        $Pages = ceil( scalar @ArticleBoxAll / $Limit );
996    }
997
998    my $ArticleIDFound = 0;
999    ARTICLE:
1000    for my $Article (@ArticleBox) {
1001        next ARTICLE if !$Self->{ArticleID};
1002        next ARTICLE if !$Article->{ArticleID};
1003        next ARTICLE if $Self->{ArticleID} ne $Article->{ArticleID};
1004
1005        $ArticleIDFound = 1;
1006    }
1007
1008    # get selected or last customer article
1009    if ($ArticleIDFound) {
1010        $ArticleID = $Self->{ArticleID};
1011    }
1012    else {
1013        if ( !$ArticleID ) {
1014            if (@ArticleBox) {
1015
1016                # set first listed article as fallback
1017                $ArticleID = $ArticleBox[0]->{ArticleID};
1018
1019                # set last customer article as selected article replacing last set
1020                ARTICLETMP:
1021                for my $ArticleTmp (@ArticleBox) {
1022                    if ( $ArticleSenderTypeList{ $ArticleTmp->{SenderTypeID} } eq 'customer' ) {
1023                        $ArticleID = $ArticleTmp->{ArticleID};
1024                        last ARTICLETMP if $Self->{ZoomExpandSort} eq 'reverse';
1025                    }
1026                }
1027            }
1028        }
1029    }
1030
1031    # check if expand view is usable (only for less then 400 article)
1032    # if you have more articles is going to be slow and not usable
1033    my $ArticleMaxLimit = $ConfigObject->Get('Ticket::Frontend::MaxArticlesZoomExpand')
1034        // 400;
1035    if ( $Self->{ZoomExpand} && $#ArticleBox > $ArticleMaxLimit ) {
1036        $Self->{ZoomExpand} = 0;
1037    }
1038
1039    # get shown article(s)
1040    my @ArticleBoxShown;
1041    if ( !$Self->{ZoomExpand} ) {
1042        ARTICLEBOX:
1043        for my $ArticleTmp (@ArticleBox) {
1044            if ( $ArticleID eq $ArticleTmp->{ArticleID} ) {
1045                push @ArticleBoxShown, $ArticleTmp;
1046                last ARTICLEBOX;
1047            }
1048        }
1049    }
1050    else {
1051        @ArticleBoxShown = @ArticleBox;
1052    }
1053
1054    # get layout object
1055    my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
1056
1057    # age design
1058    $Ticket{Age} = $LayoutObject->CustomerAge(
1059        Age   => $Ticket{Age},
1060        Space => ' '
1061    );
1062
1063    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');
1064
1065    my %Widgets;
1066    my %AsyncWidgetActions;
1067    WIDGET:
1068    for my $Key ( sort keys %{ $Self->{DisplaySettings}->{Widgets} // {} } ) {
1069        my $Config = $Self->{DisplaySettings}->{Widgets}->{$Key};
1070
1071        if ( $Config->{Async} ) {
1072            if ( !$Config->{Location} ) {
1073                $Kernel::OM->Get('Kernel::System::Log')->Log(
1074                    Priority => 'error',
1075                    Message =>
1076                        "The configuration for $Config->{Module} must contain a Location, because it is marked as Async.",
1077                );
1078                next WIDGET;
1079            }
1080            my $ElementID = 'Async_' . $LayoutObject->LinkEncode($Key);
1081            push @{ $Widgets{ $Config->{Location} } }, {
1082                Async => 1,
1083                Rank  => $Config->{Rank} || $Key,
1084                %Ticket,
1085                ElementID => $ElementID,
1086            };
1087            $AsyncWidgetActions{$ElementID} = "Action=$Self->{Action};Subaction=LoadWidget;"
1088                . "TicketID=$Self->{TicketID};ElementID=$ElementID";
1089            next WIDGET;
1090        }
1091        my $Success = eval { $MainObject->Require( $Config->{Module} ) };
1092        next WIDGET if !$Success;
1093        my $Module = eval { $Config->{Module}->new(%$Self) };
1094        if ( !$Module ) {
1095            $Kernel::OM->Get('Kernel::System::Log')->Log(
1096                Priority => 'error',
1097                Message  => "new() of Widget module $Config->{Module} not successful!",
1098
1099            );
1100            next WIDGET;
1101        }
1102        my $WidgetOutput = $Module->Run(
1103            Ticket    => \%Ticket,
1104            AclAction => \%AclAction,
1105            Config    => $Config,
1106        );
1107        if ( !$WidgetOutput ) {
1108            next WIDGET;
1109        }
1110        $WidgetOutput->{Rank} //= $Key;
1111        my $Location = $WidgetOutput->{Location} || $Config->{Location};
1112        push @{ $Widgets{$Location} }, $WidgetOutput;
1113    }
1114    for my $Location ( sort keys %Widgets ) {
1115        $Param{ $Location . 'Widgets' } = [
1116            sort { $a->{Rank} cmp $b->{Rank} } @{ $Widgets{$Location} }
1117        ];
1118    }
1119    $LayoutObject->AddJSData(
1120        Key   => 'AsyncWidgetActions',
1121        Value => \%AsyncWidgetActions,
1122    );
1123
1124    # set display options
1125    $Param{Hook} = $ConfigObject->Get('Ticket::Hook') || 'Ticket#';
1126
1127    # only show article tree if articles are present,
1128    # or if a filter is set (so that the user has the option to
1129    # disable the filter)
1130    if ( @ArticleBox || $Self->{ArticleFilter} ) {
1131
1132        my $Pagination;
1133
1134        if ($NeedPagination) {
1135            $Pagination = {
1136                Pages       => $Pages,
1137                CurrentPage => $Page,
1138                TicketID    => $Ticket{TicketID},
1139            };
1140        }
1141
1142        # show article tree
1143        $Param{ArticleTree} = $Self->_ArticleTree(
1144            Ticket            => \%Ticket,
1145            ArticleFlags      => \%ArticleFlags,
1146            ArticleID         => $ArticleID,
1147            ArticleMaxLimit   => $ArticleMaxLimit,
1148            ArticleBox        => \@ArticleBox,
1149            Pagination        => $Pagination,
1150            Page              => $Page,
1151            ArticleCount      => scalar @ArticleBox,
1152            AclAction         => \%AclAction,
1153            StandardResponses => $StandardTemplates{Answer},
1154            StandardForwards  => $StandardTemplates{Forward},
1155        );
1156    }
1157
1158    # show articles items
1159    if ( !$Self->{ZoomTimeline} ) {
1160
1161        my @ArticleIDs;
1162        $Param{ArticleItems} = '';
1163
1164        my $ArticleWidgetsHTML = '';
1165
1166        ARTICLE:
1167        for my $ArticleTmp (@ArticleBoxShown) {
1168            my %Article = %$ArticleTmp;
1169
1170            $ArticleWidgetsHTML .= $Self->_ArticleItem(
1171                Ticket            => \%Ticket,
1172                Article           => \%Article,
1173                AclAction         => \%AclAction,
1174                StandardResponses => $StandardTemplates{Answer},
1175                StandardForwards  => $StandardTemplates{Forward},
1176                ActualArticleID   => $ArticleID,
1177                Type              => 'Static',
1178            );
1179            push @ArticleIDs, $ArticleTmp->{ArticleID};
1180        }
1181
1182        # send data to JS
1183        $LayoutObject->AddJSData(
1184            Key   => 'ArticleIDs',
1185            Value => \@ArticleIDs,
1186        );
1187        $LayoutObject->AddJSData(
1188            Key   => 'MenuItems',
1189            Value => $Self->{MenuItems},
1190        );
1191
1192        $Param{ArticleItems} .= $LayoutObject->Output(
1193            TemplateFile => 'AgentTicketZoom',
1194            Data         => {
1195                %Ticket,
1196                %AclAction,
1197                ArticleWidgetsHTML => $ArticleWidgetsHTML,
1198            },
1199        );
1200    }
1201
1202    # always show archived tickets as seen
1203    if ( $Self->{ZoomExpand} && $Ticket{ArchiveFlag} ne 'y' ) {
1204
1205        # send data to JS
1206        $LayoutObject->AddJSData(
1207            Key   => 'TicketItemMarkAsSeen',
1208            Value => 1,
1209        );
1210    }
1211
1212    # number of articles
1213    $Param{ArticleCount} = scalar @ArticleBox;
1214
1215    $LayoutObject->Block(
1216        Name => 'Header',
1217        Data => { %Param, %Ticket, %AclAction },
1218    );
1219
1220    my %ActionLookup;
1221    my $UserObject = $Kernel::OM->Get('Kernel::System::User');
1222
1223    # run ticket menu modules
1224    if ( ref $ConfigObject->Get('Ticket::Frontend::MenuModule') eq 'HASH' ) {
1225        my %Menus = %{ $ConfigObject->Get('Ticket::Frontend::MenuModule') };
1226        my %MenuClusters;
1227        my %ZoomMenuItems;
1228
1229        MENU:
1230        for my $Menu ( sort keys %Menus ) {
1231
1232            # load module
1233            if ( !$Kernel::OM->Get('Kernel::System::Main')->Require( $Menus{$Menu}->{Module} ) ) {
1234                return $LayoutObject->FatalError();
1235            }
1236
1237            my $Object = $Menus{$Menu}->{Module}->new(
1238                %{$Self},
1239                TicketID => $Self->{TicketID},
1240            );
1241
1242            # run module
1243            my $Item = $Object->Run(
1244                %Param,
1245                Ticket => \%Ticket,
1246                ACL    => \%AclAction,
1247                Config => $Menus{$Menu},
1248            );
1249            next MENU if !$Item;
1250            if ( $Menus{$Menu}->{PopupType} ) {
1251                $Item->{Class} = "AsPopup PopupType_$Menus{$Menu}->{PopupType}";
1252            }
1253
1254            if ( $Menus{$Menu}->{Action} ) {
1255                $ActionLookup{ $Menus{$Menu}->{Action} } = {
1256                    Link           => $Item->{Link},
1257                    Class          => $Item->{Class},
1258                    LinkParam      => $Item->{LinkParam},
1259                    Description    => $Item->{Description},
1260                    Name           => $Item->{Name},
1261                    TranslatedName => $Kernel::OM->Get('Kernel::Language')->Translate( $Item->{Name} ),
1262                };
1263            }
1264
1265            if ( !$Menus{$Menu}->{ClusterName} ) {
1266
1267                $ZoomMenuItems{$Menu} = $Item;
1268            }
1269            else {
1270
1271                # check the configured priority for this item. The lowest ClusterPriority
1272                # within the same cluster wins.
1273                my $Priority = $MenuClusters{ $Menus{$Menu}->{ClusterName} }->{Priority} || 0;
1274                $Menus{$Menu}->{ClusterPriority} ||= 0;
1275                if ( !$Priority || $Priority !~ /^\d{3}$/ || $Priority > $Menus{$Menu}->{ClusterPriority} ) {
1276                    $Priority = $Menus{$Menu}->{ClusterPriority};
1277                }
1278                $MenuClusters{ $Menus{$Menu}->{ClusterName} }->{Priority} = $Priority;
1279                $MenuClusters{ $Menus{$Menu}->{ClusterName} }->{Items}->{$Menu} = $Item;
1280            }
1281        }
1282
1283        for my $Cluster ( sort keys %MenuClusters ) {
1284            $ZoomMenuItems{ $MenuClusters{$Cluster}->{Priority} . $Cluster } = {
1285                Name  => $Cluster,
1286                Type  => 'Cluster',
1287                Link  => '#',
1288                Class => 'ClusterLink',
1289                Items => $MenuClusters{$Cluster}->{Items},
1290            };
1291        }
1292
1293        # display all items
1294        for my $Item ( sort keys %ZoomMenuItems ) {
1295            if ( $ZoomMenuItems{$Item}->{ExternalLink} && $ZoomMenuItems{$Item}->{ExternalLink} == 1 ) {
1296                $LayoutObject->Block(
1297                    Name => 'TicketMenuExternalLink',
1298                    Data => $ZoomMenuItems{$Item},
1299                );
1300            }
1301            else {
1302                $LayoutObject->Block(
1303                    Name => 'TicketMenu',
1304                    Data => $ZoomMenuItems{$Item},
1305                );
1306            }
1307
1308            if ( $ZoomMenuItems{$Item}->{Type} eq 'Cluster' ) {
1309
1310                $LayoutObject->Block(
1311                    Name => 'TicketMenuSubContainer',
1312                    Data => {
1313                        Name => $ZoomMenuItems{$Item}->{Name},
1314                    },
1315                );
1316
1317                for my $SubItem ( sort keys %{ $ZoomMenuItems{$Item}->{Items} } ) {
1318                    $LayoutObject->Block(
1319                        Name => 'TicketMenuSubContainerItem',
1320                        Data => $ZoomMenuItems{$Item}->{Items}->{$SubItem},
1321                    );
1322                }
1323            }
1324        }
1325    }
1326
1327    # get MoveQueuesStrg
1328    if ( $ConfigObject->Get('Ticket::Frontend::MoveType') =~ /^form$/i ) {
1329        $MoveQueues{0}         = '- ' . $LayoutObject->{LanguageObject}->Translate('Move') . ' -';
1330        $Param{MoveQueuesStrg} = $LayoutObject->AgentQueueListOption(
1331            Name           => 'DestQueueID',
1332            Data           => \%MoveQueues,
1333            Class          => 'Modernize Small',
1334            CurrentQueueID => $Ticket{QueueID},
1335        );
1336    }
1337    my %AclActionLookup = reverse %AclAction;
1338    if (
1339        $ConfigObject->Get('Frontend::Module')->{AgentTicketMove}
1340        && ( $AclActionLookup{AgentTicketMove} )
1341        )
1342    {
1343        my $Access = $TicketObject->TicketPermission(
1344            Type     => 'move',
1345            TicketID => $Ticket{TicketID},
1346            UserID   => $Self->{UserID},
1347            LogNo    => 1,
1348        );
1349        $Param{TicketID} = $Ticket{TicketID};
1350        if ($Access) {
1351            if ( $ConfigObject->Get('Ticket::Frontend::MoveType') =~ /^form$/i ) {
1352                $LayoutObject->Block(
1353                    Name => 'MoveLink',
1354                    Data => { %Param, %AclAction },
1355                );
1356            }
1357            else {
1358                $LayoutObject->Block(
1359                    Name => 'MoveForm',
1360                    Data => { %Param, %AclAction },
1361                );
1362            }
1363
1364            $ActionLookup{AgentTicketMove} = {
1365                Link           => 'Action=AgentTicketMove;TicketID=[% Data.TicketID | uri %]',
1366                Class          => 'AsPopup PopupType_TicketAction',
1367                LinkParam      => '',
1368                Description    => Translatable('Change Queue'),
1369                Name           => Translatable('Queue'),
1370                TranslatedName => $Kernel::OM->Get('Kernel::Language')->Translate('Queue'),
1371            };
1372        }
1373    }
1374
1375    # Check if AgentTicketCompose and AgentTicketForward are allowed as action (for display of FormDrafts).
1376    my %ActionConfig = (
1377        AgentTicketCompose => {
1378            Link           => 'Action=AgentTicketCompose;TicketID=[% Data.TicketID | uri %]',
1379            Class          => 'AsPopup PopupType_TicketAction',
1380            LinkParam      => '',
1381            Description    => Translatable('Reply'),
1382            Name           => Translatable('Reply'),
1383            TranslatedName => $Kernel::OM->Get('Kernel::Language')->Translate('Reply'),
1384        },
1385        AgentTicketForward => {
1386            Link           => 'Action=AgentTicketForward;TicketID=[% Data.TicketID | uri %]',
1387            Class          => 'AsPopup PopupType_TicketAction',
1388            LinkParam      => '',
1389            Description    => Translatable('Forward article via mail'),
1390            Name           => Translatable('Forward'),
1391            TranslatedName => $Kernel::OM->Get('Kernel::Language')->Translate('Forward'),
1392        },
1393    );
1394    ACTION:
1395    for my $Action (qw(AgentTicketCompose AgentTicketForward)) {
1396        next ACTION if !$ConfigObject->Get('Frontend::Module')->{$Action};
1397        next ACTION if !$AclActionLookup{$Action};
1398
1399        my $Config = $ConfigObject->Get( 'Ticket::Frontend::' . $Action );
1400        if ( $Config->{Permission} ) {
1401            next ACTION if !$TicketObject->TicketPermission(
1402                Type     => $Config->{Permission},
1403                TicketID => $Ticket{TicketID},
1404                UserID   => $Self->{UserID},
1405                LogNo    => 1,
1406            );
1407        }
1408        $ActionLookup{$Action} = $ActionConfig{$Action};
1409    }
1410
1411    # Get and show available FormDrafts.
1412    my %ShownFormDraftEntries;
1413    my $FormDraftList = $Kernel::OM->Get('Kernel::System::FormDraft')->FormDraftListGet(
1414        ObjectType => 'Ticket',
1415        ObjectID   => $Self->{TicketID},
1416        UserID     => $Self->{UserID},
1417    );
1418    if ( IsArrayRefWithData($FormDraftList) ) {
1419        FormDraft:
1420        for my $FormDraft ( @{$FormDraftList} ) {
1421            next FormDraft if !$ActionLookup{ $FormDraft->{Action} };
1422            push @{ $ShownFormDraftEntries{ $FormDraft->{Action} } }, $FormDraft;
1423        }
1424    }
1425    if (%ShownFormDraftEntries) {
1426
1427        my $LastArticle;
1428        if ( $Order eq 'DESC' ) {
1429            $LastArticle = $ArticleBoxAll[0];
1430        }
1431        else {
1432            $LastArticle = $ArticleBoxAll[-1];
1433        }
1434
1435        my $LastArticleSystemTime;
1436        if ( $LastArticle->{CreateTime} ) {
1437            my $LastArticleSystemTimeObject = $Kernel::OM->Create(
1438                'Kernel::System::DateTime',
1439                ObjectParams => {
1440                    String => $LastArticle->{CreateTime},
1441                },
1442            );
1443            $LastArticleSystemTime = $LastArticleSystemTimeObject->ToEpoch();
1444        }
1445
1446        my @FormDrafts;
1447
1448        for my $Action (
1449            sort {
1450                $ActionLookup{$a}->{TranslatedName}
1451                    cmp
1452                    $ActionLookup{$b}->{TranslatedName}
1453            } keys %ShownFormDraftEntries
1454            )
1455        {
1456            my $ActionData = $ActionLookup{$Action};
1457
1458            SHOWNFormDraftACTIONENTRY:
1459            for my $ShownFormDraftActionEntry (
1460                sort {
1461                    $a->{Title}
1462                        cmp
1463                        $b->{Title}
1464                        ||
1465                        $a->{FormDraftID}
1466                        <=>
1467                        $b->{FormDraftID}
1468                } @{ $ShownFormDraftEntries{$Action} }
1469                )
1470            {
1471                $ShownFormDraftActionEntry->{CreatedByUser} = $UserObject->UserName(
1472                    UserID => $ShownFormDraftActionEntry->{CreateBy},
1473                );
1474                $ShownFormDraftActionEntry->{ChangedByUser} = $UserObject->UserName(
1475                    UserID => $ShownFormDraftActionEntry->{ChangeBy},
1476                );
1477
1478                $ShownFormDraftActionEntry = {
1479                    %{$ShownFormDraftActionEntry},
1480                    %{$ActionData},
1481
1482                };
1483
1484                push @FormDrafts, $ShownFormDraftActionEntry;
1485            }
1486        }
1487
1488        $LayoutObject->Block(
1489            Name => 'FormDraftTable',
1490            Data => {
1491                FormDrafts => \@FormDrafts,
1492                TicketID   => $Self->{TicketID},
1493            },
1494        );
1495    }
1496
1497    # show created by if different then User ID 1
1498    if ( $Ticket{CreateBy} > 1 ) {
1499
1500        # get user object
1501        my $UserObject = $Kernel::OM->Get('Kernel::System::User');
1502        $Ticket{CreatedByUser} = $UserObject->UserName( UserID => $Ticket{CreateBy} );
1503        $LayoutObject->Block(
1504            Name => 'CreatedBy',
1505            Data => {%Ticket},
1506        );
1507    }
1508
1509    # show no articles block if ticket does not contain articles
1510    if ( !@ArticleBox && !$Self->{ZoomTimeline} ) {
1511        $LayoutObject->Block(
1512            Name => 'HintNoArticles',
1513        );
1514    }
1515
1516    # check if ticket is normal or process ticket
1517    my $IsProcessTicket = $TicketObject->TicketCheckForProcessType(
1518        'TicketID' => $Self->{TicketID}
1519    );
1520
1521    # show process widget  and activity dialogs on process tickets
1522    if ($IsProcessTicket) {
1523
1524        $Param{WidgetTitle} = $Self->{DisplaySettings}->{ProcessDisplay}->{WidgetTitle};
1525
1526        # get the DF where the ProcessEntityID is stored
1527        my $ProcessEntityIDField = 'DynamicField_'
1528            . $ConfigObject->Get("Process::DynamicFieldProcessManagementProcessID");
1529
1530        # get the DF where the AtivityEntityID is stored
1531        my $ActivityEntityIDField = 'DynamicField_'
1532            . $ConfigObject->Get("Process::DynamicFieldProcessManagementActivityID");
1533
1534        my $ProcessData = $Kernel::OM->Get('Kernel::System::ProcessManagement::Process')->ProcessGet(
1535            ProcessEntityID => $Ticket{$ProcessEntityIDField},
1536        );
1537        my $ActivityData = $Kernel::OM->Get('Kernel::System::ProcessManagement::Activity')->ActivityGet(
1538            Interface        => 'AgentInterface',
1539            ActivityEntityID => $Ticket{$ActivityEntityIDField},
1540        );
1541
1542        # send data to JS
1543        $LayoutObject->AddJSData(
1544            Key   => 'ProcessWidget',
1545            Value => 1,
1546        );
1547
1548        # output the process widget in the main screen
1549        $LayoutObject->Block(
1550            Name => 'ProcessWidget',
1551            Data => {
1552                WidgetTitle => $Param{WidgetTitle},
1553            },
1554        );
1555
1556        # get next activity dialogs
1557        my $NextActivityDialogs;
1558        if ( $Ticket{$ActivityEntityIDField} ) {
1559            $NextActivityDialogs = ${ActivityData}->{ActivityDialog} || {};
1560        }
1561        my $ActivityName = $ActivityData->{Name};
1562
1563        if ($NextActivityDialogs) {
1564
1565            # get ActivityDialog object
1566            my $ActivityDialogObject = $Kernel::OM->Get('Kernel::System::ProcessManagement::ActivityDialog');
1567
1568            # we have to check if the current user has the needed permissions to view the
1569            # different activity dialogs, so we loop over every activity dialog and check if there
1570            # is a permission configured. If there is a permission configured we check this
1571            # and display/hide the activity dialog link
1572            my %PermissionRights;
1573            my %PermissionActivityDialogList;
1574            ACTIVITYDIALOGPERMISSION:
1575            for my $Index ( sort { $a <=> $b } keys %{$NextActivityDialogs} ) {
1576                my $CurrentActivityDialogEntityID = $NextActivityDialogs->{$Index};
1577                my $CurrentActivityDialog         = $ActivityDialogObject->ActivityDialogGet(
1578                    Interface              => 'AgentInterface',
1579                    ActivityDialogEntityID => $CurrentActivityDialogEntityID
1580                );
1581
1582                # create an interface lookup-list
1583                my %InterfaceLookup = map { $_ => 1 } @{ $CurrentActivityDialog->{Interface} };
1584
1585                next ACTIVITYDIALOGPERMISSION if !$InterfaceLookup{AgentInterface};
1586
1587                if ( $CurrentActivityDialog->{Permission} ) {
1588
1589                    # performance-boost/cache
1590                    if ( !defined $PermissionRights{ $CurrentActivityDialog->{Permission} } ) {
1591                        $PermissionRights{ $CurrentActivityDialog->{Permission} } = $TicketObject->TicketPermission(
1592                            Type     => $CurrentActivityDialog->{Permission},
1593                            TicketID => $Ticket{TicketID},
1594                            UserID   => $Self->{UserID},
1595                        );
1596                    }
1597
1598                    if ( !$PermissionRights{ $CurrentActivityDialog->{Permission} } ) {
1599                        next ACTIVITYDIALOGPERMISSION;
1600                    }
1601                }
1602
1603                $PermissionActivityDialogList{$Index} = $CurrentActivityDialogEntityID;
1604            }
1605
1606            # reduce next activity dialogs to the ones that have permissions
1607            $NextActivityDialogs = \%PermissionActivityDialogList;
1608
1609            # get ACL restrictions
1610            my $ACL = $TicketObject->TicketAcl(
1611                Data          => \%PermissionActivityDialogList,
1612                TicketID      => $Ticket{TicketID},
1613                ReturnType    => 'ActivityDialog',
1614                ReturnSubType => '-',
1615                UserID        => $Self->{UserID},
1616            );
1617
1618            if ($ACL) {
1619                %{$NextActivityDialogs} = $TicketObject->TicketAclData();
1620            }
1621
1622            $LayoutObject->Block(
1623                Name => 'NextActivityDialogs',
1624                Data => {
1625                    'ActivityName' => $ActivityName,
1626                },
1627            );
1628
1629            if ( IsHashRefWithData($NextActivityDialogs) ) {
1630                for my $NextActivityDialogKey ( sort { $a <=> $b } keys %{$NextActivityDialogs} ) {
1631                    my $ActivityDialogData = $ActivityDialogObject->ActivityDialogGet(
1632                        Interface              => 'AgentInterface',
1633                        ActivityDialogEntityID => $NextActivityDialogs->{$NextActivityDialogKey},
1634                    );
1635                    $LayoutObject->Block(
1636                        Name => 'ActivityDialog',
1637                        Data => {
1638                            ActivityDialogEntityID
1639                                => $NextActivityDialogs->{$NextActivityDialogKey},
1640                            Name            => $ActivityDialogData->{Name},
1641                            ProcessEntityID => $Ticket{$ProcessEntityIDField},
1642                            TicketID        => $Ticket{TicketID},
1643                        },
1644                    );
1645                }
1646            }
1647            else {
1648                $LayoutObject->Block(
1649                    Name => 'NoActivityDialogs',
1650                    Data => {},
1651                );
1652            }
1653        }
1654    }
1655
1656    # get dynamic field config for frontend module
1657    my $DynamicFieldFilter = {
1658        %{ $ConfigObject->Get("Ticket::Frontend::AgentTicketZoom")->{DynamicField} || {} },
1659        %{
1660            $ConfigObject->Get("Ticket::Frontend::AgentTicketZoom")
1661                ->{ProcessWidgetDynamicField}
1662                || {}
1663        },
1664    };
1665
1666    # get the dynamic fields for ticket object
1667    my $DynamicField = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet(
1668        Valid       => 1,
1669        ObjectType  => ['Ticket'],
1670        FieldFilter => $DynamicFieldFilter || {},
1671    );
1672    my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');
1673
1674    # to store dynamic fields to be displayed in the process widget and in the sidebar
1675    my (@FieldsWidget);
1676
1677    # cycle trough the activated Dynamic Fields for ticket object
1678    DYNAMICFIELD:
1679    for my $DynamicFieldConfig ( @{$DynamicField} ) {
1680        next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
1681        next DYNAMICFIELD if !defined $Ticket{ 'DynamicField_' . $DynamicFieldConfig->{Name} };
1682        next DYNAMICFIELD if $Ticket{ 'DynamicField_' . $DynamicFieldConfig->{Name} } eq '';
1683
1684        # use translation here to be able to reduce the character length in the template
1685        my $Label = $LayoutObject->{LanguageObject}->Translate( $DynamicFieldConfig->{Label} );
1686
1687        if (
1688            $IsProcessTicket &&
1689            $Self->{DisplaySettings}->{ProcessWidgetDynamicField}->{ $DynamicFieldConfig->{Name} }
1690            )
1691        {
1692            my $ValueStrg = $DynamicFieldBackendObject->DisplayValueRender(
1693                DynamicFieldConfig => $DynamicFieldConfig,
1694                Value              => $Ticket{ 'DynamicField_' . $DynamicFieldConfig->{Name} },
1695                LayoutObject       => $LayoutObject,
1696
1697                # no ValueMaxChars here, enough space available
1698            );
1699
1700            push @FieldsWidget, {
1701                $DynamicFieldConfig->{Name} => $ValueStrg->{Title},
1702                Name                        => $DynamicFieldConfig->{Name},
1703                Title                       => $ValueStrg->{Title},
1704                Value                       => $ValueStrg->{Value},
1705                ValueKey                    => $Ticket{ 'DynamicField_' . $DynamicFieldConfig->{Name} },
1706                Label                       => $Label,
1707                Link                        => $ValueStrg->{Link},
1708                LinkPreview                 => $ValueStrg->{LinkPreview},
1709
1710                # Include unique parameter with dynamic field name in case of collision with others.
1711                #   Please see bug#13362 for more information.
1712                "DynamicField_$DynamicFieldConfig->{Name}" => $ValueStrg->{Title},
1713            };
1714        }
1715    }
1716
1717    if ($IsProcessTicket) {
1718
1719        # output dynamic fields registered for a group in the process widget
1720        my @FieldsInAGroup;
1721        for my $GroupName (
1722            sort keys %{ $Self->{DisplaySettings}->{ProcessWidgetDynamicFieldGroups} }
1723            )
1724        {
1725
1726            $LayoutObject->Block(
1727                Name => 'ProcessWidgetDynamicFieldGroups',
1728            );
1729
1730            my $GroupFieldsString = $Self->{DisplaySettings}->{ProcessWidgetDynamicFieldGroups}->{$GroupName};
1731
1732            $GroupFieldsString =~ s{\s}{}xmsg;
1733            my @GroupFields = split( ',', $GroupFieldsString );
1734
1735            if ( $#GroupFields + 1 ) {
1736
1737                my $ShowGroupTitle = 0;
1738                for my $Field (@FieldsWidget) {
1739
1740                    if ( grep { $_ eq $Field->{Name} } @GroupFields ) {
1741
1742                        $ShowGroupTitle = 1;
1743                        $LayoutObject->Block(
1744                            Name => 'ProcessWidgetDynamicField',
1745                            Data => {
1746                                Label => $Field->{Label},
1747                                Name  => $Field->{Name},
1748                            },
1749                        );
1750
1751                        $LayoutObject->Block(
1752                            Name => 'ProcessWidgetDynamicFieldValueOverlayTrigger',
1753                        );
1754
1755                        if ( $Field->{Link} ) {
1756                            $LayoutObject->Block(
1757                                Name => 'ProcessWidgetDynamicFieldLink',
1758                                Data => {
1759                                    $Field->{Name} => $Field->{Title},
1760                                    %Ticket,
1761
1762                                    # alias for ticket title, Title will be overwritten
1763                                    TicketTitle => $Ticket{Title},
1764                                    Value       => $Field->{Value},
1765                                    Title       => $Field->{Title},
1766                                    Link        => $Field->{Link},
1767                                    LinkPreview => $Field->{LinkPreview},
1768
1769                                    # Include unique parameter with dynamic field name in case of collision with others.
1770                                    #   Please see bug#13362 for more information.
1771                                    "DynamicField_$Field->{Name}" => $Field->{Title},
1772                                },
1773                            );
1774                        }
1775                        else {
1776                            $LayoutObject->Block(
1777                                Name => 'ProcessWidgetDynamicFieldPlain',
1778                                Data => {
1779                                    Value => $Field->{Value},
1780                                    Title => $Field->{Title},
1781                                },
1782                            );
1783                        }
1784                        push @FieldsInAGroup, $Field->{Name};
1785                    }
1786                }
1787
1788                if ($ShowGroupTitle) {
1789                    $LayoutObject->Block(
1790                        Name => 'ProcessWidgetDynamicFieldGroupSeparator',
1791                        Data => {
1792                            Name => $GroupName,
1793                        },
1794                    );
1795                }
1796            }
1797        }
1798
1799        # output dynamic fields not registered in a group in the process widget
1800        my @RemainingFieldsWidget;
1801        for my $Field (@FieldsWidget) {
1802
1803            if ( !grep { $_ eq $Field->{Name} } @FieldsInAGroup ) {
1804                push @RemainingFieldsWidget, $Field;
1805            }
1806        }
1807
1808        $LayoutObject->Block(
1809            Name => 'ProcessWidgetDynamicFieldGroups',
1810        );
1811
1812        if ( $#RemainingFieldsWidget + 1 ) {
1813
1814            $LayoutObject->Block(
1815                Name => 'ProcessWidgetDynamicFieldGroupSeparator',
1816                Data => {
1817                    Name =>
1818                        $LayoutObject->{LanguageObject}->Translate('Fields with no group'),
1819                },
1820            );
1821        }
1822        for my $Field (@RemainingFieldsWidget) {
1823
1824            $LayoutObject->Block(
1825                Name => 'ProcessWidgetDynamicField',
1826                Data => {
1827                    Label => $Field->{Label},
1828                    Name  => $Field->{Name},
1829                },
1830            );
1831
1832            $LayoutObject->Block(
1833                Name => 'ProcessWidgetDynamicFieldValueOverlayTrigger',
1834            );
1835
1836            if ( $Field->{Link} ) {
1837                $LayoutObject->Block(
1838                    Name => 'ProcessWidgetDynamicFieldLink',
1839                    Data => {
1840                        $Field->{Name} => $Field->{Title},
1841                        %Ticket,
1842
1843                        # alias for ticket title, Title will be overwritten
1844                        TicketTitle => $Ticket{Title},
1845                        Value       => $Field->{Value},
1846                        Title       => $Field->{Title},
1847                        Link        => $Field->{Link},
1848
1849                        # Include unique parameter with dynamic field name in case of collision with others.
1850                        #   Please see bug#13362 for more information.
1851                        "DynamicField_$Field->{Name}" => $Field->{Title},
1852                    },
1853                );
1854            }
1855            else {
1856                $LayoutObject->Block(
1857                    Name => 'ProcessWidgetDynamicFieldPlain',
1858                    Data => {
1859                        Value => $Field->{Value},
1860                        Title => $Field->{Title},
1861                    },
1862                );
1863            }
1864        }
1865    }
1866
1867    # article filter is activated in sysconfig
1868    if ( $Self->{ArticleFilterActive} ) {
1869
1870        if ( $Self->{ZoomTimeline} ) {
1871
1872            # build event type list for filter dialog
1873            $Param{EventTypeFilterString} = $LayoutObject->BuildSelection(
1874                Data        => $Self->{HistoryTypeMapping},
1875                SelectedID  => $Self->{EventTypeFilter}->{EventTypeID},
1876                Translation => 1,
1877                Multiple    => 1,
1878                Sort        => 'AlphanumericValue',
1879                Name        => 'EventTypeFilter',
1880                Class       => 'Modernize',
1881            );
1882
1883            # send data to JS
1884            $LayoutObject->AddJSData(
1885                Key   => 'ArticleFilterDialog',
1886                Value => 0,
1887            );
1888
1889            $LayoutObject->Block(
1890                Name => 'EventTypeFilterDialog',
1891                Data => {%Param},
1892            );
1893        }
1894        else {
1895
1896            my @CommunicationChannels = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelList(
1897                ValidID => 1,
1898            );
1899
1900            my %Channels = map { $_->{ChannelID} => $_->{DisplayName} } @CommunicationChannels;
1901
1902            # build article type list for filter dialog
1903            $Param{Channels} = $LayoutObject->BuildSelection(
1904                Data        => \%Channels,
1905                SelectedID  => $Self->{ArticleFilter}->{CommunicationChannelID},
1906                Translation => 1,
1907                Multiple    => 1,
1908                Sort        => 'AlphanumericValue',
1909                Name        => 'CommunicationChannelFilter',
1910                Class       => 'Modernize',
1911            );
1912
1913            $Param{CustomerVisibility} = $LayoutObject->BuildSelection(
1914                Data => {
1915                    0 => Translatable('Invisible only'),
1916                    1 => Translatable('Visible only'),
1917                    2 => Translatable('Visible and invisible'),
1918                },
1919                SelectedID  => $Self->{ArticleFilter}->{CustomerVisibility} // 2,
1920                Translation => 1,
1921                Sort        => 'NumericKey',
1922                Name        => 'CustomerVisibilityFilter',
1923                Class       => 'Modernize',
1924            );
1925
1926            # get sender types
1927            my %ArticleSenderTypes = $ArticleObject->ArticleSenderTypeList();
1928
1929            # build article sender type list for filter dialog
1930            $Param{ArticleSenderTypeFilterString} = $LayoutObject->BuildSelection(
1931                Data        => \%ArticleSenderTypes,
1932                SelectedID  => $Self->{ArticleFilter}->{ArticleSenderTypeID},
1933                Translation => 1,
1934                Multiple    => 1,
1935                Sort        => 'AlphanumericValue',
1936                Name        => 'ArticleSenderTypeFilter',
1937                Class       => 'Modernize',
1938            );
1939
1940            # Ticket ID
1941            $Param{TicketID} = $Self->{TicketID};
1942
1943            # send data to JS
1944            $LayoutObject->AddJSData(
1945                Key   => 'ArticleFilterDialog',
1946                Value => 1,
1947            );
1948
1949            $LayoutObject->Block(
1950                Name => 'ArticleFilterDialog',
1951                Data => {%Param},
1952            );
1953        }
1954    }
1955
1956    # check if ticket need to be marked as seen
1957    my $ArticleAllSeen = 1;
1958    ARTICLE:
1959    for my $Article (@ArticleBox) {
1960
1961        # ignore system sender type
1962        next ARTICLE
1963            if $ConfigObject->Get('Ticket::NewArticleIgnoreSystemSender')
1964            && $ArticleSenderTypeList{ $Article->{SenderTypeID} } eq 'system';
1965
1966        # last ARTICLE if article was not shown
1967        if ( !$ArticleFlags{ $Article->{ArticleID} }->{Seen} ) {
1968            $ArticleAllSeen = 0;
1969            last ARTICLE;
1970        }
1971    }
1972
1973    # mark ticket as seen if all article are shown
1974    if ($ArticleAllSeen) {
1975        $TicketObject->TicketFlagSet(
1976            TicketID => $Self->{TicketID},
1977            Key      => 'Seen',
1978            Value    => 1,
1979            UserID   => $Self->{UserID},
1980        );
1981    }
1982
1983    # send data to JS
1984    $LayoutObject->AddJSData(
1985        Key   => 'ArticleTableHeight',
1986        Value => $LayoutObject->{UserTicketZoomArticleTableHeight},
1987    );
1988    $LayoutObject->AddJSData(
1989        Key   => 'Ticket::Frontend::HTMLArticleHeightDefault',
1990        Value => $ConfigObject->Get('Ticket::Frontend::HTMLArticleHeightDefault'),
1991    );
1992    $LayoutObject->AddJSData(
1993        Key   => 'Ticket::Frontend::HTMLArticleHeightMax',
1994        Value => $ConfigObject->Get('Ticket::Frontend::HTMLArticleHeightMax'),
1995    );
1996    $LayoutObject->AddJSData(
1997        Key   => 'Language',
1998        Value => {
1999            AttachmentViewMessage => Translatable(
2000                'Article could not be opened! Perhaps it is on another article page?'
2001            ),
2002        },
2003    );
2004
2005    # init js
2006    $LayoutObject->Block(
2007        Name => 'TicketZoomInit',
2008    );
2009
2010    # return output
2011    return $LayoutObject->Output(
2012        TemplateFile => 'AgentTicketZoom',
2013        Data         => { %Param, %Ticket, %AclAction },
2014    );
2015}
2016
2017sub _ArticleTree {
2018    my ( $Self, %Param ) = @_;
2019
2020    my %Ticket          = %{ $Param{Ticket} };
2021    my %ArticleFlags    = %{ $Param{ArticleFlags} };
2022    my @ArticleBox      = @{ $Param{ArticleBox} };
2023    my $ArticleMaxLimit = $Param{ArticleMaxLimit};
2024    my $ArticleID       = $Param{ArticleID};
2025    my $TableClasses;
2026
2027    # get layout object
2028    my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
2029
2030    # build thread string
2031    $LayoutObject->Block(
2032        Name => 'Tree',
2033        Data => {
2034            %Param,
2035            TableClasses => $TableClasses,
2036            ZoomTimeline => $Self->{ZoomTimeline},
2037        },
2038    );
2039
2040    if ( $Param{Pagination} && !$Self->{ZoomTimeline} ) {
2041        $LayoutObject->Block(
2042            Name => 'ArticlePages',
2043            Data => $Param{Pagination},
2044        );
2045    }
2046
2047    my @ArticleViews = (
2048        {
2049            Key   => 'Collapse',
2050            Value => Translatable('Show one article'),
2051        },
2052        {
2053            Key   => 'Expand',
2054            Value => Translatable('Show all articles'),
2055        },
2056    );
2057
2058    # Add timeline view option only if enabled.
2059    if ( $Kernel::OM->Get('Kernel::Config')->Get('TimelineViewEnabled') ) {
2060        push @ArticleViews, {
2061            Key   => 'Timeline',
2062            Value => Translatable('Show Ticket Timeline View'),
2063        };
2064    }
2065
2066    my $ArticleViewSelected = 'Collapse';
2067    if ( $Self->{ZoomExpand} ) {
2068        $ArticleViewSelected = 'Expand';
2069    }
2070    elsif ( $Self->{ZoomTimeline} ) {
2071        $ArticleViewSelected = 'Timeline';
2072    }
2073
2074    # Add disabled teaser option for OTRSBusiness timeline view
2075    my $OTRSBusinessIsInstalled = $Kernel::OM->Get('Kernel::System::OTRSBusiness')->OTRSBusinessIsInstalled();
2076    if ( !$OTRSBusinessIsInstalled ) {
2077        push @ArticleViews, {
2078            Key   => 'Timeline',
2079            Value => $LayoutObject->{LanguageObject}
2080                ->Translate( 'Show Ticket Timeline View (%s)', 'OTRS Business Solution™' ),
2081            Disabled => 1,
2082        };
2083    }
2084
2085    my $ArticleViewStrg = $LayoutObject->BuildSelection(
2086        Data        => \@ArticleViews,
2087        SelectedID  => $ArticleViewSelected,
2088        Translation => 1,
2089        Sort        => 'AlphanumericValue',
2090        Name        => 'ArticleView',
2091        Class       => 'Modernize',
2092    );
2093
2094    # Send data to JS.
2095    $LayoutObject->AddJSData(
2096        Key   => 'ArticleViewStrg',
2097        Value => $ArticleViewStrg,
2098    );
2099    $LayoutObject->AddJSData(
2100        Key   => 'ZoomExpand',
2101        Value => $Self->{ZoomExpand},
2102    );
2103
2104    # article filter is activated in sysconfig
2105    if ( $Self->{ArticleFilterActive} ) {
2106
2107        # define highlight style for links if filter is active
2108        my $HighlightStyle = 'menu';
2109        if ( $Self->{ArticleFilter} ) {
2110            $HighlightStyle = 'PriorityID-5';
2111        }
2112
2113        # build article filter links
2114        $LayoutObject->Block(
2115            Name => 'ArticleFilterDialogLink',
2116            Data => {
2117                %Param,
2118                HighlightStyle => $HighlightStyle,
2119            },
2120        );
2121
2122        # build article filter reset link only if filter is set
2123        if (
2124            ( !$Self->{ZoomTimeline} && IsHashRefWithData( $Self->{ArticleFilter} ) )
2125            || ( $Self->{ZoomTimeline} && $Self->{EventTypeFilter} )
2126            )
2127        {
2128            $LayoutObject->Block(
2129                Name => 'ArticleFilterResetLink',
2130                Data => {%Param},
2131            );
2132        }
2133    }
2134
2135    # get needed objects
2136    my $TicketObject  = $Kernel::OM->Get('Kernel::System::Ticket');
2137    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
2138    my $ConfigObject  = $Kernel::OM->Get('Kernel::Config');
2139
2140    # Create a list of article sender types for lookup
2141    my %ArticleSenderTypeList = $ArticleObject->ArticleSenderTypeList();
2142
2143    # show article tree
2144    if ( !$Self->{ZoomTimeline} ) {
2145
2146        $LayoutObject->Block(
2147            Name => 'ArticleList',
2148            Data => {
2149                %Param,
2150                TableClasses => $TableClasses,
2151            },
2152        );
2153
2154        ARTICLE:
2155        for my $ArticleTmp (@ArticleBox) {
2156            my %Article = %$ArticleTmp;
2157
2158            # article filter is activated in sysconfig and there are articles
2159            # that passed the filter
2160            if ( $Self->{ArticleFilterActive} ) {
2161                if ( $Self->{ArticleFilter} && $Self->{ArticleFilter}->{ShownArticleIDs} ) {
2162
2163                    # do not show article in tree if it does not match the filter
2164                    if ( !$Self->{ArticleFilter}->{ShownArticleIDs}->{ $Article{ArticleID} } ) {
2165                        next ARTICLE;
2166                    }
2167                }
2168            }
2169
2170            # show article flags
2171            my $Class      = '';
2172            my $ClassRow   = '';
2173            my $NewArticle = 0;
2174
2175            # ignore system sender types
2176            if (
2177                !$ArticleFlags{ $Article{ArticleID} }->{Seen}
2178                && (
2179                    !$ConfigObject->Get('Ticket::NewArticleIgnoreSystemSender')
2180                    || $ConfigObject->Get('Ticket::NewArticleIgnoreSystemSender')
2181                    && $ArticleSenderTypeList{ $Article{SenderTypeID} } ne 'system'
2182                )
2183                )
2184            {
2185                $NewArticle = 1;
2186
2187                # show ticket flags
2188
2189                # always show archived tickets as seen
2190                if ( $Ticket{ArchiveFlag} ne 'y' ) {
2191                    $Class    .= ' UnreadArticles';
2192                    $ClassRow .= ' UnreadArticles';
2193                }
2194
2195                # just show ticket flags if agent belongs to the ticket
2196                my $ShowMeta;
2197                if (
2198                    $Self->{UserID} == $Ticket{OwnerID}
2199                    || $Self->{UserID} == $Ticket{ResponsibleID}
2200                    )
2201                {
2202                    $ShowMeta = 1;
2203                }
2204                if ( !$ShowMeta && $ConfigObject->Get('Ticket::Watcher') ) {
2205                    my %Watch = $TicketObject->TicketWatchGet(
2206                        TicketID => $Article{TicketID},
2207                    );
2208                    if ( $Watch{ $Self->{UserID} } ) {
2209                        $ShowMeta = 1;
2210                    }
2211                }
2212
2213                # show ticket flags
2214                if ($ShowMeta) {
2215                    $Class .= ' Remarkable';
2216                }
2217                else {
2218                    $Class .= ' Ordinary';
2219                }
2220            }
2221
2222            # if this is the shown article -=> set class to active
2223            if ( $ArticleID eq $Article{ArticleID} && !$Self->{ZoomExpand} ) {
2224                $ClassRow .= ' Active';
2225            }
2226
2227            my $TmpSubject = $TicketObject->TicketSubjectClean(
2228
2229                TicketNumber => $Ticket{TicketNumber},
2230                Subject      => $Article{Subject} || '',
2231            );
2232
2233            my %ArticleFields = $LayoutObject->ArticleFields(%Article);
2234
2235            # Get transmission status information for email articles.
2236            my $TransmissionStatus;
2237            if ( $Article{ChannelName} && $Article{ChannelName} eq 'Email' ) {
2238                $TransmissionStatus = $ArticleObject->BackendForArticle(%Article)->ArticleTransmissionStatus(
2239                    ArticleID => $Article{ArticleID},
2240                );
2241            }
2242
2243            # check if we need to show also expand/collapse icon
2244            $LayoutObject->Block(
2245                Name => 'TreeItem',
2246                Data => {
2247                    %Article,
2248                    ArticleFields      => \%ArticleFields,
2249                    Class              => $Class,
2250                    ClassRow           => $ClassRow,
2251                    Subject            => $TmpSubject,
2252                    TransmissionStatus => $TransmissionStatus,
2253                    ZoomExpand         => $Self->{ZoomExpand},
2254                    ZoomExpandSort     => $Self->{ZoomExpandSort},
2255                },
2256            );
2257
2258            # get article flags
2259            # Always use user id 1 because other users also have to see the important flag
2260            my %ArticleImportantFlags = $ArticleObject->ArticleFlagGet(
2261                ArticleID => $Article{ArticleID},
2262                UserID    => 1,
2263            );
2264
2265            # show important flag
2266            if ( $ArticleImportantFlags{Important} ) {
2267                $LayoutObject->Block(
2268                    Name => 'TreeItemImportantArticle',
2269                    Data => {},
2270                );
2271            }
2272
2273            # always show archived tickets as seen
2274            if ( $NewArticle && $Ticket{ArchiveFlag} ne 'y' ) {
2275                $LayoutObject->Block(
2276                    Name => 'TreeItemNewArticle',
2277                    Data => {
2278                        %Article,
2279                        Class => $Class,
2280                    },
2281                );
2282            }
2283
2284            # Bugfix for IE7: a table cell should not be empty
2285            # (because otherwise the cell borders are not shown):
2286            # we add an empty element here
2287            else {
2288                $LayoutObject->Block(
2289                    Name => 'TreeItemNoNewArticle',
2290                    Data => {},
2291                );
2292            }
2293
2294            # Determine communication direction.
2295            if ( $Article{ChannelName} eq 'Internal' ) {
2296                $LayoutObject->Block( Name => 'TreeItemDirectionInternal' );
2297            }
2298            elsif ( $ArticleSenderTypeList{ $Article{SenderTypeID} } eq 'customer' ) {
2299                $LayoutObject->Block( Name => 'TreeItemDirectionIncoming' );
2300            }
2301            else {
2302                $LayoutObject->Block( Name => 'TreeItemDirectionOutgoing' );
2303            }
2304
2305            # Get attachment index (excluding body attachments).
2306            my %AtmIndex = $ArticleObject->BackendForArticle(%Article)->ArticleAttachmentIndex(
2307                ArticleID => $Article{ArticleID},
2308                %{ $Self->{ExcludeAttachments} },
2309            );
2310            $Article{Atms} = \%AtmIndex;
2311
2312            # show attachment info
2313            # Bugfix for IE7: a table cell should not be empty
2314            # (because otherwise the cell borders are not shown):
2315            # we add an empty element here
2316            if ( !$Article{Atms} || !%{ $Article{Atms} } ) {
2317                $LayoutObject->Block(
2318                    Name => 'TreeItemNoAttachment',
2319                    Data => {},
2320                );
2321
2322                next ARTICLE;
2323            }
2324            else {
2325
2326                my $Attachments = $Self->_CollectArticleAttachments(
2327                    Article => \%Article,
2328                );
2329
2330                $LayoutObject->Block(
2331                    Name => 'TreeItemAttachment',
2332                    Data => {
2333                        TicketID    => $Article{TicketID},
2334                        ArticleID   => $Article{ArticleID},
2335                        Attachments => $Attachments,
2336                    },
2337                );
2338            }
2339        }
2340    }
2341
2342    # show timeline view
2343    else {
2344
2345        # get ticket history
2346        my @HistoryLines = $TicketObject->HistoryGet(
2347            TicketID => $Self->{TicketID},
2348            UserID   => $Self->{UserID},
2349        );
2350
2351        # get articles for later use
2352        my @TimelineArticleBox = $ArticleObject->ArticleList(
2353            TicketID => $Self->{TicketID},
2354        );
2355
2356        for my $ArticleItem (@TimelineArticleBox) {
2357            my $ArticleBackendObject = $ArticleObject->BackendForArticle( %{$ArticleItem} );
2358
2359            my %Article = $ArticleBackendObject->ArticleGet(
2360                TicketID      => $Self->{TicketID},
2361                ArticleID     => $ArticleItem->{ArticleID},
2362                DynamicFields => 1,
2363                RealNames     => 1,
2364            );
2365
2366            # Append article meta data.
2367            $ArticleItem = {
2368                %{$ArticleItem},
2369                %Article,
2370            };
2371        }
2372
2373        my $ArticlesByArticleID = {};
2374        for my $Article ( sort @TimelineArticleBox ) {
2375            my $ArticleBackendObject = $ArticleObject->BackendForArticle( %{$Article} );
2376
2377            # Get attachment index (excluding body attachments).
2378            my %AtmIndex = $ArticleBackendObject->ArticleAttachmentIndex(
2379                ArticleID => $Article->{ArticleID},
2380                %{ $Self->{ExcludeAttachments} },
2381            );
2382            $Article->{Atms}                                = \%AtmIndex;
2383            $Article->{Backend}                             = $ArticleBackendObject->ChannelNameGet();
2384            $ArticlesByArticleID->{ $Article->{ArticleID} } = $Article;
2385
2386            # Check if there is HTML body attachment.
2387            my %AttachmentIndexHTMLBody = $ArticleBackendObject->ArticleAttachmentIndex(
2388                ArticleID    => $Article->{ArticleID},
2389                OnlyHTMLBody => 1,
2390            );
2391            ( $Article->{HTMLBodyAttachmentID} ) = sort keys %AttachmentIndexHTMLBody;
2392        }
2393
2394        # do not display these types
2395        my @TypesDodge = qw(
2396            Misc
2397            ArchiveFlagUpdate
2398            LoopProtection
2399            Remove
2400            Subscribe
2401            Unsubscribe
2402            SystemRequest
2403            SendAgentNotification
2404            SendCustomerNotification
2405            SendAutoReject
2406        );
2407
2408        # sort out non-filtered event types (if applicable)
2409        if (
2410            $Self->{EventTypeFilter}->{EventTypeID}
2411            && IsArrayRefWithData( $Self->{EventTypeFilter}->{EventTypeID} )
2412            )
2413        {
2414            for my $EventType ( sort keys %{ $Self->{HistoryTypeMapping} } ) {
2415                if (
2416                    $EventType ne 'NewTicket' && !grep { $_ eq $EventType }
2417                    @{ $Self->{EventTypeFilter}->{EventTypeID} }
2418                    )
2419                {
2420                    push @TypesDodge, $EventType;
2421                }
2422            }
2423        }
2424
2425        # types which can be described as 'action on a ticket'
2426        my @TypesTicketAction = qw(
2427            ServiceUpdate
2428            SLAUpdate
2429            StateUpdate
2430            SetPendingTime
2431            Unlock
2432            Lock
2433            ResponsibleUpdate
2434            OwnerUpdate
2435            CustomerUpdate
2436            NewTicket
2437            TicketLinkAdd
2438            TicketLinkDelete
2439            TicketDynamicFieldUpdate
2440            Move
2441            Merged
2442            PriorityUpdate
2443            TitleUpdate
2444            TypeUpdate
2445            EscalationResponseTimeNotifyBefore
2446            EscalationResponseTimeStart
2447            EscalationResponseTimeStop
2448            EscalationSolutionTimeNotifyBefore
2449            EscalationSolutionTimeStart
2450            EscalationSolutionTimeStop
2451            EscalationUpdateTimeNotifyBefore
2452            EscalationUpdateTimeStart
2453            EscalationUpdateTimeStop
2454            TimeAccounting
2455        );
2456
2457        # types which are usually being connected to some kind of
2458        # automatic process (e.g. triggered by another action)
2459        my @TypesTicketAutoAction = qw(
2460            SendAutoFollowUp
2461            SendAutoReject
2462            SendAutoReply
2463        );
2464
2465        # types which can be considered as internal
2466        my @TypesInternal = qw(
2467            AddNote
2468            ChatInternal
2469            EmailAgentInternal
2470        );
2471
2472        # outgoing types
2473        my @TypesOutgoing = qw(
2474            AddSMS
2475            Forward
2476            EmailAgent
2477            PhoneCallAgent
2478            Bounce
2479            SendAnswer
2480        );
2481
2482        # incoming types
2483        my @TypesIncoming = qw(
2484            EmailCustomer
2485            AddNoteCustomer
2486            AddSMSCustomer
2487            PhoneCallCustomer
2488            FollowUp
2489            WebRequestCustomer
2490            ChatExternal
2491        );
2492
2493        my @TypesLeft = (
2494            @TypesOutgoing,
2495            @TypesInternal,
2496            @TypesTicketAutoAction,
2497        );
2498
2499        my @TypesRight = (
2500            @TypesIncoming,
2501            @TypesTicketAction,
2502        );
2503
2504        my @TypesWithArticles = (
2505            @TypesOutgoing,
2506            @TypesInternal,
2507            @TypesIncoming,
2508            'PhoneCallCustomer',
2509        );
2510
2511        my %HistoryItems;
2512        my $ItemCounter = 0;
2513        my $LastCreateTime;
2514        my $LastCreateSystemTime;
2515
2516        # Get mapping of history types to readable strings
2517        my %HistoryTypes;
2518        my %HistoryTypeConfig = %{ $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Frontend::HistoryTypes') // {} };
2519        for my $Entry ( sort keys %HistoryTypeConfig ) {
2520            %HistoryTypes = (
2521                %HistoryTypes,
2522                %{ $HistoryTypeConfig{$Entry} },
2523            );
2524        }
2525
2526        HISTORYITEM:
2527        for my $Item ( reverse @HistoryLines ) {
2528
2529            # special treatment for certain types, e.g. external notes from customers
2530            if (
2531                $Item->{ArticleID}
2532                && $Item->{HistoryType} eq 'AddNote'
2533                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
2534                && $ArticleSenderTypeList{ $ArticlesByArticleID->{ $Item->{ArticleID} }->{SenderTypeID} } eq 'customer'
2535                )
2536            {
2537                $Item->{Class} = 'TypeIncoming';
2538
2539                # We fake a custom history type because external notes from customers still
2540                # have the history type 'AddNote' which does not allow for distinguishing.
2541                $Item->{HistoryType} = 'AddNoteCustomer';
2542            }
2543
2544            # special treatment for certain types, e.g. external SMS from customers
2545            elsif (
2546                $Item->{ArticleID}
2547                && $Item->{HistoryType} eq 'AddSMS'
2548                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
2549                && $ArticleSenderTypeList{ $ArticlesByArticleID->{ $Item->{ArticleID} }->{SenderTypeID} } eq 'customer'
2550                )
2551            {
2552                $Item->{Class} = 'TypeIncoming';
2553
2554                # We fake a custom history type because external notes from customers still
2555                # have the history type 'AddSMS' which does not allow for distinguishing.
2556                $Item->{HistoryType} = 'AddSMSCustomer';
2557            }
2558
2559            # special treatment for internal emails
2560            elsif (
2561                $Item->{ArticleID}
2562                && $Item->{HistoryType} eq 'EmailAgent'
2563                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
2564                && $ArticlesByArticleID->{ $Item->{ArticleID} }->{Backend} eq 'Email'
2565                && $ArticleSenderTypeList{ $ArticlesByArticleID->{ $Item->{ArticleID} }->{SenderTypeID} } eq 'agent'
2566                && !$ArticlesByArticleID->{ $Item->{ArticleID} }->{IsVisibleForCustomer}
2567                )
2568            {
2569                $Item->{Class}       = 'TypeNoteInternal';
2570                $Item->{HistoryType} = 'EmailAgentInternal';
2571            }
2572
2573            # special treatment for certain types, e.g. external notes from customers
2574            elsif (
2575                $Item->{ArticleID}
2576                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
2577                && $ArticlesByArticleID->{ $Item->{ArticleID} }->{Backend} eq 'Chat'
2578                && $ArticlesByArticleID->{ $Item->{ArticleID} }->{IsVisibleForCustomer}
2579                )
2580            {
2581                $Item->{HistoryType} = 'ChatExternal';
2582                $Item->{Class}       = 'TypeIncoming';
2583            }
2584            elsif (
2585                $Item->{ArticleID}
2586                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
2587                && $ArticlesByArticleID->{ $Item->{ArticleID} }->{Backend} eq 'Chat'
2588                && !$ArticlesByArticleID->{ $Item->{ArticleID} }->{IsVisibleForCustomer}
2589                )
2590            {
2591                $Item->{HistoryType} = 'ChatInternal';
2592                $Item->{Class}       = 'TypeInternal';
2593            }
2594            elsif (
2595                $Item->{HistoryType} eq 'Forward'
2596                && $Item->{ArticleID}
2597                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
2598                && $ArticlesByArticleID->{ $Item->{ArticleID} }->{Backend} eq 'Email'
2599                && $ArticleSenderTypeList{ $ArticlesByArticleID->{ $Item->{ArticleID} }->{SenderTypeID} } eq 'agent'
2600                && !$ArticlesByArticleID->{ $Item->{ArticleID} }->{IsVisibleForCustomer}
2601                )
2602            {
2603
2604                $Item->{Class} = 'TypeNoteInternal';
2605            }
2606            elsif ( grep { $_ eq $Item->{HistoryType} } @TypesTicketAction ) {
2607                $Item->{Class} = 'TypeTicketAction';
2608            }
2609            elsif ( grep { $_ eq $Item->{HistoryType} } @TypesTicketAutoAction ) {
2610                $Item->{Class} = 'TypeTicketAutoAction';
2611            }
2612            elsif ( grep { $_ eq $Item->{HistoryType} } @TypesInternal ) {
2613                $Item->{Class} = 'TypeNoteInternal';
2614            }
2615            elsif ( grep { $_ eq $Item->{HistoryType} } @TypesIncoming ) {
2616                $Item->{Class} = 'TypeIncoming';
2617            }
2618            elsif ( grep { $_ eq $Item->{HistoryType} } @TypesOutgoing ) {
2619                $Item->{Class} = 'TypeOutgoing';
2620            }
2621
2622            if ( grep { $_ eq $Item->{HistoryType} } @TypesDodge ) {
2623                next HISTORYITEM;
2624            }
2625
2626            $Item->{Counter} = $ItemCounter++;
2627
2628            if ( $Item->{HistoryType} eq 'NewTicket' ) {
2629
2630                # if the 'NewTicket' item has an article, display this "creation article" event separately
2631                if ( $Item->{ArticleID} ) {
2632                    push @{ $Param{Items} }, {
2633                        %{$Item},
2634                        Counter             => $Item->{Counter}++,
2635                        Class               => 'NewTicket',
2636                        Name                => '',
2637                        ArticleID           => '',
2638                        HistoryTypeReadable => Translatable('Ticket Created'),
2639                        Orientation         => 'Right',
2640                    };
2641                }
2642                else {
2643                    $Item->{Class} = 'NewTicket';
2644                    delete $Item->{ArticleID};
2645                    delete $Item->{Name};
2646                }
2647            }
2648
2649            # remove article information from types which should not display articles
2650            if ( !grep { $_ eq $Item->{HistoryType} } @TypesWithArticles ) {
2651                delete $Item->{ArticleID};
2652            }
2653
2654            # get article (if present)
2655            if ( $Item->{ArticleID} ) {
2656                $Item->{ArticleData} = $ArticlesByArticleID->{ $Item->{ArticleID} };
2657
2658                my %ArticleFields = $LayoutObject->ArticleFields(
2659                    TicketID  => $Item->{ArticleData}->{TicketID},
2660                    ArticleID => $Item->{ArticleData}->{ArticleID},
2661                );
2662                $Item->{ArticleData}->{ArticleFields} = \%ArticleFields;
2663
2664                # Get dynamic fields and accounted time
2665                my $Backend = $ArticlesByArticleID->{ $Item->{ArticleID} }->{Backend};
2666
2667                # Get dynamic fields and accounted time
2668                my %ArticleMetaFields
2669                    = $Kernel::OM->Get("Kernel::Output::HTML::TicketZoom::Agent::$Backend")->ArticleMetaFields(
2670                    TicketID  => $Item->{ArticleData}->{TicketID},
2671                    ArticleID => $Item->{ArticleData}->{ArticleID},
2672                    UserID    => $Self->{UserID},
2673                    );
2674                $Item->{ArticleData}->{ArticleMetaFields} = \%ArticleMetaFields;
2675
2676                my @ArticleActions = $LayoutObject->ArticleActions(
2677                    TicketID  => $Item->{ArticleData}->{TicketID},
2678                    ArticleID => $Item->{ArticleData}->{ArticleID},
2679                    Type      => 'OnLoad',
2680                );
2681
2682                $Item->{ArticleData}->{ArticlePlain} = $LayoutObject->ArticlePreview(
2683                    TicketID   => $Item->{ArticleData}->{TicketID},
2684                    ArticleID  => $Item->{ArticleData}->{ArticleID},
2685                    ResultType => 'plain',
2686                );
2687
2688                $Item->{ArticleData}->{ArticleHTML}
2689                    = $Kernel::OM->Get("Kernel::Output::HTML::TimelineView::$Backend")->ArticleRender(
2690                    TicketID       => $Item->{ArticleData}->{TicketID},
2691                    ArticleID      => $Item->{ArticleData}->{ArticleID},
2692                    ArticleActions => \@ArticleActions,
2693                    UserID         => $Self->{UserID},
2694                    );
2695
2696                # remove empty lines
2697                $Item->{ArticleData}->{ArticlePlain} =~ s{^[\n\r]+}{}xmsg;
2698
2699                # Modify plain text and body to avoid '</script>' tag issue (see bug#14023).
2700                $Item->{ArticleData}->{ArticlePlain} =~ s{</script>}{<###/script>}xmsg;
2701                $Item->{ArticleData}->{Body}         =~ s{</script>}{<###/script>}xmsg;
2702
2703                my %ArticleFlagsAll = $ArticleObject->ArticleFlagGet(
2704                    ArticleID => $Item->{ArticleID},
2705                    UserID    => 1,
2706                );
2707
2708                my %ArticleFlagsMe = $ArticleObject->ArticleFlagGet(
2709                    ArticleID => $Item->{ArticleID},
2710                    UserID    => $Self->{UserID},
2711                );
2712
2713                $Item->{ArticleData}->{ArticleIsImportant} = $ArticleFlagsAll{Important};
2714                $Item->{ArticleData}->{ArticleIsSeen}      = $ArticleFlagsMe{Seen};
2715            }
2716            else {
2717
2718                if ( $Item->{Name} && $Item->{Name} =~ m/^%%/x ) {
2719
2720                    $Item->{Name} =~ s/^%%//xg;
2721                    my @Values = split( /%%/x, $Item->{Name} );
2722
2723                    # See documentation in AgentTicketHistory.pm, line 141+
2724                    if ( $Item->{HistoryType} eq 'TicketDynamicFieldUpdate' ) {
2725                        @Values = ( $Values[1], $Values[5] // '', $Values[3] // '' );
2726                    }
2727                    elsif ( $Item->{HistoryType} eq 'TypeUpdate' ) {
2728                        @Values = ( $Values[2], $Values[3], $Values[0], $Values[1] );
2729                    }
2730
2731                    $Item->{Name} = $LayoutObject->{LanguageObject}->Translate(
2732                        $HistoryTypes{ $Item->{HistoryType} },
2733                        @Values,
2734                    );
2735
2736                    # remove not needed place holder
2737                    $Item->{Name} =~ s/\%s//xg;
2738                }
2739            }
2740
2741            # make the history type more readable (if applicable)
2742            $Item->{HistoryTypeReadable} = $Self->{HistoryTypeMapping}->{ $Item->{HistoryType} }
2743                || $Item->{HistoryType};
2744
2745            # group items which happened (nearly) coincidently together
2746            my $CreateSystemTimeObject = $Kernel::OM->Create(
2747                'Kernel::System::DateTime',
2748                ObjectParams => {
2749                    String => $Item->{CreateTime},
2750                },
2751            );
2752            $Item->{CreateSystemTime} = $CreateSystemTimeObject
2753                ? $CreateSystemTimeObject->ToEpoch()
2754                : undef;
2755
2756            # if we have two events that happened 'nearly' the same time, treat
2757            # them as if they happened exactly on the same time (threshold 5 seconds)
2758            if (
2759                $LastCreateSystemTime
2760                && $Item->{CreateSystemTime} <= $LastCreateSystemTime
2761                && $Item->{CreateSystemTime} >= ( $LastCreateSystemTime - 5 )
2762                )
2763            {
2764                push @{ $HistoryItems{$LastCreateTime} }, $Item;
2765                $Item->{CreateTime} = $LastCreateTime;
2766            }
2767            else {
2768                push @{ $HistoryItems{ $Item->{CreateTime} } }, $Item;
2769            }
2770
2771            $LastCreateTime       = $Item->{CreateTime};
2772            $LastCreateSystemTime = $Item->{CreateSystemTime};
2773        }
2774
2775        my $SortByArticle = sub {
2776
2777            my $IsA = grep { $_ eq $a->{HistoryType} } @TypesWithArticles;
2778            my $IsB = grep { $_ eq $b->{HistoryType} } @TypesWithArticles;
2779            $IsB cmp $IsA;
2780        };
2781
2782        # sort history items based on items with articles
2783        # these items should always be on top of a list of connected items
2784        $ItemCounter = 0;
2785        for my $Item ( reverse sort keys %HistoryItems ) {
2786
2787            for my $SubItem ( sort $SortByArticle @{ $HistoryItems{$Item} } ) {
2788                $SubItem->{Counter} = $ItemCounter++;
2789
2790                if ( grep { $_ eq $SubItem->{HistoryType} } @TypesRight ) {
2791                    $SubItem->{Orientation} = 'Right';
2792                }
2793                else {
2794                    $SubItem->{Orientation} = 'Left';
2795                }
2796                push @{ $Param{Items} }, $SubItem;
2797            }
2798        }
2799
2800        # set TicketID for usage in JS
2801        $Param{TicketID} = $Self->{TicketID};
2802
2803        # set key 'TimeLong' for JS
2804        for my $Item ( @{ $Param{Items} } ) {
2805            $Item->{TimeLong}
2806                = $LayoutObject->{LanguageObject}->FormatTimeString( $Item->{CreateTime}, 'DateFormatLong' );
2807        }
2808
2809        for my $ArticleID ( sort keys %{$ArticlesByArticleID} ) {
2810
2811            # Check if article has attachment(s).
2812            if ( IsHashRefWithData( $ArticlesByArticleID->{$ArticleID}->{Atms} ) ) {
2813
2814                my ($Index)
2815                    = grep { $Param{Items}->[$_]->{ArticleID} && $Param{Items}->[$_]->{ArticleID} == $ArticleID }
2816                    0 .. @{ $Param{Items} };
2817                $Param{Items}->[$Index]->{HasAttachment} = 1;
2818            }
2819        }
2820
2821        # Get NoTimelineViewAutoArticle config value for usage in JS.
2822        $LayoutObject->AddJSData(
2823            Key   => 'NoTimelineViewAutoArticle',
2824            Value => $ConfigObject->Get('NoTimelineViewAutoArticle') || '0',
2825        );
2826
2827        # Include current article ID only if it's selected.
2828        $Param{CurrentArticleID} //= $Self->{ArticleID};
2829
2830        # Modify body text to avoid '</script>' tag issue (see bug#14023).
2831        for my $ArticleBoxItem (@ArticleBox) {
2832            $ArticleBoxItem->{Body} =~ s{</script>}{<###/script>}xmsg;
2833        }
2834
2835        # send data to JS
2836        $LayoutObject->AddJSData(
2837            Key   => 'TimelineView',
2838            Value => {
2839                Enabled => $ConfigObject->Get('TimelineViewEnabled'),
2840                Data    => \%Param,
2841            },
2842        );
2843
2844        $LayoutObject->Block(
2845            Name => 'TimelineView',
2846            Data => \%Param,
2847        );
2848
2849        # jump to selected article
2850        if ( $Self->{ArticleID} ) {
2851            $LayoutObject->Block(
2852                Name => 'ShowSelectedArticle',
2853                Data => {
2854                    ArticleID => $Self->{ArticleID},
2855                },
2856            );
2857        }
2858
2859        # render action menu for all articles
2860        for my $ArticleID ( sort keys %{$ArticlesByArticleID} ) {
2861
2862            # show attachments box
2863            if ( IsHashRefWithData( $ArticlesByArticleID->{$ArticleID}->{Atms} ) ) {
2864
2865                my $ArticleAttachments = $Self->_CollectArticleAttachments(
2866                    Article => $ArticlesByArticleID->{$ArticleID},
2867                );
2868
2869                $LayoutObject->Block(
2870                    Name => 'TimelineViewArticleAttachments',
2871                    Data => {
2872                        TicketID    => $Self->{TicketID},
2873                        ArticleID   => $ArticleID,
2874                        Attachments => $ArticleAttachments,
2875                    },
2876                );
2877            }
2878        }
2879    }
2880
2881    # return output
2882    return $LayoutObject->Output(
2883        TemplateFile => 'AgentTicketZoom',
2884        Data         => { %Param, %Ticket },
2885    );
2886}
2887
2888sub _TicketItemSeen {
2889    my ( $Self, %Param ) = @_;
2890
2891    my @Articles = $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleList(
2892        TicketID => $Param{TicketID},
2893    );
2894
2895    for my $Article (@Articles) {
2896        $Self->_ArticleItemSeen(
2897            TicketID  => $Param{TicketID},
2898            ArticleID => $Article->{ArticleID},
2899        );
2900    }
2901
2902    return 1;
2903}
2904
2905sub _ArticleItemSeen {
2906    my ( $Self, %Param ) = @_;
2907
2908    # mark shown article as seen
2909    $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleFlagSet(
2910        TicketID  => $Param{TicketID},
2911        ArticleID => $Param{ArticleID},
2912        Key       => 'Seen',
2913        Value     => 1,
2914        UserID    => $Self->{UserID},
2915    );
2916
2917    return 1;
2918}
2919
2920sub _ArticleItem {
2921    my ( $Self, %Param ) = @_;
2922
2923    my %Ticket    = %{ $Param{Ticket} };
2924    my %Article   = %{ $Param{Article} };
2925    my %AclAction = %{ $Param{AclAction} };
2926
2927    my $TicketObject  = $Kernel::OM->Get('Kernel::System::Ticket');
2928    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
2929    my $LayoutObject  = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
2930
2931    # Get article data.
2932    # my $ArticleBackendObject = $Kernel::OM->Get('Kernel::System::Ticket::Article')->BackendForArticle(%Param);
2933
2934    # show article actions
2935    my @MenuItems = $LayoutObject->ArticleActions(
2936        %Param,
2937        TicketID  => $Param{Ticket}->{TicketID},
2938        ArticleID => $Param{Article}->{ArticleID},
2939        Type      => $Param{Type},
2940    );
2941
2942    push @{ $Self->{MenuItems} }, \@MenuItems;
2943
2944    # TODO: Review
2945    return $Self->_ArticleRender(
2946        TicketID               => $Ticket{TicketID},
2947        ArticleID              => $Article{ArticleID},
2948        UserID                 => $Self->{UserID},
2949        ShowBrowserLinkMessage => $Self->{DoNotShowBrowserLinkMessage} ? 0 : 1,
2950        Type                   => $Param{Type},
2951        MenuItems              => \@MenuItems,
2952    );
2953}
2954
2955sub _CollectArticleAttachments {
2956
2957    my ( $Self, %Param ) = @_;
2958
2959    my %Article = %{ $Param{Article} };
2960
2961    my %Attachments;
2962
2963    # get cofig object
2964    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
2965
2966    # download type
2967    my $Type = $ConfigObject->Get('AttachmentDownloadType') || 'attachment';
2968
2969    $Article{AtmCount} = scalar keys %{ $Article{Atms} // {} };
2970
2971    # if attachment will be forced to download, don't open a new download window!
2972    my $Target = 'target="AttachmentWindow" ';
2973    if ( $Type =~ /inline/i ) {
2974        $Target = 'target="attachment" ';
2975    }
2976
2977    $Attachments{ZoomAttachmentDisplayCount} = $ConfigObject->Get('Ticket::ZoomAttachmentDisplayCount');
2978
2979    ATTACHMENT:
2980    for my $FileID ( sort keys %{ $Article{Atms} } ) {
2981        push @{ $Attachments{Files} }, {
2982            ArticleID => $Article{ArticleID},
2983            %{ $Article{Atms}->{$FileID} },
2984            FileID => $FileID,
2985            Target => $Target,
2986        };
2987    }
2988
2989    return \%Attachments;
2990}
2991
2992sub _ArticleBoxGet {
2993    my ( $Self, %Param ) = @_;
2994
2995    # Check needed stuff.
2996    for my $Needed (qw(Page ArticleBoxAll Limit)) {
2997        if ( !$Param{$Needed} ) {
2998            $Kernel::OM->Get('Kernel::System::Log')->Log(
2999                Priority => 'error',
3000                Message  => "Need $Needed!",
3001            );
3002            return;
3003        }
3004    }
3005
3006    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
3007
3008    my $Start = ( $Param{Page} - 1 ) * $Param{Limit};
3009
3010    my $End = $Param{Page} * $Param{Limit} - 1;
3011    if ( $End >= scalar @{ $Param{ArticleBoxAll} } ) {
3012
3013        # Make sure that end index doesn't exceed array size.
3014        $End = scalar @{ $Param{ArticleBoxAll} } - 1;
3015    }
3016
3017    my @ArticleIndexes = ( $Start .. $End );
3018
3019    my $CommunicationChannelObject = $Kernel::OM->Get('Kernel::System::CommunicationChannel');
3020
3021    # Save communication channel data to improve performance.
3022    my %CommunicationChannelData;
3023
3024    my @ArticleBox;
3025    for my $Index (@ArticleIndexes) {
3026        my $ArticleBackendObject = $ArticleObject->BackendForArticle(
3027            TicketID  => $Self->{TicketID},
3028            ArticleID => $Param{ArticleBoxAll}->[$Index]->{ArticleID},
3029        );
3030
3031        my %Article = $ArticleBackendObject->ArticleGet(
3032            TicketID      => $Self->{TicketID},
3033            ArticleID     => $Param{ArticleBoxAll}->[$Index]->{ArticleID},
3034            DynamicFields => 1,
3035            RealNames     => 1,
3036        );
3037
3038        # Include some information about communication channel.
3039        if ( !$CommunicationChannelData{ $Article{CommunicationChannelID} } ) {
3040
3041            # Communication channel display name is part of the configuration.
3042            my %CommunicationChannel = $CommunicationChannelObject->ChannelGet(
3043                ChannelID => $Article{CommunicationChannelID},
3044            );
3045
3046            # Presence of communication channel object indicates its validity.
3047            my $ChannelObject = $CommunicationChannelObject->ChannelObjectGet(
3048                ChannelID => $Article{CommunicationChannelID},
3049            );
3050
3051            $CommunicationChannelData{ $Article{CommunicationChannelID} } = {
3052                ChannelName        => $CommunicationChannel{ChannelName},
3053                ChannelDisplayName => $CommunicationChannel{DisplayName},
3054                ChannelInvalid     => !$ChannelObject,
3055            };
3056        }
3057
3058        %Article = ( %Article, %{ $CommunicationChannelData{ $Article{CommunicationChannelID} } } );
3059
3060        push @ArticleBox, \%Article;
3061    }
3062
3063    return @ArticleBox;
3064}
3065
3066=head2 _ArticleRender()
3067
3068Returns article html.
3069
3070    my $HTML = $Self->_ArticleRender(
3071        TicketID               => 123,      # (required)
3072        ArticleID              => 123,      # (required)
3073        Type                   => 'Static', # (required) Static or OnLoad
3074        ShowBrowserLinkMessage => 1,        # (optional)
3075    );
3076
3077Result:
3078    $HTML = "<div>...</div>";
3079
3080=cut
3081
3082sub _ArticleRender {
3083    my ( $Self, %Param ) = @_;
3084
3085    # Check needed stuff.
3086    for my $Needed (qw(TicketID ArticleID Type)) {
3087        if ( !$Param{$Needed} ) {
3088            $Kernel::OM->Get('Kernel::System::Log')->Log(
3089                Priority => 'error',
3090                Message  => "Need $Needed!",
3091            );
3092            return;
3093        }
3094    }
3095
3096    # Get article data.
3097    my $ArticleBackendObject = $Kernel::OM->Get('Kernel::System::Ticket::Article')->BackendForArticle(%Param);
3098
3099    # Determine channel name for this Article.
3100    my $ChannelName = $ArticleBackendObject->ChannelNameGet();
3101
3102    my $Loaded = $Kernel::OM->Get('Kernel::System::Main')->Require(
3103        "Kernel::Output::HTML::TicketZoom::Agent::$ChannelName",
3104    );
3105    return if !$Loaded;
3106
3107    return $Kernel::OM->Get("Kernel::Output::HTML::TicketZoom::Agent::$ChannelName")->ArticleRender(
3108        %Param,
3109        ArticleActions => $Param{MenuItems},
3110        UserID         => $Self->{UserID},
3111    );
3112}
3113
31141;
3115