Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / share / html / Search / Schedule.html
index 0f8896c..df7b53d 100644 (file)
-<& /Elements/Header, Title => 'Schedule' &>
+<& /Elements/Header, Title => 'Schedule', JavaScript => 0 &>
 
-%#init_overlib.html
-%foreach my $file (@files) {
-<SCRIPT TYPE="text/javascript" SRC="<%$fsurl%>elements/<%$file%>.js"></SCRIPT>
-%}
+<SCRIPT TYPE="text/javascript">
+
+  // sets cell content and bgcolor in a div, for use as a draggable
+  //   (draggable tds have border problems on FF/IE)
+  function set_cell_div ($cell,content,bgcolor) {
+    var $div = $cell.data('div');
+    if (!$div) {
+      $div = $(document.createElement('div'));
+      $div.data('cell',$cell);
+      $cell.data('div',$div);
+      $cell.append($div);
+    }
+    $div.css('white-space','nowrap');
+    $div.css('width','100%');
+    $div.css('background-color', bgcolor);
+    $div.html(content || '&nbsp;<br>&nbsp;<br>&nbsp;');
+  }
+
+  // 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'));
+    set_cell_div($cell,$cell.data('content'),$cell.data('bgcolor'));
+  }
+
+  // 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,labeltime,labeltitle,length,cells,offset) {
+    $cell.data('bgcolor',  bgcolor );
+    $cell.data('ticketid', ticketid );
+    $cell.data('length',   length );
+    $cell.data('cells',    cells );
+    $cell.data('offset',   offset );
+    var label = labeltime + ' <br>' + labeltitle + ' <br>';
+    $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');
+      set_cell_div($this,'','#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);
+    }
+  }
+
+  function boxoff(what) {
+    var $this = $(what);
+    for ( var c=0; c < <%$cells%>; c++) {
+      $this.css('background-color', '#ffffff');
+      set_cell_div($this,'','#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
+
+  // 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('cell').data('cells');
+    var ticketid = ui.draggable.data('cell').data('ticketid');
+    for (var c=0; c < cells; c++) {
+      if (!$where.is('.ui-droppable')) {
+        return false;
+      }
+      if ($where.data('ticketid')) {
+        if ($where.data('ticketid') != ticketid) {
+          return false;
+        }
+        if ($where.data('offset') == c) { // don't reschedule in the same slot
+          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;
+  }
+
+  // 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) {
+    var $div = $cell.data('div');
+    $div.draggable({
+      containment: '.titlebox-content',
+      revert: true,
+      revertDuration: 0,
+      start: appointment_drag_start,
+      stop: appointment_drag_stop,
+      zIndex: 10,
+    });
+  }
+
+  // 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');
+    set_cell_div($cell,'','#FFFFFF');
+  }
+
+  // track drag highlighting
+  var drag_hi;
+
+  // clear drag highlighting
+  function clear_drag_hi (cells) {
+    if ( drag_hi ) {
+      for ( var c=0; c < cells; c++) {
+        if (drag_hi.data('isdragging')) {
+          drag_hi.css('border',  '1px solid #D7D7D7' );
+          drag_hi.css('background-color',  '#FFFFFF' );
+        } 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;
+    }
+  }
+
+  // drag start event
+  function appointment_drag_start(event, ui) {
+    var $this = $(this);
+    // cell that's dragging
+    $this = $this.data('cell');
+    set_cell_div($this,$this.data('label'),$this.data('bgcolor'));
+    $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 ($this.data('isdragging')) {
+        $this.css('background-color', '#FFFFFF');
+      } else {
+        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);
+    }
+  }
+
+  // drag stop event
+  function appointment_drag_stop(event, ui) {
+    var $this = $(this);
+    // cell that's dragging
+    $this = $this.data('cell');
+    var cells = $this.data('cells');
+    clear_drag_hi(cells);
+    $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's dragging
+    var cells = ui.draggable.data('cell').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++) {
+      $this.css('background-color', '#ffffdd');
+      if ( !$this.data('isdragging')) {
+        set_cell_div($this,'','#ffffdd');
+      } else {
+        $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);
+    }
+  }
+
+  // drop event
+  function reschedule_appointment( event, ui ) {
+
+       // the droppable cell that you're over
+    var $this = $(this);
+
+    if (!can_drop($this, ui)) return;
+
+%   #get the ticket number and appointment length (from the draggable object)
+    var dragcell = ui.draggable.data('cell');
+    var ticketid = dragcell.data('ticketid');
+    var length   = dragcell.data('length');
+    var bgcolor  = dragcell.data('bgcolor');
+    var offset   = dragcell.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 droppable = $this;
+    ui.draggable.effect( "transfer", { to: droppable }, 420 );
+
+%   #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);
+
+      } else {
+
+        var label = data.sched_label;
+        var labeltime = data.sched_label_time;
+        var labeltitle = data.sched_label_title;
+
+        // jump to first cell in appointment
+        var rownum = dragcell.parent().prevAll('tr').length;
+        var colnum = dragcell.prevAll('td').length;
+        dragcell = dragcell.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
+
+        // remove old appointment entirely
+        var epoch        = dragcell.data('epoch');
+        var st_tod_row   = dragcell.data('tod_row');
+        var old_username = dragcell.data('username');
+        var cells        = dragcell.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;
+          var $cell = $('#'+td_id);
+          $cell.data('div').draggable('destroy');
+          set_schedulable_cell($cell);
+          set_droppable_cell($cell);
+        }
+
+        // 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;
+          var $cell = $('#'+n_td_id);
+          set_appointment_cell($cell,ticketid,bgcolor,labeltime,labeltitle,length,cells,d);
+          set_draggable_cell($cell);
+          set_droppable_cell($cell);
+        }
+      }
+    });
+  }
+
+% } # end of rescheduling functions
+
+</SCRIPT>
 
 <& /Search/Calendar.html,
      @_,
-     Embed          => 'Schedule.html',
+     Query       => "( Status = 'new' OR Status = 'open' OR Status = 'stalled')
+                     AND ( Type = 'reminder' OR 'Type' = 'ticket' )
+                     AND Queue = $queueid ",
+     slots       => scalar(@usernames),
+     Embed       => 'Schedule.html',
+     DimPast     => 1,
      Display     => 'Schedule',
-     DisplayArgs => [ username => $ARGS{username} ],
+     DisplayArgs => [ username  => \@usernames,
+                      LengthMin => $LengthMin,
+                      #oops, more freeside abstraction-leaking
+                      custnum   => $ARGS{custnum},
+                      pkgnum    => $ARGS{pkgnum},
+                      RedirectToBasics => $ARGS{RedirectToBasics},
+                    ],
 &>
 
+<%ONCE>
+
+my $timestep =  RT->Config->Get('CalendarWeeklySizeMin') || 30; #1/2h
+
+</%ONCE>
 <%init>
+
+#abstraction-leaking
+my $conf = new FS::Conf;
+my $queueid = $conf->config('ticket_system-appointment-queueid')
+  or die "ticket_system-appointment-queueid configuration not set";
+
 my @files = ();
 #if ( ! $initialized ) {
   push @files, map "overlibmws$_", ( '', qw( _iframe _draggable _crossframe ) );
   push @files, map { "${_}contentmws" } qw( iframe ajax );
 #%}
+
+my @usernames = ();
+if ( ref($ARGS{username}) ) {
+  @usernames = @{ $ARGS{username} };
+} elsif ( $ARGS{username} ) {
+  @usernames = ( $ARGS{username} );
+} else {
+  #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';
+
+my $cells = int($LengthMin / $timestep);
+$cells++ if $LengthMin % $timestep;
+
 </%init>