LP##1661688 - Want easy way to clear a hold when picked up by other patron.
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / services / circ.js
index 2d34b81..3bf866c 100644 (file)
@@ -5,9 +5,10 @@
 angular.module('egCoreMod')
 
 .factory('egCirc',
-       ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog',
+
+       ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog','egAddCopyAlertDialog','egCopyAlertManagerDialog','egCopyAlertEditorDialog',
         'egWorkLog',
-function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
+function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAlertDialog , egCopyAlertManagerDialog,  egCopyAlertEditorDialog ,
          egWorkLog) {
 
     var service = {
@@ -26,9 +27,12 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
             'ui.staff.require_initials.patron_standing_penalty',
             'ui.admin.work_log.max_entries',
             'ui.admin.patron_log.max_entries',
-            'circ.staff_client.do_not_auto_attempt_print'
+            'circ.staff_client.do_not_auto_attempt_print',
+            'circ.clear_hold_on_checkout'
         ]).then(function(set) {
             service.require_initials = Boolean(set['ui.staff.require_initials.patron_standing_penalty']);
+           service.clearHold = Boolean(set['circ.clear_hold_on_checkout']);
+
             if (angular.isArray(set['circ.staff_client.do_not_auto_attempt_print'])) {
                 if (set['circ.staff_client.do_not_auto_attempt_print'].indexOf('Hold Slip') > 1)
                     service.never_auto_print['hold_shelf_slip'] = true;
@@ -101,6 +105,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         'CIRC_CLAIMS_RETURNED',
         'COPY_ALERT_MESSAGE',
         'COPY_STATUS_LOST',
+        'COPY_STATUS_LOST_AND_PAID',
         'COPY_STATUS_LONG_OVERDUE',
         'COPY_STATUS_MISSING',
         'PATRON_EXCEEDS_FINES'
@@ -109,6 +114,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // these events can be overridden by staff during checkin
     service.checkin_overridable_events = 
         service.checkin_suppress_overrides.concat([
+        'HOLD_CAPTURE_DELAYED', // not technically overridable, but special prompt and param
         'TRANSIT_CHECKIN_INTERVAL_BLOCK'
     ])
 
@@ -121,35 +127,52 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // options : non-parameter controls.  e.g. "override", "check_barcode"
     service.checkout = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
 
         console.debug('egCirc.checkout() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
 
-        var promise = options.check_barcode ? 
-            service.test_barcode(params.copy_barcode) : $q.when();
+        // handle barcode completion
+        return service.handle_barcode_completion(params.copy_barcode)
+        .then(function(barcode) {
+            console.debug('barcode after completion: ' + barcode);
+            params.copy_barcode = barcode;
 
-        // avoid re-check on override, etc.
-        delete options.check_barcode;
+            var promise = options.check_barcode ? 
+                service.test_barcode(params.copy_barcode) : $q.when();
 
-        return promise.then(function() {
+            // avoid re-check on override, etc.
+            delete options.check_barcode;
 
-            var method = 'open-ils.circ.checkout.full';
-            if (options.override) method += '.override';
+            return promise.then(function() {
 
-            return egCore.net.request(
-                'open-ils.circ', method, egCore.auth.token(), params
+                var method = 'open-ils.circ.checkout.full';
+                if (options.override) method += '.override';
 
-            ).then(function(evt) {
+                return egCore.net.request(
+                    'open-ils.circ', method, egCore.auth.token(), params
 
-                if (!angular.isArray(evt)) evt = [evt];
+                ).then(function(evt) {
 
-                return service.flesh_response_data('checkout', evt, params, options)
-                .then(function() {
-                    return service.handle_checkout_resp(evt, params, options);
-                })
-                .then(function(final_resp) {
-                    return service.munge_resp_data(final_resp,'checkout',method)
-                })
+                    if (!angular.isArray(evt)) evt = [evt];
+
+                    if (evt[0].payload && evt[0].payload.auto_renew == 1) {
+                        // open circulation found with auto-renew toggle on.
+                        console.debug('Auto-renewing item ' + params.copy_barcode);
+                        options.auto_renew = true;
+                        return service.renew(params, options);
+                    }
+
+                    var action = params.noncat ? 'noncat_checkout' : 'checkout';
+
+                    return service.flesh_response_data(action, evt, params, options)
+                    .then(function() {
+                        return service.handle_checkout_resp(evt, params, options);
+                    })
+                    .then(function(final_resp) {
+                        return service.munge_resp_data(final_resp,action,method)
+                    })
+                });
             });
         });
     }
@@ -160,36 +183,44 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // Rejected if the renewal cannot be completed.
     service.renew = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
 
         console.debug('egCirc.renew() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
 
-        var promise = options.check_barcode ? 
-            service.test_barcode(params.copy_barcode) : $q.when();
+        // handle barcode completion
+        return service.handle_barcode_completion(params.copy_barcode)
+        .then(function(barcode) {
+            params.copy_barcode = barcode;
 
-        // avoid re-check on override, etc.
-        delete options.check_barcode;
+            var promise = options.check_barcode ? 
+                service.test_barcode(params.copy_barcode) : $q.when();
 
-        return promise.then(function() {
+            // avoid re-check on override, etc.
+            delete options.check_barcode;
 
-            var method = 'open-ils.circ.renew';
-            if (options.override) method += '.override';
+            return promise.then(function() {
 
-            return egCore.net.request(
-                'open-ils.circ', method, egCore.auth.token(), params
+                var method = 'open-ils.circ.renew';
+                if (options.override) method += '.override';
 
-            ).then(function(evt) {
+                return egCore.net.request(
+                    'open-ils.circ', method, egCore.auth.token(), params
 
-                if (!angular.isArray(evt)) evt = [evt];
+                ).then(function(evt) {
 
-                return service.flesh_response_data(
-                    'renew', evt, params, options)
-                .then(function() {
-                    return service.handle_renew_resp(evt, params, options);
-                })
-                .then(function(final_resp) {
-                    return service.munge_resp_data(final_resp,'renew',method)
-                })
+                    if (!angular.isArray(evt)) evt = [evt];
+
+                    return service.flesh_response_data(
+                        'renew', evt, params, options)
+                    .then(function() {
+                        return service.handle_renew_resp(evt, params, options);
+                    })
+                    .then(function(final_resp) {
+                        final_resp.auto_renew = options.auto_renew;
+                        return service.munge_resp_data(final_resp,'renew',method)
+                    })
+                });
             });
         });
     }
@@ -200,35 +231,42 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // Rejected if the checkin cannot be completed.
     service.checkin = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
 
         console.debug('egCirc.checkin() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
 
-        var promise = options.check_barcode ? 
-            service.test_barcode(params.copy_barcode) : $q.when();
+        // handle barcode completion
+        return service.handle_barcode_completion(params.copy_barcode)
+        .then(function(barcode) {
+            params.copy_barcode = barcode;
 
-        // avoid re-check on override, etc.
-        delete options.check_barcode;
+            var promise = options.check_barcode ? 
+                service.test_barcode(params.copy_barcode) : $q.when();
 
-        return promise.then(function() {
+            // avoid re-check on override, etc.
+            delete options.check_barcode;
 
-            var method = 'open-ils.circ.checkin';
-            if (options.override) method += '.override';
+            return promise.then(function() {
 
-            return egCore.net.request(
-                'open-ils.circ', method, egCore.auth.token(), params
+                var method = 'open-ils.circ.checkin';
+                if (options.override) method += '.override';
 
-            ).then(function(evt) {
+                return egCore.net.request(
+                    'open-ils.circ', method, egCore.auth.token(), params
 
-                if (!angular.isArray(evt)) evt = [evt];
-                return service.flesh_response_data(
-                    'checkin', evt, params, options)
-                .then(function() {
-                    return service.handle_checkin_resp(evt, params, options);
-                })
-                .then(function(final_resp) {
-                    return service.munge_resp_data(final_resp,'checkin',method)
-                })
+                ).then(function(evt) {
+
+                    if (!angular.isArray(evt)) evt = [evt];
+                    return service.flesh_response_data(
+                        'checkin', evt, params, options)
+                    .then(function() {
+                        return service.handle_checkin_resp(evt, params, options);
+                    })
+                    .then(function(final_resp) {
+                        return service.munge_resp_data(final_resp,'checkin',method)
+                    })
+                });
             });
         });
     }
@@ -238,16 +276,24 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         var data = final_resp.data = {};
 
         if (!final_resp.evt[0]) {
-            egCore.audio.play('warning.unknown.no_event');
+            egCore.audio.play('error.unknown.no_event');
             return;
         }
 
         var payload = final_resp.evt[0].payload;
         if (!payload) {
-            egCore.audio.play('warning.unknown.no_payload');
+            egCore.audio.play('error.unknown.no_payload');
             return;
         }
 
+        // retrieve call number affixes prior to sending payload data to the grid
+        if (payload.volume && typeof payload.volume.prefix() != 'object') {
+            egCore.pcrud.retrieve('acnp',payload.volume.prefix()).then(function(p) {payload.volume.prefix(p)});
+        };
+        if (payload.volume && typeof payload.volume.suffix() != 'object') {
+            egCore.pcrud.retrieve('acns',payload.volume.suffix()).then(function(s) {payload.volume.suffix(s)});
+        };
+
         data.circ = payload.circ;
         data.parent_circ = payload.parent_circ;
         data.hold = payload.hold;
@@ -263,6 +309,9 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         data.isbn = final_resp.evt[0].isbn;
         data.route_to = final_resp.evt[0].route_to;
 
+        if (payload.circ) data.duration = payload.circ.duration();
+        if (payload.circ) data.circ_lib = payload.circ.circ_lib();
+
         // for checkin, the mbts lives on the main circ
         if (payload.circ && payload.circ.billable_transaction())
             data.mbts = payload.circ.billable_transaction().summary();
@@ -280,7 +329,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         }
 
         egWorkLog.record(
-            worklog_action == 'checkout'
+            (worklog_action == 'checkout' || worklog_action == 'noncat_checkout')
             ? egCore.strings.EG_WORK_LOG_CHECKOUT
             : (worklog_action == 'renew'
                 ? egCore.strings.EG_WORK_LOG_RENEW
@@ -319,7 +368,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
 
         switch(evt[0].textcode) {
             case 'COPY_NOT_AVAILABLE':
-                return service.copy_not_avail_dialog(evt[0], params, options);
+                return service.copy_not_avail_dialog(evt, params, options);
             case 'COPY_ALERT_MESSAGE':
                 return service.copy_alert_dialog(evt[0], params, options, 'checkout');
             default: 
@@ -384,6 +433,8 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         switch(evt[0].textcode) {
             case 'COPY_ALERT_MESSAGE':
                 return service.copy_alert_dialog(evt[0], params, options, 'checkin');
+            case 'HOLD_CAPTURE_DELAYED':
+                return service.hold_capture_delay_dialog(evt[0], params, options, 'checkin');
             default: 
                 return service.override_dialog(evt, params, options, 'checkin');
         }
@@ -404,7 +455,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         // Other events
         switch (evt[0].textcode) {
             case 'SUCCESS':
-                egCore.audio.play('success.renew');
+                egCore.audio.play('info.renew');
                 return $q.when(final_resp);
 
             case 'COPY_IN_TRANSIT':
@@ -418,13 +469,6 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                     {barcode : params.copy_barcode}
                 );
 
-            case 'PERM_FAILURE':
-                egCore.audio.play('warning.renew.permission');
-                return service.exit_alert(
-                    egCore.strings[evt[0].textcode],
-                    {permission : evt[0].ilsperm}
-                );
-
             default:
                 egCore.audio.play('warning.renew.unknown');
                 return service.exit_alert(
@@ -456,11 +500,12 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                 return $q.when(final_resp);
 
             case 'ITEM_NOT_CATALOGED':
-                egCore.audio.play('warning.checkout.no_cataloged');
+                egCore.audio.play('error.checkout.no_cataloged');
                 return service.precat_dialog(params, options);
 
             case 'OPEN_CIRCULATION_EXISTS':
-                egCore.audio.play('warning.checkout.open_circ');
+                // auto_renew checked in service.checkout()
+                egCore.audio.play('error.checkout.open_circ');
                 return service.circ_exists_dialog(evt, params, options);
 
             case 'COPY_IN_TRANSIT':
@@ -477,15 +522,8 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                     {barcode : params.copy_barcode}
                 );
 
-            case 'PERM_FAILURE':
-                egCore.audio.play('warning.checkout.permission');
-                return service.exit_alert(
-                    egCore.strings[evt[0].textcode],
-                    {permission : evt[0].ilsperm}
-                );
-
             default:
-                egCore.audio.play('warning.checkout.unknown');
+                egCore.audio.play('error.checkout.unknown');
                 return service.exit_alert(
                     egCore.strings.CHECKOUT_FAILED_GENERIC, {
                         barcode : params.copy_barcode,
@@ -540,9 +578,12 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         var promises = [];
         var payload;
         if (!evt[0] || !(payload = evt[0].payload)) return $q.when();
-
+        
         promises.push(service.flesh_copy_location(payload.copy));
         if (payload.copy) {
+            promises.push(service.flesh_acn_owning_lib(payload.volume));
+            promises.push(service.flesh_copy_circ_library(payload.copy));
+            promises.push(service.flesh_copy_circ_modifier(payload.copy));
             promises.push(
                 service.flesh_copy_status(payload.copy)
 
@@ -571,11 +612,16 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         } 
 
         // TODO: renewal responses should include the patron
-        if (!payload.patron && payload.circ) {
-            promises.push(
-                egCore.pcrud.retrieve('au', payload.circ.usr())
-                .then(function(user) {payload.patron = user})
-            );
+        if (!payload.patron) {
+            var user_id;
+            if (payload.circ) user_id = payload.circ.usr();
+            if (payload.noncat_circ) user_id = payload.noncat_circ.patron();
+            if (user_id) {
+                promises.push(
+                    egCore.pcrud.retrieve('au', user_id)
+                    .then(function(user) {payload.patron = user})
+                );
+            }
         }
 
         // extract precat values
@@ -591,6 +637,30 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         return $q.all(promises);
     }
 
+    service.flesh_acn_owning_lib = function(acn) {
+        if (!acn) return $q.when();
+        return $q.when(acn.owning_lib(egCore.org.get( acn.owning_lib() )));
+    }
+
+    service.flesh_copy_circ_library = function(copy) {
+        if (!copy) return $q.when();
+        
+        return $q.when(copy.circ_lib(egCore.org.get( copy.circ_lib() )));
+    }
+
+    // fetches the full list of circ modifiers
+    service.flesh_copy_circ_modifier = function(copy) {
+        if (!copy) return $q.when();
+        if (egCore.env.ccm)
+            return $q.when(copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]));
+        return egCore.pcrud.retrieveAll('ccm', {}, {atomic : true}).then(
+            function(list) {
+                egCore.env.absorbList(list, 'ccm');
+                copy.circ_modifier(egCore.env.ccm.map[copy.circ_modifier()]);
+            }
+        );
+    }
+
     // fetches the full list of copy statuses
     service.flesh_copy_status = function(copy) {
         if (!copy) return $q.when();
@@ -629,6 +699,8 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         var org = egCore.org.get(org_id);
         var addr_id = org[addr_type]();
 
+        if (!addr_id) return $q.when(null);
+
         if (egCore.env.aoa && egCore.env.aoa.map[addr_id]) 
             return $q.when(egCore.env.aoa.map[addr_id]); 
 
@@ -647,18 +719,69 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // the returned event.
     service.override_dialog = function(evt, params, options, action) {
         if (!angular.isArray(evt)) evt = [evt];
+
+        egCore.audio.play('warning.circ.event_override');
+        var copy_alert = evt.filter(function(e) {
+            return e.textcode == 'COPY_ALERT_MESSAGE';
+        });
+        evt = evt.filter(function(e) {
+            return e.textcode !== 'COPY_ALERT_MESSAGE';
+        });
+
         return $uibModal.open({
             templateUrl: './circ/share/t_event_override_dialog',
+            backdrop: 'static',
             controller: 
                 ['$scope', '$uibModalInstance', 
                 function($scope, $uibModalInstance) {
                 $scope.events = evt;
+
+                // Find the event, if any, that is for ITEM_ON_HOLDS_SHELF
+                //  and grab the patron name of the owner. 
+                $scope.holdEvent = evt.filter(
+                    function(e) {
+                        return e.textcode === 'ITEM_ON_HOLDS_SHELF'
+                     }
+                );
+
+                if ($scope.holdEvent) {
+                  // Ensure we have a scalar here 
+                   if (angular.isArray($scope.holdEvent)) {
+                       $scope.holdEvent = $scope.holdEvent[0];
+                   }
+
+                   $scope.patronName = $scope.holdEvent.payload.patron_name;
+                   $scope.holdID = $scope.holdEvent.payload.hold_id;
+                }
+
                 $scope.auto_override =
                     evt.filter(function(e){
                         return service.checkout_auto_override_after_first.indexOf(evt.textcode) > -1;
                     }).length > 0;
                 $scope.copy_barcode = params.copy_barcode; // may be null
-                $scope.ok = function() { $uibModalInstance.close() }
+
+                // Implementation note: Why not use a primitive here? It
+                // doesn't work.  See: 
+                // http://stackoverflow.com/questions/18642371/checkbox-not-binding-to-scope-in-angularjs
+                $scope.formdata = {clearHold : service.clearHold};
+
+                $scope.ok = function() { 
+                        $uibModalInstance.close();
+
+                        // Handle the cancellation of the assciated hold here
+                        if ($scope.formdata.clearHold && $scope.holdID) {
+                            $scope.args = {
+                            cancel_reason : 5,
+                                note: 'Item checked out by other patron'
+                           };
+                            egCore.net.request(
+                                'open-ils.circ', 'open-ils.circ.hold.cancel',
+                                egCore.auth.token(), $scope.holdID,
+                                $scope.args.cancel_reason,
+                                $scope.args.note);
+                        }
+               }
+
                 $scope.cancel = function ($event) { 
                     $uibModalInstance.dismiss();
                     $event.preventDefault();
@@ -668,6 +791,10 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
             function() {
                 options.override = true;
 
+                if (copy_alert.length > 0) {
+                    return service.copy_alert_dialog(copy_alert, params, options, action);
+                }
+
                 if (action == 'checkin') {
                     return service.checkin(params, options);
                 }
@@ -684,9 +811,19 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     }
 
     service.copy_not_avail_dialog = function(evt, params, options) {
-        if (angular.isArray(evt)) evt = evt[0];
+        if (!angular.isArray(evt)) evt = [evt];
+
+        var copy_alert = evt.filter(function(e) {
+            return e.textcode == 'COPY_ALERT_MESSAGE';
+        });
+        evt = evt.filter(function(e) {
+            return e.textcode !== 'COPY_ALERT_MESSAGE';
+        });
+        evt = evt[0];
+
         return $uibModal.open({
             templateUrl: './circ/share/t_copy_not_avail_dialog',
+            backdrop: 'static',
             controller: 
                        ['$scope','$uibModalInstance','copyStatus',
                 function($scope , $uibModalInstance , copyStatus) {
@@ -703,6 +840,11 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         }).result.then(
             function() {
                 options.override = true;
+
+                if (copy_alert.length > 0) {
+                    return service.copy_alert_dialog(copy_alert, params, options, 'checkout');
+                }
+
                 return service.checkout(params, options);
             }
         );
@@ -727,6 +869,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
 
             return $uibModal.open({
                 templateUrl: './circ/share/t_noncat_dialog',
+                backdrop: 'static',
                 controller: 
                     ['$scope', '$uibModalInstance',
                     function($scope, $uibModalInstance) {
@@ -759,17 +902,24 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
 
         return $uibModal.open({
             templateUrl: './circ/share/t_precat_dialog',
+            backdrop: 'static',
             controller: 
                 ['$scope', '$uibModalInstance', 'circMods',
                 function($scope, $uibModalInstance, circMods) {
                 $scope.focusMe = true;
                 $scope.precatArgs = {
-                    copy_barcode : params.copy_barcode,
-                    circ_modifier : circMods.length ? circMods[0].code() : null
+                    copy_barcode : params.copy_barcode
                 };
                 $scope.circModifiers = circMods;
                 $scope.ok = function(args) { $uibModalInstance.close(args) }
                 $scope.cancel = function () { $uibModalInstance.dismiss() }
+
+                // use this function as a keydown handler on form
+                // elements that should not submit the form on enter.
+                $scope.preventSubmit = function($event) {
+                    if ($event.keyCode == 13)
+                        $event.preventDefault();
+                }
             }],
             resolve : {
                 circMods : function() { 
@@ -779,6 +929,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         }).result.then(
             function(args) {
                 if (!args || !args.dummy_title) return $q.reject();
+                if(args.circ_modifier == "") args.circ_modifier = null;
                 angular.forEach(args, function(val, key) {params[key] = val});
                 params.precat = true;
                 return service.checkout(params, options);
@@ -820,6 +971,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         if (angular.isArray(evt)) evt = evt[0];
         return $uibModal.open({
             templateUrl: './circ/share/t_copy_in_transit_dialog',
+            backdrop: 'static',
             controller: 
                        ['$scope','$uibModalInstance','transit',
                 function($scope , $uibModalInstance , transit) {
@@ -869,12 +1021,24 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         if (angular.isArray(evt)) evt = evt[0];
 
         if (!evt.payload.old_circ) {
-            return egCore.pcrud.search('circ',
-                {target_copy : evt.payload.copy.id(), checkin_time : null},
-                {limit : 1} // should only ever be 1
-            ).then(function(old_circ) {
-                evt.payload.old_circ = old_circ;
-               return service.circ_exists_dialog_impl(evt, params, options);
+            return egCore.net.request(
+                'open-ils.search',
+                'open-ils.search.asset.copy.fleshed2.find_by_barcode',
+                params.copy_barcode
+            ).then(function(resp){
+                console.log(resp);
+                if (egCore.evt.parse(resp)) {
+                    console.error(egCore.evt.parse(resp));
+                } else {
+                    return egCore.net.request(
+                         'open-ils.circ',
+                         'open-ils.circ.copy_checkout_history.retrieve',
+                         egCore.auth.token(), resp.id(), 1
+                    ).then( function (circs) {
+                        evt.payload.old_circ = circs[0];
+                        return service.circ_exists_dialog_impl( evt, params, options );
+                    });
+                }
             });
         } else {
             return service.circ_exists_dialog_impl( evt, params, options );
@@ -888,6 +1052,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         
         return $uibModal.open({
             templateUrl: './circ/share/t_circ_exists_dialog',
+            backdrop: 'static',
             controller: 
                        ['$scope','$uibModalInstance',
                 function($scope , $uibModalInstance) {
@@ -904,14 +1069,12 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
             function(args) {
                 if (sameUser) {
                     params.void_overdues = args.forgive_fines;
-                    options.override = true;
                     return service.renew(params, options);
                 }
 
                 return service.checkin({
                     barcode : params.copy_barcode,
                     noop : true,
-                    override : true,
                     void_overdues : args.forgive_fines
                 }).then(function(checkin_resp) {
                     if (checkin_resp.evt[0].textcode == 'SUCCESS') {
@@ -935,6 +1098,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     service.backdate_dialog = function(circ_ids) {
         return $uibModal.open({
             templateUrl: './circ/share/t_backdate_dialog',
+            backdrop: 'static',
             controller: 
                        ['$scope','$uibModalInstance',
                 function($scope , $uibModalInstance) {
@@ -1024,6 +1188,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
 
         return $uibModal.open({
             templateUrl: './circ/share/t_mark_claims_returned_dialog',
+            backdrop: 'static',
             controller: 
                        ['$scope','$uibModalInstance',
                 function($scope , $uibModalInstance) {
@@ -1102,32 +1267,80 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         return deferred.promise;
     }
 
-    service.mark_damaged = function(copy_ids) {
-        return egConfirmDialog.open(
-            egCore.strings.MARK_DAMAGED_CONFIRM, '',
-            {   num_items : copy_ids.length,
-                ok : function() {},
-                cancel : function() {}
-            }
+    service.mark_damaged = function(params) {
+        if (!params) return $q.when();
+        return $uibModal.open({
+            templateUrl: './circ/share/t_mark_damaged',
+            controller:
+                ['$scope', '$uibModalInstance', 'egCore', 'egBilling', 'egItem',
+                function($scope, $uibModalInstance, egCore, egBilling, egItem) {
+                    var doRefresh = params.refresh;
+                    
+                    $scope.billArgs = {charge: params.charge};
+                    $scope.mode = 'charge';
+                    $scope.barcode = params.barcode;
+                    if (params.charge && params.charge > 0) {
+                        $scope.applyFine = "apply";
+                    }
+                    if (params.circ) {
+                        $scope.circ = params.circ;
+                        $scope.circ_checkin_time = params.circ.checkin_time();
+                        $scope.circ_patron_name = params.circ.usr().family_name() + ", "
+                            + params.circ.usr().first_given_name() + " "
+                            + params.circ.usr().second_given_name();
+                    }
+                    egBilling.fetchBillingTypes().then(function(res) {
+                        $scope.billingTypes = res;
+                    });
 
-        ).result.then(function() {
-            var promises = [];
-            angular.forEach(copy_ids, function(copy_id) {
-                promises.push(
-                    egCore.net.request(
-                        'open-ils.circ',
-                        'open-ils.circ.mark_item_damaged',
-                        egCore.auth.token(), copy_id
-                    ).then(function(resp) {
-                        if (evt = egCore.evt.parse(resp)) {
-                            console.error('mark damaged failed: ' + evt);
-                        }
-                    })
-                );
-            });
+                    $scope.btnChargeFees = function() {
+                        $scope.mode = 'charge';
+                        $scope.billArgs.charge = params.charge;
+                        $scope.applyFine = "apply";
+                    }
+                    $scope.btnWaiveFees = function() {
+                        $scope.mode = 'waive';
+                        $scope.billArgs.charge = 0;
+                        $scope.applyFine = "noapply";
+                    }
 
-            return $q.all(promises);
-        });
+                    $scope.cancel = function ($event) { 
+                        $uibModalInstance.dismiss();
+                    }
+                    $scope.ok = function() {
+                        handle_mark_item_damaged();
+                    }
+
+                    var handle_mark_item_damaged = function() {
+                        egCore.net.request(
+                            'open-ils.circ',
+                            'open-ils.circ.mark_item_damaged',
+                            egCore.auth.token(), params.id, {
+                                apply_fines: $scope.applyFine,
+                                override_amount: $scope.billArgs.charge,
+                                override_btype: $scope.billArgs.type,
+                                override_note: $scope.billArgs.note,
+                                handle_checkin: !$scope.applyFine
+                        }).then(function(resp) {
+                            if (evt = egCore.evt.parse(resp)) {
+                                doRefresh = false;
+                                console.debug("mark damaged more information required. Pushing back.");
+                                service.mark_damaged({
+                                    id: params.id,
+                                    barcode: params.barcode,
+                                    charge: evt.payload.charge,
+                                    circ: evt.payload.circ,
+                                    refresh: params.refresh
+                                });
+                                console.error('mark damaged failed: ' + evt);
+                            }
+                        }).then(function() {
+                            if (doRefresh) egItem.add_barcode_to_list(params.barcode);
+                        });
+                        $uibModalInstance.close();
+                    }
+                }]
+        }).result;
     }
 
     service.mark_missing = function(copy_ids) {
@@ -1215,7 +1428,21 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         });
     }
 
+    service.add_copy_alerts = function(item_ids) {
+        return egAddCopyAlertDialog.open({
+            copy_ids : item_ids,
+            ok : function() { },
+            cancel : function() {}
+        }).result.then(function() { });
+    }
 
+    service.manage_copy_alerts = function(item_ids) {
+        return egCopyAlertEditorDialog.open({
+            copy_id : item_ids[0],
+            ok : function() { },
+            cancel : function() {}
+        }).result.then(function() { });
+    }
 
     // alert when copy location alert_message is set.
     // This does not affect processing, it only produces a click-through
@@ -1310,8 +1537,8 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                         return $q.when(final_resp);
 
                     default:
-                        egCore.audio.play('error.checkin.unknown');
-                        console.error('Unhandled checkin copy status: ' 
+                        egCore.audio.play('success.checkin');
+                        console.debug('Unusual checkin copy status (may have been set via copy alert): '
                             + copy.status().id() + ' : ' + copy.status().name());
                         return $q.when(final_resp);
                 }
@@ -1323,13 +1550,13 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
                 ).then(function() { return final_resp });
 
             case 'ASSET_COPY_NOT_FOUND':
-                egCore.audio.play('warning.checkin.not_found');
+                egCore.audio.play('error.checkin.not_found');
                 return egAlertDialog.open(
                     egCore.strings.UNCAT_ALERT_DIALOG, params)
                     .result.then(function() {return final_resp});
 
             case 'ITEM_NOT_CATALOGED':
-                egCore.audio.play('warning.checkin.not_cataloged');
+                egCore.audio.play('error.checkin.not_cataloged');
                 evt[0].route_to = egCore.strings.ROUTE_TO_CATALOGING;
                 if (options.no_precat_alert) 
                     return $q.when(final_resp);
@@ -1369,10 +1596,20 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
             );
         }
 
+
         if (!tmpl.match(/hold_shelf/)) {
+            var courier_deferred = $q.defer();
+            promises.push(courier_deferred.promise);
             promises.push(
                 service.find_copy_transit(evt, params, options)
-                .then(function(trans) {data.transit = trans})
+                .then(function(trans) {
+                    data.transit = trans;
+                    egCore.org.settings('lib.courier_code', trans.dest().id())
+                    .then(function(s) {
+                        data.dest_courier_code = s['lib.courier_code'];
+                        courier_deferred.resolve();
+                    });
+                })
             );
         }
 
@@ -1400,20 +1637,35 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
             var print_context = {
                 copy : egCore.idl.toHash(evt.payload.copy),
                 title : evt.title,
-                author : evt.author
-            }
+                author : evt.author,
+                call_number : egCore.idl.toHash(evt.payload.volume)
+            };
+
+            var acn = print_context.call_number; // fix up pre/suffixes
+            if (acn.prefix == -1) acn.prefix = "";
+            if (acn.suffix == -1) acn.suffix = "";
 
             if (data.transit) {
                 // route_dialog includes the "route to holds shelf" 
                 // dialog, which has no transit
                 print_context.transit = egCore.idl.toHash(data.transit);
-                print_context.dest_address = egCore.idl.toHash(data.address);
+                print_context.dest_courier_code = data.dest_courier_code;
+                if (data.address) {
+                    print_context.dest_address = egCore.idl.toHash(data.address);
+                }
                 print_context.dest_location =
                     egCore.idl.toHash(egCore.org.get(data.transit.dest()));
             }
 
             if (data.patron) {
                 print_context.hold = egCore.idl.toHash(evt.payload.hold);
+                var notes = print_context.hold.notes;
+                if(notes.length > 0){
+                    print_context.hold_notes = [];
+                    angular.forEach(notes, function(n){
+                        print_context.hold_notes.push(n);
+                    });
+                }
                 print_context.patron = egCore.idl.toHash(data.patron);
             }
 
@@ -1436,6 +1688,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
 
             return $uibModal.open({
                 templateUrl: tmpl,
+                backdrop: 'static',
                 controller: [
                             '$scope','$uibModalInstance',
                     function($scope , $uibModalInstance) {
@@ -1462,17 +1715,66 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // action == what action to take if the user confirms the alert
     service.copy_alert_dialog = function(evt, params, options, action) {
         if (angular.isArray(evt)) evt = evt[0];
-        return egConfirmDialog.open(
-            egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, 
-            evt.payload,  // payload == alert message text
-            {   copy_barcode : params.copy_barcode,
-                ok : function() {},
+        if (!angular.isArray(evt.payload)) {
+            return egConfirmDialog.open(
+                egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, 
+                evt.payload,  // payload == alert message text
+                {   copy_barcode : params.copy_barcode,
+                    ok : function() {},
+                    cancel : function() {}
+                }
+            ).result.then(function() {
+                options.override = true;
+                return service[action](params, options);
+            });
+        } else { // we got a list of copy alert objects ...
+            return egCopyAlertManagerDialog.open({
+                alerts : evt.payload,
+                mode : action,
+                ok : function(the_next_status) {
+                        if (the_next_status !== null) {
+                            params.next_copy_status = [ the_next_status ];
+                            params.capture = 'nocapture';
+                        }
+                     },
                 cancel : function() {}
+            }).result.then(function() {
+                options.override = true;
+                return service[action](params, options);
+            });
+        }
+    }
+
+    // action == what action to take if the user confirms the alert
+    service.hold_capture_delay_dialog = function(evt, params, options, action) {
+        if (angular.isArray(evt)) evt = evt[0];
+        return $uibModal.open({
+            templateUrl: './circ/checkin/t_hold_verify',
+            backdrop: 'static',
+            controller:
+                       ['$scope','$uibModalInstance','params',
+                function($scope , $uibModalInstance , params) {
+                $scope.copy_barcode = params.copy_barcode;
+                $scope.capture = function() {
+                    params.capture = 'capture';
+                    $uibModalInstance.close();
+                };
+                $scope.nocapture = function() {
+                    params.capture = 'nocapture';
+                    $uibModalInstance.close();
+                };
+                $scope.cancel = function() { $uibModalInstance.dismiss(); };
+            }],
+            resolve : {
+                params : function() {
+                    return params;
+                }
             }
-        ).result.then(function() {
-            options.override = true;
-            return service[action](params, options);
-        });
+        }).result.then(
+            function(r) {
+                return service[action](params, options);
+            }
+        );
     }
 
     // check the barcode.  If it's no good, show the warning dialog
@@ -1482,8 +1784,10 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         var ok = service.check_barcode(bc);
         if (ok) return $q.when();
 
+        egCore.audio.play('warning.circ.bad_barcode');
         return $uibModal.open({
             templateUrl: './circ/share/t_bad_barcode_dialog',
+            backdrop: 'static',
             controller: 
                 ['$scope', '$uibModalInstance', 
                 function($scope, $uibModalInstance) {
@@ -1528,9 +1832,77 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         return check_digit;
     }
 
+    service.handle_barcode_completion = function(barcode) {
+        return egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.get_barcodes',
+            egCore.auth.token(), egCore.auth.user().ws_ou(), 
+            'asset', barcode)
+
+        .then(function(resp) {
+            // TODO: handle event during barcode lookup
+            if (evt = egCore.evt.parse(resp)) {
+                console.error(evt.toString());
+                return $q.reject();
+            }
+
+            // no matching barcodes: return the barcode as entered
+            // by the user (so that, e.g., checkout can fall back to
+            // precat/noncat handling)
+            if (!resp || !resp[0]) {
+                return barcode;
+            }
+
+            // exactly one matching barcode: return it
+            if (resp.length == 1) {
+                return resp[0].barcode;
+            }
+
+            // multiple matching barcodes: let the user pick one 
+            console.debug('multiple matching barcodes');
+            var matches = [];
+            var promises = [];
+            var final_barcode;
+            angular.forEach(resp, function(cp) {
+                promises.push(
+                    egCore.net.request(
+                        'open-ils.circ',
+                        'open-ils.circ.copy_details.retrieve',
+                        egCore.auth.token(), cp.id
+                    ).then(function(r) {
+                        matches.push({
+                            barcode: r.copy.barcode(),
+                            title: r.mvr.title(),
+                            org_name: egCore.org.get(r.copy.circ_lib()).name(),
+                            org_shortname: egCore.org.get(r.copy.circ_lib()).shortname()
+                        });
+                    })
+                );
+            });
+            return $q.all(promises)
+            .then(function() {
+                return $uibModal.open({
+                    templateUrl: './circ/share/t_barcode_choice_dialog',
+                    backdrop: 'static',
+                    controller:
+                        ['$scope', '$uibModalInstance',
+                        function($scope, $uibModalInstance) {
+                        $scope.matches = matches;
+                        $scope.ok = function(barcode) {
+                            $uibModalInstance.close();
+                            final_barcode = barcode;
+                        }
+                        $scope.cancel = function() {$uibModalInstance.dismiss()}
+                    }],
+                }).result.then(function() { return final_barcode });
+            })
+        });
+    }
+
     service.create_penalty = function(user_id) {
         return $uibModal.open({
             templateUrl: './circ/share/t_new_message_dialog',
+            backdrop: 'static',
             controller: 
                    ['$scope','$uibModalInstance','staffPenalties',
             function($scope , $uibModalInstance , staffPenalties) {
@@ -1571,6 +1943,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     service.edit_penalty = function(usr_penalty) {
         return $uibModal.open({
             templateUrl: './circ/share/t_new_message_dialog',
+            backdrop: 'static',
             controller: 
                    ['$scope','$uibModalInstance','staffPenalties',
             function($scope , $uibModalInstance , staffPenalties) {