1cbb05e220ffb655a2dd335bb20f42ace7e277d1
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / patron / checkout.js
1 /**
2  * Checkout items to patrons
3  */
4
5 angular.module('egPatronApp').controller('PatronCheckoutCtrl',
6
7        ['$scope','$q','$routeParams','egCore','egUser','patronSvc',
8         'egGridDataProvider','$location','$timeout','egCirc','ngToast',
9
10 function($scope , $q , $routeParams , egCore , egUser , patronSvc , 
11          egGridDataProvider , $location , $timeout , egCirc , ngToast) {
12
13     $scope.initTab('checkout', $routeParams.id).finally(function(){
14         $scope.focusMe = true;
15     });
16     $scope.checkouts = patronSvc.checkouts;
17     $scope.checkoutArgs = {
18         noncat_type : 'barcode',
19         due_date : new Date()
20     };
21
22     $scope.gridDataProvider = egGridDataProvider.instance({
23         get : function(offset, count) {
24             return this.arrayNotifier($scope.checkouts, offset, count);
25         }
26     });
27
28     $scope.disable_checkout = function() {
29         return (
30             !patronSvc.current ||
31             patronSvc.current.active() == 'f' ||
32             patronSvc.current.deleted() == 't' ||
33             patronSvc.current.card().active() == 'f' ||
34             patronSvc.fetchedWithInactiveCard()
35         );
36     }
37
38     function setting_value (user, setting) {
39         if (user) {
40             var list = user.settings().filter(function(s){
41                 return s.name() == setting;
42             });
43
44             if (list.length) return list[0].value();
45         }
46     }
47
48     $scope.date_options = {
49         due_date : egCore.hatch.getSessionItem('eg.circ.checkout.due_date'),
50         has_sticky_date : egCore.hatch.getSessionItem('eg.circ.checkout.is_until_logout'),
51         is_until_logout : egCore.hatch.getSessionItem('eg.circ.checkout.is_until_logout')
52     };
53
54     if ($scope.date_options.is_until_logout) { // If until_logout is set there should also be a date set.
55         $scope.checkoutArgs.due_date = new Date($scope.date_options.due_date);
56         $scope.checkoutArgs.sticky_date = true;
57     }
58
59     $scope.toggle_opt = function(opt) {
60         if ($scope.date_options[opt]) {
61             $scope.date_options[opt] = false;
62         } else {
63             $scope.date_options[opt] = true;
64         }
65     };
66
67     // The interactions between these options are complicated enough that $watch'ing them all is the only safe way to keep things sane.
68     $scope.$watch('date_options.has_sticky_date', function(newval) {
69         if ( newval ) { // was false, is true
70             // $scope.date_options.due_date = checkoutArgs.due_date;
71         } else {
72             $scope.date_options.is_until_logout = false;
73         }
74         $scope.checkoutArgs.sticky_date = newval;
75     });
76
77     $scope.$watch('date_options.is_until_logout', function(newval) {
78         if ( newval ) { // was false, is true
79             $scope.date_options.has_sticky_date = true;
80             $scope.date_options.due_date = $scope.checkoutArgs.due_date;
81             egCore.hatch.setSessionItem('eg.circ.checkout.is_until_logout', true);
82             egCore.hatch.setSessionItem('eg.circ.checkout.due_date', $scope.checkoutArgs.due_date);
83         } else {
84             egCore.hatch.removeSessionItem('eg.circ.checkout.is_until_logout');
85             egCore.hatch.removeSessionItem('eg.circ.checkout.due_date');
86         }
87     });
88
89     $scope.$watch('checkoutArgs.due_date', function(newval) {
90         if ( $scope.date_options.is_until_logout ) {
91             egCore.hatch.setSessionItem('eg.circ.checkout.due_date', newval);
92         }
93     });
94
95     $scope.has_email_address = function() {
96         return (
97             patronSvc.current &&
98             patronSvc.current.email() &&
99             patronSvc.current.email().match(/.*@.*/).length
100         );
101     }
102
103     $scope.may_email_receipt = function() {
104         return (
105             $scope.has_email_address() &&
106             setting_value(
107                 patronSvc.current,
108                 'circ.send_email_checkout_receipts'
109             ) == 'true'
110         );
111     }
112
113     $scope.using_hatch_printer = egCore.hatch.usePrinting();
114
115     egCore.hatch.getItem('circ.checkout.strict_barcode')
116         .then(function(sb){ $scope.strict_barcode = sb });
117
118     // avoid multiple, in-flight attempts on the same barcode
119     var pending_barcodes = {};
120
121     var printOnComplete = true;
122     egCore.org.settings([
123         'circ.staff_client.do_not_auto_attempt_print'
124     ]).then(function(settings) { 
125         printOnComplete = !Boolean(
126             angular.isArray(settings['circ.staff_client.do_not_auto_attempt_print']) &&
127             (settings['circ.staff_client.do_not_auto_attempt_print'].indexOf('Checkout') > -1)
128         );
129     });
130
131     egCirc.get_noncat_types().then(function(list) {
132         $scope.nonCatTypes = list;
133     });
134
135     $scope.selectedNcType = function() {
136         if (!egCore.env.cnct) return null; // too soon
137         var type = egCore.env.cnct.map[$scope.checkoutArgs.noncat_type];
138         return type ? type.name() : null;
139     }
140
141     $scope.checkout = function(args) {
142         var params = angular.copy(args);
143         params.patron_id = patronSvc.current.id();
144
145         if (args.sticky_date) {
146             params.due_date = args.due_date.toISOString();
147         } else {
148             delete params.due_date;
149         }
150         delete params.sticky_date;
151
152         if (params.noncat_type == 'barcode') {
153             if (!args.copy_barcode) return;
154
155             args.copy_barcode = ''; // reset UI input
156             params.noncat_type = ''; // "barcode"
157
158             if (pending_barcodes[params.copy_barcode]) {
159                 console.log(
160                     "Skipping checkout of redundant barcode " 
161                     + params.copy_barcode
162                 );
163                 return;
164             }
165
166             pending_barcodes[params.copy_barcode] = true;
167             send_checkout(params);
168
169         } else {
170             egCirc.noncat_dialog(params).then(function() {
171                 send_checkout(params)
172             });
173         }
174
175         $scope.focusMe = true; // return focus to barcode input
176     }
177
178     function send_checkout(params) {
179
180         params.noncat_type = params.noncat ? params.noncat_type : '';
181
182         // populate the grid row before we send the request so that the
183         // order of actions is maintained and so the user gets an 
184         // immediate reaction to their barcode input action.
185         var row_item = {
186             index : $scope.checkouts.length,
187             input_barcode : params.copy_barcode,
188             noncat_type : params.noncat_type
189         };
190
191         $scope.checkouts.unshift(row_item);
192         $scope.gridDataProvider.refresh();
193
194         egCore.hatch.setItem('circ.checkout.strict_barcode', $scope.strict_barcode);
195         var options = {check_barcode : $scope.strict_barcode};
196
197         egCirc.checkout(params, options).then(
198             function(co_resp) {
199                 // update stats locally so we don't have to fetch them w/
200                 // each checkout.
201
202                 // Avoid updating checkout counts when a checkout turns
203                 // into a renewal via auto_renew.
204                 if (!co_resp.auto_renew && !params.noncat) {
205                     patronSvc.patron_stats.checkouts.out++;
206                     patronSvc.patron_stats.checkouts.total_out++;
207                 }
208
209                 // copy the response event into the original grid row item
210                 // note: angular.copy clobbers the destination
211                 row_item.evt = co_resp.evt;
212                 angular.forEach(co_resp.data, function(val, key) {
213                     row_item[key] = val;
214                 });
215                
216                 if (row_item.acp) { // unset for non-cat items.
217                     row_item['copy_barcode'] = row_item.acp.barcode();
218                 }
219
220                 munge_checkout_resp(co_resp, row_item);
221             },
222             function() {
223                 // Circ was rejected somewhere along the way.
224                 // Remove the copy from the grid since there was no action.
225                 // note: since checkouts are unshifted onto the array, the
226                 // index value does not (generally) match the array position.
227                 var pos = -1;
228                 angular.forEach($scope.checkouts, function(co, idx) {
229                     if (co.index == row_item.index) pos = idx;
230                 });
231                 $scope.checkouts.splice(pos, 1);
232                 $scope.gridDataProvider.refresh();
233             }
234
235         ).finally(function() {
236
237             // regardless of the outcome of the circ, remove the 
238             // barcode from the pending list.
239             if (params.copy_barcode)
240                 delete pending_barcodes[params.copy_barcode];
241
242             $scope.focusMe = true; // return focus to barcode input
243         });
244     }
245
246     // add some checkout-specific additions for display
247     function munge_checkout_resp(co_resp, row_item) {
248         var params = co_resp.params;
249         if (params.noncat) {
250             row_item.title = egCore.env.cnct.map[params.noncat_type].name();
251             row_item.noncat_count = params.noncat_count;
252             row_item.circ = new egCore.idl.circ();
253             row_item.circ.due_date(co_resp.evt[0].payload.noncat_circ.duedate());
254             // Non-cat circs don't return the full list of circs.
255             // Refresh the list of non-cat circs from the server.
256             patronSvc.getUserNonCats(patronSvc.current.id());
257             row_item.copy_alert_count = 0;
258         } else {
259             row_item.copy_alert_count = 0;
260             egCore.pcrud.search(
261                 'aca',
262                 { copy : co_resp.data.acp.id(), ack_time : null },
263                 null,
264                 { atomic : true }
265             ).then(function(list) {
266                 row_item.copy_alert_count = list.length;
267             });
268         }
269     }
270
271     $scope.addCopyAlerts = function(items) {
272         var copy_ids = [];
273         angular.forEach(items, function(item) {
274             if (item.acp) copy_ids.push(item.acp.id());
275         });
276         egCirc.add_copy_alerts(copy_ids).then(function() {
277             // update grid items?
278         });
279     }
280
281     $scope.manageCopyAlerts = function(items) {
282         var copy_ids = [];
283         angular.forEach(items, function(item) {
284             if (item.acp) copy_ids.push(item.acp.id());
285         });
286         egCirc.manage_copy_alerts(copy_ids).then(function() {
287             // update grid items?
288         });
289     }
290
291     $scope.gridCellHandlers = {};
292     $scope.gridCellHandlers.copyAlertsEdit = function(id) {
293         egCirc.manage_copy_alerts([id]).then(function() {
294             // update grid items?
295         });
296     };
297
298     $scope.print_receipt = function() {
299         var print_data = {circulations : []};
300         var cusr = patronSvc.current;
301
302         if ($scope.checkouts.length == 0) return $q.when();
303
304         angular.forEach($scope.checkouts, function(co) {
305             if (co.circ) {
306                 print_data.circulations.push({
307                     circ : egCore.idl.toHash(co.circ),
308                     copy : egCore.idl.toHash(co.acp),
309                     call_number : egCore.idl.toHash(co.acn),
310                     title : co.title,
311                     author : co.author
312                 })
313             };
314         });
315
316         // This is repeated in patron.* so everyting is in one place but left here so existing templates don't break.
317         print_data.patron_money = patronSvc.patron_stats.fines;
318         print_data.patron = {
319             prefix : cusr.prefix(),
320             first_given_name : cusr.first_given_name(),
321             second_given_name : cusr.second_given_name(),
322             family_name : cusr.family_name(),
323             suffix : cusr.suffix(),
324             pref_prefix : cusr.pref_prefix(),
325             pref_first_given_name : cusr.pref_first_given_name(),
326             pref_secondg_given_name : cusr.second_given_name(),
327             pref_family_name : cusr.pref_family_name(),
328             suffix : cusr.suffix(),
329             card : { barcode : cusr.card().barcode() },
330             money_summary : patronSvc.patron_stats.fines,
331             expire_date : cusr.expire_date(),
332             alias : cusr.alias(),
333             has_email : Boolean($scope.has_email_address()),
334             has_phone : Boolean(cusr.day_phone() || cusr.evening_phone() || cusr.other_phone())
335         };
336
337         return egCore.print.print({
338             context : 'default', 
339             template : 'checkout', 
340             scope : print_data,
341             show_dialog : $scope.show_print_dialog
342         });
343     }
344
345     $scope.email_receipt = function() {
346         if ($scope.has_email_address() && $scope.checkouts.length) {
347             return egCore.net.request(
348                 'open-ils.circ',
349                 'open-ils.circ.checkout.batch_notify.session.atomic',
350                 egCore.auth.token(),
351                 patronSvc.current.id(),
352                 $scope.checkouts.map(function (c) { return c.circ.id() })
353             ).then(function() {
354                 ngToast.create(egCore.strings.EMAILED_CHECKOUT_RECEIPT);
355                 return $q.when();
356             });
357         }
358         return $q.when();
359     }
360
361     $scope.print_or_email_receipt = function() {
362         if ($scope.may_email_receipt()) return $scope.email_receipt();
363         $scope.print_receipt();
364     }
365
366     // set of functions to issue a receipt (if desired), then
367     // redirect
368     $scope.done_auto_receipt = function() {
369         if ($scope.may_email_receipt()) {
370             $scope.email_receipt().then(function() {
371                 $scope.done_redirect();
372             });
373         } else {
374             if (printOnComplete) {
375
376                 $scope.print_receipt().then(function() {
377                     $scope.done_redirect();
378                 });
379
380             } else {
381                 $scope.done_redirect();
382             }
383         }
384     }
385     $scope.done_print_receipt = function() {
386         $scope.print_receipt().then( function () {
387             $scope.done_redirect();
388         });
389     }
390     $scope.done_email_receipt = function() {
391         $scope.email_receipt().then( function () {
392             $scope.done_redirect();
393         });
394     }
395     $scope.done_no_receipt = function() {
396         $scope.done_redirect();
397     }
398
399     // Redirect the user to the barcode entry page to load a new patron.
400     $scope.done_redirect = function() {
401         $location.path('/circ/patron/bcsearch');
402     }
403 }])
404