RT#34237: installer scheduling [major refactor, many bugs fixed]
authorJonathan Prykop <jonathan@freeside.biz>
Wed, 30 Mar 2016 12:41:51 +0000 (07:41 -0500)
committerJonathan Prykop <jonathan@freeside.biz>
Tue, 5 Apr 2016 08:22:12 +0000 (03:22 -0500)
rt/share/html/Elements/CalendarSlotSchedule
rt/share/html/Search/Schedule.html

index b82997b..c8d3bfa 100644 (file)
@@ -11,6 +11,7 @@
   $pkgnum    => undef
   $RedirectToBasics => 0
 </%ARGS>
+% my $scheduling = ($custnum && $LengthMin) ? 1 : 0;
 % foreach my $username ( @username ) {
 %
 %   my %schedule = UserDaySchedule( username => $username,
 %                                   Tickets  => \@Tickets,
 %                                 );
 %
-%   my $bgcolor = '666666;border-color:#555555';
+%   my $bgcolor = '#666666';
+%   my $border = '1px solid #555555';
 %   my $content = '';
-%   my $link = '';
-%   my $selectable = 0;
-%   my $draggable_ticketid = 0;
+%   my $selectable = 0; # can we schedule a new appointment
+%   my $ticketid = 0;
 %   my $draggable_length = 0;
-%   my $droppable = 0;
-%   my $cells = 0;
+%   my $droppable = 0; # can we reschedule an appointment here
+%   my $cells = 0; # total cell count for appointment
+%   my $offset = 0; # position of cell in appointment
 %
 %   #white out available times
 %   foreach my $avail ( @{ $schedule{'avail'} } ) {
 %     my( $start, $end ) = @$avail;
 %     next if $start >= ($tod_row+$timestep) || $end <= $tod_row;
-%     $bgcolor = 'FFFFFF';
+%     $bgcolor = '#FFFFFF';
+%     $border = '1px solid #D7D7D7';
 %     $selectable = 1
 %       if $LengthMin <= $end - $tod_row  #the slot is long enough
 %       && ! grep { $_ > $tod_row && $LengthMin > $_ - $tod_row }
 %   }
 %
 %   #block out / show / color code existing appointments
-%   #my %line = ();
 %   foreach my $id ( keys %{ $schedule{'scheduled'} } ) {
 %
 %     my( $starts, $due, $col, $t ) = @{ $schedule{'scheduled'}->{$id} };
 %
+%     # misleading loop--at most one id will pass this test
 %     next if $starts >= ($tod_row+$timestep) || $due <= $tod_row;
 %
-%     $bgcolor = $col;
+%     $ticketid = $id;
+%     $bgcolor = '#'.$col;
+%     $border = '1px solid #D7D7D7';
+%     # can't schedule a new appointment
 %     $selectable = 0;
+%     # but can reschedule a ticket overlapping its old slot (filtered by can_drop)
+%     $droppable = 1 unless $scheduling;
+%     $draggable_length = $due - $starts;
+%     $cells = int( ($due-$starts) / $timestep );
+%     $cells++ if ($due-$starts) % $timestep;
 %
 %     if ( $starts >= $tod_row ) { #first row
 %
 %       my %hash = $m->comp('/Ticket/Elements/Customers', Ticket => $t);
 %       my @cust_main = values( %{$hash{cust_main}} );
 %
-%       $content .= ($content?', ':''). #$id. ': '.
-%                   #false laziness w/xmlhttp-ticket-update.html
-%                   FS::sched_avail::pretty_time($starts). '-'.
+%       #false laziness w/xmlhttp-ticket-update.html
+%       $content .= FS::sched_avail::pretty_time($starts). '-'.
 %                   FS::sched_avail::pretty_time($due).
 %                   ': '. $cust_main[0]->_FreesideURILabel;
 %                   #'install for custname XX miles away'; #XXX placeholder/more
-%       $link = qq( <A HREF="$RT::WebPath/Ticket/Display.html?id=$id" target="_blank">view</A> ).
-%               include('/elements/popup_link.html',
-%                         action=>$RT::WebPath.'/Ticket/ModifyCustomFieldsPopup.html?id='.$id,
-%                         label =>'edit',
-%                         actionlabel => 'Edit appointment',
-%                         height      => 436, # better: A + B * (num_custom_fields)
-%                      );
-%       $draggable_ticketid = $id;
-%       $draggable_length = $due - $starts;
-%
-%       $cells = int( ($due-$starts) / $timestep );
-%       $cells++ if ($due-$starts) % $timestep;
-%       
-%     #} else {
-%     #  $content .= ($content?', ':''). $id;
+%
+%     } else {
+%       $offset = int( ($tod_row - $starts) / $timestep );
+%       $offset++ if ($tod_row - $starts) % $timestep;
 %     }
 %   }
 %
 %   my $td_id = 'td_'. $Date->epoch. '_'. $tod_row. '_'. $username;
 
-    <td style = "background-color:#<%$bgcolor%>"
+    <td style = "background-color: <% $bgcolor %>; border: <% $border %>"
           ID="<% $td_id %>"
-        class = "<% ($selectable && $custnum && $LengthMin) ? 'weeklyselectable' : 'weekly' %>"
+        class = "<% ($selectable && $scheduling) ? 'weeklyselectable' : 'weekly' %>"
 %#                 <%   $is_today     ? 'today'
 %#                    : $is_yesterday ? 'yesterday'
 %#                    : $is_aweekago  ? 'aweekago'
@@ -91,7 +90,8 @@
 %#                 %>"
 %     if ( $selectable ) {
 %
-%       if ( $custnum && $LengthMin ) {
+%       # Scheduling a new appointment
+%       if ( $scheduling ) {
 %
 %         #XXX for now, construct a ticket creation URL
 %         # eventually, do much the same, but say "appointment made", show time
                         %>"
           onclick     = "window.location.href = '<% $url %>'"
 %
+%       # If not scheduling, allow drag-and-drop rescheduling
 %       } else {
 %         $droppable = 1;
 %       }
 %
 %     }
-    ><% $content |h %><% $link |n %></td>
+    ><% $content |h %></td>
     <SCRIPT TYPE="text/javascript">
 
-      $('#<% $td_id %>').data('username', "<% $username %>");
-      $('#<% $td_id %>').data('starts',   <% $Date->epoch + $tod_row*60 %>);
-      $('#<% $td_id %>').data('epoch',    <% $Date->epoch %>);
-      $('#<% $td_id %>').data('tod_row',  <% $tod_row %>);
+      var $cell_<% $td_id %> = $('#<% $td_id %>');
+      $cell_<% $td_id %>.data('username', "<% $username %>");
+      $cell_<% $td_id %>.data('starts',   <% $Date->epoch + $tod_row*60 %>);
+      $cell_<% $td_id %>.data('epoch',    <% $Date->epoch %>);
+      $cell_<% $td_id %>.data('tod_row',  <% $tod_row %>);
 
-%     if ( $droppable ) {
-        $('#<% $td_id %>').droppable({
-          over: boxon_drop,
-          drop: reschedule_appointment,
-          tolerance: 'pointer'
-        });
+%     if ($selectable) {
+      set_schedulable_cell($cell_<% $td_id %>);
 %     }
 
-%     if ( $draggable_ticketid ) {
-        $('#<% $td_id %>').draggable({
-          containment: '.titlebox-content',
-%#          revert:      'invalid',
-          revert: true,
-          revertDuration: 0,
-          stop: clear_drag_hi,
-        });
-        $('#<% $td_id %>').data('ticketid', <% $draggable_ticketid %>);
-        $('#<% $td_id %>').data('length',   <% $draggable_length * 60 %>);
-        $('#<% $td_id %>').data('cells',    <% $cells %>);
-        $('#<% $td_id %>').data('bgcolor',  "#<% $bgcolor %>");
+%     if ($ticketid) {
+      set_appointment_cell(
+        $cell_<% $td_id %>,
+        <% $ticketid |js_string %>,
+        <% $bgcolor |n,js_string %>,
+        <% $content |n,js_string %>,
+        <% $draggable_length * 60 %>,
+        <% $cells %>,
+        <% $offset %>
+      );
+%     }
+%     if ( $droppable ) {
+%       if ( $draggable_length ) {
+      set_draggable_cell($cell_<% $td_id %>);
+%       }
+      set_droppable_cell($cell_<% $td_id %>);
 %     }
 
     </SCRIPT>
index 0dbe8c3..b16f609 100644 (file)
@@ -2,8 +2,57 @@
 
 <SCRIPT TYPE="text/javascript">
 
+  // gives cell the appearance dictated by its data
+  function set_data_cell ($cell) {
+    $cell.css('border',  '1px solid #D7D7D7' );
+    $cell.css('background-color', $cell.data('bgcolor'));
+    $cell.html($cell.data('content'));
+  }
+
+  // sets cell data and appearance to schedulable
+  function set_schedulable_cell ($cell) {
+    $cell.data('bgcolor',  '#FFFFFF' );
+    $cell.data('ticketid', 0 );
+    $cell.data('length',   0 );
+    $cell.data('cells',    0 );
+    $cell.data('offset',   0 );
+    $cell.data('label',    '' );
+    $cell.data('content',  '' );
+    set_data_cell($cell);
+  }
+
+  // sets cell data and appearance as an appointment
+  function set_appointment_cell ($cell,ticketid,bgcolor,label,length,cells,offset) {
+    $cell.data('bgcolor',  bgcolor );
+    $cell.css('background-color', bgcolor);
+    $cell.css('border',  '1px solid #D7D7D7' );
+    $cell.data('ticketid', ticketid );
+    $cell.data('length',   length );
+    $cell.data('cells',    cells );
+    $cell.data('offset',   offset );
+    $cell.data('label',  label );
+    $cell.data('content', '');
+    if ( offset == 0 ) { // first row
+      var title = 
+        label +
+        ' <A HREF="<%$RT::WebPath%>/Ticket/Display.html?id=' + ticketid + '" target="_blank">view</A> ' +
+        <% include('/elements/popup_link.html',
+             action=>$RT::WebPath.'/Ticket/ModifyCustomFieldsPopup.html?id=__MAGIC_TICKET_ID__',
+             label =>'edit',
+             actionlabel => 'Edit appointment',
+             height      => 436, # better: A + B * (num_custom_fields)
+          ) |n,js_string
+        %>;
+      title = title.replace( /__MAGIC_TICKET_ID__/, ticketid );
+      $cell.data('content', title);
+    }
+    set_data_cell($cell);
+  }
+
 % if ( $cells ) {
 
+  // hover effects for scheduling new appointment
+
   function boxon(what) {
     var $this = $(what);
     for ( var c=0; c < <%$cells%>; c++) {
       $this.css('background-color', '#ffffdd');
       if ( c == 0 ) {
         $this.css('border-top', '1px double black');
-        $this.css('border-left', '1px double black');
-        $this.css('border-right', '1px solid black');
-      } else if ( c == <%$cells-1%> ) {
-        $this.css('border-left', '1px double black');
-        $this.css('border-right', '1px solid black');
+      }
+      if ( c == <%$cells-1%> ) {
         $this.css('border-bottom', '1px solid black');
-      } else {
-        $this.css('border-left', '1px double black');
-        $this.css('border-right', '1px solid black');
       }
+      $this.css('border-left', '1px double black');
+      $this.css('border-right', '1px solid black');
 
       var rownum = $this.parent().prevAll('tr').length;
       var colnum = $this.prevAll('td').length;
   function boxoff(what) {
     var $this = $(what);
     for ( var c=0; c < <%$cells%>; c++) {
-
-      //$this.css('background-color', '');
-      //$this.css('border', ''); //IE8 woes, removes cell borders
-      $this.removeAttr('style'); //slightly "flashy" on cell changes under IE8
-                                 //but at least it doesn't remove cell borders
-
+      $this.css('background-color', '#ffffff');
+      $this.css('border', '1px solid #D7D7D7'); //watch out in IE8 woes, empty string removes cell borders
       var rownum = $this.parent().prevAll('tr').length;
       var colnum = $this.prevAll('td').length;
       $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
   }
 
 
-% }
+% } else {
+
+  // functions for drag-and-drop rescheduling
 
-% # it would be better if we had draggable-specific droppables, but this will prevent overlap for now...
-  function can_drop ($where, cells) {
+  // ticket-dependant test if we can drop here
+  // prevent overlap with other appointments, while allowing appointment to overlap itself
+  function can_drop ($where, ui) {
+    var cells = ui.draggable.data('cells');
+    var ticketid = ui.draggable.data('ticketid');
     for (var c=0; c < cells; c++) {
       if (!$where.is('.ui-droppable')) {
         return false;
       }
+      if ($where.data('ticketid') && ($where.data('ticketid') != ticketid)) {
+        return false;
+      }
       var rownum = $where.parent().prevAll('tr').length;
       var colnum = $where.prevAll('td').length;
       $where = $where.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
     return true;
   }
 
-  var drag_cells = 0;
+  // makes cell droppable (can reschedule here, subject to can_drop)
+  function set_droppable_cell ($cell) {
+    $cell.droppable({
+      over: appointment_drag_over,
+      drop: reschedule_appointment,
+      tolerance: 'pointer'
+    });
+  }
+
+  // makes cell draggable (able to be rescheduled)
+  function set_draggable_cell ($cell) {
+    $cell.draggable({
+      containment: '.titlebox-content',
+      revert: true,
+      revertDuration: 0,
+      start: appointment_drag_start,
+      stop: appointment_drag_stop,
+    });
+  }
+
+  // gives cell a white (schedulable) appearance, without changing cell data
+  function set_white_cell ($cell) {
+    $cell.css('border',  '1px solid #D7D7D7' );
+    $cell.css('background-color', '#FFFFFF');
+    $cell.html('');
+  }
+
+  // track drag highlighting
   var drag_hi;
 
-  // on drag stop (regardless of if it was dropped)
-  function clear_drag_hi () {
+  // clear drag highlighting
+  function clear_drag_hi (cells) {
     if ( drag_hi ) {
-      boxoff_do(drag_hi);
+      for ( var c=0; c < cells; c++) {
+        if (drag_hi.data('isdragging')) {
+          drag_hi.css('border',  '1px solid #D7D7D7' );
+        } else {
+          set_white_cell(drag_hi);
+        }
+        var rownum = drag_hi.parent().prevAll('tr').length;
+        var colnum = drag_hi.prevAll('td').length;
+        drag_hi = drag_hi.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
+      }
       drag_hi = undefined;
     }
   }
 
-  // on drag over
-  function boxon_drop(event, ui) {
-    //var $this = $(what);
+  // drag start event
+  function appointment_drag_start(event, ui) {
     var $this = $(this);
-
-    drag_cells = ui.draggable.data('cells');
-
-    clear_drag_hi();
-
-    if (!can_drop($this, drag_cells)) return;
-
-    drag_hi = $this;
-
-    for ( var c=0; c < drag_cells; c++) {
-
-      /* well, its not exactly what i want, would prefer if it could properly
-         mouse in-out, but this sorta helps for now?
-         revisit when everthing else is working */
-/*      $this.effect("highlight", {}, 1500); */
-
-      $this.css('background-color', '#ffffdd');
-      if ( c == 0 ) {
-        $this.css('border-top', '1px double black');
-        $this.css('border-left', '1px double black');
-        $this.css('border-right', '1px solid black');
-      } else if ( c == (drag_cells-1) ) {
-        $this.css('border-left', '1px double black');
-        $this.css('border-right', '1px solid black');
-        $this.css('border-bottom', '1px solid black');
-      } else {
-        $this.css('border-left', '1px double black');
-        $this.css('border-right', '1px solid black');
-      }
-
+    // cell that's actually dragging
+    $this.html($this.data('label'));
+    $this.css('z-index',10);
+    $this.data('isdragging',true);
+    var offset = $this.data('offset');
+    var cells  = $this.data('cells');
+    // jump to first cell in appointment
+    var rownum = $this.parent().prevAll('tr').length;
+    var colnum = $this.prevAll('td').length;
+    $this = $this.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
+    // loop through all cells in appointment
+    for ( var c=0; c < cells; c++) {
+      if (c != offset) set_white_cell($this);
       var rownum = $this.parent().prevAll('tr').length;
       var colnum = $this.prevAll('td').length;
       $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
     }
-
-
   }
 
-  // clears highlighted box, used by clear_hi_drag (drag stop event)
-  function boxoff_do(what) {
-
-    var $this = what;
-
-    for ( var c=0; c < drag_cells; c++) {
-
-      //$this.css('background-color', '');
-      //$this.css('border', ''); //IE8 woes, removes cell borders
-      $this.removeAttr('style'); //slightly "flashy" on cell changes under IE8
-                                 //but at least it doesn't remove cell borders
+  // drag stop event
+  function appointment_drag_stop(event, ui) {
+    var $this = $(this);
+    // the cell that was dragging
+    var cells = $this.data('cells');
+    clear_drag_hi(cells);
+    $this.css('z-index','initial');
+    $this.data('isdragging',false);
+    var offset = $this.data('offset');
+    // jump to first cell in appointment
+    var rownum = $this.parent().prevAll('tr').length;
+    var colnum = $this.prevAll('td').length;
+    $this = $this.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
+    // loop through all cells in appointment
+    for ( var c=0; c < cells; c++) {
+      set_data_cell($this);
+      var rownum = $this.parent().prevAll('tr').length;
+      var colnum = $this.prevAll('td').length;
+      $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
+    }
+  }
 
+  // drag over event
+  function appointment_drag_over(event, ui) {
+    // the cell that is dragging
+    var cells = ui.draggable.data('cells');
+       // the droppable cell that you're over
+    var $this = $(this);
+    clear_drag_hi(cells);
+    if (!can_drop($this, ui)) return;
+    drag_hi = $this;
+    // loop through potential appointment cells
+    for ( var c=0; c < cells; c++) {
+      if ( !$this.data('isdragging')) {
+        $this.css('background-color', '#ffffdd');
+      }
+      if ( c == 0 ) {
+        $this.css('border-top', '1px double black');
+      }
+      if ( c == (cells-1) ) {
+        $this.css('border-bottom', '1px solid black');
+      }
+      $this.css('border-left', '1px double black');
+      $this.css('border-right', '1px solid black');
       var rownum = $this.parent().prevAll('tr').length;
       var colnum = $this.prevAll('td').length;
       $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
 
     var $this = $(this);
 
-    if (!can_drop($this, ui.draggable.data('cells'))) return;
+    if (!can_drop($this, ui)) return;
 
 %   #get the ticket number and appointment length (from the draggable object)
-    var ticketid = ui.draggable.data('ticketid');
-    var length   = ui.draggable.data('length');
-    var bgcolor  = ui.draggable.data('bgcolor');
+    var draggable = ui.draggable;
+    var ticketid = draggable.data('ticketid');
+    var length   = draggable.data('length');
+    var bgcolor  = draggable.data('bgcolor');
+    var offset   = draggable.data('offset');
 
 %   #and.. the new date and time, and username (from the droppable object)
     var starts   = $this.data('starts');
     var username = $this.data('username');
-
     var due = parseInt(starts) + parseInt(length);
-
     var n_epoch        = $this.data('epoch');
     var n_st_tod_row   = $this.data('tod_row');
 
-    var draggable = ui.draggable;
     var droppable = $this;
     draggable.effect( "transfer", { to: droppable }, 420 );
 
       if ( data.error && data.error.length ) {
 %       #error?  "that shouldn't happen" but should display 
         alert(data.error);
-%       #XX and should revert the dragable...
-      } else {
 
-        //draggable.effect( "transfer", { to: droppable }, 1000 );
+      } else {
 
         var label = data.sched_label;
 
-%       #remove the old appointment entirely
-        var epoch        = ui.draggable.data('epoch');
-        var st_tod_row   = ui.draggable.data('tod_row');
-        var old_username = ui.draggable.data('username');
-        var cells        = ui.draggable.data('cells');
+        // jump to first cell in appointment
+        var rownum = draggable.parent().prevAll('tr').length;
+        var colnum = draggable.prevAll('td').length;
+        draggable = draggable.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
+
+        // remove old appointment entirely
+        var epoch        = draggable.data('epoch');
+        var st_tod_row   = draggable.data('tod_row');
+        var old_username = draggable.data('username');
+        var cells        = draggable.data('cells');
         for ( var c=0; c < cells; c++) {
           var tod_row = parseInt(st_tod_row) + (c * <%$timestep%>);
           var td_id = 'td_' + epoch +
                       '_' + String( tod_row ) +
                       '_' + old_username;
-          $('#'+td_id).css('background-color', '#FFFFFF');
-          $('#'+td_id).text('');
-%         #(and make those boxes droppable)
-          $('#'+td_id).droppable({
-            over: boxon_drop,
-            drop: reschedule_appointment,
-            tolerance: 'pointer'
-          });
+          var $cell = $('#'+td_id);
+          set_schedulable_cell($cell);
+          $cell.draggable('destroy');
+          set_droppable_cell($cell);
         }
 
-%       #maybe use that animation which shows the box from point A to B
-
-        clear_drag_hi();
+        // set appointment in new position
+        clear_drag_hi(cells);
         for ( var d=0; d < cells; d++) {
           var n_tod_row = parseInt(n_st_tod_row) + (d * <%$timestep%>);
           var n_td_id = 'td_' + n_epoch +
                         '_' + String( n_tod_row ) +
                         '_' + username;
-          $('#'+n_td_id).css('background-color', bgcolor);
-%         #remove their droppable
-          $('#'+n_td_id).droppable('destroy');
-          if ( d == 0 ) {
-            var title = 
-              label +
-              ' <A HREF="<%$RT::WebPath%>/Ticket/Display.html?id=' + ticketid + '" target="_blank">view</A> ' +
-              <% include('/elements/popup_link.html',
-                   action=>$RT::WebPath.'/Ticket/ModifyCustomFieldsPopup.html?id=__MAGIC_TICKET_ID__',
-                   label =>'edit',
-                   actionlabel => 'Edit appointment',
-                   height      => 436, # better: A + B * (num_custom_fields)
-                 ) |n,js_string
-              %>;
-            title = title.replace( /__MAGIC_TICKET_ID__/, ticketid );
-            $('#'+n_td_id).html( title );
-%           #(and make the top draggable, so we could do it all over again)
-            $('#'+n_td_id).draggable({
-              containment: '.titlebox-content',
-%#              revert:      'invalid',
-              revert: true,
-              revertDuration: 0,
-              stop: clear_drag_hi,
-            });
-            $('#'+n_td_id).data('ticketid', ticketid );
-            $('#'+n_td_id).data('length',   length );
-            $('#'+n_td_id).data('cells',    cells );
-            $('#'+n_td_id).data('bgcolor',  bgcolor );
-          }
+          var $cell = $('#'+n_td_id);
+          set_appointment_cell($cell,ticketid,bgcolor,label,length,cells,d);
+          set_draggable_cell($cell);
+          set_droppable_cell($cell);
         }
-
       }
-
     });
-
   }
 
+% } # end of rescheduling functions
+
 </SCRIPT>
 
 <& /Search/Calendar.html,