]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/circ/services/circ.js
LP2045292 Color contrast for AngularJS patron bills
[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
9        ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog','egAddCopyAlertDialog','egCopyAlertManagerDialog','egCopyAlertEditorDialog',
10         'egWorkLog',
11 function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAlertDialog , egCopyAlertManagerDialog,  egCopyAlertEditorDialog ,
12          egWorkLog) {
13
14     var service = {
15         // auto-override these events after the first override
16         auto_override_circ_events : {},
17         // auto-skip these events after the first skip
18         auto_skip_circ_events : {},
19         require_initials : false,
20         never_auto_print : {
21             hold_shelf_slip : false,
22             hold_transit_slip : false,
23             transit_slip : false
24         },
25         in_flight_checkins: {}
26     };
27
28     egCore.startup.go().finally(function() {
29         egCore.org.settings([
30             'ui.staff.require_initials.patron_standing_penalty',
31             'ui.admin.work_log.max_entries',
32             'ui.admin.patron_log.max_entries',
33             'circ.staff_client.do_not_auto_attempt_print',
34             'circ.clear_hold_on_checkout'
35         ]).then(function(set) {
36             service.require_initials = Boolean(set['ui.staff.require_initials.patron_standing_penalty']);
37             service.clearHold = Boolean(set['circ.clear_hold_on_checkout']);
38
39             if (angular.isArray(set['circ.staff_client.do_not_auto_attempt_print'])) {
40                 if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Hold Slip') > 1)
41                     service.never_auto_print['hold_shelf_slip'] = true;
42                 if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Hold/Transit Slip') > 1)
43                     service.never_auto_print['hold_transit_slip'] = true;
44                 if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Transit Slip') > 1)
45                     service.never_auto_print['transit_slip'] = true;
46             }
47         });
48     });
49
50     service.reset = function() {
51         service.auto_override_circ_events = {};
52         service.auto_skip_circ_events = {};
53     }
54
55     // these events cannot be overriden
56     service.nonoverridable_events = [
57         'ACTION_CIRCULATION_NOT_FOUND',
58         'ACTOR_USER_NOT_FOUND',
59         'ASSET_COPY_NOT_FOUND',
60         'PATRON_INACTIVE',
61         'PATRON_CARD_INACTIVE',
62         'PATRON_ACCOUNT_EXPIRED',
63         'PERM_FAILURE' // should be handled elsewhere
64     ]
65
66     // Default to checked for "Automatically override for subsequent items?"
67     service.default_auto_override = [
68         'PATRON_EXCEEDS_OVERDUE_COUNT',
69         'PATRON_BARRED',
70         'PATRON_EXCEEDS_LOST_COUNT',
71         'PATRON_EXCEEDS_CHECKOUT_COUNT',
72         'PATRON_EXCEEDS_FINES',
73         'PATRON_EXCEEDS_LONGOVERDUE_COUNT'
74     ]
75
76     // these checkin events do not produce alerts when 
77     // options.suppress_alerts is in effect.
78     service.checkin_suppress_overrides = [
79         'COPY_BAD_STATUS',
80         'PATRON_BARRED',
81         'PATRON_INACTIVE',
82         'PATRON_ACCOUNT_EXPIRED',
83         'ITEM_DEPOSIT_PAID',
84         'CIRC_CLAIMS_RETURNED',
85         'COPY_ALERT_MESSAGE',
86         'COPY_STATUS_LOST',
87         'COPY_STATUS_LOST_AND_PAID',
88         'COPY_STATUS_LONG_OVERDUE',
89         'COPY_STATUS_MISSING',
90         'PATRON_EXCEEDS_FINES'
91     ]
92
93     // these events can be overridden by staff during checkin
94     service.checkin_overridable_events = 
95         service.checkin_suppress_overrides.concat([
96         'HOLD_CAPTURE_DELAYED', // not technically overridable, but special prompt and param
97         'TRANSIT_CHECKIN_INTERVAL_BLOCK'
98     ])
99
100     // Performs a checkout.
101     // Returns a promise resolved with the original params and options
102     // and the final checkout event (e.g. in the case of override).
103     // Rejected if the checkout cannot be completed.
104     //
105     // params : passed directly as arguments to the server API 
106     // options : non-parameter controls.  e.g. "override", "check_barcode"
107     service.checkout = function(params, options) {
108         if (!options) options = {};
109         params.new_copy_alerts = 1;
110
111         console.debug('egCirc.checkout() : ' 
112             + js2JSON(params) + ' : ' + js2JSON(options));
113
114         // handle barcode completion
115         return service.handle_barcode_completion(params.copy_barcode)
116         .then(function(barcode) {
117             console.debug('barcode after completion: ' + barcode);
118             params.copy_barcode = barcode;
119
120             var promise = options.check_barcode ? 
121                 service.test_barcode(params.copy_barcode) : $q.when();
122
123             // avoid re-check on override, etc.
124             delete options.check_barcode;
125
126             return promise.then(function() {
127
128                 var method = 'open-ils.circ.checkout.full';
129                 if (options.override) method += '.override';
130
131                 return egCore.net.request(
132                     'open-ils.circ', method, egCore.auth.token(), params
133
134                 ).then(function(evt) {
135
136                     if (!angular.isArray(evt)) evt = [evt];
137
138                     if (evt[0].payload && evt[0].payload.auto_renew == 1) {
139                         // open circulation found with auto-renew toggle on.
140                         console.debug('Auto-renewing item ' + params.copy_barcode);
141                         options.auto_renew = true;
142                         return service.renew(params, options);
143                     }
144
145                     var action = params.noncat ? 'noncat_checkout' : 'checkout';
146
147                     return service.flesh_response_data(action, evt, params, options)
148                     .then(function() {
149                         return service.handle_checkout_resp(evt, params, options);
150                     })
151                     .then(function(final_resp) {
152                         return service.munge_resp_data(final_resp,action,method)
153                     })
154                 });
155             });
156         });
157     }
158
159     // Performs a renewal.
160     // Returns a promise resolved with the original params and options
161     // and the final checkout event (e.g. in the case of override)
162     // Rejected if the renewal cannot be completed.
163     service.renew = function(params, options) {
164         if (!options) options = {};
165         params.new_copy_alerts = 1;
166
167         console.debug('egCirc.renew() : ' 
168             + js2JSON(params) + ' : ' + js2JSON(options));
169
170         // handle barcode completion
171         return service.handle_barcode_completion(params.copy_barcode)
172         .then(function(barcode) {
173             params.copy_barcode = barcode;
174
175             var promise = options.check_barcode ? 
176                 service.test_barcode(params.copy_barcode) : $q.when();
177
178             // avoid re-check on override, etc.
179             delete options.check_barcode;
180
181             return promise.then(function() {
182
183                 var method = 'open-ils.circ.renew';
184                 if (options.override) method += '.override';
185
186                 return egCore.net.request(
187                     'open-ils.circ', method, egCore.auth.token(), params
188
189                 ).then(function(evt) {
190
191                     if (!angular.isArray(evt)) evt = [evt];
192
193                     return service.flesh_response_data(
194                         'renew', evt, params, options)
195                     .then(function() {
196                         return service.handle_renew_resp(evt, params, options);
197                     })
198                     .then(function(final_resp) {
199                         final_resp.auto_renew = options.auto_renew;
200                         return service.munge_resp_data(final_resp,'renew',method)
201                     })
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         params.new_copy_alerts = 1;
214
215         console.debug('egCirc.checkin() : ' 
216             + js2JSON(params) + ' : ' + js2JSON(options));
217
218         // handle barcode completion
219         return service.handle_barcode_completion(params.copy_barcode)
220         .then(function(barcode) {
221             params.copy_barcode = barcode;
222
223             var promise = options.check_barcode ? 
224                 service.test_barcode(params.copy_barcode) : $q.when();
225
226             // avoid re-check on override, etc.
227             delete options.check_barcode;
228
229             return promise.then(function() {
230
231                 var method = 'open-ils.circ.checkin';
232                 if (options.override) method += '.override';
233
234                 // Multiple checkin API calls should never be active
235                 // for a single barcode.
236                 if (service.in_flight_checkins[barcode]) {
237                     console.error('Barcode ' + barcode 
238                         + ' is already in flight for checkin, skipping');
239                     return $q.reject();
240                 }
241                 service.in_flight_checkins[barcode] = true;
242
243                 return egCore.net.request(
244                     'open-ils.circ', method, egCore.auth.token(), params
245
246                 ).then(function(evt) {
247                     delete service.in_flight_checkins[barcode];
248
249                     if (!angular.isArray(evt)) evt = [evt];
250                     return service.flesh_response_data(
251                         'checkin', evt, params, options)
252                     .then(function() {
253                         return service.handle_checkin_resp(evt, params, options);
254                     })
255                     .then(function(final_resp) {
256                         return service.munge_resp_data(final_resp,'checkin',method)
257                     })
258                 }, function() {delete service.in_flight_checkins[barcode]});
259             });
260         });
261     }
262
263     // provide consistent formatting of the final response data
264     service.munge_resp_data = function(final_resp,worklog_action,worklog_method) {
265         var data = final_resp.data = {};
266
267         if (!final_resp.evt[0]) {
268             egCore.audio.play('error.unknown.no_event');
269             return;
270         }
271
272         var payload = final_resp.evt[0].payload;
273         if (!payload) {
274             egCore.audio.play('error.unknown.no_payload');
275             return;
276         }
277
278         // retrieve call number affixes prior to sending payload data to the grid
279         if (payload.volume && typeof payload.volume.prefix() != 'object') {
280             egCore.pcrud.retrieve('acnp',payload.volume.prefix()).then(function(p) {payload.volume.prefix(p)});
281         };
282         if (payload.volume && typeof payload.volume.suffix() != 'object') {
283             egCore.pcrud.retrieve('acns',payload.volume.suffix()).then(function(s) {payload.volume.suffix(s)});
284         };
285
286         data.circ = payload.circ;
287         data.parent_circ = payload.parent_circ;
288         data.hold = payload.hold;
289         data.record = payload.record;
290         data.acp = payload.copy;
291         data.acn = payload.volume ?  payload.volume : payload.copy ? payload.copy.call_number() : null;
292         data.au = payload.patron;
293         data.transit = payload.transit;
294         data.status = payload.status;
295         data.message = payload.message;
296         data.title = final_resp.evt[0].title;
297         data.author = final_resp.evt[0].author;
298         data.isbn = final_resp.evt[0].isbn;
299         data.route_to = final_resp.evt[0].route_to;
300
301
302         if (payload.circ) data.duration = payload.circ.duration();
303         if (payload.circ) data.circ_lib = payload.circ.circ_lib();
304
305         // for checkin, the mbts lives on the main circ
306         if (payload.circ && payload.circ.billable_transaction())
307             data.mbts = payload.circ.billable_transaction().summary();
308
309         // on renewals, the mbts lives on the parent circ
310         if (payload.parent_circ && payload.parent_circ.billable_transaction())
311             data.mbts = payload.parent_circ.billable_transaction().summary();
312
313         if (!data.route_to) {
314             if (data.transit && !data.transit.dest_recv_time() && !data.transit.cancel_time()) {
315                 data.route_to = data.transit.dest().shortname();
316             } else if (data.acp) {
317                 data.route_to = data.acp.location().name();
318             }
319         }
320         // allow us to get at the monograph parts associated with a copy
321         if (payload.copy && payload.copy.parts()) {
322             data._monograph_part = payload.copy.parts().map(function(part) {
323                 return part.label();
324             }).join(',');
325         }
326
327         egWorkLog.record(
328             (worklog_action == 'checkout' || worklog_action == 'noncat_checkout')
329             ? egCore.strings.EG_WORK_LOG_CHECKOUT
330             : (worklog_action == 'renew'
331                 ? egCore.strings.EG_WORK_LOG_RENEW
332                 : egCore.strings.EG_WORK_LOG_CHECKIN // worklog_action == 'checkin'
333             ),{
334                 'action' : worklog_action,
335                 'method' : worklog_method,
336                 'response' : final_resp
337             }
338         );
339
340         return final_resp;
341     }
342
343     service.handle_overridable_checkout_event = function(evt, params, options) {
344
345         if (options.override) {
346             // override attempt already made and failed.
347             // NOTE: I don't think we'll ever get here, since the
348             // override attempt should produce a perm failure...
349             angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
350             return $q.reject();
351
352         } 
353
354         if (evt.filter(function(e){return !service.auto_override_circ_events[e.textcode];}).length == 0) {
355             // user has already opted to override these type
356             // of events.  Re-run the checkout w/ override.
357             options.override = true;
358             return service.checkout(params, options);
359         } 
360
361         // Ask the user if they would like to override this event.
362         // Some events offer a stock override dialog, while others
363         // require additional context.
364
365         switch(evt[0].textcode) {
366             case 'COPY_NOT_AVAILABLE':
367                 return service.copy_not_avail_dialog(evt, params, options);
368             case 'COPY_ALERT_MESSAGE':
369                 return service.copy_alert_dialog(evt[0], params, options, 'checkout');
370             default: 
371                 return service.override_dialog(evt, params, options, 'checkout');
372         }
373     }
374
375     service.handle_overridable_renew_event = function(evt, params, options) {
376
377         if (options.override) {
378             // override attempt already made and failed.
379             // NOTE: I don't think we'll ever get here, since the
380             // override attempt should produce a perm failure...
381             angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
382             return $q.reject();
383
384         } 
385
386         // renewal auto-overrides are the same as checkout
387         if (evt.filter(function(e){return !service.auto_override_circ_events[e.textcode];}).length == 0) {
388             // user has already opted to override these type
389             // of events.  Re-run the renew w/ override.
390             options.override = true;
391             return service.renew(params, options);
392         } 
393
394         // Ask the user if they would like to override this event.
395         // Some events offer a stock override dialog, while others
396         // require additional context.
397
398         switch(evt[0].textcode) {
399             case 'COPY_ALERT_MESSAGE':
400                 return service.copy_alert_dialog(evt[0], params, options, 'renew');
401             default: 
402                 return service.override_dialog(evt, params, options, 'renew');
403         }
404     }
405
406
407     service.handle_overridable_checkin_event = function(evt, params, options) {
408
409         if (options.override) {
410             // override attempt already made and failed.
411             // NOTE: I don't think we'll ever get here, since the
412             // override attempt should produce a perm failure...
413             angular.forEach(evt, function(e){ console.debug('override failed: ' + e.textcode); });
414             return $q.reject();
415
416         } 
417
418         if (options.suppress_popups
419             && evt.filter(function(e){return service.checkin_suppress_overrides.indexOf(e.textcode) == -1;}).length == 0) {
420             // Events are suppressed.  Re-run the checkin w/ override.
421             options.override = true;
422             return service.checkin(params, options);
423         } 
424
425         // Ask the user if they would like to override this event.
426         // Some events offer a stock override dialog, while others
427         // require additional context.
428
429         switch(evt[0].textcode) {
430             case 'COPY_ALERT_MESSAGE':
431                 return service.copy_alert_dialog(evt[0], params, options, 'checkin');
432             case 'HOLD_CAPTURE_DELAYED':
433                 return service.hold_capture_delay_dialog(evt[0], params, options, 'checkin');
434             default: 
435                 return service.override_dialog(evt, params, options, 'checkin');
436         }
437     }
438
439
440     service.handle_renew_resp = function(evt, params, options) {
441
442         var final_resp = {evt : evt, params : params, options : options};
443
444         // track the barcode regardless of whether it refers to a copy
445         angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
446
447         // test for success first to simplify things
448         if (evt[0].textcode == 'SUCCESS') {
449             egCore.audio.play('info.renew');
450             return $q.when(final_resp);
451         }
452
453         // handle Overridable and Non-Overridable Events, but only if no skipped non-overridable events
454         if (evt.filter(function(e){return service.auto_skip_circ_events[e.textcode];}).length == 0) {
455             return service.handle_overridable_renew_event(evt, params, options);
456         }
457
458         // Other events
459         switch (evt[0].textcode) {
460             case 'COPY_IN_TRANSIT':
461             case 'PATRON_CARD_INACTIVE':
462             case 'PATRON_INACTIVE':
463             case 'PATRON_ACCOUNT_EXPIRED':
464             case 'CIRC_CLAIMS_RETURNED':
465             case 'ITEM_NOT_CATALOGED':
466             case 'ASSET_COPY_NOT_FOUND':
467                 // since handle_overridable_renew_event essentially advertises these events at some point,
468                 // we no longer need the original alerts; however, the sound effects are still nice.
469                 egCore.audio.play('warning.renew');
470                 return $q.reject();
471
472             default:
473                 egCore.audio.play('warning.renew.unknown');
474                 return service.exit_alert(
475                     egCore.strings.CHECKOUT_FAILED_GENERIC, {
476                         barcode : params.copy_barcode,
477                         textcode : evt[0].textcode,
478                         desc : evt[0].desc
479                     }
480                 );
481         }
482     }
483
484
485     service.handle_checkout_resp = function(evt, params, options) {
486
487         var final_resp = {evt : evt, params : params, options : options};
488
489         // track the barcode regardless of whether it refers to a copy
490         angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
491
492         // test for success first to simplify things
493         if (evt[0].textcode == 'SUCCESS') {
494             egCore.audio.play('success.checkout');
495             return $q.when(final_resp);
496         }
497
498         // other events that should precede generic overridable/non-overridable handling
499         switch (evt[0].textcode) {
500             case 'ITEM_NOT_CATALOGED':
501                 egCore.audio.play('error.checkout.no_cataloged');
502                 return service.precat_dialog(params, options);
503
504             case 'OPEN_CIRCULATION_EXISTS':
505                 // auto_renew checked in service.checkout()
506                 egCore.audio.play('error.checkout.open_circ');
507                 return service.circ_exists_dialog(evt, params, options);
508
509             case 'COPY_IN_TRANSIT':
510                 egCore.audio.play('warning.checkout.in_transit');
511                 return service.copy_in_transit_dialog(evt, params, options);
512         }
513
514         // handle Overridable and Non-Overridable Events, but only if no skipped non-overridable events
515         if (evt.filter(function(e){return service.auto_skip_circ_events[e.textcode];}).length == 0) {
516             return service.handle_overridable_checkout_event(evt, params, options);
517         }
518
519         // Other events
520         switch (evt[0].textcode) {
521             case 'PATRON_CARD_INACTIVE':
522             case 'PATRON_INACTIVE':
523             case 'PATRON_ACCOUNT_EXPIRED':
524             case 'CIRC_CLAIMS_RETURNED':
525             case 'ITEM_NOT_CATALOGED':
526             case 'ASSET_COPY_NOT_FOUND':
527                 // since handle_overridable_checkout_event essentially advertises these events at some point,
528                 // we no longer need the original alerts; however, the sound effects are still nice.
529                 egCore.audio.play('warning.checkout');
530                 return $q.reject();
531
532             default:
533                 egCore.audio.play('error.checkout.unknown');
534                 return service.exit_alert(
535                     egCore.strings.CHECKOUT_FAILED_GENERIC, {
536                         barcode : params.copy_barcode,
537                         textcode : evt[0].textcode,
538                         desc : evt[0].desc
539                     }
540                 );
541         }
542     }
543
544     // returns a promise resolved with the list of circ mods
545     service.get_circ_mods = function() {
546         if (egCore.env.ccm) 
547             return $q.when(egCore.env.ccm.list);
548
549         return egCore.pcrud.retrieveAll('ccm', null, {atomic : true})
550         .then(function(list) { 
551             egCore.env.absorbList(list, 'ccm');
552             return list;
553         });
554     };
555
556     // returns a promise resolved with the list of noncat types
557     service.get_noncat_types = function() {
558         if (egCore.env.cnct) 
559             return $q.when(egCore.env.cnct.list);
560
561         return egCore.pcrud.search('cnct', 
562             {owning_lib : 
563                 egCore.org.fullPath(egCore.auth.user().ws_ou(), true)}, 
564             null, {atomic : true}
565         ).then(function(list) { 
566             egCore.env.absorbList(list, 'cnct');
567             return list;
568         });
569     }
570
571     service.get_all_penalty_types = function() {
572         if (egCore.env.csp) 
573             return $q.when(egCore.env.csp.list);
574         return egCore.pcrud.retrieveAll('csp', {}, {atomic : true}).then(
575             function(penalties) {
576                 return egCore.env.absorbList(penalties, 'csp').list;
577             }
578         );
579     }
580
581     // ideally all of these data should be returned with the response,
582     // but until then, grab what we need.
583     service.flesh_response_data = function(action, evt, params, options) {
584         var promises = [];
585         var payload;
586         if (!evt[0] || !(payload = evt[0].payload)) return $q.when();
587         
588         promises.push(service.flesh_copy_location(payload.copy));
589         if (payload.copy) {
590             promises.push(service.flesh_acn_owning_lib(payload.volume));
591             promises.push(service.flesh_copy_circ_library(payload.copy));
592             promises.push(service.flesh_copy_circ_modifier(payload.copy));
593             promises.push(
594                 service.flesh_copy_status(payload.copy)
595
596                 .then(function() {
597                     // copy is in transit, but no transit was delivered
598                     // in the payload.  Do this here instead of below to
599                     // ensure consistent copy status fleshiness
600                     if (!payload.transit && payload.copy.status().id() == 6) { // in-transit
601                         return service.find_copy_transit(evt, params, options)
602                         .then(function(trans) {
603                             if (trans) {
604                                 trans.source(egCore.org.get(trans.source()));
605                                 trans.dest(egCore.org.get(trans.dest()));
606                                 payload.transit = trans;
607                             }
608                         })
609                     }
610                 })
611             );
612         }
613
614         // local flesh transit
615         if (transit = payload.transit) {
616             transit.source(egCore.org.get(transit.source()));
617             transit.dest(egCore.org.get(transit.dest()));
618         } 
619
620         // TODO: renewal responses should include the patron
621         if (!payload.patron) {
622             var user_id;
623             if (payload.circ) user_id = payload.circ.usr();
624             if (payload.noncat_circ) user_id = payload.noncat_circ.patron();
625             if (user_id) {
626                 promises.push(
627                     egCore.pcrud.retrieve('au', user_id)
628                     .then(function(user) {payload.patron = user})
629                 );
630             }
631         }
632
633         // extract precat values
634         angular.forEach(evt, function(e){ e.title = payload.record ? payload.record.title() : 
635             (payload.copy ? payload.copy.dummy_title() : null);});
636
637         angular.forEach(evt, function(e){ e.author = payload.record ? payload.record.author() : 
638             (payload.copy ? payload.copy.dummy_author() : null);});
639
640         angular.forEach(evt, function(e){ e.isbn = payload.record ? payload.record.isbn() : 
641             (payload.copy ? payload.copy.dummy_isbn() : null);});
642
643         return $q.all(promises);
644     }
645
646     service.flesh_acn_owning_lib = function(acn) {
647         if (!acn) return $q.when();
648         return $q.when(acn.owning_lib(egCore.org.get( acn.owning_lib() )));
649     }
650
651     service.flesh_copy_circ_library = function(copy) {
652         if (!copy) return $q.when();
653         
654         return $q.when(copy.circ_lib(egCore.org.get( copy.circ_lib() )));
655     }
656
657     // fetches the full list of circ modifiers
658     service.flesh_copy_circ_modifier = function(copy) {
659         if (!copy) return $q.when();
660         if (egCore.env.ccm)
661             return $q.when(copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]));
662         return egCore.pcrud.retrieveAll('ccm', {}, {atomic : true}).then(
663             function(list) {
664                 egCore.env.absorbList(list, 'ccm');
665                 copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]);
666             }
667         );
668     }
669
670     // fetches the full list of copy statuses
671     service.flesh_copy_status = function(copy) {
672         if (!copy) return $q.when();
673         if (egCore.env.ccs) 
674             return $q.when(copy.status(egCore.env.ccs.map[copy.status()]));
675         return egCore.pcrud.retrieveAll('ccs', {}, {atomic : true}).then(
676             function(list) {
677                 egCore.env.absorbList(list, 'ccs');
678                 copy.status(egCore.env.ccs.map[copy.status()]);
679             }
680         );
681     }
682
683     // there may be *many* copy locations and we may be handling items
684     // for other locations.  Fetch copy locations as-needed and cache.
685     service.flesh_copy_location = function(copy) {
686         if (!copy) return $q.when();
687         if (angular.isObject(copy.location())) return $q.when(copy);
688         if (egCore.env.acpl) {
689             if (egCore.env.acpl.map[copy.location()]) {
690                 copy.location(egCore.env.acpl.map[copy.location()]);
691                 return $q.when(copy);
692             }
693         } 
694         return egCore.pcrud.retrieve('acpl', copy.location())
695         .then(function(loc) {
696             egCore.env.absorbList([loc], 'acpl'); // append to cache
697             copy.location(loc);
698             return copy;
699         });
700     }
701
702
703     // fetch org unit addresses as needed.
704     service.get_org_addr = function(org_id, addr_type) {
705         var org = egCore.org.get(org_id);
706         var addr_id = org[addr_type]();
707
708         if (!addr_id) return $q.when(null);
709
710         if (egCore.env.aoa && egCore.env.aoa.map[addr_id]) 
711             return $q.when(egCore.env.aoa.map[addr_id]); 
712
713         return egCore.pcrud.retrieve('aoa', addr_id).then(function(addr) {
714             egCore.env.absorbList([addr], 'aoa');
715             return egCore.env.aoa.map[addr_id]; 
716         });
717     }
718
719     service.exit_alert = function(msg, scope) {
720         return egAlertDialog.open(msg, scope).result.then(
721             function() {return $q.reject()});
722     }
723
724     // opens a dialog asking the user if they would like to override
725     // the returned event.
726     service.override_dialog = function(evt, params, options, action) {
727         if (!angular.isArray(evt)) evt = [evt];
728
729         egCore.audio.play('warning.circ.event_override');
730         var copy_alert = evt.filter(function(e) {
731             return e.textcode == 'COPY_ALERT_MESSAGE';
732         });
733         evt = evt.filter(function(e) {
734             return e.textcode !== 'COPY_ALERT_MESSAGE';
735         });
736
737         return $uibModal.open({
738             templateUrl: './circ/share/t_event_override_dialog',
739             backdrop: 'static',
740             controller: 
741                 ['$scope', '$uibModalInstance', 
742                 function($scope, $uibModalInstance) {
743                 $scope.events = evt;
744                 $scope.action = action;
745
746                 // Find the event, if any, that is for ITEM_ON_HOLDS_SHELF
747                 //  and grab the patron name of the owner. 
748                 $scope.holdEvent = evt.filter(function(e) {
749                     return e.textcode === 'ITEM_ON_HOLDS_SHELF'
750                 });
751
752                 if ($scope.holdEvent.length > 0) {
753                     // Ensure we have a scalar here
754                     if (angular.isArray($scope.holdEvent)) {
755                         $scope.holdEvent = $scope.holdEvent[0];
756                     }
757
758                     $scope.patronName = $scope.holdEvent.payload.patron_name;
759                     $scope.holdID = $scope.holdEvent.payload.hold_id;
760                     $scope.patronID = $scope.holdEvent.payload.patron_id;
761                 }
762
763                 $scope.copy_barcode = params.copy_barcode; // may be null
764
765                 // Implementation note: Why not use a primitive here? It
766                 // doesn't work.  See: 
767                 // http://stackoverflow.com/questions/18642371/checkbox-not-binding-to-scope-in-angularjs
768                 $scope.formdata = {
769                     clearHold : service.clearHold,
770                     nonoverridable: evt.filter(function(e){
771                         return service.nonoverridable_events.indexOf(e.textcode) > -1;}).length > 0,
772                     event_ui_data : Object.fromEntries(
773                         evt.map( e => [ e.ilsevent, {
774                             // non-overridable events will be rare, but they are skippable.  We use
775                             // the same checkbox variable to track desired skip and auto-override
776                             // selections.
777                             overridable: service.nonoverridable_events.indexOf(e.textcode) == -1,
778                             // for non-overridable events, we'll default the checkbox to any previous
779                             // choice made for the current patron, though normally the UI will be
780                             // suppressed unless some previously unencountered events are in the set
781                             checkbox: service.nonoverridable_events.indexOf(e.textcode) > -1
782                             ? (service.auto_skip_circ_events[e.textcode] == undefined
783                                 ? false
784                                 : service.auto_skip_circ_events[e.textcode]
785                             )
786                             // if a given event is overridable, said checkbox will default to any previous
787                             // choice made for the current patron, as long as there are no non-overridable
788                             // events in the set (because we'll disable the checkbox in that case and don't
789                             // want to imply that we're going to set an auto-override)
790                             : (service.auto_override_circ_events[e.textcode] == undefined
791                                 ? (
792                                     service.nonoverridable_events.indexOf(e.textcode) > -1
793                                     ? false
794                                     : service.default_auto_override.indexOf(e.textcode) > -1
795                                 )
796                                 : service.auto_override_circ_events[e.textcode]
797                             )
798                         }])
799                     ) 
800                 };
801
802                 function update_auto_override_and_skip_lists() {
803                     angular.forEach(evt, function(e){
804                         if ($scope.formdata.nonoverridable) {
805                             // the action had at least one non-overridable event, so let's only
806                             // record skip choices for those
807                             if (!$scope.formdata.event_ui_data[e.ilsevent].overridable) {
808                                 if ($scope.formdata.event_ui_data[e.ilsevent].checkbox) {
809                                     // grow the skip list
810                                     service.auto_skip_circ_events[e.textcode] = true;
811                                 } else {
812                                     // shrink the skip list
813                                     service.auto_skip_circ_events[e.textcode] = false;
814                                 }
815                             }
816                         } else {
817                             // record all auto-override choices
818                             if ($scope.formdata.event_ui_data[e.ilsevent].checkbox) {
819                                 // grow the auto-override list
820                                 service.auto_override_circ_events[e.textcode] = true;
821                             } else {
822                                 // shrink the auto-override list
823                                 service.auto_override_circ_events[e.textcode] = false;
824                             }
825                         }
826                     });
827                     // for debugging
828                     window.oils_auto_skip_circ_events = service.auto_skip_circ_events;
829                     window.oils_auto_override_circ_events = service.auto_override_circ_events;
830                 }
831
832                 $scope.ok = function() { 
833                     update_auto_override_and_skip_lists();
834                     // Handle the cancellation of the assciated hold here
835                     if ($scope.formdata.clearHold && $scope.holdID) {
836                         egCore.net.request(
837                             'open-ils.circ',
838                             'open-ils.circ.hold.cancel',
839                             egCore.auth.token(), $scope.holdID,
840                             5, // staff forced
841                             'Item checked out by other patron' // FIXME I18n
842                         );
843                     }
844                     $uibModalInstance.close();
845                 }
846
847                 $scope.skip = function($event) {
848                     update_auto_override_and_skip_lists();
849                     $uibModalInstance.dismiss();
850                     $event.preventDefault();
851                 }
852
853                 $scope.cancel = function ($event) { 
854                     window.oils_cancel_batch = true;
855                     $uibModalInstance.dismiss();
856                     $event.preventDefault();
857                 }
858             }]
859         }).result.then(
860             function() {
861                 options.override = true;
862
863                 if (copy_alert.length > 0) {
864                     return service.copy_alert_dialog(copy_alert, params, options, action);
865                 }
866
867                 if (action == 'checkin') {
868                     return service.checkin(params, options);
869                 }
870
871                 return service[action](params, options);
872             }
873         );
874     }
875
876     service.copy_not_avail_dialog = function(evt, params, options) {
877         if (!angular.isArray(evt)) evt = [evt];
878
879         var copy_alert = evt.filter(function(e) {
880             return e.textcode == 'COPY_ALERT_MESSAGE';
881         });
882         evt = evt.filter(function(e) {
883             return e.textcode !== 'COPY_ALERT_MESSAGE';
884         });
885         evt = evt[0];
886
887         return $uibModal.open({
888             templateUrl: './circ/share/t_copy_not_avail_dialog',
889             backdrop: 'static',
890             controller: 
891                        ['$scope','$uibModalInstance','copyStatus',
892                 function($scope , $uibModalInstance , copyStatus) {
893                 $scope.copyStatus = copyStatus;
894                 $scope.ok = function() {$uibModalInstance.close()}
895                 $scope.cancel = function() {$uibModalInstance.dismiss()}
896             }],
897             resolve : {
898                 copyStatus : function() {
899                     return egCore.pcrud.retrieve(
900                         'ccs', evt.payload.status());
901                 }
902             }
903         }).result.then(
904             function() {
905                 options.override = true;
906
907                 if (copy_alert.length > 0) {
908                     return service.copy_alert_dialog(copy_alert, params, options, 'checkout');
909                 }
910
911                 return service.checkout(params, options);
912             }
913         );
914     }
915
916     // Opens a dialog allowing the user to fill in the desired non-cat count.
917     // Unlike other dialogs, which kickoff circ actions internally
918     // as a result of events, this dialog does not kick off any circ
919     // actions. It just collects the count and and resolves the promise.
920     //
921     // This assumes the caller has already handled the noncat-type
922     // selection and just needs to collect the count info.
923     service.noncat_dialog = function(params, options) {
924         var noncatMax = 99; // hard-coded max
925         
926         // the caller should presumably have fetched the noncat_types via
927         // our API already, but fetch them again (from cache) to be safe.
928         return service.get_noncat_types().then(function() {
929
930             params.noncat = true;
931             var type = egCore.env.cnct.map[params.noncat_type];
932
933             return $uibModal.open({
934                 templateUrl: './circ/share/t_noncat_dialog',
935                 backdrop: 'static',
936                 controller: 
937                     ['$scope', '$uibModalInstance',
938                     function($scope, $uibModalInstance) {
939                     $scope.focusMe = true;
940                     $scope.type = type;
941                     $scope.count = 1;
942                     $scope.noncatMax = noncatMax;
943                     $scope.ok = function(count) { $uibModalInstance.close(count) }
944                     $scope.cancel = function ($event) { 
945                         $uibModalInstance.dismiss() 
946                         $event.preventDefault();
947                     }
948                 }],
949             }).result.then(
950                 function(count) {
951                     if (count && count > 0 && count <= noncatMax) { 
952                         // NOTE: in Chrome, form validation ensure a valid number
953                         params.noncat_count = count;
954                         return $q.when(params);
955                     } else {
956                         return $q.reject();
957                     }
958                 }
959             );
960         });
961     }
962
963     // Opens a dialog allowing the user to fill in pre-cat copy info.
964     service.precat_dialog = function(params, options) {
965
966         return $uibModal.open({
967             templateUrl: './circ/share/t_precat_dialog',
968             backdrop: 'static',
969             controller: 
970                 ['$scope', '$uibModalInstance', 'circMods', 'has_precat_perm',
971                 function($scope, $uibModalInstance, circMods, has_precat_perm) {
972                 $scope.focusMe = true;
973                 $scope.precatArgs = {
974                     copy_barcode : params.copy_barcode
975                 };
976
977                 $scope.can_create_precats = has_precat_perm;
978                 $scope.circModifiers = circMods;
979                 $scope.ok = function(args) { $uibModalInstance.close(args) }
980                 $scope.cancel = function () { $uibModalInstance.dismiss() }
981
982                 // use this function as a keydown handler on form
983                 // elements that should not submit the form on enter.
984                 $scope.preventSubmit = function($event) {
985                     if ($event.keyCode == 13)
986                         $event.preventDefault();
987                 }
988             }],
989             resolve : {
990                 circMods : function() { return service.get_circ_mods(); },
991                 has_precat_perm : function(){ return egCore.perm.hasPermHere('CREATE_PRECAT'); }
992             }
993         }).result.then(
994             function(args) {
995                 if (!args || !args.dummy_title) return $q.reject();
996                 if(args.circ_modifier == "") args.circ_modifier = null;
997                 angular.forEach(args, function(val, key) {params[key] = val});
998                 params.precat = true;
999                 return service.checkout(params, options);
1000             }
1001         );
1002     }
1003
1004     // find the open transit for the given copy barcode; flesh the org
1005     // units locally.
1006     service.find_copy_transit = function(evt, params, options) {
1007         if (angular.isArray(evt)) evt = evt[0];
1008
1009         // NOTE: evt.payload.transit may exist, but it's not necessarily
1010         // the transit we want, since a transit close + open in the API
1011         // returns the closed transit.
1012
1013          return egCore.pcrud.search('atc',
1014             {   dest_recv_time : null, cancel_time : null},
1015             {   flesh : 1, 
1016                 flesh_fields : {atc : ['target_copy']},
1017                 join : {
1018                     acp : {
1019                         filter : {
1020                             barcode : params.copy_barcode,
1021                             deleted : 'f'
1022                         }
1023                     }
1024                 },
1025                 limit : 1,
1026                 order_by : {atc : 'source_send_time desc'}, 
1027             }, {authoritative : true}
1028         ).then(function(transit) {
1029             transit.source(egCore.org.get(transit.source()));
1030             transit.dest(egCore.org.get(transit.dest()));
1031             return transit;
1032         });
1033     }
1034
1035     service.copy_in_transit_dialog = function(evt, params, options) {
1036         if (angular.isArray(evt)) evt = evt[0];
1037         return $uibModal.open({
1038             templateUrl: './circ/share/t_copy_in_transit_dialog',
1039             backdrop: 'static',
1040             controller: 
1041                        ['$scope','$uibModalInstance','transit',
1042                 function($scope , $uibModalInstance , transit) {
1043                 $scope.transit = transit;
1044                 $scope.ok = function() { $uibModalInstance.close(transit) }
1045                 $scope.cancel = function() { $uibModalInstance.dismiss() }
1046             }],
1047             resolve : {
1048                 // fetch the conflicting open transit w/ fleshed copy
1049                 transit : function() {
1050                     return service.find_copy_transit(evt, params, options);
1051                 }
1052             }
1053         }).result.then(
1054             function(transit) {
1055                 // user chose to abort the transit then checkout
1056                 return service.abort_transit(transit.id())
1057                 .then(function() {
1058                     return service.checkout(params, options);
1059                 });
1060             }
1061         );
1062     }
1063
1064     service.abort_transit = function(transit_id) {
1065         return egCore.net.request(
1066             'open-ils.circ',
1067             'open-ils.circ.transit.abort',
1068             egCore.auth.token(), {transitid : transit_id}
1069         ).then(function(resp) {
1070             if (evt = egCore.evt.parse(resp)) {
1071                 alert(evt);
1072                 return $q.reject();
1073             }
1074             return $q.when();
1075         });
1076     }
1077
1078     service.last_copy_circ = function(copy_id) {
1079         return egCore.pcrud.search('circ', 
1080             {target_copy : copy_id},
1081             {order_by : {circ : 'xact_start desc' }, limit : 1}
1082         );
1083     }
1084
1085     service.circ_exists_dialog = function(evt, params, options) {
1086         if (angular.isArray(evt)) evt = evt[0];
1087
1088         if (!evt.payload.old_circ) {
1089             return egCore.net.request(
1090                 'open-ils.search',
1091                 'open-ils.search.asset.copy.fleshed2.find_by_barcode',
1092                 params.copy_barcode
1093             ).then(function(resp){
1094                 console.log(resp);
1095                 if (egCore.evt.parse(resp)) {
1096                     console.error(egCore.evt.parse(resp));
1097                 } else {
1098                     return egCore.net.request(
1099                          'open-ils.circ',
1100                          'open-ils.circ.copy_checkout_history.retrieve',
1101                          egCore.auth.token(), resp.id(), 1
1102                     ).then( function (circs) {
1103                         evt.payload.old_circ = circs[0];
1104                         return service.circ_exists_dialog_impl( evt, params, options );
1105                     });
1106                 }
1107             });
1108         } else {
1109             return service.circ_exists_dialog_impl( evt, params, options );
1110         }
1111     },
1112
1113     service.circ_exists_dialog_impl = function (evt, params, options) {
1114
1115         var openCirc = evt.payload.old_circ;
1116         var sameUser = openCirc.usr() == params.patron_id;
1117         
1118         return $uibModal.open({
1119             templateUrl: './circ/share/t_circ_exists_dialog',
1120             backdrop: 'static',
1121             controller: 
1122                        ['$scope','$uibModalInstance',
1123                 function($scope , $uibModalInstance) {
1124                 $scope.args = {forgive_fines : false};
1125                 $scope.circDate = openCirc.xact_start();
1126                 $scope.sameUser = sameUser;
1127                 $scope.ok = function() { $uibModalInstance.close($scope.args) }
1128                 $scope.cancel = function($event) { 
1129                     $uibModalInstance.dismiss();
1130                     $event.preventDefault(); // form, avoid calling ok();
1131                 }
1132             }]
1133         }).result.then(
1134             function(args) {
1135                 if (sameUser) {
1136                     params.void_overdues = args.forgive_fines;
1137                     options.sameCopyCheckout = true;
1138                     return service.renew(params, options);
1139                 }
1140
1141                 return service.checkin({
1142                     barcode : params.copy_barcode,
1143                     noop : true,
1144                     void_overdues : args.forgive_fines
1145                 }).then(function(checkin_resp) {
1146                     if (checkin_resp.evt[0].textcode == 'SUCCESS') {
1147                         return service.checkout(params, options);
1148                     } else {
1149                         alert(egCore.evt.parse(checkin_resp.evt[0]));
1150                         return $q.reject();
1151                     }
1152                 });
1153             }
1154         );
1155     }
1156
1157     service.batch_backdate = function(circ_ids, backdate) {
1158         return egCore.net.request(
1159             'open-ils.circ',
1160             'open-ils.circ.post_checkin_backdate.batch',
1161             egCore.auth.token(), circ_ids, backdate);
1162     }
1163
1164     service.backdate_dialog = function(circ_ids) {
1165         return $uibModal.open({
1166             templateUrl: './circ/share/t_backdate_dialog',
1167             backdrop: 'static',
1168             controller: 
1169                        ['$scope','$uibModalInstance',
1170                 function($scope , $uibModalInstance) {
1171
1172                 var today = new Date();
1173                 $scope.dialog = {
1174                     num_circs : circ_ids.length,
1175                     num_processed : 0,
1176                     backdate : today
1177                 }
1178
1179                 $scope.$watch('dialog.backdate', function(newval) {
1180                     if (newval && newval > today) 
1181                         $scope.dialog.backdate = today;
1182                 });
1183
1184
1185                 $scope.cancel = function() { 
1186                     $uibModalInstance.dismiss();
1187                 }
1188
1189                 $scope.ok = function() { 
1190
1191                     var bd = $scope.dialog.backdate.toISOString().replace(/T.*/,'');
1192                     service.batch_backdate(circ_ids, bd)
1193                     .then(
1194                         function() { // on complete
1195                             $uibModalInstance.close({backdate : bd});
1196                         },
1197                         null,
1198                         function(resp) { // on response
1199                             console.debug('backdate returned ' + resp);
1200                             if (resp == '1') {
1201                                 $scope.num_processed++;
1202                             } else {
1203                                 console.error(egCore.evt.parse(resp));
1204                             }
1205                         }
1206                     );
1207                 }
1208             }]
1209         }).result;
1210     }
1211
1212     service.mark_claims_returned = function(barcode, date, override) {
1213
1214         var method = 'open-ils.circ.circulation.set_claims_returned';
1215         if (override) method += '.override';
1216
1217         console.debug('claims returned ' + method);
1218
1219         return egCore.net.request(
1220             'open-ils.circ', method, egCore.auth.token(),
1221             {barcode : barcode, backdate : date})
1222
1223         .then(function(resp) {
1224
1225             if (resp == 1) { // success
1226                 console.debug('claims returned succeeded for ' + barcode);
1227                 return barcode;
1228
1229             } else if (evt = egCore.evt.parse(resp)) {
1230                 console.debug('claims returned failed: ' + evt.toString());
1231
1232                 if (evt.textcode == 'PATRON_EXCEEDS_CLAIMS_RETURN_COUNT') {
1233                     // TODO check perms before offering override option?
1234
1235                     if (override) return;// just to be safe
1236
1237                     return egConfirmDialog.open(
1238                         egCore.strings.TOO_MANY_CLAIMS_RETURNED, '', {}
1239                     ).result.then(function() {
1240                         return service.mark_claims_returned(barcode, date, true);
1241                     });
1242                 }
1243
1244                 if (evt.textcode == 'PERM_FAILURE') {
1245                     console.error('claims returned permission denied')
1246                     // TODO: auth override dialog?
1247                 }
1248             }
1249         });
1250     }
1251
1252     service.mark_claims_returned_dialog = function(copy_barcodes) {
1253         if (!copy_barcodes.length) return;
1254
1255         return $uibModal.open({
1256             templateUrl: './circ/share/t_mark_claims_returned_dialog',
1257             backdrop: 'static',
1258             controller: 
1259                        ['$scope','$uibModalInstance',
1260                 function($scope , $uibModalInstance) {
1261
1262                 var today = new Date();
1263                 $scope.args = {
1264                     barcodes : copy_barcodes,
1265                     date : today
1266                 };
1267
1268                 $scope.$watch('args.date', function(newval) {
1269                     if (newval && newval > today) 
1270                         $scope.args.backdate = today;
1271                 });
1272
1273                 $scope.cancel = function() {$uibModalInstance.dismiss()}
1274                 $scope.ok = function() { 
1275
1276                     var date = $scope.args.date.toISOString().replace(/T.*/,'');
1277
1278                     var deferred = $q.defer();
1279
1280                     // serialize the action on each barcode so that the 
1281                     // caller will never see multiple alerts at the same time.
1282                     function mark_one() {
1283                         var bc = copy_barcodes.pop();
1284                         if (!bc) {
1285                             deferred.resolve();
1286                             $uibModalInstance.close();
1287                             return;
1288                         }
1289
1290                         // finally -> continue even when one fails
1291                         service.mark_claims_returned(bc, date)
1292                         .finally(function(barcode) {
1293                             if (barcode) deferred.notify(barcode);
1294                             mark_one();
1295                         });
1296                     }
1297                     mark_one(); // kick it off
1298                     return deferred.promise;
1299                 }
1300             }]
1301         }).result;
1302     }
1303
1304     // serially checks in each barcode with claims_never_checked_out set
1305     // returns promise, notified on each barcode, resolved after all
1306     // checkins are complete.
1307     service.mark_claims_never_checked_out = function(barcodes) {
1308         if (!barcodes.length) return;
1309
1310         var deferred = $q.defer();
1311         egConfirmDialog.open(
1312             egCore.strings.MARK_NEVER_CHECKED_OUT, '', {barcodes : barcodes}
1313
1314         ).result.then(function() {
1315             function mark_one() {
1316                 var bc = barcodes.pop();
1317
1318                 if (!bc) { // all done
1319                     deferred.resolve();
1320                     return;
1321                 }
1322
1323                 service.checkin(
1324                     {claims_never_checked_out : true, copy_barcode : bc})
1325                 .finally(function() { 
1326                     deferred.notify(bc);
1327                     mark_one();
1328                 })
1329             }
1330             mark_one();
1331         });
1332
1333         return deferred.promise;
1334     }
1335
1336     service.mark_damaged = function(params) {
1337         if (!params) return $q.when();
1338         return $uibModal.open({
1339             backdrop: 'static',
1340             templateUrl: './circ/share/t_mark_damaged',
1341             controller:
1342                 ['$scope', '$uibModalInstance', 'egCore', 'egBilling', 'egItem',
1343                 function($scope, $uibModalInstance, egCore, egBilling, egItem) {
1344                     var doRefresh = params.refresh;
1345                     
1346                     $scope.showBill = params.charge != null && params.circ;
1347                     $scope.billArgs = {charge: params.charge};
1348                     $scope.mode = 'charge';
1349                     $scope.barcode = params.barcode;
1350                     if (params.circ) {
1351                         $scope.circ = params.circ;
1352                         $scope.circ_checkin_time = params.circ.checkin_time();
1353                         $scope.circ_patron_name = params.circ.usr().family_name() + ", "
1354                             + params.circ.usr().first_given_name() + " "
1355                             + params.circ.usr().second_given_name();
1356                     }
1357                     egBilling.fetchBillingTypes().then(function(res) {
1358                         $scope.billingTypes = res;
1359                     });
1360
1361                     $scope.btnChargeFees = function() {
1362                         $scope.mode = 'charge';
1363                         $scope.billArgs.charge = params.charge;
1364                     }
1365                     $scope.btnWaiveFees = function() {
1366                         $scope.mode = 'waive';
1367                         $scope.billArgs.charge = 0;
1368                     }
1369
1370                     $scope.cancel = function ($event) { 
1371                         $uibModalInstance.dismiss();
1372                     }
1373                     $scope.ok = function() {
1374                         handle_mark_item_damaged();
1375                     }
1376
1377                     var handle_mark_item_damaged = function() {
1378                         var applyFines;
1379                         if ($scope.showBill)
1380                             applyFines = $scope.billArgs.charge ? 'apply' : 'noapply';
1381
1382                         egCore.net.request(
1383                             'open-ils.circ',
1384                             'open-ils.circ.mark_item_damaged',
1385                             egCore.auth.token(), params.id, {
1386                                 apply_fines: applyFines,
1387                                 override_amount: $scope.billArgs.charge,
1388                                 override_btype: $scope.billArgs.type,
1389                                 override_note: $scope.billArgs.note,
1390                                 handle_checkin: !applyFines
1391                         }).then(function(resp) {
1392                             if (evt = egCore.evt.parse(resp)) {
1393                                 doRefresh = false;
1394                                 console.debug("mark damaged more information required. Pushing back.");
1395                                 service.mark_damaged({
1396                                     id: params.id,
1397                                     barcode: params.barcode,
1398                                     charge: evt.payload.charge,
1399                                     circ: evt.payload.circ,
1400                                     refresh: params.refresh
1401                                 });
1402                                 console.error('mark damaged failed: ' + evt);
1403                             }
1404                         }).then(function() {
1405                             if (doRefresh) egItem.add_barcode_to_list(params.barcode);
1406                         });
1407                         $uibModalInstance.close();
1408                     }
1409                 }]
1410         }).result;
1411     }
1412
1413     service.handle_mark_item_event = function(copy, status, args, event) {
1414         var dlogTitle, dlogMessage;
1415         switch (event.textcode) {
1416         case 'ITEM_TO_MARK_CHECKED_OUT':
1417             dlogTitle = egCore.strings.MARK_ITEM_CHECKED_OUT;
1418             dlogMessage = egCore.strings.MARK_ITEM_CHECKIN_CONTINUE;
1419             args.handle_checkin = 1;
1420             break;
1421         case 'ITEM_TO_MARK_IN_TRANSIT':
1422             dlogTitle = egCore.strings.MARK_ITEM_IN_TRANSIT;
1423             dlogMessage = egCore.strings.MARK_ITEM_ABORT_CONTINUE;
1424             args.handle_transit = 1;
1425             break;
1426         case 'ITEM_TO_MARK_LAST_HOLD_COPY':
1427             dlogTitle = egCore.strings.MARK_ITEM_LAST_HOLD_COPY;
1428             dlogMessage = egCore.strings.MARK_ITEM_CONTINUE;
1429             args.handle_last_hold_copy = 1;
1430             break;
1431         case 'COPY_DELETE_WARNING':
1432             dlogTitle = egCore.strings.MARK_ITEM_RESTRICT_DELETE;
1433             dlogMessage = egCore.strings.MARK_ITEM_CONTINUE;
1434             args.handle_copy_delete_warning = 1;
1435             break;
1436         case 'PERM_FAILURE':
1437             console.error('Mark item ' + status.name() + ' for ' + copy.barcode + ' failed: ' +
1438                           event);
1439             return service.exit_alert(egCore.strings.PERMISSION_DENIED,
1440                                       {permission : event.ilsperm});
1441             break;
1442         default:
1443             console.error('Mark item ' + status.name() + ' for ' + copy.barcode + ' failed: ' +
1444                           event);
1445             return service.exit_alert(egCore.strings.MARK_ITEM_FAILURE,
1446                                       {status : status.name(), barcode : copy.barcode,
1447                                        textcode : event.textcode});
1448             break;
1449         }
1450         return egConfirmDialog.open(
1451             dlogTitle, dlogMessage,
1452             {
1453                 barcode : copy.barcode,
1454                 status : status.name(),
1455                 ok : function () {},
1456                 cancel : function () {}
1457             }
1458         ).result.then(function() {
1459             return service.mark_item(copy, status, args);
1460         });
1461     }
1462
1463     service.mark_item = function(copy, markstatus, args) {
1464         if (!copy) return $q.when();
1465
1466         // If any new back end mark_item calls are added, also add
1467         // them here to use them from the staff client.
1468         // TODO: I didn't find any JS constants for copy status.
1469         var req;
1470         switch (markstatus.id()) {
1471         case 2:
1472             // Not implemented in the staff client, yet.
1473             // req = "open-ils.circ.mark_item_bindery";
1474             break;
1475         case 4:
1476             req = "open-ils.circ.mark_item_missing";
1477             break;
1478         case 9:
1479             // Not implemented in the staff client, yet.
1480             // req = "open-ils.circ.mark_item_on_order";
1481             break;
1482         case 10:
1483             // Not implemented in the staff client, yet.
1484             // req = "open-ils.circ.mark_item_ill";
1485             break;
1486         case 11:
1487             // Not implemented in the staff client, yet.
1488             // req = "open-ils.circ.mark_item_cataloging";
1489             break;
1490         case 12:
1491             // Not implemented in the staff client, yet.
1492             // req = "open-ils.circ.mark_item_reserves";
1493             break;
1494         case 13:
1495             req = "open-ils.circ.mark_item_discard";
1496             break;
1497         case 14:
1498             // Damaged is for handling of events. It's main handler is elsewhere.
1499             req = "open-ils.circ.mark_item_damaged";
1500             break;
1501         }
1502
1503         return egCore.net.request(
1504             'open-ils.circ',
1505             req,
1506             egCore.auth.token(),
1507             copy.id,
1508             args
1509         ).then(function(resp) {
1510             if (evt = egCore.evt.parse(resp)) {
1511                 return service.handle_mark_item_event(copy, markstatus, args, evt);
1512             }
1513         });
1514     }
1515
1516     service.mark_discard = function(copies) {
1517         return egConfirmDialog.open(
1518             egCore.strings.MARK_DISCARD_CONFIRM, '',
1519             {
1520                 num_items : copies.length,
1521                 ok : function() {},
1522                 cancel : function() {}
1523             }
1524         ).result.then(function() {
1525             return egCore.pcrud.retrieve('ccs', 13)
1526                 .then(function(resp) {
1527                     var promises = [];
1528                     angular.forEach(copies, function(copy) {
1529                         promises.push(service.mark_item(copy, resp, {}))
1530                     });
1531                     return $q.all(promises);
1532                 });
1533         });
1534     }
1535
1536     service.mark_missing = function(copies) {
1537         return egConfirmDialog.open(
1538             egCore.strings.MARK_MISSING_CONFIRM, '',
1539             {
1540                 num_items : copies.length,
1541                 ok : function() {},
1542                 cancel : function() {}
1543             }
1544         ).result.then(function() {
1545             return egCore.pcrud.retrieve('ccs', 4)
1546                 .then(function(resp) {
1547                     var promise = $q.when();
1548                     angular.forEach(copies, function(copy) {
1549                         promise = promise.then(function() {
1550                             return service.mark_item(copy, resp, {});
1551                         });
1552                     });
1553                     return promise;
1554                 });
1555         });
1556     }
1557
1558
1559
1560     // Mark circulations as lost via copy barcode.  As each item is 
1561     // processed, the returned promise is notified of the barcode.
1562     // No confirmation dialog is presented.
1563     service.mark_lost = function(copy_barcodes) {
1564         var deferred = $q.defer();
1565         var promises = [];
1566
1567         angular.forEach(copy_barcodes, function(barcode) {
1568             promises.push(
1569                 egCore.net.request(
1570                     'open-ils.circ',
1571                     'open-ils.circ.circulation.set_lost',
1572                     egCore.auth.token(), {barcode : barcode}
1573                 ).then(function(resp) {
1574                     if (evt = egCore.evt.parse(resp)) {
1575                         console.error("Mark lost failed: " + evt.toString());
1576                         return;
1577                     }
1578                     // inform the caller as each item is processed
1579                     deferred.notify(barcode);
1580                 })
1581             );
1582         });
1583
1584         $q.all(promises).then(function() {deferred.resolve()});
1585         return deferred.promise;
1586     }
1587
1588     service.abort_transits = function(transit_ids) {
1589         return egConfirmDialog.open(
1590             egCore.strings.ABORT_TRANSIT_CONFIRM, '',
1591             {   num_transits : transit_ids.length,
1592                 ok : function() {},
1593                 cancel : function() {}
1594             }
1595
1596         ).result.then(function() {
1597             var promises = [];
1598             angular.forEach(transit_ids, function(transit_id) {
1599                 promises.push(
1600                     egCore.net.request(
1601                         'open-ils.circ',
1602                         'open-ils.circ.transit.abort',
1603                         egCore.auth.token(), {transitid : transit_id}
1604                     ).then(function(resp) {
1605                         if (evt = egCore.evt.parse(resp)) {
1606                             console.error('abort transit failed: ' + evt);
1607                         }
1608                     })
1609                 );
1610             });
1611
1612             return $q.all(promises);
1613         });
1614     }
1615
1616     service.add_copy_alerts = function(item_ids) {
1617         return egAddCopyAlertDialog.open({
1618             copy_ids : item_ids,
1619             ok : function() { },
1620             cancel : function() {}
1621         }).result.then(function() { });
1622     }
1623
1624     service.manage_copy_alerts = function(item_ids) {
1625         return egCopyAlertEditorDialog.open({
1626             copy_id : item_ids[0],
1627             ok : function() { },
1628             cancel : function() {}
1629         }).result.then(function() { });
1630     }
1631
1632     // alert when copy location alert_message is set.
1633     // This does not affect processing, it only produces a click-through
1634     service.handle_checkin_loc_alert = function(evt, params, options) {
1635         if (angular.isArray(evt)) evt = evt[0];
1636
1637         var copy = evt && evt.payload ? evt.payload.copy : null;
1638
1639         if (copy && !options.suppress_popups
1640             && copy.location().checkin_alert() == 't') {
1641
1642             return egAlertDialog.open(
1643                 egCore.strings.LOCATION_ALERT_MSG, {copy : copy}).result;
1644         }
1645
1646         return $q.when();
1647     }
1648
1649     service.handle_checkin_resp = function(evt, params, options) {
1650         if (!angular.isArray(evt)) evt = [evt];
1651
1652         var final_resp = {evt : evt, params : params, options : options};
1653
1654         var copy, hold, transit;
1655         if (evt[0].payload) {
1656             copy = evt[0].payload.copy;
1657             hold = evt[0].payload.hold;
1658             transit = evt[0].payload.transit;
1659         }
1660
1661         // track the barcode regardless of whether it's valid
1662         angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; });
1663
1664         angular.forEach(evt, function(e){ console.debug('checkin event ' + e.textcode); });
1665
1666         if (evt.filter(function(e){return service.checkin_overridable_events.indexOf(e.textcode) > -1;}).length > 0)
1667             return service.handle_overridable_checkin_event(evt, params, options);
1668
1669         switch (evt[0].textcode) {
1670
1671             case 'SUCCESS':
1672             case 'NO_CHANGE':
1673
1674                 switch(Number(copy.status().id())) {
1675
1676                     case 0: /* AVAILABLE */                                        
1677                     case 4: /* MISSING */                                          
1678                     case 7: /* RESHELVING */ 
1679
1680                         egCore.audio.play('success.checkin');
1681
1682                         // see if the copy location requires an alert
1683                         return service.handle_checkin_loc_alert(evt, params, options)
1684                         .then(function() {return final_resp});
1685
1686                     case 8: /* ON HOLDS SHELF */
1687                         egCore.audio.play('info.checkin.holds_shelf');
1688                         
1689                         if (hold) {
1690
1691                             if (hold.pickup_lib() == egCore.auth.user().ws_ou()) {
1692                                 // inform user if the item is on the local holds shelf
1693                             
1694                                 evt[0].route_to = egCore.strings.ROUTE_TO_HOLDS_SHELF;
1695                                 return service.route_dialog(
1696                                     './circ/share/t_hold_shelf_dialog', 
1697                                     evt[0], params, options
1698                                 ).then(function() { return final_resp });
1699
1700                             } else {
1701                                 // normally, if the hold was on the shelf at a 
1702                                 // different location, it would be put into 
1703                                 // transit, resulting in a ROUTE_ITEM event.
1704                                 egCore.audio.play('warning.checkin.wrong_shelf');
1705                                 return $q.when(final_resp);
1706                             }
1707                         } else {
1708
1709                             console.error('checkin: item on holds shelf, '
1710                                 + 'but hold info not returned from checkin');
1711                             return $q.when(final_resp);
1712                         }
1713
1714                     case 11: /* CATALOGING */
1715                         egCore.audio.play('info.checkin.cataloging');
1716                         evt[0].route_to = egCore.strings.ROUTE_TO_CATALOGING;
1717                         if (options.no_precat_alert || options.suppress_popups)
1718                             return $q.when(final_resp);
1719                         return egAlertDialog.open(
1720                             egCore.strings.PRECAT_CHECKIN_MSG, params)
1721                             .result.then(function() {return final_resp});
1722
1723
1724                     case 15: /* ON_RESERVATION_SHELF */
1725                         egCore.audio.play('info.checkin.reservation');
1726                         // TODO: show booking reservation dialog
1727                         return $q.when(final_resp);
1728
1729                     default:
1730                         egCore.audio.play('success.checkin');
1731                         console.debug('Unusual checkin copy status (may have been set via copy alert): '
1732                             + copy.status().id() + ' : ' + copy.status().name());
1733                         return $q.when(final_resp);
1734                 }
1735                 
1736             case 'ROUTE_ITEM':
1737                 return service.route_dialog(
1738                     './circ/share/t_transit_dialog', 
1739                     evt[0], params, options
1740                 ).then(function(data) {
1741                     if (transit && data.transit && transit.dest().id() != data.transit.dest().id())
1742                         final_resp.evt[0].route_to = data.transit.dest().shortname();
1743                     return final_resp;
1744                 });
1745
1746             case 'ASSET_COPY_NOT_FOUND':
1747                 egCore.audio.play('error.checkin.not_found');
1748                 if (options.suppress_popups) return $q.when(final_resp);
1749                 return egAlertDialog.open(
1750                     egCore.strings.UNCAT_ALERT_DIALOG, params)
1751                     .result.then(function() {return final_resp});
1752
1753             case 'ITEM_NOT_CATALOGED':
1754                 egCore.audio.play('error.checkin.not_cataloged');
1755                 evt[0].route_to = egCore.strings.ROUTE_TO_CATALOGING;
1756                 if (options.no_precat_alert || options.suppress_popups)
1757                     return $q.when(final_resp);
1758                 return egAlertDialog.open(
1759                     egCore.strings.PRECAT_CHECKIN_MSG, params)
1760                     .result.then(function() {return final_resp});
1761
1762             default:
1763                 egCore.audio.play('error.checkin.unknown');
1764                 console.warn('unhandled checkin response : ' + evt[0].textcode);
1765                 return $q.when(final_resp);
1766         }
1767     }
1768
1769     // collect transit, address, and hold info that's not already
1770     // included in responses.
1771     service.collect_route_data = function(tmpl, evt, params, options) {
1772         if (angular.isArray(evt)) evt = evt[0];
1773         var promises = [];
1774         var data = {};
1775
1776         if (evt.org && !tmpl.match(/hold_shelf/)) {
1777             promises.push(
1778                 service.get_org_addr(evt.org, 'holds_address')
1779                 .then(function(addr) { data.address = addr })
1780             );
1781         }
1782
1783         if (evt.payload.hold) {
1784             promises.push(
1785                 egCore.pcrud.retrieve('au', 
1786                     evt.payload.hold.usr(), {
1787                         flesh : 1,
1788                         flesh_fields : {'au' : ['card']}
1789                     }
1790                 ).then(function(patron) {data.patron = patron})
1791             );
1792         }
1793
1794
1795         if (!tmpl.match(/hold_shelf/)) {
1796             var courier_deferred = $q.defer();
1797             promises.push(courier_deferred.promise);
1798             promises.push(
1799                 service.find_copy_transit(evt, params, options)
1800                 .then(function(trans) {
1801                     data.transit = trans;
1802                     egCore.org.settings('lib.courier_code', trans.dest().id())
1803                     .then(function(s) {
1804                         data.dest_courier_code = s['lib.courier_code'];
1805                         courier_deferred.resolve();
1806                     });
1807                 })
1808             );
1809         }
1810
1811         return $q.all(promises).then(function() { return data });
1812     }
1813
1814     service.route_dialog = function(tmpl, evt, params, options) {
1815         if (angular.isArray(evt)) evt = evt[0];
1816
1817         return service.collect_route_data(tmpl, evt, params, options)
1818         .then(function(data) {
1819
1820             var template = data.transit ?
1821                 (data.patron ? 'hold_transit_slip' : 'transit_slip') :
1822                 'hold_shelf_slip';
1823             if (service.never_auto_print[template]) {
1824                 // do not show the dialog or print if the
1825                 // disabled automatic print attempt type list includes
1826                 // the specified template
1827                 return data;
1828             }
1829
1830             // All actions flow from the print data
1831
1832             var print_context = {
1833                 copy : egCore.idl.toHash(evt.payload.copy),
1834                 title : evt.title,
1835                 author : evt.author,
1836                 call_number : egCore.idl.toHash(evt.payload.volume)
1837             };
1838
1839             var acn = print_context.call_number; // fix up pre/suffixes
1840             if (acn.prefix == -1) acn.prefix = "";
1841             if (acn.suffix == -1) acn.suffix = "";
1842
1843             if (data.transit) {
1844                 // route_dialog includes the "route to holds shelf" 
1845                 // dialog, which has no transit
1846                 print_context.transit = egCore.idl.toHash(data.transit);
1847                 print_context.dest_courier_code = data.dest_courier_code;
1848                 if (data.address) {
1849                     print_context.dest_address = egCore.idl.toHash(data.address);
1850                 }
1851                 print_context.dest_location =
1852                     egCore.idl.toHash(egCore.org.get(data.transit.dest()));
1853                 print_context.copy.status = egCore.idl.toHash(print_context.copy.status);
1854             }
1855
1856             if (data.patron) {
1857                 print_context.hold = egCore.idl.toHash(evt.payload.hold);
1858                 var notes = print_context.hold.notes;
1859                 if(notes.length > 0){
1860                     print_context.hold_notes = [];
1861                     angular.forEach(notes, function(n){
1862                         print_context.hold_notes.push(n);
1863                     });
1864                 }
1865                 print_context.patron = egCore.idl.toHash(data.patron);
1866             }
1867
1868             var sound = 'info.checkin.transit';
1869             if (evt.payload.hold) sound += '.hold';
1870             egCore.audio.play(sound);
1871
1872             function print_transit(template) {
1873                 return egCore.print.print({
1874                     context : 'default', 
1875                     template : template, 
1876                     scope : print_context
1877                 }).then(function() { return data });
1878             }
1879
1880             // when auto-print is on, skip the dialog and go straight
1881             // to printing.
1882             if (options.auto_print_holds_transits || options.suppress_popups) 
1883                 return print_transit(template);
1884
1885             return $uibModal.open({
1886                 templateUrl: tmpl,
1887                 backdrop: 'static',
1888                 controller: [
1889                             '$scope','$uibModalInstance',
1890                     function($scope , $uibModalInstance) {
1891
1892                     $scope.today = new Date();
1893
1894                     // copy the print scope into the dialog scope
1895                     angular.forEach(print_context, function(val, key) {
1896                         $scope[key] = val;
1897                     });
1898
1899                     $scope.ok = function() {$uibModalInstance.close()}
1900
1901                     $scope.print = function() { 
1902                         $uibModalInstance.close();
1903                         print_transit(template);
1904                     }
1905                 }]
1906
1907             }).result.then(function() { return data });
1908         });
1909     }
1910
1911     // action == what action to take if the user confirms the alert
1912     service.copy_alert_dialog = function(evt, params, options, action) {
1913         egCore.audio.play('warning.circ.item_alert');
1914         if (angular.isArray(evt)) evt = evt[0];
1915         if (!angular.isArray(evt.payload)) {
1916             return egConfirmDialog.open(
1917                 egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, 
1918                 evt.payload,  // payload == alert message text
1919                 {   copy_barcode : params.copy_barcode,
1920                     ok : function() {},
1921                     cancel : function() {}
1922                 }
1923             ).result.then(function() {
1924                 options.override = true;
1925                 return service[action](params, options);
1926             });
1927         } else { // we got a list of copy alert objects ...
1928             return egCopyAlertManagerDialog.open({
1929                 alerts : evt.payload,
1930                 mode : action,
1931                 ok : function(the_next_status) {
1932                         if (the_next_status !== null) {
1933                             params.next_copy_status = [ the_next_status ];
1934                             params.capture = 'nocapture';
1935                         }
1936                      },
1937                 cancel : function() {}
1938             }).result.then(function() {
1939                 options.override = true;
1940                 return service[action](params, options);
1941             });
1942         }
1943     }
1944
1945     // action == what action to take if the user confirms the alert
1946     service.hold_capture_delay_dialog = function(evt, params, options, action) {
1947         if (angular.isArray(evt)) evt = evt[0];
1948         return $uibModal.open({
1949             templateUrl: './circ/checkin/t_hold_verify',
1950             backdrop: 'static',
1951             controller:
1952                        ['$scope','$uibModalInstance','params',
1953                 function($scope , $uibModalInstance , params) {
1954                 $scope.copy_barcode = params.copy_barcode;
1955                 $scope.capture = function() {
1956                     params.capture = 'capture';
1957                     $uibModalInstance.close();
1958                 };
1959                 $scope.nocapture = function() {
1960                     params.capture = 'nocapture';
1961                     $uibModalInstance.close();
1962                 };
1963                 $scope.cancel = function() { $uibModalInstance.dismiss(); };
1964             }],
1965             resolve : {
1966                 params : function() {
1967                     return params;
1968                 }
1969             }
1970         }).result.then(
1971             function(r) {
1972                 return service[action](params, options);
1973             }
1974         );
1975     }
1976
1977     // check the barcode.  If it's no good, show the warning dialog
1978     // Resolves on success, rejected on error
1979     service.test_barcode = function(bc) {
1980
1981         var ok = service.check_barcode(bc);
1982         if (ok) return $q.when();
1983
1984         egCore.audio.play('warning.circ.bad_barcode');
1985         return $uibModal.open({
1986             templateUrl: './circ/share/t_bad_barcode_dialog',
1987             backdrop: 'static',
1988             controller: 
1989                 ['$scope', '$uibModalInstance', 
1990                 function($scope, $uibModalInstance) {
1991                 $scope.barcode = bc;
1992                 $scope.ok = function() { $uibModalInstance.close() }
1993                 $scope.cancel = function() { $uibModalInstance.dismiss() }
1994             }]
1995         }).result;
1996     }
1997
1998     // check() and checkdigit() copied directly 
1999     // from chrome/content/util/barcode.js
2000
2001     service.check_barcode = function(bc) {
2002         if (bc != Number(bc)) return false;
2003         bc = bc.toString();
2004         // "16.00" == Number("16.00"), but the . is bad.
2005         // Throw out any barcode that isn't just digits
2006         if (bc.search(/\D/) != -1) return false;
2007         var last_digit = bc.substr(bc.length-1);
2008         var stripped_barcode = bc.substr(0,bc.length-1);
2009         return service.barcode_checkdigit(stripped_barcode).toString() == last_digit;
2010     }
2011
2012     service.barcode_checkdigit = function(bc) {
2013         var reverse_barcode = bc.toString().split('').reverse();
2014         var check_sum = 0; var multiplier = 2;
2015         for (var i = 0; i < reverse_barcode.length; i++) {
2016             var digit = reverse_barcode[i];
2017             var product = digit * multiplier; product = product.toString();
2018             var temp_sum = 0;
2019             for (var j = 0; j < product.length; j++) {
2020                 temp_sum += Number( product[j] );
2021             }
2022             check_sum += Number( temp_sum );
2023             multiplier = ( multiplier == 2 ? 1 : 2 );
2024         }
2025         check_sum = check_sum.toString();
2026         var next_multiple_of_10 = (check_sum.match(/(\d*)\d$/)[1] * 10) + 10;
2027         var check_digit = next_multiple_of_10 - Number(check_sum);
2028         if (check_digit == 10) check_digit = 0;
2029         return check_digit;
2030     }
2031
2032     service.handle_barcode_completion = function(barcode) {
2033         return egCore.net.request(
2034             'open-ils.actor',
2035             'open-ils.actor.get_barcodes',
2036             egCore.auth.token(), egCore.auth.user().ws_ou(), 
2037             'asset', barcode)
2038
2039         .then(function(resp) {
2040             // TODO: handle event during barcode lookup
2041             if (evt = egCore.evt.parse(resp)) {
2042                 console.error(evt.toString());
2043                 return $q.reject();
2044             }
2045
2046             // no matching barcodes: return the barcode as entered
2047             // by the user (so that, e.g., checkout can fall back to
2048             // precat/noncat handling)
2049             if (!resp || !resp[0]) {
2050                 return barcode;
2051             }
2052
2053             // exactly one matching barcode: return it
2054             if (resp.length == 1) {
2055                 return resp[0].barcode;
2056             }
2057
2058             // multiple matching barcodes: let the user pick one 
2059             console.debug('multiple matching barcodes');
2060             var matches = [];
2061             var promises = [];
2062             var final_barcode;
2063             angular.forEach(resp, function(cp) {
2064                 promises.push(
2065                     egCore.net.request(
2066                         'open-ils.circ',
2067                         'open-ils.circ.copy_details.retrieve',
2068                         egCore.auth.token(), cp.id
2069                     ).then(function(r) {
2070                         matches.push({
2071                             barcode: r.copy.barcode(),
2072                             title: r.mvr.title(),
2073                             org_name: egCore.org.get(r.copy.circ_lib()).name(),
2074                             org_shortname: egCore.org.get(r.copy.circ_lib()).shortname()
2075                         });
2076                     })
2077                 );
2078             });
2079             return $q.all(promises)
2080             .then(function() {
2081                 return $uibModal.open({
2082                     templateUrl: './circ/share/t_barcode_choice_dialog',
2083                     backdrop: 'static',
2084                     controller:
2085                         ['$scope', '$uibModalInstance',
2086                         function($scope, $uibModalInstance) {
2087                         $scope.matches = matches;
2088                         $scope.ok = function(barcode) {
2089                             $uibModalInstance.close();
2090                             final_barcode = barcode;
2091                         }
2092                         $scope.cancel = function() {$uibModalInstance.dismiss()}
2093                     }],
2094                 }).result.then(function() { return final_barcode });
2095             })
2096         });
2097     }
2098
2099     function generate_penalty_dialog_watch_callback($scope,egCore,allPenalties) {
2100         return function(newval) {
2101             if (newval) {
2102                 var selected_penalty = allPenalties.filter(function(p) {
2103                         return p.id() == newval; })[0];
2104                 var penalty_id = selected_penalty.id();
2105                 if (penalty_id == 20 || penalty_id == 21 || penalty_id == 25) {
2106                     $scope.args.custom_penalty = penalty_id;
2107                     $scope.args.penalty = penalty_id;
2108                 }
2109                 if (penalty_id > 100) {
2110                     $scope.args.custom_penalty = penalty_id;
2111                     $scope.args.penalty = null;
2112                 }
2113                 // there's a $watch on custom_depth
2114                 if (selected_penalty.org_depth() || selected_penalty.org_depth() == 0) {
2115                     $scope.args.custom_depth = selected_penalty.org_depth();
2116                 } else {
2117                     $scope.args.custom_depth = $scope.args.org.ou_type().depth();
2118                 }
2119             }
2120         };
2121     }
2122
2123     service.create_penalty = function(user_id) {
2124         return $uibModal.open({
2125             templateUrl: './circ/share/t_new_message_dialog',
2126             backdrop: 'static',
2127             controller: 
2128                    ['$scope','$uibModalInstance','allPenalties','goodOrgs',
2129             function($scope , $uibModalInstance , allPenalties , goodOrgs) {
2130                 $scope.focusNote = true;
2131                 $scope.penalties = allPenalties.filter(
2132                     function(p) { return p.id() > 100 || p.id() == 20 || p.id() == 21 || p.id() == 25; });
2133                 $scope.set_penalty = function(id) {
2134                     if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
2135                         $scope.args.penalty = id;
2136                     }
2137                 }
2138                 $scope.require_initials = service.require_initials;
2139                 $scope.update_org = function(org) {
2140                     if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
2141                         $scope.args.org = org;
2142                     }
2143                 }
2144                 $scope.cant_use_org = function(org_id) {
2145                     return ($scope.args.pub && $scope.args.read_date) || $scope.args.deleted || goodOrgs.indexOf(org_id) == -1;
2146                 }
2147                 $scope.args = {
2148                     pub : false,
2149                     penalty : 21, // default to Note
2150                     org : egCore.org.get(egCore.auth.user().ws_ou())
2151                 };
2152                 $scope.args.max_depth = $scope.args.org.ou_type().depth();
2153                 $scope.ok = function(count) { $uibModalInstance.close($scope.args) }
2154                 $scope.cancel = function($event) { 
2155                     $uibModalInstance.dismiss();
2156                     $event.preventDefault();
2157                 }
2158                 $scope.$watch('args.penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
2159                 $scope.$watch('args.custom_penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
2160                 $scope.$watch('args.custom_depth', function(org_depth) {
2161                     if (org_depth || org_depth == 0) {
2162                         egCore.net.request(
2163                             'open-ils.actor',
2164                             'open-ils.actor.org_unit.ancestor_at_depth.retrieve',
2165                             egCore.auth.token(), egCore.auth.user().ws_ou(), org_depth
2166                         ).then(function(ctx_org) {
2167                             if (ctx_org) {
2168                                 $scope.args.org = egCore.org.get(ctx_org);
2169                             }
2170                         });
2171                     }
2172                 });
2173             }],
2174             resolve : {
2175                 allPenalties : service.get_all_penalty_types,
2176                 goodOrgs : egCore.perm.hasPermAt('UPDATE_USER', true)
2177             }
2178         }).result.then(
2179             function(args) {
2180                 var pen = new egCore.idl.ausp();
2181                 var msg = {
2182                     pub : args.pub,
2183                     title : args.title,
2184                     message : args.note ? args.note : ''
2185                 };
2186                 pen.usr(user_id);
2187                 pen.org_unit(args.org.id());
2188                 if (args.initials) msg.message = (args.note ? args.note : '') + ' [' + args.initials + ']';
2189                 if (args.custom_penalty) {
2190                     pen.standing_penalty(args.custom_penalty);
2191                 } else {
2192                     pen.standing_penalty(args.penalty);
2193                 }
2194                 pen.staff(egCore.auth.user().id());
2195                 pen.set_date('now');
2196
2197                 return egCore.net.request(
2198                     'open-ils.actor',
2199                     'open-ils.actor.user.penalty.apply',
2200                     egCore.auth.token(), pen, msg
2201                 );
2202             }
2203         );
2204     }
2205
2206     // assumes, for now anyway,  penalty type is fleshed onto usr_penalty.
2207     service.edit_penalty = function(pen,aum) {
2208         return $uibModal.open({
2209             templateUrl: './circ/share/t_new_message_dialog',
2210             backdrop: 'static',
2211             controller: 
2212                    ['$scope','$uibModalInstance','allPenalties','goodOrgs',
2213             function($scope , $uibModalInstance , allPenalties , goodOrgs) {
2214                 // We may need to vivicate usr_penalty (pen) or usr_message (aum)
2215                 if (!pen) {
2216                     pen = new egCore.idl.ausp();
2217                     pen.usr(aum.usr());
2218                     pen.org_unit(aum.sending_lib()); // FIXME: preserve sending_lib or use ws_ou?
2219                     pen.staff(egCore.auth.user().id());
2220                     pen.set_date('now');
2221                     pen.usr_message(aum.id());
2222                     pen.isnew(true);
2223                     aum.ischanged(true);
2224                 }
2225                 if (!aum) {
2226                     aum = new egCore.idl.aum();
2227                     aum.create_date('now');
2228                     aum.sending_lib(pen.org_unit());
2229                     aum.pub(false);
2230                     aum.usr(pen.usr());
2231                     aum.isnew(true);
2232                     pen.ischanged(true);
2233                 }
2234
2235                 $scope.focusNote = true;
2236                 $scope.penalties = allPenalties.filter(
2237                     function(p) { return p.id() > 100 || p.id() == 20 || p.id() == 21 || p.id() == 25; });
2238                 $scope.set_penalty = function(id) {
2239                     if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
2240                         $scope.args.penalty = id;
2241                     }
2242                 }
2243                 $scope.require_initials = service.require_initials;
2244                 $scope.update_org = function(org) {
2245                     if (!($scope.args.pub && $scope.args.read_date) && !$scope.args.deleted) {
2246                         $scope.args.org = org;
2247                     }
2248                 }
2249                 $scope.cant_use_org = function(org_id) {
2250                     return ($scope.args.pub && $scope.args.read_date) || $scope.args.deleted || goodOrgs.indexOf(org_id) == -1;
2251                 }
2252                 var penalty_id = pen.standing_penalty();
2253                 $scope.args = {
2254                     penalty : pen.isnew()
2255                         ? 21 // default to Note
2256                         : penalty_id,
2257                     pub : typeof aum.pub() == 'boolean'
2258                         ? aum.pub()
2259                         : aum.pub() == 't',
2260                     title : aum.title(),
2261                     note : aum.message() ? aum.message() : '',
2262                     org : egCore.org.get(pen.org_unit()),
2263                     deleted : typeof aum.deleted() == 'boolean'
2264                         ? aum.deleted()
2265                         : aum.deleted() == 't',
2266                     read_date : aum.read_date(),
2267                     edit_date : aum.edit_date(),
2268                     editor : aum.editor()
2269                 }
2270                 $scope.args.max_depth = $scope.args.org.ou_type().depth();
2271                 $scope.original_org = $scope.args.org;
2272                 $scope.workstation_depth = egCore.org.get(egCore.auth.user().ws_ou()).ou_type().depth();
2273                 if (penalty_id == 20 || penalty_id == 21 || penalty_id == 25) {
2274                     $scope.args.custom_penalty = penalty_id;
2275                 }
2276                 if (penalty_id > 100) {
2277                     $scope.args.custom_penalty = penalty_id;
2278                     $scope.args.penalty = null;
2279                 }
2280                 $scope.ok = function(count) { $uibModalInstance.close($scope.args) }
2281                 $scope.cancel = function($event) { 
2282                     $uibModalInstance.dismiss();
2283                     $event.preventDefault();
2284                 }
2285                 $scope.$watch('args.penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
2286                 $scope.$watch('args.custom_penalty', generate_penalty_dialog_watch_callback($scope,egCore,allPenalties));
2287                 $scope.$watch('args.custom_depth', function(org_depth) {
2288                     if (org_depth || org_depth == 0) {
2289                         if (org_depth > $scope.workstation_depth) {
2290                             $scope.args.org = $scope.original_org;
2291                         } else {
2292                             egCore.net.request(
2293                                 'open-ils.actor',
2294                                 'open-ils.actor.org_unit.ancestor_at_depth.retrieve',
2295                                 egCore.auth.token(), egCore.auth.user().ws_ou(), org_depth
2296                             ).then(function(ctx_org) {
2297                                 if (ctx_org) {
2298                                     $scope.args.org = egCore.org.get(ctx_org);
2299                                 }
2300                             });
2301                         }
2302                     }
2303                 });
2304             }],
2305             resolve : {
2306                 allPenalties : service.get_all_penalty_types,
2307                 goodOrgs : egCore.perm.hasPermAt('UPDATE_USER', true)
2308             }
2309         }).result.then(
2310             function(args) {
2311                 aum.pub(args.pub);
2312                 aum.title(args.title);
2313                 aum.message(args.note);
2314                 aum.sending_lib(egCore.org.get(egCore.auth.user().ws_ou()).id());
2315                 pen.org_unit(egCore.org.get(args.org).id());
2316                 if (args.initials) aum.message((args.note ? args.note : '') + ' [' + args.initials + ']');
2317                 if (args.custom_penalty) {
2318                     pen.standing_penalty(args.custom_penalty);
2319                 } else {
2320                     pen.standing_penalty(args.penalty);
2321                 }
2322                 return egCore.net.request(
2323                     'open-ils.actor',
2324                     'open-ils.actor.user.penalty.modify',
2325                     egCore.auth.token(), pen, aum
2326                 );
2327             }
2328         );
2329     }
2330
2331     return service;
2332
2333 }]);
2334
2335