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