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