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