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