[Bps-public-commit] rtx-calendar branch multiple-days-events created. 1.05-29-gcd887c1

BPS Git Server git at git.bestpractical.com
Fri Sep 22 19:08:39 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  cd887c193f957952ec5bcddf5a79b5cc27d7bb5a (commit)

- Log -----------------------------------------------------------------
commit cd887c193f957952ec5bcddf5a79b5cc27d7bb5a
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 abbc3fc655b821ae935b43769f861298a7f3e975
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 8e0c0a1..83ca93a 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -453,6 +453,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 22fe4868f865a30e51151b4a89cbbd231b979756
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 4c0b7e181abd6f5b94bbe23c1dbb34c7a1c8f7bd
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..8e0c0a1 100644
--- a/lib/RTx/Calendar.pm
+++ b/lib/RTx/Calendar.pm
@@ -372,9 +372,22 @@ 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

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