appointment drag and drop, RT#34237
authorIvan Kohler <ivan@freeside.biz>
Mon, 27 Jul 2015 09:28:20 +0000 (02:28 -0700)
committerIvan Kohler <ivan@freeside.biz>
Mon, 27 Jul 2015 09:28:20 +0000 (02:28 -0700)
httemplate/elements/menu.html
rt/share/html/Elements/CalendarSlotSchedule
rt/share/html/Search/Calendar.html
rt/share/html/Search/Schedule.html
rt/share/static/css/calendar.css

index a01530e..a5fb15b 100644 (file)
@@ -448,6 +448,26 @@ $report_menu{'Logs'}           = [ \%report_logs, 'System and email logs' ]
 $report_menu{'SQL Query'}      = [ $fsurl.'search/report_sql.html', 'SQL Query']
   if $curuser->access_right('Raw SQL');
 
+tie my %tools_customers, 'Tie::IxHash', ();
+$tools_customers{'Appointments'} = [ $fsurl.'rt/Search/Schedule.html?LengthMin=0', 'View appointment schedule' ]
+  if $curuser->access_right('View appointments');
+$tools_customers{'Attachments'} = [ $fsurl.'browse/cust_attachment.html', 'View customer attachments' ]
+  if !$conf->config('disable_cust_attachment') and $curuser->access_right('View attachments') and $curuser->access_right('Browse attachments');
+$tools_customers{'Customer email settings'} = [ $fsurl.'misc/manage_cust_email.html' ]
+  if $curuser->access_right('Edit customer');
+
+tie my %tools_billing, 'Tie::IxHash', ();
+$tools_billing{'Quick payment entry'} =  [ $fsurl.'misc/batch-cust_pay.html', 'Enter multiple payments in a batch' ]
+  if $curuser->access_right('Post payment batch');
+$tools_billing{'Process payment batches'} = [ $fsurl.'search/pay_batch.cgi?magic=_date;open=1;intransit=1', 'Process credit card and electronic check batches' ]
+  if ( $conf->exists('batch-enable') || $conf->config('batch-enable_payby') )
+     && $curuser->access_right('Process batches');
+$tools_billing{'Download invoice batches'} = [ $fsurl.'search/bill_batch.cgi' ]
+  if $curuser->access_right('Process invoice batches')
+  || $curuser->access_right('Process global invoice batches')
+  || $curuser->access_right('Configuration'); #XXX remove in 2.5
+  #now there's a standalone event#if $conf->exists('invoice_print_pdf');
+
 tie my %tools_importing, 'Tie::IxHash',
   'Customers'            => [ $fsurl.'misc/cust_main-import.cgi', '' ],
   'Package definitions'  => [ $fsurl.'misc/part_pkg-import.html', '' ],
@@ -469,9 +489,17 @@ if ( $conf->config('tax_data_vendor') eq 'cch' ) {
   }
 }
 
-tie my %tools_exporting, 'Tie::IxHash',
-  'Download database dump' => [ $fsurl. 'misc/dump.cgi', '' ],
-;
+tie my %tools_misc, 'Tie::IxHash', ();
+$tools_misc{'Bulk DID Orders'} =  [ $fsurl.'browse/did_order.html', 'View/manage bulk DID orders' ]
+  if $curuser->access_right('Import');
+$tools_misc{'Business card scan'} = [ $fsurl.'edit/prospect_main-upload.html' ]
+  if $curuser->access_right('New prospect');
+$tools_misc{'Time Queue'} =  [ $fsurl.'search/report_timeworked.html', 'View pending support time' ]
+  if $curuser->access_right('Time queue');
+
+#tie my %tools_exporting, 'Tie::IxHash',
+#  'Download database dump' => [ $fsurl. 'misc/dump.cgi', '' ],
+#;
 
 tie my %tools_ticketing_articles, 'Tie::IxHash',
   'Overview' => [ $fsurl.'rt/Articles/index.html', '' ],
@@ -489,38 +517,27 @@ tie my %tools_ticketing, 'Tie::IxHash',
 $tools_ticketing{'Cron Tool'} = [ $fsurl.'rt/Developer/CronTool/', '' ]
   if $conf->exists('rt-crontool');
 
-tie my %tools_menu, 'Tie::IxHash', ();
-$tools_menu{'Quick payment entry'} =  [ $fsurl.'misc/batch-cust_pay.html', 'Enter multiple payments in a batch' ]
-  if $curuser->access_right('Post payment batch');
-$tools_menu{'Process payment batches'} = [ $fsurl.'search/pay_batch.cgi?magic=_date;open=1;intransit=1', 'Process credit card and electronic check batches' ]
-  if ( $conf->exists('batch-enable') || $conf->config('batch-enable_payby') )
-     && $curuser->access_right('Process batches');
-$tools_menu{'Download invoice batches'} = [ $fsurl.'search/bill_batch.cgi' ]
-  if $curuser->access_right('Process invoice batches')
-  || $curuser->access_right('Process global invoice batches')
-  || $curuser->access_right('Configuration'); #XXX remove in 2.5
-  #now there's a standalone event#if $conf->exists('invoice_print_pdf');
-$tools_menu{'Bulk DID Orders'} =  [ $fsurl.'browse/did_order.html', 'View/manage bulk DID orders' ]
-  if $curuser->access_right('Import');
-$tools_menu{'Job Queue'} =  [ $fsurl.'search/queue.html', 'View pending job queue' ]
+tie my %tools_system, 'Tie::IxHash', ();
+$tools_system{'Status'} = [ $fsurl.'view/Status.html', 'System status' ]
+  if $curuser->access_right('Configuration'); # 'View system status');
+$tools_system{'Job Queue'} =  [ $fsurl.'search/queue.html', 'View pending job queue' ]
   if $curuser->access_right('Job queue');
+
+tie my %tools_menu, 'Tie::IxHash', ();
+$tools_menu{'Customers'} = [ \%tools_customers, 'Customer tools' ]
+  if keys %tools_customers;
+$tools_menu{'Billing'} = [ \%tools_billing, 'Payment and invoice tools' ]
+  if keys %tools_billing;
 $tools_menu{'Ticketing'} = [ \%tools_ticketing, 'Ticketing tools' ]
   if $conf->config('ticket_system');
-$tools_menu{'Customer email settings'} = [ $fsurl.'misc/manage_cust_email.html' ]
-  if $curuser->access_right('Edit customer');
-$tools_menu{'Business card scan'} = [ $fsurl.'edit/prospect_main-upload.html' ]
-  if $curuser->access_right('New prospect');
-$tools_menu{'Time Queue'} =  [ $fsurl.'search/report_timeworked.html', 'View pending support time' ]
-  if $curuser->access_right('Time queue');
-$tools_menu{'Attachments'} = [ $fsurl.'browse/cust_attachment.html', 'View customer attachments' ]
-  if !$conf->config('disable_cust_attachment') and $curuser->access_right('View attachments') and $curuser->access_right('Browse attachments');
+$tools_menu{'Miscellaneous'} = [ \%tools_misc, '' ]
+  if keys %tools_misc;
 $tools_menu{'Importing'} =  [ \%tools_importing, 'Import tools' ]
   if $curuser->access_right('Import');
-$tools_menu{'Exporting'} =  [ \%tools_exporting, 'Export tools' ]
-  if $curuser->access_right('Export');
-$tools_menu{'Status'} = [ $fsurl.'view/Status.html', 'System status' ]
-  if $curuser->access_right('Configuration'); # 'View system status');
-
+#$tools_menu{'Exporting'} =  [ \%tools_exporting, 'Export tools' ]
+#  if $curuser->access_right('Export');
+$tools_menu{'System'} = [ \%tools_system, 'System tools' ]
+  if keys %tools_menu;
 
 tie my %config_employees, 'Tie::IxHash',
   'Employees' => [ $fsurl.'browse/access_user.html', 'Setup internal users' ],
index ff3e634..045d6e4 100644 (file)
 %   my $bgcolor = '666666;border-color:#555555';
 %   my $content = '';
 %   my $selectable = 0;
+%   my $draggable_ticketid = 0;
+%   my $draggable_length = 0;
+%   my $droppable = 0;
+%   my $cells = 0;
 %
 %   #white out available times
 %   foreach my $avail ( @{ $schedule{'avail'} } ) {
 %     $selectable = 0;
 %
 %     if ( $starts >= $tod_row ) { #first row
-%       $content .= ($content?', ':''). $id.
-%                   ': '. FS::sched_avail::pretty_time($starts). '-'.
-%                         FS::sched_avail::pretty_time($due);
+%       $content .= ($content?', ':''). #$id. ': '.
+%                   #false laziness w/xmlhttp-ticket-update.html
+%                   FS::sched_avail::pretty_time($starts). '-'.
+%                   FS::sched_avail::pretty_time($due);
 %                   #'install for custname XX miles away'; #XXX placeholder/more
+%       $draggable_ticketid = $id;
+%       $draggable_length = $due - $starts;
+%
+%       $cells = int( ($due-$starts) / $timestep );
+%       $cells++ if ($due-$starts) % $timestep;
+%       
 %     #} else {
 %     #  $content .= ($content?', ':''). $id;
 %     }
 %   }
+%
+%   my $td_id = 'td_'. $Date->epoch. '_'. $tod_row. '_'. $username;
 
-    <td style="background:#<%$bgcolor%>"
-        class="<% $selectable ? 'weeklyselectable' : 'weekly' %>"
-%#               <%   $is_today     ? 'today'
-%#                  : $is_yesterday ? 'yesterday'
-%#                  : $is_aweekago  ? 'aweekago'
-%#                  : ''
-%#               %>"
+    <td style = "background-color:#<%$bgcolor%>"
+          ID="<% $td_id %>"
+        class = "<% ($selectable && $custnum && $LengthMin) ? 'weeklyselectable' : 'weekly' %>"
+%#                 <%   $is_today     ? 'today'
+%#                    : $is_yesterday ? 'yesterday'
+%#                    : $is_aweekago  ? 'aweekago'
+%#                    : ''
+%#                 %>"
 %     if ( $selectable ) {
 %
-%       #XXX for now, construct a ticket creation URL
-%       # eventually, do much the same, but say "appointment made", show time
-%       # and date, have # options to do things with it? etc.
-%       # then redir back to customer/appointment view i guess
-%
-%       #abstraction is leaking like a sieve... linking back to freeside cust
-%       # (XXX and eventually, package)
-%       my $cust_main = qsearchs('cust_main', { custnum=>$custnum } )
-%         or die "unknown custnum $custnum";
-%       my $Queue = $cust_main->agent->ticketing_queueid || 1; # || $default_queueid;#XXX really, pick pkg_category queue
-%       my $member = "freeside://freeside/cust_main/$custnum";
-%
-%warn       my $Starts = int($tod_row/60). ':'. sprintf('%02d',$tod_row%60). ':00';
-%warn       my $Due    = int(($tod_row+$LengthMin)/60). ':'.
-%                    sprintf('%02d',($tod_row+$LengthMin)%60). ':00';
-%
-%       my $url = $RT::WebPath. '/Ticket/Display.html?id=new'.
-%                 "&Queue=$Queue".
-%                 "&Owner=$username".
-%                 '&Starts='. $Date->strftime('%F').'%20'. $Starts.
-%                 '&Due='.    $Date->strftime('%F').'%20'. $Due.
-%                 '&new-MemberOf='. $member. #XXX uri_escape?
-%                 '&Status=new';
-%                 #'&Requestors='. #XXX Freeside customer requestor(s) (package?
+%       if ( $custnum && $LengthMin ) {
+%
+%         #XXX for now, construct a ticket creation URL
+%         # eventually, do much the same, but say "appointment made", show time
+%         # and date, have # options to do things with it? etc.
+%         # then redir back to customer/appointment view i guess
+%
+%         #abstraction is leaking like a sieve... linking back to freeside cust
+%         # (XXX and eventually, package)
+%         my $cust_main = qsearchs('cust_main', { custnum=>$custnum } )
+%           or die "unknown custnum $custnum";
+%         my $Queue = $cust_main->agent->ticketing_queueid || 1; # || $default_queueid;#XXX really, pick pkg_category queue
+%         my $member = "freeside://freeside/cust_main/$custnum";
+%
+%warn         my $Starts = int($tod_row/60). ':'. sprintf('%02d',$tod_row%60). ':00';
+%warn         my $Due    = int(($tod_row+$LengthMin)/60). ':'.
+%                      sprintf('%02d',($tod_row+$LengthMin)%60). ':00';
+%
+%         my $url = $RT::WebPath. '/Ticket/Display.html?id=new'.
+%                   "&Queue=$Queue".
+%                   "&Owner=$username".
+%                   '&Starts='. $Date->strftime('%F').'%20'. $Starts.
+%                   '&Due='.    $Date->strftime('%F').'%20'. $Due.
+%                   '&new-MemberOf='. $member. #XXX uri_escape?
+%                   '&Status=new';
+%                   #'&Requestors='. #XXX Freeside customer requestor(s) (package?
 
-        onmouseover = "boxon(this);"
-        onmouseout  = "boxoff(this);"
-        title       = "<% 'Make appointment for '.
-                            FS::sched_avail::pretty_time($tod_row). '-'.
-                            FS::sched_avail::pretty_time($tod_row+$LengthMin)
-                      %>"
-        onclick     = "window.location.href = '<% $url %>'"
+          onmouseover = "boxon(this);"
+          onmouseout  = "boxoff(this);"
+          title       = "<% 'Make appointment for '.
+                              FS::sched_avail::pretty_time($tod_row). '-'.
+                              FS::sched_avail::pretty_time($tod_row+$LengthMin)
+                        %>"
+          onclick     = "window.location.href = '<% $url %>'"
+%
+%       } else {
+%         $droppable = 1;
+%       }
+%
 %     }
     ><% $content %></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 %>);
+
+%     if ( $droppable ) {
+        $('#<% $td_id %>').droppable({
+          over: boxon_drop,
+          drop: reschedule_appointment,
+          tolerance: 'pointer'
+        });
+%     }
+
+%     if ( $draggable_ticketid ) {
+        $('#<% $td_id %>').draggable({
+          containment: '.titlebox-content',
+%#          revert:      'invalid',
+          revert: true,
+          revertDuration: 0,
+        });
+        $('#<% $td_id %>').data('ticketid', <% $draggable_ticketid %>);
+        $('#<% $td_id %>').data('length',   <% $draggable_length * 60 %>);
+        $('#<% $td_id %>').data('cells',    <% $cells %>);
+        $('#<% $td_id %>').data('bgcolor',  "#<% $bgcolor %>");
+%     }
+
+    </SCRIPT>
 % }
 <%ONCE>
 my $default_slots = RT->Config->Get('CalendarWeeklySlots') || 5;
index 092f6a5..2c19296 100644 (file)
@@ -78,7 +78,7 @@ $DimPast     => 0
     </table>
 % }
 
-<table class="rtxcalendar">
+<table class="<% $WeekDay ? 'rtxweeklycalendar' : 'rtxcalendar' %>">
 
   <thead>
 
index 34ba142..be5a140 100644 (file)
@@ -1,14 +1,9 @@
-<& /Elements/Header, Title => 'Schedule' &>
-
-%#init_overlib.html
-%foreach my $file (@files) {
-<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/<%$file%>.js"></SCRIPT>
-%}
-
-<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/jquery.js"></SCRIPT>
+<& /Elements/Header, Title => 'Schedule', JavaScript => 0 &>
 
 <SCRIPT TYPE="text/javascript">
 
+% if ( $cells ) {
+
   function boxon(what) {
     var $this = $(what);
     for ( var c=0; c < <%$cells%>; c++) {
     }
   }
 
+
+% }
+
+  var drag_cells = 0;
+  var drag_hi;
+  function boxon_drop(event, ui) {
+    //var $this = $(what);
+    var $this = $(this);
+
+    drag_cells = ui.draggable.data('cells');
+
+    if ( drag_hi ) {
+      boxoff_do(drag_hi);
+    }
+    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');
+      }
+
+      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);
+    }
+
+
+  }
+
+  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
+
+      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);
+    }
+  }
+
+  function reschedule_appointment( event, ui ) {
+
+%   #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');
+
+%   #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 }, 1000 );
+
+%   #tell the backend to reschedule it
+    var url = "<% popurl(3) %>misc/xmlhttp-ticket-update.html?" +
+              "id=" + ticketid + ";starts=" + starts + ";due=" + due +
+              ";username=" + username;
+
+    $.getJSON( url, function( data ) {
+      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 );
+
+        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');
+        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'
+          });
+        }
+
+%       #maybe use that animation which shows the box from point A to B
+
+        if ( drag_hi ) {
+          boxoff_do(drag_hi);
+        }
+        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 ) {
+            $('#'+n_td_id).text(label);
+%           #(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,
+            });
+            $('#'+n_td_id).data('ticketid', ticketid );
+            $('#'+n_td_id).data('length',   length );
+            $('#'+n_td_id).data('cells',    cells );
+            $('#'+n_td_id).data('bgcolor',  bgcolor );
+          }
+        }
+
+      }
+
+    });
+
+  }
+
 </SCRIPT>
 
 <& /Search/Calendar.html,
@@ -86,9 +234,13 @@ if ( ref($ARGS{username}) ) {
 } elsif ( $ARGS{username} ) {
   @usernames = ( $ARGS{username} );
 } else {
-  #XXX shouldn't even get offered the link in the first place rather than perl
-  # barf, but this is better than erroring out later or empty @username
-  die "Can't schedule an appointment - no employees are configured as installers";
+  #look them up ourslves... again, more FS abstraction-leaking, but 
+  # we want to link to the schedule view, and better than doing this every
+  # menu render
+  use FS::Record qw( qsearch );
+  use FS::sched_item;
+  my @sched_item = qsearch('sched_item', { 'disabled' => '', });
+  @usernames = map $_->access_user->username, @sched_item;
 }
 
 ( my $LengthMin = $ARGS{LengthMin} ) =~ /^\d+$/ or die 'non-numeric LengthMin';
index a91917c..566f969 100644 (file)
@@ -201,3 +201,7 @@ table.rtxweeklycalendar td.labels {
     border-bottom: 1px solid #eeeeee;
 }
 
+.ui-effects-transfer {
+  border: 1px solid black;
+}
+