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.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
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__',
40 actionlabel => 'Edit appointment',
41 height => 436, # better: A + B * (num_custom_fields)
44 title = title.replace( /__MAGIC_TICKET_ID__/, ticketid );
45 $cell.data('content', title);
52 // hover effects for scheduling new appointment
54 function boxon(what) {
56 for ( var c=0; c < <%$cells%>; c++) {
58 $this.css('background-color', '#ffffdd');
60 $this.css('border-top', '1px double black');
62 if ( c == <%$cells-1%> ) {
63 $this.css('border-bottom', '1px solid black');
65 $this.css('border-left', '1px double black');
66 $this.css('border-right', '1px solid black');
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);
74 function boxoff(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);
88 // functions for drag-and-drop rescheduling
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')) {
99 if ($where.data('ticketid') && ($where.data('ticketid') != ticketid)) {
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);
109 // makes cell droppable (can reschedule here, subject to can_drop)
110 function set_droppable_cell ($cell) {
112 over: appointment_drag_over,
113 drop: reschedule_appointment,
118 // makes cell draggable (able to be rescheduled)
119 function set_draggable_cell ($cell) {
121 containment: '.titlebox-content',
124 start: appointment_drag_start,
125 stop: appointment_drag_stop,
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');
136 // track drag highlighting
139 // clear drag highlighting
140 function clear_drag_hi (cells) {
142 for ( var c=0; c < cells; c++) {
143 if (drag_hi.data('isdragging')) {
144 drag_hi.css('border', '1px solid #D7D7D7' );
146 set_white_cell(drag_hi);
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);
157 function appointment_drag_start(event, ui) {
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);
179 function appointment_drag_stop(event, ui) {
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);
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
206 clear_drag_hi(cells);
207 if (!can_drop($this, ui)) return;
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');
215 $this.css('border-top', '1px double black');
217 if ( c == (cells-1) ) {
218 $this.css('border-bottom', '1px solid black');
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);
229 function reschedule_appointment( event, ui ) {
233 if (!can_drop($this, ui)) return;
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');
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');
249 var droppable = $this;
250 draggable.effect( "transfer", { to: droppable }, 420 );
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;
257 $.getJSON( url, function( data ) {
258 if ( data.error && data.error.length ) {
259 % #error? "that shouldn't happen" but should display
264 var label = data.sched_label;
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);
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 ) +
281 var $cell = $('#'+td_id);
282 set_schedulable_cell($cell);
283 $cell.draggable('destroy');
284 set_droppable_cell($cell);
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 ) +
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);
303 % } # end of rescheduling functions
307 <& /Search/Calendar.html,
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',
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},
327 my $timestep = RT->Config->Get('CalendarWeeklySizeMin') || 30; #1/2h
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";
338 #if ( ! $initialized ) {
339 push @files, map "overlibmws$_", ( '', qw( _iframe _draggable _crossframe ) );
340 push @files, map { "${_}contentmws" } qw( iframe ajax );
344 if ( ref($ARGS{username}) ) {
345 @usernames = @{ $ARGS{username} };
346 } elsif ( $ARGS{username} ) {
347 @usernames = ( $ARGS{username} );
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
352 use FS::Record qw( qsearch );
354 my @sched_item = qsearch('sched_item', { 'disabled' => '', });
355 @usernames = map $_->access_user->username, @sched_item;
358 ( my $LengthMin = $ARGS{LengthMin} ) =~ /^\d+$/ or die 'non-numeric LengthMin';
360 my $cells = int($LengthMin / $timestep);
361 $cells++ if $LengthMin % $timestep;