RT#34237: installer scheduling [major refactor, many bugs fixed]
[freeside.git] / rt / share / html / Search / Schedule.html
1 <& /Elements/Header, Title => 'Schedule', JavaScript => 0 &>
2
3 <SCRIPT TYPE="text/javascript">
4
5   // gives cell the appearance dictated by its data
6   function set_data_cell ($cell) {
7     $cell.css('border',  '1px solid #D7D7D7' );
8     $cell.css('background-color', $cell.data('bgcolor'));
9     $cell.html($cell.data('content'));
10   }
11
12   // sets cell data and appearance to schedulable
13   function set_schedulable_cell ($cell) {
14     $cell.data('bgcolor',  '#FFFFFF' );
15     $cell.data('ticketid', 0 );
16     $cell.data('length',   0 );
17     $cell.data('cells',    0 );
18     $cell.data('offset',   0 );
19     $cell.data('label',    '' );
20     $cell.data('content',  '' );
21     set_data_cell($cell);
22   }
23
24   // sets cell data and appearance as an appointment
25   function set_appointment_cell ($cell,ticketid,bgcolor,label,length,cells,offset) {
26     $cell.data('bgcolor',  bgcolor );
27     $cell.css('background-color', bgcolor);
28     $cell.css('border',  '1px solid #D7D7D7' );
29     $cell.data('ticketid', ticketid );
30     $cell.data('length',   length );
31     $cell.data('cells',    cells );
32     $cell.data('offset',   offset );
33     $cell.data('label',  label );
34     $cell.data('content', '');
35     if ( offset == 0 ) { // first row
36       var title = 
37         label +
38         ' <A HREF="<%$RT::WebPath%>/Ticket/Display.html?id=' + ticketid + '" target="_blank">view</A> ' +
39         <% include('/elements/popup_link.html',
40              action=>$RT::WebPath.'/Ticket/ModifyCustomFieldsPopup.html?id=__MAGIC_TICKET_ID__',
41              label =>'edit',
42              actionlabel => 'Edit appointment',
43              height      => 436, # better: A + B * (num_custom_fields)
44           ) |n,js_string
45         %>;
46       title = title.replace( /__MAGIC_TICKET_ID__/, ticketid );
47       $cell.data('content', title);
48     }
49     set_data_cell($cell);
50   }
51
52 % if ( $cells ) {
53
54   // hover effects for scheduling new appointment
55
56   function boxon(what) {
57     var $this = $(what);
58     for ( var c=0; c < <%$cells%>; c++) {
59
60       $this.css('background-color', '#ffffdd');
61       if ( c == 0 ) {
62         $this.css('border-top', '1px double black');
63       }
64       if ( c == <%$cells-1%> ) {
65         $this.css('border-bottom', '1px solid black');
66       }
67       $this.css('border-left', '1px double black');
68       $this.css('border-right', '1px solid black');
69
70       var rownum = $this.parent().prevAll('tr').length;
71       var colnum = $this.prevAll('td').length;
72       $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
73     }
74   }
75
76   function boxoff(what) {
77     var $this = $(what);
78     for ( var c=0; c < <%$cells%>; c++) {
79       $this.css('background-color', '#ffffff');
80       $this.css('border', '1px solid #D7D7D7'); //watch out in IE8 woes, empty string removes cell borders
81       var rownum = $this.parent().prevAll('tr').length;
82       var colnum = $this.prevAll('td').length;
83       $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
84     }
85   }
86
87
88 % } else {
89
90   // functions for drag-and-drop rescheduling
91
92   // ticket-dependant test if we can drop here
93   // prevent overlap with other appointments, while allowing appointment to overlap itself
94   function can_drop ($where, ui) {
95     var cells = ui.draggable.data('cells');
96     var ticketid = ui.draggable.data('ticketid');
97     for (var c=0; c < cells; c++) {
98       if (!$where.is('.ui-droppable')) {
99         return false;
100       }
101       if ($where.data('ticketid') && ($where.data('ticketid') != ticketid)) {
102         return false;
103       }
104       var rownum = $where.parent().prevAll('tr').length;
105       var colnum = $where.prevAll('td').length;
106       $where = $where.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
107     }
108     return true;
109   }
110
111   // makes cell droppable (can reschedule here, subject to can_drop)
112   function set_droppable_cell ($cell) {
113     $cell.droppable({
114       over: appointment_drag_over,
115       drop: reschedule_appointment,
116       tolerance: 'pointer'
117     });
118   }
119
120   // makes cell draggable (able to be rescheduled)
121   function set_draggable_cell ($cell) {
122     $cell.draggable({
123       containment: '.titlebox-content',
124       revert: true,
125       revertDuration: 0,
126       start: appointment_drag_start,
127       stop: appointment_drag_stop,
128     });
129   }
130
131   // gives cell a white (schedulable) appearance, without changing cell data
132   function set_white_cell ($cell) {
133     $cell.css('border',  '1px solid #D7D7D7' );
134     $cell.css('background-color', '#FFFFFF');
135     $cell.html('');
136   }
137
138   // track drag highlighting
139   var drag_hi;
140
141   // clear drag highlighting
142   function clear_drag_hi (cells) {
143     if ( drag_hi ) {
144       for ( var c=0; c < cells; c++) {
145         if (drag_hi.data('isdragging')) {
146           drag_hi.css('border',  '1px solid #D7D7D7' );
147         } else {
148           set_white_cell(drag_hi);
149         }
150         var rownum = drag_hi.parent().prevAll('tr').length;
151         var colnum = drag_hi.prevAll('td').length;
152         drag_hi = drag_hi.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
153       }
154       drag_hi = undefined;
155     }
156   }
157
158   // drag start event
159   function appointment_drag_start(event, ui) {
160     var $this = $(this);
161     // cell that's actually dragging
162     $this.html($this.data('label'));
163     $this.css('z-index',10);
164     $this.data('isdragging',true);
165     var offset = $this.data('offset');
166     var cells  = $this.data('cells');
167     // jump to first cell in appointment
168     var rownum = $this.parent().prevAll('tr').length;
169     var colnum = $this.prevAll('td').length;
170     $this = $this.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
171     // loop through all cells in appointment
172     for ( var c=0; c < cells; c++) {
173       if (c != offset) set_white_cell($this);
174       var rownum = $this.parent().prevAll('tr').length;
175       var colnum = $this.prevAll('td').length;
176       $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
177     }
178   }
179
180   // drag stop event
181   function appointment_drag_stop(event, ui) {
182     var $this = $(this);
183     // the cell that was dragging
184     var cells = $this.data('cells');
185     clear_drag_hi(cells);
186     $this.css('z-index','initial');
187     $this.data('isdragging',false);
188     var offset = $this.data('offset');
189     // jump to first cell in appointment
190     var rownum = $this.parent().prevAll('tr').length;
191     var colnum = $this.prevAll('td').length;
192     $this = $this.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
193     // loop through all cells in appointment
194     for ( var c=0; c < cells; c++) {
195       set_data_cell($this);
196       var rownum = $this.parent().prevAll('tr').length;
197       var colnum = $this.prevAll('td').length;
198       $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
199     }
200   }
201
202   // drag over event
203   function appointment_drag_over(event, ui) {
204     // the cell that is dragging
205     var cells = ui.draggable.data('cells');
206         // the droppable cell that you're over
207     var $this = $(this);
208     clear_drag_hi(cells);
209     if (!can_drop($this, ui)) return;
210     drag_hi = $this;
211     // loop through potential appointment cells
212     for ( var c=0; c < cells; c++) {
213       if ( !$this.data('isdragging')) {
214         $this.css('background-color', '#ffffdd');
215       }
216       if ( c == 0 ) {
217         $this.css('border-top', '1px double black');
218       }
219       if ( c == (cells-1) ) {
220         $this.css('border-bottom', '1px solid black');
221       }
222       $this.css('border-left', '1px double black');
223       $this.css('border-right', '1px solid black');
224       var rownum = $this.parent().prevAll('tr').length;
225       var colnum = $this.prevAll('td').length;
226       $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
227     }
228   }
229
230   // drop event
231   function reschedule_appointment( event, ui ) {
232
233     var $this = $(this);
234
235     if (!can_drop($this, ui)) return;
236
237 %   #get the ticket number and appointment length (from the draggable object)
238     var draggable = ui.draggable;
239     var ticketid = draggable.data('ticketid');
240     var length   = draggable.data('length');
241     var bgcolor  = draggable.data('bgcolor');
242     var offset   = draggable.data('offset');
243
244 %   #and.. the new date and time, and username (from the droppable object)
245     var starts   = $this.data('starts');
246     var username = $this.data('username');
247     var due = parseInt(starts) + parseInt(length);
248     var n_epoch        = $this.data('epoch');
249     var n_st_tod_row   = $this.data('tod_row');
250
251     var droppable = $this;
252     draggable.effect( "transfer", { to: droppable }, 420 );
253
254 %   #tell the backend to reschedule it
255     var url = "<% popurl(3) %>misc/xmlhttp-ticket-update.html?" +
256               "id=" + ticketid + ";starts=" + starts + ";due=" + due +
257               ";username=" + username;
258
259     $.getJSON( url, function( data ) {
260       if ( data.error && data.error.length ) {
261 %       #error?  "that shouldn't happen" but should display 
262         alert(data.error);
263
264       } else {
265
266         var label = data.sched_label;
267
268         // jump to first cell in appointment
269         var rownum = draggable.parent().prevAll('tr').length;
270         var colnum = draggable.prevAll('td').length;
271         draggable = draggable.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
272
273         // remove old appointment entirely
274         var epoch        = draggable.data('epoch');
275         var st_tod_row   = draggable.data('tod_row');
276         var old_username = draggable.data('username');
277         var cells        = draggable.data('cells');
278         for ( var c=0; c < cells; c++) {
279           var tod_row = parseInt(st_tod_row) + (c * <%$timestep%>);
280           var td_id = 'td_' + epoch +
281                       '_' + String( tod_row ) +
282                       '_' + old_username;
283           var $cell = $('#'+td_id);
284           set_schedulable_cell($cell);
285           $cell.draggable('destroy');
286           set_droppable_cell($cell);
287         }
288
289         // set appointment in new position
290         clear_drag_hi(cells);
291         for ( var d=0; d < cells; d++) {
292           var n_tod_row = parseInt(n_st_tod_row) + (d * <%$timestep%>);
293           var n_td_id = 'td_' + n_epoch +
294                         '_' + String( n_tod_row ) +
295                         '_' + username;
296           var $cell = $('#'+n_td_id);
297           set_appointment_cell($cell,ticketid,bgcolor,label,length,cells,d);
298           set_draggable_cell($cell);
299           set_droppable_cell($cell);
300         }
301       }
302     });
303   }
304
305 % } # end of rescheduling functions
306
307 </SCRIPT>
308
309 <& /Search/Calendar.html,
310      @_,
311      Query       => "( Status = 'new' OR Status = 'open' OR Status = 'stalled')
312                      AND ( Type = 'reminder' OR 'Type' = 'ticket' )
313                      AND Queue = $queueid ",
314      slots       => scalar(@usernames),
315      Embed       => 'Schedule.html',
316      DimPast     => 1,
317      Display     => 'Schedule',
318      DisplayArgs => [ username  => \@usernames,
319                       LengthMin => $LengthMin,
320                       #oops, more freeside abstraction-leaking
321                       custnum   => $ARGS{custnum},
322                       pkgnum    => $ARGS{pkgnum},
323                       RedirectToBasics => $ARGS{RedirectToBasics},
324                     ],
325 &>
326
327 <%ONCE>
328
329 my $timestep =  RT->Config->Get('CalendarWeeklySizeMin') || 30; #1/2h
330
331 </%ONCE>
332 <%init>
333
334 #abstraction-leaking
335 my $conf = new FS::Conf;
336 my $queueid = $conf->config('ticket_system-appointment-queueid')
337   or die "ticket_system-appointment-queueid configuration not set";
338
339 my @files = ();
340 #if ( ! $initialized ) {
341   push @files, map "overlibmws$_", ( '', qw( _iframe _draggable _crossframe ) );
342   push @files, map { "${_}contentmws" } qw( iframe ajax );
343 #%}
344
345 my @usernames = ();
346 if ( ref($ARGS{username}) ) {
347   @usernames = @{ $ARGS{username} };
348 } elsif ( $ARGS{username} ) {
349   @usernames = ( $ARGS{username} );
350 } else {
351   #look them up ourslves... again, more FS abstraction-leaking, but 
352   # we want to link to the schedule view, and better than doing this every
353   # menu render
354   use FS::Record qw( qsearch );
355   use FS::sched_item;
356   my @sched_item = qsearch('sched_item', { 'disabled' => '', });
357   @usernames = map $_->access_user->username, @sched_item;
358 }
359
360 ( my $LengthMin = $ARGS{LengthMin} ) =~ /^\d+$/ or die 'non-numeric LengthMin';
361
362 my $cells = int($LengthMin / $timestep);
363 $cells++ if $LengthMin % $timestep;
364
365 </%init>