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