# --
# OTOBO is a web-based ticketing system for service organisations.
# --
# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/
# Copyright (C) 2019-2025 Rother OSS GmbH, https://otobo.io/
# --
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# --

package Kernel::Modules::AgentTicketZoom;

use strict;
use warnings;
use utf8;

our $ObjectManagerDisabled = 1;

# core modules
use List::Util qw(any);
use POSIX      qw(ceil);

# CPAN modules

# OTOBO modules
use Kernel::System::VariableCheck qw(:all);
use Kernel::Language              qw(Translatable);

sub new {
    my ( $Type, %Param ) = @_;

    # allocate new hash for object
    my $Self = bless {%Param}, $Type;

    # set debug
    $Self->{Debug} = 0;

    # get needed objects
    my $ParamObject  = $Kernel::OM->Get('Kernel::System::Web::Request');
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
    my $UserObject   = $Kernel::OM->Get('Kernel::System::User');
    my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');

    $Self->{ArticleID}      = $ParamObject->GetParam( Param => 'ArticleID' );
    $Self->{ArticleView}    = $ParamObject->GetParam( Param => 'ArticleView' );
    $Self->{ZoomExpand}     = $ParamObject->GetParam( Param => 'ZoomExpand' );
    $Self->{ZoomExpandSort} = $ParamObject->GetParam( Param => 'ZoomExpandSort' );
    $Self->{ZoomTimeline}   = $ParamObject->GetParam( Param => 'ZoomTimeline' );

    my %UserPreferences = $UserObject->GetPreferences(
        UserID => $Self->{UserID},
    );

    # save last used view type in preferences
    if ( !$Self->{Subaction} ) {

        if (
            !defined $Self->{ArticleView}
            && !defined $Self->{ZoomExpand}
            && !defined $Self->{ZoomTimeline}
            )
        {
            $Self->{ZoomExpand} = $ConfigObject->Get('Ticket::Frontend::AgentZoomExpand');
            if ( $UserPreferences{UserLastUsedZoomViewType} ) {
                if ( $UserPreferences{UserLastUsedZoomViewType} eq 'Expand' ) {
                    $Self->{ZoomExpand} = 1;
                }
                elsif ( $UserPreferences{UserLastUsedZoomViewType} eq 'Collapse' ) {
                    $Self->{ZoomExpand} = 0;
                }
                elsif ( $UserPreferences{UserLastUsedZoomViewType} eq 'Timeline' ) {
                    $Self->{ZoomTimeline} = 1;
                }
            }
        }
        elsif (
            defined $Self->{ArticleView}
            || defined $Self->{ZoomExpand}
            || defined $Self->{ZoomTimeline}
            )
        {
            my $LastUsedZoomViewType = '';

            if ( defined $Self->{ArticleView} ) {
                $LastUsedZoomViewType = $Self->{ArticleView};

                if ( $Self->{ArticleView} eq 'Expand' ) {
                    $Self->{ZoomExpand} = 1;
                }
                elsif ( $Self->{ArticleView} eq 'Collapse' ) {
                    $Self->{ZoomExpand} = 0;
                }
                elsif ( $Self->{ArticleView} eq 'Timeline' ) {
                    $Self->{ZoomTimeline} = 1;
                }
                else {
                    $LastUsedZoomViewType = $ConfigObject->Get('Ticket::Frontend::AgentZoomExpand')
                        ? 'Expand'
                        : 'Collapse';
                }
            }
            elsif ( defined $Self->{ZoomExpand} && $Self->{ZoomExpand} == 1 ) {
                $LastUsedZoomViewType = 'Expand';
            }
            elsif ( defined $Self->{ZoomExpand} && $Self->{ZoomExpand} == 0 ) {
                $LastUsedZoomViewType = 'Collapse';
            }
            elsif ( defined $Self->{ZoomTimeline} && $Self->{ZoomTimeline} == 1 ) {
                $LastUsedZoomViewType = 'Timeline';
            }
            $UserObject->SetPreferences(
                UserID => $Self->{UserID},
                Key    => 'UserLastUsedZoomViewType',
                Value  => $LastUsedZoomViewType,
            );
        }
    }

    if ( !$ConfigObject->Get('TimelineViewEnabled') ) {
        $Self->{ZoomTimeline} = 0;
    }

    # whether the message "To open links in the following article, ..." is shown
    $Self->{DoNotShowBrowserLinkMessage} //= $UserPreferences{UserAgentDoNotShowBrowserLinkMessage};

    $Self->{ZoomExpandSort} //= $ConfigObject->Get('Ticket::Frontend::ZoomExpandSort');

    $Self->{ArticleFilterActive} = $ConfigObject->Get('Ticket::Frontend::TicketArticleFilter');

    # define if rich text should be used
    $Self->{RichText} = $ConfigObject->Get('Ticket::Frontend::ZoomRichTextForce')
        || $LayoutObject->{BrowserRichText}
        || 0;

    # Always exclude plain text attachment, but exclude HTML body only if rich text is enabled.
    $Self->{ExcludeAttachments} = {
        ExcludePlainText => 1,
        ExcludeHTMLBody  => $Self->{RichText},
        ExcludeInline    => $Self->{RichText},
    };

    # get ticket object
    my $TicketObject = $Kernel::OM->Get('Kernel::System::Ticket');

    # ticket id lookup
    if ( !$Self->{TicketID} && $ParamObject->GetParam( Param => 'TicketNumber' ) ) {
        $Self->{TicketID} = $TicketObject->TicketIDLookup(
            TicketNumber => $ParamObject->GetParam( Param => 'TicketNumber' ),
            UserID       => $Self->{UserID},
        );
    }

    # get display settings for AgentTicketZoom, e.g. which widgets are shown
    $Self->{DisplaySettings} = $ConfigObject->Get("Ticket::Frontend::AgentTicketZoom");

    # this is a mapping of history types which is being used
    # for the timeline view and its event type filter
    $Self->{HistoryTypeMapping} = {
        TicketLinkDelete                => Translatable('Link Deleted'),
        Lock                            => Translatable('Ticket Locked'),
        SetPendingTime                  => Translatable('Pending Time Set'),
        TicketDynamicFieldUpdate        => Translatable('Dynamic Field Updated'),
        EmailAgentInternal              => Translatable('Outgoing Email (internal)'),
        NewTicket                       => Translatable('Ticket Created'),
        TypeUpdate                      => Translatable('Type Updated'),
        EscalationUpdateTimeStart       => Translatable('Escalation Update Time In Effect'),
        EscalationUpdateTimeStop        => Translatable('Escalation Update Time Stopped'),
        EscalationFirstResponseTimeStop => Translatable('Escalation First Response Time Stopped'),
        CustomerUpdate                  => Translatable('Customer Updated'),
        ChatInternal                    => Translatable('Internal Chat'),
        SendAutoFollowUp                => Translatable('Automatic Follow-Up Sent'),
        AddNote                         => Translatable('Note Added'),
        AddNoteCustomer                 => Translatable('Note Added (Customer)'),
        AddSMS                          => Translatable('SMS Added'),
        AddSMSCustomer                  => Translatable('SMS Added (Customer)'),
        StateUpdate                     => Translatable('State Updated'),
        SendAnswer                      => Translatable('Outgoing Answer'),
        ServiceUpdate                   => Translatable('Service Updated'),
        TicketLinkAdd                   => Translatable('Link Added'),
        EmailCustomer                   => Translatable('Incoming Customer Email'),
        WebRequestCustomer              => Translatable('Incoming Web Request'),
        PriorityUpdate                  => Translatable('Priority Updated'),
        Unlock                          => Translatable('Ticket Unlocked'),
        EmailAgent                      => Translatable('Outgoing Email'),
        TitleUpdate                     => Translatable('Title Updated'),
        OwnerUpdate                     => Translatable('New Owner'),
        Merged                          => Translatable('Ticket Merged'),
        PhoneCallAgent                  => Translatable('Outgoing Phone Call'),
        Forward                         => Translatable('Forwarded Message'),
        Unsubscribe                     => Translatable('Removed User Subscription'),
        TimeAccounting                  => Translatable('Time Accounted'),
        PhoneCallCustomer               => Translatable('Incoming Phone Call'),
        SystemRequest                   => Translatable('System Request.'),
        FollowUp                        => Translatable('Incoming Follow-Up'),
        SendAutoReply                   => Translatable('Automatic Reply Sent'),
        SendAutoReject                  => Translatable('Automatic Reject Sent'),
        ResponsibleUpdate               => Translatable('New Responsible'),
        EscalationSolutionTimeStart     => Translatable('Escalation Solution Time In Effect'),
        EscalationSolutionTimeStop      => Translatable('Escalation Solution Time Stopped'),
        EscalationResponseTimeStart     => Translatable('Escalation Response Time In Effect'),
        EscalationResponseTimeStop      => Translatable('Escalation Response Time Stopped'),
        SLAUpdate                       => Translatable('SLA Updated'),
        ChatExternal                    => Translatable('External Chat'),
        Move                            => Translatable('Queue Changed'),
        SendAgentNotification           => Translatable('Notification Was Sent'),
    };

    # Add custom files to the zoom's frontend module registration on the fly
    #    to avoid conflicts with other modules.
    if (
        defined $ConfigObject->Get('TimelineViewEnabled')
        && $ConfigObject->Get('TimelineViewEnabled') == 1
        )
    {
        $ConfigObject->Set(
            Key   => 'Loader::Module::AgentTicketZoom###003-DynamicField',
            Value => {
                JavaScript => [
                    'Core.Agent.TicketZoom.TimelineView.js',
                ],
            },
        );
    }

    my $ArticleShowStatus = $Kernel::OM->Get('Kernel::System::Ticket::ArticleFeatures')->ShowDeletedArticles(
        TicketID  => $Self->{TicketID},
        UserID    => $Self->{UserID},
        GetStatus => 1
    );

    $Self->{ShowDeletedArticles} = $ArticleShowStatus ? 1 : 0;
    $Self->{ArticleStorage}      = $ConfigObject->Get('Ticket::Article::Backend::MIMEBase::ArticleStorage');

    return $Self;
}

sub Run {
    my ( $Self, %Param ) = @_;

    # get layout object
    my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');

    # check needed stuff
    if ( !$Self->{TicketID} ) {
        return $LayoutObject->ErrorScreen(
            Message => Translatable('No TicketID is given!'),
            Comment => Translatable('Please contact the administrator.'),
        );
    }

    # get needed objects
    my $TicketObject  = $Kernel::OM->Get('Kernel::System::Ticket');
    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');

    # check permissions
    my $Access = $TicketObject->TicketPermission(
        Type     => 'ro',
        TicketID => $Self->{TicketID},
        UserID   => $Self->{UserID}
    );

    # error screen, don't show ticket
    return $LayoutObject->NoPermission(
        Message => Translatable(
            "This ticket does not exist, or you don't have permissions to access it in its current state."
        ),
        WithHeader => $Self->{Subaction} && $Self->{Subaction} eq 'ArticleUpdate' ? 'no' : 'yes',
    ) if !$Access;

    # get ticket attributes
    my %Ticket = $TicketObject->TicketGet(
        TicketID      => $Self->{TicketID},
        DynamicFields => 1,
    );

    # get ACL restrictions
    my %PossibleActions;
    my $Counter = 0;

    # get config object
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');

    # get all registered Actions
    if ( ref $ConfigObject->Get('Frontend::Module') eq 'HASH' ) {

        my %Actions = %{ $ConfigObject->Get('Frontend::Module') };

        # only use those Actions that stats with Agent
        %PossibleActions = map { ++$Counter => $_ }
            grep { substr( $_, 0, length 'Agent' ) eq 'Agent' }
            sort keys %Actions;
    }

    my $ACL = $TicketObject->TicketAcl(
        Data          => \%PossibleActions,
        Action        => $Self->{Action},
        TicketID      => $Self->{TicketID},
        ReturnType    => 'Action',
        ReturnSubType => '-',
        UserID        => $Self->{UserID},
    );

    my %AclAction = %PossibleActions;
    if ($ACL) {
        %AclAction = $TicketObject->TicketAclActionData();
    }

    # check if ACL restrictions exist
    my %AclActionLookup = reverse %AclAction;

    # show error screen if ACL prohibits this action
    if ( !$AclActionLookup{ $Self->{Action} } ) {
        return $LayoutObject->NoPermission( WithHeader => 'yes' );
    }

    # send parameter TicketID to JS
    $LayoutObject->AddJSData(
        Key   => 'TicketID',
        Value => $Self->{TicketID},
    );

    # mark shown ticket as seen
    if ( $Self->{Subaction} eq 'TicketMarkAsSeen' ) {
        my $Success = 1;

        # always show archived tickets as seen
        if ( $Ticket{ArchiveFlag} ne 'y' ) {
            $Success = $Self->_TicketItemSeen( TicketID => $Self->{TicketID} );
        }

        return $LayoutObject->Attachment(
            ContentType => 'text/html',
            Content     => $Success,
            Type        => 'inline',
            NoCache     => 1,
        );
    }

    if ( $Self->{Subaction} eq 'MarkAsImportant' ) {

        # Owner and Responsible can mark articles as important or remove mark
        if (
            $Self->{UserID} == $Ticket{OwnerID}
            || (
                $ConfigObject->Get('Ticket::Responsible')
                && $Self->{UserID} == $Ticket{ResponsibleID}
            )
            )
        {

            # Always use user id 1 because other users also have to see the important flag
            my %ArticleFlag = $ArticleObject->ArticleFlagGet(
                TicketID  => $Self->{TicketID},
                ArticleID => $Self->{ArticleID},
                UserID    => 1,
            );

            my $ArticleIsImportant = $ArticleFlag{Important};
            if ($ArticleIsImportant) {

                # Always use user id 1 because other users also have to see the important flag
                $ArticleObject->ArticleFlagDelete(
                    TicketID  => $Self->{TicketID},
                    ArticleID => $Self->{ArticleID},
                    Key       => 'Important',
                    UserID    => 1,
                );
            }
            else {

                # Always use user id 1 because other users also have to see the important flag
                $ArticleObject->ArticleFlagSet(
                    TicketID  => $Self->{TicketID},
                    ArticleID => $Self->{ArticleID},
                    Key       => 'Important',
                    Value     => 1,
                    UserID    => 1,
                );
            }
        }

        return $LayoutObject->Redirect(
            OP => "Action=AgentTicketZoom;TicketID=$Self->{TicketID};ArticleID=$Self->{ArticleID}",
        );
    }

    # get required objects
    my $ParamObject = $Kernel::OM->Get('Kernel::System::Web::Request');
    my $MainObject  = $Kernel::OM->Get('Kernel::System::Main');

    if ( $Self->{Subaction} eq 'FormDraftDelete' ) {
        my %Response;

        my $FormDraftID = $ParamObject->GetParam( Param => 'FormDraftID' ) || '';
        if ($FormDraftID) {
            $Response{Success} = $Kernel::OM->Get('Kernel::System::FormDraft')->FormDraftDelete(
                FormDraftID => $FormDraftID,
                UserID      => $Self->{UserID},
            );
        }
        else {
            $Response{Error} = $LayoutObject->{LanguageObject}->Translate("Missing FormDraftID!");
        }

        # build JSON output
        my $JSON = $LayoutObject->JSONEncode(
            Data => \%Response,
        );

        # send JSON response
        return $LayoutObject->Attachment(
            ContentType => 'application/json',
            Content     => $JSON,
            Type        => 'inline',
            NoCache     => 1,
        );
    }

    if ( $Self->{Subaction} eq 'LoadWidget' ) {
        my $ElementID = $ParamObject->GetParam( Param => 'ElementID' );
        my $Config;
        WIDGET:
        for my $Key ( sort keys %{ $Self->{DisplaySettings}->{Widgets} // {} } ) {
            if ( $ElementID eq 'Async_' . $LayoutObject->LinkEncode($Key) ) {
                $Config = $Self->{DisplaySettings}->{Widgets}->{$Key};
                last WIDGET;
            }
        }
        if ($Config) {
            my $Success = eval { $MainObject->Require( $Config->{Module} ) };
            if ( !$Success ) {
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  => "Cannot load $Config->{Module}: $@",
                );
                return $LayoutObject->Attachment(
                    ContentType => 'text/html',
                    Content     => '',
                    Type        => 'inline',
                    NoCache     => 1,
                );
            }
            my $Module = eval { $Config->{Module}->new( %{$Self} ) };
            if ( !$Module ) {
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  => "new() of Widget module $Config->{Module} not successful!",

                );
                return $LayoutObject->Attachment(
                    ContentType => 'text/html',
                    Content     => '',
                    Type        => 'inline',
                    NoCache     => 1,
                );
            }
            my $WidgetOutput = $Module->Run(
                Ticket    => \%Ticket,
                AclAction => \%AclAction,
                Config    => $Config,
            );

            return $LayoutObject->Attachment(
                ContentType => 'text/html',
                Content     => $WidgetOutput->{Output} // ' ',
                Type        => 'inline',
                NoCache     => 1,
                Charset     => 'utf-8',
            );
        }
        else {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Cannot locate module for ElementID $ElementID",
            );
            return $LayoutObject->Attachment(
                ContentType => 'text/html',
                Content     => '',
                Type        => 'inline',
                NoCache     => 1,
            );
        }
    }

    # mark shown article as seen
    if ( $Self->{Subaction} eq 'MarkAsSeen' ) {
        my $Success = 1;

        my $IsArticleDeleted = $Kernel::OM->Get('Kernel::System::Ticket::ArticleFeatures')->IsArticleDeleted(
            ArticleID => $Self->{ArticleID},
        );

        if ($IsArticleDeleted) {
            $Success = 2;
        }
        else {
            # always show archived tickets as seen
            if ( $Ticket{ArchiveFlag} ne 'y' ) {
                $Success = $Self->_ArticleItemSeen(
                    TicketID  => $Self->{TicketID},
                    ArticleID => $Self->{ArticleID}
                );
            }
        }

        return $LayoutObject->Attachment(
            ContentType => 'text/html',
            Content     => $Success,
            Type        => 'inline',
            NoCache     => 1,
        );
    }

    # article update
    elsif ( $Self->{Subaction} eq 'ArticleUpdate' ) {
        my $Count = $ParamObject->GetParam( Param => 'Count' );

        my $ArticleBackendObject = $ArticleObject->BackendForArticle(
            TicketID            => $Self->{TicketID},
            ArticleID           => $Self->{ArticleID},
            ShowDeletedArticles => $Self->{ShowDeletedArticles}
        );

        my %Article = $ArticleBackendObject->ArticleGet(
            TicketID      => $Self->{TicketID},
            ArticleID     => $Self->{ArticleID},
            RealNames     => 1,
            DynamicFields => 0,
        );
        $Article{Count} = $Count;

        # Get attachment index (excluding body attachments).
        my %AtmIndex = $ArticleBackendObject->ArticleAttachmentIndex(
            ArticleID => $Self->{ArticleID},
            %{ $Self->{ExcludeAttachments} },
        );
        $Article{Atms} = \%AtmIndex;

        # fetch all std. templates
        my %StandardTemplates = $Kernel::OM->Get('Kernel::System::Queue')->QueueStandardTemplateMemberList(
            QueueID       => $Ticket{QueueID},
            TemplateTypes => 1,
            Valid         => 1,
        );

        my $ArticleWidgetsHTML = $Self->_ArticleItem(
            Ticket            => \%Ticket,
            Article           => \%Article,
            AclAction         => \%AclAction,
            StandardResponses => $StandardTemplates{Answer},
            StandardForwards  => $StandardTemplates{Forward},
            Type              => 'OnLoad',
        );

        # send data to JS
        $LayoutObject->AddJSData(
            Key   => 'ArticleIDs',
            Value => [ $Self->{ArticleID} ],
        );
        $LayoutObject->AddJSData(
            Key   => 'MenuItems',
            Value => $Self->{MenuItems},
        );

        my $Content = $LayoutObject->Output(
            TemplateFile => 'AgentTicketZoom',
            Data         => {
                %Ticket,
                %Article,
                %AclAction,
                ArticleWidgetsHTML => $ArticleWidgetsHTML
            },
            AJAX => 1,
        );
        if ( !$Content ) {
            $LayoutObject->FatalError(
                Message =>
                    $LayoutObject->{LanguageObject}->Translate( 'Can\'t get for ArticleID %s!', $Self->{ArticleID} ),
            );
        }
        return $LayoutObject->Attachment(
            ContentType => 'text/html',
            Charset     => $LayoutObject->{UserCharset},
            Content     => $Content,
            Type        => 'inline',
            NoCache     => 1,
        );
    }

    # get needed objects
    my $UserObject    = $Kernel::OM->Get('Kernel::System::User');
    my $SessionObject = $Kernel::OM->Get('Kernel::System::AuthSession');

    # write article filter settings to session
    if ( $Self->{Subaction} eq 'ArticleFilterSet' ) {

        # get params
        my $TicketID                      = $ParamObject->GetParam( Param => 'TicketID' );
        my $SaveDefaults                  = $ParamObject->GetParam( Param => 'SaveDefaults' );
        my @CommunicationChannelFilterIDs = $ParamObject->GetArray( Param => 'CommunicationChannelFilter' );
        my $CustomerVisibility            = $ParamObject->GetParam( Param => 'CustomerVisibilityFilter' );
        my @ArticleSenderTypeFilterIDs    = $ParamObject->GetArray( Param => 'ArticleSenderTypeFilter' );

        # build session string
        my $SessionString = '';
        if (@CommunicationChannelFilterIDs) {
            $SessionString .= 'CommunicationChannelFilter<';
            $SessionString .= join ',', @CommunicationChannelFilterIDs;
            $SessionString .= '>';
        }
        if ( defined $CustomerVisibility && $CustomerVisibility != 2 ) {
            $SessionString .= "CustomerVisibilityFilter<$CustomerVisibility>";
        }
        if (@ArticleSenderTypeFilterIDs) {
            $SessionString .= 'ArticleSenderTypeFilter<';
            $SessionString .= join ',', @ArticleSenderTypeFilterIDs;
            $SessionString .= '>';
        }

        # write the session

        # save default filter settings to user preferences
        if ($SaveDefaults) {
            $UserObject->SetPreferences(
                UserID => $Self->{UserID},
                Key    => 'ArticleFilterDefault',
                Value  => $SessionString,
            );
            $SessionObject->UpdateSessionID(
                SessionID => $Self->{SessionID},
                Key       => 'ArticleFilterDefault',
                Value     => $SessionString,
            );
        }

        # turn off filter explicitly for this ticket
        if ( $SessionString eq '' ) {
            $SessionString = 'off';
        }

        # update the session
        my $Update = $SessionObject->UpdateSessionID(
            SessionID => $Self->{SessionID},
            Key       => "ArticleFilter$TicketID",
            Value     => $SessionString,
        );

        # build JSON output
        my $JSON = '';
        if ($Update) {
            $JSON = $LayoutObject->JSONEncode(
                Data => {
                    Message => Translatable('Article filter settings were saved.'),
                },
            );
        }

        # send JSON response
        return $LayoutObject->Attachment(
            ContentType => 'application/json',
            Content     => $JSON,
            Type        => 'inline',
            NoCache     => 1,
        );
    }

    # write article filter settings to session
    if ( $Self->{Subaction} eq 'EvenTypeFilterSet' ) {

        # get params
        my $TicketID           = $ParamObject->GetParam( Param => 'TicketID' );
        my $SaveDefaults       = $ParamObject->GetParam( Param => 'SaveDefaults' );
        my @EventTypeFilterIDs = $ParamObject->GetArray( Param => 'EventTypeFilter' );

        # build session string
        my $SessionString = '';
        if (@EventTypeFilterIDs) {
            $SessionString .= 'EventTypeFilter<';
            $SessionString .= join ',', @EventTypeFilterIDs;
            $SessionString .= '>';
        }

        # write the session

        # save default filter settings to user preferences
        if ($SaveDefaults) {
            $UserObject->SetPreferences(
                UserID => $Self->{UserID},
                Key    => 'EventTypeFilterDefault',
                Value  => $SessionString,
            );
            $SessionObject->UpdateSessionID(
                SessionID => $Self->{SessionID},
                Key       => 'EventTypeFilterDefault',
                Value     => $SessionString,
            );
        }

        # turn off filter explicitly for this ticket
        if ( $SessionString eq '' ) {
            $SessionString = 'off';
        }

        # update the session
        my $Update = $SessionObject->UpdateSessionID(
            SessionID => $Self->{SessionID},
            Key       => "EventTypeFilter$TicketID",
            Value     => $SessionString,
        );

        # build JSON output
        my $JSON = '';
        if ($Update) {
            $JSON = $LayoutObject->JSONEncode(
                Data => {
                    Message => Translatable('Event type filter settings were saved.'),
                },
            );
        }

        # send JSON response
        return $LayoutObject->Attachment(
            ContentType => 'application/json',
            Content     => $JSON,
            Type        => 'inline',
            NoCache     => 1,
        );
    }

    # article filter is activated in sysconfig
    if ( $Self->{ArticleFilterActive} ) {

        # get article filter settings from session string
        my $ArticleFilterSessionString = $Self->{ 'ArticleFilter' . $Self->{TicketID} };

        # set article filter for this ticket from user preferences
        if ( !$ArticleFilterSessionString ) {
            $ArticleFilterSessionString = $Self->{ArticleFilterDefault};
        }

        # do not use defaults for this ticket if filter was explicitly turned off
        elsif ( $ArticleFilterSessionString eq 'off' ) {
            $ArticleFilterSessionString = '';
        }

        # extract CommunicationChannels
        if (
            $ArticleFilterSessionString
            && $ArticleFilterSessionString =~ m{ CommunicationChannelFilter < ( [^<>]+ ) > }xms
            )
        {
            my @IDs = split /,/, $1;
            $Self->{ArticleFilter}->{CommunicationChannelID} = \@IDs;
        }

        # extract CustomerVisibility
        if (
            $ArticleFilterSessionString
            && $ArticleFilterSessionString =~ m{ CustomerVisibilityFilter < ( [^<>]+ ) > }xms
            )
        {
            $Self->{ArticleFilter}->{CustomerVisibility} = $1;
        }

        # extract ArticleSenderTypeIDs
        if (
            $ArticleFilterSessionString
            && $ArticleFilterSessionString =~ m{ ArticleSenderTypeFilter < ( [^<>]+ ) > }xms
            )
        {
            my @IDs = split /,/, $1;
            $Self->{ArticleFilter}->{ArticleSenderTypeID} = \@IDs;
        }

        # get event type filter settings from session string
        my $EventTypeFilterSessionString = $Self->{ 'EventTypeFilter' . $Self->{TicketID} };

        # set article filter for this ticket from user preferences
        if ( !$EventTypeFilterSessionString ) {
            $EventTypeFilterSessionString = $Self->{EventTypeFilterDefault};
        }

        # do not use defaults for this ticket if filter was explicitly turned off
        elsif ( $EventTypeFilterSessionString eq 'off' ) {
            $EventTypeFilterSessionString = '';
        }

        # Set article filter with value if it exists.
        if (
            $EventTypeFilterSessionString
            && $EventTypeFilterSessionString =~ m{ EventTypeFilter < ( [^<>]+ ) > }xms
            )
        {
            my @IDs = split /,/, $1;
            $Self->{EventTypeFilter}->{EventTypeID} = \@IDs;
        }
    }

    # generate output
    return join '',
        $LayoutObject->Header(
            Value    => $Ticket{TicketNumber},
            TicketID => $Ticket{TicketID},
        ),
        $LayoutObject->NavigationBar,
        $Self->MaskAgentZoom(
            Ticket    => \%Ticket,
            AclAction => \%AclAction
        ),
        $LayoutObject->Footer;
}

sub MaskAgentZoom {
    my ( $Self, %Param ) = @_;

    my %Ticket    = %{ $Param{Ticket} };
    my %AclAction = %{ $Param{AclAction} };

    # get needed objects
    my $TicketObject  = $Kernel::OM->Get('Kernel::System::Ticket');
    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');

    # Create a list of article sender types for lookup
    my %ArticleSenderTypeList = $ArticleObject->ArticleSenderTypeList();

    # else show normal ticket zoom view
    # fetch all move queues
    my %MoveQueues = $TicketObject->MoveList(
        TicketID => $Ticket{TicketID},
        UserID   => $Self->{UserID},
        Action   => 'AgentTicketMove',
        Type     => 'move_into',
    );

    # fetch all std. templates
    my %StandardTemplates = $Kernel::OM->Get('Kernel::System::Queue')->QueueStandardTemplateMemberList(
        QueueID       => $Ticket{QueueID},
        TemplateTypes => 1,
    );

    # get config object
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');

    # generate shown articles
    my $Limit = $ConfigObject->Get('Ticket::Frontend::MaxArticlesPerPage');

    my $Order = $Self->{ZoomExpandSort} eq 'reverse' ? 'DESC' : 'ASC';
    my $Page;

    # get param object
    my $ParamObject = $Kernel::OM->Get('Kernel::System::Web::Request');

    # get article page
    my $ArticlePage = $ParamObject->GetParam( Param => 'ArticlePage' );

    my $IsVisibleForCustomer;
    if ( defined $Self->{ArticleFilter}->{CustomerVisibility} ) {
        $IsVisibleForCustomer = $Self->{ArticleFilter}->{CustomerVisibility};
    }

    # Get all articles.
    my @ArticleBoxAll = $ArticleObject->ArticleList(
        TicketID             => $Self->{TicketID},
        IsVisibleForCustomer => $IsVisibleForCustomer,
        ShowDeletedArticles  => $Self->{ShowDeletedArticles}
    );

    if ( IsArrayRefWithData( $Self->{ArticleFilter}->{CommunicationChannelID} ) ) {
        my %Filter = map { $_ => 1 } @{ $Self->{ArticleFilter}->{CommunicationChannelID} };

        @ArticleBoxAll = grep { $Filter{ $_->{CommunicationChannelID} } } @ArticleBoxAll;
    }

    if ( IsArrayRefWithData( $Self->{ArticleFilter}->{ArticleSenderTypeID} ) ) {
        my %Filter = map { $_ => 1 } @{ $Self->{ArticleFilter}->{ArticleSenderTypeID} };

        @ArticleBoxAll = grep { $Filter{ $_->{SenderTypeID} } } @ArticleBoxAll;
    }

    if ( $Order eq 'DESC' ) {
        @ArticleBoxAll = reverse @ArticleBoxAll;
    }

    my %ArticleFlags = $ArticleObject->ArticleFlagsOfTicketGet(
        TicketID => $Ticket{TicketID},
        UserID   => $Self->{UserID},
    );
    my $ArticleID;

    if ( $Self->{ArticleID} ) {

        my @ArticleIDs = map { $_->{ArticleID} } @ArticleBoxAll;

        my %ArticleIndex;
        @ArticleIndex{@ArticleIDs} = ( 0 .. $#ArticleIDs );

        my $Index = $ArticleIndex{ $Self->{ArticleID} };
        $Index //= 0;
        $Page = int( $Index / $Limit ) + 1;
    }
    elsif ($ArticlePage) {
        $Page = $ArticlePage;
    }
    else {

        # Find latest not seen article.
        ARTICLE:
        for my $Article (@ArticleBoxAll) {

            # Ignore system sender type.
            if (
                $ConfigObject->Get('Ticket::NewArticleIgnoreSystemSender')
                && $ArticleSenderTypeList{ $Article->{SenderTypeID} } eq 'system'
                )
            {
                next ARTICLE;
            }

            next ARTICLE if $ArticleFlags{ $Article->{ArticleID} }->{Seen};
            $ArticleID = $Article->{ArticleID};

            my @ArticleIDs = map { $_->{ArticleID} } @ArticleBoxAll;

            my %ArticleIndex;
            @ArticleIndex{@ArticleIDs} = ( 0 .. $#ArticleIDs );

            my $Index = $ArticleIndex{$ArticleID};
            $Page = int( $Index / $Limit ) + 1;

            last ARTICLE;
        }

        if ( !$ArticleID ) {
            $Page = 1;
        }
    }

    # We need to find out whether pagination is actually necessary.
    # The easiest way would be count the articles, but that would slow
    # down the most common case (fewer articles than $Limit in the ticket).
    # So instead we use the following trick:
    # 1) if the $Page > 1, we need pagination
    # 2) if not, request $Limit + 1 articles. If $Limit + 1 are actually
    #    returned, pagination is necessary
    my $Extra = $Page > 1 ? 0 : 1;
    my $NeedPagination;

    my @ArticleBox = $Self->_ArticleBoxGet(
        Page          => $Page,
        ArticleBoxAll => \@ArticleBoxAll,
        Limit         => $Limit,
    );

    if ( !@ArticleBox && $Page > 1 ) {

        # If the page argument is past the actual number of pages.
        # This can happen when a new article filter was added.
        # Try to get results for the 1st page.

        @ArticleBox = $Self->_ArticleBoxGet(
            Page          => 1,
            ArticleBoxAll => \@ArticleBoxAll,
            Limit         => $Limit,
        );
    }

    if ( @ArticleBox > $Limit ) {
        pop @ArticleBox;
        $NeedPagination = 1;
    }
    elsif ( $Page == 1 && scalar @ArticleBoxAll <= $Limit ) {
        $NeedPagination = 0;
    }
    else {
        $NeedPagination = 1;
    }

    $Page ||= 1;

    my $Pages;
    if ($NeedPagination) {
        $Pages = ceil( scalar @ArticleBoxAll / $Limit );
    }

    my $ArticleIDFound = 0;
    ARTICLE:
    for my $Article (@ArticleBox) {
        next ARTICLE if !$Self->{ArticleID};
        next ARTICLE if !$Article->{ArticleID};
        next ARTICLE if $Self->{ArticleID} ne $Article->{ArticleID};

        $ArticleIDFound = 1;
    }

    # get selected or last customer article
    if ($ArticleIDFound) {
        $ArticleID = $Self->{ArticleID};
    }
    else {
        if ( !$ArticleID ) {
            if (@ArticleBox) {

                # set first listed article as fallback
                $ArticleID = $ArticleBox[0]->{ArticleID};

                # set last customer article as selected article replacing last set
                ARTICLETMP:
                for my $ArticleTmp (@ArticleBox) {
                    if ( $ArticleSenderTypeList{ $ArticleTmp->{SenderTypeID} } eq 'customer' ) {
                        $ArticleID = $ArticleTmp->{ArticleID};
                        last ARTICLETMP if $Self->{ZoomExpandSort} eq 'reverse';
                    }
                }
            }
        }
    }

    # check if expand view is usable (only for less then 400 article)
    # if you have more articles is going to be slow and not usable
    my $ArticleMaxLimit = $ConfigObject->Get('Ticket::Frontend::MaxArticlesZoomExpand')
        // 400;
    if ( $Self->{ZoomExpand} && $#ArticleBox > $ArticleMaxLimit ) {
        $Self->{ZoomExpand} = 0;
    }

    # get shown article(s)
    my @ArticleBoxShown;
    if ( !$Self->{ZoomExpand} ) {
        ARTICLEBOX:
        for my $ArticleTmp (@ArticleBox) {
            if ( $ArticleID eq $ArticleTmp->{ArticleID} ) {
                push @ArticleBoxShown, $ArticleTmp;
                last ARTICLEBOX;
            }
        }
    }
    else {
        @ArticleBoxShown = @ArticleBox;
    }

    # get layout object
    my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');

    # age design
    $Ticket{Age} = $LayoutObject->CustomerAge(
        Age   => $Ticket{Age},
        Space => ' '
    );

    my $MainObject = $Kernel::OM->Get('Kernel::System::Main');

    my %Widgets;
    my %AsyncWidgetActions;
    WIDGET:
    for my $Key ( sort keys %{ $Self->{DisplaySettings}->{Widgets} // {} } ) {
        my $Config = $Self->{DisplaySettings}->{Widgets}->{$Key};

        if ( $Config->{Async} ) {
            if ( !$Config->{Location} ) {
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  =>
                        "The configuration for $Config->{Module} must contain a Location, because it is marked as Async.",
                );
                next WIDGET;
            }
            my $ElementID = 'Async_' . $LayoutObject->LinkEncode($Key);
            push @{ $Widgets{ $Config->{Location} } }, {
                Async => 1,
                Rank  => $Config->{Rank} || $Key,
                %Ticket,
                ElementID => $ElementID,
            };
            $AsyncWidgetActions{$ElementID} = "Action=$Self->{Action};Subaction=LoadWidget;"
                . "TicketID=$Self->{TicketID};ElementID=$ElementID";
            next WIDGET;
        }
        my $Success = eval { $MainObject->Require( $Config->{Module} ) };

        next WIDGET unless $Success;

        my $Module = eval { $Config->{Module}->new(%$Self) };
        if ( !$Module ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "new() of Widget module $Config->{Module} not successful!",

            );

            next WIDGET;
        }

        my $WidgetOutput = $Module->Run(
            Ticket    => \%Ticket,
            AclAction => \%AclAction,
            Config    => $Config,
        );

        next WIDGET unless $WidgetOutput;

        $WidgetOutput->{Rank} //= $Key;
        my $Location = $WidgetOutput->{Location} || $Config->{Location};
        push @{ $Widgets{$Location} }, $WidgetOutput;
    }
    for my $Location ( sort keys %Widgets ) {
        $Param{ $Location . 'Widgets' } = [
            sort { $a->{Rank} cmp $b->{Rank} } @{ $Widgets{$Location} }
        ];
    }
    $LayoutObject->AddJSData(
        Key   => 'AsyncWidgetActions',
        Value => \%AsyncWidgetActions,
    );

    # set display options
    $Param{Hook} = $ConfigObject->Get('Ticket::Hook') || 'Ticket#';

    # only show article tree if articles are present,
    # or if a filter is set (so that the user has the option to
    # disable the filter)
    if ( @ArticleBox || $Self->{ArticleFilter} ) {

        my $Pagination;

        if ($NeedPagination) {
            $Pagination = {
                Pages       => $Pages,
                CurrentPage => $Page,
                TicketID    => $Ticket{TicketID},
            };
        }

        # show article tree
        $Param{ArticleTree} = $Self->_ArticleTree(
            Ticket            => \%Ticket,
            ArticleFlags      => \%ArticleFlags,
            ArticleID         => $ArticleID,
            ArticleMaxLimit   => $ArticleMaxLimit,
            ArticleBox        => \@ArticleBox,
            Pagination        => $Pagination,
            Page              => $Page,
            ArticleCount      => scalar @ArticleBox,
            AclAction         => \%AclAction,
            StandardResponses => $StandardTemplates{Answer},
            StandardForwards  => $StandardTemplates{Forward},
        );
    }

    # show articles items
    if ( !$Self->{ZoomTimeline} ) {

        my @ArticleIDs;
        $Param{ArticleItems} = '';

        my $ArticleWidgetsHTML = '';

        ARTICLE:
        for my $ArticleTmp (@ArticleBoxShown) {
            my %Article = %$ArticleTmp;

            $ArticleWidgetsHTML .= $Self->_ArticleItem(
                Ticket            => \%Ticket,
                Article           => \%Article,
                AclAction         => \%AclAction,
                StandardResponses => $StandardTemplates{Answer},
                StandardForwards  => $StandardTemplates{Forward},
                ActualArticleID   => $ArticleID,
                Type              => 'Static',
            );
            push @ArticleIDs, $ArticleTmp->{ArticleID};
        }

        # send data to JS
        $LayoutObject->AddJSData(
            Key   => 'ArticleIDs',
            Value => \@ArticleIDs,
        );
        $LayoutObject->AddJSData(
            Key   => 'MenuItems',
            Value => $Self->{MenuItems},
        );

        $Param{ArticleItems} .= $LayoutObject->Output(
            TemplateFile => 'AgentTicketZoom',
            Data         => {
                %Ticket,
                %AclAction,
                ArticleWidgetsHTML => $ArticleWidgetsHTML,
            },
        );
    }

    # always show archived tickets as seen
    if ( $Self->{ZoomExpand} && $Ticket{ArchiveFlag} ne 'y' ) {

        # send data to JS
        $LayoutObject->AddJSData(
            Key   => 'TicketItemMarkAsSeen',
            Value => 1,
        );
    }

    # number of articles
    $Param{ArticleCount} = scalar @ArticleBox;

    $LayoutObject->Block(
        Name => 'Header',
        Data => { %Param, %Ticket, %AclAction },
    );

    my %ActionLookup;
    my $UserObject = $Kernel::OM->Get('Kernel::System::User');

    # run ticket menu modules
    if ( ref $ConfigObject->Get('Ticket::Frontend::MenuModule') eq 'HASH' ) {
        my %Menus = %{ $ConfigObject->Get('Ticket::Frontend::MenuModule') };
        my %MenuClusters;
        my %ZoomMenuItems;

        MENU:
        for my $Menu ( sort keys %Menus ) {

            # load module
            if ( !$Kernel::OM->Get('Kernel::System::Main')->Require( $Menus{$Menu}->{Module} ) ) {
                return $LayoutObject->FatalError();
            }

            my $Object = $Menus{$Menu}->{Module}->new(
                %{$Self},
                TicketID => $Self->{TicketID},
            );

            # run module
            my $Item = $Object->Run(
                %Param,
                Ticket => \%Ticket,
                ACL    => \%AclAction,
                Config => $Menus{$Menu},
            );
            next MENU if !$Item;
            if ( $Menus{$Menu}->{PopupType} ) {
                $Item->{Class} = "AsPopup PopupType_$Menus{$Menu}->{PopupType}";
            }

            if ( $Menus{$Menu}->{Action} ) {
                $ActionLookup{ $Menus{$Menu}->{Action} } = {
                    Link           => $Item->{Link},
                    Class          => $Item->{Class},
                    LinkParam      => $Item->{LinkParam},
                    Description    => $Item->{Description},
                    Name           => $Item->{Name},
                    TranslatedName => $Kernel::OM->Get('Kernel::Language')->Translate( $Item->{Name} ),
                };
            }

            if ( !$Menus{$Menu}->{ClusterName} ) {

                $ZoomMenuItems{$Menu} = $Item;
            }
            else {

                # check the configured priority for this item. The lowest ClusterPriority
                # within the same cluster wins.
                my $Priority = $MenuClusters{ $Menus{$Menu}->{ClusterName} }->{Priority} || 0;
                $Menus{$Menu}->{ClusterPriority} ||= 0;
                if ( !$Priority || $Priority !~ /^\d{3}$/ || $Priority > $Menus{$Menu}->{ClusterPriority} ) {
                    $Priority = $Menus{$Menu}->{ClusterPriority};
                }
                $MenuClusters{ $Menus{$Menu}->{ClusterName} }->{Priority} = $Priority;
                $MenuClusters{ $Menus{$Menu}->{ClusterName} }->{Items}->{$Menu} = $Item;
            }
        }

        for my $Cluster ( sort keys %MenuClusters ) {
            $ZoomMenuItems{ $MenuClusters{$Cluster}->{Priority} . $Cluster } = {
                Name  => $Cluster,
                Type  => 'Cluster',
                Link  => '#',
                Class => 'ClusterLink',
                Items => $MenuClusters{$Cluster}->{Items},
            };
        }

        # display all items
        for my $Item ( sort keys %ZoomMenuItems ) {
            if ( $ZoomMenuItems{$Item}->{ExternalLink} && $ZoomMenuItems{$Item}->{ExternalLink} == 1 ) {
                $LayoutObject->Block(
                    Name => 'TicketMenuExternalLink',
                    Data => $ZoomMenuItems{$Item},
                );
            }
            else {
                $LayoutObject->Block(
                    Name => 'TicketMenu',
                    Data => $ZoomMenuItems{$Item},
                );
            }

            if ( $ZoomMenuItems{$Item}->{Type} eq 'Cluster' ) {

                $LayoutObject->Block(
                    Name => 'TicketMenuSubContainer',
                    Data => {
                        Name => $ZoomMenuItems{$Item}->{Name},
                    },
                );

                for my $SubItem ( sort keys %{ $ZoomMenuItems{$Item}->{Items} } ) {
                    $LayoutObject->Block(
                        Name => 'TicketMenuSubContainerItem',
                        Data => $ZoomMenuItems{$Item}->{Items}->{$SubItem},
                    );
                }
            }
        }
    }

    # get MoveQueuesStrg
    if ( $ConfigObject->Get('Ticket::Frontend::MoveType') =~ /^form$/i ) {
        $MoveQueues{0}         = '- ' . $LayoutObject->{LanguageObject}->Translate('Move') . ' -';
        $Param{MoveQueuesStrg} = $LayoutObject->AgentQueueListOption(
            Name           => 'DestQueueID',
            TreeView       => $ConfigObject->Get('Ticket::Frontend::ListType') eq 'tree' ? 1 : 0,
            Data           => \%MoveQueues,
            Class          => 'Modernize Small',
            CurrentQueueID => $Ticket{QueueID},
        );
    }
    my %AclActionLookup = reverse %AclAction;
    if (
        $ConfigObject->Get('Frontend::Module')->{AgentTicketMove}
        && ( $AclActionLookup{AgentTicketMove} )
        )
    {
        my $Access = $TicketObject->TicketPermission(
            Type     => 'move',
            TicketID => $Ticket{TicketID},
            UserID   => $Self->{UserID},
            LogNo    => 1,
        );
        $Param{TicketID} = $Ticket{TicketID};
        if ($Access) {
            if ( $ConfigObject->Get('Ticket::Frontend::MoveType') =~ /^form$/i ) {
                $LayoutObject->Block(
                    Name => 'MoveLink',
                    Data => { %Param, %AclAction },
                );
            }
            else {
                $LayoutObject->Block(
                    Name => 'MoveForm',
                    Data => { %Param, %AclAction },
                );
            }

            $ActionLookup{AgentTicketMove} = {
                Link           => 'Action=AgentTicketMove;TicketID=[% Data.TicketID | uri %]',
                Class          => 'AsPopup PopupType_TicketAction',
                LinkParam      => '',
                Description    => Translatable('Change Queue'),
                Name           => Translatable('Queue'),
                TranslatedName => $Kernel::OM->Get('Kernel::Language')->Translate('Queue'),
            };
        }
    }

    # Check if AgentTicketCompose and AgentTicketForward are allowed as action (for display of FormDrafts).
    my %ActionConfig = (
        AgentTicketCompose => {
            Link           => 'Action=AgentTicketCompose;TicketID=[% Data.TicketID | uri %]',
            Class          => 'AsPopup PopupType_TicketAction',
            LinkParam      => '',
            Description    => Translatable('Reply'),
            Name           => Translatable('Reply'),
            TranslatedName => $Kernel::OM->Get('Kernel::Language')->Translate('Reply'),
        },
        AgentTicketForward => {
            Link           => 'Action=AgentTicketForward;TicketID=[% Data.TicketID | uri %]',
            Class          => 'AsPopup PopupType_TicketAction',
            LinkParam      => '',
            Description    => Translatable('Forward article via mail'),
            Name           => Translatable('Forward'),
            TranslatedName => $Kernel::OM->Get('Kernel::Language')->Translate('Forward'),
        },
    );
    ACTION:
    for my $Action (qw(AgentTicketCompose AgentTicketForward)) {
        next ACTION if !$ConfigObject->Get('Frontend::Module')->{$Action};
        next ACTION if !$AclActionLookup{$Action};

        my $Config = $ConfigObject->Get( 'Ticket::Frontend::' . $Action );
        if ( $Config->{Permission} ) {
            next ACTION if !$TicketObject->TicketPermission(
                Type     => $Config->{Permission},
                TicketID => $Ticket{TicketID},
                UserID   => $Self->{UserID},
                LogNo    => 1,
            );
        }
        $ActionLookup{$Action} = $ActionConfig{$Action};
    }

    # Get and show available FormDrafts.
    my %ShownFormDraftEntries;
    my $FormDraftList = $Kernel::OM->Get('Kernel::System::FormDraft')->FormDraftListGet(
        ObjectType => 'Ticket',
        ObjectID   => $Self->{TicketID},
        UserID     => $Self->{UserID},
    );
    if ( IsArrayRefWithData($FormDraftList) ) {
        FormDraft:
        for my $FormDraft ( @{$FormDraftList} ) {
            next FormDraft if !$ActionLookup{ $FormDraft->{Action} };
            push @{ $ShownFormDraftEntries{ $FormDraft->{Action} } }, $FormDraft;
        }
    }
    if (%ShownFormDraftEntries) {

        my $LastArticle;
        if ( $Order eq 'DESC' ) {
            $LastArticle = $ArticleBoxAll[0];
        }
        else {
            $LastArticle = $ArticleBoxAll[-1];
        }

        my $LastArticleSystemTime;
        if ( $LastArticle->{CreateTime} ) {
            my $LastArticleSystemTimeObject = $Kernel::OM->Create(
                'Kernel::System::DateTime',
                ObjectParams => {
                    String => $LastArticle->{CreateTime},
                },
            );
            $LastArticleSystemTime = $LastArticleSystemTimeObject->ToEpoch();
        }

        my @FormDrafts;

        for my $Action (
            sort {
                $ActionLookup{$a}->{TranslatedName}
                    cmp
                    $ActionLookup{$b}->{TranslatedName}
            } keys %ShownFormDraftEntries
            )
        {
            my $ActionData = $ActionLookup{$Action};

            SHOWNFormDraftACTIONENTRY:
            for my $ShownFormDraftActionEntry (
                sort {
                    $a->{Title}
                        cmp
                        $b->{Title}
                        ||
                        $a->{FormDraftID}
                        <=>
                        $b->{FormDraftID}
                } @{ $ShownFormDraftEntries{$Action} }
                )
            {
                $ShownFormDraftActionEntry->{CreatedByUser} = $UserObject->UserName(
                    UserID => $ShownFormDraftActionEntry->{CreateBy},
                );
                $ShownFormDraftActionEntry->{ChangedByUser} = $UserObject->UserName(
                    UserID => $ShownFormDraftActionEntry->{ChangeBy},
                );

                $ShownFormDraftActionEntry = {
                    %{$ShownFormDraftActionEntry},
                    %{$ActionData},

                };

                push @FormDrafts, $ShownFormDraftActionEntry;
            }
        }

        $LayoutObject->Block(
            Name => 'FormDraftTable',
            Data => {
                FormDrafts => \@FormDrafts,
                TicketID   => $Self->{TicketID},
            },
        );
    }

    # show created by if different then User ID 1
    if ( $Ticket{CreateBy} > 1 ) {

        # get user object
        my $UserObject = $Kernel::OM->Get('Kernel::System::User');
        $Ticket{CreatedByUser} = $UserObject->UserName( UserID => $Ticket{CreateBy} );
        $LayoutObject->Block(
            Name => 'CreatedBy',
            Data => {%Ticket},
        );
    }

    # show no articles block if ticket does not contain articles
    if ( !@ArticleBox && !$Self->{ZoomTimeline} ) {
        $LayoutObject->Block(
            Name => 'HintNoArticles',
        );
    }

    # check if ticket is normal or process ticket
    my $IsProcessTicket = $TicketObject->TicketCheckForProcessType(
        'TicketID' => $Self->{TicketID}
    );

    # collect data for overview widget
    my %WidgetData;
    if ( $IsProcessTicket || $Self->{DisplaySettings}{DynamicFieldWidgetDisplay} ) {
        %WidgetData = $IsProcessTicket
            ? (
                WidgetDisplay            => $Self->{DisplaySettings}{ProcessDisplay},
                WidgetTitle              => $Self->{DisplaySettings}{ProcessDisplay}{WidgetTitle},
                WidgetDynamicFieldGroups => $Self->{DisplaySettings}{ProcessWidgetDynamicFieldGroups},
            )
            : (
                WidgetDisplay            => $Self->{DisplaySettings}{DynamicFieldWidgetDisplay},
                WidgetTitle              => $Self->{DisplaySettings}{DynamicFieldWidgetDisplay}{WidgetTitle},
                WidgetDynamicFieldGroups => $Self->{DisplaySettings}{DynamicFieldWidgetDynamicFieldGroups},

            );

        $WidgetData{WidgetDynamicField} = $ConfigObject->Get("Ticket::Frontend::AgentTicketZoom")
            ->{ ( $IsProcessTicket ? 'ProcessWidgetDynamicField' : 'DynamicFieldWidgetDynamicField' ) } // {};
    }

    # decide if widget should be shown
    my $ShowWidget = 0;

    # always show if we have a process ticket for activity dialogs
    if ($IsProcessTicket) {
        $ShowWidget = 1;
    }

    # else show only if dynamic fields are defined and at least one of them has a value
    elsif ( IsHashRefWithData( $WidgetData{WidgetDynamicField} ) ) {
        DFVALUE:
        for my $FieldName ( keys $WidgetData{WidgetDynamicField}->%* ) {
            next DFVALUE unless $Ticket{"DynamicField_$FieldName"};

            $ShowWidget = 1;

            last DFVALUE;
        }
    }

    # show overview widget with either dynamic field data or with process and activity dialog data
    if ($ShowWidget) {

        # send data to JS
        $LayoutObject->AddJSData(
            Key   => 'OverviewWidget',
            Value => 1,
        );

        # output the overview widget in the main screen
        $LayoutObject->Block(
            Name => 'OverviewWidget',
            Data => {
                WidgetTitle => $WidgetData{WidgetTitle},
            },
        );

        # collect and render process data if necessary
        my $ActivityName;
        my $NextActivityDialogs;
        my $ProcessEntityIDField;
        if ($IsProcessTicket) {

            # get the DF where the ProcessEntityID is stored
            $ProcessEntityIDField = 'DynamicField_'
                . $ConfigObject->Get("Process::DynamicFieldProcessManagementProcessID");

            # get the DF where the AtivityEntityID is stored
            my $ActivityEntityIDField = 'DynamicField_'
                . $ConfigObject->Get("Process::DynamicFieldProcessManagementActivityID");

            my $ActivityData = $Kernel::OM->Get('Kernel::System::ProcessManagement::Activity')->ActivityGet(
                Interface        => 'AgentInterface',
                ActivityEntityID => $Ticket{$ActivityEntityIDField},
            );

            # get next activity dialogs
            if ( $Ticket{$ActivityEntityIDField} ) {
                $NextActivityDialogs = ${ActivityData}->{ActivityDialog} || {};
            }
            $ActivityName = $ActivityData->{Name};
        }

        if ($NextActivityDialogs) {

            # get ActivityDialog object
            my $ActivityDialogObject = $Kernel::OM->Get('Kernel::System::ProcessManagement::ActivityDialog');

            # we have to check if the current user has the needed permissions to view the
            # different activity dialogs, so we loop over every activity dialog and check if there
            # is a permission configured. If there is a permission configured we check this
            # and display/hide the activity dialog link
            my %PermissionRights;
            my %PermissionActivityDialogList;
            ACTIVITYDIALOGPERMISSION:
            for my $Index ( sort { $a <=> $b } keys %{$NextActivityDialogs} ) {
                my $CurrentActivityDialogEntityID = $NextActivityDialogs->{$Index};
                my $CurrentActivityDialog         = $ActivityDialogObject->ActivityDialogGet(
                    Interface              => 'AgentInterface',
                    ActivityDialogEntityID => $CurrentActivityDialogEntityID
                );

                # create an interface lookup-list
                my %InterfaceLookup = map { $_ => 1 } @{ $CurrentActivityDialog->{Interface} };

                next ACTIVITYDIALOGPERMISSION if !$InterfaceLookup{AgentInterface};

                if ( $CurrentActivityDialog->{Permission} ) {

                    # performance-boost/cache
                    if ( !defined $PermissionRights{ $CurrentActivityDialog->{Permission} } ) {
                        $PermissionRights{ $CurrentActivityDialog->{Permission} } = $TicketObject->TicketPermission(
                            Type     => $CurrentActivityDialog->{Permission},
                            TicketID => $Ticket{TicketID},
                            UserID   => $Self->{UserID},
                        );
                    }

                    if ( !$PermissionRights{ $CurrentActivityDialog->{Permission} } ) {
                        next ACTIVITYDIALOGPERMISSION;
                    }
                }

                $PermissionActivityDialogList{$Index} = $CurrentActivityDialogEntityID;
            }

            # reduce next activity dialogs to the ones that have permissions
            $NextActivityDialogs = \%PermissionActivityDialogList;

            # get ACL restrictions
            my $ACL = $TicketObject->TicketAcl(
                Data          => \%PermissionActivityDialogList,
                TicketID      => $Ticket{TicketID},
                Action        => $Self->{Action},
                ReturnType    => 'ActivityDialog',
                ReturnSubType => '-',
                UserID        => $Self->{UserID},
            );

            if ($ACL) {
                %{$NextActivityDialogs} = $TicketObject->TicketAclData();
            }

            $LayoutObject->Block(
                Name => 'NextActivityDialogs',
                Data => {
                    'ActivityName' => $ActivityName,
                },
            );

            if ( IsHashRefWithData($NextActivityDialogs) ) {
                for my $NextActivityDialogKey ( sort { $a <=> $b } keys %{$NextActivityDialogs} ) {
                    my $ActivityDialogData = $ActivityDialogObject->ActivityDialogGet(
                        Interface              => 'AgentInterface',
                        ActivityDialogEntityID => $NextActivityDialogs->{$NextActivityDialogKey},
                    );

                    # check if direct submit is active for this activity dialog
                    my $DirectSubmit = $ActivityDialogData->{DirectSubmit};
                    if ( any { $ActivityDialogData->{Fields}{$_}{Display} } keys $ActivityDialogData->{Fields}->%* ) {
                        $DirectSubmit = 0;
                    }

                    if ($DirectSubmit) {
                        $LayoutObject->Block(
                            Name => 'ActivityDialogDirectSubmit',
                            Data => {
                                ActivityDialogEntityID
                                    => $NextActivityDialogs->{$NextActivityDialogKey},
                                Name            => $ActivityDialogData->{SubmitButtonText} || $ActivityDialogData->{Name},
                                ProcessEntityID => $Ticket{$ProcessEntityIDField},
                                TicketID        => $Ticket{TicketID},
                            },
                        );
                    }
                    else {
                        $LayoutObject->Block(
                            Name => 'ActivityDialog',
                            Data => {
                                ActivityDialogEntityID
                                    => $NextActivityDialogs->{$NextActivityDialogKey},
                                Name            => $ActivityDialogData->{Name},
                                ProcessEntityID => $Ticket{$ProcessEntityIDField},
                                TicketID        => $Ticket{TicketID},
                            },
                        );
                    }
                }
            }
            else {
                $LayoutObject->Block(
                    Name => 'NoActivityDialogs',
                    Data => {},
                );
            }
        }

        # get dynamic field config for frontend module
        my $DynamicFieldFilter = {
            %{ $ConfigObject->Get("Ticket::Frontend::AgentTicketZoom")->{DynamicField} || {} },
            %{
                $WidgetData{WidgetDynamicField} || {}
            },
        };

        # get the dynamic fields for ticket object
        my $DynamicField = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet(
            Valid       => 1,
            ObjectType  => ['Ticket'],
            FieldFilter => $DynamicFieldFilter || {},
        );
        my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');

        # to store dynamic fields to be displayed in the overview widget
        my (@FieldsWidget);

        # cycle trough the activated Dynamic Fields for ticket object
        DYNAMICFIELD:
        for my $DynamicFieldConfig ( @{$DynamicField} ) {
            next DYNAMICFIELD if !IsHashRefWithData($DynamicFieldConfig);
            next DYNAMICFIELD if !defined $Ticket{ 'DynamicField_' . $DynamicFieldConfig->{Name} };
            next DYNAMICFIELD if $Ticket{ 'DynamicField_' . $DynamicFieldConfig->{Name} } eq '';

            # use translation here to be able to reduce the character length in the template
            my $Label = $LayoutObject->{LanguageObject}->Translate( $DynamicFieldConfig->{Label} );

            if ( $WidgetData{WidgetDynamicField}{ $DynamicFieldConfig->{Name} } ) {
                my $ValueStrg = $DynamicFieldBackendObject->DisplayValueRender(
                    DynamicFieldConfig => $DynamicFieldConfig,
                    Value              => $Ticket{ 'DynamicField_' . $DynamicFieldConfig->{Name} },
                    LayoutObject       => $LayoutObject,

                    # no ValueMaxChars here, enough space available
                );

                push @FieldsWidget, {
                    $DynamicFieldConfig->{Name} => $ValueStrg->{Title},
                    Name                        => $DynamicFieldConfig->{Name},
                    Title                       => $ValueStrg->{Title},
                    Value                       => $ValueStrg->{Value},
                    ValueKey                    => $Ticket{ 'DynamicField_' . $DynamicFieldConfig->{Name} },
                    Label                       => $Label,
                    Link                        => $ValueStrg->{Link},
                    LinkPreview                 => $ValueStrg->{LinkPreview},

                    # Include unique parameter with dynamic field name in case of collision with others.
                    #   Please see bug#13362 for more information.
                    "DynamicField_$DynamicFieldConfig->{Name}" => $ValueStrg->{Title},
                };
            }
        }

        # build dynamic field lookup hash for widget groups
        my %DynamicFieldLookup = map { ( $_->{Name} => $_ ) } $DynamicField->@*;

        # output dynamic fields registered for a group in the overview widget
        my @FieldsInAGroup;
        for my $GroupName (
            sort keys %{ $WidgetData{WidgetDynamicFieldGroups} }
            )
        {

            $LayoutObject->Block(
                Name => 'OverviewWidgetDynamicFieldGroups',
            );

            my $GroupFieldsString = $WidgetData{WidgetDynamicFieldGroups}{$GroupName};

            $GroupFieldsString =~ s{\s}{}xmsg;
            my @GroupFields = split /,/, $GroupFieldsString;

            if ( $#GroupFields + 1 ) {

                my $ShowGroupTitle = 0;
                for my $Field (@FieldsWidget) {

                    if ( grep { $_ eq $Field->{Name} } @GroupFields ) {

                        $ShowGroupTitle = 1;
                        $LayoutObject->Block(
                            Name => 'OverviewWidgetDynamicField',
                            Data => {
                                Label => $Field->{Label},
                                Name  => $Field->{Name},
                            },
                        );

                        my $DFConfig = $DynamicFieldLookup{ $Field->{Name} };

                        # set field
                        if ( $DFConfig->{FieldType} eq 'Set' ) {
                            $LayoutObject->Block(
                                Name => 'SetField',
                            );

                            $LayoutObject->Block(
                                Name => 'DynamicFieldSetSeparator',
                                Data => {
                                    Label => $Field->{Label},
                                },
                            );

                            my @IncludedFields = $Self->_GetIncludedFieldOrdered(
                                Include => $DFConfig->{Config}{Include},
                            );

                            for my $IncludeField (@IncludedFields) {

                                my $IncludeDFConfig = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldGet(
                                    Name => $IncludeField,
                                );
                                my $ValueStrg = $DynamicFieldBackendObject->DisplayValueRender(
                                    DynamicFieldConfig => $IncludeDFConfig,
                                    Value              => $Ticket{ 'DynamicField_' . $IncludeDFConfig->{Name} },
                                    LayoutObject       => $LayoutObject,

                                    # no ValueMaxChars here, enough space available
                                );

                                my %IncludeField = (
                                    $IncludeDFConfig->{Name} => $ValueStrg->{Title},
                                    Name                     => $IncludeDFConfig->{Name},
                                    Title                    => $ValueStrg->{Title},
                                    Value                    => $ValueStrg->{Value},
                                    ValueKey                 => $Ticket{ 'DynamicField_' . $IncludeDFConfig->{Name} },
                                    Label                    => $IncludeDFConfig->{Label},
                                    Link                     => $ValueStrg->{Link},
                                    LinkPreview              => $ValueStrg->{LinkPreview},

                                    # Include unique parameter with dynamic field name in case of collision with others.
                                    #   Please see bug#13362 for more information.
                                    "DynamicField_$IncludeDFConfig->{Name}" => $ValueStrg->{Title},
                                );
                                $LayoutObject->Block(
                                    Name => 'SetDynamicField',
                                    Data => {
                                        Name  => $IncludeField{Name},
                                        Label => $IncludeField{Label},
                                    }
                                );
                                $LayoutObject->Block(
                                    Name => 'SetDynamicFieldValueOverlayTrigger',
                                );

                                if ( $IncludeDFConfig->{Link} ) {
                                    $LayoutObject->Block(
                                        Name => 'SetDynamicFieldLink',
                                        Data => {
                                            $IncludeField{Name} => $IncludeField{Title},
                                            %Ticket,

                                            # alias for ticket title, Title will be overwritten
                                            TicketTitle => $Ticket{Title},
                                            Value       => $IncludeField{Value},
                                            Title       => $IncludeField{Title},
                                            Link        => $IncludeField{Link},
                                            LinkPreview => $IncludeField{LinkPreview},

                                            # Include unique parameter with dynamic field name in case of collision with others.
                                            #   Please see bug#13362 for more information.
                                            "DynamicField_$IncludeField{Name}" => $IncludeField{Title},
                                        },
                                    );
                                }
                                else {
                                    $LayoutObject->Block(
                                        Name => 'SetDynamicFieldPlain',
                                        Data => {
                                            Value => $IncludeField{Value},
                                            Title => $IncludeField{Title},
                                        },
                                    );
                                }
                                push @FieldsInAGroup, $Field->{Name};
                            }
                        }

                        # standard field
                        else {
                            $LayoutObject->Block(
                                Name => 'StandardField',
                                Data => {
                                    Name  => $Field->{Name},
                                    Label => $Field->{Label},
                                },
                            );

                            $LayoutObject->Block(
                                Name => 'OverviewWidgetDynamicFieldValueOverlayTrigger',
                            );

                            if ( $Field->{Link} ) {
                                $LayoutObject->Block(
                                    Name => 'OverviewWidgetDynamicFieldLink',
                                    Data => {
                                        $Field->{Name} => $Field->{Title},
                                        %Ticket,

                                        # alias for ticket title, Title will be overwritten
                                        TicketTitle => $Ticket{Title},
                                        Value       => $Field->{Value},
                                        Title       => $Field->{Title},
                                        Link        => $Field->{Link},
                                        LinkPreview => $Field->{LinkPreview},

                                        # Include unique parameter with dynamic field name in case of collision with others.
                                        #   Please see bug#13362 for more information.
                                        "DynamicField_$Field->{Name}" => $Field->{Title},
                                    },
                                );
                            }
                            else {
                                $LayoutObject->Block(
                                    Name => 'OverviewWidgetDynamicFieldPlain',
                                    Data => {
                                        Value => $Field->{Value},
                                        Title => $Field->{Title},
                                    },
                                );
                            }
                            push @FieldsInAGroup, $Field->{Name};

                        }

                    }
                }

                if ($ShowGroupTitle) {
                    $LayoutObject->Block(
                        Name => 'OverviewWidgetDynamicFieldGroupSeparator',
                        Data => {
                            Name => $GroupName,
                        },
                    );
                }
            }
        }

        # output dynamic fields not registered in a group in the overview widget
        my @RemainingFieldsWidget;
        for my $Field (@FieldsWidget) {

            if ( !grep { $_ eq $Field->{Name} } @FieldsInAGroup ) {
                push @RemainingFieldsWidget, $Field;
            }
        }

        $LayoutObject->Block(
            Name => 'OverviewWidgetDynamicFieldGroups',
        );

        if ( $#RemainingFieldsWidget + 1 ) {

            $LayoutObject->Block(
                Name => 'OverviewWidgetDynamicFieldGroupSeparator',
                Data => {
                    Name =>
                        $LayoutObject->{LanguageObject}->Translate('Fields with no group'),
                },
            );
        }
        for my $Field (@RemainingFieldsWidget) {

            $LayoutObject->Block(
                Name => 'OverviewWidgetDynamicField',
            );

            my ($DFConfig) = grep { $_->{Name} eq $Field->{Name} } $DynamicField->@*;

            # set field
            if ( $DFConfig->{FieldType} eq 'Set' ) {
                $LayoutObject->Block(
                    Name => 'SetField',
                );

                $LayoutObject->Block(
                    Name => 'DynamicFieldSetSeparator',
                    Data => {
                        Label => $Field->{Label},
                    },
                );

                my @IncludedFields = $Self->_GetIncludedFieldOrdered(
                    Include => $DFConfig->{Config}{Include},
                );

                for my $ValueIndex ( 0 .. $#{ $Ticket{ 'DynamicField_' . $Field->{Name} } } ) {
                    my $ValueItem = $Ticket{ 'DynamicField_' . $Field->{Name} }[$ValueIndex];

                    for my $IncludeField (@IncludedFields) {

                        my $IncludeDFConfig = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldGet(
                            Name => $IncludeField,
                        );
                        my $ValueStrg = $DynamicFieldBackendObject->DisplayValueRender(
                            DynamicFieldConfig => $IncludeDFConfig,
                            Value              => $ValueItem->{ $IncludeDFConfig->{Name} },
                            LayoutObject       => $LayoutObject,

                            # no ValueMaxChars here, enough space available
                        );

                        my %IncludeField = (
                            $IncludeDFConfig->{Name} => $ValueStrg->{Title},
                            Name                     => $IncludeDFConfig->{Name},
                            Title                    => $ValueStrg->{Title},
                            Value                    => $ValueStrg->{Value},
                            ValueKey                 => $ValueItem->{ $IncludeDFConfig->{Name} },
                            Label                    => $IncludeDFConfig->{Label},
                            Link                     => $ValueStrg->{Link},
                            LinkPreview              => $ValueStrg->{LinkPreview},

                            # Include unique parameter with dynamic field name in case of collision with others.
                            #   Please see bug#13362 for more information.
                            "DynamicField_$IncludeDFConfig->{Name}" => $ValueStrg->{Title},
                        );
                        $LayoutObject->Block(
                            Name => 'SetDynamicField',
                            Data => {
                                Name  => $IncludeField{Name},
                                Label => $IncludeField{Label},
                            }
                        );
                        $LayoutObject->Block(
                            Name => 'SetDynamicFieldValueOverlayTrigger',
                        );

                        if ( $IncludeDFConfig->{Link} ) {
                            $LayoutObject->Block(
                                Name => 'SetDynamicFieldLink',
                                Data => {
                                    $IncludeField{Name} => $IncludeField{Title},
                                    %Ticket,

                                    # alias for ticket title, Title will be overwritten
                                    TicketTitle => $Ticket{Title},
                                    Value       => $IncludeField{Value},
                                    Title       => $IncludeField{Title},
                                    Link        => $IncludeField{Link},
                                    LinkPreview => $IncludeField{LinkPreview},

                                    # Include unique parameter with dynamic field name in case of collision with others.
                                    #   Please see bug#13362 for more information.
                                    "DynamicField_$IncludeField{Name}" => $IncludeField{Title},
                                },
                            );
                        }
                        else {
                            $LayoutObject->Block(
                                Name => 'SetDynamicFieldPlain',
                                Data => {
                                    Value => $IncludeField{Value},
                                    Title => $IncludeField{Title},
                                },
                            );
                        }
                        push @FieldsInAGroup, $Field->{Name};
                    }
                    if ( $ValueIndex != $#{ $Ticket{ 'DynamicField_' . $Field->{Name} } } ) {
                        $LayoutObject->Block(
                            Name => 'DynamicFieldSetValueSeparator',
                        );
                    }
                }
            }

            # standard field
            else {
                $LayoutObject->Block(
                    Name => 'StandardField',
                    Data => {
                        Name  => $Field->{Name},
                        Label => $Field->{Label},
                    },
                );

                $LayoutObject->Block(
                    Name => 'OverviewWidgetDynamicFieldValueOverlayTrigger',
                );

                if ( $Field->{Link} ) {
                    $LayoutObject->Block(
                        Name => 'OverviewWidgetDynamicFieldLink',
                        Data => {
                            $Field->{Name} => $Field->{Title},
                            %Ticket,

                            # alias for ticket title, Title will be overwritten
                            TicketTitle => $Ticket{Title},
                            Value       => $Field->{Value},
                            Title       => $Field->{Title},
                            Link        => $Field->{Link},
                            LinkPreview => $Field->{LinkPreview},

                            # Include unique parameter with dynamic field name in case of collision with others.
                            #   Please see bug#13362 for more information.
                            "DynamicField_$Field->{Name}" => $Field->{Title},
                        },
                    );
                }
                else {
                    $LayoutObject->Block(
                        Name => 'OverviewWidgetDynamicFieldPlain',
                        Data => {
                            Value => $Field->{Value},
                            Title => $Field->{Title},
                        },
                    );
                }
                push @FieldsInAGroup, $Field->{Name};

            }

        }
    }

    # article filter is activated in sysconfig
    if ( $Self->{ArticleFilterActive} ) {

        if ( $Self->{ZoomTimeline} ) {

            # build event type list for filter dialog
            $Param{EventTypeFilterString} = $LayoutObject->BuildSelection(
                Data        => $Self->{HistoryTypeMapping},
                SelectedID  => $Self->{EventTypeFilter}->{EventTypeID},
                Translation => 1,
                Multiple    => 1,
                Sort        => 'AlphanumericValue',
                Name        => 'EventTypeFilter',
                Class       => 'Modernize',
            );

            # send data to JS
            $LayoutObject->AddJSData(
                Key   => 'ArticleFilterDialog',
                Value => 0,
            );

            $LayoutObject->Block(
                Name => 'EventTypeFilterDialog',
                Data => {%Param},
            );
        }
        else {

            my @CommunicationChannels = $Kernel::OM->Get('Kernel::System::CommunicationChannel')->ChannelList(
                ValidID => 1,
            );

            my %Channels = map { $_->{ChannelID} => $_->{DisplayName} } @CommunicationChannels;

            # build article type list for filter dialog
            $Param{Channels} = $LayoutObject->BuildSelection(
                Data        => \%Channels,
                SelectedID  => $Self->{ArticleFilter}->{CommunicationChannelID},
                Translation => 1,
                Multiple    => 1,
                Sort        => 'AlphanumericValue',
                Name        => 'CommunicationChannelFilter',
                Class       => 'Modernize',
            );

            $Param{CustomerVisibility} = $LayoutObject->BuildSelection(
                Data => {
                    0 => Translatable('Invisible only'),
                    1 => Translatable('Visible only'),
                    2 => Translatable('Visible and invisible'),
                },
                SelectedID  => $Self->{ArticleFilter}->{CustomerVisibility} // 2,
                Translation => 1,
                Sort        => 'NumericKey',
                Name        => 'CustomerVisibilityFilter',
                Class       => 'Modernize',
            );

            # get sender types
            my %ArticleSenderTypes = $ArticleObject->ArticleSenderTypeList();

            # build article sender type list for filter dialog
            $Param{ArticleSenderTypeFilterString} = $LayoutObject->BuildSelection(
                Data        => \%ArticleSenderTypes,
                SelectedID  => $Self->{ArticleFilter}->{ArticleSenderTypeID},
                Translation => 1,
                Multiple    => 1,
                Sort        => 'AlphanumericValue',
                Name        => 'ArticleSenderTypeFilter',
                Class       => 'Modernize',
            );

            # Ticket ID
            $Param{TicketID} = $Self->{TicketID};

            # send data to JS
            $LayoutObject->AddJSData(
                Key   => 'ArticleFilterDialog',
                Value => 1,
            );

            $LayoutObject->Block(
                Name => 'ArticleFilterDialog',
                Data => {%Param},
            );
        }
    }

    # check if ticket need to be marked as seen
    my $ArticleAllSeen = 1;
    ARTICLE:
    for my $Article (@ArticleBox) {

        # ignore system sender type
        next ARTICLE
            if $ConfigObject->Get('Ticket::NewArticleIgnoreSystemSender')
            && $ArticleSenderTypeList{ $Article->{SenderTypeID} } eq 'system';

        # last ARTICLE if article was not shown
        if ( !$ArticleFlags{ $Article->{ArticleID} }->{Seen} ) {
            $ArticleAllSeen = 0;
            last ARTICLE;
        }
    }

    # mark ticket as seen if all article are shown
    if ($ArticleAllSeen) {
        $TicketObject->TicketFlagSet(
            TicketID => $Self->{TicketID},
            Key      => 'Seen',
            Value    => 1,
            UserID   => $Self->{UserID},
        );
    }

    # send data to JS
    $LayoutObject->AddJSData(
        Key   => 'ArticleTableHeight',
        Value => $LayoutObject->{UserTicketZoomArticleTableHeight},
    );
    $LayoutObject->AddJSData(
        Key   => 'Ticket::Frontend::HTMLArticleHeightDefault',
        Value => $ConfigObject->Get('Ticket::Frontend::HTMLArticleHeightDefault'),
    );
    $LayoutObject->AddJSData(
        Key   => 'Ticket::Frontend::HTMLArticleHeightMax',
        Value => $ConfigObject->Get('Ticket::Frontend::HTMLArticleHeightMax'),
    );
    $LayoutObject->AddJSData(
        Key   => 'Language',
        Value => {
            AttachmentViewMessage => Translatable(
                'Article could not be opened! Perhaps it is on another article page?'
            ),
        },
    );

    # init js
    $LayoutObject->Block(
        Name => 'TicketZoomInit',
    );

    # return output
    return $LayoutObject->Output(
        TemplateFile => 'AgentTicketZoom',
        Data         => { %Param, %Ticket, %AclAction },
    );
}

sub _ArticleTree {
    my ( $Self, %Param ) = @_;

    my %Ticket          = %{ $Param{Ticket} };
    my %ArticleFlags    = %{ $Param{ArticleFlags} };
    my @ArticleBox      = @{ $Param{ArticleBox} };
    my $ArticleMaxLimit = $Param{ArticleMaxLimit};
    my $ArticleID       = $Param{ArticleID};
    my $TableClasses;

    # get layout object
    my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');

    # build thread string
    $LayoutObject->Block(
        Name => 'Tree',
        Data => {
            %Param,
            TableClasses => $TableClasses,
            ZoomTimeline => $Self->{ZoomTimeline},
        },
    );

    if ( $Param{Pagination} && !$Self->{ZoomTimeline} ) {
        $LayoutObject->Block(
            Name => 'ArticlePages',
            Data => $Param{Pagination},
        );
    }

    my @ArticleViews = (
        {
            Key   => 'Collapse',
            Value => Translatable('Show one article'),
        },
        {
            Key   => 'Expand',
            Value => Translatable('Show all articles'),
        },
    );

    # Add timeline view option only if enabled.
    if ( $Kernel::OM->Get('Kernel::Config')->Get('TimelineViewEnabled') ) {
        push @ArticleViews, {
            Key   => 'Timeline',
            Value => Translatable('Show Ticket Timeline View'),
        };
    }

    my $ArticleViewSelected = 'Collapse';
    if ( $Self->{ZoomExpand} ) {
        $ArticleViewSelected = 'Expand';
    }
    elsif ( $Self->{ZoomTimeline} ) {
        $ArticleViewSelected = 'Timeline';
    }

    my $ArticleViewStrg = $LayoutObject->BuildSelection(
        Data        => \@ArticleViews,
        SelectedID  => $ArticleViewSelected,
        Translation => 1,
        Sort        => 'AlphanumericValue',
        Name        => 'ArticleView',
        Class       => 'Modernize',
    );

    # Send data to JS.
    $LayoutObject->AddJSData(
        Key   => 'ArticleViewStrg',
        Value => $ArticleViewStrg,
    );
    $LayoutObject->AddJSData(
        Key   => 'ZoomExpand',
        Value => $Self->{ZoomExpand},
    );

    # article filter is activated in sysconfig
    if ( $Self->{ArticleFilterActive} ) {

        # define highlight style for links if filter is active
        my $HighlightStyle = 'menu';
        if ( $Self->{ArticleFilter} ) {
            $HighlightStyle = 'PriorityID-5';
        }

        # build article filter links
        $LayoutObject->Block(
            Name => 'ArticleFilterDialogLink',
            Data => {
                %Param,
                HighlightStyle => $HighlightStyle,
            },
        );

        # build article filter reset link only if filter is set
        if (
            ( !$Self->{ZoomTimeline} && IsHashRefWithData( $Self->{ArticleFilter} ) )
            || ( $Self->{ZoomTimeline} && $Self->{EventTypeFilter} )
            )
        {
            $LayoutObject->Block(
                Name => 'ArticleFilterResetLink',
                Data => {%Param},
            );
        }
    }

    # get needed objects
    my $TicketObject  = $Kernel::OM->Get('Kernel::System::Ticket');
    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
    my $ConfigObject  = $Kernel::OM->Get('Kernel::Config');

    # Create a list of article sender types for lookup
    my %ArticleSenderTypeList = $ArticleObject->ArticleSenderTypeList();

    # show article tree
    if ( !$Self->{ZoomTimeline} ) {

        $LayoutObject->Block(
            Name => 'ArticleList',
            Data => {
                %Param,
                ZoomExpandSortOrder => $Self->{ZoomExpandSort} eq 'reverse' ? 'Descending' : 'Ascending',
                TableClasses        => $TableClasses,
            },
        );

        ARTICLE:
        for my $ArticleTmp (@ArticleBox) {
            my %Article = %$ArticleTmp;

            # article filter is activated in sysconfig and there are articles
            # that passed the filter
            if ( $Self->{ArticleFilterActive} ) {
                if ( $Self->{ArticleFilter} && $Self->{ArticleFilter}->{ShownArticleIDs} ) {

                    # do not show article in tree if it does not match the filter
                    if ( !$Self->{ArticleFilter}->{ShownArticleIDs}->{ $Article{ArticleID} } ) {
                        next ARTICLE;
                    }
                }
            }

            # show article flags
            my $Class      = '';
            my $ClassRow   = '';
            my $NewArticle = 0;

            # ignore system sender types
            if (
                !$ArticleFlags{ $Article{ArticleID} }->{Seen}
                && (
                    !$ConfigObject->Get('Ticket::NewArticleIgnoreSystemSender')
                    || $ConfigObject->Get('Ticket::NewArticleIgnoreSystemSender')
                    && $ArticleSenderTypeList{ $Article{SenderTypeID} } ne 'system'
                )
                )
            {
                $NewArticle = 1;

                # show ticket flags

                # always show archived tickets as seen
                if ( $Ticket{ArchiveFlag} ne 'y' ) {
                    $Class    .= ' UnreadArticles';
                    $ClassRow .= ' UnreadArticles';
                }

                # just show ticket flags if agent belongs to the ticket
                my $ShowMeta;
                if (
                    $Self->{UserID} == $Ticket{OwnerID}
                    || $Self->{UserID} == $Ticket{ResponsibleID}
                    )
                {
                    $ShowMeta = 1;
                }
                if ( !$ShowMeta && $ConfigObject->Get('Ticket::Watcher') ) {
                    my %Watch = $TicketObject->TicketWatchGet(
                        TicketID => $Article{TicketID},
                    );
                    if ( $Watch{ $Self->{UserID} } ) {
                        $ShowMeta = 1;
                    }
                }

                # show ticket flags
                if ($ShowMeta) {
                    $Class .= ' Remarkable';
                }
                else {
                    $Class .= ' Ordinary';
                }
            }

            # if this is the shown article -=> set class to active
            if ( $ArticleID eq $Article{ArticleID} && !$Self->{ZoomExpand} ) {
                $ClassRow .= ' Active';
            }

            my $TmpSubject = $TicketObject->TicketSubjectClean(

                TicketNumber => $Ticket{TicketNumber},
                Subject      => $Article{Subject} || '',
            );

            my %ArticleFields = $LayoutObject->ArticleFields(
                %Article,
                ShowDeletedArticles => $Self->{ShowDeletedArticles}
            );

            # Get transmission status information for email articles.
            my $TransmissionStatus;
            if ( $Article{ChannelName} && $Article{ChannelName} eq 'Email' ) {
                $TransmissionStatus = $ArticleObject->BackendForArticle(%Article)->ArticleTransmissionStatus(
                    ArticleID           => $Article{ArticleID},
                    ShowDeletedArticles => $Self->{ShowDeletedArticles}
                );
            }

            # check if we need to show also expand/collapse icon
            $LayoutObject->Block(
                Name => 'TreeItem',
                Data => {
                    %Article,
                    ArticleFields      => \%ArticleFields,
                    Class              => $Class,
                    ClassRow           => $ClassRow,
                    Subject            => $TmpSubject,
                    TransmissionStatus => $TransmissionStatus,
                    ZoomExpand         => $Self->{ZoomExpand},
                    ZoomExpandSort     => $Self->{ZoomExpandSort},
                },
            );

            # get article flags
            # Always use user id 1 because other users also have to see the important flag
            my %ArticleImportantFlags = $ArticleObject->ArticleFlagGet(
                ArticleID => $Article{ArticleID},
                UserID    => 1,
            );

            # show important flag
            if ( $ArticleImportantFlags{Important} ) {
                $LayoutObject->Block(
                    Name => 'TreeItemImportantArticle',
                    Data => {},
                );
            }

            # always show archived tickets as seen
            if ( $NewArticle && $Ticket{ArchiveFlag} ne 'y' ) {
                $LayoutObject->Block(
                    Name => 'TreeItemNewArticle',
                    Data => {
                        %Article,
                        Class => $Class,
                    },
                );
            }

            # Bugfix for IE7: a table cell should not be empty
            # (because otherwise the cell borders are not shown):
            # we add an empty element here
            else {
                $LayoutObject->Block(
                    Name => 'TreeItemNoNewArticle',
                    Data => {},
                );
            }

            # Determine communication direction.
            if ( $Article{ChannelName} eq 'Internal' ) {
                $LayoutObject->Block( Name => 'TreeItemDirectionInternal' );
            }
            elsif ( $ArticleSenderTypeList{ $Article{SenderTypeID} } eq 'customer' ) {
                $LayoutObject->Block( Name => 'TreeItemDirectionIncoming' );
            }
            else {
                $LayoutObject->Block( Name => 'TreeItemDirectionOutgoing' );
            }

            # Get attachment index (excluding body attachments).
            my %AtmIndex;

            if ( !$Article{ArticleDeleted} || $Self->{ArticleStorage} =~ m/ArticleStorageFS/ ) {
                %AtmIndex = $ArticleObject->BackendForArticle(%Article)->ArticleAttachmentIndex(
                    ArticleID           => $Article{ArticleID},
                    ShowDeletedArticles => $Self->{ShowDeletedArticles},
                    %{ $Self->{ExcludeAttachments} },
                );
            }
            else {
                %AtmIndex = $ArticleObject->BackendForArticle(%Article)->ArticleAttachmentIndex(
                    ArticleID           => $Article{DeletedVersionID},
                    SourceArticleID     => $Article{ArticleID},
                    ShowDeletedArticles => $Self->{ShowDeletedArticles},
                    VersionView         => 1,
                    %{ $Self->{ExcludeAttachments} }
                );
            }
            $Article{Atms} = \%AtmIndex;

            # show attachment info
            # Bugfix for IE7: a table cell should not be empty
            # (because otherwise the cell borders are not shown):
            # we add an empty element here
            if ( !$Article{Atms} || !%{ $Article{Atms} } ) {
                $LayoutObject->Block(
                    Name => 'TreeItemNoAttachment',
                    Data => {},
                );

                next ARTICLE;
            }
            else {

                my $Attachments = $Self->_CollectArticleAttachments(
                    Article => \%Article,
                );

                $LayoutObject->Block(
                    Name => 'TreeItemAttachment',
                    Data => {
                        TicketID    => $Article{TicketID},
                        ArticleID   => $Article{ArticleID},
                        Attachments => $Attachments,
                    },
                );
            }
        }
    }

    # show timeline view
    else {

        # get ticket history
        my @HistoryLines = $TicketObject->HistoryGet(
            TicketID => $Self->{TicketID},
            UserID   => $Self->{UserID},
        );

        # get articles for later use
        my @TimelineArticleBox = $ArticleObject->ArticleList(
            TicketID            => $Self->{TicketID},
            ShowDeletedArticles => $Self->{ShowDeletedArticles}
        );

        for my $ArticleItem (@TimelineArticleBox) {
            my $ArticleBackendObject = $ArticleObject->BackendForArticle(
                %{$ArticleItem},
                ShowDeletedArticles => $Self->{ShowDeletedArticles}
            );

            my %Article = $ArticleBackendObject->ArticleGet(
                TicketID      => $Self->{TicketID},
                ArticleID     => $ArticleItem->{ArticleID},
                DynamicFields => 1,
                RealNames     => 1,
            );

            # Append article meta data.
            $ArticleItem = {
                %{$ArticleItem},
                %Article,
            };
        }

        my $ArticlesByArticleID = {};
        for my $Article ( sort @TimelineArticleBox ) {
            my $ArticleBackendObject = $ArticleObject->BackendForArticle(
                %{$Article},
                ShowDeletedArticles => $Self->{ShowDeletedArticles}
            );

            # Get attachment index (excluding body attachments).
            my %AtmIndex = $ArticleBackendObject->ArticleAttachmentIndex(
                ArticleID => $Article->{ArticleID},
                %{ $Self->{ExcludeAttachments} },
            );
            $Article->{Atms}                                = \%AtmIndex;
            $Article->{Backend}                             = $ArticleBackendObject->ChannelNameGet();
            $ArticlesByArticleID->{ $Article->{ArticleID} } = $Article;

            # Check if there is HTML body attachment.
            my %AttachmentIndexHTMLBody = $ArticleBackendObject->ArticleAttachmentIndex(
                ArticleID    => $Article->{ArticleID},
                OnlyHTMLBody => 1,
            );
            ( $Article->{HTMLBodyAttachmentID} ) = sort keys %AttachmentIndexHTMLBody;
        }

        # do not display these types
        my @TypesDodge = qw(
            Misc
            ArchiveFlagUpdate
            LoopProtection
            Remove
            Subscribe
            Unsubscribe
            SystemRequest
            SendAgentNotification
            SendCustomerNotification
            SendAutoReject
        );

        # sort out non-filtered event types (if applicable)
        if (
            $Self->{EventTypeFilter}->{EventTypeID}
            && IsArrayRefWithData( $Self->{EventTypeFilter}->{EventTypeID} )
            )
        {
            for my $EventType ( sort keys %{ $Self->{HistoryTypeMapping} } ) {
                if (
                    $EventType ne 'NewTicket' && !grep { $_ eq $EventType }
                    @{ $Self->{EventTypeFilter}->{EventTypeID} }
                    )
                {
                    push @TypesDodge, $EventType;
                }
            }
        }

        # types which can be described as 'action on a ticket'
        my @TypesTicketAction = qw(
            ServiceUpdate
            SLAUpdate
            StateUpdate
            SetPendingTime
            Unlock
            Lock
            ResponsibleUpdate
            OwnerUpdate
            CustomerUpdate
            NewTicket
            TicketLinkAdd
            TicketLinkDelete
            TicketDynamicFieldUpdate
            Move
            Merged
            PriorityUpdate
            TitleUpdate
            TypeUpdate
            EscalationResponseTimeNotifyBefore
            EscalationResponseTimeStart
            EscalationResponseTimeStop
            EscalationSolutionTimeNotifyBefore
            EscalationSolutionTimeStart
            EscalationSolutionTimeStop
            EscalationUpdateTimeNotifyBefore
            EscalationUpdateTimeStart
            EscalationUpdateTimeStop
            TimeAccounting
        );

        # types which are usually being connected to some kind of
        # automatic process (e.g. triggered by another action)
        my @TypesTicketAutoAction = qw(
            SendAutoFollowUp
            SendAutoReject
            SendAutoReply
        );

        # types which can be considered as internal
        my @TypesInternal = qw(
            AddNote
            ChatInternal
            EmailAgentInternal
        );

        # outgoing types
        my @TypesOutgoing = qw(
            AddSMS
            Forward
            EmailAgent
            PhoneCallAgent
            Bounce
            SendAnswer
        );

        # incoming types
        my @TypesIncoming = qw(
            EmailCustomer
            AddNoteCustomer
            AddSMSCustomer
            PhoneCallCustomer
            FollowUp
            WebRequestCustomer
            ChatExternal
        );

        my @TypesLeft = (
            @TypesOutgoing,
            @TypesInternal,
            @TypesTicketAutoAction,
        );

        my @TypesRight = (
            @TypesIncoming,
            @TypesTicketAction,
        );

        my @TypesWithArticles = (
            @TypesOutgoing,
            @TypesInternal,
            @TypesIncoming,
            'PhoneCallCustomer',
        );

        my %HistoryItems;
        my $ItemCounter = 0;
        my $LastCreateTime;
        my $LastCreateSystemTime;

        # Get mapping of history types to readable strings
        my %HistoryTypes;
        my %HistoryTypeConfig = %{ $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Frontend::HistoryTypes') // {} };
        for my $Entry ( sort keys %HistoryTypeConfig ) {
            %HistoryTypes = (
                %HistoryTypes,
                %{ $HistoryTypeConfig{$Entry} },
            );
        }

        HISTORYITEM:
        for my $Item ( reverse @HistoryLines ) {

            # special treatment for certain types, e.g. external notes from customers
            if (
                $Item->{ArticleID}
                && $Item->{HistoryType} eq 'AddNote'
                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
                && $ArticleSenderTypeList{ $ArticlesByArticleID->{ $Item->{ArticleID} }->{SenderTypeID} } eq 'customer'
                )
            {
                $Item->{Class} = 'TypeIncoming';

                # We fake a custom history type because external notes from customers still
                # have the history type 'AddNote' which does not allow for distinguishing.
                $Item->{HistoryType} = 'AddNoteCustomer';
            }

            # special treatment for certain types, e.g. external SMS from customers
            elsif (
                $Item->{ArticleID}
                && $Item->{HistoryType} eq 'AddSMS'
                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
                && $ArticleSenderTypeList{ $ArticlesByArticleID->{ $Item->{ArticleID} }->{SenderTypeID} } eq 'customer'
                )
            {
                $Item->{Class} = 'TypeIncoming';

                # We fake a custom history type because external notes from customers still
                # have the history type 'AddSMS' which does not allow for distinguishing.
                $Item->{HistoryType} = 'AddSMSCustomer';
            }

            # special treatment for internal emails
            elsif (
                $Item->{ArticleID}
                && $Item->{HistoryType} eq 'EmailAgent'
                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
                && $ArticlesByArticleID->{ $Item->{ArticleID} }->{Backend} eq 'Email'
                && $ArticleSenderTypeList{ $ArticlesByArticleID->{ $Item->{ArticleID} }->{SenderTypeID} } eq 'agent'
                && !$ArticlesByArticleID->{ $Item->{ArticleID} }->{IsVisibleForCustomer}
                )
            {
                $Item->{Class}       = 'TypeNoteInternal';
                $Item->{HistoryType} = 'EmailAgentInternal';
            }

            # special treatment for certain types, e.g. external notes from customers
            elsif (
                $Item->{ArticleID}
                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
                && $ArticlesByArticleID->{ $Item->{ArticleID} }->{Backend} eq 'Chat'
                && $ArticlesByArticleID->{ $Item->{ArticleID} }->{IsVisibleForCustomer}
                )
            {
                $Item->{HistoryType} = 'ChatExternal';
                $Item->{Class}       = 'TypeIncoming';
            }
            elsif (
                $Item->{ArticleID}
                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
                && $ArticlesByArticleID->{ $Item->{ArticleID} }->{Backend} eq 'Chat'
                && !$ArticlesByArticleID->{ $Item->{ArticleID} }->{IsVisibleForCustomer}
                )
            {
                $Item->{HistoryType} = 'ChatInternal';
                $Item->{Class}       = 'TypeInternal';
            }
            elsif (
                $Item->{HistoryType} eq 'Forward'
                && $Item->{ArticleID}
                && IsHashRefWithData( $ArticlesByArticleID->{ $Item->{ArticleID} } )
                && $ArticlesByArticleID->{ $Item->{ArticleID} }->{Backend} eq 'Email'
                && $ArticleSenderTypeList{ $ArticlesByArticleID->{ $Item->{ArticleID} }->{SenderTypeID} } eq 'agent'
                && !$ArticlesByArticleID->{ $Item->{ArticleID} }->{IsVisibleForCustomer}
                )
            {

                $Item->{Class} = 'TypeNoteInternal';
            }
            elsif ( grep { $_ eq $Item->{HistoryType} } @TypesTicketAction ) {
                $Item->{Class} = 'TypeTicketAction';
            }
            elsif ( grep { $_ eq $Item->{HistoryType} } @TypesTicketAutoAction ) {
                $Item->{Class} = 'TypeTicketAutoAction';
            }
            elsif ( grep { $_ eq $Item->{HistoryType} } @TypesInternal ) {
                $Item->{Class} = 'TypeNoteInternal';
            }
            elsif ( grep { $_ eq $Item->{HistoryType} } @TypesIncoming ) {
                $Item->{Class} = 'TypeIncoming';
            }
            elsif ( grep { $_ eq $Item->{HistoryType} } @TypesOutgoing ) {
                $Item->{Class} = 'TypeOutgoing';
            }

            if ( grep { $_ eq $Item->{HistoryType} } @TypesDodge ) {
                next HISTORYITEM;
            }

            $Item->{Counter} = $ItemCounter++;

            if ( $Item->{HistoryType} eq 'NewTicket' ) {

                # if the 'NewTicket' item has an article, display this "creation article" event separately
                if ( $Item->{ArticleID} ) {
                    push @{ $Param{Items} }, {
                        %{$Item},
                        Counter             => $Item->{Counter}++,
                        Class               => 'NewTicket',
                        Name                => '',
                        ArticleID           => '',
                        HistoryTypeReadable => Translatable('Ticket Created'),
                        Orientation         => 'Right',
                    };
                }
                else {
                    $Item->{Class} = 'NewTicket';
                    delete $Item->{ArticleID};
                    delete $Item->{Name};
                }
            }

            # remove article information from types which should not display articles
            if ( !grep { $_ eq $Item->{HistoryType} } @TypesWithArticles ) {
                delete $Item->{ArticleID};
            }

            # get article (if present)
            if ( $Item->{ArticleID} ) {
                $Item->{ArticleData} = $ArticlesByArticleID->{ $Item->{ArticleID} };

                my %ArticleFields = $LayoutObject->ArticleFields(
                    TicketID            => $Item->{ArticleData}->{TicketID},
                    ArticleID           => $Item->{ArticleData}->{ArticleID},
                    ShowDeletedArticles => $Self->{ShowDeletedArticles}
                );
                $Item->{ArticleData}->{ArticleFields} = \%ArticleFields;

                # Get dynamic fields and accounted time
                my $Backend = $ArticlesByArticleID->{ $Item->{ArticleID} }->{Backend};

                # Get dynamic fields and accounted time
                my %ArticleMetaFields = $Kernel::OM->Get("Kernel::Output::HTML::TicketZoom::Agent::$Backend")->ArticleMetaFields(
                    TicketID  => $Item->{ArticleData}->{TicketID},
                    ArticleID => $Item->{ArticleData}->{ArticleID},
                    UserID    => $Self->{UserID},
                );
                $Item->{ArticleData}->{ArticleMetaFields} = \%ArticleMetaFields;

                my @ArticleActions = $LayoutObject->ArticleActions(
                    TicketID            => $Item->{ArticleData}->{TicketID},
                    ArticleID           => $Item->{ArticleData}->{ArticleID},
                    Type                => 'OnLoad',
                    ShowDeletedArticles => $Self->{ShowDeletedArticles}
                );

                $Item->{ArticleData}->{ArticlePlain} = $LayoutObject->ArticlePreview(
                    TicketID   => $Item->{ArticleData}->{TicketID},
                    ArticleID  => $Item->{ArticleData}->{ArticleID},
                    ResultType => 'plain',
                );

                $Item->{ArticleData}->{ArticleHTML} = $Kernel::OM->Get("Kernel::Output::HTML::TimelineView::$Backend")->ArticleRender(
                    TicketID       => $Item->{ArticleData}->{TicketID},
                    ArticleID      => $Item->{ArticleData}->{ArticleID},
                    ArticleActions => \@ArticleActions,
                    UserID         => $Self->{UserID},
                );

                # remove empty lines
                $Item->{ArticleData}->{ArticlePlain} =~ s{^[\n\r]+}{}xmsg;

                # Modify plain text and body to avoid '</script>' tag issue (see bug#14023).
                $Item->{ArticleData}->{ArticlePlain} =~ s{</script>}{<###/script>}xmsg;
                $Item->{ArticleData}->{Body}         =~ s{</script>}{<###/script>}xmsg;

                my %ArticleFlagsAll = $ArticleObject->ArticleFlagGet(
                    ArticleID => $Item->{ArticleID},
                    UserID    => 1,
                );

                my %ArticleFlagsMe = $ArticleObject->ArticleFlagGet(
                    ArticleID => $Item->{ArticleID},
                    UserID    => $Self->{UserID},
                );

                $Item->{ArticleData}->{ArticleIsImportant} = $ArticleFlagsAll{Important};
                $Item->{ArticleData}->{ArticleIsSeen}      = $ArticleFlagsMe{Seen};
            }
            else {

                if ( $Item->{Name} && $Item->{Name} =~ m/^%%/x ) {

                    $Item->{Name} =~ s/^%%//xg;
                    my @Values = split( /%%/x, $Item->{Name} );

                    # See documentation in AgentTicketHistory.pm, line 141+
                    if ( $Item->{HistoryType} eq 'TicketDynamicFieldUpdate' ) {
                        @Values = ( $Values[1], $Values[5] // '', $Values[3] // '' );
                    }
                    elsif ( $Item->{HistoryType} eq 'TypeUpdate' ) {
                        @Values = ( $Values[2], $Values[3], $Values[0], $Values[1] );
                    }

                    $Item->{Name} = $LayoutObject->{LanguageObject}->Translate(
                        $HistoryTypes{ $Item->{HistoryType} },
                        @Values,
                    );

                    # remove not needed place holder
                    $Item->{Name} =~ s/\%s//xg;
                }
            }

            # make the history type more readable (if applicable)
            $Item->{HistoryTypeReadable} = $Self->{HistoryTypeMapping}->{ $Item->{HistoryType} }
                || $Item->{HistoryType};

            # group items which happened (nearly) coincidently together
            my $CreateSystemTimeObject = $Kernel::OM->Create(
                'Kernel::System::DateTime',
                ObjectParams => {
                    String => $Item->{CreateTime},
                },
            );
            $Item->{CreateSystemTime} = $CreateSystemTimeObject
                ? $CreateSystemTimeObject->ToEpoch()
                : undef;

            # if we have two events that happened 'nearly' the same time, treat
            # them as if they happened exactly on the same time (threshold 5 seconds)
            if (
                $LastCreateSystemTime
                && $Item->{CreateSystemTime} <= $LastCreateSystemTime
                && $Item->{CreateSystemTime} >= ( $LastCreateSystemTime - 5 )
                )
            {
                push @{ $HistoryItems{$LastCreateTime} }, $Item;
                $Item->{CreateTime} = $LastCreateTime;
            }
            else {
                push @{ $HistoryItems{ $Item->{CreateTime} } }, $Item;
            }

            $LastCreateTime       = $Item->{CreateTime};
            $LastCreateSystemTime = $Item->{CreateSystemTime};
        }

        my $SortByArticle = sub {

            my $IsA = grep { $_ eq $a->{HistoryType} } @TypesWithArticles;
            my $IsB = grep { $_ eq $b->{HistoryType} } @TypesWithArticles;
            $IsB cmp $IsA;
        };

        # sort history items based on items with articles
        # these items should always be on top of a list of connected items
        $ItemCounter = 0;
        for my $Item ( reverse sort keys %HistoryItems ) {

            for my $SubItem ( sort $SortByArticle @{ $HistoryItems{$Item} } ) {
                $SubItem->{Counter} = $ItemCounter++;

                if ( grep { $_ eq $SubItem->{HistoryType} } @TypesRight ) {
                    $SubItem->{Orientation} = 'Right';
                }
                else {
                    $SubItem->{Orientation} = 'Left';
                }
                push @{ $Param{Items} }, $SubItem;
            }
        }

        # set TicketID for usage in JS
        $Param{TicketID} = $Self->{TicketID};

        # set key 'TimeLong' for JS
        for my $Item ( @{ $Param{Items} } ) {
            $Item->{TimeLong} = $LayoutObject->{LanguageObject}->FormatTimeString( $Item->{CreateTime}, 'DateFormatLong' );
        }

        for my $ArticleID ( sort keys %{$ArticlesByArticleID} ) {

            # Check if article has attachment(s).
            if ( IsHashRefWithData( $ArticlesByArticleID->{$ArticleID}->{Atms} ) ) {

                my ($Index) = grep { $Param{Items}->[$_]->{ArticleID} && $Param{Items}->[$_]->{ArticleID} == $ArticleID }
                    0 .. @{ $Param{Items} };
                $Param{Items}->[$Index]->{HasAttachment} = 1;
            }
        }

        # Get NoTimelineViewAutoArticle config value for usage in JS.
        $LayoutObject->AddJSData(
            Key   => 'NoTimelineViewAutoArticle',
            Value => $ConfigObject->Get('NoTimelineViewAutoArticle') || '0',
        );

        # Include current article ID only if it's selected.
        $Param{CurrentArticleID} //= $Self->{ArticleID};

        # Modify body text to avoid '</script>' tag issue (see bug#14023).
        for my $ArticleBoxItem (@ArticleBox) {
            $ArticleBoxItem->{Body} =~ s{</script>}{<###/script>}xmsg;
        }

        # send data to JS
        $LayoutObject->AddJSData(
            Key   => 'TimelineView',
            Value => {
                Enabled => $ConfigObject->Get('TimelineViewEnabled'),
                Data    => \%Param,
            },
        );

        $LayoutObject->Block(
            Name => 'TimelineView',
            Data => \%Param,
        );

        # jump to selected article
        if ( $Self->{ArticleID} ) {
            $LayoutObject->Block(
                Name => 'ShowSelectedArticle',
                Data => {
                    ArticleID => $Self->{ArticleID},
                },
            );
        }

        # render action menu for all articles
        for my $ArticleID ( sort keys %{$ArticlesByArticleID} ) {

            # show attachments box
            if ( IsHashRefWithData( $ArticlesByArticleID->{$ArticleID}->{Atms} ) ) {

                my $ArticleAttachments = $Self->_CollectArticleAttachments(
                    Article => $ArticlesByArticleID->{$ArticleID},
                );

                $LayoutObject->Block(
                    Name => 'TimelineViewArticleAttachments',
                    Data => {
                        TicketID    => $Self->{TicketID},
                        ArticleID   => $ArticleID,
                        Attachments => $ArticleAttachments,
                    },
                );
            }
        }
    }

    # return output
    return $LayoutObject->Output(
        TemplateFile => 'AgentTicketZoom',
        Data         => { %Param, %Ticket },
    );
}

sub _TicketItemSeen {
    my ( $Self, %Param ) = @_;

    my @Articles = $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleList(
        TicketID            => $Param{TicketID},
        ShowDeletedArticles => $Self->{ShowDeletedArticles}
    );

    for my $Article (@Articles) {
        $Self->_ArticleItemSeen(
            TicketID  => $Param{TicketID},
            ArticleID => $Article->{ArticleID},
        );
    }

    return 1;
}

sub _ArticleItemSeen {
    my ( $Self, %Param ) = @_;

    my $IsArticleDeleted = $Kernel::OM->Get('Kernel::System::Ticket::ArticleFeatures')->IsArticleDeleted(
        ArticleID => $Param{ArticleID}
    );

    # mark shown article as seen
    $Kernel::OM->Get('Kernel::System::Ticket::Article')->ArticleFlagSet(
        TicketID       => $Param{TicketID},
        ArticleID      => $Param{ArticleID},
        Key            => 'Seen',
        Value          => 1,
        UserID         => $Self->{UserID},
        ArticleDeleted => $IsArticleDeleted
    );

    return 1;
}

sub _ArticleItem {
    my ( $Self, %Param ) = @_;

    my %Ticket    = %{ $Param{Ticket} };
    my %Article   = %{ $Param{Article} };
    my %AclAction = %{ $Param{AclAction} };

    my $TicketObject  = $Kernel::OM->Get('Kernel::System::Ticket');
    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');
    my $LayoutObject  = $Kernel::OM->Get('Kernel::Output::HTML::Layout');

    # show article actions
    my @MenuItems = $LayoutObject->ArticleActions(
        %Param,
        TicketID            => $Param{Ticket}->{TicketID},
        ArticleID           => $Param{Article}->{ArticleID},
        Type                => $Param{Type},
        ShowDeletedArticles => $Self->{ShowDeletedArticles}
    );

    push @{ $Self->{MenuItems} }, \@MenuItems;

    # TODO: Review
    return $Self->_ArticleRender(
        TicketID               => $Ticket{TicketID},
        ArticleID              => $Article{ArticleID},
        UserID                 => $Self->{UserID},
        ShowBrowserLinkMessage => $Self->{DoNotShowBrowserLinkMessage} ? 0 : 1,
        Type                   => $Param{Type},
        MenuItems              => \@MenuItems,
        ShowDeletedArticles    => $Self->{ShowDeletedArticles}
    );
}

sub _CollectArticleAttachments {

    my ( $Self, %Param ) = @_;

    my %Article = %{ $Param{Article} };

    my %Attachments;

    # get config object
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');

    # download type
    my $Type = $ConfigObject->Get('AttachmentDownloadType') || 'attachment';

    $Article{AtmCount} = scalar keys %{ $Article{Atms} // {} };

    # if attachment will be forced to download, don't open a new download window!
    my $Target = 'target="AttachmentWindow" ';
    if ( $Type =~ /inline/i ) {
        $Target = 'target="attachment" ';
    }

    $Attachments{ZoomAttachmentDisplayCount} = $ConfigObject->Get('Ticket::ZoomAttachmentDisplayCount');

    ATTACHMENT:
    for my $FileID ( sort keys %{ $Article{Atms} } ) {
        push @{ $Attachments{Files} }, {
            ArticleID => $Article{ArticleID},
            %{ $Article{Atms}->{$FileID} },
            FileID => $FileID,
            Target => $Target,
        };
    }

    return \%Attachments;
}

sub _ArticleBoxGet {
    my ( $Self, %Param ) = @_;

    # Check needed stuff.
    for my $Needed (qw(Page ArticleBoxAll Limit)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!",
            );
            return;
        }
    }

    my $ArticleObject = $Kernel::OM->Get('Kernel::System::Ticket::Article');

    my $Start = ( $Param{Page} - 1 ) * $Param{Limit};

    my $End = $Param{Page} * $Param{Limit} - 1;
    if ( $End >= scalar @{ $Param{ArticleBoxAll} } ) {

        # Make sure that end index doesn't exceed array size.
        $End = scalar @{ $Param{ArticleBoxAll} } - 1;
    }

    my @ArticleIndexes = ( $Start .. $End );

    my $CommunicationChannelObject = $Kernel::OM->Get('Kernel::System::CommunicationChannel');

    # Save communication channel data to improve performance.
    my %CommunicationChannelData;

    my @ArticleBox;
    for my $Index (@ArticleIndexes) {
        my $ArticleBackendObject = $ArticleObject->BackendForArticle(
            TicketID            => $Self->{TicketID},
            ArticleID           => $Param{ArticleBoxAll}->[$Index]->{ArticleID},
            ShowDeletedArticles => $Self->{ShowDeletedArticles}
        );

        my %Article = $ArticleBackendObject->ArticleGet(
            TicketID      => $Self->{TicketID},
            ArticleID     => $Param{ArticleBoxAll}->[$Index]->{ArticleID},
            DynamicFields => 1,
            RealNames     => 1,
        );

        # Include some information about communication channel.
        if ( !$CommunicationChannelData{ $Article{CommunicationChannelID} } ) {

            # Communication channel display name is part of the configuration.
            my %CommunicationChannel = $CommunicationChannelObject->ChannelGet(
                ChannelID => $Article{CommunicationChannelID},
            );

            # Presence of communication channel object indicates its validity.
            my $ChannelObject = $CommunicationChannelObject->ChannelObjectGet(
                ChannelID => $Article{CommunicationChannelID},
            );

            $CommunicationChannelData{ $Article{CommunicationChannelID} } = {
                ChannelName        => $CommunicationChannel{ChannelName},
                ChannelDisplayName => $CommunicationChannel{DisplayName},
                ChannelInvalid     => !$ChannelObject,
            };
        }

        %Article = ( %Article, %{ $CommunicationChannelData{ $Article{CommunicationChannelID} } } );

        push @ArticleBox, \%Article;
    }

    return @ArticleBox;
}

=head2 _ArticleRender()

Returns article html.

    my $HTML = $Self->_ArticleRender(
        TicketID               => 123,      # (required)
        ArticleID              => 123,      # (required)
        Type                   => 'Static', # (required) Static or OnLoad
        ShowBrowserLinkMessage => 1,        # (optional)
    );

Result:
    $HTML = "<div>...</div>";

=cut

sub _ArticleRender {
    my ( $Self, %Param ) = @_;

    # Check needed stuff.
    for my $Needed (qw(TicketID ArticleID Type)) {
        if ( !$Param{$Needed} ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Need $Needed!",
            );
            return;
        }
    }

    # Get article data.
    my $ArticleBackendObject = $Kernel::OM->Get('Kernel::System::Ticket::Article')->BackendForArticle(%Param);

    # Determine channel name for this Article.
    my $ChannelName = $ArticleBackendObject->ChannelNameGet();

    my $Loaded = $Kernel::OM->Get('Kernel::System::Main')->Require(
        "Kernel::Output::HTML::TicketZoom::Agent::$ChannelName",
    );
    return if !$Loaded;

    return $Kernel::OM->Get("Kernel::Output::HTML::TicketZoom::Agent::$ChannelName")->ArticleRender(
        %Param,
        ArticleActions => $Param{MenuItems},
        UserID         => $Self->{UserID},
    );
}

sub _GetIncludedFieldOrdered {
    my ( $Self, %Param ) = @_;

    my @Return;

    ITEM:
    for my $IncludeItem ( @{ $Param{Include} } ) {

        if ( $IncludeItem->{Grid} ) {
            for my $Row ( @{ $IncludeItem->{Grid}{Rows} } ) {

                COLUMN:
                for my $DFEntry ( $Row->@* ) {
                    next COLUMN if !$DFEntry->{DF};

                    push @Return, $DFEntry->{DF};
                }
            }
        }
        elsif ( $IncludeItem->{DF} ) {
            push @Return, $IncludeItem->{DF};
        }
    }

    return @Return;
}

1;