1 <& /Elements/Header, Title => 'Schedule', JavaScript => 0 &>
3 <SCRIPT TYPE="text/javascript">
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'));
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', '' );
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
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__',
42 actionlabel => 'Edit appointment',
43 height => 436, # better: A + B * (num_custom_fields)
46 title = title.replace( /__MAGIC_TICKET_ID__/, ticketid );
47 $cell.data('content', title);
54 // hover effects for scheduling new appointment
56 function boxon(what) {
58 for ( var c=0; c < <%$cells%>; c++) {
60 $this.css('background-color', '#ffffdd');
62 $this.css('border-top', '1px double black');
64 if ( c == <%$cells-1%> ) {
65 $this.css('border-bottom', '1px solid black');
67 $this.css('border-left', '1px double black');
68 $this.css('border-right', '1px solid black');
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);
76 function boxoff(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);
90 // functions for drag-and-drop rescheduling
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')) {
101 if ($where.data('ticketid') && ($where.data('ticketid') != ticketid)) {
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);
111 // makes cell droppable (can reschedule here, subject to can_drop)
112 function set_droppable_cell ($cell) {
114 over: appointment_drag_over,
115 drop: reschedule_appointment,
120 // makes cell draggable (able to be rescheduled)
121 function set_draggable_cell ($cell) {
123 containment: '.titlebox-content',
126 start: appointment_drag_start,
127 stop: appointment_drag_stop,
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');
138 // track drag highlighting
141 // clear drag highlighting
142 function clear_drag_hi (cells) {
144 for ( var c=0; c < cells; c++) {
145 if (drag_hi.data('isdragging')) {
146 drag_hi.css('border', '1px solid #D7D7D7' );
148 set_white_cell(drag_hi);
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);
159 function appointment_drag_start(event, ui) {
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);
181 function appointment_drag_stop(event, ui) {
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);
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
208 clear_drag_hi(cells);
209 if (!can_drop($this, ui)) return;
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');
217 $this.css('border-top', '1px double black');
219 if ( c == (cells-1) ) {
220 $this.css('border-bottom', '1px solid black');
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);
231 function reschedule_appointment( event, ui ) {
235 if (!can_drop($this, ui)) return;
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');
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');
251 var droppable = $this;
252 draggable.effect( "transfer", { to: droppable }, 420 );
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;
259 $.getJSON( url, function( data ) {
260 if ( data.error && data.error.length ) {
261 % #error? "that shouldn't happen" but should display
266 var label = data.sched_label;
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);
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 ) +
283 var $cell = $('#'+td_id);
284 set_schedulable_cell($cell);
285 $cell.draggable('destroy');
286 set_droppable_cell($cell);
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 ) +
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);
305 % } # end of rescheduling functions
309 <& /Search/Calendar.html,
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',
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},
329 my $timestep = RT->Config->Get('CalendarWeeklySizeMin') || 30; #1/2h
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";
340 #if ( ! $initialized ) {
341 push @files, map "overlibmws$_", ( '', qw( _iframe _draggable _crossframe ) );
342 push @files, map { "${_}contentmws" } qw( iframe ajax );
346 if ( ref($ARGS{username}) ) {
347 @usernames = @{ $ARGS{username} };
348 } elsif ( $ARGS{username} ) {
349 @usernames = ( $ARGS{username} );
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
354 use FS::Record qw( qsearch );
356 my @sched_item = qsearch('sched_item', { 'disabled' => '', });
357 @usernames = map $_->access_user->username, @sched_item;
360 ( my $LengthMin = $ARGS{LengthMin} ) =~ /^\d+$/ or die 'non-numeric LengthMin';
362 my $cells = int($LengthMin / $timestep);
363 $cells++ if $LengthMin % $timestep;