[Bps-public-commit] rtx-calendar branch multiple-days-events-2 created. 1.05-20-g8b28a35

BPS Git Server git at git.bestpractical.com
Tue Nov 7 18:19:22 UTC 2023


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "rtx-calendar".

The branch, multiple-days-events-2 has been created
        at  8b28a35ec32be1d0fc1e8b41ae0621125a6b59b0 (commit)

- Log -----------------------------------------------------------------
commit 8b28a35ec32be1d0fc1e8b41ae0621125a6b59b0
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Fri Nov 3 15:09:11 2023 -0300

    Prevent infinite loops when rendering multiple days events
    
    This patch prevents infinite or long loops when rendering multiple days
    events if user set end date to a date many years in the future.

diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 46eeec1..be2486b 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -140,7 +140,11 @@ sub FindTickets {
 
             my $end_date = $ends_date->ISO( Time => 0, Timezone => 'user' );
             my $first_day = 1;
-            while ( $current_date->ISO( Time => 0, Timezone => 'user' ) le $end_date )
+            # We want to prevent infinite loops if user for some reason
+            # set a future date for year 3000 or something like that
+            my $prevent_infinite_loop = 0;
+            while ( ( $current_date->ISO( Time => 0, Timezone => 'user' ) le $end_date )
+                && ( $prevent_infinite_loop++ < 10000 ) )
             {
                 my $dateindex = $current_date->ISO( Time => 0, Timezone => 'user' );
 

commit 552a281e157fd5d75b66a4cb56035f9b2994a1fb
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Fri Nov 3 15:02:53 2023 -0300

    Fix bug multiple days events not showing correctly
    
    The loop on the code that was checking if an event had multiple days had
    a minor bug because it was adding one day to each loop but using
    unixtime to check if the event had the next day ahead. That was causing
    the last day of the event to be disconnected from the rest if it was
    starting at a time lower than the start time of the event.

diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index d37cae8..46eeec1 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -138,9 +138,9 @@ sub FindTickets {
                 Value => $starts_date->Unix,
             );
 
-            my $end_date_unix = $ends_date->Unix;
+            my $end_date = $ends_date->ISO( Time => 0, Timezone => 'user' );
             my $first_day = 1;
-            while ( $current_date->Unix <= $end_date_unix )
+            while ( $current_date->ISO( Time => 0, Timezone => 'user' ) le $end_date )
             {
                 my $dateindex = $current_date->ISO( Time => 0, Timezone => 'user' );
 

commit 7e24fe3c1f57eba397d8f2bea4c4de35d44351c9
Author: Jason Crome <jcrome at bestpractical.com>
Date:   Tue Oct 24 17:25:02 2023 -0400

    Prep 1.06

diff --git a/CHANGES b/CHANGES
index 80ace77..65b8f30 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,11 @@
+1.06 2023-10-24
+ - Minimum version of RT 5.0
+ - Add color-coded display of events by ticket status
+ - Filter event display by ticket status
+ - Added help panel
+ - Add multi-day event support
+ - Add dark mode support
+
 1.05 2023-01-29
  - add a callback named BeforeFindTickets so user can modify the query
    or format of the calendar search
diff --git a/MANIFEST b/MANIFEST
index f88506c..4129aa7 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -1,8 +1,13 @@
 CHANGES
-etc/tabs_privileged_callback.patch
+etc/RTxCalendar_Config.pm
 html/Callbacks/RTx-Calendar/Elements/Tabs/Privileged
+html/Elements/Calendar
 html/Elements/CalendarEvent
+html/Elements/CalendarFooter
+html/Elements/CalendarSidebar
+html/Elements/CalendarWithSidebar
 html/Elements/MyCalendar
+html/Helpers/CalendarEventInfo
 html/Search/Calendar.html
 inc/Module/Install.pm
 inc/Module/Install/Base.pm
@@ -32,4 +37,5 @@ static/images/started.png
 static/images/starts.png
 static/images/starts_due.png
 static/images/updated.png
+static/js/calendar.js
 TODO
diff --git a/META.yml b/META.yml
index 6fc2872..858676f 100644
--- a/META.yml
+++ b/META.yml
@@ -26,6 +26,6 @@ requires:
   perl: 5.10.1
 resources:
   license: http://opensource.org/licenses/gpl-license.php
-version: '1.05'
+version: '1.06'
 x_module_install_rtx_version: '0.43'
-x_requires_rt: 4.2.0
+x_requires_rt: '5.0'
diff --git a/README b/README
index 479243e..3e23eb1 100644
--- a/README
+++ b/README
@@ -15,7 +15,11 @@ DESCRIPTION
     the CONFIGURATION section below for details on adding it.
 
 RT VERSION
-    Works with RT 4.2, 4.4, 5.0
+    Works with RT 5.
+
+    If you need to install this for RT 4.4.x, install version 1.05:
+
+        cpanm RTx::Calendar at 1.05
 
 INSTALLATION
     perl Makefile.PL
@@ -23,11 +27,6 @@ INSTALLATION
     make install
         May need root permissions
 
-    patch RT
-        Apply for versions prior to 4.4.2:
-
-            patch -p1 -d /path/to/rt < etc/tabs_privileged_callback.patch
-
     Edit your /opt/rt5/etc/RT_SiteConfig.pm
         Add this line:
 
@@ -39,23 +38,90 @@ INSTALLATION
     Restart your webserver
 
 CONFIGURATION
-  Base configuration
-    To use the MyCalendar portlet, you must add MyCalendar to
+  Use the calendar on Dashboard
+    The calendar comes with 3 different portlets that can be added to your
+    RT dashboards:
+
+    MyCalendar, a summary of the events for the current week.
+    Calendar, a full month of the calendar view, without sidebar.
+    CalendarWithSidebar, a full month of the calendar view, with sidebar
+    which includes an extra status filter and legends of the calendars.
+
     $HomepageComponents in etc/RT_SiteConfig.pm:
 
-      Set($HomepageComponents, [qw(QuickCreate Quicksearch MyCalendar
+      Set($HomepageComponents, [qw(QuickCreate Quicksearch
+         MyCalendar Calendar CalendarWithSidebar
          MyAdminQueues MySupportQueues MyReminders RefreshHomepage)]);
 
   Display configuration
+   Displaying the owner
     You can show the owner in each day box by adding this line to your
     etc/RT_SiteConfig.pm:
 
         Set($CalendarDisplayOwner, 1);
 
+   Choosing the fields to be displayed in the popup
     You can change which fields show up in the popup display when you mouse
     over a date in etc/RT_SiteConfig.pm:
 
-        Set(@CalendarPopupFields, ('Status', 'OwnerObj->Name', 'DueObj->ISO'));
+        Set(@CalendarPopupFields,
+            ('Status',
+             'OwnerObj->Name',
+             'DueObj->ISO',
+             'CustomField.{Maintenance Estimated Start Date/Time - ET}'));
+
+   Event colors
+    It's also possible to change the color of the events in the calendar by
+    adding the $CalendarStatusColorMap setting to your etc/RT_SiteConfig.pm:
+
+        Set(%CalendarStatusColorMap, (
+            'new'                                   => 'blue',
+            'open'                                  => 'blue',
+            'approved'                              => 'green',
+            'rejected'                              => 'red',
+            'resolved'                              => '#aaa',
+        ));
+
+    You can use any color declaration that CSS supports, including hex
+    codes, color names, and RGB values.
+
+   Event filtering by status
+    You can change the statuses available for filtering on the calendar by
+    adding the @CalendarFilterStatuses setting to your etc/RT_SiteConfig.pm:
+
+        Set(@CalendarFilterStatuses, qw(new open stalled rejected resolved));
+
+   Default selected status on Filtering on Status field
+    You can change the default selected statuses by adding them to the
+    @CalendarFilterDefaultStatuses setting to your etc/RT_SiteConfig.pm:
+
+        Set(@CalendarFilterDefaultStatuses, qw(new open));
+
+   Custom icons
+    Custom Icons can be defined for the events in the calendar by adding the
+    $CalendarIcons setting to your etc/RT_SiteConfig.pm:
+
+        Set(%CalendarIcons, (
+            'CF.{Maintenance Estimated Start Date/Time - ET}'
+                => 'maint.png',
+        ));
+
+    The images should be placed on local/static/images.
+
+   Multiple days events
+    You can define multiple days events by adding the
+    %CalendarMultipleDaysEvents setting to your etc/RT_SiteConfig.pm:
+
+        Set( %CalendarMultipleDaysEvents, (
+                'Maintenance' => {
+                    'Starts' => 'Starts',
+                    'Ends'   => 'Due',
+                },
+            )
+        );
+
+    Note that the Starts and Ends fields must be included in the search
+    result Format in order the event to be displayed on the calendar.
 
 USAGE
     A small help section is available in /Search/Calendar.html
@@ -75,7 +141,7 @@ BUGS
         L<rt.cpan.org|http://rt.cpan.org/Public/Dist/Display.html?Name=RTx-Calendar>.
 
 LICENSE AND COPYRIGHT
-    This software is Copyright (c) 2010-2022 by Best Practical Solutions
+    This software is Copyright (c) 2010-2023 by Best Practical Solutions
 
     Copyright 2007-2009 by Nicolas Chuche
 
diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index c34f236..d37cae8 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -4,7 +4,7 @@ use strict;
 use DateTime;
 use DateTime::Set;
 
-our $VERSION = "1.05";
+our $VERSION = "1.06";
 
 RT->AddStyleSheets('calendar.css');
 RT->AddJavaScript('calendar.js');

commit df36ef788be32523ea85a38fc1f19f14aa7cf99d
Author: Jason Crome <jcrome at bestpractical.com>
Date:   Wed Sep 6 09:35:04 2023 -0400

    Update Module::Install

diff --git a/META.yml b/META.yml
index d7cb0e0..6fc2872 100644
--- a/META.yml
+++ b/META.yml
@@ -8,7 +8,7 @@ configure_requires:
   ExtUtils::MakeMaker: 6.59
 distribution_type: module
 dynamic_config: 1
-generated_by: 'Module::Install version 1.19'
+generated_by: 'Module::Install version 1.21'
 license: gpl
 meta-spec:
   url: http://module-build.sourceforge.net/META-spec-v1.4.html
diff --git a/inc/Module/Install.pm b/inc/Module/Install.pm
index 7ba98c2..3dd721b 100644
--- a/inc/Module/Install.pm
+++ b/inc/Module/Install.pm
@@ -31,7 +31,7 @@ BEGIN {
 	# This is not enforced yet, but will be some time in the next few
 	# releases once we can make sure it won't clash with custom
 	# Module::Install extensions.
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 
 	# Storage for the pseudo-singleton
 	$MAIN    = undef;
diff --git a/inc/Module/Install/Base.pm b/inc/Module/Install/Base.pm
index 9fa42c2..67ce900 100644
--- a/inc/Module/Install/Base.pm
+++ b/inc/Module/Install/Base.pm
@@ -4,7 +4,7 @@ package Module::Install::Base;
 use strict 'vars';
 use vars qw{$VERSION};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 }
 
 # Suspend handler for "redefined" warnings
diff --git a/inc/Module/Install/Can.pm b/inc/Module/Install/Can.pm
index d65c753..93fc4f9 100644
--- a/inc/Module/Install/Can.pm
+++ b/inc/Module/Install/Can.pm
@@ -8,7 +8,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Fetch.pm b/inc/Module/Install/Fetch.pm
index 3072b08..3c9390a 100644
--- a/inc/Module/Install/Fetch.pm
+++ b/inc/Module/Install/Fetch.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Include.pm b/inc/Module/Install/Include.pm
index 13fdcd0..b9b926f 100644
--- a/inc/Module/Install/Include.pm
+++ b/inc/Module/Install/Include.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Makefile.pm b/inc/Module/Install/Makefile.pm
index 13a4464..1e214a0 100644
--- a/inc/Module/Install/Makefile.pm
+++ b/inc/Module/Install/Makefile.pm
@@ -8,7 +8,7 @@ use Fcntl qw/:flock :seek/;
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/Metadata.pm b/inc/Module/Install/Metadata.pm
index 11bf971..2ae8036 100644
--- a/inc/Module/Install/Metadata.pm
+++ b/inc/Module/Install/Metadata.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
@@ -455,12 +455,8 @@ sub author_from {
 my %license_urls = (
     perl         => 'http://dev.perl.org/licenses/',
     apache       => 'http://apache.org/licenses/LICENSE-2.0',
-    apache_1_1   => 'http://apache.org/licenses/LICENSE-1.1',
     artistic     => 'http://opensource.org/licenses/artistic-license.php',
-    artistic_2   => 'http://opensource.org/licenses/artistic-license-2.0.php',
     lgpl         => 'http://opensource.org/licenses/lgpl-license.php',
-    lgpl2        => 'http://opensource.org/licenses/lgpl-2.1.php',
-    lgpl3        => 'http://opensource.org/licenses/lgpl-3.0.html',
     bsd          => 'http://opensource.org/licenses/bsd-license.php',
     gpl          => 'http://opensource.org/licenses/gpl-license.php',
     gpl2         => 'http://opensource.org/licenses/gpl-2.0.php',
@@ -471,6 +467,12 @@ my %license_urls = (
     unrestricted => undef,
     restrictive  => undef,
     unknown      => undef,
+
+    # these are not actually allowed in meta-spec v1.4 but are left here for compatibility:
+    apache_1_1   => 'http://apache.org/licenses/LICENSE-1.1',
+    artistic_2   => 'http://opensource.org/licenses/artistic-license-2.0.php',
+    lgpl2        => 'http://opensource.org/licenses/lgpl-2.1.php',
+    lgpl3        => 'http://opensource.org/licenses/lgpl-3.0.html',
 );
 
 sub license {
diff --git a/inc/Module/Install/Win32.pm b/inc/Module/Install/Win32.pm
index f7aa615..b6c1d37 100644
--- a/inc/Module/Install/Win32.pm
+++ b/inc/Module/Install/Win32.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = 'Module::Install::Base';
 	$ISCORE  = 1;
 }
diff --git a/inc/Module/Install/WriteAll.pm b/inc/Module/Install/WriteAll.pm
index 2db861a..d87eb9a 100644
--- a/inc/Module/Install/WriteAll.pm
+++ b/inc/Module/Install/WriteAll.pm
@@ -6,7 +6,7 @@ use Module::Install::Base ();
 
 use vars qw{$VERSION @ISA $ISCORE};
 BEGIN {
-	$VERSION = '1.19';
+	$VERSION = '1.21';
 	@ISA     = qw{Module::Install::Base};
 	$ISCORE  = 1;
 }
diff --git a/inc/YAML/Tiny.pm b/inc/YAML/Tiny.pm
index fb157a6..db3ae5c 100644
--- a/inc/YAML/Tiny.pm
+++ b/inc/YAML/Tiny.pm
@@ -2,12 +2,12 @@
 use 5.008001; # sane UTF-8 support
 use strict;
 use warnings;
-package YAML::Tiny; # git description: v1.72-7-g8682f63
+package YAML::Tiny; # git description: v1.73-12-ge02f827
 # XXX-INGY is 5.8.1 too old/broken for utf8?
 # XXX-XDG Lancaster consensus was that it was sufficient until
 # proven otherwise
 
-our $VERSION = '1.73';
+our $VERSION = '1.74';
 
 #####################################################################
 # The YAML::Tiny API.

commit 5acb7fba2f98a8d945841814aebcc98aab939fab
Author: Jason Crome <jcrome at bestpractical.com>
Date:   Wed Sep 27 12:58:54 2023 -0400

    Remove remaining RT4 support

diff --git a/Makefile.PL b/Makefile.PL
index ed70d43..9b28b3e 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -6,7 +6,7 @@ RTx('RTx-Calendar');
 requires 'DateTime';
 requires 'DateTime::Set';
 
-requires_rt '4.2.0';
+requires_rt '5.0.0';
 
 sign();
 WriteAll();
diff --git a/etc/tabs_privileged_callback.patch b/etc/tabs_privileged_callback.patch
deleted file mode 100644
index 493c0f8..0000000
--- a/etc/tabs_privileged_callback.patch
+++ /dev/null
@@ -1,32 +0,0 @@
-diff --git a/share/html/Elements/Tabs b/share/html/Elements/Tabs
-index c6c6505..d4b2e59 100644
---- a/share/html/Elements/Tabs
-+++ b/share/html/Elements/Tabs
-@@ -757,6 +757,9 @@ my $build_main_nav = sub {
-         }
-     }
- 
-+    # Scope here so we can share in the Privileged callback
-+    my $args      = '';
-+    my $has_query = '';
-     if (
-         (
-                $request_path =~ m{^/(?:Ticket|Search)/}
-@@ -767,8 +770,6 @@ my $build_main_nav = sub {
-       )
-     {
-         my $search = Menu()->child('search')->child('tickets');
--        my $args      = '';
--        my $has_query = '';
-         my $current_search = $session{"CurrentSearchHash"} || {};
-         my $search_id = $DECODED_ARGS->{'SavedSearchLoad'} || $DECODED_ARGS->{'SavedSearchId'} || $current_search->{'SearchId'} || '';
-         my $chart_id = $DECODED_ARGS->{'SavedChartSearchId'} || $current_search->{SavedChartSearchId};
-@@ -936,7 +937,7 @@ my $build_main_nav = sub {
-         PageMenu()->child( edit => title => loc('Edit'), path => '/Prefs/MyRT.html' );
-     }
- 
--    $m->callback( CallbackName => 'Privileged', Path => $request_path );
-+    $m->callback( CallbackName => 'Privileged', Path => $request_path, Search_Args => $args, Has_Query => $has_query );
- };
- 
- my $build_selfservice_nav = sub {
diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 45adb94..c34f236 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -330,7 +330,11 @@ CONFIGURATION section below for details on adding it.
 
 =head1 RT VERSION
 
-Works with RT 4.2, 4.4, 5.0
+Works with RT 5.
+
+If you need to install this for RT 4.4.x, install version 1.05:
+
+    cpanm RTx::Calendar at 1.05
 
 =head1 INSTALLATION
 
@@ -344,12 +348,6 @@ Works with RT 4.2, 4.4, 5.0
 
 May need root permissions
 
-=item patch RT
-
-Apply for versions prior to 4.4.2:
-
-    patch -p1 -d /path/to/rt < etc/tabs_privileged_callback.patch
-
 =item Edit your F</opt/rt5/etc/RT_SiteConfig.pm>
 
 Add this line:

commit 8779ce3e23de99cc60cb61728d1132669b4787b6
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Oct 24 17:33:16 2023 -0400

    Call old BeforeFindTickets callback for back compatibility

diff --git a/html/Elements/Calendar b/html/Elements/Calendar
index 1c37225..4af069e 100644
--- a/html/Elements/Calendar
+++ b/html/Elements/Calendar
@@ -316,6 +316,9 @@ if (@FilterOnStatus) {
   $TempQuery .= "($StatusClause)";
 }
 
+# For back compatibility
+$m->callback( CallbackName => 'BeforeFindTickets', ARGSRef => \%ARGS, QueryRef => \$TempQuery, FormatRef => \$TempFormat, CallbackPage => '/Search/Calendar.html' ) if $m->request_path eq '/Search/Calendar.html';
+
 $m->callback( CallbackName => 'BeforeFindTickets', ARGSRef => \%ARGS, QueryRef => \$TempQuery, FormatRef => \$TempFormat );
 
 my ($Tickets, $TicketsSpanningDays) = RTx::Calendar::FindTickets($session{'CurrentUser'}, $TempQuery, \@Dates, $date->strftime("%F"), $end->strftime("%F"));

commit d8f5e7f00ac56f6ac81c53becd70894831a5e346
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Tue Oct 24 17:02:15 2023 -0400

    Update pod for new added features

diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 08791b4..45adb94 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -366,25 +366,106 @@ Add this line:
 
 =head1 CONFIGURATION
 
-=head2 Base configuration
+=head2 Use the calendar on Dashboard
+
+The calendar comes with 3 different portlets that can be added to your
+RT dashboards:
+
+=over
+
+=item C<MyCalendar>, a summary of the events for the current week.
+
+=item C<Calendar>, a full month of the calendar view, without sidebar.
+
+=item C<CalendarWithSidebar>, a full month of the calendar view, with
+sidebar which includes an extra status filter and legends of the calendars.
+
+=back
 
-To use the C<MyCalendar> portlet, you must add C<MyCalendar> to
 C<$HomepageComponents> in F<etc/RT_SiteConfig.pm>:
 
-  Set($HomepageComponents, [qw(QuickCreate Quicksearch MyCalendar
+  Set($HomepageComponents, [qw(QuickCreate Quicksearch
+     MyCalendar Calendar CalendarWithSidebar
      MyAdminQueues MySupportQueues MyReminders RefreshHomepage)]);
 
 =head2 Display configuration
 
+=head3 Displaying the owner
+
 You can show the owner in each day box by adding this line to your
 F<etc/RT_SiteConfig.pm>:
 
     Set($CalendarDisplayOwner, 1);
 
+=head3 Choosing the fields to be displayed in the popup
+
 You can change which fields show up in the popup display when you
 mouse over a date in F<etc/RT_SiteConfig.pm>:
 
-    Set(@CalendarPopupFields, ('Status', 'OwnerObj->Name', 'DueObj->ISO'));
+    Set(@CalendarPopupFields,
+        ('Status',
+         'OwnerObj->Name',
+         'DueObj->ISO',
+         'CustomField.{Maintenance Estimated Start Date/Time - ET}'));
+
+=head3 Event colors
+
+It's also possible to change the color of the events in the calendar by
+adding the C<$CalendarStatusColorMap> setting to your F<etc/RT_SiteConfig.pm>:
+
+    Set(%CalendarStatusColorMap, (
+        'new'                                   => 'blue',
+        'open'                                  => 'blue',
+        'approved'                              => 'green',
+        'rejected'                              => 'red',
+        'resolved'                              => '#aaa',
+    ));
+
+You can use any color declaration that CSS supports, including hex codes,
+color names, and RGB values.
+
+=head3 Event filtering by status
+
+You can change the statuses available for filtering on the calendar by
+adding the C<@CalendarFilterStatuses> setting to your
+F<etc/RT_SiteConfig.pm>:
+
+    Set(@CalendarFilterStatuses, qw(new open stalled rejected resolved));
+
+=head3 Default selected status on Filtering on Status field
+
+You can change the default selected statuses by adding them to the
+C<@CalendarFilterDefaultStatuses> setting to your F<etc/RT_SiteConfig.pm>:
+
+    Set(@CalendarFilterDefaultStatuses, qw(new open));
+
+=head3 Custom icons
+
+Custom Icons can be defined for the events in the calendar by adding the
+C<$CalendarIcons> setting to your F<etc/RT_SiteConfig.pm>:
+
+    Set(%CalendarIcons, (
+        'CF.{Maintenance Estimated Start Date/Time - ET}'
+            => 'maint.png',
+    ));
+
+The images should be placed on F<local/static/images>.
+
+=head3 Multiple days events
+
+You can define multiple days events by adding the C<%CalendarMultipleDaysEvents>
+setting to your F<etc/RT_SiteConfig.pm>:
+
+    Set( %CalendarMultipleDaysEvents, (
+            'Maintenance' => {
+                'Starts' => 'Starts',
+                'Ends'   => 'Due',
+            },
+        )
+    );
+
+Note that the Starts and Ends fields must be included in the search result
+Format in order the event to be displayed on the calendar.
 
 =head1 USAGE
 
@@ -408,7 +489,7 @@ or via the web at
 
 =head1 LICENSE AND COPYRIGHT
 
-This software is Copyright (c) 2010-2022 by Best Practical Solutions
+This software is Copyright (c) 2010-2023 by Best Practical Solutions
 
 Copyright 2007-2009 by Nicolas Chuche
 

commit 0c1ff33d6412ca2eaf9de92bced5ddc54d5fa6c1
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Tue Oct 24 16:10:55 2023 -0400

    Add default config

diff --git a/etc/RTxCalendar_Config.pm b/etc/RTxCalendar_Config.pm
new file mode 100644
index 0000000..5241e22
--- /dev/null
+++ b/etc/RTxCalendar_Config.pm
@@ -0,0 +1,25 @@
+Set(%CalendarIcons, (
+    'Reminder'     => 'reminder.png',
+    'Resolved'     => 'resolved.png',
+    'Starts, Due'  => 'starts_due.png',
+    'Created, Due' => 'created_due.png',
+    'Created'      => 'created.png',
+    'Due'          => 'due.png',
+    'Starts'       => 'starts.png',
+    'Started'      => 'started.png',
+    'LastUpdated'  => 'updated.png',
+));
+
+Set(%CalendarStatusColorMap, (
+    'new'                                   => '#87873c',
+    'open'                                  => '#5555f8',
+    'rejected'                              => '#FF0000',
+    'resolved'                              => '#72b872',
+    'stalled'                               => '#FF0000',
+));
+
+Set(@CalendarFilterStatuses, qw(new open stalled rejected resolved));
+
+Set(@CalendarFilterDefaultStatuses, qw(new open));
+
+1;

commit 3b40dcad14f3dd2429c34fc351a0a4e22d908712
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Fri Sep 29 12:14:00 2023 -0400

    Tweak dark mode styles

diff --git a/static/css/calendar.css b/static/css/calendar.css
index cfe7825..1eef16d 100644
--- a/static/css/calendar.css
+++ b/static/css/calendar.css
@@ -181,3 +181,17 @@ table.rtxcalendar .day.last-day {
 table.rtxcalendar .day.first-day.last-day {
     border-radius: 5px;
 }
+
+.darkmode table.rtxcalendar * {
+    background: unset !important;
+}
+
+.darkmode table.rtxcalendar div.day div.event-info:hover span.tip{
+    border: 1px solid #555;
+    background-color: #2C3539 !important;
+    color: #fff !important;
+}
+
+.darkmode table.rtxcalendar div.day a {
+    color: #fff !important;
+}

commit 5eb4908f2106bb16cef969b17526e03ea79bf14d
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Mon Oct 16 11:37:55 2023 -0300

    Load event details with Ajax call when hovering event title
    
    For improving performance, the event details are loaded now with an Ajax
    call when hovering the event title.

diff --git a/html/Elements/CalendarEvent b/html/Elements/CalendarEvent
index 515e1e0..d06e38c 100644
--- a/html/Elements/CalendarEvent
+++ b/html/Elements/CalendarEvent
@@ -67,46 +67,9 @@ if ( ( !grep { $_ eq $TicketId } @$spanning_tickets_for_tomorrow ) ) {
            <% $display_owner ? 'by ' . $Object->OwnerObj->Name : '' %>
            <% length($Object->Subject) > 80 ? substr($Object->Subject, 0, 77) . "..." : $Object->Subject %>
         </a>
+% # Placeholder for the event details that will be loaded via AJAX on hover
         <span class="tip">
-            <a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>">
-               <% $Object->QueueObj->Name %> #<% $TicketId %>
-            </a>
-	        :</strong> <% $subject%><br />
-	<br />
-
-<%perl>
-# logic taken from Ticket/Search/Results.tsv
- foreach my $attr (@display_fields) {
-    my $value;
-
-    if ($attr =~ /(.*)->ISO$/ and $Object->$1->Unix <= 0) {
-        $value = '-';
-    } elsif ($attr =~ /CustomField\.\{(.*)\}$/) {
-        my $cf = $1;
-        my $cf_obj = $Object->LoadCustomFieldByIdentifier($cf);
-        unless ($cf_obj->id) {
-            $RT::Logger->debug("Custom field '$cf' not available for ticket #".$Object->id);
-            next;
-        }
-        $value = $Object->FirstCustomFieldValue($cf);
-        if (grep { $_ eq $cf_obj->Type} qw(DateTime Date)) {
-            my $date_value = RT::Date->new($session{'CurrentUser'});
-            my $date_format = $cf_obj->Type eq 'DateTime' ? 'ISO' : 'unknown';
-            $date_value->Set(Format => $date_format, Value => $value);
-            $value = $date_value->AsString( Timezone => 'user', Time => $cf_obj->Type eq 'DateTime' ? 1 : 0 );
-        }
-    } else {
-        my $method = '$Object->'.$attr.'()';
-        $method =~ s/->ISO\(\)$/->ISO( Timezone => 'user' )/;
-        $value = eval $method;
-        if ($@) {die "<b>Check your CalendarPopupFields config in etc/RT_SiteConfig.pm</b>.<br /><br />Failed to find \"$attr\" - ". $@};
-    }
-</%perl>
-	<strong><&|/l&><% $label_of{$attr} %></&>:</strong> <% $value %><br />
-% }
-
-    <br />
-</span>
+        </span>
 % }
     </div>
 </small>
@@ -146,31 +109,4 @@ my $display_owner = $RT::CalendarDisplayOwner;
 $display_owner ||= RT->Config->Get('CalendarDisplayOwner')
     if RT->can('Config');
 
-
-# 3.6 config
-my @display_fields = @RT::CalendarPopupFields;
-
-# 3.8 config
-# the if condition is weird but it doesn't work with 3.8.0 without the last part
- at display_fields = RT->Config->Get('CalendarPopupFields')
-    if 0 == @display_fields and RT->can('Config') and RT->Config->Get('CalendarPopupFields');
-
-# default
-if (0 == @display_fields) {
-    @display_fields = qw(OwnerObj->Name CreatedObj->ISO StartsObj->ISO
-			 StartedObj->ISO LastUpdatedObj->ISO DueObj->ISO
-			 ResolvedObj->ISO Status Priority
-			 Requestors->MemberEmailAddressesAsString);
-}
-
-
-my %label_of;
-for my $field (@display_fields) {
-    my $label = $field;
-    $label =~ s'Obj-.(?:AsString|Name|ISO)''g;
-    $label =~ s'-\>MemberEmailAddressesAsString''g;
-    $label =~ s/CustomField\.\{(.*)\}/$1/g;
-    $label_of{$field} = $label;
-}
-
 </%init>
diff --git a/html/Helpers/CalendarEventInfo b/html/Helpers/CalendarEventInfo
new file mode 100644
index 0000000..cca3221
--- /dev/null
+++ b/html/Helpers/CalendarEventInfo
@@ -0,0 +1,83 @@
+<%args>
+$event
+</%args>
+<%init>
+my @event_details = split /-/, $event;
+my $object_type = $event_details[0]; # ticket or reminder
+my $ticket_id = $event_details[1];
+my $Object = RT::Ticket->new($session{'CurrentUser'});
+$Object->Load($ticket_id);
+
+my $status;
+my $TicketId;
+my $subject = $Object->Subject;
+
+if ($Object->Type eq 'reminder') {
+    if ($Object->RefersTo->First) {
+	my $ticket   = $Object->RefersTo->First->TargetObj;
+	$TicketId = $ticket->Id;
+	$subject = $Object->Subject . " (" . $ticket->Subject . ")";
+        $status = $Object->Status;
+    }
+} else {
+    $TicketId = $Object->Id;
+    $subject = $Object->Subject;
+    $status = $Object->Status;
+}
+
+my @display_fields = RT->Config->Get('CalendarPopupFields');
+
+# default
+if (0 == @display_fields) {
+    @display_fields = qw(OwnerObj->Name CreatedObj->ISO StartsObj->ISO
+			 StartedObj->ISO LastUpdatedObj->ISO DueObj->ISO
+			 ResolvedObj->ISO Status Priority
+			 Requestors->MemberEmailAddressesAsString);
+}
+
+my %label_of;
+for my $field (@display_fields) {
+    my $label = $field;
+    $label =~ s'Obj-.(?:AsString|Name|ISO)''g;
+    $label =~ s'-\>MemberEmailAddressesAsString''g;
+    $label =~ s/CustomField\.\{(.*)\}/$1/g;
+    $label_of{$field} = $label;
+}
+</%init>
+<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>">
+    <% $Object->QueueObj->Name %> #<% $TicketId %>
+</a>
+:</strong> <% $subject%><br />
+<br />
+<%perl>
+# logic taken from Ticket/Search/Results.tsv
+foreach my $attr (@display_fields) {
+    my $value;
+
+    if ($attr =~ /(.*)->ISO$/ and $Object->$1->Unix <= 0) {
+        $value = '-';
+    } elsif ($attr =~ /CustomField\.\{(.*)\}$/) {
+        my $cf = $1;
+        my $cf_obj = $Object->LoadCustomFieldByIdentifier($cf);
+        unless ($cf_obj->id) {
+            $RT::Logger->debug("Custom field '$cf' not available for ticket #".$Object->id);
+            next;
+        }
+        $value = $Object->FirstCustomFieldValue($cf);
+        if (grep { $_ eq $cf_obj->Type} qw(DateTime Date)) {
+            my $date_value = RT::Date->new($session{'CurrentUser'});
+            my $date_format = $cf_obj->Type eq 'DateTime' ? 'ISO' : 'unknown';
+            $date_value->Set(Format => $date_format, Value => $value);
+            $value = $date_value->AsString( Timezone => 'user', Time => $cf_obj->Type eq 'DateTime' ? 1 : 0 );
+        }
+    } else {
+        my $method = '$Object->'.$attr.'()';
+        $method =~ s/->ISO\(\)$/->ISO( Timezone => 'user' )/;
+        $value = eval $method;
+        if ($@) {die "<b>Check your CalendarPopupFields config in etc/RT_SiteConfig.pm</b>.<br /><br />Failed to find \"$attr\" - ". $@};
+    }
+</%perl>
+	<strong><&|/l&><% $label_of{$attr} %></&>:</strong> <% $value %><br />
+% }
+<br />
+% $m->abort;
diff --git a/static/js/calendar.js b/static/js/calendar.js
index 2c48449..662fa8d 100644
--- a/static/js/calendar.js
+++ b/static/js/calendar.js
@@ -1,6 +1,12 @@
 window.onresize = resizeCalendarEventTitles;
 jQuery(function() {
     resizeCalendarEventTitles();
+
+    jQuery('div[data-object]>small>div.event-info>a.event-title').hover(
+        function(e) {
+            loadCalendarEventDetails(e);
+        }
+    );
 });
 
 /*
@@ -46,3 +52,20 @@ function changeCalendarMonth() {
     var querystring = jQuery('.changeCalendarMonth #querystring').val();
     window.location.href = "?Month=" + month + "&Year=" + year + "&" + querystring;
 }
+
+function loadCalendarEventDetails(e) {
+    // data-object
+    var event = jQuery(e.currentTarget).parents('[data-object]').attr('data-object');
+    // remove hover event from the element to run only once
+    jQuery(e.currentTarget).off('mouseenter mouseleave');
+
+    var url = RT.Config.WebHomePath + '/Helpers/CalendarEventInfo?event=' + event;
+
+    jQuery.ajax({
+        url: url,
+        success: function(data) {
+            jQuery(e.currentTarget).parents('[data-object]')
+                .find('div.event-info>span.tip').html(data);
+        }
+    });
+}

commit c1797ea74c4da67a8a633cc875fd61c490dcc9ad
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Thu Sep 7 18:26:10 2023 -0300

    Add Multiple Days Events to Calendar
    
    Add Multiple Days Events to Calendar where an event can span multiple
    based on customizable start and end time fields.

diff --git a/html/Elements/Calendar b/html/Elements/Calendar
index 9e08ad7..1c37225 100644
--- a/html/Elements/Calendar
+++ b/html/Elements/Calendar
@@ -62,27 +62,92 @@ $ShowSidebar => 0
 
 <tbody>
 <tr>
-% while ($date <= $end) {
-%   my @classes = ();
-%   push @classes, "offmonth"  if $date->month != ($Month + 1);
-%   push @classes, "today"     if (DateTime->compare($today,     $date) == 0);
-%   push @classes, "yesterday" if (DateTime->compare($yesterday, $date) == 0);
-%   push @classes, "aweekago"  if (DateTime->compare($aweekago,  $date) == 0);
-
-    <td class="<% @classes %>"><div class="inside-day">
-      <div class="calendardate"><%$date->day%></div>
+<%perl>
+# We use %week_ticket_position to control the display of tickets on the
+# calendar. It has the following structure:
+# {
+#   1 => { id => 123, TicketObj => $t },
+#   2 => { id => 312, TicketObj => $t },
+#   3 => { id => '', TicketObj => undef }, # empty position
+#   4 => { id => 111, TicketObj => $t },
+# }
+# where the key is the position/line of the ticket in the current week
+# when an event ends during the week, it's removed from the hash, openning
+# the position for a new ticket to be placed at the same line on the week,
+# saving some height on the calendar.
+# This variable is cleaned every time we start a new week.
+my %week_ticket_position;
+my $day_of_week = 1;
+
+while ($date <= $end) {
+  my @classes = ();
+  push @classes, "offmonth"  if $date->month != ($Month + 1);
+  push @classes, "today"     if (DateTime->compare($today,     $date) == 0);
+  push @classes, "yesterday" if (DateTime->compare($yesterday, $date) == 0);
+  push @classes, "aweekago"  if (DateTime->compare($aweekago,  $date) == 0);
+  push @classes, "weekday-$day_of_week";
+
+  for my $t ( @{ $Tickets->{ $date->strftime("%F") } || [] } ) {
+    # check if ticket was already displayed this week, if not, we need to find a
+    # position for it
+    unless ( grep { $week_ticket_position{$_}{id} eq $t->id } keys %week_ticket_position ) {
+      # new tickets should assume the first empty spot.
+      my $i = 1;
+      my $free_index = 0;
+      for my $index ( sort { $a <=> $b } keys %week_ticket_position ) {
+        if ( $week_ticket_position{$index}{id} eq "" ) {
+          $free_index = $i;
+          last;
+        }
+        $i++;
+      }
+      # if we found a free spot, we place the ticket there
+      if ( $free_index != 0 ) {
+        $week_ticket_position{$free_index}{id} = $t->id;
+        $week_ticket_position{$free_index}{TicketObj} = $t;
+      }
+      # if not, we add it to the end of the hash
+      else {
+        $week_ticket_position{((scalar keys %week_ticket_position)+1)}{id} = $t->id;
+        $week_ticket_position{((scalar keys %week_ticket_position))}{TicketObj} = $t;
+      }
+    }
+  }
+</%perl>
 
-%     for my $t ( @{ $Tickets{$date->strftime("%F")} } ) {
-        <& /Elements/CalendarEvent, Object => $t, Date => $date, DateTypes => \%DateTypes &>
+    <td class="<% join(' ', @classes) %>"><div class="inside-day">
+      <div class="calendardate"><%$date->day%></div>
+%     for my $index ( sort { $a <=> $b } keys %week_ticket_position ) {
+%       if ( grep { $_->id eq $week_ticket_position{$index}{id} }
+%                 @{ $Tickets->{ $date->strftime("%F") } || [] } ) {
+%         my $t = $week_ticket_position{$index}{TicketObj};
+        <& /Elements/CalendarEvent,
+          Object              => $t,
+          Date                => $date,
+          DateTypes           => \%DateTypes,
+          DayOfWeek           => $day_of_week,
+          TicketsSpanningDays => $TicketsSpanningDays,
+          WeekTicketPosition  => \%week_ticket_position,
+          CurrentPostion      => $index,
+        &>
+%       }
+%       else {
+%         # if there's no ticket for this position, we add an empty space
+             <div class="day"> </div>
+%       }
 %     }
-
     </div></td>
 
 %   $date = $set->next($date);
 %   if ( $date->day_of_week == $startday_of_week ) {
+% #   we start a new week with empty positions
+%     %week_ticket_position = ();
+%     $day_of_week=1;
       </tr><tr>
 %   }
-
+%   else {
+%     $day_of_week = $day_of_week + 1;
+%   }
 % }
 </tr>
 </tbody>
@@ -253,7 +318,7 @@ if (@FilterOnStatus) {
 
 $m->callback( CallbackName => 'BeforeFindTickets', ARGSRef => \%ARGS, QueryRef => \$TempQuery, FormatRef => \$TempFormat );
 
-my %Tickets = RTx::Calendar::FindTickets($session{'CurrentUser'}, $TempQuery, \@Dates, $date->strftime("%F"), $end->strftime("%F"));
+my ($Tickets, $TicketsSpanningDays) = RTx::Calendar::FindTickets($session{'CurrentUser'}, $TempQuery, \@Dates, $date->strftime("%F"), $end->strftime("%F"));
 
 my $DownloadQueryString =
       $m->comp(
@@ -263,5 +328,4 @@ my $DownloadQueryString =
         Order   => $Order,
         OrderBy => $OrderBy,
       );
-
 </%INIT>
diff --git a/html/Elements/CalendarEvent b/html/Elements/CalendarEvent
index b8023d9..515e1e0 100644
--- a/html/Elements/CalendarEvent
+++ b/html/Elements/CalendarEvent
@@ -2,52 +2,122 @@
 $Date => undef
 $Object => undef
 $DateTypes => undef
+$DayOfWeek => undef
+$TicketsSpanningDays => undef
+$WeekTicketPosition => undef
+$CurrentPostion => undef
 </%args>
-<div class="day">
-<small>
 
-    <% RTx::Calendar::GetEventImg($Object, $today, $DateTypes, $IsReminder)|n %>
-	<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>"
+<%perl>
+my $spanning_tickets_for_today    = $TicketsSpanningDays->{$today}    || [];
+my $spanning_tickets_for_tomorrow = $TicketsSpanningDays->{$tomorrow} || [];
+my $first_day_of_the_event = 0;
+my $last_day_of_the_event = 0;
+# If the ticket is not in the spanning tickets for today array, it means
+# it's the first day of the event
+if ( ( ! grep { $_ eq $TicketId } @$spanning_tickets_for_today ) ) {
+    $first_day_of_the_event = 1;
+}
+if ( ( !grep { $_ eq $TicketId } @$spanning_tickets_for_tomorrow ) ) {
+    $last_day_of_the_event = 1;
+    # This frees up the position for the next ticket
+    $WeekTicketPosition->{$CurrentPostion}->{id} = "";
+}
+</%perl>
+
+<div class="day
+% if ( $last_day_of_the_event || $DayOfWeek eq 7 ) {
+    last-day
+% }
+% if ( $first_day_of_the_event || $DayOfWeek eq 1 ) {
+    first-day
+% }
+% my $status_class = 'event-status-'.$status;
+% $status_class =~ s/\s+/-/g;
+<% $status_class %>
+" style="
 % if ( $CalendarStatusColorMap{$status} ) {
-    style="color: <%$CalendarStatusColorMap{$status}%>;"
+    background-color: <%$CalendarStatusColorMap{$status}%> !important;
+% }
+% # We need to decrease the z-index of the spanning days of an event
+% # so the event title (which is placed on the div of the first day of the
+% # event and has a z-index of 4) is visible, since it cross multiple days.
+% if ( (grep { $_ eq $TicketId } @$spanning_tickets_for_today)
+%        && $DayOfWeek ne 1 ) {
+    z-index: 3;
 % }
-    >
+" data-object="<% $Object->Type %>-<% $Object->id %>">
+
+    <small>
+        <div class="event-icon" style="
+% if ($last_day_of_the_event
+% && !$first_day_of_the_event) {
+            float: right;
+% }
+        ">
+% if ( $first_day_of_the_event || $last_day_of_the_event ) {
+% my $icon = RTx::Calendar::GetEventImg( $Object, $today, $DateTypes, $IsReminder, $session{'CurrentUser'} );
+            <% $icon|n %>
+% }
+        </div>
+        <div class="event-info">
+% if ( $first_day_of_the_event || $DayOfWeek eq 1 ) {
+        <a class="event-title" href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>">
            <% $Object->QueueObj->Name %> #<% $TicketId %>
            <% $display_owner ? 'by ' . $Object->OwnerObj->Name : '' %>
-           <% length($Object->Subject) > 80 ? substr($Object->Subject, 0, 77) . "..." : $Object->Subject %></a></small><br />
-	<span class="tip">
-	<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>">
-           <% $Object->QueueObj->Name %> #<% $TicketId %>
+           <% length($Object->Subject) > 80 ? substr($Object->Subject, 0, 77) . "..." : $Object->Subject %>
         </a>
-	:</strong> <% $subject%><br />
+        <span class="tip">
+            <a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>">
+               <% $Object->QueueObj->Name %> #<% $TicketId %>
+            </a>
+	        :</strong> <% $subject%><br />
 	<br />
 
-%# logic taken from Ticket/Search/Results.tsv
-% foreach my $attr (@display_fields) {
-%    my $value;
-%
-%    if ($attr =~ /(.*)->ISO$/ and $Object->$1->Unix <= 0) {
-%        $value = '-';
-%    } elsif ($attr =~ /CustomField\.\{(.*)\}$/) {
-%        my $cf = $1;
-%        $value = $Object->FirstCustomFieldValue($cf);
-%    } else {
-%        my $method = '$Object->'.$attr.'()';
-%        $method =~ s/->ISO\(\)$/->ISO( Timezone => 'user' )/;
-%        $value = eval $method;
-%        if ($@) {die "<b>Check your CalendarPopupFields config in etc/RT_SiteConfig.pm</b>.<br /><br />Failed to find \"$attr\" - ". $@};
-%    }
+<%perl>
+# logic taken from Ticket/Search/Results.tsv
+ foreach my $attr (@display_fields) {
+    my $value;
+
+    if ($attr =~ /(.*)->ISO$/ and $Object->$1->Unix <= 0) {
+        $value = '-';
+    } elsif ($attr =~ /CustomField\.\{(.*)\}$/) {
+        my $cf = $1;
+        my $cf_obj = $Object->LoadCustomFieldByIdentifier($cf);
+        unless ($cf_obj->id) {
+            $RT::Logger->debug("Custom field '$cf' not available for ticket #".$Object->id);
+            next;
+        }
+        $value = $Object->FirstCustomFieldValue($cf);
+        if (grep { $_ eq $cf_obj->Type} qw(DateTime Date)) {
+            my $date_value = RT::Date->new($session{'CurrentUser'});
+            my $date_format = $cf_obj->Type eq 'DateTime' ? 'ISO' : 'unknown';
+            $date_value->Set(Format => $date_format, Value => $value);
+            $value = $date_value->AsString( Timezone => 'user', Time => $cf_obj->Type eq 'DateTime' ? 1 : 0 );
+        }
+    } else {
+        my $method = '$Object->'.$attr.'()';
+        $method =~ s/->ISO\(\)$/->ISO( Timezone => 'user' )/;
+        $value = eval $method;
+        if ($@) {die "<b>Check your CalendarPopupFields config in etc/RT_SiteConfig.pm</b>.<br /><br />Failed to find \"$attr\" - ". $@};
+    }
+</%perl>
 	<strong><&|/l&><% $label_of{$attr} %></&>:</strong> <% $value %><br />
 % }
 
-<br />
-	</span>
+    <br />
+</span>
+% }
+    </div>
+</small>
+
 </div>
 
 <%init>
 use RTx::Calendar;
 
 my $today = $Date->strftime("%F");
+my $tomorrow = $Date->clone()->add(days => 1)->strftime("%F");
 
 my $TicketId;
 
diff --git a/html/Elements/MyCalendar b/html/Elements/MyCalendar
index a2b8aaa..4589bfb 100644
--- a/html/Elements/MyCalendar
+++ b/html/Elements/MyCalendar
@@ -14,21 +14,68 @@
 </thead>
 <tbody>
 <tr>
-% $date = $begin->clone;
-% while ($date <= $end) {
-%   my @classes = ();
-%   push @classes, "today"     if (DateTime->compare($today,     $date) == 0);
-%   push @classes, "yesterday" if (DateTime->compare($yesterday, $date) == 0);
-
-    <td class="<% @classes %>"><div class="inside-day">
+<%perl>
+my %week_ticket_position;
+my $day_of_week = 1;
+$date = $begin->clone;
+
+while ($date <= $end) {
+  my @classes = ();
+  push @classes, "today"     if (DateTime->compare($today,     $date) == 0);
+  push @classes, "yesterday" if (DateTime->compare($yesterday, $date) == 0);
+  push @classes, "weekday-$day_of_week";
+  for my $t ( @{ $Tickets->{ $date->strftime("%F") } || [] } ) {
+    # check if ticket was already displayed this week, if not, we need to find a
+    # position for it
+    unless ( grep { $week_ticket_position{$_}{id} eq $t->id } keys %week_ticket_position ) {
+      # new tickets should assume the first empty spot.
+      my $i = 1;
+      my $free_index = 0;
+      for my $index ( sort { $a <=> $b } keys %week_ticket_position ) {
+        if ( $week_ticket_position{$index}{id} eq "" ) {
+          $free_index = $i;
+          last;
+        }
+        $i++;
+      }
+      # if we found a free spot, we place the ticket there
+      if ( $free_index != 0 ) {
+        $week_ticket_position{$free_index}{id} = $t->id;
+        $week_ticket_position{$free_index}{TicketObj} = $t;
+      }
+      # if not, we add it to the end of the array
+      else {
+        $week_ticket_position{((scalar keys %week_ticket_position)+1)}{id} = $t->id;
+        $week_ticket_position{((scalar keys %week_ticket_position))}{TicketObj} = $t;
+      }
+    }
+  }
+</%perl>
+
+    <td class="<% join(' ', @classes) %>"><div class="inside-day">
       <div class="calendardate"><%$date->day%></div>
-
-%     for my $t ( @{ $Tickets{$date->strftime("%F")} } ) {
-        <& /Elements/CalendarEvent, Object => $t, Date => $date, DateTypes => \%DateTypes &>
+%      for my $index ( sort { $a <=> $b } keys %week_ticket_position ) {
+%       if ( grep { $_->id eq $week_ticket_position{$index}{id} }
+%                 @{ $Tickets->{ $date->strftime("%F") } || [] } ) {
+%         my $t = $week_ticket_position{$index}{TicketObj};
+        <& /Elements/CalendarEvent,
+          Object              => $t,
+          Date                => $date,
+          DateTypes           => \%DateTypes,
+          DayOfWeek           => $day_of_week,
+          TicketsSpanningDays => $TicketsSpanningDays,
+          WeekTicketPosition  => \%week_ticket_position,
+          CurrentPostion      => $index,
+        &>
+%       }
+%       else {
+%         # if there's no ticket for this position, we add an empty space
+             <div class="day"> </div>
+%       }
 %     }
-
     </div></td>
 % $date = $set->next($date);
+% $day_of_week = $day_of_week + 1;
 % }
 </tr>
 </tbody>
@@ -95,6 +142,5 @@ $Query .= RTx::Calendar::DatesClauses(\@Dates, $begin->strftime("%F"), $end->str
 
 $m->callback( CallbackName => 'BeforeFindTickets', ARGSRef => \%ARGS, QueryRef => \$Query, FormatRef => \$Format );
 
-my %Tickets = RTx::Calendar::FindTickets($session{'CurrentUser'}, $Query, \@Dates);
-
+my ($Tickets, $TicketsSpanningDays) = RTx::Calendar::FindTickets($session{'CurrentUser'}, $Query, \@Dates);
 </%INIT>
diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 49c5220..08791b4 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -34,14 +34,6 @@ sub LastDay {
     $day;
 }
 
-# we can't use RT::Date::Date because it uses gmtime
-# and we need localtime
-sub LocalDate {
-    my $ts = shift;
-    my ( $d, $m, $y ) = ( localtime($ts) )[ 3 .. 5 ];
-    sprintf "%4d-%02d-%02d", ( $y + 1900 ), ++$m, $d;
-}
-
 sub DatesClauses {
     my ( $Dates, $begin, $end ) = @_;
 
@@ -50,6 +42,30 @@ sub DatesClauses {
     my @DateClauses = map {
         "($_ >= '" . $begin . " 00:00:00' AND $_ <= '" . $end . " 23:59:59')"
     } @$Dates;
+
+    # All multiple days events are already covered on the query above
+    # The following code works for covering events that start before and ends
+    # after the selected period.
+    # Start and end fields of the multiple days must also be present on the
+    # format.
+    my $multiple_days_events = RT->Config->Get('CalendarMultipleDaysEvents');
+    for my $event ( keys %$multiple_days_events ) {
+        next unless
+            grep { $_ eq $multiple_days_events->{$event}{'Starts'} } @$Dates;
+        next unless
+            grep { $_ eq $multiple_days_events->{$event}{'Ends'} } @$Dates;
+        push @DateClauses,
+            "("
+            . $multiple_days_events->{$event}{Starts}
+            . " <= '"
+            . $end
+            . " 00:00:00' AND "
+            . $multiple_days_events->{$event}{Ends}
+            . " >= '"
+            . $begin
+            . " 23:59:59')";
+    }
+
     $clauses .= " AND " . " ( " . join( " OR ", @DateClauses ) . " ) "
         if @DateClauses;
 
@@ -59,49 +75,40 @@ sub DatesClauses {
 sub FindTickets {
     my ( $CurrentUser, $Query, $Dates, $begin, $end ) = @_;
 
+    my $multiple_days_events = RT->Config->Get('CalendarMultipleDaysEvents');
+    my @multiple_days_fields;
+    for my $event ( keys %$multiple_days_events ) {
+        next unless
+            grep { $_ eq $multiple_days_events->{$event}{'Starts'} } @$Dates;
+        next unless
+            grep { $_ eq $multiple_days_events->{$event}{'Ends'} } @$Dates;
+        for my $type ( keys %{ $multiple_days_events->{$event} } ) {
+            push @multiple_days_fields,
+                $multiple_days_events->{$event}{$type};
+        }
+    }
+
     $Query .= DatesClauses( $Dates, $begin, $end )
         if $begin and $end;
 
     my $Tickets = RT::Tickets->new($CurrentUser);
     $Tickets->FromSQL($Query);
-
+    $Tickets->OrderBy( FIELD => 'id', ORDER => 'ASC' );
     my %Tickets;
     my %AlreadySeen;
+    my %TicketsSpanningDays;
+    my %TicketsSpanningDaysAlreadySeen;
 
     while ( my $Ticket = $Tickets->Next() ) {
-
         # How to find the LastContacted date ?
+        # Find single day events fields
         for my $Date (@$Dates) {
-
             # $dateindex is the date to use as key in the Tickets Hash
             # in the YYYY-MM-DD format
             # Tickets are then groupd by date in the %Tickets hash
-            my $dateindex;
-            if ($Date =~ /^CF\./){
-                my $cf = $Date;
-                $cf =~ s/^CF\.\{(.*)\}/$1/;
-
-                my $CFDateValue = $Ticket->FirstCustomFieldValue($cf);
-                next unless $CFDateValue;
-                my $CustomFieldObj = RT::CustomField->new($CurrentUser);
-                $CustomFieldObj->LoadByName( Name => $cf );
-                my $CustomFieldObjType = $CustomFieldObj->Type;
-                my $DateObj            = RT::Date->new($CurrentUser);
-                if ( $CustomFieldObjType eq 'Date' ) {
-                    $DateObj->Set(
-                        Format   => 'unknown',
-                        Value    => $CFDateValue,
-                        Timezone => 'utc'
-                    );
-                } else {
-                    $DateObj->Set( Format => 'ISO', Value => $CFDateValue );
-                }
-                $dateindex = LocalDate( $DateObj->Unix );
-            } else {
-                my $DateObj = $Date . "Obj";
-                $dateindex = LocalDate( $Ticket->$DateObj->Unix );
-            }
-
+            my $dateindex_obj = GetDate( $Date, $Ticket, $CurrentUser );
+            next unless $dateindex_obj;
+            my $dateindex = $dateindex_obj->ISO( Time => 0, Timezone => 'user' );
             push @{ $Tickets{$dateindex } },
                 $Ticket
 
@@ -111,8 +118,91 @@ sub FindTickets {
                 or $AlreadySeen{ $dateindex }
                 {$Ticket}++;
         }
+
+        # Find spanning days of multiple days events
+        for my $event (sort keys %$multiple_days_events) {
+            next unless
+                grep { $_ eq $multiple_days_events->{$event}{'Starts'} } @$Dates;
+            next unless
+                grep { $_ eq $multiple_days_events->{$event}{'Ends'} } @$Dates;
+            my $starts_field = $multiple_days_events->{$event}{'Starts'};
+            my $ends_field   = $multiple_days_events->{$event}{'Ends'};
+            my $starts_date  = GetDate( $starts_field, $Ticket, $CurrentUser );
+            my $ends_date    = GetDate( $ends_field,   $Ticket, $CurrentUser );
+            next unless $starts_date and $ends_date;
+            # Loop through all days between start and end and add the ticket
+            # to it
+            my $current_date = RT::Date->new($CurrentUser);
+            $current_date->Set(
+                Format => 'unix',
+                Value => $starts_date->Unix,
+            );
+
+            my $end_date_unix = $ends_date->Unix;
+            my $first_day = 1;
+            while ( $current_date->Unix <= $end_date_unix )
+            {
+                my $dateindex = $current_date->ISO( Time => 0, Timezone => 'user' );
+
+                push @{ $TicketsSpanningDays{$dateindex} }, $Ticket->id
+                    unless $first_day
+                    || $TicketsSpanningDaysAlreadySeen{$dateindex}
+                    {$Ticket}++;
+                push @{ $Tickets{$dateindex } },
+                    $Ticket
+                    # if reminder, check it's refering to a ticket
+                    unless ( $Ticket->Type eq 'reminder'
+                    and not $Ticket->RefersTo->First )
+                    or $AlreadySeen{ $dateindex }
+                    {$Ticket}++;
+
+                $current_date->AddDay();
+                $first_day = 0;
+            }
+        }
+    }
+    if ( wantarray ) {
+        return ( \%Tickets, \%TicketsSpanningDays );
+    } else {
+        return \%Tickets;
+    }
+}
+
+sub GetDate {
+    my $date_field = shift;
+    my $Ticket = shift;
+    my $CurrentUser = shift;
+
+    unless ($date_field) {
+        $RT::Logger->debug("No date field provided. Using created date.");
+        $date_field = 'Created';
+    }
+
+    if ($date_field =~ /^CF\./){
+        my $cf = $date_field;
+        $cf =~ s/^CF\.\{(.*)\}/$1/;
+        my $CustomFieldObj = $Ticket->LoadCustomFieldByIdentifier($cf);
+        unless ($CustomFieldObj->id) {
+            RT->Logger->debug("$cf Custom Field is not available for this object.");
+            return;
+        }
+        my $CFDateValue = $Ticket->FirstCustomFieldValue($cf);
+        return unless $CFDateValue;
+        my $CustomFieldObjType = $CustomFieldObj->Type;
+        my $DateObj            = RT::Date->new($CurrentUser);
+        if ( $CustomFieldObjType eq 'Date' ) {
+            $DateObj->Set(
+                Format   => 'unknown',
+                Value    => $CFDateValue,
+            );
+        } else {
+            $DateObj->Set( Format => 'ISO', Value => $CFDateValue );
+        }
+        return $DateObj;
+    } else {
+        my $DateObj = $date_field . "Obj";
+        return $Ticket->$DateObj;
     }
-    return %Tickets;
 }
 
 #
diff --git a/static/css/calendar.css b/static/css/calendar.css
index 565a1e3..cfe7825 100644
--- a/static/css/calendar.css
+++ b/static/css/calendar.css
@@ -2,9 +2,17 @@
 table.rtxcalendar .day {
     position: relative;
     z-index: 1;
+    padding: 3px 3px 3px 6px;
+    margin-top: 4px;
+    margin-bottom: 4px;
+    width:120%;
+    height: 1.75rem;
+    z-index: 4;
 }
 
-
+table.rtxcalendar .day.last-day {
+    width: 100%;
+}
 
 table.rtxcalendar .day:hover {
     z-index: 5;
@@ -14,10 +22,10 @@ table.rtxcalendar .day span.tip {
     display: none;
     text-align: left;
 }
-table.rtxcalendar div.day:hover span.tip{
+table.rtxcalendar div.day div.event-info:hover span.tip{
     display: block;
     position: absolute;
-    top:12px; left:24px; width:350px;
+    top:1rem; left:24px; width:350px;
     border: 1px solid #555;
     background-color: #fff;
     padding: 4px;
@@ -25,6 +33,12 @@ table.rtxcalendar div.day:hover span.tip{
     color: #505050;
 }
 
+table.rtxcalendar td.weekday-7 div.day div.event-info:hover span.tip,
+table.rtxcalendar td.weekday-6 div.day div.event-info:hover span.tip,
+table.rtxcalendar td.weekday-5 div.day div.event-info:hover span.tip {
+    left: unset;
+    right: 24px;
+}
 
 /* For the full calendar */
 table.rtxcalendar {
@@ -142,3 +156,28 @@ a.calendar-toggle-sidebar.sidebar-off::before {
 .calendar-container {
     display: flow-root;
 }
+
+.event-icon {
+    float: left;
+    margin-right: 5px;
+}
+
+.event-info a.event-title {
+    overflow: hidden;
+    max-width: 100%;
+    left: 0px;
+    padding-left: 21px;
+    white-space: nowrap;
+    position: absolute;
+    color: white;
+}
+
+table.rtxcalendar .day.first-day {
+    border-radius: 5px 0 0 5px;
+}
+table.rtxcalendar .day.last-day {
+    border-radius: 0 5px 5px 0;
+}
+table.rtxcalendar .day.first-day.last-day {
+    border-radius: 5px;
+}
diff --git a/static/js/calendar.js b/static/js/calendar.js
index 5b84bc0..2c48449 100644
--- a/static/js/calendar.js
+++ b/static/js/calendar.js
@@ -1,3 +1,45 @@
+window.onresize = resizeCalendarEventTitles;
+jQuery(function() {
+    resizeCalendarEventTitles();
+});
+
+/*
+* Adjust the max-width of the event title according to the number spanning
+* days of an event for each week of the calendar (including MyCalendar
+* portlet) so it doesn't escape the event box.
+*/
+function resizeCalendarEventTitles() {
+    if (jQuery('.rtxcalendar').length == 0){
+        return;
+    }
+    var current_width = jQuery('.rtxcalendar')
+        .find('.inside-day').first().css('width').replace('px','');
+    jQuery('.rtxcalendar').find('tr').each(
+        function(i, tr){
+            var event_repetions_on_week = {};
+            /* Each event day (first and spanning) is marked with the
+            * data-object attribute in a format like ticket-123 */
+            jQuery(tr).find('[data-object]').each(function(j, event_day){
+                if (event_repetions_on_week[jQuery(event_day).attr('data-object')] == undefined){
+                    event_repetions_on_week[jQuery(event_day).attr('data-object')] = 1;
+                } else {
+                    event_repetions_on_week[jQuery(event_day).attr('data-object')]++;
+                }
+            })
+            for (var key in event_repetions_on_week){
+                // Find the title of the first day of the event and adjust the max-width
+                // we substract 22px to display the icon of the last day of the event
+                jQuery(tr).find('.first-day[data-object="' + key + '"]')
+                    .each(function(x, first_event_day){
+                        jQuery(first_event_day).find('.event-title')
+                        .css('max-width',
+                            ((event_repetions_on_week[key] * current_width)-22) + 'px');
+                    })
+            }
+        }
+    )
+}
+
 function changeCalendarMonth() {
     var month = jQuery('.changeCalendarMonth select[name="Month"]').val();
     var year = jQuery('.changeCalendarMonth select[name="Year"]').val();

commit afb77469de1483ec19219c5290119b5a4fa1fe40
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Tue Aug 29 20:54:29 2023 -0300

    Refactor calendar legend to show custom icons
    
    Calendar legend now shows custom icons; removed the hard coded icons
    for the default events.

diff --git a/html/Elements/Calendar b/html/Elements/Calendar
index 96229f3..9e08ad7 100644
--- a/html/Elements/Calendar
+++ b/html/Elements/Calendar
@@ -120,41 +120,10 @@ $ShowSidebar => 0
 </tr>
 </table>
 
-<table width="100%">
-% foreach my $legend (sort keys %legend) {
-    <tr>
-      <td align="right">
-        <img src="<%$RT::WebImagesURL%>/<%$legend%>.png" />
-      </td>
-      <td align="left">
-%       my $more = 0;
-%       foreach ( @{$legend{$legend}} ) {
-          <% $more++ ? ', ' : '' %>
-          <&|/l&><% $_ %></&>
-%       }
-      </td>
-    </tr>
-% }
-
-</table>
 </div>
 
 </&>
 
-<%ONCE>
-
-my %legend = (
-  'created'     => ['Created'],
-  'due'         => ['Due'],
-  'resolved'    => ['Resolved'],
-  'updated'     => ['Last Updated'],
-  'created_due' => ['Created','Due'],
-  'reminder'    => ['Reminders'],
-  'started'     => ['Started'],
-  'starts_due'  => ['Starts','Due'],
-);
-
-</%ONCE>
 </div>
 <%INIT>
 my $NotFirstAccess = $DECODED_ARGS->{NotFirstAccess};
diff --git a/html/Elements/CalendarSidebar b/html/Elements/CalendarSidebar
index 25b9918..bb04437 100644
--- a/html/Elements/CalendarSidebar
+++ b/html/Elements/CalendarSidebar
@@ -30,6 +30,23 @@
     </form>
     </&>
 
+    <&| /Widgets/TitleBox,
+      title => loc('Event Types'),
+      &>
+% foreach my $TranslatedLegend (sort keys %CalendarIconsTranslated) {
+      <span class="tip">
+        <span class="tipimg">
+          <img
+            src="<% $RT::WebImagesURL %>/<%
+            $CalendarIcons{$CalendarIconsTranslated{$TranslatedLegend}}|n %>" />
+        </span>
+        <span class="tiplegend">
+          <% $TranslatedLegend %>
+        </span>
+      </span>
+% }
+    </&>
+
 <&| /Widgets/TitleBox,
      title => loc('State Colors'),
      &>
@@ -51,3 +68,29 @@
     });
   });
 </script>
+
+<%init>
+my %CalendarIcons = RT->Config->Get('CalendarIcons');
+# Sort the legend after translation
+my %CalendarIconsTranslated;
+
+my %date = map { $_ => 1 } @Dates;
+
+foreach my $legend (sort { lc($a) cmp lc($b) } keys %CalendarIcons) {
+  # We might have multiple labels for the same icon
+  # such as "LastUpdated, CF.{Maintenance Date}"
+  # so we need to split them and translate them individually
+  my @LegendLabels = split '\s*,\s*', $legend;
+  next if grep { !$date{$_} } @LegendLabels;
+
+  my $LegendLabel = join ', ',
+    map {
+      my $label = $_;
+      $_ =~ s/^\s+|\s+$//g;
+      $_ =~ s/^CF\.\{(.*)\}/$1/;
+      $_ = 'Last Updated' if $_ eq 'LastUpdated';
+      loc($_)
+    } @LegendLabels;
+  $CalendarIconsTranslated{$LegendLabel} = $legend;
+}
+</%init>

commit 1889d3909dc1d33b2da2a870c1f229df72b9be0d
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Tue Aug 29 21:30:47 2023 -0300

    Add feature to customize icons of calendar events
    
    Since we now allow adding of custom fields to the calendar, we need to
    allow customizing of the icon of the event based on its type.
    
    If custom icon is not set to a Custom Field based event, then we will
    not show a standard icon for that.

diff --git a/html/Elements/CalendarEvent b/html/Elements/CalendarEvent
index 6d3d874..b8023d9 100644
--- a/html/Elements/CalendarEvent
+++ b/html/Elements/CalendarEvent
@@ -6,43 +6,7 @@ $DateTypes => undef
 <div class="day">
 <small>
 
-% if ($IsReminder and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today) {
-     <img src="<%$RT::WebImagesURL%>/reminder.png" />
-
-% } elsif ($DateTypes->{Resolved}
-%           and RTx::Calendar::LocalDate($Object->ResolvedObj->Unix) eq $today) {
-         <img src="<%$RT::WebImagesURL%>/resolved.png" />
-
-% } elsif ($DateTypes->{Starts} and $DateTypes->{Due}
-%           and RTx::Calendar::LocalDate($Object->StartsObj->Unix) eq $today and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today ) {
-    <img src="<%$RT::WebImagesURL%>/starts_due.png" />
-
-% } elsif ($DateTypes->{Due} and $DateTypes->{Created}
-%           and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today and RTx::Calendar::LocalDate($Object->CreatedObj->Unix) eq $today ) {
-    <img src="<%$RT::WebImagesURL%>/created_due.png" />
-
-% } elsif ($DateTypes->{Starts}
-%           and RTx::Calendar::LocalDate($Object->StartsObj->Unix) eq $today) {
-    <img src="<%$RT::WebImagesURL%>/starts.png" />
-
-% } elsif ($DateTypes->{Due}
-%           and RTx::Calendar::LocalDate($Object->DueObj->Unix) eq $today) {
-    <img src="<%$RT::WebImagesURL%>/due.png" />
-
-% } elsif ($DateTypes->{Created}
-%           and RTx::Calendar::LocalDate($Object->CreatedObj->Unix) eq $today) {
-    <img src="<%$RT::WebImagesURL%>/created.png" />
-
-% } elsif ($DateTypes->{Started}
-%           and RTx::Calendar::LocalDate($Object->StartedObj->Unix) eq $today) {
-    <img src="<%$RT::WebImagesURL%>/started.png" />
-
-% } elsif ($DateTypes->{LastUpdated}
-%           and RTx::Calendar::LocalDate($Object->LastUpdatedObj->Unix) eq $today) {
-    <img src="<%$RT::WebImagesURL%>/updated.png" />
-
-% }
-
+    <% RTx::Calendar::GetEventImg($Object, $today, $DateTypes, $IsReminder)|n %>
 	<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>"
 % if ( $CalendarStatusColorMap{$status} ) {
     style="color: <%$CalendarStatusColorMap{$status}%>;"
diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 926e29b..49c5220 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -161,6 +161,62 @@ sub SearchDefaultCalendar {
     }
 }
 
+sub GetEventImg {
+    my $Object      = shift;
+    my $CurrentDate = shift;
+    my $DateTypes   = shift;
+    my $IsReminder  = shift;
+    my $CurrentUser = shift;
+    my $EventIcon;
+    my %CalendarIcons = RT->Config->Get('CalendarIcons');
+
+CALENDAR_ICON:
+    for my $legend ( sort { (split /\s*,\s*/, $b) <=> (split /\s*,\s*/, $a) or ($a cmp $b) } keys %CalendarIcons ) {
+        if (   $legend eq 'Reminder'
+            && $IsReminder
+            && $Object->DueObj->ISO( Time => 0, Timezone => 'user' ) eq $CurrentDate )
+        {
+            $EventIcon = 'reminder.png';
+            last;
+        }
+
+        for my $DateField ( split /\s*,\s*/, $legend ) {
+            next CALENDAR_ICON unless $DateTypes->{$DateField};
+
+            if ( $DateField =~ /^CF\./ ) {
+                my $cf = $DateField;
+                $cf =~ s/^CF\.\{(.*)\}/$1/;
+                my $CustomFieldObj = $Object->LoadCustomFieldByIdentifier($cf);
+                next CALENDAR_ICON unless $CustomFieldObj->id;
+                my $DateValue = $Object->FirstCustomFieldValue($cf);
+                next CALENDAR_ICON unless $DateValue;
+                unless ( $CustomFieldObj->Type eq 'Date' ) {
+                    my $DateObj = RT::Date->new( $CurrentUser );
+                    $DateObj->Set( Format => 'ISO', Value => $DateValue );
+                    $DateValue = $DateObj->ISO( Time => 0, Timezone => 'user' );
+                }
+                next CALENDAR_ICON unless $DateValue eq $CurrentDate;
+            } else {
+                my $DateObj = $DateField . "Obj";
+                my $DateValue
+                    = $Object->$DateObj->ISO( Time => 0, Timezone => 'user' );
+                next CALENDAR_ICON unless $DateValue eq $CurrentDate;
+            }
+        }
+
+        # If we are here, it means that all comparissons are true
+        $EventIcon = $CalendarIcons{$legend};
+        last;
+    }
+
+    if ($EventIcon) {
+        return '<img src="' . $RT::WebImagesURL . '/' . $EventIcon . '" />';
+    } else {
+        return '';
+    }
+}
+
+
 1;
 
 __END__

commit bc4093657f5348cb5f089e96ace83b98cde337d7
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Tue Aug 29 21:26:35 2023 -0300

    Add feature to customize color of events on the calendar

diff --git a/html/Elements/CalendarEvent b/html/Elements/CalendarEvent
index 11d090d..6d3d874 100644
--- a/html/Elements/CalendarEvent
+++ b/html/Elements/CalendarEvent
@@ -43,7 +43,11 @@ $DateTypes => undef
 
 % }
 
-	<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>">
+	<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>"
+% if ( $CalendarStatusColorMap{$status} ) {
+    style="color: <%$CalendarStatusColorMap{$status}%>;"
+% }
+    >
            <% $Object->QueueObj->Name %> #<% $TicketId %>
            <% $display_owner ? 'by ' . $Object->OwnerObj->Name : '' %>
            <% length($Object->Subject) > 80 ? substr($Object->Subject, 0, 77) . "..." : $Object->Subject %></a></small><br />
@@ -86,6 +90,7 @@ my $TicketId;
 my $ticket;
 my $subject;
 my $IsReminder;
+my $status;
 
 if ($Object->Type eq 'reminder') {
     $IsReminder = 1;
@@ -93,12 +98,16 @@ if ($Object->Type eq 'reminder') {
 	$ticket   = $Object->RefersTo->First->TargetObj;
 	$TicketId = $ticket->Id;
 	$subject = $Object->Subject . " (" . $ticket->Subject . ")";
+        $status = $Object->Status;
     }
 } else {
     $TicketId = $Object->Id;
     $subject = $Object->Subject;
+    $status = $Object->Status;
 }
 
+my %CalendarStatusColorMap = RT->Config->Get('CalendarStatusColorMap');
+
 my $display_owner = $RT::CalendarDisplayOwner;
 $display_owner ||= RT->Config->Get('CalendarDisplayOwner')
     if RT->can('Config');
diff --git a/html/Elements/CalendarSidebar b/html/Elements/CalendarSidebar
index 629d7a2..25b9918 100644
--- a/html/Elements/CalendarSidebar
+++ b/html/Elements/CalendarSidebar
@@ -29,6 +29,16 @@
       </div>
     </form>
     </&>
+
+<&| /Widgets/TitleBox,
+     title => loc('State Colors'),
+     &>
+% my %ColorStatusMap = RT->Config->Get('CalendarStatusColorMap');
+% foreach my $Status (sort { lc($a) cmp lc($b) } keys %ColorStatusMap) {
+    <span style="color: <% $ColorStatusMap{$Status} %> !important;"><% $Status %></span><br />
+% }
+</&>
+
   </div>
 </div>
 

commit 2f43541fd915c6a719a8df2a14593a4b82707504
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Tue Aug 29 20:38:07 2023 -0300

    Add link to Download Calendar events
    
    Add link on the top right of the Calendar to download the events that
    are shown in the current view as a Spreadsheet.

diff --git a/html/Elements/Calendar b/html/Elements/Calendar
index 61702db..96229f3 100644
--- a/html/Elements/Calendar
+++ b/html/Elements/Calendar
@@ -22,6 +22,8 @@ $ShowSidebar => 0
 <&| /Widgets/TitleBox,
      title => loc('Calendar for [_1] [_2]', $rtdate->GetMonth($Month), $Year),
      title_href => "?Month=$Month&Year=$Year&$QueryString",
+     titleright => loc('Download Spreadsheet'),
+     titleright_href => $RT::WebPath. "/Search/Results.tsv?". $DownloadQueryString
      &>
 
 <table width="100%">
@@ -220,21 +222,6 @@ my $set = DateTime::Set->from_recurrence(
     next => sub { $_[0]->truncate( to => 'day' )->add( days => 1 ) }
 );
 
-my $QueryString =
-      $m->comp(
-        '/Elements/QueryString',
-        Query   => $BaseQuery,
-        FilterOnStatus => \@FilterOnStatus,
-        Format  => $Format,
-        Order   => $Order,
-        OrderBy => $OrderBy,
-        Rows    => $RowsPerPage,
-        NotFirstAccess => $NotFirstAccess,
-      )
-      if ($Query);
-
-$QueryString ||= 'NewQuery=1&NotFirstAccess=1';
-
 # Default Query and Format
 my $TempFormat = "__Starts__ __Due__";
 my $TempQuery = "( Status = 'new' OR Status = 'open' OR Status = 'stalled')
@@ -249,6 +236,21 @@ if ( my $Search = RTx::Calendar::SearchDefaultCalendar($session{CurrentUser}) )
 # we overide them if needed
 $TempQuery  = $Query  if $Query;
 $TempFormat = $Format if $Format;
+$Format = $TempFormat unless $Format;
+
+my $QueryString =
+      $m->comp(
+        '/Elements/QueryString',
+        Query   => $BaseQuery,
+        FilterOnStatus => \@FilterOnStatus,
+        Format  => $Format,
+        Order   => $Order,
+        OrderBy => $OrderBy,
+        Rows    => $RowsPerPage,
+        NotFirstAccess => $NotFirstAccess,
+      );
+
+$QueryString ||= 'NewQuery=1&NotFirstAccess=1';
 
 # we search all date types in Format string
 my @CoreDates    = grep { $TempFormat =~ m/__${_}(Relative)?__/ } @DateTypes;
@@ -284,4 +286,13 @@ $m->callback( CallbackName => 'BeforeFindTickets', ARGSRef => \%ARGS, QueryRef =
 
 my %Tickets = RTx::Calendar::FindTickets($session{'CurrentUser'}, $TempQuery, \@Dates, $date->strftime("%F"), $end->strftime("%F"));
 
+my $DownloadQueryString =
+      $m->comp(
+        '/Elements/QueryString',
+        Query   => $TempQuery,
+        Format  => $Format,
+        Order   => $Order,
+        OrderBy => $OrderBy,
+      );
+
 </%INIT>

commit cfc4489af1f557feb808ac5165a0576d604af1a5
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Tue Aug 29 21:51:24 2023 -0300

    Improve calendar help
    
    Add custom field examples.
    
    Fixed title capitalization.
    
    Updated header level to decrease title size.
    
    Fixed some typos.

diff --git a/html/Elements/CalendarFooter b/html/Elements/CalendarFooter
index a8244f0..86b7ccb 100644
--- a/html/Elements/CalendarFooter
+++ b/html/Elements/CalendarFooter
@@ -1,34 +1,36 @@
 <&| /Widgets/TitleBox, title => loc('Help') &>
 
-<h3><&|/l&>displaying reminders</&>:</h3>
+<h4 class="mt-2"><&|/l&>Displaying reminders</&>:</h4>
 <p>
 <&|/l_unsafe, qq{<a href="$RT::WebPath/Search/Edit.html">} . loc("Advanced") . '</a>' &>
-If you want to have reminders in a search you need to go to [_1] tab
-and add something to the Query like that:
+If you want to see reminders on the calendar, you need to go to the [_1] tab
+of your query and explicitly add the following clause to it:
 </&>
  <pre>
    AND ( Type = 'ticket' OR Type = 'reminder' )
 </pre>
 </p>
 
-<h3><&|/l&>displaying other kind of dates</&>:</h3>
+<h4><&|/l&>Displaying other kind of dates</&>:</h4>
 <p>
 <&|/l_unsafe, qq{<a href="$RT::WebPath/Search/Build.html">} . loc("Query Builder") . '</a>'&>
-By default RTx::Calendar display Due and Starts dates. You can select other
-kind of events you want with the Display Columns section in the [_1].
-The following one will display the two latter and LastUpdated dates:
+By default, RTx::Calendar displays Due and Starts dates. You can select other
+date fields with the Display Columns section in the [_1].
+The following format will display the two additional date fields, LastUpdated and a
+custom field called Maintenance Date:
 </&>
 <pre>
   '<small>__Due__</small>',
   '<small>__Starts__</small>',
-  '<small>__LastUpdated__</small>'
+  '<small>__LastUpdated__</small>',
+  '<small>__CustomField.{Maintenance Date}__</small>'
 </pre>
 </p>
 
-<h3><&|/l&>changing the default query</&>:</h3>
+<h4><&|/l&>Changing the default query</&>:</h4>
 <p>
 <&|/l_unsafe, qq{<a href="$RT::WebPath/Search/Build.html">} . loc("Query Builder") . '</a>'&>
-You can change the default Query of Calendar.html and MyCalendar
+You can change the default query used by Calendar.html and the MyCalendar
 portlet by saving a query with the name <code>calendar</code> in the [_1].
 </&>
 </p>

commit 0ae7e0d929f7079a4c6e1cfce3f0e849f881005d
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Tue Aug 29 19:22:28 2023 -0300

    Allow Date and Date/Time Custom Fields to be used in Calendar
    
    It's possible now to add __CustomField.{Custom Field Name}__ to search
    format so it will appear on calendar.

diff --git a/html/Elements/Calendar b/html/Elements/Calendar
index 509bd0d..61702db 100644
--- a/html/Elements/Calendar
+++ b/html/Elements/Calendar
@@ -251,7 +251,23 @@ $TempQuery  = $Query  if $Query;
 $TempFormat = $Format if $Format;
 
 # we search all date types in Format string
-my @Dates = grep { $TempFormat =~ m/__${_}(Relative)?__/ } @DateTypes;
+my @CoreDates    = grep { $TempFormat =~ m/__${_}(Relative)?__/ } @DateTypes;
+my @CustomFields = ( $TempFormat =~ /__(CustomField\.\{.*\})__/g );
+my @DateCustomFields;
+
+for my $CustomField (@CustomFields) {
+    my $LintCustomField = $CustomField;
+    $LintCustomField =~ s/CustomField\.\{(.*)\}/$1/;
+    my $CustomFieldObj = RT::CustomField->new( RT->SystemUser );
+    $CustomFieldObj->LoadByName( Name => $LintCustomField );
+    push @DateCustomFields, $CustomField
+        if $CustomFieldObj->id
+        && ( $CustomFieldObj->Type eq 'Date'
+        || $CustomFieldObj->Type eq 'DateTime' );
+}
+
+my @Dates = (@CoreDates, @DateCustomFields);
+ at Dates = map { $_ =~ s/^CustomField\.(.*)$/CF.$1/; $_ } @Dates;
 
 # used to display or not a date in Element/CalendarEvent
 my %DateTypes = map { $_ => 1 } @Dates;
diff --git a/html/Elements/CalendarEvent b/html/Elements/CalendarEvent
index 5b8a6c5..11d090d 100644
--- a/html/Elements/CalendarEvent
+++ b/html/Elements/CalendarEvent
@@ -60,6 +60,9 @@ $DateTypes => undef
 %
 %    if ($attr =~ /(.*)->ISO$/ and $Object->$1->Unix <= 0) {
 %        $value = '-';
+%    } elsif ($attr =~ /CustomField\.\{(.*)\}$/) {
+%        my $cf = $1;
+%        $value = $Object->FirstCustomFieldValue($cf);
 %    } else {
 %        my $method = '$Object->'.$attr.'()';
 %        $method =~ s/->ISO\(\)$/->ISO( Timezone => 'user' )/;
@@ -123,6 +126,7 @@ for my $field (@display_fields) {
     my $label = $field;
     $label =~ s'Obj-.(?:AsString|Name|ISO)''g;
     $label =~ s'-\>MemberEmailAddressesAsString''g;
+    $label =~ s/CustomField\.\{(.*)\}/$1/g;
     $label_of{$field} = $label;
 }
 
diff --git a/html/Elements/MyCalendar b/html/Elements/MyCalendar
index 175c7e9..a2b8aaa 100644
--- a/html/Elements/MyCalendar
+++ b/html/Elements/MyCalendar
@@ -71,8 +71,23 @@ if ( my $Search = RTx::Calendar::SearchDefaultCalendar($session{CurrentUser}) )
 }
 
 # we search all date types in Format string
-my @Dates = grep { $Format =~ m/__${_}(Relative)?__/ } @DateTypes;
+my @CoreDates    = grep { $Format =~ m/__${_}(Relative)?__/ } @DateTypes;
+my @CustomFields = ( $Format =~ /__(CustomField\.\{.*\})__/g );
+my @DateCustomFields;
+
+for my $CustomField (@CustomFields) {
+    my $LintCustomField = $CustomField;
+    $LintCustomField =~ s/CustomField\.\{(.*)\}/$1/;
+    my $CustomFieldObj = RT::CustomField->new( RT->SystemUser );
+    $CustomFieldObj->LoadByName( Name => $LintCustomField );
+    push @DateCustomFields, $CustomField
+        if $CustomFieldObj->id
+        && ( $CustomFieldObj->Type eq 'Date'
+        || $CustomFieldObj->Type eq 'DateTime' );
+}
 
+my @Dates = (@CoreDates, @DateCustomFields);
+ at Dates = map { $_ =~ s/^CustomField\.(.*)$/CF.$1/; $_ } @Dates;
 # used to display or not a date in Element/CalendarEvent
 my %DateTypes = map { $_ => 1 } @Dates;
 
diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 45c79fe..926e29b 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -72,14 +72,43 @@ sub FindTickets {
 
         # How to find the LastContacted date ?
         for my $Date (@$Dates) {
-            my $DateObj = $Date . "Obj";
-            push @{ $Tickets{ LocalDate( $Ticket->$DateObj->Unix ) } },
+
+            # $dateindex is the date to use as key in the Tickets Hash
+            # in the YYYY-MM-DD format
+            # Tickets are then groupd by date in the %Tickets hash
+            my $dateindex;
+            if ($Date =~ /^CF\./){
+                my $cf = $Date;
+                $cf =~ s/^CF\.\{(.*)\}/$1/;
+
+                my $CFDateValue = $Ticket->FirstCustomFieldValue($cf);
+                next unless $CFDateValue;
+                my $CustomFieldObj = RT::CustomField->new($CurrentUser);
+                $CustomFieldObj->LoadByName( Name => $cf );
+                my $CustomFieldObjType = $CustomFieldObj->Type;
+                my $DateObj            = RT::Date->new($CurrentUser);
+                if ( $CustomFieldObjType eq 'Date' ) {
+                    $DateObj->Set(
+                        Format   => 'unknown',
+                        Value    => $CFDateValue,
+                        Timezone => 'utc'
+                    );
+                } else {
+                    $DateObj->Set( Format => 'ISO', Value => $CFDateValue );
+                }
+                $dateindex = LocalDate( $DateObj->Unix );
+            } else {
+                my $DateObj = $Date . "Obj";
+                $dateindex = LocalDate( $Ticket->$DateObj->Unix );
+            }
+
+            push @{ $Tickets{$dateindex } },
                 $Ticket
 
                 # if reminder, check it's refering to a ticket
                 unless ( $Ticket->Type eq 'reminder'
                 and not $Ticket->RefersTo->First )
-                or $AlreadySeen{ LocalDate( $Ticket->$DateObj->Unix ) }
+                or $AlreadySeen{ $dateindex }
                 {$Ticket}++;
         }
     }

commit a0311dc5d6428890e70e10bc2b6f017e51995380
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Tue Aug 29 21:16:42 2023 -0300

    Add sidebar to calendar
    
    The sidebar holds the legend, filters, and other future controls.

diff --git a/html/Elements/Calendar b/html/Elements/Calendar
index af630b2..509bd0d 100644
--- a/html/Elements/Calendar
+++ b/html/Elements/Calendar
@@ -1,6 +1,24 @@
 <%args>
+$ShowSidebar => 0
 </%args>
 
+<div class="calendar-container">
+
+% if ($ShowSidebar) {
+  <& /Elements/CalendarSidebar,
+    BaseQuery => $BaseQuery,
+    Month => $Month,
+    Year => $Year,
+    Format => $Format,
+    Order => $Order,
+    OrderBy => $OrderBy,
+    RowsPerPage => $RowsPerPage,
+    FilterOnStatus => \@FilterOnStatus,
+    Dates => \@Dates,
+  &>
+% }
+
+<div class="calendar-content">
 <&| /Widgets/TitleBox,
      title => loc('Calendar for [_1] [_2]', $rtdate->GetMonth($Month), $Year),
      title_href => "?Month=$Month&Year=$Year&$QueryString",
@@ -117,6 +135,7 @@
 % }
 
 </table>
+</div>
 
 </&>
 
@@ -134,7 +153,9 @@ my %legend = (
 );
 
 </%ONCE>
+</div>
 <%INIT>
+my $NotFirstAccess = $DECODED_ARGS->{NotFirstAccess};
 my $Month = $DECODED_ARGS->{Month} || (localtime)[4];
 my $Year = $DECODED_ARGS->{Year}  || (localtime)[5] + 1900;
 my $Query = $DECODED_ARGS->{Query};
@@ -143,7 +164,36 @@ my $Order = $DECODED_ARGS->{Order};
 my $OrderBy = $DECODED_ARGS->{OrderBy};
 my $RowsPerPage = $DECODED_ARGS->{RowsPerPage};
 my $NewQuery = $DECODED_ARGS->{NewQuery};
+my $BaseQuery = $DECODED_ARGS->{BaseQuery};
+my $FilterOnStatusClear = $DECODED_ARGS->{FilterOnStatusClear};
+my @FilterOnStatus;
+if ( $DECODED_ARGS->{NewFilterOnStatus} ) {
+  if ( ref $DECODED_ARGS->{NewFilterOnStatus} eq 'ARRAY' ) {
+    @FilterOnStatus = @{$DECODED_ARGS->{NewFilterOnStatus}};
+  }
+  else {
+    push @FilterOnStatus, $DECODED_ARGS->{NewFilterOnStatus};
+  }
+}
+# This comes from the month changing form and link
+elsif ( $DECODED_ARGS->{FilterOnStatus} ) {
+  if ( ref $DECODED_ARGS->{FilterOnStatus} eq 'ARRAY' ) {
+    @FilterOnStatus = @{$DECODED_ARGS->{FilterOnStatus}};
+  }
+  else {
+    push @FilterOnStatus, $DECODED_ARGS->{FilterOnStatus};
+  }
+} else {
+  @FilterOnStatus = @{RT->Config->Get('CalendarFilterDefaultStatuses')}
+    unless $NotFirstAccess;
+}
+$NotFirstAccess = 1;
 
+if ($FilterOnStatusClear) {
+  $Query = $BaseQuery if $BaseQuery;
+  @FilterOnStatus = ();
+}
+$BaseQuery ||= $Query;
 my $title = loc("Calendar");
 
 my @DateTypes = qw/Created Starts Started Due LastUpdated Resolved/;
@@ -173,15 +223,17 @@ my $set = DateTime::Set->from_recurrence(
 my $QueryString =
       $m->comp(
         '/Elements/QueryString',
-        Query   => $Query,
+        Query   => $BaseQuery,
+        FilterOnStatus => \@FilterOnStatus,
         Format  => $Format,
         Order   => $Order,
         OrderBy => $OrderBy,
-        Rows    => $RowsPerPage
+        Rows    => $RowsPerPage,
+        NotFirstAccess => $NotFirstAccess,
       )
       if ($Query);
 
-$QueryString ||= 'NewQuery=1';
+$QueryString ||= 'NewQuery=1&NotFirstAccess=1';
 
 # Default Query and Format
 my $TempFormat = "__Starts__ __Due__";
@@ -206,6 +258,12 @@ my %DateTypes = map { $_ => 1 } @Dates;
 
 $TempQuery .= RTx::Calendar::DatesClauses(\@Dates, $date->strftime("%F"), $end->strftime("%F"));
 
+if (@FilterOnStatus) {
+  my $StatusClause = join " OR ", map { "Status = '$_'" } @FilterOnStatus;
+  $TempQuery .= " AND " if $TempQuery;
+  $TempQuery .= "($StatusClause)";
+}
+
 $m->callback( CallbackName => 'BeforeFindTickets', ARGSRef => \%ARGS, QueryRef => \$TempQuery, FormatRef => \$TempFormat );
 
 my %Tickets = RTx::Calendar::FindTickets($session{'CurrentUser'}, $TempQuery, \@Dates, $date->strftime("%F"), $end->strftime("%F"));
diff --git a/html/Elements/CalendarSidebar b/html/Elements/CalendarSidebar
new file mode 100644
index 0000000..629d7a2
--- /dev/null
+++ b/html/Elements/CalendarSidebar
@@ -0,0 +1,43 @@
+<%args>
+ at FilterOnStatus => undef
+ at Dates => undef
+</%args>
+<div class="calendar-sidebar-toggle-content">
+  <a title="Toggle Filter" href="javascript:;" class="calendar-toggle-sidebar"></a>
+  <div class="calendar-sidebar">
+    <&| /Widgets/TitleBox,
+      title => loc('Filter on Status'),
+      class => 'calendar-filter-status-box',
+      &>
+
+    <form id="FilterOnStatusForm"
+     method="post">
+      <select name="NewFilterOnStatus" id="NewFilterOnStatus"
+        class="selectpicker form-control filteronstatus mt-3 mb-3" multiple="multiple" size="6">
+% for my $Status (sort {lc(loc($a)) cmp lc(loc($b))} @{RT->Config->Get('CalendarFilterStatuses')}) {
+        <option value="<% $Status %>"
+% if (@FilterOnStatus && $FilterOnStatus[0]) {
+      <% (grep { $Status eq $_ } @FilterOnStatus) ? 'selected="selected"':''%>
+% }
+        ><% loc($Status) %></option>
+% }
+      </select>
+      <div class="text-center">
+        <input type="submit" value="<% loc('Filter') %>" class="mr-2 button btn btn-primary" />
+        <button type="submit" id="FilterOnStatusClear" name="FilterOnStatusClear"
+          value="1" class="button btn btn-primary form-control"><% loc('Clear Filter') %></button>
+      </div>
+    </form>
+    </&>
+  </div>
+</div>
+
+<script type="text/javascript">
+  jQuery(document).ready(function() {
+    jQuery('.calendar-toggle-sidebar').click(function() {
+      jQuery('.calendar-sidebar').toggle();
+      jQuery('.calendar-sidebar-toggle-content,.calendar-toggle-sidebar').toggleClass('sidebar-off');
+      jQuery('.calendar-content').toggleClass('sidebar-off');
+    });
+  });
+</script>
diff --git a/html/Elements/CalendarWithSidebar b/html/Elements/CalendarWithSidebar
new file mode 100644
index 0000000..7cd30c5
--- /dev/null
+++ b/html/Elements/CalendarWithSidebar
@@ -0,0 +1,3 @@
+<& /Elements/Calendar,
+  ShowSidebar => 1,
+&>
diff --git a/html/Search/Calendar.html b/html/Search/Calendar.html
index edad94b..c12b25d 100644
--- a/html/Search/Calendar.html
+++ b/html/Search/Calendar.html
@@ -1,4 +1,6 @@
 <& /Elements/Header, Title => loc("Calendar") &>
 <& /Elements/Tabs &>
-<& /Elements/Calendar &>
+<& /Elements/Calendar,
+  ShowSidebar => 1,
+&>
 <& /Elements/CalendarFooter &>
diff --git a/static/css/calendar.css b/static/css/calendar.css
index 4adfc4a..565a1e3 100644
--- a/static/css/calendar.css
+++ b/static/css/calendar.css
@@ -72,3 +72,73 @@ table.rtxcalendar td.yesterday {
 table.rtxcalendar td.aweekago {
     border-bottom: none;
 }
+
+.calendar-sidebar {
+    padding-right: 0.5rem;
+}
+
+.calendar-content {
+    width: auto;
+}
+
+.calendar-sidebar .tipimg {
+    display: table-cell;
+    width: 25px;
+}
+.calendar-sidebar .tiplegend {
+    display: table-cell;
+}
+.calendar-sidebar .tip {
+    display: table-row;
+}
+
+a.calendar-toggle-sidebar,
+a.calendar-toggle-sidebar.off {
+    background: none !important;
+    width: 0px !important;
+    position: relative;
+    float: right;
+    top: 300px;
+    margin-right: 10px;
+}
+a.calendar-toggle-sidebar::before {
+    content: '';
+    display: block;
+    width: 8px;
+    height: 8px;
+    border: solid #B0B3BC;
+    border-width: 0 2px 2px 0;
+    -webkit-transform: rotate(135deg);
+    -ms-transform: rotate(135deg);
+    transform: rotate(135deg);
+    transition: transform .25s;
+    position: absolute;
+    top: 1.2em;
+}
+a.calendar-toggle-sidebar.sidebar-off::before {
+    transform: rotate(315deg);
+}
+.calendar-sidebar-toggle-content {
+    float: left;
+    width: 230px;
+    margin-top: 20px;
+}
+.calendar-sidebar-toggle-content.sidebar-off {
+    width:unset;
+}
+.calendar-content.sidebar-off {
+    margin-left: 20px;
+}
+.filteronstatus {
+    width: 100% !important;
+}
+.calendar-filter-status-box {
+    margin-top: 0px;
+}
+.calendar-sidebar {
+    margin-right: 10px;
+}
+
+.calendar-container {
+    display: flow-root;
+}

commit f10215a871ec8d1569bffda969f4c65c3c89938f
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Fri Sep 15 15:30:04 2023 -0300

    Move calendar to Element file
    
    Calendar can now be used as a dashboard portlet.

diff --git a/html/Search/Calendar.html b/html/Elements/Calendar
similarity index 80%
copy from html/Search/Calendar.html
copy to html/Elements/Calendar
index a40ba73..af630b2 100644
--- a/html/Search/Calendar.html
+++ b/html/Elements/Calendar
@@ -1,26 +1,10 @@
 <%args>
-$Month => (localtime)[4]
-$Year => (localtime)[5] + 1900
-$Query => undef
-$Format => undef
-$Order => undef
-$OrderBy => undef
-$RowsPerPage => undef
-$NewQuery => 0
 </%args>
 
-<& /Elements/Header, Title => $title &>
-
-% if ( $m->comp_exists( '/Ticket/Elements/Tabs' ) ) {
-<& /Ticket/Elements/Tabs,
-    current_tab => "Search/Calendar.html?$QueryString",
-    Title => $title &>
-% } else {
-    <& /Elements/Tabs &>
-% }
-
 <&| /Widgets/TitleBox,
-     title => loc('Calendar for [_1] [_2]', $rtdate->GetMonth($Month), $Year) &>
+     title => loc('Calendar for [_1] [_2]', $rtdate->GetMonth($Month), $Year),
+     title_href => "?Month=$Month&Year=$Year&$QueryString",
+     &>
 
 <table width="100%">
 <tr>
@@ -30,7 +14,7 @@ $NewQuery => 0
 %    $PYear--;
 %    $PMonth = 11;
 % }
-<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$PMonth%>&Year=<%$PYear%>&<%$QueryString%>">« <%$rtdate->GetMonth($PMonth)%></a>
+<a href="?Month=<%$PMonth%>&Year=<%$PYear%>&<%$QueryString%>">« <%$rtdate->GetMonth($PMonth)%></a>
 </td>
 <th align="center">
   <font size="+1"><% $rtdate->GetMonth($Month). " $Year" %></font>
@@ -41,7 +25,7 @@ $NewQuery => 0
 %    $NYear++;
 %    $NMonth = 0;
 % }
-<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$NMonth%>&Year=<%$NYear%>&<%$QueryString%>"><%$rtdate->GetMonth($NMonth)%> »</a>
+<a href="?Month=<%$NMonth%>&Year=<%$NYear%>&<%$QueryString%>"><%$rtdate->GetMonth($NMonth)%> »</a>
 </td>
 </tr>
 </table>
@@ -87,31 +71,31 @@ $NewQuery => 0
 <table width="100%">
 <tr>
 <td align="left">
-<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$PMonth%>&Year=<%$PYear%>&<%$QueryString%>">« <%$rtdate->GetMonth($PMonth)%></a>
+<a href="?Month=<%$PMonth%>&Year=<%$PYear%>&<%$QueryString%>">« <%$rtdate->GetMonth($PMonth)%></a>
 </td>
 
 <td valign="top" align="center">
-<form action="<%$RT::WebPath%>/Search/Calendar.html?<%$QueryString%>" method="post">
+<form method="post" class="changeCalendarMonth" onsubmit="changeCalendarMonth()">
 
-<select name="Month" class="selectpicker">
+<select name="Month" class="selectpicker form-control col-3">
 % for (0..11) {
 <option value="<%$_%>" <% $_ == $Month ? 'selected' : ''%> ><%$rtdate->GetMonth($_)%></option>
 % }
 </select>
 % my $year = (localtime)[5] + 1900;
-<select name="Year" class="selectpicker">
+<select name="Year" class="selectpicker form-control col-3">
 % for ( ($year-5) .. ($year+5)) {
 <option value="<%$_%>" <% $_ == $Year ? 'selected' : ''%>><%$_%></option>
 % }
 </select>
-
+<input type="hidden" id="querystring" value="<% $QueryString|n %>" />
 <input type="submit" value="<% loc('Submit') %>" class="button btn btn-primary" />
 
 </form>
 </td>
 
 <td align="right">
-<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$NMonth%>&Year=<%$NYear%>&<%$QueryString%>"><%$rtdate->GetMonth($NMonth)%> »</a>
+<a href="?Month=<%$NMonth%>&Year=<%$NYear%>&<%$QueryString%>"><%$rtdate->GetMonth($NMonth)%> »</a>
 </td>
 </tr>
 </table>
@@ -151,6 +135,15 @@ my %legend = (
 
 </%ONCE>
 <%INIT>
+my $Month = $DECODED_ARGS->{Month} || (localtime)[4];
+my $Year = $DECODED_ARGS->{Year}  || (localtime)[5] + 1900;
+my $Query = $DECODED_ARGS->{Query};
+my $Format = $DECODED_ARGS->{Format};
+my $Order = $DECODED_ARGS->{Order};
+my $OrderBy = $DECODED_ARGS->{OrderBy};
+my $RowsPerPage = $DECODED_ARGS->{RowsPerPage};
+my $NewQuery = $DECODED_ARGS->{NewQuery};
+
 my $title = loc("Calendar");
 
 my @DateTypes = qw/Created Starts Started Due LastUpdated Resolved/;
diff --git a/html/Search/Calendar.html b/html/Search/Calendar.html
index a40ba73..edad94b 100644
--- a/html/Search/Calendar.html
+++ b/html/Search/Calendar.html
@@ -1,220 +1,4 @@
-<%args>
-$Month => (localtime)[4]
-$Year => (localtime)[5] + 1900
-$Query => undef
-$Format => undef
-$Order => undef
-$OrderBy => undef
-$RowsPerPage => undef
-$NewQuery => 0
-</%args>
-
-<& /Elements/Header, Title => $title &>
-
-% if ( $m->comp_exists( '/Ticket/Elements/Tabs' ) ) {
-<& /Ticket/Elements/Tabs,
-    current_tab => "Search/Calendar.html?$QueryString",
-    Title => $title &>
-% } else {
-    <& /Elements/Tabs &>
-% }
-
-<&| /Widgets/TitleBox,
-     title => loc('Calendar for [_1] [_2]', $rtdate->GetMonth($Month), $Year) &>
-
-<table width="100%">
-<tr>
-<td align="left">
-% my ($PMonth, $PYear) = ($Month - 1, $Year);
-% if ($PMonth < 0) {
-%    $PYear--;
-%    $PMonth = 11;
-% }
-<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$PMonth%>&Year=<%$PYear%>&<%$QueryString%>">« <%$rtdate->GetMonth($PMonth)%></a>
-</td>
-<th align="center">
-  <font size="+1"><% $rtdate->GetMonth($Month). " $Year" %></font>
-</th>
-<td align="right">
-% my ($NMonth, $NYear) = ($Month + 1, $Year);
-% if ($NMonth > 11) {
-%    $NYear++;
-%    $NMonth = 0;
-% }
-<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$NMonth%>&Year=<%$NYear%>&<%$QueryString%>"><%$rtdate->GetMonth($NMonth)%> »</a>
-</td>
-</tr>
-</table>
-
-<table class="rtxcalendar">
-
-<thead>
-<tr>
-% for ( @{$week{$weekstart}} ) {
-<th width="14%"><%$rtdate->GetWeekday($_)%></th>
-% }
-</tr>
-</thead>
-
-<tbody>
-<tr>
-% while ($date <= $end) {
-%   my @classes = ();
-%   push @classes, "offmonth"  if $date->month != ($Month + 1);
-%   push @classes, "today"     if (DateTime->compare($today,     $date) == 0);
-%   push @classes, "yesterday" if (DateTime->compare($yesterday, $date) == 0);
-%   push @classes, "aweekago"  if (DateTime->compare($aweekago,  $date) == 0);
-
-    <td class="<% @classes %>"><div class="inside-day">
-      <div class="calendardate"><%$date->day%></div>
-
-%     for my $t ( @{ $Tickets{$date->strftime("%F")} } ) {
-        <& /Elements/CalendarEvent, Object => $t, Date => $date, DateTypes => \%DateTypes &>
-%     }
-
-    </div></td>
-
-%   $date = $set->next($date);
-%   if ( $date->day_of_week == $startday_of_week ) {
-      </tr><tr>
-%   }
-
-% }
-</tr>
-</tbody>
-</table>
-
-<table width="100%">
-<tr>
-<td align="left">
-<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$PMonth%>&Year=<%$PYear%>&<%$QueryString%>">« <%$rtdate->GetMonth($PMonth)%></a>
-</td>
-
-<td valign="top" align="center">
-<form action="<%$RT::WebPath%>/Search/Calendar.html?<%$QueryString%>" method="post">
-
-<select name="Month" class="selectpicker">
-% for (0..11) {
-<option value="<%$_%>" <% $_ == $Month ? 'selected' : ''%> ><%$rtdate->GetMonth($_)%></option>
-% }
-</select>
-% my $year = (localtime)[5] + 1900;
-<select name="Year" class="selectpicker">
-% for ( ($year-5) .. ($year+5)) {
-<option value="<%$_%>" <% $_ == $Year ? 'selected' : ''%>><%$_%></option>
-% }
-</select>
-
-<input type="submit" value="<% loc('Submit') %>" class="button btn btn-primary" />
-
-</form>
-</td>
-
-<td align="right">
-<a href="<%$RT::WebPath%>/Search/Calendar.html?Month=<%$NMonth%>&Year=<%$NYear%>&<%$QueryString%>"><%$rtdate->GetMonth($NMonth)%> »</a>
-</td>
-</tr>
-</table>
-
-<table width="100%">
-% foreach my $legend (sort keys %legend) {
-    <tr>
-      <td align="right">
-        <img src="<%$RT::WebImagesURL%>/<%$legend%>.png" />
-      </td>
-      <td align="left">
-%       my $more = 0;
-%       foreach ( @{$legend{$legend}} ) {
-          <% $more++ ? ', ' : '' %>
-          <&|/l&><% $_ %></&>
-%       }
-      </td>
-    </tr>
-% }
-
-</table>
-
-</&>
-
-<%ONCE>
-
-my %legend = (
-  'created'     => ['Created'],
-  'due'         => ['Due'],
-  'resolved'    => ['Resolved'],
-  'updated'     => ['Last Updated'],
-  'created_due' => ['Created','Due'],
-  'reminder'    => ['Reminders'],
-  'started'     => ['Started'],
-  'starts_due'  => ['Starts','Due'],
-);
-
-</%ONCE>
-<%INIT>
-my $title = loc("Calendar");
-
-my @DateTypes = qw/Created Starts Started Due LastUpdated Resolved/;
-
-my $rtdate = RT::Date->new($session{'CurrentUser'});
-
-my $weekstart = 'Sunday'; #RT::SiteConfig?  user pref?
-my %week = (
-  'Saturday' => [6,0..5],
-  'Sunday'   => [0..6],
-  'Monday'   => [1..6,0],
-);
-my $startday_of_week = ${$week{$weekstart}}[0]  || 7;
-my $endday_of_week   = ${$week{$weekstart}}[-1] || 7;
-
-my $today = DateTime->today;
-my $yesterday = $today->clone->subtract( days=>1 );
-my $aweekago  = $today->clone->subtract( days=>7 );
-my $date = RTx::Calendar::FirstDay($Year, $Month + 1, $startday_of_week );
-my $end  = RTx::Calendar::LastDay ($Year, $Month + 1, $endday_of_week );
-
-# use this to loop over days until $end
-my $set = DateTime::Set->from_recurrence(
-    next => sub { $_[0]->truncate( to => 'day' )->add( days => 1 ) }
-);
-
-my $QueryString =
-      $m->comp(
-        '/Elements/QueryString',
-        Query   => $Query,
-        Format  => $Format,
-        Order   => $Order,
-        OrderBy => $OrderBy,
-        Rows    => $RowsPerPage
-      )
-      if ($Query);
-
-$QueryString ||= 'NewQuery=1';
-
-# Default Query and Format
-my $TempFormat = "__Starts__ __Due__";
-my $TempQuery = "( Status = 'new' OR Status = 'open' OR Status = 'stalled')
- AND ( Owner = '" . $session{CurrentUser}->Id ."' OR Owner = 'Nobody'  )
- AND ( Type = 'reminder' OR 'Type' = 'ticket' )";
-
-if ( my $Search = RTx::Calendar::SearchDefaultCalendar($session{CurrentUser}) ) {
-  $TempFormat = $Search->SubValue('Format');
-  $TempQuery = $Search->SubValue('Query');
-}
-
-# we overide them if needed
-$TempQuery  = $Query  if $Query;
-$TempFormat = $Format if $Format;
-
-# we search all date types in Format string
-my @Dates = grep { $TempFormat =~ m/__${_}(Relative)?__/ } @DateTypes;
-
-# used to display or not a date in Element/CalendarEvent
-my %DateTypes = map { $_ => 1 } @Dates;
-
-$TempQuery .= RTx::Calendar::DatesClauses(\@Dates, $date->strftime("%F"), $end->strftime("%F"));
-
-$m->callback( CallbackName => 'BeforeFindTickets', ARGSRef => \%ARGS, QueryRef => \$TempQuery, FormatRef => \$TempFormat );
-
-my %Tickets = RTx::Calendar::FindTickets($session{'CurrentUser'}, $TempQuery, \@Dates, $date->strftime("%F"), $end->strftime("%F"));
-
-</%INIT>
+<& /Elements/Header, Title => loc("Calendar") &>
+<& /Elements/Tabs &>
+<& /Elements/Calendar &>
+<& /Elements/CalendarFooter &>
diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 9bb375d..45c79fe 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -7,6 +7,7 @@ use DateTime::Set;
 our $VERSION = "1.05";
 
 RT->AddStyleSheets('calendar.css');
+RT->AddJavaScript('calendar.js');
 
 sub FirstDay {
     my ( $year, $month, $matchday ) = @_;
diff --git a/static/js/calendar.js b/static/js/calendar.js
new file mode 100644
index 0000000..5b84bc0
--- /dev/null
+++ b/static/js/calendar.js
@@ -0,0 +1,6 @@
+function changeCalendarMonth() {
+    var month = jQuery('.changeCalendarMonth select[name="Month"]').val();
+    var year = jQuery('.changeCalendarMonth select[name="Year"]').val();
+    var querystring = jQuery('.changeCalendarMonth #querystring').val();
+    window.location.href = "?Month=" + month + "&Year=" + year + "&" + querystring;
+}

commit 18f093834a14cc6b1b72c9722479497cab594571
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Mon Sep 11 10:50:17 2023 -0300

    Create Element for the Calendar footer
    
    Move the footer with help information of the calendar to an Element
    file to make it easier to maintain.

diff --git a/html/Elements/CalendarFooter b/html/Elements/CalendarFooter
new file mode 100644
index 0000000..a8244f0
--- /dev/null
+++ b/html/Elements/CalendarFooter
@@ -0,0 +1,36 @@
+<&| /Widgets/TitleBox, title => loc('Help') &>
+
+<h3><&|/l&>displaying reminders</&>:</h3>
+<p>
+<&|/l_unsafe, qq{<a href="$RT::WebPath/Search/Edit.html">} . loc("Advanced") . '</a>' &>
+If you want to have reminders in a search you need to go to [_1] tab
+and add something to the Query like that:
+</&>
+ <pre>
+   AND ( Type = 'ticket' OR Type = 'reminder' )
+</pre>
+</p>
+
+<h3><&|/l&>displaying other kind of dates</&>:</h3>
+<p>
+<&|/l_unsafe, qq{<a href="$RT::WebPath/Search/Build.html">} . loc("Query Builder") . '</a>'&>
+By default RTx::Calendar display Due and Starts dates. You can select other
+kind of events you want with the Display Columns section in the [_1].
+The following one will display the two latter and LastUpdated dates:
+</&>
+<pre>
+  '<small>__Due__</small>',
+  '<small>__Starts__</small>',
+  '<small>__LastUpdated__</small>'
+</pre>
+</p>
+
+<h3><&|/l&>changing the default query</&>:</h3>
+<p>
+<&|/l_unsafe, qq{<a href="$RT::WebPath/Search/Build.html">} . loc("Query Builder") . '</a>'&>
+You can change the default Query of Calendar.html and MyCalendar
+portlet by saving a query with the name <code>calendar</code> in the [_1].
+</&>
+</p>
+
+</&>
diff --git a/html/Search/Calendar.html b/html/Search/Calendar.html
index 9c5e71e..a40ba73 100644
--- a/html/Search/Calendar.html
+++ b/html/Search/Calendar.html
@@ -136,43 +136,6 @@ $NewQuery => 0
 
 </&>
 
-<&| /Widgets/TitleBox, title => loc('Help') &>
-
-<h3><&|/l&>displaying reminders</&>:</h3>
-<p>
-<&|/l_unsafe, qq{<a href="$RT::WebPath/Search/Edit.html">} . loc("Advanced") . '</a>' &>
-If you want to have reminders in a search you need to go to [_1] tab
-and add something to the Query like that:
-</&>
- <pre>
-   AND ( Type = 'ticket' OR Type = 'reminder' )
-</pre>
-</p>
-
-<h3><&|/l&>displaying other kind of dates</&>:</h3>
-<p>
-<&|/l_unsafe, qq{<a href="$RT::WebPath/Search/Build.html">} . loc("Query Builder") . '</a>'&>
-By default RTx::Calendar display Due and Starts dates. You can select other
-kind of events you want with the Display Columns section in the [_1].
-The following one will display the two latter and LastUpdated dates:
-</&>
-<pre>
-  '<small>__Due__</small>',
-  '<small>__Starts__</small>',
-  '<small>__LastUpdated__</small>'
-</pre>
-</p>
-
-<h3><&|/l&>changing the default query</&>:</h3>
-<p>
-<&|/l_unsafe, qq{<a href="$RT::WebPath/Search/Build.html">} . loc("Query Builder") . '</a>'&>
-You can change the default Query of Calendar.html and MyCalendar
-portlet by saving a query with the name <code>calendar</code> in the [_1].
-</&>
-</p>
-
-</&>
-
 <%ONCE>
 
 my %legend = (

-----------------------------------------------------------------------


hooks/post-receive
-- 
rtx-calendar


More information about the Bps-public-commit mailing list