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