initial Renew Items UI
authorBill Erickson <berick@esilibrary.com>
Mon, 30 Jun 2014 14:47:13 +0000 (10:47 -0400)
committerBill Erickson <berick@esilibrary.com>
Mon, 30 Jun 2014 14:47:13 +0000 (10:47 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
12 files changed:
Open-ILS/src/templates/staff/circ/checkin/index.tt2
Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2
Open-ILS/src/templates/staff/circ/renew/index.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/circ/renew/t_renew.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/circ/share/circ_strings.tt2
Open-ILS/src/templates/staff/css/circ.css.tt2
Open-ILS/src/templates/staff/css/style.css.tt2
Open-ILS/src/templates/staff/navbar.tt2
Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js
Open-ILS/web/js/ui/default/staff/circ/renew/app.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/circ/services/circ.js

index fea404b..ed53423 100644 (file)
 <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/circ.css" />
 [% END %]
 
-<!-- alerts row -->
-<div class="row pad-vert">
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    [% l('Checkin Items') %]
+  </div>
+</div>
+
+<div class="row">
   <div class="col-md-12">
     <div class="flex-row left-anchored">
       <div ng-if="is_backdate()" class="alert-danger pad-all-min">
@@ -55,7 +60,7 @@
 </div>
 
 <!-- checkin form -->
-<div class="row">
+<div class="row pad-vert">
   <div class="col-md-4">
     <form ng-submit="checkin(checkinArgs)" role="form" class="form-inline">
       <div class="input-group">
   </div>
 </div>
 
-<div class="row pad-vert" ng-if="fine_total">
+<div class="row" ng-if="fine_total">
   <div class="col-md-12">
     <span>[% l('Fine Tally:') %]</span>
     <span class="pad-horiz alert alert-danger">{{fine_total | currency}}</span>
index 3a855f6..6559ce7 100644 (file)
@@ -57,7 +57,6 @@
   <eg-grid-field label="[% l('Location') %]"    
     path='acp.location.name'></eg-grid-field>
 
-  <!-- FIXME: CATALOGING, HOLDS SHELF, acp LOCATION -->
   <eg-grid-field label="[% l('Route To') %]" path='route_to'>
   </eg-grid-field>
 
@@ -87,7 +86,5 @@
   <eg-grid-field path="au.*" parent-idl-class="au" hidden></eg-grid-field>
   <eg-grid-field path="transit.*" parent-idl-class="atc" hidden></eg-grid-field>
   <eg-grid-field path="hold.*" parent-idl-class="ahr" hidden></eg-grid-field>
-
-  <!-- TODO: add support for wildcard fields sans idl-class -->
 </eg-grid>
 
diff --git a/Open-ILS/src/templates/staff/circ/renew/index.tt2 b/Open-ILS/src/templates/staff/circ/renew/index.tt2
new file mode 100644 (file)
index 0000000..415556b
--- /dev/null
@@ -0,0 +1,20 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Renew"); 
+  ctx.page_app = "egRenewApp";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/circ.js"></script>
+[% INCLUDE 'staff/circ/share/circ_strings.tt2' %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/app.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/renew/app.js"></script>
+<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/circ.css" />
+[% END %]
+
+<div ng-view></div>
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2 b/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2
new file mode 100644 (file)
index 0000000..ea8efe5
--- /dev/null
@@ -0,0 +1,144 @@
+<!-- item renewal form / list -->
+
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    [% l('Renew Items') %]
+  </div>
+</div>
+
+<div class="row">
+  <div class="col-md-6">
+    <form ng-submit="renew(renewalArgs)" role="form" class="form-inline">
+      <div class="input-group">
+
+        <label class="input-group-addon" 
+          for="patron-renewal-barcode" >[% l('Barcode') %]</label>
+
+        <input focus-me="focusBarcode" class="form-control"
+          ng-model="renewalArgs.copy_barcode" 
+          id="patron-renewal-barcode" type="text"/> 
+
+        <input class="btn btn-default" type="submit" value="[% l('Submit') %]"/>
+      </div>
+    </form>
+  </div>
+  <div class="col-md-6">
+    <div class="flex-row">
+      <div class="flex-cell"></div>
+      <div class="checkbox pad-horiz">
+        <label>
+          <input type="checkbox" ng-model="renewalArgs.sticky_date"/>
+          [% l('Specific Due Date') %]
+        </label>
+      </div>
+      <!-- FIXME: This needs a time component as well, but type="datetime" 
+            is not yet supported by any browsers -->
+      <div><input eg-date-input class="form-control" ng-model="renewalArgs.due_date"/>
+      </div>
+    </div>
+  </div>
+</div>
+<hr/>
+
+<eg-grid
+  id-field="index"
+  features="-sort,-multisort"
+  main-label="[% l('Items Checked In') %]"
+  items-provider="gridDataProvider"
+  grid-controls="gridControls"
+  persist-key="circ.checkin">
+
+  <eg-grid-action 
+    handler="fetchLastCircPatron"
+    label="[% l('Retrieve Last Patron Who Circulated Item') %]">
+  </eg-grid-action>
+  <eg-grid-action 
+    handler="showBackdateDialog"
+    label="[% l('Backdate Post-Checkin') %]">
+  </eg-grid-action>
+  <eg-grid-action 
+    handler="showMarkDamaged"
+    label="[% l('Mark Items Damaged') %]">
+  </eg-grid-action>
+  <eg-grid-action 
+    handler="abortTransit"
+    label="[% l('Abort Transits') %]">
+  </eg-grid-action>
+
+  <eg-grid-field label="[% l('Alert Msg') %]"   
+    path="acp.alert_message"></eg-grid-field>
+
+  <eg-grid-field label="[% l('Balance Owed') %]"     
+    path='circ.billable_transaction.summary.balance_owed'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Barcode') %]" path="acp_barcode">
+    <!-- FIXME: ng-if / ng-disabled not working since the contents 
+        are $interpolate'd and not $compile'd.
+        I want to hide / disable the href when there is no acp ID 
+    -->
+    <a href="./cat/item/{{item.acp.id()}}/summary" target="_self">
+      {{item.copy_barcode}}
+    </a>
+  </eg-grid-field>
+
+  <eg-grid-field label="[% l('Bill #') %]"     
+    path='circ.id'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Call Number') %]" 
+    path="acn.label" hidden></eg-grid-field>
+
+  <eg-grid-field label="[% l('Due Date') %]"    
+    path='circ.due_date' dateformat='short' hidden></eg-grid-field>
+
+  <eg-grid-field label="[% l('Checkin Date') %]"    
+    path='circ.checkin_time' dateformat='short'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Family Name') %]"    
+    path='au.family_name'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Finish') %]"    
+    path='circ.stop_fines_time'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Location') %]" 
+    path='acp.location.name'> </eg-grid-field>
+
+  <eg-grid-field label="[% l('Remaining Renewals') %]" 
+    path='circ.renewal_remaining'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Start') %]" 
+    path='circ.xact_start'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Title') %]" path="title">
+    <a target="_self" href="[% ctx.base_path %]/opac/record/{{record.doc_id()}}">
+      {{item.title}}
+    </a>
+  </eg-grid-field>
+
+  <eg-grid-field label="[% l('Author') %]"      
+    path="author" hidden></eg-grid-field>
+
+  <eg-grid-field path="circ.*" parent-idl-class="circ" hidden></eg-grid-field>
+  <eg-grid-field path="acp.*" parent-idl-class="acp" hidden></eg-grid-field>
+  <eg-grid-field path="acn.*" parent-idl-class="acn" hidden></eg-grid-field>
+  <eg-grid-field path="record.*" parent-idl-class="mvr" hidden></eg-grid-field>
+  <eg-grid-field path="mbts.*" parent-idl-class="mbts" hidden></eg-grid-field>
+  <eg-grid-field path="au.*" parent-idl-class="au" hidden></eg-grid-field>
+</eg-grid>
+
+<div class="flex-row pad-vert">
+  <div class="flex-cell"></div>
+  <div class="checkbox">
+    <label>
+      <input ng-model="trim_list" type="checkbox"/>
+      [% l('Trim List (20 Rows)') %]
+    </label>
+  </div>
+  <div class="pad-horiz"></div>
+  <div class="checkbox">
+    <label>
+      <input ng-model="strict_barcode" type="checkbox"/>
+      [% l('Strict Barcode') %]
+    </label>
+  </div>
+</div>
+
index ca2b283..bccc9d4 100644 (file)
@@ -27,6 +27,7 @@ s.MARK_DAMAGED_CONFIRM = '[% l("Mark {{num_items}} items as DAMAGED?") %]';
 s.ABORT_TRANSIT_CONFIRM = '[% l("Abort {{num_transits}} transits?") %]';
 s.ROUTE_TO_HOLDS_SHELF = '[% l("Holds Shelf") %]';
 s.ROUTE_TO_CATALOGING = '[% l("Cataloging") %]';
+s.COPY_IN_TRANSIT = '[% l("Copy is In-Transit") %]';
 }]);
 </script>
 
index ca2966c..7bf4080 100644 (file)
@@ -10,6 +10,7 @@ but the ones I'm finding aren't quite cutting it..*/
 .patron-summary-act-link {font-size: .8em;}
 
 #patron-checkout-barcode,
+#patron-renewal-barcode,
 #patron-checkin-barcode { width: 16em; }
 
 #patron-search-form div.form-group {
index 63b06ca..ebfa1b1 100644 (file)
     padding-bottom: 10px;
 }
 
-table.list tr.selected td {
+table.list tr.selected td { /* deprecated? */
     color: #2a6496;
     background-color: #F5F5F5;
 }
@@ -155,9 +155,14 @@ table.list tr.selected td {
   width: 8em;
 }
 
-/* barcode inputs are everywhere.  Let's have a consistent style */
+/* barcode inputs are everywhere.  Let's have a consistent style.
+ * In most cases, form-control (etc.) CSS overrides this, so we
+ * still have to use id-based style. */
 .barcode { width: 16em; }
 
+/* bootstrap alerts are heavily padded.  use this to reduce */
+.alert-less-pad {padding: 5px;}
+
 /* ----------------------------------------------------------------------
  * Grid
  * ---------------------------------------------------------------------- */
index 0d1981d..61c4bf8 100644 (file)
             </a>
           </li>
           <li>
+            <a href="./circ/renew/renew" target="_self">
+              <span class="glyphicon glyphicon-refresh"></span>
+              [% l('Renew Items') %]
+            </a>
+          </li>
+          <li>
             <a href="./circ/patron/last" target="_self">
               <span class="glyphicon glyphicon-share-alt"></span>
               [% l('Retrieve Last Patron') %]
index f47ac93..366dd26 100644 (file)
@@ -154,34 +154,10 @@ function($scope , $q , $window , $location , egCore , checkinSvc , egGridDataPro
         egCirc.checkin(params, options).then(
         function(final_resp) {
 
-            var payload = final_resp.evt.payload;
-
-            if (payload) {
-                row_item.circ = payload.circ;
-                row_item.hold = payload.hold;
-                row_item.mbts = payload.circ ?
-                    payload.circ.billable_transaction().summary() : null;
-                row_item.record = payload.record;
-                row_item.acp = payload.copy;
-                row_item.acn = payload.volume ? 
-                    payload.volume : payload.copy.call_number();
-                row_item.au = payload.patron;
-                row_item.transit = payload.transit;
-                row_item.status = payload.status;
-                row_item.message = payload.message;
-                row_item.title = final_resp.evt.title;
-                row_item.author = final_resp.evt.author;
-                row_item.isbn = final_resp.evt.isbn;
-                row_item.route_to = final_resp.evt.route_to;
-            }
-
-            if (!row_item.route_to) {
-                if (row_item.transit) {
-                    row_item.route_to = row_item.transit.dest().shortname();
-                } else if (row_item.acp) {
-                    row_item.route_to = row_item.acp.location().name();
-                }
-            }
+            row_item.evt = final_resp.evt;
+            angular.forEach(final_resp.data, function(val, key) {
+                row_item[key] = val;
+            });
 
             if (row_item.mbts) {
                 var amt = Number(row_item.mbts.balance_owed());
index ffa4347..94314a2 100644 (file)
@@ -115,6 +115,8 @@ function($scope , $q , $modal , $routeParams , egCore , egUser , patronSvc ,
                 // update stats locally so we don't have to fetch them w/
                 // each checkout.
                 patronSvc.patron_stats.checkouts.out++
+
+                // TODO: munge from egCirc
     
                 munge_checkout_resp(co_resp);
     
diff --git a/Open-ILS/web/js/ui/default/staff/circ/renew/app.js b/Open-ILS/web/js/ui/default/staff/circ/renew/app.js
new file mode 100644 (file)
index 0000000..5a960bd
--- /dev/null
@@ -0,0 +1,159 @@
+/**
+ * Renewal
+ */
+
+angular.module('egRenewApp', 
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
+    var resolver = {delay : function(egStartup) {return egStartup.go()}};
+
+    $routeProvider.when('/circ/renew/renew', {
+        templateUrl: './circ/renew/t_renew',
+        controller: 'RenewCtrl',
+        resolve : resolver
+    });
+
+    $routeProvider.when('/circ/renew/renew', {
+        templateUrl: './circ/renew/t_renew',
+        controller: 'RenewCtrl',
+        resolve : resolver
+    });
+    
+    $routeProvider.otherwise({redirectTo : '/circ/renew/renew'});
+})
+
+
+
+
+.controller('RenewCtrl',
+
+       ['$scope','egCore','egGridDataProvider','egCirc',
+
+function($scope , egCore , egGridDataProvider , egCirc) {
+
+    $scope.focusBarcode = true;
+    $scope.renewals = [];
+
+    var today = new Date();
+    $scope.renewalsArgs = {due_date : today};
+
+    $scope.gridDataProvider = egGridDataProvider.instance({
+        get : function(offset, count) {
+            return this.arrayNotifier($scope.renewals, offset, count);
+        }
+    });
+
+    // avoid multiple, in-flight attempts on the same barcode
+    var pending_barcodes = {};
+
+    $scope.renew = function(args) {
+        var params = angular.copy(args);
+
+        if (args.sticky_date) {
+            params.due_date = args.due_date.toISOString();
+        } else {
+            delete params.due_date;
+        }
+        delete params.sticky_date;
+         if (!args.copy_barcode) return;
+
+        args.copy_barcode = ''; // reset UI input
+
+        if (pending_barcodes[params.copy_barcode]) {
+            console.log(
+                "Skipping renewals of redundant barcode " 
+                + params.copy_barcode
+            );
+            return;
+        }
+
+        pending_barcodes[params.copy_barcode] = true;
+        send_renewal(params);
+
+        $scope.focusBarcode = true; // return focus to barcode input
+    }
+
+    function send_renewal(params) {
+
+        params.noncat_type = params.noncat ? params.noncat_type : '';
+
+        // populate the grid row before we send the request so that the
+        // order of actions is maintained and so the user gets an 
+        // immediate reaction to their barcode input action.
+        var row_item = {
+            index : $scope.renewals.length,
+            copy_barcode : params.copy_barcode,
+            noncat_type : params.noncat_type
+        };
+
+        $scope.renewals.unshift(row_item);
+        $scope.gridDataProvider.refresh();
+
+        var options = {check_barcode : $scope.strict_barcode};
+
+        egCirc.renew(params, options).then(
+            function(final_resp) {
+
+                row_item.evt = final_resp.evt;
+                angular.forEach(final_resp.data, function(val, key) {
+                    row_item[key] = val;
+                });
+
+                if (row_item.mbts) {
+                    var amt = Number(row_item.mbts.balance_owed());
+                    if (amt != 0) {
+                        $scope.billable_barcode = row_item.copy_barcode;
+                        $scope.billable_amount = amt;
+                        $scope.fine_total = 
+                            ($scope.fine_total * 100 + amt * 100) / 100;
+                    }
+                }
+
+                if ($scope.trim_list && checkinSvc.checkins.length > 20)
+                    checkinSvc.checkins = checkinSvc.checkins.splice(0, 20);
+
+            },
+            function() {
+                // Circ was rejected somewhere along the way.
+                // Remove the copy from the grid since there was no action.
+                // note: since renewals are unshifted onto the array, the
+                // index value does not (generally) match the array position.
+                var pos = -1;
+                angular.forEach($scope.renewals, function(co, idx) {
+                    if (co.index == row_item.index) pos = idx;
+                });
+                $scope.renewals.splice(pos, 1);
+                $scope.gridDataProvider.refresh();
+            }
+
+        )['finally'](function() {
+
+            // regardless of the outcome of the circ, remove the 
+            // barcode from the pending list.
+            if (params.copy_barcode)
+                delete pending_barcodes[params.copy_barcode];
+        });
+    }
+
+    $scope.print_receipt = function() {
+        var print_data = {circulations : []}
+
+        if ($scope.renewals.length == 0) return $q.when();
+
+        angular.forEach($srenewalpe.renewals, function(co) {
+            var circ = egCore.idl.toHash(renewal.payload.circ);
+            circ.title = renewal.payload.record.title;
+            print_data.circulations.push(circ);
+        });
+
+        return egCore.print.print({
+            context : 'default', 
+            template : 'renew', 
+            scope : print_data,
+        });
+    }
+}])
+
index 902eef7..e3f1be3 100644 (file)
@@ -45,6 +45,25 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         'PATRON_EXCEEDS_FINES'
     ]
 
+
+    // overridable during renewal
+    service.renew_overridable_events = [
+        'PATRON_EXCEEDS_OVERDUE_COUNT',
+        'PATRON_EXCEEDS_LOST_COUNT',
+        'PATRON_EXCEEDS_CHECKOUT_COUNT',
+        'PATRON_EXCEEDS_FINES',
+        'CIRC_EXCEEDS_COPY_RANGE',
+        'ITEM_DEPOSIT_REQUIRED',
+        'ITEM_RENTAL_FEE_REQUIRED',
+        'ITEM_DEPOSIT_PAID',
+        'COPY_CIRC_NOT_ALLOWED',
+        'COPY_IS_REFERENCE',
+        'COPY_ALERT_MESSAGE',
+        'COPY_NEEDED_FOR_HOLD',
+        'MAX_RENEWALS_REACHED',
+        'CIRC_CLAIMS_RETURNED'
+    ];
+
     // these checkin events do not produce alerts when 
     // options.suppress_alerts is in effect.
     service.checkin_suppress_overrides = [
@@ -102,6 +121,9 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                 .then(function() {
                     return service.handle_checkout_resp(evt, params, options);
                 })
+                .then(function(final_resp) {
+                    return service.munge_resp_data(final_resp)
+                })
             });
         });
     }
@@ -116,21 +138,33 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
         console.debug('egCirc.renew() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
 
-        var method = 'open-ils.circ.renew';
-        if (options.override) method += '.override';
+        var promise = options.check_barcode ? 
+            service.test_barcode(params.copy_barcode) : $q.when();
 
-        return egCore.net.request(
-            'open-ils.circ', method, egCore.auth.token(), params
+        // avoid re-check on override, etc.
+        delete options.check_barcode;
+
+        return promise.then(function() {
 
-        ).then(function(evt) {
+            var method = 'open-ils.circ.renew';
+            if (options.override) method += '.override';
+
+            return egCore.net.request(
+                'open-ils.circ', method, egCore.auth.token(), params
 
-            if (angular.isArray(evt)) evt = evt[0];
+            ).then(function(evt) {
+
+                if (angular.isArray(evt)) evt = evt[0];
 
-            return service.flesh_response_data(
-                'renew', evt, params, options)
-            .then(function() {
-                return service.handle_checkout_resp(evt, params, options);
-            })
+                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)
+                })
+            });
         });
     }
 
@@ -166,10 +200,50 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                 .then(function() {
                     return service.handle_checkin_resp(evt, params, options);
                 })
+                .then(function(final_resp) {
+                    return service.munge_resp_data(final_resp)
+                })
             });
         });
     }
 
+    // provide consistent formatting of the final response data
+    service.munge_resp_data = function(final_resp) {
+        var data = final_resp.data = {};
+
+        if (!final_resp.evt) return;
+
+        var payload = final_resp.evt.payload;
+        if (!payload) return;
+
+        data.circ = payload.circ;
+        data.hold = payload.hold;
+        data.record = payload.record;
+        data.acp = payload.copy;
+        data.acn = payload.volume ?  payload.volume : payload.copy.call_number();
+        data.au = payload.patron;
+        data.transit = payload.transit;
+        data.status = payload.status;
+        data.message = payload.message;
+        data.title = final_resp.evt.title;
+        data.author = final_resp.evt.author;
+        data.isbn = final_resp.evt.isbn;
+        data.route_to = final_resp.evt.route_to;
+
+        if (payload.circ && payload.circ.billable_transaction())
+            data.mbts = payload.circ.billable_transaction().summary();
+
+        if (!data.route_to) {
+            if (data.transit) {
+                data.route_to = data.transit.dest().shortname();
+            } else if (data.acp) {
+                data.route_to = data.acp.location().name();
+            }
+        }
+
+        return final_resp;
+    }
+
     service.handle_overridable_checkout_event = function(evt, params, options) {
 
         if (options.override) {
@@ -198,10 +272,42 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
             case 'COPY_ALERT_MESSAGE':
                 return service.copy_alert_dialog(evt, params, options, 'checkout');
             default: 
-                return service.override_dialog(evt, params, options, 'checkin');
+                return service.override_dialog(evt, params, options, 'checkout');
         }
     }
 
+    service.handle_overridable_renew_event = function(evt, params, options) {
+
+        if (options.override) {
+            // override attempt already made and failed.
+            // NOTE: I don't think we'll ever get here, since the
+            // override attempt should produce a perm failure...
+            console.debug('override failed: ' + evt.textcode);
+            return $q.reject();
+
+        } 
+
+        // renewal auto-overrides are the same as checkout
+        if (service.auto_override_checkout_events[evt.textcode]) {
+            // user has already opted to override this type
+            // of event.  Re-run the renew w/ override.
+            options.override = true;
+            return service.renew(params, options);
+        } 
+
+        // Ask the user if they would like to override this event.
+        // Some events offer a stock override dialog, while others
+        // require additional context.
+
+        switch(evt.textcode) {
+            case 'COPY_ALERT_MESSAGE':
+                return service.copy_alert_dialog(evt, params, options, 'renew');
+            default: 
+                return service.override_dialog(evt, params, options, 'renew');
+        }
+    }
+
+
     service.handle_overridable_checkin_event = function(evt, params, options) {
 
         if (options.override) {
@@ -233,6 +339,50 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
     }
 
 
+    service.handle_renew_resp = function(evt, params, options) {
+
+        var final_resp = {evt : evt, params : params, options : options};
+
+        // track the barcode regardless of whether it refers to a copy
+        evt.copy_barcode = params.copy_barcode;
+
+        // Overridable Events
+        if (service.renew_overridable_events.indexOf(evt.textcode) > -1) 
+            return service.handle_overridable_renew_event(evt, params, options);
+
+        // Other events
+        switch (evt.textcode) {
+            case 'SUCCESS':
+                return $q.when(final_resp);
+
+            case 'COPY_IN_TRANSIT':
+            case 'PATRON_CARD_INACTIVE':
+            case 'PATRON_INACTIVE':
+            case 'PATRON_ACCOUNT_EXPIRED':
+            case 'CIRC_CLAIMS_RETURNED':
+                return service.exit_alert(
+                    egCore.strings[evt.textcode],
+                    {barcode : params.copy_barcode}
+                );
+
+            case 'PERM_FAILURE':
+                return service.exit_alert(
+                    egCore.strings[evt.textcode],
+                    {permission : evt.ilsperm}
+                );
+
+            default:
+                return service.exit_alert(
+                    egCore.strings.CHECKOUT_FAILED_GENERIC, {
+                        barcode : params.copy_barcode,
+                        textcode : evt.textcode,
+                        desc : evt.desc
+                    }
+                );
+        }
+    }
+
+
     service.handle_checkout_resp = function(evt, params, options) {
 
         var final_resp = {evt : evt, params : params, options : options};
@@ -421,7 +571,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog) {
                 if (service.checkout_auto_override_after_first.indexOf(evt.textcode) > -1)
                     service.auto_override_checkout_events[evt.textcode] = true;
 
-                return service.checkout(params, options);
+                return service[action](params, options);
             }
         );
     }