]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/circ/services/circ.js
LP#1661685 - Adds missing Circulation Modifier column to several grids
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / services / circ.js
1 /**
2  * Checkin, checkout, and renew
3  */
4
5 angular.module('egCoreMod')
6
7 .factory('egCirc',
8        ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog',
9         'egWorkLog',
10 function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
11          egWorkLog) {
12
13     var service = {
14         // auto-override these events after the first override
15         auto_override_checkout_events : {},
16         require_initials : false,
17         never_auto_print : {
18             hold_shelf_slip : false,
19             hold_transit_slip : false,
20             transit_slip : false
21         }
22     };
23
24     egCore.startup.go().finally(function() {
25         egCore.org.settings([
26             'ui.staff.require_initials.patron_standing_penalty',
27             'ui.admin.work_log.max_entries',
28             'ui.admin.patron_log.max_entries',
29             'circ.staff_client.do_not_auto_attempt_print'
30         ]).then(function(set) {
31             service.require_initials = Boolean(set['ui.staff.require_initials.patron_standing_penalty']);
32             if (angular.isArray(set['circ.staff_client.do_not_auto_attempt_print'])) {
33                 if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Hold Slip') > 1)
34                     service.never_auto_print['hold_shelf_slip'] = true;
35                 if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Hold/Transit Slip') > 1)
36                     service.never_auto_print['hold_transit_slip'] = true;
37                 if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Transit Slip') > 1)
38                     service.never_auto_print['transit_slip'] = true;
39             }
40         });
41     });
42
43     service.reset = function() {
44         service.auto_override_checkout_events = {};
45     }
46
47     // these events can be overridden by staff during checkout
48     service.checkout_overridable_events = [
49         'PATRON_EXCEEDS_OVERDUE_COUNT',
50         'PATRON_EXCEEDS_CHECKOUT_COUNT',
51         'PATRON_EXCEEDS_FINES',
52         'PATRON_BARRED',
53         'CIRC_EXCEEDS_COPY_RANGE',
54         'ITEM_DEPOSIT_REQUIRED',
55         'ITEM_RENTAL_FEE_REQUIRED',
56         'PATRON_EXCEEDS_LOST_COUNT',
57         'COPY_CIRC_NOT_ALLOWED',
58         'COPY_NOT_AVAILABLE',
59         'COPY_IS_REFERENCE',
60         'COPY_ALERT_MESSAGE',
61         'ITEM_ON_HOLDS_SHELF'                 
62     ]
63
64     // after the first override of any of these events, 
65     // auto-override them in subsequent calls.
66     service.checkout_auto_override_after_first = [
67         'PATRON_EXCEEDS_OVERDUE_COUNT',
68         'PATRON_BARRED',
69         'PATRON_EXCEEDS_LOST_COUNT',
70         'PATRON_EXCEEDS_CHECKOUT_COUNT',
71         'PATRON_EXCEEDS_FINES'
72     ]
73
74
75     // overridable during renewal
76     service.renew_overridable_events = [
77         'PATRON_EXCEEDS_OVERDUE_COUNT',
78         'PATRON_EXCEEDS_LOST_COUNT',
79         'PATRON_EXCEEDS_CHECKOUT_COUNT',
80         'PATRON_EXCEEDS_FINES',
81         'CIRC_EXCEEDS_COPY_RANGE',
82         'ITEM_DEPOSIT_REQUIRED',
83         'ITEM_RENTAL_FEE_REQUIRED',
84         'ITEM_DEPOSIT_PAID',
85         'COPY_CIRC_NOT_ALLOWED',
86         'COPY_IS_REFERENCE',
87         'COPY_ALERT_MESSAGE',
88         'COPY_NEEDED_FOR_HOLD',
89         'MAX_RENEWALS_REACHED',
90         'CIRC_CLAIMS_RETURNED'
91     ];
92
93     // these checkin events do not produce alerts when 
94     // options.suppress_alerts is in effect.
95     service.checkin_suppress_overrides = [
96         'COPY_BAD_STATUS',
97         'PATRON_BARRED',
98         'PATRON_INACTIVE',
99         'PATRON_ACCOUNT_EXPIRED',
100         'ITEM_DEPOSIT_PAID',
101         'CIRC_CLAIMS_RETURNED',
102         'COPY_ALERT_MESSAGE',
103         'COPY_STATUS_LOST',
104         'COPY_STATUS_LONG_OVERDUE',
105         'COPY_STATUS_MISSING',
106         'PATRON_EXCEEDS_FINES'
107     ]
108
109     // these events can be overridden by staff during checkin
110     service.checkin_overridable_events = 
111         service.checkin_suppress_overrides.concat([
112         'TRANSIT_CHECKIN_INTERVAL_BLOCK'
113     ])
114
115     // Performs a checkout.
116     // Returns a promise resolved with the original params and options
117     // and the final checkout event (e.g. in the case of override).
118     // Rejected if the checkout cannot be completed.
119     //
120     // params : passed directly as arguments to the server API 
121     // options : non-parameter controls.  e.g. "override", "check_barcode"
122     service.checkout = function(params, options) {
123         if (!options) options = {};
124
125         console.debug('egCirc.checkout() : ' 
126             + js2JSON(params) + ' : ' + js2JSON(options));
127
128         var promise = options.check_barcode ? 
129             service.test_barcode(params.copy_barcode) : $q.when();
130
131         // avoid re-check on override, etc.
132         delete options.check_barcode;
133
134         return promise.then(function() {
135
136             var method = 'open-ils.circ.checkout.full';
137             if (options.override) method += '.override';
138
139             return egCore.net.request(
140                 'open-ils.circ', method, egCore.auth.token(), params
141
142             ).then(function(evt) {
143
144                 if (!angular.isArray(evt)) evt = [evt];
145
146                 if (evt[0].payload && evt[0].payload.auto_renew == 1) {
147                     // open circulation found with auto-renew toggle on.
148                     console.debug('Auto-renewing item ' + params.copy_barcode);
149                     options.auto_renew = true;
150                     return service.renew(params, options);
151                 }
152
153                 var action = params.noncat ? 'noncat_checkout' : 'checkout';
154
155                 return service.flesh_response_data(action, evt, params, options)
156                 .then(function() {
157                     return service.handle_checkout_resp(evt, params, options);
158                 })
159                 .then(function(final_resp) {
160                     return service.munge_resp_data(final_resp,action,method)
161                 })
162             });
163         });
164     }
165
166     // Performs a renewal.
167     // Returns a promise resolved with the original params and options
168     // and the final checkout event (e.g. in the case of override)
169     // Rejected if the renewal cannot be completed.
170     service.renew = function(params, options) {
171         if (!options) options = {};
172
173         console.debug('egCirc.renew() : ' 
174             + js2JSON(params) + ' : ' + js2JSON(options));
175
176         var promise = options.check_barcode ? 
177             service.test_barcode(params.copy_barcode) : $q.when();
178
179         // avoid re-check on override, etc.
180         delete options.check_barcode;
181
182         return promise.then(function() {
183
184             var method = 'open-ils.circ.renew';
185             if (options.override) method += '.override';
186
187             return egCore.net.request(
188                 'open-ils.circ', method, egCore.auth.token(), params
189
190             ).then(function(evt) {
191
192                 if (!angular.isArray(evt)) evt = [evt];
193
194                 return service.flesh_response_data(
195                     'renew', evt, params, options)
196                 .then(function() {
197                     return service.handle_renew_resp(evt, params, options);
198                 })
199                 .then(function(final_resp) {
200                     final_resp.auto_renew = options.auto_renew;
201                     return service.munge_resp_data(final_resp,'renew',method)
202                 })
203             });
204         });
205     }
206
207     // Performs a checkin
208     // Returns a promise resolved with the original params and options,
209     // plus the final checkin event (e.g. in the case of override).
210     // Rejected if the checkin cannot be completed.
211     service.checkin = function(params, options) {
212         if (!options) options = {};
213
214         console.debug('egCirc.checkin() : ' 
215             + js2JSON(params) + ' : ' + js2JSON(options));
216
217         var promise = options.check_barcode ? 
218             service.test_barcode(params.copy_barcode) : $q.when();
219
220         // avoid re-check on override, etc.
221         delete options.check_barcode;
222
223         return promise.then(function() {
224
225             var method = 'open-ils.circ.checkin';
226             if (options.override) method += '.override';
227
228             return egCore.net.request(
229                 'open-ils.circ', method, egCore.auth.token(), params
230
231             ).then(function(evt) {
232
233                 if (!angular.isArray(evt)) evt = [evt];
234                 return service.flesh_response_data(
235                     'checkin', evt, params, options)
236                 .then(function() {
237                     return service.handle_checkin_resp(evt, params, options);
238                 })
239                 .then(function(final_resp) {
240                     return service.munge_resp_data(final_resp,'checkin',method)
241                 })
242             });
243         });
244     }
245
246     // provide consistent formatting of the final response data
247     service.munge_resp_data = function(final_resp,worklog_action,worklog_method) {
248         var data = final_resp.data = {};
249
250         if (!final_resp.evt[0]) {
251             egCore.audio.play('error.unknown.no_event');
252             return;
253         }
254
255         var payload = final_resp.evt[0].payload;
256         if (!payload) {
257             egCore.audio.play('error.unknown.no_payload');
258             return;
259         }
260
261         data.circ = payload.circ;
262         data.parent_circ = payload.parent_circ;
263         data.hold = payload.hold;
264         data.record = payload.record;
265         data.acp = payload.copy;
266         data.acn = payload.volume ?  payload.volume : payload.copy ? payload.copy.call_number() : null;
267         data.au = payload.patron;
268         data.transit = payload.transit;
269         data.status = payload.status;
270         data.message = payload.message;
271         data.title = final_resp.evt[0].title;
272         data.author = final_resp.evt[0].author;
273         data.isbn = final_resp.evt[0].isbn;
274         data.route_to = final_resp.evt[0].route_to;
275
276         // for checkin, the mbts lives on the main circ
277         if (payload.circ && payload.circ.billable_transaction())
278             data.mbts = payload.circ.billable_transaction().summary();
279
280         // on renewals, the mbts lives on the parent circ
281         if (payload.parent_circ && payload.parent_circ.billable_transaction())
282             data.mbts = payload.parent_circ.billable_transaction().summary();
283
284         if (!data.route_to) {
285             if (data.transit) {
286                 data.route_to = data.transit.dest().shortname();
287             } else if (data.acp) {
288                 data.route_to = data.acp.location().name();
289             }
290         }
291
292         egWorkLog.record(
293             (worklog_action == 'checkout' || worklog_action == 'noncat_checkout')
294             ? egCore.strings.EG_WORK_LOG_CHECKOUT
295             : (worklog_action == 'renew'
296                 ? egCore.strings.EG_WORK_LOG_RENEW
297                 : egCore.strings.EG_WORK_LOG_CHECKIN // worklog_action == 'checkin'
298             ),{
299                 'action' : worklog_action,
300                 'method' : worklog_method,
301                 'response' : final_resp
302             }
303         );
304
305         return final_resp;
306     }
307
308     service.handle_overridable_checkout_event = function(evt, params, options) {
309
310         if (options.override) {
311             // override attempt already made and failed.
312             // NOTE: I don't think we'll ever get here, since the
313             // override attempt should produce a perm failure...
314             angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
315             return $q.reject();
316
317         } 
318
319         if (evt.filter(function(e){return !service.auto_override_checkout_events[e.textcode];}).length == 0) {
320             // user has already opted to override these type
321             // of events.  Re-run the checkout w/ override.
322             options.override = true;
323             return service.checkout(params, options);
324         } 
325
326         // Ask the user if they would like to override this event.
327         // Some events offer a stock override dialog, while others
328         // require additional context.
329
330         switch(evt[0].textcode) {
331             case 'COPY_NOT_AVAILABLE':
332                 return service.copy_not_avail_dialog(evt[0], params, options);
333             case 'COPY_ALERT_MESSAGE':
334                 return service.copy_alert_dialog(evt[0], params, options, 'checkout');
335             default: 
336                 return service.override_dialog(evt, params, options, 'checkout');
337         }
338     }
339
340     service.handle_overridable_renew_event = function(evt, params, options) {
341
342         if (options.override) {
343             // override attempt already made and failed.
344             // NOTE: I don't think we'll ever get here, since the
345             // override attempt should produce a perm failure...
346             angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
347             return $q.reject();
348
349         } 
350
351         // renewal auto-overrides are the same as checkout
352         if (evt.filter(function(e){return !service.auto_override_checkout_events[e.textcode];}).length == 0) {
353             // user has already opted to override these type
354             // of events.  Re-run the renew w/ override.
355             options.override = true;
356             return service.renew(params, options);
357         } 
358
359         // Ask the user if they would like to override this event.
360         // Some events offer a stock override dialog, while others
361         // require additional context.
362
363         switch(evt[0].textcode) {
364             case 'COPY_ALERT_MESSAGE':
365                 return service.copy_alert_dialog(evt[0], params, options, 'renew');
366             default: 
367                 return service.override_dialog(evt, params, options, 'renew');
368         }
369     }
370
371
372     service.handle_overridable_checkin_event = function(evt, params, options) {
373
374         if (options.override) {
375             // override attempt already made and failed.
376             // NOTE: I don't think we'll ever get here, since the
377             // override attempt should produce a perm failure...
378             angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
379             return $q.reject();
380
381         } 
382
383         if (options.suppress_checkin_popups
384             && evt.filter(function(e){return service.checkin_suppress_overrides.indexOf(e.textcode) == -1;}).length == 0) {
385             // Events are suppressed.  Re-run the checkin w/ override.
386             options.override = true;
387             return service.checkin(params, options);
388         } 
389
390         // Ask the user if they would like to override this event.
391         // Some events offer a stock override dialog, while others
392         // require additional context.
393
394         switch(evt[0].textcode) {
395             case 'COPY_ALERT_MESSAGE':
396                 return service.copy_alert_dialog(evt[0], params, options, 'checkin');
397             default: 
398                 return service.override_dialog(evt, params, options, 'checkin');
399         }
400     }
401
402
403     service.handle_renew_resp = function(evt, params, options) {
404
405         var final_resp = {evt : evt, params : params, options : options};
406
407         // track the barcode regardless of whether it refers to a copy
408         angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
409
410         // Overridable Events
411         if (evt.filter(function(e){return service.renew_overridable_events.indexOf(e.textcode) > -1;}).length > 0)
412             return service.handle_overridable_renew_event(evt, params, options);
413
414         // Other events
415         switch (evt[0].textcode) {
416             case 'SUCCESS':
417                 egCore.audio.play('info.renew');
418                 return $q.when(final_resp);
419
420             case 'COPY_IN_TRANSIT':
421             case 'PATRON_CARD_INACTIVE':
422             case 'PATRON_INACTIVE':
423             case 'PATRON_ACCOUNT_EXPIRED':
424             case 'CIRC_CLAIMS_RETURNED':
425                 egCore.audio.play('warning.renew');
426                 return service.exit_alert(
427                     egCore.strings[evt[0].textcode],
428                     {barcode : params.copy_barcode}
429                 );
430
431             default:
432                 egCore.audio.play('warning.renew.unknown');
433                 return service.exit_alert(
434                     egCore.strings.CHECKOUT_FAILED_GENERIC, {
435                         barcode : params.copy_barcode,
436                         textcode : evt[0].textcode,
437                         desc : evt[0].desc
438                     }
439                 );
440         }
441     }
442
443
444     service.handle_checkout_resp = function(evt, params, options) {
445
446         var final_resp = {evt : evt, params : params, options : options};
447
448         // track the barcode regardless of whether it refers to a copy
449         angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
450
451         // Overridable Events
452         if (evt.filter(function(e){return service.checkout_overridable_events.indexOf(e.textcode) > -1;}).length > 0)
453             return service.handle_overridable_checkout_event(evt, params, options);
454
455         // Other events
456         switch (evt[0].textcode) {
457             case 'SUCCESS':
458                 egCore.audio.play('success.checkout');
459                 return $q.when(final_resp);
460
461             case 'ITEM_NOT_CATALOGED':
462                 egCore.audio.play('error.checkout.no_cataloged');
463                 return service.precat_dialog(params, options);
464
465             case 'OPEN_CIRCULATION_EXISTS':
466                 // auto_renew checked in service.checkout()
467                 egCore.audio.play('error.checkout.open_circ');
468                 return service.circ_exists_dialog(evt, params, options);
469
470             case 'COPY_IN_TRANSIT':
471                 egCore.audio.play('warning.checkout.in_transit');
472                 return service.copy_in_transit_dialog(evt, params, options);
473
474             case 'PATRON_CARD_INACTIVE':
475             case 'PATRON_INACTIVE':
476             case 'PATRON_ACCOUNT_EXPIRED':
477             case 'CIRC_CLAIMS_RETURNED':
478                 egCore.audio.play('warning.checkout');
479                 return service.exit_alert(
480                     egCore.strings[evt[0].textcode],
481                     {barcode : params.copy_barcode}
482                 );
483
484             default:
485                 egCore.audio.play('error.checkout.unknown');
486                 return service.exit_alert(
487                     egCore.strings.CHECKOUT_FAILED_GENERIC, {
488                         barcode : params.copy_barcode,
489                         textcode : evt[0].textcode,
490                         desc : evt[0].desc
491                     }
492                 );
493         }
494     }
495
496     // returns a promise resolved with the list of circ mods
497     service.get_circ_mods = function() {
498         if (egCore.env.ccm) 
499             return $q.when(egCore.env.ccm.list);
500
501         return egCore.pcrud.retrieveAll('ccm', null, {atomic : true})
502         .then(function(list) { 
503             egCore.env.absorbList(list, 'ccm');
504             return list;
505         });
506     };
507
508     // returns a promise resolved with the list of noncat types
509     service.get_noncat_types = function() {
510         if (egCore.env.cnct) 
511             return $q.when(egCore.env.cnct.list);
512
513         return egCore.pcrud.search('cnct', 
514             {owning_lib : 
515                 egCore.org.fullPath(egCore.auth.user().ws_ou(), true)}, 
516             null, {atomic : true}
517         ).then(function(list) { 
518             egCore.env.absorbList(list, 'cnct');
519             return list;
520         });
521     }
522
523     service.get_staff_penalty_types = function() {
524         if (egCore.env.csp) 
525             return $q.when(egCore.env.csp.list);
526         return egCore.pcrud.search(
527             // id <= 100 are reserved for system use
528             'csp', {id : {'>': 100}}, {}, {atomic : true})
529         .then(function(penalties) {
530             return egCore.env.absorbList(penalties, 'csp').list;
531         });
532     }
533
534     // ideally all of these data should be returned with the response,
535     // but until then, grab what we need.
536     service.flesh_response_data = function(action, evt, params, options) {
537         var promises = [];
538         var payload;
539         if (!evt[0] || !(payload = evt[0].payload)) return $q.when();
540
541         promises.push(service.flesh_copy_location(payload.copy));
542         if (payload.copy) {
543             promises.push(service.flesh_copy_circ_modifier(payload.copy));
544             promises.push(
545                 service.flesh_copy_status(payload.copy)
546
547                 .then(function() {
548                     // copy is in transit, but no transit was delivered
549                     // in the payload.  Do this here instead of below to
550                     // ensure consistent copy status fleshiness
551                     if (!payload.transit && payload.copy.status().id() == 6) { // in-transit
552                         return service.find_copy_transit(evt, params, options)
553                         .then(function(trans) {
554                             if (trans) {
555                                 trans.source(egCore.org.get(trans.source()));
556                                 trans.dest(egCore.org.get(trans.dest()));
557                                 payload.transit = trans;
558                             }
559                         })
560                     }
561                 })
562             );
563         }
564
565         // local flesh transit
566         if (transit = payload.transit) {
567             transit.source(egCore.org.get(transit.source()));
568             transit.dest(egCore.org.get(transit.dest()));
569         } 
570
571         // TODO: renewal responses should include the patron
572         if (!payload.patron) {
573             var user_id;
574             if (payload.circ) user_id = payload.circ.usr();
575             if (payload.noncat_circ) user_id = payload.noncat_circ.patron();
576             if (user_id) {
577                 promises.push(
578                     egCore.pcrud.retrieve('au', user_id)
579                     .then(function(user) {payload.patron = user})
580                 );
581             }
582         }
583
584         // extract precat values
585         angular.forEach(evt, function(e){ e.title = payload.record ? payload.record.title() : 
586             (payload.copy ? payload.copy.dummy_title() : null);});
587
588         angular.forEach(evt, function(e){ e.author = payload.record ? payload.record.author() : 
589             (payload.copy ? payload.copy.dummy_author() : null);});
590
591         angular.forEach(evt, function(e){ e.isbn = payload.record ? payload.record.isbn() : 
592             (payload.copy ? payload.copy.dummy_isbn() : null);});
593
594         return $q.all(promises);
595     }
596
597     // fetches the full list of circ modifiers
598     service.flesh_copy_circ_modifier = function(copy) {
599         if (!copy) return $q.when();
600         if (egCore.env.ccm)
601             return $q.when(copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]));
602         return egCore.pcrud.retrieveAll('ccm', {}, {atomic : true}).then(
603             function(list) {
604                 egCore.env.absorbList(list, 'ccm');
605                 copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]);
606             }
607         );
608     }
609
610     // fetches the full list of copy statuses
611     service.flesh_copy_status = function(copy) {
612         if (!copy) return $q.when();
613         if (egCore.env.ccs) 
614             return $q.when(copy.status(egCore.env.ccs.map[copy.status()]));
615         return egCore.pcrud.retrieveAll('ccs', {}, {atomic : true}).then(
616             function(list) {
617                 egCore.env.absorbList(list, 'ccs');
618                 copy.status(egCore.env.ccs.map[copy.status()]);
619             }
620         );
621     }
622
623     // there may be *many* copy locations and we may be handling items
624     // for other locations.  Fetch copy locations as-needed and cache.
625     service.flesh_copy_location = function(copy) {
626         if (!copy) return $q.when();
627         if (angular.isObject(copy.location())) return $q.when(copy);
628         if (egCore.env.acpl) {
629             if (egCore.env.acpl.map[copy.location()]) {
630                 copy.location(egCore.env.acpl.map[copy.location()]);
631                 return $q.when(copy);
632             }
633         } 
634         return egCore.pcrud.retrieve('acpl', copy.location())
635         .then(function(loc) {
636             egCore.env.absorbList([loc], 'acpl'); // append to cache
637             copy.location(loc);
638             return copy;
639         });
640     }
641
642
643     // fetch org unit addresses as needed.
644     service.get_org_addr = function(org_id, addr_type) {
645         var org = egCore.org.get(org_id);
646         var addr_id = org[addr_type]();
647
648         if (!addr_id) return $q.when(null);
649
650         if (egCore.env.aoa && egCore.env.aoa.map[addr_id]) 
651             return $q.when(egCore.env.aoa.map[addr_id]); 
652
653         return egCore.pcrud.retrieve('aoa', addr_id).then(function(addr) {
654             egCore.env.absorbList([addr], 'aoa');
655             return egCore.env.aoa.map[addr_id]; 
656         });
657     }
658
659     service.exit_alert = function(msg, scope) {
660         return egAlertDialog.open(msg, scope).result.then(
661             function() {return $q.reject()});
662     }
663
664     // opens a dialog asking the user if they would like to override
665     // the returned event.
666     service.override_dialog = function(evt, params, options, action) {
667         if (!angular.isArray(evt)) evt = [evt];
668
669         egCore.audio.play('warning.circ.event_override');
670         return $uibModal.open({
671             templateUrl: './circ/share/t_event_override_dialog',
672             controller: 
673                 ['$scope', '$uibModalInstance', 
674                 function($scope, $uibModalInstance) {
675                 $scope.events = evt;
676                 $scope.auto_override =
677                     evt.filter(function(e){
678                         return service.checkout_auto_override_after_first.indexOf(evt.textcode) > -1;
679                     }).length > 0;
680                 $scope.copy_barcode = params.copy_barcode; // may be null
681                 $scope.ok = function() { $uibModalInstance.close() }
682                 $scope.cancel = function ($event) { 
683                     $uibModalInstance.dismiss();
684                     $event.preventDefault();
685                 }
686             }]
687         }).result.then(
688             function() {
689                 options.override = true;
690
691                 if (action == 'checkin') {
692                     return service.checkin(params, options);
693                 }
694
695                 // checkout/renew support override-after-first
696                 angular.forEach(evt, function(e){
697                     if (service.checkout_auto_override_after_first.indexOf(e.textcode) > -1)
698                         service.auto_override_checkout_events[e.textcode] = true;
699                 });
700
701                 return service[action](params, options);
702             }
703         );
704     }
705
706     service.copy_not_avail_dialog = function(evt, params, options) {
707         if (angular.isArray(evt)) evt = evt[0];
708         return $uibModal.open({
709             templateUrl: './circ/share/t_copy_not_avail_dialog',
710             controller: 
711                        ['$scope','$uibModalInstance','copyStatus',
712                 function($scope , $uibModalInstance , copyStatus) {
713                 $scope.copyStatus = copyStatus;
714                 $scope.ok = function() {$uibModalInstance.close()}
715                 $scope.cancel = function() {$uibModalInstance.dismiss()}
716             }],
717             resolve : {
718                 copyStatus : function() {
719                     return egCore.pcrud.retrieve(
720                         'ccs', evt.payload.status());
721                 }
722             }
723         }).result.then(
724             function() {
725                 options.override = true;
726                 return service.checkout(params, options);
727             }
728         );
729     }
730
731     // Opens a dialog allowing the user to fill in the desired non-cat count.
732     // Unlike other dialogs, which kickoff circ actions internally
733     // as a result of events, this dialog does not kick off any circ
734     // actions. It just collects the count and and resolves the promise.
735     //
736     // This assumes the caller has already handled the noncat-type
737     // selection and just needs to collect the count info.
738     service.noncat_dialog = function(params, options) {
739         var noncatMax = 99; // hard-coded max
740         
741         // the caller should presumably have fetched the noncat_types via
742         // our API already, but fetch them again (from cache) to be safe.
743         return service.get_noncat_types().then(function() {
744
745             params.noncat = true;
746             var type = egCore.env.cnct.map[params.noncat_type];
747
748             return $uibModal.open({
749                 templateUrl: './circ/share/t_noncat_dialog',
750                 controller: 
751                     ['$scope', '$uibModalInstance',
752                     function($scope, $uibModalInstance) {
753                     $scope.focusMe = true;
754                     $scope.type = type;
755                     $scope.count = 1;
756                     $scope.noncatMax = noncatMax;
757                     $scope.ok = function(count) { $uibModalInstance.close(count) }
758                     $scope.cancel = function ($event) { 
759                         $uibModalInstance.dismiss() 
760                         $event.preventDefault();
761                     }
762                 }],
763             }).result.then(
764                 function(count) {
765                     if (count && count > 0 && count <= noncatMax) { 
766                         // NOTE: in Chrome, form validation ensure a valid number
767                         params.noncat_count = count;
768                         return $q.when(params);
769                     } else {
770                         return $q.reject();
771                     }
772                 }
773             );
774         });
775     }
776
777     // Opens a dialog allowing the user to fill in pre-cat copy info.
778     service.precat_dialog = function(params, options) {
779
780         return $uibModal.open({
781             templateUrl: './circ/share/t_precat_dialog',
782             controller: 
783                 ['$scope', '$uibModalInstance', 'circMods',
784                 function($scope, $uibModalInstance, circMods) {
785                 $scope.focusMe = true;
786                 $scope.precatArgs = {
787                     copy_barcode : params.copy_barcode
788                 };
789                 $scope.circModifiers = circMods;
790                 $scope.ok = function(args) { $uibModalInstance.close(args) }
791                 $scope.cancel = function () { $uibModalInstance.dismiss() }
792             }],
793             resolve : {
794                 circMods : function() { 
795                     return service.get_circ_mods();
796                 }
797             }
798         }).result.then(
799             function(args) {
800                 if (!args || !args.dummy_title) return $q.reject();
801                 if(args.circ_modifier == "") args.circ_modifier = null;
802                 angular.forEach(args, function(val, key) {params[key] = val});
803                 params.precat = true;
804                 return service.checkout(params, options);
805             }
806         );
807     }
808
809     // find the open transit for the given copy barcode; flesh the org
810     // units locally.
811     service.find_copy_transit = function(evt, params, options) {
812         if (angular.isArray(evt)) evt = evt[0];
813
814         if (evt && evt.payload && evt.payload.transit)
815             return $q.when(evt.payload.transit);
816
817          return egCore.pcrud.search('atc',
818             {   dest_recv_time : null},
819             {   flesh : 1, 
820                 flesh_fields : {atc : ['target_copy']},
821                 join : {
822                     acp : {
823                         filter : {
824                             barcode : params.copy_barcode,
825                             deleted : 'f'
826                         }
827                     }
828                 },
829                 limit : 1,
830                 order_by : {atc : 'source_send_time desc'}, 
831             }
832         ).then(function(transit) {
833             transit.source(egCore.org.get(transit.source()));
834             transit.dest(egCore.org.get(transit.dest()));
835             return transit;
836         });
837     }
838
839     service.copy_in_transit_dialog = function(evt, params, options) {
840         if (angular.isArray(evt)) evt = evt[0];
841         return $uibModal.open({
842             templateUrl: './circ/share/t_copy_in_transit_dialog',
843             controller: 
844                        ['$scope','$uibModalInstance','transit',
845                 function($scope , $uibModalInstance , transit) {
846                 $scope.transit = transit;
847                 $scope.ok = function() { $uibModalInstance.close(transit) }
848                 $scope.cancel = function() { $uibModalInstance.dismiss() }
849             }],
850             resolve : {
851                 // fetch the conflicting open transit w/ fleshed copy
852                 transit : function() {
853                     return service.find_copy_transit(evt, params, options);
854                 }
855             }
856         }).result.then(
857             function(transit) {
858                 // user chose to abort the transit then checkout
859                 return service.abort_transit(transit.id())
860                 .then(function() {
861                     return service.checkout(params, options);
862                 });
863             }
864         );
865     }
866
867     service.abort_transit = function(transit_id) {
868         return egCore.net.request(
869             'open-ils.circ',
870             'open-ils.circ.transit.abort',
871             egCore.auth.token(), {transitid : transit_id}
872         ).then(function(resp) {
873             if (evt = egCore.evt.parse(resp)) {
874                 alert(evt);
875                 return $q.reject();
876             }
877             return $q.when();
878         });
879     }
880
881     service.last_copy_circ = function(copy_id) {
882         return egCore.pcrud.search('circ', 
883             {target_copy : copy_id},
884             {order_by : {circ : 'xact_start desc' }, limit : 1}
885         );
886     }
887
888     service.circ_exists_dialog = function(evt, params, options) {
889         if (angular.isArray(evt)) evt = evt[0];
890
891         if (!evt.payload.old_circ) {
892             return egCore.pcrud.search('circ',
893                 {target_copy : evt.payload.copy.id(), checkin_time : null},
894                 {limit : 1} // should only ever be 1
895             ).then(function(old_circ) {
896                 evt.payload.old_circ = old_circ;
897                return service.circ_exists_dialog_impl(evt, params, options);
898             });
899         } else {
900             return service.circ_exists_dialog_impl( evt, params, options );
901         }
902     },
903
904     service.circ_exists_dialog_impl = function (evt, params, options) {
905
906         var openCirc = evt.payload.old_circ;
907         var sameUser = openCirc.usr() == params.patron_id;
908         
909         return $uibModal.open({
910             templateUrl: './circ/share/t_circ_exists_dialog',
911             controller: 
912                        ['$scope','$uibModalInstance',
913                 function($scope , $uibModalInstance) {
914                 $scope.args = {forgive_fines : false};
915                 $scope.circDate = openCirc.xact_start();
916                 $scope.sameUser = sameUser;
917                 $scope.ok = function() { $uibModalInstance.close($scope.args) }
918                 $scope.cancel = function($event) { 
919                     $uibModalInstance.dismiss();
920                     $event.preventDefault(); // form, avoid calling ok();
921                 }
922             }]
923         }).result.then(
924             function(args) {
925                 if (sameUser) {
926                     params.void_overdues = args.forgive_fines;
927                     options.override = true;
928                     return service.renew(params, options);
929                 }
930
931                 return service.checkin({
932                     barcode : params.copy_barcode,
933                     noop : true,
934                     override : true,
935                     void_overdues : args.forgive_fines
936                 }).then(function(checkin_resp) {
937                     if (checkin_resp.evt[0].textcode == 'SUCCESS') {
938                         return service.checkout(params, options);
939                     } else {
940                         alert(egCore.evt.parse(checkin_resp.evt[0]));
941                         return $q.reject();
942                     }
943                 });
944             }
945         );
946     }
947
948     service.batch_backdate = function(circ_ids, backdate) {
949         return egCore.net.request(
950             'open-ils.circ',
951             'open-ils.circ.post_checkin_backdate.batch',
952             egCore.auth.token(), circ_ids, backdate);
953     }
954
955     service.backdate_dialog = function(circ_ids) {
956         return $uibModal.open({
957             templateUrl: './circ/share/t_backdate_dialog',
958             controller: 
959                        ['$scope','$uibModalInstance',
960                 function($scope , $uibModalInstance) {
961
962                 var today = new Date();
963                 $scope.dialog = {
964                     num_circs : circ_ids.length,
965                     num_processed : 0,
966                     backdate : today
967                 }
968
969                 $scope.$watch('dialog.backdate', function(newval) {
970                     if (newval && newval > today) 
971                         $scope.dialog.backdate = today;
972                 });
973
974
975                 $scope.cancel = function() { 
976                     $uibModalInstance.dismiss();
977                 }
978
979                 $scope.ok = function() { 
980
981                     var bd = $scope.dialog.backdate.toISOString().replace(/T.*/,'');
982                     service.batch_backdate(circ_ids, bd)
983                     .then(
984                         function() { // on complete
985                             $uibModalInstance.close({backdate : bd});
986                         },
987                         null,
988                         function(resp) { // on response
989                             console.debug('backdate returned ' + resp);
990                             if (resp == '1') {
991                                 $scope.num_processed++;
992                             } else {
993                                 console.error(egCore.evt.parse(resp));
994                             }
995                         }
996                     );
997                 }
998             }]
999         }).result;
1000     }
1001
1002     service.mark_claims_returned = function(barcode, date, override) {
1003
1004         var method = 'open-ils.circ.circulation.set_claims_returned';
1005         if (override) method += '.override';
1006
1007         console.debug('claims returned ' + method);
1008
1009         return egCore.net.request(
1010             'open-ils.circ', method, egCore.auth.token(),
1011             {barcode : barcode, backdate : date})
1012
1013         .then(function(resp) {
1014
1015             if (resp == 1) { // success
1016                 console.debug('claims returned succeeded for ' + barcode);
1017                 return barcode;
1018
1019             } else if (evt = egCore.evt.parse(resp)) {
1020                 console.debug('claims returned failed: ' + evt.toString());
1021
1022                 if (evt.textcode == 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT') {
1023                     // TODO check perms before offering override option?
1024
1025                     if (override) return;// just to be safe
1026
1027                     return egConfirmDialog.open(
1028                         egCore.strings.TOO_MANY_CLAIMS_RETURNED, '', {}
1029                     ).result.then(function() {
1030                         return service.mark_claims_returned(barcode, date, true);
1031                     });
1032                 }
1033
1034                 if (evt.textcode == 'PERM_FAILURE') {
1035                     console.error('claims returned permission denied')
1036                     // TODO: auth override dialog?
1037                 }
1038             }
1039         });
1040     }
1041
1042     service.mark_claims_returned_dialog = function(copy_barcodes) {
1043         if (!copy_barcodes.length) return;
1044
1045         return $uibModal.open({
1046             templateUrl: './circ/share/t_mark_claims_returned_dialog',
1047             controller: 
1048                        ['$scope','$uibModalInstance',
1049                 function($scope , $uibModalInstance) {
1050
1051                 var today = new Date();
1052                 $scope.args = {
1053                     barcodes : copy_barcodes,
1054                     date : today
1055                 };
1056
1057                 $scope.$watch('args.date', function(newval) {
1058                     if (newval && newval > today) 
1059                         $scope.args.backdate = today;
1060                 });
1061
1062                 $scope.cancel = function() {$uibModalInstance.dismiss()}
1063                 $scope.ok = function() { 
1064
1065                     var date = $scope.args.date.toISOString().replace(/T.*/,'');
1066
1067                     var deferred = $q.defer();
1068
1069                     // serialize the action on each barcode so that the 
1070                     // caller will never see multiple alerts at the same time.
1071                     function mark_one() {
1072                         var bc = copy_barcodes.pop();
1073                         if (!bc) {
1074                             deferred.resolve();
1075                             $uibModalInstance.close();
1076                             return;
1077                         }
1078
1079                         // finally -> continue even when one fails
1080                         service.mark_claims_returned(bc, date)
1081                         .finally(function(barcode) {
1082                             if (barcode) deferred.notify(barcode);
1083                             mark_one();
1084                         });
1085                     }
1086                     mark_one(); // kick it off
1087                     return deferred.promise;
1088                 }
1089             }]
1090         }).result;
1091     }
1092
1093     // serially checks in each barcode with claims_never_checked_out set
1094     // returns promise, notified on each barcode, resolved after all
1095     // checkins are complete.
1096     service.mark_claims_never_checked_out = function(barcodes) {
1097         if (!barcodes.length) return;
1098
1099         var deferred = $q.defer();
1100         egConfirmDialog.open(
1101             egCore.strings.MARK_NEVER_CHECKED_OUT, '', {barcodes : barcodes}
1102
1103         ).result.then(function() {
1104             function mark_one() {
1105                 var bc = barcodes.pop();
1106
1107                 if (!bc) { // all done
1108                     deferred.resolve();
1109                     return;
1110                 }
1111
1112                 service.checkin(
1113                     {claims_never_checked_out : true, copy_barcode : bc})
1114                 .finally(function() { 
1115                     deferred.notify(bc);
1116                     mark_one();
1117                 })
1118             }
1119             mark_one();
1120         });
1121
1122         return deferred.promise;
1123     }
1124
1125     service.mark_damaged = function(copy_ids) {
1126         return egConfirmDialog.open(
1127             egCore.strings.MARK_DAMAGED_CONFIRM, '',
1128             {   num_items : copy_ids.length,
1129                 ok : function() {},
1130                 cancel : function() {}
1131             }
1132
1133         ).result.then(function() {
1134             var promises = [];
1135             angular.forEach(copy_ids, function(copy_id) {
1136                 promises.push(
1137                     egCore.net.request(
1138                         'open-ils.circ',
1139                         'open-ils.circ.mark_item_damaged',
1140                         egCore.auth.token(), copy_id
1141                     ).then(function(resp) {
1142                         if (evt = egCore.evt.parse(resp)) {
1143                             console.error('mark damaged failed: ' + evt);
1144                         }
1145                     })
1146                 );
1147             });
1148
1149             return $q.all(promises);
1150         });
1151     }
1152
1153     service.mark_missing = function(copy_ids) {
1154         return egConfirmDialog.open(
1155             egCore.strings.MARK_MISSING_CONFIRM, '',
1156             {   num_items : copy_ids.length,
1157                 ok : function() {},
1158                 cancel : function() {}
1159             }
1160         ).result.then(function() {
1161             var promises = [];
1162             angular.forEach(copy_ids, function(copy_id) {
1163                 promises.push(
1164                     egCore.net.request(
1165                         'open-ils.circ',
1166                         'open-ils.circ.mark_item_missing',
1167                         egCore.auth.token(), copy_id
1168                     ).then(function(resp) {
1169                         if (evt = egCore.evt.parse(resp)) {
1170                             console.error('mark missing failed: ' + evt);
1171                         }
1172                     })
1173                 );
1174             });
1175
1176             return $q.all(promises);
1177         });
1178     }
1179
1180
1181
1182     // Mark circulations as lost via copy barcode.  As each item is 
1183     // processed, the returned promise is notified of the barcode.
1184     // No confirmation dialog is presented.
1185     service.mark_lost = function(copy_barcodes) {
1186         var deferred = $q.defer();
1187         var promises = [];
1188
1189         angular.forEach(copy_barcodes, function(barcode) {
1190             promises.push(
1191                 egCore.net.request(
1192                     'open-ils.circ',
1193                     'open-ils.circ.circulation.set_lost',
1194                     egCore.auth.token(), {barcode : barcode}
1195                 ).then(function(resp) {
1196                     if (evt = egCore.evt.parse(resp)) {
1197                         console.error("Mark lost failed: " + evt.toString());
1198                         return;
1199                     }
1200                     // inform the caller as each item is processed
1201                     deferred.notify(barcode);
1202                 })
1203             );
1204         });
1205
1206         $q.all(promises).then(function() {deferred.resolve()});
1207         return deferred.promise;
1208     }
1209
1210     service.abort_transits = function(transit_ids) {
1211         return egConfirmDialog.open(
1212             egCore.strings.ABORT_TRANSIT_CONFIRM, '',
1213             {   num_transits : transit_ids.length,
1214                 ok : function() {},
1215                 cancel : function() {}
1216             }
1217
1218         ).result.then(function() {
1219             var promises = [];
1220             angular.forEach(transit_ids, function(transit_id) {
1221                 promises.push(
1222                     egCore.net.request(
1223                         'open-ils.circ',
1224                         'open-ils.circ.transit.abort',
1225                         egCore.auth.token(), {transitid : transit_id}
1226                     ).then(function(resp) {
1227                         if (evt = egCore.evt.parse(resp)) {
1228                             console.error('abort transit failed: ' + evt);
1229                         }
1230                     })
1231                 );
1232             });
1233
1234             return $q.all(promises);
1235         });
1236     }
1237
1238
1239
1240     // alert when copy location alert_message is set.
1241     // This does not affect processing, it only produces a click-through
1242     service.handle_checkin_loc_alert = function(evt, params, options) {
1243         if (angular.isArray(evt)) evt = evt[0];
1244
1245         var copy = evt && evt.payload ? evt.payload.copy : null;
1246
1247         if (copy && !options.suppress_checkin_popups
1248             && copy.location().checkin_alert() == 't') {
1249
1250             return egAlertDialog.open(
1251                 egCore.strings.LOCATION_ALERT_MSG, {copy : copy}).result;
1252         }
1253
1254         return $q.when();
1255     }
1256
1257     service.handle_checkin_resp = function(evt, params, options) {
1258         if (!angular.isArray(evt)) evt = [evt];
1259
1260         var final_resp = {evt : evt, params : params, options : options};
1261
1262         var copy, hold, transit;
1263         if (evt[0].payload) {
1264             copy = evt[0].payload.copy;
1265             hold = evt[0].payload.hold;
1266             transit = evt[0].payload.transit;
1267         }
1268
1269         // track the barcode regardless of whether it's valid
1270         angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
1271
1272         angular.forEach(evt, function(e){ console.debug('checkin event ' + e.textcode); });
1273
1274         if (evt.filter(function(e){return service.checkin_overridable_events.indexOf(e.textcode) > -1;}).length > 0)
1275             return service.handle_overridable_checkin_event(evt, params, options);
1276
1277         switch (evt[0].textcode) {
1278
1279             case 'SUCCESS':
1280             case 'NO_CHANGE':
1281
1282                 switch(Number(copy.status().id())) {
1283
1284                     case 0: /* AVAILABLE */                                        
1285                     case 4: /* MISSING */                                          
1286                     case 7: /* RESHELVING */ 
1287
1288                         egCore.audio.play('success.checkin');
1289
1290                         // see if the copy location requires an alert
1291                         return service.handle_checkin_loc_alert(evt, params, options)
1292                         .then(function() {return final_resp});
1293
1294                     case 8: /* ON HOLDS SHELF */
1295                         egCore.audio.play('info.checkin.holds_shelf');
1296                         
1297                         if (hold) {
1298
1299                             if (hold.pickup_lib() == egCore.auth.user().ws_ou()) {
1300                                 // inform user if the item is on the local holds shelf
1301                             
1302                                 evt[0].route_to = egCore.strings.ROUTE_TO_HOLDS_SHELF;
1303                                 return service.route_dialog(
1304                                     './circ/share/t_hold_shelf_dialog', 
1305                                     evt[0], params, options
1306                                 ).then(function() { return final_resp });
1307
1308                             } else {
1309                                 // normally, if the hold was on the shelf at a 
1310                                 // different location, it would be put into 
1311                                 // transit, resulting in a ROUTE_ITEM event.
1312                                 egCore.audio.play('warning.checkin.wrong_shelf');
1313                                 return $q.when(final_resp);
1314                             }
1315                         } else {
1316
1317                             console.error('checkin: item on holds shelf, '
1318                                 + 'but hold info not returned from checkin');
1319                             return $q.when(final_resp);
1320                         }
1321
1322                     case 11: /* CATALOGING */
1323                         egCore.audio.play('info.checkin.cataloging');
1324                         evt[0].route_to = egCore.strings.ROUTE_TO_CATALOGING;
1325                         return $q.when(final_resp);
1326
1327                     case 15: /* ON_RESERVATION_SHELF */
1328                         egCore.audio.play('info.checkin.reservation');
1329                         // TODO: show booking reservation dialog
1330                         return $q.when(final_resp);
1331
1332                     default:
1333                         egCore.audio.play('error.checkin.unknown');
1334                         console.error('Unhandled checkin copy status: ' 
1335                             + copy.status().id() + ' : ' + copy.status().name());
1336                         return $q.when(final_resp);
1337                 }
1338                 
1339             case 'ROUTE_ITEM':
1340                 return service.route_dialog(
1341                     './circ/share/t_transit_dialog', 
1342                     evt[0], params, options
1343                 ).then(function() { return final_resp });
1344
1345             case 'ASSET_COPY_NOT_FOUND':
1346                 egCore.audio.play('error.checkin.not_found');
1347                 return egAlertDialog.open(
1348                     egCore.strings.UNCAT_ALERT_DIALOG, params)
1349                     .result.then(function() {return final_resp});
1350
1351             case 'ITEM_NOT_CATALOGED':
1352                 egCore.audio.play('error.checkin.not_cataloged');
1353                 evt[0].route_to = egCore.strings.ROUTE_TO_CATALOGING;
1354                 if (options.no_precat_alert) 
1355                     return $q.when(final_resp);
1356                 return egAlertDialog.open(
1357                     egCore.strings.PRECAT_CHECKIN_MSG, params)
1358                     .result.then(function() {return final_resp});
1359
1360             default:
1361                 egCore.audio.play('error.checkin.unknown');
1362                 console.warn('unhandled checkin response : ' + evt[0].textcode);
1363                 return $q.when(final_resp);
1364         }
1365     }
1366
1367     // collect transit, address, and hold info that's not already
1368     // included in responses.
1369     service.collect_route_data = function(tmpl, evt, params, options) {
1370         if (angular.isArray(evt)) evt = evt[0];
1371         var promises = [];
1372         var data = {};
1373
1374         if (evt.org && !tmpl.match(/hold_shelf/)) {
1375             promises.push(
1376                 service.get_org_addr(evt.org, 'holds_address')
1377                 .then(function(addr) { data.address = addr })
1378             );
1379         }
1380
1381         if (evt.payload.hold) {
1382             promises.push(
1383                 egCore.pcrud.retrieve('au', 
1384                     evt.payload.hold.usr(), {
1385                         flesh : 1,
1386                         flesh_fields : {'au' : ['card']}
1387                     }
1388                 ).then(function(patron) {data.patron = patron})
1389             );
1390         }
1391
1392         if (!tmpl.match(/hold_shelf/)) {
1393             promises.push(
1394                 service.find_copy_transit(evt, params, options)
1395                 .then(function(trans) {data.transit = trans})
1396             );
1397         }
1398
1399         return $q.all(promises).then(function() { return data });
1400     }
1401
1402     service.route_dialog = function(tmpl, evt, params, options) {
1403         if (angular.isArray(evt)) evt = evt[0];
1404
1405         return service.collect_route_data(tmpl, evt, params, options)
1406         .then(function(data) {
1407
1408             var template = data.transit ?
1409                 (data.patron ? 'hold_transit_slip' : 'transit_slip') :
1410                 'hold_shelf_slip';
1411             if (service.never_auto_print[template]) {
1412                 // do not show the dialog or print if the
1413                 // disabled automatic print attempt type list includes
1414                 // the specified template
1415                 return;
1416             }
1417
1418             // All actions flow from the print data
1419
1420             var print_context = {
1421                 copy : egCore.idl.toHash(evt.payload.copy),
1422                 title : evt.title,
1423                 author : evt.author
1424             }
1425
1426             if (data.transit) {
1427                 // route_dialog includes the "route to holds shelf" 
1428                 // dialog, which has no transit
1429                 print_context.transit = egCore.idl.toHash(data.transit);
1430                 if (data.address) {
1431                     print_context.dest_address = egCore.idl.toHash(data.address);
1432                 }
1433                 print_context.dest_location =
1434                     egCore.idl.toHash(egCore.org.get(data.transit.dest()));
1435             }
1436
1437             if (data.patron) {
1438                 print_context.hold = egCore.idl.toHash(evt.payload.hold);
1439                 print_context.patron = egCore.idl.toHash(data.patron);
1440             }
1441
1442             var sound = 'info.checkin.transit';
1443             if (evt.payload.hold) sound += '.hold';
1444             egCore.audio.play(sound);
1445
1446             function print_transit(template) {
1447                 return egCore.print.print({
1448                     context : 'default', 
1449                     template : template, 
1450                     scope : print_context
1451                 });
1452             }
1453
1454             // when auto-print is on, skip the dialog and go straight
1455             // to printing.
1456             if (options.auto_print_holds_transits) 
1457                 return print_transit(template);
1458
1459             return $uibModal.open({
1460                 templateUrl: tmpl,
1461                 controller: [
1462                             '$scope','$uibModalInstance',
1463                     function($scope , $uibModalInstance) {
1464
1465                     $scope.today = new Date();
1466
1467                     // copy the print scope into the dialog scope
1468                     angular.forEach(print_context, function(val, key) {
1469                         $scope[key] = val;
1470                     });
1471
1472                     $scope.ok = function() {$uibModalInstance.close()}
1473
1474                     $scope.print = function() { 
1475                         $uibModalInstance.close();
1476                         print_transit(template);
1477                     }
1478                 }]
1479
1480             }).result;
1481         });
1482     }
1483
1484     // action == what action to take if the user confirms the alert
1485     service.copy_alert_dialog = function(evt, params, options, action) {
1486         if (angular.isArray(evt)) evt = evt[0];
1487         return egConfirmDialog.open(
1488             egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, 
1489             evt.payload,  // payload == alert message text
1490             {   copy_barcode : params.copy_barcode,
1491                 ok : function() {},
1492                 cancel : function() {}
1493             }
1494         ).result.then(function() {
1495             options.override = true;
1496             return service[action](params, options);
1497         });
1498     }
1499
1500     // check the barcode.  If it's no good, show the warning dialog
1501     // Resolves on success, rejected on error
1502     service.test_barcode = function(bc) {
1503
1504         var ok = service.check_barcode(bc);
1505         if (ok) return $q.when();
1506
1507         egCore.audio.play('warning.circ.bad_barcode');
1508         return $uibModal.open({
1509             templateUrl: './circ/share/t_bad_barcode_dialog',
1510             controller: 
1511                 ['$scope', '$uibModalInstance', 
1512                 function($scope, $uibModalInstance) {
1513                 $scope.barcode = bc;
1514                 $scope.ok = function() { $uibModalInstance.close() }
1515                 $scope.cancel = function() { $uibModalInstance.dismiss() }
1516             }]
1517         }).result;
1518     }
1519
1520     // check() and checkdigit() copied directly 
1521     // from chrome/content/util/barcode.js
1522
1523     service.check_barcode = function(bc) {
1524         if (bc != Number(bc)) return false;
1525         bc = bc.toString();
1526         // "16.00" == Number("16.00"), but the . is bad.
1527         // Throw out any barcode that isn't just digits
1528         if (bc.search(/\D/) != -1) return false;
1529         var last_digit = bc.substr(bc.length-1);
1530         var stripped_barcode = bc.substr(0,bc.length-1);
1531         return service.barcode_checkdigit(stripped_barcode).toString() == last_digit;
1532     }
1533
1534     service.barcode_checkdigit = function(bc) {
1535         var reverse_barcode = bc.toString().split('').reverse();
1536         var check_sum = 0; var multiplier = 2;
1537         for (var i = 0; i < reverse_barcode.length; i++) {
1538             var digit = reverse_barcode[i];
1539             var product = digit * multiplier; product = product.toString();
1540             var temp_sum = 0;
1541             for (var j = 0; j < product.length; j++) {
1542                 temp_sum += Number( product[j] );
1543             }
1544             check_sum += Number( temp_sum );
1545             multiplier = ( multiplier == 2 ? 1 : 2 );
1546         }
1547         check_sum = check_sum.toString();
1548         var next_multiple_of_10 = (check_sum.match(/(\d*)\d$/)[1] * 10) + 10;
1549         var check_digit = next_multiple_of_10 - Number(check_sum);
1550         if (check_digit == 10) check_digit = 0;
1551         return check_digit;
1552     }
1553
1554     service.create_penalty = function(user_id) {
1555         return $uibModal.open({
1556             templateUrl: './circ/share/t_new_message_dialog',
1557             controller: 
1558                    ['$scope','$uibModalInstance','staffPenalties',
1559             function($scope , $uibModalInstance , staffPenalties) {
1560                 $scope.focusNote = true;
1561                 $scope.penalties = staffPenalties;
1562                 $scope.require_initials = service.require_initials;
1563                 $scope.args = {penalty : 21}; // default to Note
1564                 $scope.setPenalty = function(id) {
1565                     args.penalty = id;
1566                 }
1567                 $scope.ok = function(count) { $uibModalInstance.close($scope.args) }
1568                 $scope.cancel = function($event) { 
1569                     $uibModalInstance.dismiss();
1570                     $event.preventDefault();
1571                 }
1572             }],
1573             resolve : { staffPenalties : service.get_staff_penalty_types }
1574         }).result.then(
1575             function(args) {
1576                 var pen = new egCore.idl.ausp();
1577                 pen.usr(user_id);
1578                 pen.org_unit(egCore.auth.user().ws_ou());
1579                 pen.note(args.note);
1580                 if (args.initials) pen.note(args.note + ' [' + args.initials + ']');
1581                 if (args.custom_penalty) {
1582                     pen.standing_penalty(args.custom_penalty);
1583                 } else {
1584                     pen.standing_penalty(args.penalty);
1585                 }
1586                 pen.staff(egCore.auth.user().id());
1587                 pen.set_date('now');
1588                 return egCore.pcrud.create(pen);
1589             }
1590         );
1591     }
1592
1593     // assumes, for now anyway,  penalty type is fleshed onto usr_penalty.
1594     service.edit_penalty = function(usr_penalty) {
1595         return $uibModal.open({
1596             templateUrl: './circ/share/t_new_message_dialog',
1597             controller: 
1598                    ['$scope','$uibModalInstance','staffPenalties',
1599             function($scope , $uibModalInstance , staffPenalties) {
1600                 $scope.focusNote = true;
1601                 $scope.penalties = staffPenalties;
1602                 $scope.require_initials = service.require_initials;
1603                 $scope.args = {
1604                     penalty : usr_penalty.standing_penalty().id(),
1605                     note : usr_penalty.note()
1606                 }
1607                 $scope.setPenalty = function(id) { args.penalty = id; }
1608                 $scope.ok = function(count) { $uibModalInstance.close($scope.args) }
1609                 $scope.cancel = function($event) { 
1610                     $uibModalInstance.dismiss();
1611                     $event.preventDefault();
1612                 }
1613             }],
1614             resolve : { staffPenalties : service.get_staff_penalty_types }
1615         }).result.then(
1616             function(args) {
1617                 usr_penalty.note(args.note);
1618                 if (args.initials) usr_penalty.note(args.note + ' [' + args.initials + ']');
1619                 usr_penalty.standing_penalty(args.penalty);
1620                 return egCore.pcrud.update(usr_penalty);
1621             }
1622         );
1623     }
1624
1625     return service;
1626
1627 }]);
1628
1629