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