[Bps-public-commit] rtx-calendar branch multiple-days-events created. 1.05-34-g9bac33b

BPS Git Server git at git.bestpractical.com
Wed Sep 27 14:46:24 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 has been created
        at  9bac33b06a19edb4121d5acf9eae639110081ace (commit)

- Log -----------------------------------------------------------------
commit 9bac33b06a19edb4121d5acf9eae639110081ace
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Tue Sep 26 21:38:59 2023 -0300

    Fix filter on status bad behavior when changing months
    
    When users changed the month on the calendar with the filter on status
    enabled, and on the next month they changed the status filter, the
    filter pre selecting the statuses from the args on the URI, and not from
    the posted form.

diff --git a/html/Elements/Calendar b/html/Elements/Calendar
index b2bafe8..b87cd78 100644
--- a/html/Elements/Calendar
+++ b/html/Elements/Calendar
@@ -205,7 +205,16 @@ my $NewQuery = $DECODED_ARGS->{NewQuery};
 my $BaseQuery = $DECODED_ARGS->{BaseQuery};
 my $FilterOnStatusClear = $DECODED_ARGS->{FilterOnStatusClear};
 my @FilterOnStatus;
-if ( $DECODED_ARGS->{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}};
   }
diff --git a/html/Elements/CalendarSidebar b/html/Elements/CalendarSidebar
index 34ceaf0..b1f96ce 100644
--- a/html/Elements/CalendarSidebar
+++ b/html/Elements/CalendarSidebar
@@ -12,7 +12,7 @@
 
     <form id="FilterOnStatusForm"
      method="post">
-      <select name="FilterOnStatus" id="FilterOnStatus"
+      <select name="NewFilterOnStatus" id="NewFilterOnStatus"
         class="selectpicker filteronstatus mt-3 mb-3" multiple="multiple" size="6">
 % for my $Status (sort {loc($a) cmp loc($b)} @{RT->Config->Get('CalendarFilterStatuses')}) {
         <option value="<% $Status %>"

commit f944f3655513d3d4d0fce92279e7f990ce045d04
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Tue Sep 26 21:21:31 2023 -0300

    Fix filter on status to not replace the default query
    
    Filter on Status was replacing the default query of the Calendar
    element, causing the calendar to show wrong events when selecting a
    status that was not part of the default query.

diff --git a/html/Elements/Calendar b/html/Elements/Calendar
index 0042415..b2bafe8 100644
--- a/html/Elements/Calendar
+++ b/html/Elements/Calendar
@@ -249,12 +249,6 @@ my $set = DateTime::Set->from_recurrence(
     next => sub { $_[0]->truncate( to => 'day' )->add( days => 1 ) }
 );
 
-if (@FilterOnStatus) {
-  my $StatusClause = join " OR ", map { "Status = '$_'" } @FilterOnStatus;
-  $Query .= " AND " if $Query;
-  $Query .= "($StatusClause)";
-}
-
 # Default Query and Format
 my $TempFormat = "__Starts__ __Due__";
 my $TempQuery = "( Status = 'new' OR Status = 'open' OR Status = 'stalled')
@@ -310,6 +304,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, $TicketsSpanningDays) = RTx::Calendar::FindTickets($session{'CurrentUser'}, $TempQuery, \@Dates, $date->strftime("%F"), $end->strftime("%F"));

commit 2b49822632b349da291e6aa137f4c911419e7407
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue Sep 26 11:47:07 2023 -0400

    Handle Date custom fields correctly
    
    Unlike DateTime, Date values don't have time part and thus no timezone.
    E.g. for 2023-09-26, it should always be 2023-09-26, no matter what
    timezone current user is in.

diff --git a/html/Elements/CalendarEvent b/html/Elements/CalendarEvent
index db29d65..5f018b3 100644
--- a/html/Elements/CalendarEvent
+++ b/html/Elements/CalendarEvent
@@ -92,8 +92,8 @@ if ( ( !grep { $_ eq $TicketId } @$spanning_tickets_for_tomorrow ) ) {
         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, Timezone => 'UTC');
-            $value = $date_value->AsString( Timezone => 'user' );
+            $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.'()';
diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 60c5b7c..395c2d8 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -199,7 +199,6 @@ sub GetDate {
             $DateObj->Set(
                 Format   => 'unknown',
                 Value    => $CFDateValue,
-                Timezone => 'utc'
             );
         } else {
             $DateObj->Set( Format => 'ISO', Value => $CFDateValue );

commit 2478f2037142f30877035e1ba9a3eed9532bb489
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Fri Sep 22 16:42:24 2023 -0300

    Fix event popup to appear correctly on the right side of the screen
    
    The event popup was disappearing off the right side of the screen when
    the event was near the right edge of the calendar.  This commit fixes
    that by making the popup appear on the left side of the event when the
    event is near the right edge of the calendar.

diff --git a/html/Elements/Calendar b/html/Elements/Calendar
index f6568f3..0042415 100644
--- a/html/Elements/Calendar
+++ b/html/Elements/Calendar
@@ -85,6 +85,7 @@ while ($date <= $end) {
   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 ( RTx::Calendar::SortCalendarEvents(
                 \@{ $Tickets->{ $date->strftime("%F") } || [] },
@@ -117,7 +118,7 @@ while ($date <= $end) {
   }
 </%perl>
 
-    <td class="<% @classes %>"><div class="inside-day">
+    <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} }
diff --git a/html/Elements/MyCalendar b/html/Elements/MyCalendar
index 5bff533..da45a4e 100644
--- a/html/Elements/MyCalendar
+++ b/html/Elements/MyCalendar
@@ -23,6 +23,7 @@ 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 ( RTx::Calendar::SortCalendarEvents(
                 \@{ $Tickets->{ $date->strftime("%F") } || [] },
                 $sorting_field,
@@ -54,7 +55,7 @@ while ($date <= $end) {
   }
 </%perl>
 
-    <td class="<% @classes %>"><div class="inside-day">
+    <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} }
diff --git a/static/css/calendar.css b/static/css/calendar.css
index c6cb8c8..915a0b0 100644
--- a/static/css/calendar.css
+++ b/static/css/calendar.css
@@ -33,6 +33,12 @@ table.rtxcalendar div.day div.event-info: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 {

commit 2630b043e8c4a469e65546b8628e79274a172ce0
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Fri Sep 22 16:10:07 2023 -0300

    Remove the daily events sorting sub routine from the settings
    
    The CalendarSortingEvents setting was introduced to allow the user to
    sort the daily events in the calendar. That was first implemented for
    giving a possibility sort events by status, grouping the events that are
    in the same status together. Although that could be useful in a few
    cases, it was a blocker to implement the multiple days events feature.
    
    Since this feature was never introduced in a final version of the
    extension, it is safe to remove it and sort the events by date.
    
    Now the events are sorted by the first date field of the search format.
    
    In order to make this sorting, we are using the GetDate method that was
    previously a private method of the RTx::Calendar package. So we renamed
    it from _GetDate to GetDate and made it public.

diff --git a/etc/RTxCalendar_Config.pm b/etc/RTxCalendar_Config.pm
index 6340a1f..5241e22 100644
--- a/etc/RTxCalendar_Config.pm
+++ b/etc/RTxCalendar_Config.pm
@@ -18,12 +18,6 @@ Set(%CalendarStatusColorMap, (
     'stalled'                               => '#FF0000',
 ));
 
-Set($CalendarSortEvents, sub {
-    my @Tickets = @_;
-    my @SortedTickets = sort { lc($a->Status) cmp lc($b->Status) } @Tickets;
-    return @SortedTickets;
-});
-
 Set(@CalendarFilterStatuses, qw(new open stalled rejected resolved));
 
 Set(@CalendarFilterDefaultStatuses, qw(new open));
diff --git a/html/Elements/Calendar b/html/Elements/Calendar
index c621d40..f6568f3 100644
--- a/html/Elements/Calendar
+++ b/html/Elements/Calendar
@@ -86,14 +86,17 @@ while ($date <= $end) {
   push @classes, "yesterday" if (DateTime->compare($yesterday, $date) == 0);
   push @classes, "aweekago"  if (DateTime->compare($aweekago,  $date) == 0);
 
-  for my $t ( $SortCalendarEvents->( @{ $Tickets->{ $date->strftime("%F") } || [] } )) {
+  for my $t ( RTx::Calendar::SortCalendarEvents(
+                \@{ $Tickets->{ $date->strftime("%F") } || [] },
+                $sorting_field,
+                $session{CurrentUser} ) ) {
     # 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 keys %week_ticket_position ) {
+      for my $index ( sort { $a <=> $b } keys %week_ticket_position ) {
         if ( $week_ticket_position{$index}{id} eq "" ) {
           $free_index = $i;
           last;
@@ -116,7 +119,7 @@ while ($date <= $end) {
 
     <td class="<% @classes %>"><div class="inside-day">
       <div class="calendardate"><%$date->day%></div>
-%     for my $index ( sort keys %week_ticket_position ) {
+%     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};
@@ -319,5 +322,5 @@ my $DownloadQueryString =
         OrderBy => $OrderBy,
       );
 
-my $SortCalendarEvents = RT->Config->Get("CalendarSortEvents");
+my $sorting_field = $Dates[0] || '';
 </%INIT>
diff --git a/html/Elements/MyCalendar b/html/Elements/MyCalendar
index fb2e227..5bff533 100644
--- a/html/Elements/MyCalendar
+++ b/html/Elements/MyCalendar
@@ -23,14 +23,17 @@ while ($date <= $end) {
   my @classes = ();
   push @classes, "today"     if (DateTime->compare($today,     $date) == 0);
   push @classes, "yesterday" if (DateTime->compare($yesterday, $date) == 0);
-  for my $t ( $SortCalendarEvents->( @{ $Tickets->{ $date->strftime("%F") } || [] } )) {
+  for my $t ( RTx::Calendar::SortCalendarEvents(
+                \@{ $Tickets->{ $date->strftime("%F") } || [] },
+                $sorting_field,
+                $session{CurrentUser} ) ) {
     # 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 keys %week_ticket_position ) {
+      for my $index ( sort { $a <=> $b } keys %week_ticket_position ) {
         if ( $week_ticket_position{$index}{id} eq "" ) {
           $free_index = $i;
           last;
@@ -53,7 +56,7 @@ while ($date <= $end) {
 
     <td class="<% @classes %>"><div class="inside-day">
       <div class="calendardate"><%$date->day%></div>
-%     for my $index ( sort keys %week_ticket_position ) {
+%      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};
@@ -143,5 +146,5 @@ $m->callback( CallbackName => 'BeforeFindTickets', ARGSRef => \%ARGS, QueryRef =
 
 my ($Tickets, $TicketsSpanningDays) = RTx::Calendar::FindTickets($session{'CurrentUser'}, $Query, \@Dates);
 
-my $SortCalendarEvents = RT->Config->Get("CalendarSortEvents");
+my $sorting_field = $Dates[0] || '';
 </%INIT>
diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 185cc43..60c5b7c 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -106,7 +106,7 @@ sub FindTickets {
             # $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 = _GetDate( $Date, $Ticket, $CurrentUser );
+            my $dateindex = GetDate( $Date, $Ticket, $CurrentUser );
 
             push @{ $Tickets{$dateindex } },
                 $Ticket
@@ -126,8 +126,8 @@ sub FindTickets {
                 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 );
+            my $starts_date  = GetDate( $starts_field, $Ticket, $CurrentUser );
+            my $ends_date    = GetDate( $ends_field,   $Ticket, $CurrentUser );
 
             # Loop through all days between start and end and add the ticket
             # to it
@@ -173,11 +173,16 @@ sub FindTickets {
     }
 }
 
-sub _GetDate {
+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/;
@@ -206,6 +211,19 @@ sub _GetDate {
     }
 }
 
+sub SortCalendarEvents {
+    my $tickets_of_the_day = shift;
+    my $sorting_field = shift;
+    my $current_user = shift;
+    my @sorted_tickets = sort {
+        my ($a_value, $b_value);
+        $a_value = RTx::Calendar::GetDate( $sorting_field, $a, $current_user ) . " " . $a->id;
+        $b_value = RTx::Calendar::GetDate( $sorting_field, $b, $current_user ) . " " . $b->id;
+        ($a_value cmp $b_value)
+    } @$tickets_of_the_day;
+    return @sorted_tickets;
+}
+
 #
 # Take a user object and return the search with Description "calendar" if it exists
 #

commit ece6ba01afd5bbbb234fc112f771eac6156ce09c
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Wed Sep 20 08:23:39 2023 -0300

    Remove extra line break of Event Types legend
    
    There was a unnecessary line break on Types legend taking
    extra space of the legend and sidebar. This patch removes
    it.

diff --git a/html/Elements/CalendarSidebar b/html/Elements/CalendarSidebar
index 1619fca..34ceaf0 100644
--- a/html/Elements/CalendarSidebar
+++ b/html/Elements/CalendarSidebar
@@ -43,7 +43,7 @@
         <span class="tiplegend">
           <% $TranslatedLegend %>
         </span>
-      </span><br />
+      </span>
 % }
     </&>
 

commit 1e47535adc4fd0f2e3b02c8062bbf7209115af0a
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Fri Sep 15 16:14:06 2023 -0300

    Update POD with CalendarFilterDefaultStatuses config information

diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 0421abe..185cc43 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -454,6 +454,12 @@ 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
 

commit 9ef3675d0774da334e2cd0c71cc663c677abd0d1
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Fri Sep 15 16:09:23 2023 -0300

    Add option to pre-select Filter on Status values
    
    Add CalendarFilterDefaultStatuses option which allows to pre-select
    Status values in the Filter on Status box for the first time the
    calendar is displayed.

diff --git a/etc/RTxCalendar_Config.pm b/etc/RTxCalendar_Config.pm
index d042d47..6340a1f 100644
--- a/etc/RTxCalendar_Config.pm
+++ b/etc/RTxCalendar_Config.pm
@@ -26,4 +26,6 @@ Set($CalendarSortEvents, sub {
 
 Set(@CalendarFilterStatuses, qw(new open stalled rejected resolved));
 
+Set(@CalendarFilterDefaultStatuses, qw(new open));
+
 1;
diff --git a/html/Elements/Calendar b/html/Elements/Calendar
index c5b1fc9..c621d40 100644
--- a/html/Elements/Calendar
+++ b/html/Elements/Calendar
@@ -189,6 +189,7 @@ while ($date <= $end) {
 </&>
 </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};
@@ -207,7 +208,11 @@ if ( $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;
@@ -270,11 +275,12 @@ my $QueryString =
         Format  => $Format,
         Order   => $Order,
         OrderBy => $OrderBy,
-        Rows    => $RowsPerPage
+        Rows    => $RowsPerPage,
+        NotFirstAccess => $NotFirstAccess,
       )
       if ($Query);
 
-$QueryString ||= 'NewQuery=1';
+$QueryString ||= 'NewQuery=1&NotFirstAccess=1';
 
 # we search all date types in Format string
 my @CoreDates    = grep { $TempFormat =~ m/__${_}(Relative)?__/ } @DateTypes;

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

    Update calendar POD mentioning the new Portlets
    
    Add Calendar and CalendarWithSidebar portlets to the POD.

diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 36d5ee4..0421abe 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -372,12 +372,26 @@ 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

commit aeb3159b2c1b54750568c341826bb8c503291d30
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. Created two flavors of
    the Element to be added to the dashboard. One with the sidebar and one
    without.

diff --git a/html/Search/Calendar.html b/html/Elements/Calendar
similarity index 85%
copy from html/Search/Calendar.html
copy to html/Elements/Calendar
index 01ef818..c5b1fc9 100644
--- a/html/Search/Calendar.html
+++ b/html/Elements/Calendar
@@ -1,27 +1,10 @@
 <%args>
-$Month => (localtime)[4]
-$Year => (localtime)[5] + 1900
-$Query => undef
-$Format => undef
-$Order => undef
-$OrderBy => undef
-$RowsPerPage => undef
-$NewQuery => 0
- at FilterOnStatus => undef
-$BaseQuery => undef
-$FilterOnStatusClear => undef
+$ShowSidebar => 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 &>
-% }
+<div class="calendar-container">
 
+% if ($ShowSidebar) {
   <& /Elements/CalendarSidebar,
     BaseQuery => $BaseQuery,
     Month => $Month,
@@ -33,10 +16,12 @@ $FilterOnStatusClear => undef
     FilterOnStatus => \@FilterOnStatus,
     Dates => \@Dates,
   &>
+% }
 
 <div class="calendar-content">
 <&| /Widgets/TitleBox,
      title => loc('Calendar for [_1] [_2]', $rtdate->GetMonth($Month), $Year),
+     title_href => "Search/Calendar.html",
      titleright => loc('Download Spreadsheet'),
      titleright_href => $RT::WebPath. "/Search/Results.tsv?". $DownloadQueryString
      &>
@@ -49,7 +34,7 @@ $FilterOnStatusClear => undef
 %    $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>
@@ -60,7 +45,7 @@ $FilterOnStatusClear => undef
 %    $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>
@@ -170,11 +155,11 @@ while ($date <= $end) {
 <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">
 % for (0..11) {
@@ -187,22 +172,43 @@ while ($date <= $end) {
 <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>
 
+</div>
+
 </&>
-  <& /Elements/CalendarFooter &>
 </div>
 <%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 $BaseQuery = $DECODED_ARGS->{BaseQuery};
+my $FilterOnStatusClear = $DECODED_ARGS->{FilterOnStatusClear};
+my @FilterOnStatus;
+if ( $DECODED_ARGS->{FilterOnStatus} ) {
+  if ( ref $DECODED_ARGS->{FilterOnStatus} eq 'ARRAY' ) {
+    @FilterOnStatus = @{$DECODED_ARGS->{FilterOnStatus}};
+  }
+  else {
+    push @FilterOnStatus, $DECODED_ARGS->{FilterOnStatus};
+  }
+}
+
 if ($FilterOnStatusClear) {
   $Query = $BaseQuery if $BaseQuery;
   @FilterOnStatus = ();
@@ -234,7 +240,7 @@ my $set = DateTime::Set->from_recurrence(
     next => sub { $_[0]->truncate( to => 'day' )->add( days => 1 ) }
 );
 
-if ($FilterOnStatus[0]) {
+if (@FilterOnStatus) {
   my $StatusClause = join " OR ", map { "Status = '$_'" } @FilterOnStatus;
   $Query .= " AND " if $Query;
   $Query .= "($StatusClause)";
diff --git a/html/Elements/CalendarSidebar b/html/Elements/CalendarSidebar
index c752ae8..1619fca 100644
--- a/html/Elements/CalendarSidebar
+++ b/html/Elements/CalendarSidebar
@@ -1,11 +1,4 @@
 <%args>
-$BaseQuery => undef
-$Month => undef
-$Year => undef
-$Format => undef
-$Order => undef
-$OrderBy => undef
-$RowsPerPage => undef
 @FilterOnStatus => undef
 @Dates => undef
 </%args>
@@ -18,14 +11,7 @@ $RowsPerPage => undef
       &>
 
     <form id="FilterOnStatusForm"
-    action="<%$RT::WebPath%>/Search/Calendar.html" method="post">
-      <input type="hidden" name="BaseQuery" value="<%$BaseQuery%>" />
-      <input type="hidden" name="Month" value="<%$Month%>" />
-      <input type="hidden" name="Year" value="<%$Year%>" />
-      <input type="hidden" name="Format" value="<%$Format%>" />
-      <input type="hidden" name="Order" value="<%$Order%>" />
-      <input type="hidden" name="OrderBy" value="<%$OrderBy%>" />
-      <input type="hidden" name="RowsPerPage" value="<%$RowsPerPage%>" />
+     method="post">
       <select name="FilterOnStatus" id="FilterOnStatus"
         class="selectpicker filteronstatus mt-3 mb-3" multiple="multiple" size="6">
 % for my $Status (sort {loc($a) cmp loc($b)} @{RT->Config->Get('CalendarFilterStatuses')}) {
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 01ef818..c12b25d 100644
--- a/html/Search/Calendar.html
+++ b/html/Search/Calendar.html
@@ -1,311 +1,6 @@
-<%args>
-$Month => (localtime)[4]
-$Year => (localtime)[5] + 1900
-$Query => undef
-$Format => undef
-$Order => undef
-$OrderBy => undef
-$RowsPerPage => undef
-$NewQuery => 0
- at FilterOnStatus => undef
-$BaseQuery => undef
-$FilterOnStatusClear => undef
-</%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 &>
-% }
-
-  <& /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),
-     titleright => loc('Download Spreadsheet'),
-     titleright_href => $RT::WebPath. "/Search/Results.tsv?". $DownloadQueryString
-     &>
-
-<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>
-<%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);
-
-  for my $t ( $SortCalendarEvents->( @{ $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 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>
-
-    <td class="<% @classes %>"><div class="inside-day">
-      <div class="calendardate"><%$date->day%></div>
-%     for my $index ( sort 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>
-</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>
-
-</&>
-  <& /Elements/CalendarFooter &>
-</div>
-<%INIT>
-if ($FilterOnStatusClear) {
-  $Query = $BaseQuery if $BaseQuery;
-  @FilterOnStatus = ();
-}
-$BaseQuery ||= $Query;
-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 ) }
-);
-
-if ($FilterOnStatus[0]) {
-  my $StatusClause = join " OR ", map { "Status = '$_'" } @FilterOnStatus;
-  $Query .= " AND " if $Query;
-  $Query .= "($StatusClause)";
-}
-
-# 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;
-$Format = $TempFormat unless $Format;
-
-my $QueryString =
-      $m->comp(
-        '/Elements/QueryString',
-        Query   => $BaseQuery,
-        FilterOnStatus => \@FilterOnStatus,
-        Format  => $Format,
-        Order   => $Order,
-        OrderBy => $OrderBy,
-        Rows    => $RowsPerPage
-      )
-      if ($Query);
-
-$QueryString ||= 'NewQuery=1';
-
-# we search all date types in Format string
-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;
-
-$TempQuery .= RTx::Calendar::DatesClauses(\@Dates, $date->strftime("%F"), $end->strftime("%F"));
-
-$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"));
-
-my $DownloadQueryString =
-      $m->comp(
-        '/Elements/QueryString',
-        Query   => $TempQuery,
-        Format  => $Format,
-        Order   => $Order,
-        OrderBy => $OrderBy,
-      );
-
-my $SortCalendarEvents = RT->Config->Get("CalendarSortEvents");
-</%INIT>
+<& /Elements/Header, Title => loc("Calendar") &>
+<& /Elements/Tabs &>
+<& /Elements/Calendar,
+  ShowSidebar => 1,
+&>
+<& /Elements/CalendarFooter &>
diff --git a/static/css/calendar.css b/static/css/calendar.css
index 37858ac..c6cb8c8 100644
--- a/static/css/calendar.css
+++ b/static/css/calendar.css
@@ -129,6 +129,7 @@ a.calendar-toggle-sidebar.sidebar-off::before {
 .calendar-sidebar-toggle-content {
     float: left;
     width: 230px;
+    margin-top: 20px;
 }
 .calendar-sidebar-toggle-content.sidebar-off {
     width:unset;
@@ -170,3 +171,6 @@ table.rtxcalendar .day.last-day {
 table.rtxcalendar .day.first-day.last-day {
     border-radius: 5px;
 }
+.calendar-container {
+    display: flow-root;
+}
diff --git a/static/js/calendar.js b/static/js/calendar.js
index a9a00ef..07a3958 100644
--- a/static/js/calendar.js
+++ b/static/js/calendar.js
@@ -1,6 +1,12 @@
 window.onresize = resizeCalendarEventTitles;
 jQuery(function() {
     resizeCalendarEventTitles();
+
+    jQuery('.changeCalendarMonth').submit(function(e){
+        e.preventDefault();
+        changeCalendarMonth();
+    });
+
 });
 
 /*
@@ -39,3 +45,10 @@ function resizeCalendarEventTitles() {
         }
     )
 }
+
+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 682f759318db8c61d142f52c828500b60b3fa75f
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Wed Sep 13 22:38:25 2023 -0300

    Upgrade date objects ad methods to RT::Date
    
    Many of the date calculations of the calendar were done using different
    perl date and time modules. This was causing issues to present events in
    the user timezone.

diff --git a/html/Elements/CalendarEvent b/html/Elements/CalendarEvent
index c2f35cb..db29d65 100644
--- a/html/Elements/CalendarEvent
+++ b/html/Elements/CalendarEvent
@@ -9,8 +9,10 @@ $CurrentPostion => undef
 </%args>
 
 <%perl>
-my $icon = RTx::Calendar::GetEventImg($Object, $today, $DateTypes, $IsReminder);
-my $spanning_tickets_for_today = $TicketsSpanningDays->{$today} || [];
+my $icon
+= RTx::Calendar::GetEventImg( $Object, $today, $DateTypes, $IsReminder,
+   $session{'CurrentUser'} );
+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;
@@ -19,7 +21,7 @@ my $last_day_of_the_event = 0;
 if ( ( ! grep { $_ eq $TicketId } @$spanning_tickets_for_today ) ) {
     $first_day_of_the_event = 1;
 }
-if ((!grep { $_ eq $TicketId } @$spanning_tickets_for_tomorrow )) {
+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} = "";
@@ -72,21 +74,34 @@ if ((!grep { $_ eq $TicketId } @$spanning_tickets_for_tomorrow )) {
 	:</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, Timezone => 'UTC');
+            $value = $date_value->AsString( Timezone => 'user' );
+        }
+    } 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 />
 % }
 
diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index c2882ab..36d5ee4 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 ) = @_;
 
@@ -143,19 +135,19 @@ sub FindTickets {
             $current_date->Set(
                 Format => 'unknown',
                 Value => $starts_date,
-                Timezone => 'utc'
+                Timezone => 'user'
             );
             my $end_date_unix = RT::Date->new($CurrentUser);
             $end_date_unix->Set(
                 Format => 'unknown',
                 Value => $ends_date,
-                Timezone => 'utc'
+                Timezone => 'user'
             );
             $end_date_unix = $end_date_unix->Unix;
             my $first_day = 1;
             while ( $current_date->Unix <= $end_date_unix )
             {
-                my $dateindex = LocalDate( $current_date->Unix );
+                my $dateindex = $current_date->ISO( Time => 0, Timezone => 'user' );
 
                 push @{ $TicketsSpanningDays{$dateindex} }, $Ticket->id
                     unless $first_day
@@ -207,10 +199,10 @@ sub _GetDate {
         } else {
             $DateObj->Set( Format => 'ISO', Value => $CFDateValue );
         }
-        return LocalDate( $DateObj->Unix );
+        return $DateObj->ISO( Time => 0, Timezone => 'user' );
     } else {
         my $DateObj = $date_field . "Obj";
-        return LocalDate( $Ticket->$DateObj->Unix );
+        return $Ticket->$DateObj->ISO( Time => 0, Timezone => 'user' );
     }
 }
 
@@ -265,6 +257,7 @@ sub GetEventImg {
     my $CurrentDate = shift;
     my $DateTypes   = shift;
     my $IsReminder  = shift;
+    my $CurrentUser = shift;
     my $EventIcon;
     my %CalendarIcons = RT->Config->Get('CalendarIcons');
 CALENDAR_ICON:
@@ -279,7 +272,7 @@ CALENDAR_ICON:
             $ComparedDate =~ s/^\s+|\s+$//g;
             if ( $DateField eq 'Reminder' ) {
                 if ( $IsReminder
-                    && RTx::Calendar::LocalDate( $Object->DueObj->Unix ) eq
+                    && $Object->DueObj->ISO( Time => 0, Timezone => 'user' ) eq
                     $CurrentDate )
                 {
                     $EventIcon = 'reminder.png';
@@ -290,12 +283,17 @@ CALENDAR_ICON:
                 $cf =~ s/^CF\.\{(.*)\}/$1/;
                 my $DateValue = $Object->FirstCustomFieldValue($cf);
                 next CALENDAR_ICON unless $DateValue;
-                $DateValue =~ s/(.*) (.*)/$1/;
+                my $DateObj = RT::Date->new( $CurrentUser );
+                my $CustomFieldObj = RT::CustomField->new( $CurrentUser );
+                $CustomFieldObj->LoadByName( Name => $cf );
+                my $date_format = $CustomFieldObj->Type eq 'Date' ? 'unknown' : 'ISO';
+                $DateObj->Set( Format => $date_format, Value => $DateValue );
+                $DateValue = $DateObj->ISO( Time => 0, Timezone => 'user' );
                 next CALENDAR_ICON unless $DateValue eq $CurrentDate;
             } else {
                 my $DateObj = $ComparedDate . "Obj";
                 my $DateValue
-                    = RTx::Calendar::LocalDate( $Object->$DateObj->Unix );
+                    = $Object->$DateObj->ISO( Time => 0, Timezone => 'user' );
                 next CALENDAR_ICON unless $DateValue eq $CurrentDate;
             }
 

commit 46a1cecb7005bf18be5b09cc4258e20640a82a4e
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Mon Sep 11 15:56:40 2023 -0300

    Make MyCalendar Multiday events compatible
    
    Add the multiday events to the MyCalendar portlet.

diff --git a/html/Elements/MyCalendar b/html/Elements/MyCalendar
index f95cc9d..fb2e227 100644
--- a/html/Elements/MyCalendar
+++ b/html/Elements/MyCalendar
@@ -14,21 +14,67 @@
 </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);
+<%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);
+  for my $t ( $SortCalendarEvents->( @{ $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 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="<% @classes %>"><div class="inside-day">
       <div class="calendardate"><%$date->day%></div>
-
-%     for my $t ( $SortCalendarEvents->( @{ $Tickets->{ $date->strftime("%F") } || [] } )) {
-        <& /Elements/CalendarEvent, Object => $t, Date => $date, DateTypes => \%DateTypes &>
+%     for my $index ( sort 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,8 +141,7 @@ $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);
 
 my $SortCalendarEvents = RT->Config->Get("CalendarSortEvents");
-
 </%INIT>

commit aa2650b47f8130a10e9b1ec03b3f746b8a903cba
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..86b7ccb
--- /dev/null
+++ b/html/Elements/CalendarFooter
@@ -0,0 +1,38 @@
+<&| /Widgets/TitleBox, title => loc('Help') &>
+
+<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 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>
+
+<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 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>__CustomField.{Maintenance Date}__</small>'
+</pre>
+</p>
+
+<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 used by Calendar.html and the 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 5b49c6a..01ef818 100644
--- a/html/Search/Calendar.html
+++ b/html/Search/Calendar.html
@@ -200,45 +200,7 @@ while ($date <= $end) {
 </table>
 
 </&>
-
-<&| /Widgets/TitleBox, title => loc('Help') &>
-
-<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 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>
-
-<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 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>__CustomField.{Maintenance Date}__</small>'
-</pre>
-</p>
-
-<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 used by Calendar.html and the MyCalendar
-portlet by saving a query with the name <code>calendar</code> in the [_1].
-</&>
-</p>
-
-</&>
+  <& /Elements/CalendarFooter &>
 </div>
 <%INIT>
 if ($FilterOnStatusClear) {

commit 33f8fccb079501908ef04655b6400b2c0b62a2e5
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Mon Sep 11 10:28:11 2023 -0300

    Create Element for the Sidebar
    
    Move the sidebar of the calendar to an Element file, making it easier to
    maintain and understand the code.

diff --git a/html/Elements/CalendarSidebar b/html/Elements/CalendarSidebar
new file mode 100644
index 0000000..c752ae8
--- /dev/null
+++ b/html/Elements/CalendarSidebar
@@ -0,0 +1,108 @@
+<%args>
+$BaseQuery => undef
+$Month => undef
+$Year => undef
+$Format => undef
+$Order => undef
+$OrderBy => undef
+$RowsPerPage => undef
+ 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"
+    action="<%$RT::WebPath%>/Search/Calendar.html" method="post">
+      <input type="hidden" name="BaseQuery" value="<%$BaseQuery%>" />
+      <input type="hidden" name="Month" value="<%$Month%>" />
+      <input type="hidden" name="Year" value="<%$Year%>" />
+      <input type="hidden" name="Format" value="<%$Format%>" />
+      <input type="hidden" name="Order" value="<%$Order%>" />
+      <input type="hidden" name="OrderBy" value="<%$OrderBy%>" />
+      <input type="hidden" name="RowsPerPage" value="<%$RowsPerPage%>" />
+      <select name="FilterOnStatus" id="FilterOnStatus"
+        class="selectpicker filteronstatus mt-3 mb-3" multiple="multiple" size="6">
+% for my $Status (sort {loc($a) cmp 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"><% loc('Clear Filter') %></button>
+      </div>
+    </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><br />
+% }
+    </&>
+
+<&| /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} %>"><% $Status %><span><br />
+% }
+</&>
+
+  </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>
+
+<%init>
+my %CalendarIcons = RT->Config->Get('CalendarIcons');
+# Sort the legend after translation
+my %CalendarIconsTranslated;
+my $LegendLabel;
+LEGEND:
+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 ',', $legend;
+  $LegendLabel = join ', ',
+    map {
+      my $label = $_;
+      next LEGEND unless ( grep { $label eq $_ } @Dates );
+      $_ =~ s/^\s+|\s+$//g;
+      $_ =~ s/^CF\.\{(.*)\}/$1/;
+      $_ = 'Last Updated' if $_ eq 'LastUpdated';
+      loc($_)
+    } @LegendLabels;
+  $CalendarIconsTranslated{$LegendLabel} = $legend;
+}
+</%init>
diff --git a/html/Search/Calendar.html b/html/Search/Calendar.html
index fd2926c..5b49c6a 100644
--- a/html/Search/Calendar.html
+++ b/html/Search/Calendar.html
@@ -22,93 +22,17 @@ $FilterOnStatusClear => undef
     <& /Elements/Tabs &>
 % }
 
-<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"
-  action="<%$RT::WebPath%>/Search/Calendar.html" method="post">
-  <input type="hidden" name="BaseQuery" value="<%$BaseQuery%>" />
-  <input type="hidden" name="Month" value="<%$Month%>" />
-  <input type="hidden" name="Year" value="<%$Year%>" />
-  <input type="hidden" name="Format" value="<%$Format%>" />
-  <input type="hidden" name="Order" value="<%$Order%>" />
-  <input type="hidden" name="OrderBy" value="<%$OrderBy%>" />
-  <input type="hidden" name="RowsPerPage" value="<%$RowsPerPage%>" />
-  <select name="FilterOnStatus" id="FilterOnStatus"
-    class="selectpicker filteronstatus mt-3 mb-3" multiple="multiple" size="6">
-% for my $Status (sort {loc($a) cmp 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"><% loc('Clear Filter') %></button>
-  </div>
-</form>
-</&>
-
-  <&| /Widgets/TitleBox,
-     title => loc('Event Types'),
-     &>
-
-<%perl>
-my %CalendarIcons = RT->Config->Get('CalendarIcons');
-# Sort the legend after translation
-my %CalendarIconsTranslated;
-my $LegendLabel;
-LEGEND:
-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 ',', $legend;
-  $LegendLabel = join ', ',
-    map {
-      my $label = $_;
-      next LEGEND unless ( grep { $label eq $_ } @Dates );
-      $_ =~ s/^\s+|\s+$//g;
-      $_ =~ s/^CF\.\{(.*)\}/$1/;
-      $_ = 'Last Updated' if $_ eq 'LastUpdated';
-      loc($_)
-    } @LegendLabels;
-  $CalendarIconsTranslated{$LegendLabel} = $legend;
-}
-foreach my $TranslatedLegend (sort keys %CalendarIconsTranslated) {
-</%perl>
-  <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'),
-     &>
-% my %ColorStatusMap = RT->Config->Get('CalendarStatusColorMap');
-% foreach my $Status (sort { lc($a) cmp lc($b) } keys %ColorStatusMap) {
-    <span style="color: <% $ColorStatusMap{$Status} %>"><% $Status %><span><br />
-% }
-</&>
-
-</div>
-</div>
+  <& /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,
@@ -316,17 +240,6 @@ portlet by saving a query with the name <code>calendar</code> in the [_1].
 
 </&>
 </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>
-
 <%INIT>
 if ($FilterOnStatusClear) {
   $Query = $BaseQuery if $BaseQuery;

commit bd35c81fb9116349d59fa6173c1346ad9a5b5f09
Author: Ronaldo Richieri <ronaldo at bestpractical.com>
Date:   Fri Sep 8 16:01:52 2023 -0300

    Add multievent days position mechanism
    
    This commit adds a new mechanism to position multievent days in the
    calendar. It simulates the UI of other known calendars.

diff --git a/html/Elements/CalendarEvent b/html/Elements/CalendarEvent
index b8023d9..c2f35cb 100644
--- a/html/Elements/CalendarEvent
+++ b/html/Elements/CalendarEvent
@@ -2,20 +2,70 @@
 $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 $icon = RTx::Calendar::GetEventImg($Object, $today, $DateTypes, $IsReminder);
+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
+% }
+" style="
 % if ( $CalendarStatusColorMap{$status} ) {
-    style="color: <%$CalendarStatusColorMap{$status}%>;"
+    background-color: <%$CalendarStatusColorMap{$status}%>;
+% }
+% # 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;
+% }
+        ">
+            <% $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">
+           <% length($Object->Subject) > 80 ? substr($Object->Subject, 0, 77) . "..." : $Object->Subject %>
+        </a>
+% }
+
+
+<span class="tip">
 	<a href="<%$RT::WebPath%>/Ticket/Display.html?id=<%$TicketId%>">
            <% $Object->QueueObj->Name %> #<% $TicketId %>
         </a>
@@ -40,14 +90,18 @@ $DateTypes => undef
 	<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/Search/Calendar.html b/html/Search/Calendar.html
index d827a68..fd2926c 100644
--- a/html/Search/Calendar.html
+++ b/html/Search/Calendar.html
@@ -153,27 +153,91 @@ foreach my $TranslatedLegend (sort keys %CalendarIconsTranslated) {
 
 <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);
+<%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);
+
+  for my $t ( $SortCalendarEvents->( @{ $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 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>
 
     <td class="<% @classes %>"><div class="inside-day">
       <div class="calendardate"><%$date->day%></div>
-
-%     for my $t ( $SortCalendarEvents->( @{ $Tickets->{ $date->strftime("%F") } || [] } )) {
-        <& /Elements/CalendarEvent, Object => $t, Date => $date, DateTypes => \%DateTypes &>
+%     for my $index ( sort 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>
@@ -357,7 +421,7 @@ $TempQuery .= RTx::Calendar::DatesClauses(\@Dates, $date->strftime("%F"), $end->
 
 $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(
diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index e03f2b6..c2882ab 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -7,6 +7,7 @@ use DateTime::Set;
 our $VERSION = "1.07";
 
 RT->AddStyleSheets('calendar.css');
+RT->AddJavaScript('calendar.js');
 
 sub FirstDay {
     my ( $year, $month, $matchday ) = @_;
@@ -151,12 +152,14 @@ sub FindTickets {
                 Timezone => 'utc'
             );
             $end_date_unix = $end_date_unix->Unix;
+            my $first_day = 1;
             while ( $current_date->Unix <= $end_date_unix )
             {
                 my $dateindex = LocalDate( $current_date->Unix );
 
                 push @{ $TicketsSpanningDays{$dateindex} }, $Ticket->id
-                    unless $TicketsSpanningDaysAlreadySeen{$dateindex}
+                    unless $first_day
+                    || $TicketsSpanningDaysAlreadySeen{$dateindex}
                     {$Ticket}++;
                 push @{ $Tickets{$dateindex } },
                     $Ticket
@@ -167,6 +170,7 @@ sub FindTickets {
                     {$Ticket}++;
 
                 $current_date->AddDay();
+                $first_day = 0;
             }
         }
     }
diff --git a/static/css/calendar.css b/static/css/calendar.css
index 96e9086..37858ac 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;
@@ -137,3 +145,28 @@ a.calendar-toggle-sidebar.sidebar-off::before {
 .calendar-sidebar {
     margin-right: 10px;
 }
+
+.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
new file mode 100644
index 0000000..a9a00ef
--- /dev/null
+++ b/static/js/calendar.js
@@ -0,0 +1,41 @@
+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');
+                    })
+            }
+        }
+    )
+}

commit 638c33959a8fa1d7785ea3621d6aa72897d23457
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/MyCalendar b/html/Elements/MyCalendar
index 5220934..f95cc9d 100644
--- a/html/Elements/MyCalendar
+++ b/html/Elements/MyCalendar
@@ -23,7 +23,7 @@
     <td class="<% @classes %>"><div class="inside-day">
       <div class="calendardate"><%$date->day%></div>
 
-%     for my $t ( $SortCalendarEvents->( @{ $Tickets{ $date->strftime("%F") } || [] } )) {
+%     for my $t ( $SortCalendarEvents->( @{ $Tickets->{ $date->strftime("%F") } || [] } )) {
         <& /Elements/CalendarEvent, Object => $t, Date => $date, DateTypes => \%DateTypes &>
 %     }
 
@@ -95,7 +95,7 @@ $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 = RTx::Calendar::FindTickets($session{'CurrentUser'}, $Query, \@Dates);
 
 my $SortCalendarEvents = RT->Config->Get("CalendarSortEvents");
 
diff --git a/html/Search/Calendar.html b/html/Search/Calendar.html
index 5ba02af..d827a68 100644
--- a/html/Search/Calendar.html
+++ b/html/Search/Calendar.html
@@ -163,7 +163,7 @@ foreach my $TranslatedLegend (sort keys %CalendarIconsTranslated) {
     <td class="<% @classes %>"><div class="inside-day">
       <div class="calendardate"><%$date->day%></div>
 
-%     for my $t ( $SortCalendarEvents->( @{ $Tickets{ $date->strftime("%F") } || [] } )) {
+%     for my $t ( $SortCalendarEvents->( @{ $Tickets->{ $date->strftime("%F") } || [] } )) {
         <& /Elements/CalendarEvent, Object => $t, Date => $date, DateTypes => \%DateTypes &>
 %     }
 
@@ -357,7 +357,7 @@ $TempQuery .= RTx::Calendar::DatesClauses(\@Dates, $date->strftime("%F"), $end->
 
 $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 = RTx::Calendar::FindTickets($session{'CurrentUser'}, $TempQuery, \@Dates, $date->strftime("%F"), $end->strftime("%F"));
 
 my $DownloadQueryString =
       $m->comp(
diff --git a/lib/RTx/Calendar.pm b/lib/RTx/Calendar.pm
index 8120ece..e03f2b6 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -49,6 +49,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;
 
@@ -58,6 +82,19 @@ 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;
 
@@ -66,40 +103,17 @@ sub FindTickets {
 
     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 = _GetDate( $Date, $Ticket, $CurrentUser );
 
             push @{ $Tickets{$dateindex } },
                 $Ticket
@@ -110,8 +124,90 @@ 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 );
+
+            # 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 => 'unknown',
+                Value => $starts_date,
+                Timezone => 'utc'
+            );
+            my $end_date_unix = RT::Date->new($CurrentUser);
+            $end_date_unix->Set(
+                Format => 'unknown',
+                Value => $ends_date,
+                Timezone => 'utc'
+            );
+            $end_date_unix = $end_date_unix->Unix;
+            while ( $current_date->Unix <= $end_date_unix )
+            {
+                my $dateindex = LocalDate( $current_date->Unix );
+
+                push @{ $TicketsSpanningDays{$dateindex} }, $Ticket->id
+                    unless $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();
+            }
+        }
+    }
+    if ( wantarray ) {
+        return ( \%Tickets, \%TicketsSpanningDays );
+    } else {
+        return \%Tickets;
+    }
+}
+
+sub _GetDate {
+    my $date_field = shift;
+    my $Ticket = shift;
+    my $CurrentUser = shift;
+
+    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,
+                Timezone => 'utc'
+            );
+        } else {
+            $DateObj->Set( Format => 'ISO', Value => $CFDateValue );
+        }
+        return LocalDate( $DateObj->Unix );
+    } else {
+        my $DateObj = $date_field . "Obj";
+        return LocalDate( $Ticket->$DateObj->Unix );
     }
-    return %Tickets;
 }
 
 #
@@ -355,6 +451,22 @@ C<$CalendarIcons> setting to your F<etc/RT_SiteConfig.pm>:
 
 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
 
 A small help section is available in /Search/Calendar.html

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


hooks/post-receive
-- 
rtx-calendar


More information about the Bps-public-commit mailing list