From 60fb007312c9cd3b88f4be9af3c68621a6df5a22 Mon Sep 17 00:00:00 2001 From: Jason Etheridge Date: Tue, 9 Feb 2021 09:42:06 -0500 Subject: [PATCH] lp1908439 Auto-override enhancment This reworks the override action dialogs in the patron display for Check Out and Items Out, and in the Circulation -> Renew Items interface. It exposes the auto-override behavior as checkboxes giving staff more fine grained control over which events are auto-forced or skipped upon subsequent encounters. It also changes the Cancel action for batch renewals to abort the remaining renewals in the batch, and makes it so that new authorization credentials provided during such a batch will be treated as an operator change for the entire batch. We also fix an existing bug where events marked as already encountered for auto-override could leak into other patron contexts via Patron Search. Signed-off-by: Jason Etheridge Signed-off-by: Terran McCanna Signed-off-by: Mike Rylander --- Open-ILS/src/templates/staff/base_js.tt2 | 1 + .../staff/circ/share/t_event_override_dialog.tt2 | 34 ++-- .../web/js/ui/default/staff/circ/patron/app.js | 8 +- .../js/ui/default/staff/circ/patron/items_out.js | 28 ++- .../web/js/ui/default/staff/circ/services/circ.js | 214 ++++++++++++--------- .../web/js/ui/default/staff/services/op_change.js | 20 +- .../Circulation/override-dialogs.adoc | 3 + 7 files changed, 199 insertions(+), 109 deletions(-) create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/override-dialogs.adoc diff --git a/Open-ILS/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2 index 53e6f5f9cd..f0df81e6d7 100644 --- a/Open-ILS/src/templates/staff/base_js.tt2 +++ b/Open-ILS/src/templates/staff/base_js.tt2 @@ -136,6 +136,7 @@ UpUp.start({ s.OP_CHANGE_PERM_MESSAGE = "[% l('Another staff member with the above permission may authorize this specific action. Please notify your library administrator if you need this permission. If you feel you have received this exception in error, please inform your friendly Evergreen developers or helpdesk staff of the above permission.') %]"; s.PERM_OP_CHANGE_SUCCESS = "[% l('Permission Override Login Succeeded') %]"; s.PERM_OP_CHANGE_FAILURE = "[% l('Permission Override Login Failed') %]"; + s.PERM_OP_CHANGE_PERSIST = "[% l('Re-Using Permission Override Login in Batch') %]"; s.OPT_IN_DIALOG_TITLE = "[% l('Verify Permission to Share Personal Information') %]"; s.OPT_IN_DIALOG = "[% l('Does patron [_1], [_2] from [_3] ([_4]) consent to having their personal information shared with your library?', '{{family_name}}', '{{first_given_name}}', '{{org_name}}', '{{org_shortname}}') %]"; s.OPT_IN_RESTRICTED = "[% l("This patron's record is not viewable at your library.") %]"; diff --git a/Open-ILS/src/templates/staff/circ/share/t_event_override_dialog.tt2 b/Open-ILS/src/templates/staff/circ/share/t_event_override_dialog.tt2 index 398796f20c..f3f13179f2 100644 --- a/Open-ILS/src/templates/staff/circ/share/t_event_override_dialog.tt2 +++ b/Open-ILS/src/templates/staff/circ/share/t_event_override_dialog.tt2 @@ -2,35 +2,45 @@ diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js index 736c938bb6..1cfcba6480 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js +++ b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js @@ -225,8 +225,8 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod', * * */ .controller('PatronCtrl', - ['$scope','$q','$location','$filter','egCore','egNet','egUser','egAlertDialog','egConfirmDialog','egPromptDialog','patronSvc', -function($scope, $q , $location , $filter , egCore , egNet , egUser , egAlertDialog , egConfirmDialog , egPromptDialog , patronSvc) { + ['$scope','$q','$location','$filter','egCore','egNet','egUser','egAlertDialog','egConfirmDialog','egPromptDialog','patronSvc','egCirc', +function($scope, $q , $location , $filter , egCore , egNet , egUser , egAlertDialog , egConfirmDialog , egPromptDialog , patronSvc , egCirc) { $scope.is_patron_edit = function() { return Boolean($location.path().match(/patron\/\d+\/edit$/)); @@ -283,6 +283,10 @@ function($scope, $q , $location , $filter , egCore , egNet , egUser , egAlertDi $scope.aous = egCore.env.aous; $scope.auth_user_id = egCore.auth.user().id(); + if (tab == 'search') { + egCirc.reset(); // clear out auto-override and auto-skip selections when switching patrons + } + if (patron_id) { $scope.patron_id = patron_id; return patronSvc.setPrimary($scope.patron_id) diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/items_out.js b/Open-ILS/web/js/ui/default/staff/circ/patron/items_out.js index d8cc9dad9f..16c04c9d62 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/patron/items_out.js +++ b/Open-ILS/web/js/ui/default/staff/circ/patron/items_out.js @@ -493,11 +493,35 @@ function($scope , $q , $routeParams , $timeout , egCore , egUser , patronSvc , return egConfirmDialog.open(msg, barcodes.join(' '), {}).result .then(function() { + window.oils_cancel_batch = false; + window.oils_inside_batch = true; + function batch_cleanup() { + if (window.oils_inside_batch && window.oils_op_change_within_batch) { + window.oils_op_change_undo_func(); + } + window.oils_inside_batch = false; + window.oils_op_change_within_batch = false; + reset_page(); + } function do_one() { var bc = barcodes.pop(); - if (!bc) { reset_page(); return } + if (!bc) { + batch_cleanup(); + return; + } + if (window.oils_op_change_within_batch) { + window.oils_op_change_toast_func(); + } // finally -> continue even when one fails - egCirc.renew({copy_barcode : bc}).finally(do_one); + egCirc.renew({copy_barcode : bc}).finally(function() { + if (!window.oils_cancel_batch) { + do_one(); + } else { + console.log('batch cancelled'); + batch_cleanup(); + return; + } + }); } do_one(); }); diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js index 6f22aa8cc1..975e3ce81e 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js +++ b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js @@ -13,7 +13,9 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl var service = { // auto-override these events after the first override - auto_override_checkout_events : {}, + auto_override_circ_events : {}, + // auto-skip these events after the first skip + auto_skip_circ_events : {}, require_initials : false, never_auto_print : { hold_shelf_slip : false, @@ -46,37 +48,23 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl }); service.reset = function() { - service.auto_override_checkout_events = {}; + service.auto_override_circ_events = {}; + service.auto_skip_circ_events = {}; } - // these events can be overridden by staff during checkout - service.checkout_overridable_events = [ - 'PATRON_EXCEEDS_OVERDUE_COUNT', - 'PATRON_EXCEEDS_CHECKOUT_COUNT', - 'PATRON_EXCEEDS_FINES', - 'PATRON_EXCEEDS_LONGOVERDUE_COUNT', - 'PATRON_BARRED', - 'CIRC_EXCEEDS_COPY_RANGE', - 'ITEM_DEPOSIT_REQUIRED', - 'ITEM_RENTAL_FEE_REQUIRED', - 'PATRON_EXCEEDS_LOST_COUNT', - 'COPY_CIRC_NOT_ALLOWED', - 'COPY_NOT_AVAILABLE', - 'COPY_IS_REFERENCE', - 'COPY_ALERT_MESSAGE', - 'ITEM_ON_HOLDS_SHELF', - 'STAFF_C', - 'STAFF_CH', - 'STAFF_CHR', - 'STAFF_CR', - 'STAFF_H', - 'STAFF_HR', - 'STAFF_R' + // these events cannot be overriden + service.nonoverridable_events = [ + 'ACTION_CIRCULATION_NOT_FOUND', + 'ACTOR_USER_NOT_FOUND', + 'ASSET_COPY_NOT_FOUND', + 'PATRON_INACTIVE', + 'PATRON_CARD_INACTIVE', + 'PATRON_ACCOUNT_EXPIRED', + 'PERM_FAILURE' // should be handled elsewhere ] - // after the first override of any of these events, - // auto-override them in subsequent calls. - service.checkout_auto_override_after_first = [ + // Default to checked for "Automatically override for subsequent items?" + service.default_auto_override = [ 'PATRON_EXCEEDS_OVERDUE_COUNT', 'PATRON_BARRED', 'PATRON_EXCEEDS_LOST_COUNT', @@ -85,34 +73,6 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl 'PATRON_EXCEEDS_LONGOVERDUE_COUNT' ] - - // overridable during renewal - service.renew_overridable_events = [ - 'PATRON_EXCEEDS_OVERDUE_COUNT', - 'PATRON_EXCEEDS_LOST_COUNT', - 'PATRON_EXCEEDS_CHECKOUT_COUNT', - 'PATRON_EXCEEDS_FINES', - 'PATRON_EXCEEDS_LONGOVERDUE_COUNT', - 'CIRC_EXCEEDS_COPY_RANGE', - 'ITEM_DEPOSIT_REQUIRED', - 'ITEM_RENTAL_FEE_REQUIRED', - 'ITEM_DEPOSIT_PAID', - 'COPY_CIRC_NOT_ALLOWED', - 'COPY_NOT_AVAILABLE', - 'COPY_IS_REFERENCE', - 'COPY_ALERT_MESSAGE', - 'COPY_NEEDED_FOR_HOLD', - 'MAX_RENEWALS_REACHED', - 'CIRC_CLAIMS_RETURNED', - 'STAFF_C', - 'STAFF_CH', - 'STAFF_CHR', - 'STAFF_CR', - 'STAFF_H', - 'STAFF_HR', - 'STAFF_R' - ]; - // these checkin events do not produce alerts when // options.suppress_alerts is in effect. service.checkin_suppress_overrides = [ @@ -399,7 +359,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl } - if (evt.filter(function(e){return !service.auto_override_checkout_events[e.textcode];}).length == 0) { + if (evt.filter(function(e){return !service.auto_override_circ_events[e.textcode];}).length == 0) { // user has already opted to override these type // of events. Re-run the checkout w/ override. options.override = true; @@ -432,7 +392,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl } // renewal auto-overrides are the same as checkout - if (evt.filter(function(e){return !service.auto_override_checkout_events[e.textcode];}).length == 0) { + if (evt.filter(function(e){return !service.auto_override_circ_events[e.textcode];}).length == 0) { // user has already opted to override these type // of events. Re-run the renew w/ override. options.override = true; @@ -492,26 +452,30 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl // track the barcode regardless of whether it refers to a copy angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; }); - // Overridable Events - if (evt.filter(function(e){return service.renew_overridable_events.indexOf(e.textcode) > -1;}).length > 0) + // test for success first to simplify things + if (evt[0].textcode == 'SUCCESS') { + egCore.audio.play('info.renew'); + return $q.when(final_resp); + } + + // handle Overridable and Non-Overridable Events, but only if no skipped non-overridable events + if (evt.filter(function(e){return service.auto_skip_circ_events[e.textcode];}).length == 0) { return service.handle_overridable_renew_event(evt, params, options); + } // Other events switch (evt[0].textcode) { - case 'SUCCESS': - egCore.audio.play('info.renew'); - return $q.when(final_resp); - case 'COPY_IN_TRANSIT': case 'PATRON_CARD_INACTIVE': case 'PATRON_INACTIVE': case 'PATRON_ACCOUNT_EXPIRED': case 'CIRC_CLAIMS_RETURNED': + case 'ITEM_NOT_CATALOGED': + case 'ASSET_COPY_NOT_FOUND': + // since handle_overridable_renew_event essentially advertises these events at some point, + // we no longer need the original alerts; however, the sound effects are still nice. egCore.audio.play('warning.renew'); - return service.exit_alert( - egCore.strings[evt[0].textcode], - {barcode : params.copy_barcode} - ); + return $q.reject(); default: egCore.audio.play('warning.renew.unknown'); @@ -533,16 +497,14 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl // track the barcode regardless of whether it refers to a copy angular.forEach(evt, function(e){ e.copy_barcode = params.copy_barcode; }); - // Overridable Events - if (evt.filter(function(e){return service.checkout_overridable_events.indexOf(e.textcode) > -1;}).length > 0) - return service.handle_overridable_checkout_event(evt, params, options); + // test for success first to simplify things + if (evt[0].textcode == 'SUCCESS') { + egCore.audio.play('success.checkout'); + return $q.when(final_resp); + } - // Other events + // other events that should precede generic overridable/non-overridable handling switch (evt[0].textcode) { - case 'SUCCESS': - egCore.audio.play('success.checkout'); - return $q.when(final_resp); - case 'ITEM_NOT_CATALOGED': egCore.audio.play('error.checkout.no_cataloged'); return service.precat_dialog(params, options); @@ -555,16 +517,25 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl case 'COPY_IN_TRANSIT': egCore.audio.play('warning.checkout.in_transit'); return service.copy_in_transit_dialog(evt, params, options); + } + + // handle Overridable and Non-Overridable Events, but only if no skipped non-overridable events + if (evt.filter(function(e){return service.auto_skip_circ_events[e.textcode];}).length == 0) { + return service.handle_overridable_checkout_event(evt, params, options); + } + // Other events + switch (evt[0].textcode) { case 'PATRON_CARD_INACTIVE': case 'PATRON_INACTIVE': case 'PATRON_ACCOUNT_EXPIRED': case 'CIRC_CLAIMS_RETURNED': + case 'ITEM_NOT_CATALOGED': + case 'ASSET_COPY_NOT_FOUND': + // since handle_overridable_checkout_event essentially advertises these events at some point, + // we no longer need the original alerts; however, the sound effects are still nice. egCore.audio.play('warning.checkout'); - return service.exit_alert( - egCore.strings[evt[0].textcode], - {barcode : params.copy_barcode} - ); + return $q.reject(); default: egCore.audio.play('error.checkout.unknown'); @@ -779,6 +750,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) { $scope.events = evt; + $scope.action = action; // Find the event, if any, that is for ITEM_ON_HOLDS_SHELF // and grab the patron name of the owner. @@ -797,18 +769,77 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl $scope.patronID = $scope.holdEvent.payload.patron_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 // 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.formdata = { + clearHold : service.clearHold, + nonoverridable: evt.filter(function(e){ + return service.nonoverridable_events.indexOf(e.textcode) > -1;}).length > 0, + event_ui_data : Object.fromEntries( + evt.map( e => [ e.ilsevent, { + // non-overridable events will be rare, but they are skippable. We use + // the same checkbox variable to track desired skip and auto-override + // selections. + overridable: service.nonoverridable_events.indexOf(e.textcode) == -1, + // for non-overridable events, we'll default the checkbox to any previous + // choice made for the current patron, though normally the UI will be + // suppressed unless some previously unencountered events are in the set + checkbox: service.nonoverridable_events.indexOf(e.textcode) > -1 + ? (service.auto_skip_circ_events[e.textcode] == undefined + ? false + : service.auto_skip_circ_events[e.textcode] + ) + // if a given event is overridable, said checkbox will default to any previous + // choice made for the current patron, as long as there are no non-overridable + // events in the set (because we'll disable the checkbox in that case and don't + // want to imply that we're going to set an auto-override) + : (service.auto_override_circ_events[e.textcode] == undefined + ? ( + service.nonoverridable_events.indexOf(e.textcode) > -1 + ? false + : service.default_auto_override.indexOf(e.textcode) > -1 + ) + : service.auto_override_circ_events[e.textcode] + ) + }]) + ) + }; + + function update_auto_override_and_skip_lists() { + angular.forEach(evt, function(e){ + if ($scope.formdata.nonoverridable) { + // the action had at least one non-overridable event, so let's only + // record skip choices for those + if (!$scope.formdata.event_ui_data[e.ilsevent].overridable) { + if ($scope.formdata.event_ui_data[e.ilsevent].checkbox) { + // grow the skip list + service.auto_skip_circ_events[e.textcode] = true; + } else { + // shrink the skip list + service.auto_skip_circ_events[e.textcode] = false; + } + } + } else { + // record all auto-override choices + if ($scope.formdata.event_ui_data[e.ilsevent].checkbox) { + // grow the auto-override list + service.auto_override_circ_events[e.textcode] = true; + } else { + // shrink the auto-override list + service.auto_override_circ_events[e.textcode] = false; + } + } + }); + // for debugging + window.oils_auto_skip_circ_events = service.auto_skip_circ_events; + window.oils_auto_override_circ_events = service.auto_override_circ_events; + } $scope.ok = function() { + update_auto_override_and_skip_lists(); // Handle the cancellation of the assciated hold here if ($scope.formdata.clearHold && $scope.holdID) { egCore.net.request( @@ -822,7 +853,14 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl $uibModalInstance.close(); } + $scope.skip = function($event) { + update_auto_override_and_skip_lists(); + $uibModalInstance.dismiss(); + $event.preventDefault(); + } + $scope.cancel = function ($event) { + window.oils_cancel_batch = true; $uibModalInstance.dismiss(); $event.preventDefault(); } @@ -839,12 +877,6 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog, egAddCopyAl return service.checkin(params, options); } - // checkout/renew support override-after-first - angular.forEach(evt, function(e){ - if (service.checkout_auto_override_after_first.indexOf(e.textcode) > -1) - service.auto_override_checkout_events[e.textcode] = true; - }); - return service[action](params, options); } ); diff --git a/Open-ILS/web/js/ui/default/staff/services/op_change.js b/Open-ILS/web/js/ui/default/staff/services/op_change.js index 5d06d71e96..172e81d38e 100644 --- a/Open-ILS/web/js/ui/default/staff/services/op_change.js +++ b/Open-ILS/web/js/ui/default/staff/services/op_change.js @@ -97,8 +97,24 @@ function($uibModal, $interpolate, $rootScope, $q, egAuth, egStrings, egNet, ngTo )['finally'](function() { // always undo the operator change after a perm override. - console.debug("clearing op-change after perm override redo"); - service.changeOperatorUndo(true); + // well, unless inside a UI "batch" like Renew All + if (!window.oils_inside_batch) { + console.debug("clearing op-change after perm override redo"); + window.oils_op_change_within_batch = false; + service.changeOperatorUndo(true); + } else { + // up to the batch caller to call changeOperatorUndo + console.debug("persisting op-change after perm override redo"); + window.oils_op_change_within_batch = true; + // this is an even kludgier use of window-scoped variables + window.oils_op_change_undo_func = function() { + console.debug("clearing op-change after perm override redo"); + service.changeOperatorUndo(true); + } + window.oils_op_change_toast_func = function() { + ngToast.create(egStrings.PERM_OP_CHANGE_PERSIST); + } + } }); }); } diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/override-dialogs.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/override-dialogs.adoc new file mode 100644 index 0000000000..0e13967a55 --- /dev/null +++ b/docs/RELEASE_NOTES_NEXT/Circulation/override-dialogs.adoc @@ -0,0 +1,3 @@ +== Override Dialogs == + +This reworks the override action dialogs in the patron display for Check Out and Items Out, and in the Circulation -> Renew Items interface. It exposes the auto-override behavior as checkboxes giving staff more fine grained control over which events are auto-forced or skipped upon subsequent encounters. It also changes the Cancel action for batch renewals to abort the remaining renewals in the batch, and makes it so that new authorization credentials provided during such a batch will be treated as an operator change for the entire batch. We also fix an existing bug where events marked as already encountered for auto-override could leak into other patron contexts via Patron Search. -- 2.11.0