]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/circ/patron/items_out.js
LP#1697954 TODO comments for client sort all-fetching
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / patron / items_out.js
1 /**
2  * List of patron items checked out
3  */
4
5 angular.module('egPatronApp')
6
7 .controller('PatronItemsOutCtrl',
8        ['$scope','$q','$routeParams','$timeout','egCore','egUser','patronSvc','$location',
9         'egGridDataProvider','$uibModal','egCirc','egConfirmDialog','egBilling','$window',
10 function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $location, 
11          egGridDataProvider , $uibModal , egCirc , egConfirmDialog , egBilling , $window) {
12
13     // list of noncatatloged circulations. Define before initTab to 
14     // avoid any possibility of race condition, since they are loaded
15     // during init, but may be referenced before init completes.
16     $scope.noncat_list = [];
17
18     $scope.initTab('items_out', $routeParams.id).then(function() {
19         // sort inline to support paging
20         $scope.noncat_list = patronSvc.noncat_ids.sort();
21     });
22
23     // cache of circ objects for grid display
24     patronSvc.items_out = [];
25
26     // main list of checked out items
27     $scope.main_list = [];
28
29     // list of alt circs (lost, etc.) and/or check-in with fines circs
30     $scope.alt_list = []; 
31
32     // these are fetched during startup (i.e. .configure())
33     // By default, show lost/lo/cr items in the alt list
34     var display_lost = Number(
35         egCore.env.aous['ui.circ.items_out.lost']) || 2;
36     var display_lo = Number(
37         egCore.env.aous['ui.circ.items_out.longoverdue']) || 2;
38     var display_cr = Number(
39         egCore.env.aous['ui.circ.items_out.claimsreturned']) || 2;
40
41     var fetch_checked_in = true;
42     $scope.show_alt_circs = true;
43     if (display_lost & 4 && display_lo & 4 && display_cr & 4) {
44         // all special types are configured to be hidden once
45         // checked in, so there's no need to fetch checked-in circs.
46         fetch_checked_in = false;
47
48         if (display_lost & 1 && display_lo & 1 && display_cr & 1) {                 
49             // additionally, if all types are configured to display    
50             // in the main list while checked out, nothing will         
51             // ever appear in the alternate list, so we can hide          
52             // the alternate list from the UI.  
53             $scope.show_alt_circs = false;
54         }
55     }
56
57     $scope.items_out_display = 'main';
58     $scope.show_main_list = function() {
59         // don't need a full reset_page() to swap tabs
60         $scope.items_out_display = 'main';
61         patronSvc.items_out = [];
62         provider.refresh();
63     }
64
65     $scope.show_alt_list = function() {
66         // don't need a full reset_page() to swap tabs
67         $scope.items_out_display = 'alt';
68         patronSvc.items_out = [];
69         provider.refresh();
70     }
71
72     $scope.show_noncat_list = function() {
73         // don't need a full reset_page() to swap tabs
74         $scope.items_out_display = 'noncat';
75         patronSvc.items_out = [];
76         provider.refresh();
77     }
78
79     // Reload the user to pick up changes in items out, fines, etc.
80     // Reload circs since the contents of the main vs. alt list may
81     // have changed.
82     function reset_page() {
83         patronSvc.refreshPrimary();
84         patronSvc.items_out = []; 
85         $scope.main_list = [];
86         $scope.alt_list = [];
87         provider.refresh() 
88     }
89
90     var provider = egGridDataProvider.instance({});
91     $scope.gridDataProvider = provider;
92
93     function fetch_circs(id_list, offset, count) {
94         if (!id_list.length) return $q.when();
95
96         // fetch the lot of circs and stream the results back via notify
97         return egCore.pcrud.search('circ', {id : id_list},
98             {   flesh : 4,
99                 flesh_fields : {
100                     circ : ['target_copy', 'workstation', 'checkin_workstation'],
101                     acp : ['call_number', 'holds_count', 'status'],
102                     acn : ['record'],
103                     bre : ['simple_record']
104                 },
105                 // avoid fetching the MARC blob by specifying which 
106                 // fields on the bre to select.  More may be needed.
107                 // note that fleshed fields are explicitly selected.
108                 select : { bre : ['id'] },
109                 // TODO: LP#1697954 Fetch all circs on grid render 
110                 // to support client-side sorting.  Migrate to server-side
111                 // sorting to avoid the need for fetching all items.
112                 //limit  : count,
113                 //offset : offset,
114                 // we need an order-by to support paging
115                 order_by : {circ : ['xact_start']} 
116
117         }).then(null, null, function(circ) {
118             circ.circ_lib(egCore.org.get(circ.circ_lib())); // local fleshing
119
120             if (circ.target_copy().call_number().id() == -1) {
121                 // dummy-up a record for precat items
122                 circ.target_copy().call_number().record().simple_record({
123                     title : function() {return circ.target_copy().dummy_title()},
124                     author : function() {return circ.target_copy().dummy_author()},
125                     isbn : function() {return circ.target_copy().dummy_isbn()}
126                 })
127             }
128
129             patronSvc.items_out.push(circ); // toss it into the cache
130             return circ;
131         });
132     }
133
134     function fetch_noncat_circs(id_list, offset, count) {
135         if (!id_list.length) return $q.when();
136
137         return egCore.pcrud.search('ancc', {id : id_list},
138             {   flesh : 1,
139                 flesh_fields : {ancc : ['item_type','staff']},
140                 // TODO: LP#1697954 Fetch all circs on grid render 
141                 // to support client-side sorting.  Migrate to server-side
142                 // sorting to avoid the need for fetching all items.
143                 //limit  : count,
144                 //offset : offset,
145                 // we need an order-by to support paging
146                 order_by : {circ : ['circ_time']} 
147
148         }).then(null, null, function(noncat_circ) {
149
150             // calculate the virtual due date from the item type duration
151             var seconds = egCore.date.intervalToSeconds(
152                 noncat_circ.item_type().circ_duration());
153             var d = new Date(Date.parse(noncat_circ.circ_time()));
154             d.setSeconds(d.getSeconds() + seconds);
155             noncat_circ.duedate(d.toISOString());
156
157             // local flesh org unit
158             noncat_circ.circ_lib(egCore.org.get(noncat_circ.circ_lib()));
159
160             patronSvc.items_out.push(noncat_circ); // cache it
161             return noncat_circ;
162         });
163     }
164
165
166     // decide which list each circ belongs to
167     function promote_circs(list, display_code, open) {
168         if (open) {                                                    
169             if (1 & display_code) { // bitflag 1 == top list                   
170                 $scope.main_list = $scope.main_list.concat(list);
171             } else {                                                   
172                 $scope.alt_list = $scope.alt_list.concat(list);
173             }                                                          
174         } else {                                                       
175             if (4 & display_code) return;  // bitflag 4 == hide on checkin     
176             $scope.alt_list = $scope.alt_list.concat(list);
177         } 
178     }
179
180     // fetch IDs for circs we care about
181     function get_circ_ids() {
182         $scope.main_list = [];
183         $scope.alt_list = [];
184
185         // we can fetch these in parallel
186         var promise1 = egCore.net.request(
187             'open-ils.actor',
188             'open-ils.actor.user.checked_out.authoritative',
189             egCore.auth.token(), $scope.patron_id
190         ).then(function(outs) {
191             $scope.main_list = outs.overdue.concat(outs.out);
192             promote_circs(outs.lost, display_lost, true);                            
193             promote_circs(outs.long_overdue, display_lo, true);             
194             promote_circs(outs.claims_returned, display_cr, true);
195         });
196
197         // only fetched checked-in-with-bills circs if configured to display
198         var promise2 = !fetch_checked_in ? $q.when() : egCore.net.request(
199             'open-ils.actor',
200             'open-ils.actor.user.checked_in_with_fines.authoritative',
201             egCore.auth.token(), $scope.patron_id
202         ).then(function(outs) {
203             promote_circs(outs.lost, display_lost);
204             promote_circs(outs.long_overdue, display_lo);
205             promote_circs(outs.claims_returned, display_cr);
206         });
207
208         return $q.all([promise1, promise2]);
209     }
210
211     provider.get = function(offset, count) {
212
213         var id_list = $scope[$scope.items_out_display + '_list'];
214
215         // see if we have the requested range cached
216         if (patronSvc.items_out[offset]) {
217             return provider.arrayNotifier(
218                 patronSvc.items_out, offset, count);
219         }
220
221         if ($scope.items_out_display == 'noncat') {
222             // if there are any noncat circ IDs, we already have them
223             return fetch_noncat_circs(id_list, offset, count);
224         }
225
226         // See if we have the circ IDs for this range already loaded.
227         // this would happen navigating to a subsequent page.
228         if (id_list[offset]) {
229             return fetch_circs(id_list, offset, count);
230         }
231
232         // avoid returning the request directly to the caller so the
233         // notify()'s from egCore.net.request don't leak into the 
234         // final set of notifies (i.e. the real responses);
235
236         var deferred = $q.defer();
237         get_circ_ids().then(function() {
238
239             id_list = $scope[$scope.items_out_display + '_list'];
240
241             // relay the notified circs back to the grid through our promise
242             fetch_circs(id_list, offset, count).then(
243                 deferred.resolve, null, deferred.notify);
244         });
245
246         return deferred.promise;
247     }
248
249
250     // true if circ is overdue, false otherwise
251     $scope.circIsOverdue = function(circ) {
252         // circ may not exist yet for rendered row
253         if (!circ) return false;
254
255         var date = new Date();
256         date.setTime(Date.parse(circ.due_date()));
257         return date < new Date();
258     }
259
260     $scope.edit_due_date = function(items) {
261         if (!items.length) return;
262
263         $uibModal.open({
264             templateUrl : './circ/patron/t_edit_due_date_dialog',
265             controller : [
266                         '$scope','$uibModalInstance',
267                 function($scope , $uibModalInstance) {
268
269                     // if there is only one circ, default to the due date
270                     // of that circ.  Otherwise, default to today.
271                     var due_date = items.length == 1 ? 
272                         Date.parse(items[0].due_date()) : new Date();
273
274                     $scope.args = {
275                         num_circs : items.length,
276                         due_date : due_date
277                     }
278
279                     // Fire off the due-date updater for each circ.
280                     // When all is done, close the dialog
281                     $scope.ok = function(args) {
282                         // toISOString gives us Zulu time, so
283                         // adjust for that before truncating to date
284                         var adjust_date = new Date( $scope.args.due_date );
285                         adjust_date.setMinutes(
286                             $scope.args.due_date.getMinutes() - adjust_date.getTimezoneOffset()
287                         );
288                         var due = adjust_date.toISOString().replace(/T.*/,'');
289                         console.debug("applying due date of " + due);
290
291                         var promises = [];
292                         angular.forEach(items, function(circ) {
293                             promises.push(
294                                 egCore.net.request(
295                                     'open-ils.circ',
296                                     'open-ils.circ.circulation.due_date.update',
297                                     egCore.auth.token(), circ.id(), due
298
299                                 ).then(function(new_circ) {
300                                     // update the grid circ with the canonical 
301                                     // date from the modified circulation.
302                                     circ.due_date(new_circ.due_date());
303                                 })
304                             );
305                         });
306
307                         $q.all(promises).then(function() {
308                             $uibModalInstance.close();
309                             provider.refresh();
310                         });
311                     }
312                     $scope.cancel = function($event) {
313                         $uibModalInstance.dismiss();
314                         $event.preventDefault();
315                     }
316                 }
317             ]
318         });
319     }
320
321     $scope.print_receipt = function(items) {
322         if (items.length == 0) return $q.when();
323         var print_data = {circulations : []}
324
325         angular.forEach(patronSvc.items_out, function(circ) {
326             print_data.circulations.push({
327                 circ : egCore.idl.toHash(circ),
328                 copy : egCore.idl.toHash(circ.target_copy()),
329                 call_number : egCore.idl.toHash(circ.target_copy().call_number()),
330                 title : circ.target_copy().call_number().record().simple_record().title(),
331                 author : circ.target_copy().call_number().record().simple_record().author(),
332             })
333         });
334
335         return egCore.print.print({
336             context : 'default', 
337             template : 'items_out', 
338             scope : print_data,
339         });
340     }
341
342     function batch_action_with_barcodes(items, action) {
343         if (!items.length) return;
344         var barcodes = items.map(function(circ) 
345             { return circ.target_copy().barcode() });
346         action(barcodes).then(reset_page);
347     }
348     $scope.mark_lost = function(items) {
349         batch_action_with_barcodes(items, egCirc.mark_lost);
350     }
351     $scope.mark_claims_returned = function(items) {
352         batch_action_with_barcodes(items, egCirc.mark_claims_returned_dialog);
353     }
354     $scope.mark_claims_never_checked_out = function(items) {
355         batch_action_with_barcodes(items, egCirc.mark_claims_never_checked_out);
356     }
357
358     $scope.show_recent_circs = function(items) {
359         var focus = items.length == 1;
360         angular.forEach(items, function(item) {
361             var url = egCore.env.basePath +
362                       '/cat/item/' +
363                       item.target_copy().id() +
364                       '/circ_list';
365             $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
366         });
367     }
368
369     $scope.show_triggered_events = function(items) {
370         var focus = items.length == 1;
371         angular.forEach(items, function(item) {
372             var url = egCore.env.basePath +
373                       '/cat/item/' +
374                       item.target_copy().id() +
375                       '/triggered_events';
376             $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
377         });
378     }
379
380     $scope.renew = function(items, msg) {
381         if (!items.length) return;
382         var barcodes = items.map(function(circ) 
383             { return circ.target_copy().barcode() });
384
385         if (!msg) msg = egCore.strings.RENEW_ITEMS;
386
387         return egConfirmDialog.open(msg, barcodes.join(' '), {}).result
388         .then(function() {
389             function do_one() {
390                 var bc = barcodes.pop();
391                 if (!bc) { reset_page(); return }
392                 // finally -> continue even when one fails
393                 egCirc.renew({copy_barcode : bc}).finally(do_one);
394             }
395             do_one();
396         });
397     }
398
399     $scope.renew_all = function() {
400         var circs = patronSvc.items_out.filter(function(circ) {
401             return (
402                 // all others will be rejected at the server
403                 !circ.stop_fines() ||
404                 circ.stop_fines() == 'MAXFINES'
405             );
406         });
407         $scope.renew(circs, egCore.strings.RENEW_ALL_ITEMS);
408     }
409
410     $scope.renew_with_date = function(items) {
411         if (!items.length) return;
412         var barcodes = items.map(function(circ) 
413             { return circ.target_copy().barcode() });
414
415         return $uibModal.open({
416             templateUrl : './circ/patron/t_edit_due_date_dialog',
417             templateUrl : './circ/patron/t_renew_with_date_dialog',
418             controller : [
419                         '$scope','$uibModalInstance',
420                 function($scope , $uibModalInstance) {
421                     $scope.args = {
422                         barcodes : barcodes,
423                         date : new Date()
424                     }
425                     $scope.cancel = function() {$uibModalInstance.dismiss()}
426
427                     // Fire off the due-date updater for each circ.
428                     // When all is done, close the dialog
429                     $scope.ok = function() {
430                         var due = $scope.args.date.toISOString().replace(/T.*/,'');
431                         console.debug("renewing with due date: " + due);
432
433                         function do_one() {
434                             if (bc = barcodes.pop()) {
435                                 egCirc.renew({copy_barcode : bc, due_date : due})
436                                 .finally(do_one);
437                             } else {
438                                 $uibModalInstance.close(); 
439                                 reset_page();
440                             }
441                         }
442                        do_one(); // kick it off
443                     }
444                 }
445             ]
446         }).result;
447     }
448
449     $scope.checkin = function(items) {
450         if (!items.length) return;
451         var barcodes = items.map(function(circ) 
452             { return circ.target_copy().barcode() });
453
454         return egConfirmDialog.open(
455             egCore.strings.CHECK_IN_CONFIRM, barcodes.join(' '), {
456
457         }).result.then(function() {
458             function do_one() {
459                 if (bc = barcodes.pop()) {
460                     egCirc.checkin({copy_barcode : bc})
461                     .finally(do_one);
462                 } else {
463                     reset_page();
464                 }
465             }
466             do_one(); // kick it off
467         });
468     }
469
470     $scope.add_billing = function(items) {
471         if (!items.length) return;
472         var circs = items.concat(); // don't pop from grid array
473         function do_one() {
474             var circ; // don't clobber window.circ!
475             if (circ = circs.pop()) {
476                 egBilling.showBillDialog({
477                     // let the dialog fetch the transaction, since it's
478                     // not sufficiently fleshed here.
479                     xact_id : circ.id(),
480                     patron : patronSvc.current
481                 }).finally(do_one);
482             } else {
483                 reset_page();
484             }
485         }
486         do_one();
487     }
488
489 }]);
490