<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">
</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>
<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>
<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>
--- /dev/null
+[%
+ 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 %]
--- /dev/null
+<!-- 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>
+
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>
.patron-summary-act-link {font-size: .8em;}
#patron-checkout-barcode,
+#patron-renewal-barcode,
#patron-checkin-barcode { width: 16em; }
#patron-search-form div.form-group {
padding-bottom: 10px;
}
-table.list tr.selected td {
+table.list tr.selected td { /* deprecated? */
color: #2a6496;
background-color: #F5F5F5;
}
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
* ---------------------------------------------------------------------- */
</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') %]
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());
// 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);
--- /dev/null
+/**
+ * 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,
+ });
+ }
+}])
+
'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 = [
.then(function() {
return service.handle_checkout_resp(evt, params, options);
})
+ .then(function(final_resp) {
+ return service.munge_resp_data(final_resp)
+ })
});
});
}
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)
+ })
+ });
});
}
.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) {
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) {
}
+ 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};
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);
}
);
}