1 <& /Elements/Header, Title => 'Schedule', JavaScript => 0 &>
3 <SCRIPT TYPE="text/javascript">
5 // sets cell content and bgcolor in a div, for use as a draggable
6 // (draggable tds have border problems on FF/IE)
7 function set_cell_div ($cell,content,bgcolor) {
8 var $div = $cell.data('div');
10 $div = $(document.createElement('div'));
11 $div.data('cell',$cell);
12 $cell.data('div',$div);
15 $div.css('white-space','nowrap');
16 $div.css('width','100%');
17 $div.css('background-color', bgcolor);
18 $div.html(content || ' <br> <br> ');
21 // gives cell the appearance dictated by its data
22 function set_data_cell ($cell) {
23 $cell.css('border', '1px solid #D7D7D7' );
24 $cell.css('background-color', $cell.data('bgcolor'));
25 set_cell_div($cell,$cell.data('content'),$cell.data('bgcolor'));
28 // sets cell data and appearance to schedulable
29 function set_schedulable_cell ($cell) {
30 $cell.data('bgcolor', '#FFFFFF' );
31 $cell.data('ticketid', 0 );
32 $cell.data('length', 0 );
33 $cell.data('cells', 0 );
34 $cell.data('offset', 0 );
35 $cell.data('label', '' );
36 $cell.data('content', '' );
40 // sets cell data and appearance as an appointment
41 function set_appointment_cell ($cell,ticketid,bgcolor,labeltime,labeltitle,length,cells,offset) {
42 $cell.data('bgcolor', bgcolor );
43 $cell.data('ticketid', ticketid );
44 $cell.data('length', length );
45 $cell.data('cells', cells );
46 $cell.data('offset', offset );
47 var label = labeltime + ' <br>' + labeltitle + ' <br>';
48 $cell.data('label', label );
49 $cell.data('content', '');
50 if ( offset == 0 ) { // first row
53 ' <A HREF="<%$RT::WebPath%>/Ticket/Display.html?id=' + ticketid + '" target="_blank">view</A> ' +
54 <% include('/elements/popup_link.html',
55 action=>$RT::WebPath.'/Ticket/ModifyCustomFieldsPopup.html?id=__MAGIC_TICKET_ID__',
57 actionlabel => 'Edit appointment',
58 height => 436, # better: A + B * (num_custom_fields)
61 title = title.replace( /__MAGIC_TICKET_ID__/, ticketid );
62 $cell.data('content', title);
69 // hover effects for scheduling new appointment
71 function boxon(what) {
73 for ( var c=0; c < <%$cells%>; c++) {
75 $this.css('background-color', '#ffffdd');
76 set_cell_div($this,'','#ffffdd');
78 $this.css('border-top', '1px double black');
80 if ( c == <%$cells-1%> ) {
81 $this.css('border-bottom', '1px solid black');
83 $this.css('border-left', '1px double black');
84 $this.css('border-right', '1px solid black');
86 var rownum = $this.parent().prevAll('tr').length;
87 var colnum = $this.prevAll('td').length;
88 $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
92 function boxoff(what) {
94 for ( var c=0; c < <%$cells%>; c++) {
95 $this.css('background-color', '#ffffff');
96 set_cell_div($this,'','#ffffff');
97 $this.css('border', '1px solid #D7D7D7'); //watch out in IE8 woes, empty string removes cell borders
98 var rownum = $this.parent().prevAll('tr').length;
99 var colnum = $this.prevAll('td').length;
100 $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
107 // functions for drag-and-drop rescheduling
109 // ticket-dependant test if we can drop here
110 // prevent overlap with other appointments, while allowing appointment to overlap itself
111 function can_drop ($where, ui) {
112 var cells = ui.draggable.data('cell').data('cells');
113 var ticketid = ui.draggable.data('cell').data('ticketid');
114 for (var c=0; c < cells; c++) {
115 if (!$where.is('.ui-droppable')) {
118 if ($where.data('ticketid')) {
119 if ($where.data('ticketid') != ticketid) {
122 if ($where.data('offset') == c) { // don't reschedule in the same slot
126 var rownum = $where.parent().prevAll('tr').length;
127 var colnum = $where.prevAll('td').length;
128 $where = $where.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
133 // makes cell droppable (can reschedule here, subject to can_drop)
134 function set_droppable_cell ($cell) {
136 over: appointment_drag_over,
137 drop: reschedule_appointment,
142 // makes cell draggable (able to be rescheduled)
143 function set_draggable_cell ($cell) {
144 var $div = $cell.data('div');
146 containment: '.titlebox-content',
149 start: appointment_drag_start,
150 stop: appointment_drag_stop,
155 // gives cell a white (schedulable) appearance, without changing cell data
156 function set_white_cell ($cell) {
157 $cell.css('border', '1px solid #D7D7D7' );
158 $cell.css('background-color', '#FFFFFF');
159 set_cell_div($cell,'','#FFFFFF');
162 // track drag highlighting
165 // clear drag highlighting
166 function clear_drag_hi (cells) {
168 for ( var c=0; c < cells; c++) {
169 if (drag_hi.data('isdragging')) {
170 drag_hi.css('border', '1px solid #D7D7D7' );
171 drag_hi.css('background-color', '#FFFFFF' );
173 set_white_cell(drag_hi);
175 var rownum = drag_hi.parent().prevAll('tr').length;
176 var colnum = drag_hi.prevAll('td').length;
177 drag_hi = drag_hi.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
184 function appointment_drag_start(event, ui) {
186 // cell that's dragging
187 $this = $this.data('cell');
188 set_cell_div($this,$this.data('label'),$this.data('bgcolor'));
189 $this.data('isdragging',true);
190 var offset = $this.data('offset');
191 var cells = $this.data('cells');
192 // jump to first cell in appointment
193 var rownum = $this.parent().prevAll('tr').length;
194 var colnum = $this.prevAll('td').length;
195 $this = $this.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
196 // loop through all cells in appointment
197 for ( var c=0; c < cells; c++) {
198 if ($this.data('isdragging')) {
199 $this.css('background-color', '#FFFFFF');
201 set_white_cell($this);
203 var rownum = $this.parent().prevAll('tr').length;
204 var colnum = $this.prevAll('td').length;
205 $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
210 function appointment_drag_stop(event, ui) {
212 // cell that's dragging
213 $this = $this.data('cell');
214 var cells = $this.data('cells');
215 clear_drag_hi(cells);
216 $this.data('isdragging',false);
217 var offset = $this.data('offset');
218 // jump to first cell in appointment
219 var rownum = $this.parent().prevAll('tr').length;
220 var colnum = $this.prevAll('td').length;
221 $this = $this.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
222 // loop through all cells in appointment
223 for ( var c=0; c < cells; c++) {
224 set_data_cell($this);
225 var rownum = $this.parent().prevAll('tr').length;
226 var colnum = $this.prevAll('td').length;
227 $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
232 function appointment_drag_over(event, ui) {
233 // the cell that's dragging
234 var cells = ui.draggable.data('cell').data('cells');
235 // the droppable cell that you're over
237 clear_drag_hi(cells);
238 if (!can_drop($this, ui)) return;
240 // loop through potential appointment cells
241 for ( var c=0; c < cells; c++) {
242 $this.css('background-color', '#ffffdd');
243 if ( !$this.data('isdragging')) {
244 set_cell_div($this,'','#ffffdd');
246 $this.css('background-color','#ffffdd');
249 $this.css('border-top', '1px double black');
251 if ( c == (cells-1) ) {
252 $this.css('border-bottom', '1px solid black');
254 $this.css('border-left', '1px double black');
255 $this.css('border-right', '1px solid black');
256 var rownum = $this.parent().prevAll('tr').length;
257 var colnum = $this.prevAll('td').length;
258 $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
263 function reschedule_appointment( event, ui ) {
265 // the droppable cell that you're over
268 if (!can_drop($this, ui)) return;
270 % #get the ticket number and appointment length (from the draggable object)
271 var dragcell = ui.draggable.data('cell');
272 var ticketid = dragcell.data('ticketid');
273 var length = dragcell.data('length');
274 var bgcolor = dragcell.data('bgcolor');
275 var offset = dragcell.data('offset');
277 % #and.. the new date and time, and username (from the droppable object)
278 var starts = $this.data('starts');
279 var username = $this.data('username');
280 var due = parseInt(starts) + parseInt(length);
281 var n_epoch = $this.data('epoch');
282 var n_st_tod_row = $this.data('tod_row');
284 var droppable = $this;
285 ui.draggable.effect( "transfer", { to: droppable }, 420 );
287 % #tell the backend to reschedule it
288 var url = "<% popurl(3) %>misc/xmlhttp-ticket-update.html?" +
289 "id=" + ticketid + ";starts=" + starts + ";due=" + due +
290 ";username=" + username;
292 $.getJSON( url, function( data ) {
293 if ( data.error && data.error.length ) {
294 % #error? "that shouldn't happen" but should display
299 var label = data.sched_label;
300 var labeltime = data.sched_label_time;
301 var labeltitle = data.sched_label_title;
303 // jump to first cell in appointment
304 var rownum = dragcell.parent().prevAll('tr').length;
305 var colnum = dragcell.prevAll('td').length;
306 dragcell = dragcell.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
308 // remove old appointment entirely
309 var epoch = dragcell.data('epoch');
310 var st_tod_row = dragcell.data('tod_row');
311 var old_username = dragcell.data('username');
312 var cells = dragcell.data('cells');
313 for ( var c=0; c < cells; c++) {
314 var tod_row = parseInt(st_tod_row) + (c * <%$timestep%>);
315 var td_id = 'td_' + epoch +
316 '_' + String( tod_row ) +
318 var $cell = $('#'+td_id);
319 $cell.data('div').draggable('destroy');
320 set_schedulable_cell($cell);
321 set_droppable_cell($cell);
324 // set appointment in new position
325 clear_drag_hi(cells);
326 for ( var d=0; d < cells; d++) {
327 var n_tod_row = parseInt(n_st_tod_row) + (d * <%$timestep%>);
328 var n_td_id = 'td_' + n_epoch +
329 '_' + String( n_tod_row ) +
331 var $cell = $('#'+n_td_id);
332 set_appointment_cell($cell,ticketid,bgcolor,labeltime,labeltitle,length,cells,d);
333 set_draggable_cell($cell);
334 set_droppable_cell($cell);
340 % } # end of rescheduling functions
344 <& /Search/Calendar.html,
346 Query => "( Status = 'new' OR Status = 'open' OR Status = 'stalled')
347 AND ( Type = 'reminder' OR 'Type' = 'ticket' )
348 AND Queue = $queueid ",
349 slots => scalar(@usernames),
350 Embed => 'Schedule.html',
352 Display => 'Schedule',
353 DisplayArgs => [ username => \@usernames,
354 LengthMin => $LengthMin,
355 #oops, more freeside abstraction-leaking
356 custnum => $ARGS{custnum},
357 pkgnum => $ARGS{pkgnum},
358 RedirectToBasics => $ARGS{RedirectToBasics},
364 my $timestep = RT->Config->Get('CalendarWeeklySizeMin') || 30; #1/2h
370 my $conf = new FS::Conf;
371 my $queueid = $conf->config('ticket_system-appointment-queueid')
372 or die "ticket_system-appointment-queueid configuration not set";
375 #if ( ! $initialized ) {
376 push @files, map "overlibmws$_", ( '', qw( _iframe _draggable _crossframe ) );
377 push @files, map { "${_}contentmws" } qw( iframe ajax );
381 if ( ref($ARGS{username}) ) {
382 @usernames = @{ $ARGS{username} };
383 } elsif ( $ARGS{username} ) {
384 @usernames = ( $ARGS{username} );
386 #look them up ourslves... again, more FS abstraction-leaking, but
387 # we want to link to the schedule view, and better than doing this every
389 use FS::Record qw( qsearch );
391 my @sched_item = qsearch('sched_item', { 'disabled' => '', });
392 @usernames = map $_->access_user->username, @sched_item;
395 ( my $LengthMin = $ARGS{LengthMin} ) =~ /^\d+$/ or die 'non-numeric LengthMin';
397 my $cells = int($LengthMin / $timestep);
398 $cells++ if $LengthMin % $timestep;