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