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('width','100%');
16 $div.css('background-color', bgcolor);
17 $div.html(content || ' ');
20 // gives cell the appearance dictated by its data
21 function set_data_cell ($cell) {
22 $cell.css('border', '1px solid #D7D7D7' );
23 $cell.css('background-color', $cell.data('bgcolor'));
24 set_cell_div($cell,$cell.data('content'),$cell.data('bgcolor'));
27 // sets cell data and appearance to schedulable
28 function set_schedulable_cell ($cell) {
29 $cell.data('bgcolor', '#FFFFFF' );
30 $cell.data('ticketid', 0 );
31 $cell.data('length', 0 );
32 $cell.data('cells', 0 );
33 $cell.data('offset', 0 );
34 $cell.data('label', '' );
35 $cell.data('content', '' );
39 // sets cell data and appearance as an appointment
40 function set_appointment_cell ($cell,ticketid,bgcolor,label,length,cells,offset) {
41 $cell.data('bgcolor', bgcolor );
42 $cell.data('ticketid', ticketid );
43 $cell.data('length', length );
44 $cell.data('cells', cells );
45 $cell.data('offset', offset );
46 $cell.data('label', label );
47 $cell.data('content', '');
48 if ( offset == 0 ) { // first row
51 ' <A HREF="<%$RT::WebPath%>/Ticket/Display.html?id=' + ticketid + '" target="_blank">view</A> ' +
52 <% include('/elements/popup_link.html',
53 action=>$RT::WebPath.'/Ticket/ModifyCustomFieldsPopup.html?id=__MAGIC_TICKET_ID__',
55 actionlabel => 'Edit appointment',
56 height => 436, # better: A + B * (num_custom_fields)
59 title = title.replace( /__MAGIC_TICKET_ID__/, ticketid );
60 $cell.data('content', title);
67 // hover effects for scheduling new appointment
69 function boxon(what) {
71 for ( var c=0; c < <%$cells%>; c++) {
73 $this.css('background-color', '#ffffdd');
74 set_cell_div($this,'','#ffffdd');
76 $this.css('border-top', '1px double black');
78 if ( c == <%$cells-1%> ) {
79 $this.css('border-bottom', '1px solid black');
81 $this.css('border-left', '1px double black');
82 $this.css('border-right', '1px solid black');
84 var rownum = $this.parent().prevAll('tr').length;
85 var colnum = $this.prevAll('td').length;
86 $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
90 function boxoff(what) {
92 for ( var c=0; c < <%$cells%>; c++) {
93 $this.css('background-color', '#ffffff');
94 set_cell_div($this,'','#ffffff');
95 $this.css('border', '1px solid #D7D7D7'); //watch out in IE8 woes, empty string removes cell borders
96 var rownum = $this.parent().prevAll('tr').length;
97 var colnum = $this.prevAll('td').length;
98 $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
105 // functions for drag-and-drop rescheduling
107 // ticket-dependant test if we can drop here
108 // prevent overlap with other appointments, while allowing appointment to overlap itself
109 function can_drop ($where, ui) {
110 var cells = ui.draggable.data('cell').data('cells');
111 var ticketid = ui.draggable.data('cell').data('ticketid');
112 for (var c=0; c < cells; c++) {
113 if (!$where.is('.ui-droppable')) {
116 if ($where.data('ticketid')) {
117 if ($where.data('ticketid') != ticketid) {
120 if ($where.data('offset') == c) { // don't reschedule in the same slot
124 var rownum = $where.parent().prevAll('tr').length;
125 var colnum = $where.prevAll('td').length;
126 $where = $where.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
131 // makes cell droppable (can reschedule here, subject to can_drop)
132 function set_droppable_cell ($cell) {
134 over: appointment_drag_over,
135 drop: reschedule_appointment,
140 // makes cell draggable (able to be rescheduled)
141 function set_draggable_cell ($cell) {
142 var $div = $cell.data('div');
144 containment: '.titlebox-content',
147 start: appointment_drag_start,
148 stop: appointment_drag_stop,
153 // gives cell a white (schedulable) appearance, without changing cell data
154 function set_white_cell ($cell) {
155 $cell.css('border', '1px solid #D7D7D7' );
156 $cell.css('background-color', '#FFFFFF');
157 set_cell_div($cell,'','#FFFFFF');
160 // track drag highlighting
163 // clear drag highlighting
164 function clear_drag_hi (cells) {
166 for ( var c=0; c < cells; c++) {
167 if (drag_hi.data('isdragging')) {
168 drag_hi.css('border', '1px solid #D7D7D7' );
169 drag_hi.css('background-color', '#FFFFFF' );
171 set_white_cell(drag_hi);
173 var rownum = drag_hi.parent().prevAll('tr').length;
174 var colnum = drag_hi.prevAll('td').length;
175 drag_hi = drag_hi.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
182 function appointment_drag_start(event, ui) {
184 // cell that's dragging
185 $this = $this.data('cell');
186 set_cell_div($this,$this.data('label'),$this.data('bgcolor'));
187 $this.data('isdragging',true);
188 var offset = $this.data('offset');
189 var cells = $this.data('cells');
190 // jump to first cell in appointment
191 var rownum = $this.parent().prevAll('tr').length;
192 var colnum = $this.prevAll('td').length;
193 $this = $this.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
194 // loop through all cells in appointment
195 for ( var c=0; c < cells; c++) {
196 if ($this.data('isdragging')) {
197 $this.css('background-color', '#FFFFFF');
199 set_white_cell($this);
201 var rownum = $this.parent().prevAll('tr').length;
202 var colnum = $this.prevAll('td').length;
203 $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
208 function appointment_drag_stop(event, ui) {
210 // cell that's dragging
211 $this = $this.data('cell');
212 var cells = $this.data('cells');
213 clear_drag_hi(cells);
214 $this.data('isdragging',false);
215 var offset = $this.data('offset');
216 // jump to first cell in appointment
217 var rownum = $this.parent().prevAll('tr').length;
218 var colnum = $this.prevAll('td').length;
219 $this = $this.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
220 // loop through all cells in appointment
221 for ( var c=0; c < cells; c++) {
222 set_data_cell($this);
223 var rownum = $this.parent().prevAll('tr').length;
224 var colnum = $this.prevAll('td').length;
225 $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
230 function appointment_drag_over(event, ui) {
231 // the cell that's dragging
232 var cells = ui.draggable.data('cell').data('cells');
233 // the droppable cell that you're over
235 clear_drag_hi(cells);
236 if (!can_drop($this, ui)) return;
238 // loop through potential appointment cells
239 for ( var c=0; c < cells; c++) {
240 $this.css('background-color', '#ffffdd');
241 if ( !$this.data('isdragging')) {
242 set_cell_div($this,'','#ffffdd');
244 $this.css('background-color','#ffffdd');
247 $this.css('border-top', '1px double black');
249 if ( c == (cells-1) ) {
250 $this.css('border-bottom', '1px solid black');
252 $this.css('border-left', '1px double black');
253 $this.css('border-right', '1px solid black');
254 var rownum = $this.parent().prevAll('tr').length;
255 var colnum = $this.prevAll('td').length;
256 $this = $this.parent().parent().children('tr').eq(rownum+1).children('td').eq(colnum);
261 function reschedule_appointment( event, ui ) {
263 // the droppable cell that you're over
266 if (!can_drop($this, ui)) return;
268 % #get the ticket number and appointment length (from the draggable object)
269 var dragcell = ui.draggable.data('cell');
270 var ticketid = dragcell.data('ticketid');
271 var length = dragcell.data('length');
272 var bgcolor = dragcell.data('bgcolor');
273 var offset = dragcell.data('offset');
275 % #and.. the new date and time, and username (from the droppable object)
276 var starts = $this.data('starts');
277 var username = $this.data('username');
278 var due = parseInt(starts) + parseInt(length);
279 var n_epoch = $this.data('epoch');
280 var n_st_tod_row = $this.data('tod_row');
282 var droppable = $this;
283 ui.draggable.effect( "transfer", { to: droppable }, 420 );
285 % #tell the backend to reschedule it
286 var url = "<% popurl(3) %>misc/xmlhttp-ticket-update.html?" +
287 "id=" + ticketid + ";starts=" + starts + ";due=" + due +
288 ";username=" + username;
290 $.getJSON( url, function( data ) {
291 if ( data.error && data.error.length ) {
292 % #error? "that shouldn't happen" but should display
297 var label = data.sched_label;
299 // jump to first cell in appointment
300 var rownum = dragcell.parent().prevAll('tr').length;
301 var colnum = dragcell.prevAll('td').length;
302 dragcell = dragcell.parent().parent().children('tr').eq(rownum-offset).children('td').eq(colnum);
304 // remove old appointment entirely
305 var epoch = dragcell.data('epoch');
306 var st_tod_row = dragcell.data('tod_row');
307 var old_username = dragcell.data('username');
308 var cells = dragcell.data('cells');
309 for ( var c=0; c < cells; c++) {
310 var tod_row = parseInt(st_tod_row) + (c * <%$timestep%>);
311 var td_id = 'td_' + epoch +
312 '_' + String( tod_row ) +
314 var $cell = $('#'+td_id);
315 $cell.data('div').draggable('destroy');
316 set_schedulable_cell($cell);
317 set_droppable_cell($cell);
320 // set appointment in new position
321 clear_drag_hi(cells);
322 for ( var d=0; d < cells; d++) {
323 var n_tod_row = parseInt(n_st_tod_row) + (d * <%$timestep%>);
324 var n_td_id = 'td_' + n_epoch +
325 '_' + String( n_tod_row ) +
327 var $cell = $('#'+n_td_id);
328 set_appointment_cell($cell,ticketid,bgcolor,label,length,cells,d);
329 set_draggable_cell($cell);
330 set_droppable_cell($cell);
336 % } # end of rescheduling functions
340 <& /Search/Calendar.html,
342 Query => "( Status = 'new' OR Status = 'open' OR Status = 'stalled')
343 AND ( Type = 'reminder' OR 'Type' = 'ticket' )
344 AND Queue = $queueid ",
345 slots => scalar(@usernames),
346 Embed => 'Schedule.html',
348 Display => 'Schedule',
349 DisplayArgs => [ username => \@usernames,
350 LengthMin => $LengthMin,
351 #oops, more freeside abstraction-leaking
352 custnum => $ARGS{custnum},
353 pkgnum => $ARGS{pkgnum},
354 RedirectToBasics => $ARGS{RedirectToBasics},
360 my $timestep = RT->Config->Get('CalendarWeeklySizeMin') || 30; #1/2h
366 my $conf = new FS::Conf;
367 my $queueid = $conf->config('ticket_system-appointment-queueid')
368 or die "ticket_system-appointment-queueid configuration not set";
371 #if ( ! $initialized ) {
372 push @files, map "overlibmws$_", ( '', qw( _iframe _draggable _crossframe ) );
373 push @files, map { "${_}contentmws" } qw( iframe ajax );
377 if ( ref($ARGS{username}) ) {
378 @usernames = @{ $ARGS{username} };
379 } elsif ( $ARGS{username} ) {
380 @usernames = ( $ARGS{username} );
382 #look them up ourslves... again, more FS abstraction-leaking, but
383 # we want to link to the schedule view, and better than doing this every
385 use FS::Record qw( qsearch );
387 my @sched_item = qsearch('sched_item', { 'disabled' => '', });
388 @usernames = map $_->access_user->username, @sched_item;
391 ( my $LengthMin = $ARGS{LengthMin} ) =~ /^\d+$/ or die 'non-numeric LengthMin';
393 my $cells = int($LengthMin / $timestep);
394 $cells++ if $LengthMin % $timestep;