[Bps-public-commit] rtx-calendar branch multiple-days-events created. 1.05-31-g99dd727

BPS Git Server git at git.bestpractical.com
Tue Sep 26 01:20:29 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  99dd72714e9065b0763def0a46b3b7237a409abd (commit)

- Log -----------------------------------------------------------------
commit 99dd72714e9065b0763def0a46b3b7237a409abd
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 6eb405ac4f6e4e7f6fa25a9d978ee82c1f0099e1
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 83ca93a..7160279 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 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