porting items out and checkout UI to eg-grid.
move items out and checkout to standalone js files
streamline items out to use streaming PCRUD request instead of
call/response (yay, websockets)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
<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/patron/app.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/checkout.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/patron/items_out.js"></script>
<!-- TODO: APP_JS should really be called APP_ADDONS or some such.
It just means "load these things, too, and load them last" -->
</div>
</form>
</div>
-
- <div class="col-md-1 col-md-offset-5 text-right">
- <div class="btn-group text-left">
- [% INCLUDE 'staff/parts/column_picker.tt2' listname='checkouts' %]
- </div>
- </div>
</div>
-[% INCLUDE 'staff/circ/patron/t_checkout_table.tt2' %]
+<eg-grid
+ id-field="id"
+ features="-display,-sort,-multisort"
+ main-label="[% l('Checkouts') %]"
+ items-provider="gridDataProvider"
+ persist-key="eg.staff.circ.patron.checkout">
+ <eg-grid-field label="[% ('Barcode') %]" path='copy_barcode' visible></eg-grid-field>
+ <eg-grid-field label="[% ('Circ ID') %]" path='payload.circ.id' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Due Date') %]" path='payload.circ.due_date' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Response') %]" path='textcode' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Title') %]" path='payload.record.title' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Author') %]" path='payload.record.author' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Call Number') %]" path='payload.copy.call_number.label' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Alert Msg') %]" path='payload.copy.alert_message' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Noncat #') %]" path='noncat_count' visible></eg-grid-field>
+</eg-grid>
+++ /dev/null
-
-[%
-# checkout table columns
-COLUMNS = [
-{label => l('Barcode'), name => 'copy_barcode' display => 1},
-{label => l('Circ ID'), name => 'payload.circ.id', display => 1},
-{label => l('Due Date'), name => 'payload.circ.due_date' display => 1},
-# once we are handling all response types, we probably don't need to show
-# Response. Or, at least, make it more friendly / localizable
-{label => l('Response'), name => 'textcode', display => 1},
-{label => l('Title'), name => 'payload.record.title', display => 1},
-{label => l('Author'), name => 'payload.record.author', display => 1},
-{label => l('Call Number'),name => 'payload.copy.call_number.label', display => 1},
-{label => l('Alert Msg'), name => 'payload.copy.alert_message', display => 1},
-{label => l('Noncat #'), name => 'noncat_count' display => 1},
-]
-%]
-
-<!-- tell JS about our columns so they can be dynamically managed -->
-<div ng-init="
-checkouts.setColumns([
-[%- FOR col IN COLUMNS %]
-{label:'[% col.label %]',name:'[% col.name %]'[% IF col.display %],display:true[% END %]}[% IF !loop.last; ','; END -%]
-[% END %]
-])">
-</div>
-
-<div class="row pad-vert">
- <div class="col-md-12">
- <table class="table table-hover table-condensed table-striped">
- <thead>
- <tr>
- <th>#</th>
- <th ng-repeat="col in checkouts.allColumns"
- ng-show="checkouts.displayColumns[col.name]">
- {{col.label}}
- </th>
- </tr>
- </thead>
- <tbody>
- <tr ng-repeat="checkout in checkouts.items | reverse track by $index">
- <td>{{checkouts.count() - $index}}</td>
- <td ng-repeat="col in checkouts.allColumns"
- ng-show="checkouts.displayColumns[col.name]">
- {{checkouts.fieldValue(checkout, col.name)}}
- </td>
- </tr>
- </tbody>
- </table>
- </div>
-</div>
<!-- items out list -->
-<div class="row pad-vert">
- <div class="col-md-12 text-right">
- [% INCLUDE 'staff/circ/patron/t_items_out_actions.tt2' %]
- </div>
-</div>
+<eg-grid
+ idl-class="circ"
+ id-field="id"
+ features="-display,-sort,-multisort"
+ main-label="[% l('Items Checked Out') %]"
+ items-provider="gridDataProvider"
+ persist-key="eg.staff.circ.patron.items_out">
+ <eg-grid-field label="[% ('Circ ID') %]" path='id' visible></eg-grid-field>
+ <eg-grid-field label="[% ('Barcode') %]" path='target_copy.barcode' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Due Date') %]" path='due_date' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Checkout / Renewal Library') %]" path='circ_lib.shortname' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Renewals Remaining') %]" path='renewal_remaining' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Fines Stopped') %]" path='stop_fines' visible></eg-grid-field>
+ <eg-grid-field label="[% l('Title') %]" path='target_copy.call_number.record.simple_record.title' visible></eg-grid-field>
+</eg-grid>
-[% INCLUDE 'staff/circ/patron/t_items_out_table.tt2' %]
+++ /dev/null
-<div class="btn-group text-left" ng-show="items_out.count()">
-
- <!-- PAGING -->
-
- <!--
- <button type="button" class="btn btn-default"
- ng-class="{disabled : items_out.onFirstPage()}"
- ng-click="items_out.offset = 0;draw()">[% l('Start') %]</button>
-
- <button type="button" class="btn btn-default"
- ng-class="{disabled : items_out.onFirstPage()}"
- ng-click="items_out.decrementPage();draw()">«</button>
-
- <button type="button" class="btn btn-default"
- ng-class="{disabled : !items_out.hasNextPage()}"
- ng-click="items_out.incrementPage();draw()">»</button>
- -->
-
- <div class="btn-group">
- <button type="button" class="btn btn-default dropdown-toggle"
- ng-class="{disabled : !items_out.count()}" data-toggle="dropdown">
- [% l('Actions') %] <span class="caret"></span>
- </button>
- <ul class="dropdown-menu pull-right">
- <li class="disabled" xxxng-class="{disabled : !items_out.count()}">
- <a href="" ng-click="openSelecteditems_out()">
- [% l('Renew Item(s)') %]</a>
- </li>
- </ul>
- </div>
-
- [% INCLUDE 'staff/parts/column_picker.tt2' listname='items_out' %]
-</div>
-
+++ /dev/null
-[%
-COLUMNS = [
-{label => l('Circ ID'), name => 'id', display => 1},
-{label => l('Barcode'), name => 'target_copy.barcode' display => 1},
-{label => l('Due Date'), name => 'due_date' display => 1},
-{label => l('Checkout/Renewal Library'),
- name => 'circ_lib.shortname' display => 1},
-{label => l('Renewals Remaining'), name => 'renewal_remaining' display => 1},
-{label => l('Fines Stopped'), name => 'stop_fines' display => 1},
-{label => l('Title'),
- name => 'target_copy.call_number.record.simple_record.title', display => 1},
-]
-%]
-
-<!-- tell JS about our columns so they can be dynamically managed -->
-<div ng-init="
-items_out.setColumns([
-[%- FOR col IN COLUMNS %]
-{label:'[% col.label %]',name:'[% col.name %]'[% IF col.display %],display:true[% END %]}[% IF !loop.last; ','; END -%]
-[% END %]
-]);
-">
-</div>
-
-<div class="row" ng-show="!loading && !items_out.count()">
- <div class="col-md-10 col-md-offset-1">
- <div class="alert alert-info">[% l('No Items To Display') %]</div>
- </div>
-</div>
-
-<div class="row" ng-show="items_out.count()">
- <div class="col-md-12">
- <table class="list table table-hover table-condensed">
- <thead>
- <tr>
- <th>#</th>
- <th><a href='' ng-click="items_out.toggleSelectAll()">✓</a></th>
- <th ng-repeat="col in items_out.allColumns"
- ng-show="items_out.displayColumns[col.name]">
- {{col.label}}
- </th>
- </tr>
- </thead>
- <tbody>
- <tr ng-repeat="circ in items_out.items | reverse track by $index"
- ng-click="onRowClick($event, circ)"
- ng-class="{
- selected : items_out.selected[circ.id()],
- 'patron-summary-alert' : circIsOverdue(circ)
- }">
- <td>{{$index + 1}}</td>
- <td><span ng-if="items_out.selected[circ.id()]">✓</span>
- <td ng-repeat="col in items_out.allColumns"
- ng-show="items_out.displayColumns[col.name]">
- {{items_out.fieldValue(circ, col.name)}}
- </td>
- </tr>
- </tbody>
- </table>
- </div>
-</div>
<!-- edit bucket dialog -->
<form class="form-validated" novalidate ng-submit="ok(precatArgs)" name="form">
- <div class="modal-dialog">
+ <div class="">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close"
* Patron service
*/
.factory('patronSvc',
- ['$q','egList','egNet','egAuth','egUser','egEnv','egOrg','egList',
-function($q, egList, egNet, egAuth, egUser, egEnv, egOrg, egList) {
+ ['$q','$timeout','egList','egNet','egAuth','egUser','egEnv','egOrg','egList',
+function($q , $timeout , egList, egNet, egAuth, egUser, egEnv, egOrg, egList) {
var service = {
// currently selected patron object
// patron circ stats (overdues, fines, holds)
patron_stats : null,
- // event types manually overridden, which should always
- // be overridden for checkouts to this patron.
+ // event types manually overridden, which should always be
+ // overridden for checkouts to this patron for this instance of
+ // the interface.
checkout_overrides : {},
- // keep a cache of the patron search results
- patrons : [],
- checkouts : egList.create(),
- items_out : egList.create({indexFieldAsFunction : true}), // circ.id()
- holds : egList.create(),
- bills : egList.create(),
- messages : egList.create()
+ // delivers the content of an array as a stream of promise notifications
+ arrayNotifier : function(arr) {
+ var def = $q.defer();
+ // promise notifications are only witnessed when delivered
+ // after the caller has his hands on the promise object
+ $timeout(function() {
+ angular.forEach(arr, def.notify);
+ def.resolve();
+ });
+ return def.promise;
+ }
};
// when we change the default patron, we need to clear out any
// data collected on that patron
service.resetPatronLists = function() {
- service.checkouts.reset();
- service.items_out.reset();
+ service.checkouts = [];
+ service.items_out = []
+ service.items_out_ids = [];
+ /*
service.holds.reset();
service.bills.reset();
service.messages.reset();
+ */
service.checkout_overrides = {};
}
+ service.resetPatronLists(); // initialize
// sets the default user, fetching as necessary
service.setDefault = function(id, user, force) {
$scope.focusMe = true;
$scope.searchArgs = {};
- egPrintStore.setItem('FOO', 'BAR').then(function() {
- return egPrintStore.appendItem('FOO', 'BAR')
- }).then(function() {
- return egPrintStore.getItem('FOO')
- }).then(function(result) {
- console.log('LOADED FOO ' + result);
- return egPrintStore.getKeys();
- }).then(function(keys) {
- console.log("we have keys " + keys);
- return egPrintStore.removeItem('FOO');
- }).then(function() {
- return egPrintStore.getKeys()
- }).then(function(keys) {
- console.log("we have keys " + keys)
- });
-
if (patronSvc.lastSearch) {
// populate the search form with our cached search info
angular.forEach(patronSvc.lastSearch.search, function(val, key) {
}
}
- provider.get = function(offset, count, onitem) {
+ provider.get = function(offset, count) {
var deferred = $q.defer();
var search = compileSearch($scope.searchArgs);
.controller('PatronSummaryCtrl',
['$scope','$q','egNet','egAuth','egEvent','patronSvc',
function($scope, $q, egNet, egAuth, egEvent, patronSvc) {
- // may not need this ctrl at all, since all data
- // comes directly from the scope
-}])
-
-/**
- * Manages checkout
- */
-.controller('PatronCheckoutCtrl',
- ['$scope','$q','$modal','$routeParams','egNet','egAuth','egUser',
- 'egIDL','patronSvc','egEnv','egPCRUD','egOrg',
-function($scope, $q, $modal, $routeParams, egNet, egAuth, egUser,
- egIDL, patronSvc, egEnv, egPCRUD, egOrg) {
- $scope.initTab('checkout', $routeParams.id);
-
- $scope.focusMe = true;
- $scope.checkouts = patronSvc.checkouts;
- $scope.checkoutArgs = {noncat_type : 'barcode'};
-
- $scope.selectedNcType = function() {
- if (!egEnv.cnct) return null; // too soon
- var type = egEnv.cnct.map[$scope.checkoutArgs.noncat_type];
- return type ? type.name() : null;
- }
-
- if (egEnv.cnct) {
- $scope.nonCatTypes = egEnv.cnct.list;
- } else {
- egPCRUD.search('cnct',
- {owning_lib : egOrg.fullPath(egAuth.user().ws_ou(), true)},
- null, {atomic : true}
- ).then(function(list) {
- egEnv.absorbList(list, 'cnct');
- $scope.nonCatTypes = list
- });
- }
-
- egPCRUD.retrieveAll('ccm', null, {atomic : true}).then(
- function(list) { $scope.circModifiers = list });
-
- // TODO: apply correct response order
- $scope.checkout = function(args) {
- var type = args.type;
- var coArgs = angular.copy(args);
-
- if (coArgs.noncat_type == 'barcode') {
-
- if (!args.copy_barcode) return;
- args.copy_barcode = ''; // reset UI
-
- delete coArgs.noncat_type;
- performCheckout(coArgs);
-
- } else {
- console.log('noncat..');
- openNoncatDialog(coArgs);
- }
-
- $scope.focusMe; // return focus to barcode input
- }
-
- var index = 0;
- function performCheckout(args, override) {
- console.debug('checkout: ' + js2JSON(args));
-
- var method = 'open-ils.circ.checkout.full';
- if (override) method += '.override';
-
- args.patron_id = $scope.patron_id;
-
- egNet.request(
- 'open-ils.circ', method, egAuth.token(), args
- ).then(function(evt) {
-
- if (!evt) {
- console.error('no checkout response received');
- return;
- }
-
- // TODO: how best to handle multiple response events?
- if (angular.isArray(evt)) evt = evt[0];
- evt.id = index++;
- evt.copy_barcode = args.copy_barcode;
- handleCheckoutResponse(evt, args, override)
- });
- }
-
- function handleCheckoutResponse(evt, args, override) {
-
- if (evt.payload) {
- if (args.precat) {
- evt.payload.record = {
- title : args.dummy_title,
- author : args.dummy_author,
- isbn : args.dummy_isbn
- };
- } else if (args.noncat) {
- evt.payload.record = {
- title : egEnv.cnct.map[args.noncat_type].name()
- };
- evt.noncat_count = args.noncat_count;
- evt.payload.circ = new egIDL.circ();
- evt.payload.circ.due_date(evt.payload.noncat_circ.duedate());
- }
- }
-
- switch (evt.textcode) {
- case 'SUCCESS':
- // keep the global patron object in sync with reality
- $scope.checkouts.items.push(evt);
- if (!args.noncat)
- patronSvc.patron_stats.checkouts.out++;
- break;
-
- case 'ITEM_NOT_CATALOGED':
- openPrecatDialog(evt.copy_barcode);
- break;
-
- case 'PATRON_EXCEEDS_FINES':
- if (!override) {
- if (patronSvc.checkout_overrides[evt.textcode]) {
- performCheckout(args, true);
- } else {
- openOverrideConfirmDialog(evt, args);
- }
- }
- break;
-
- /* stuff to consider
- PERM_FAILURE
- PATRON_EXCEEDS_OVERDUE_COUNT
- PATRON_BARRED
- CIRC_EXCEEDS_COPY_RANGE
- PATRON_ACCOUNT_EXPIRED
- ITEM_DEPOSIT_REQUIRED
- ITEM_RENTAL_FEE_REQUIRED
- ITEM_DEPOSIT_PAID
- PATRON_EXCEEDS_LOST_COUNT
- ACTION_CIRCULATION_NOT_FOUND
- PATRON_EXCEEDS_CHECKOUT_COUNT
- COPY_CIRC_NOT_ALLOWED
- COPY_NOT_AVAILABLE
- COPY_IS_REFERENCE
- COPY_NEEDED_FOR_HOLD
- MAX_RENEWALS_REACHED
- CIRC_CLAIMS_RETURNED
- COPY_ALERT_MESSAGE
- PATRON_EXCEEDS_FINES
- */
-
- default:
- console.warn('unhandled circ response : ' + evt.textcode);
- // push it on the list so the user can at least see
- // something happened.
- $scope.checkouts.items.push(evt);
-
- }
- }
-
- // define our modal dialogs
-
- function openNoncatDialog(coArgs) {
- coArgs.noncat = true;
- var type = egEnv.cnct.map[coArgs.noncat_type];
-
- $modal.open({
- templateUrl: './circ/patron/t_noncat_dialog',
- controller:
- ['$scope', '$modalInstance',
- function($scope, $modalInstance) {
- $scope.focusMe = true;
- $scope.type = type;
- $scope.count = 1;
- $scope.ok = function(count) { $modalInstance.close(count) }
- $scope.cancel = function () { $modalInstance.dismiss() }
- }],
- }).result.then(
- function(count) {
- $scope.focusMe = true; // main barcode input
- if (count) {
- // TODO: sanity check
- coArgs.noncat_count = count;
- performCheckout(coArgs);
- }
- }
- );
- }
-
- function openPrecatDialog(copy_barcode) {
- $modal.open({
- templateUrl: './circ/patron/t_precat_dialog',
- controller:
- ['$scope', '$modalInstance', 'circMods',
- function($scope, $modalInstance, circMods) {
- $scope.focusMe = true;
- $scope.precatArgs = {
- copy_barcode : copy_barcode,
- circ_modifier : circMods.length ? circMods[0].code() : null
- };
- $scope.circModifiers = circMods;
- $scope.ok = function(args) { $modalInstance.close(args) }
- $scope.cancel = function () { $modalInstance.dismiss() }
- }],
- // pass the circ mod list into the modal environment
- // the angular way.
- resolve : {
- circMods : function() { return $scope.circModifiers }
- }
- }).result.then(
- function(args) {
- $scope.focusMe = true; // main barcode input
- if (!args || !args.dummy_title) return;
- args.precat = true;
- performCheckout(args);
- },
- function() {
- // dialog was closed without action
- $scope.focusMe = true;
- }
- );
- }
-
- function openOverrideConfirmDialog(evt, args) {
- $modal.open({
- templateUrl: './circ/patron/t_event_override_dialog',
- controller:
- ['$scope', '$modalInstance',
- function($scope, $modalInstance) {
- $scope.evt = evt;
- $scope.ok = function() { $modalInstance.close() }
- $scope.cancel = function () { $modalInstance.dismiss() }
- }]
- }).result.then(
- function() {
- $scope.focusMe = true; // main barcode input
- patronSvc.checkout_overrides[evt.textcode] = true;
- performCheckout(args, true);
- },
- function() {
- // dialog was closed without action
- $scope.focusMe = true;
- }
- );
- }
-
-
+ // so for, all data for this controller is data inherited
+ // from the parent scope. Nothing to do here.
}])
-/**
- * Manages checkout
- */
-.controller('PatronItemsOutCtrl',
- ['$scope','$q','$routeParams','egNet','egAuth','egUser','patronSvc','egPCRUD','egOrg',
-function($scope, $q, $routeParams, egNet, egAuth, egUser, patronSvc, egPCRUD, egOrg) {
- $scope.initTab('items_out', $routeParams.id);
- $scope.items_out = patronSvc.items_out;
-
- // true if circ is overdue, false otherwise
- $scope.circIsOverdue = function(circ) {
- // circ may not exist yet for rendered row
- if (!circ) return false;
-
- var date = new Date();
- date.setTime(Date.parse(circ.due_date()));
- return date < new Date();
- }
-
- // row selection via click
- $scope.onRowClick = function($event, circ) {
- $scope.lastSelected = circ;
- // control-click / command-click (mac) selects
- // or deselects a row without altering other rows
- if ($event.ctrlKey || $event.metaKey) {
- $scope.items_out.toggleOneSelection(circ.id());
- } else {
- $scope.items_out.selectOne(circ.id());
- }
- }
-
- function fetchItemsOut() {
- var newlist = [];
- $scope.loading = true;
- egNet.request('open-ils.actor',
- 'open-ils.actor.user.checked_out.authoritative',
- egAuth.token(), $scope.patron_id)
- .then(function(outs) {
-
- // put them into a list so we can keep track of the
- // default display order
- newlist = newlist.concat(outs.out)
- .concat(outs.overdue)
- .concat(outs.long_overdue)
- .concat(outs.lost)
- .concat(outs.claims_returned)
-
- if (!newlist.length) {
- $scope.loading = false;
- return;
- }
-
- // TODO: Websockets means 1 streaming request instead of
- // multiple singles. As is, one response may be too large
- // to wait on.
- angular.forEach(newlist, function(id) {
-
- egPCRUD.retrieve('circ', id, {
- flesh : 4,
- flesh_fields : {
- circ : ['target_copy'],
- acp : ['call_number'],
- acn : ['record'],
- bre : ['simple_record']
- },
- // avoid fetching the MARC blob by specifying which
- // fields on the bre to select. More may be needed.
- // note that fleshed fields are explicitly selected.
- select : { bre : ['id'] }
- }).then(function(circ) {
- $scope.loading = false;
-
- // local fleshing
- circ.circ_lib(egOrg.get(circ.circ_lib()));
-
- if (circ.target_copy().call_number().id() == -1) {
- // dummy-up a record for precat items
- circ.target_copy().call_number().record().simple_record({
- title : function() {return circ.target_copy().dummy_title()},
- author : function() {return circ.target_copy().dummy_author()},
- isbn : function() {return circ.target_copy().dummy_isbn()}
- })
- }
-
- angular.forEach(newlist, function(id, idx) {
- // insert into the result list in the same
- // order as above
- if (id == circ.id())
- patronSvc.items_out.items[idx] = circ;
- });
- });
- })
- })
- }
-
- fetchItemsOut(); // TODO: only when necessary
-}])
/**
* Manages holds
--- /dev/null
+/**
+ * Checkout items to patrons
+ */
+
+angular.module('egPatronApp').controller('PatronCheckoutCtrl',
+
+ ['$scope','$q','$modal','$routeParams','egNet','egAuth','egUser',
+ 'egIDL','patronSvc','egEnv','egPCRUD','egOrg','egGridDataProvider',
+
+function($scope, $q, $modal, $routeParams, egNet, egAuth, egUser,
+ egIDL, patronSvc, egEnv, egPCRUD, egOrg , egGridDataProvider) {
+
+ $scope.initTab('checkout', $routeParams.id);
+
+ $scope.focusMe = true;
+ $scope.checkouts = patronSvc.checkouts;
+ $scope.checkoutArgs = {noncat_type : 'barcode'};
+
+ // Grid Provider -------------------
+ var provider = egGridDataProvider.instance({});
+ provider.get = function(offset, count) {
+ return patronSvc.arrayNotifier(
+ $scope.checkouts.slice(offset, offset + count)
+ );
+ }
+ provider.itemFieldValue = function(item, column) {
+ return provider.nestedItemFieldValue(item, column);
+ };
+ $scope.gridDataProvider = provider;
+
+ function addCheckout(co) {
+ $scope.checkouts.push(co);
+ $scope.gridDataProvider.increment();
+ }
+ // -----------------------------
+
+ $scope.selectedNcType = function() {
+ if (!egEnv.cnct) return null; // too soon
+ var type = egEnv.cnct.map[$scope.checkoutArgs.noncat_type];
+ return type ? type.name() : null;
+ }
+
+ if (egEnv.cnct) {
+ $scope.nonCatTypes = egEnv.cnct.list;
+ } else {
+ egPCRUD.search('cnct',
+ {owning_lib : egOrg.fullPath(egAuth.user().ws_ou(), true)},
+ null, {atomic : true}
+ ).then(function(list) {
+ egEnv.absorbList(list, 'cnct');
+ $scope.nonCatTypes = list
+ });
+ }
+
+ egPCRUD.retrieveAll('ccm', null, {atomic : true}).then(
+ function(list) { $scope.circModifiers = list });
+
+ // TODO: apply correct response order
+ $scope.checkout = function(args) {
+ var type = args.type;
+ var coArgs = angular.copy(args);
+
+ if (coArgs.noncat_type == 'barcode') {
+
+ if (!args.copy_barcode) return;
+ args.copy_barcode = ''; // reset UI
+
+ delete coArgs.noncat_type;
+ performCheckout(coArgs);
+
+ } else {
+ console.log('noncat..');
+ openNoncatDialog(coArgs);
+ }
+
+ $scope.focusMe; // return focus to barcode input
+ }
+
+ var index = 0;
+ function performCheckout(args, override) {
+ console.debug('checkout: ' + js2JSON(args));
+
+ var method = 'open-ils.circ.checkout.full';
+ if (override) method += '.override';
+
+ args.patron_id = $scope.patron_id;
+
+ egNet.request(
+ 'open-ils.circ', method, egAuth.token(), args
+ ).then(function(evt) {
+
+ if (!evt) {
+ console.error('no checkout response received');
+ return;
+ }
+
+ // TODO: how best to handle multiple response events?
+ if (angular.isArray(evt)) evt = evt[0];
+ evt.id = index++;
+ evt.copy_barcode = args.copy_barcode;
+ handleCheckoutResponse(evt, args, override)
+ });
+ }
+
+ function handleCheckoutResponse(evt, args, override) {
+
+ if (evt.payload) {
+ if (args.precat) {
+ evt.payload.record = {
+ title : args.dummy_title,
+ author : args.dummy_author,
+ isbn : args.dummy_isbn
+ };
+ } else if (args.noncat) {
+ evt.payload.record = {
+ title : egEnv.cnct.map[args.noncat_type].name()
+ };
+ evt.noncat_count = args.noncat_count;
+ evt.payload.circ = new egIDL.circ();
+ evt.payload.circ.due_date(evt.payload.noncat_circ.duedate());
+ }
+ }
+
+ console.log('checkout: ' + JSON.stringify(evt, null, 2));
+
+ switch (evt.textcode) {
+ case 'SUCCESS':
+ // keep the global patron object in sync with reality
+ addCheckout(evt);
+ if (!args.noncat)
+ patronSvc.patron_stats.checkouts.out++;
+ break;
+
+ case 'ITEM_NOT_CATALOGED':
+ openPrecatDialog(evt.copy_barcode);
+ break;
+
+ case 'PATRON_EXCEEDS_FINES':
+ case 'PATRON_EXCEEDS_CHECKOUT_COUNT':
+ if (!override) {
+ if (patronSvc.checkout_overrides[evt.textcode]) {
+ performCheckout(args, true);
+ } else {
+ openOverrideConfirmDialog(evt, args);
+ }
+ }
+ break;
+
+ /* stuff to consider
+ PERM_FAILURE
+ PATRON_EXCEEDS_OVERDUE_COUNT
+ PATRON_BARRED
+ CIRC_EXCEEDS_COPY_RANGE
+ PATRON_ACCOUNT_EXPIRED
+ ITEM_DEPOSIT_REQUIRED
+ ITEM_RENTAL_FEE_REQUIRED
+ ITEM_DEPOSIT_PAID
+ PATRON_EXCEEDS_LOST_COUNT
+ ACTION_CIRCULATION_NOT_FOUND
+ PATRON_EXCEEDS_CHECKOUT_COUNT
+ COPY_CIRC_NOT_ALLOWED
+ COPY_NOT_AVAILABLE
+ COPY_IS_REFERENCE
+ COPY_NEEDED_FOR_HOLD
+ MAX_RENEWALS_REACHED
+ CIRC_CLAIMS_RETURNED
+ COPY_ALERT_MESSAGE
+ PATRON_EXCEEDS_FINES
+ */
+
+ default:
+ console.warn('unhandled circ response : ' + evt.textcode);
+ // push it on the list so the user can at least see
+ // something happened.
+ addCheckout(evt);
+ }
+ }
+
+ // define our modal dialogs
+
+ function openNoncatDialog(coArgs) {
+ coArgs.noncat = true;
+ var type = egEnv.cnct.map[coArgs.noncat_type];
+
+ $modal.open({
+ templateUrl: './circ/patron/t_noncat_dialog',
+ controller:
+ ['$scope', '$modalInstance',
+ function($scope, $modalInstance) {
+ $scope.focusMe = true;
+ $scope.type = type;
+ $scope.count = 1;
+ $scope.ok = function(count) { $modalInstance.close(count) }
+ $scope.cancel = function () { $modalInstance.dismiss() }
+ }],
+ }).result.then(
+ function(count) {
+ $scope.focusMe = true; // main barcode input
+ if (count) {
+ // TODO: sanity check
+ coArgs.noncat_count = count;
+ performCheckout(coArgs);
+ }
+ }
+ );
+ }
+
+ function openPrecatDialog(copy_barcode) {
+ $modal.open({
+ templateUrl: './circ/patron/t_precat_dialog',
+ controller:
+ ['$scope', '$modalInstance', 'circMods',
+ function($scope, $modalInstance, circMods) {
+ $scope.focusMe = true;
+ $scope.precatArgs = {
+ copy_barcode : copy_barcode,
+ circ_modifier : circMods.length ? circMods[0].code() : null
+ };
+ $scope.circModifiers = circMods;
+ $scope.ok = function(args) { $modalInstance.close(args) }
+ $scope.cancel = function () { $modalInstance.dismiss() }
+ }],
+ // pass the circ mod list into the modal environment
+ // the angular way.
+ resolve : {
+ circMods : function() { return $scope.circModifiers }
+ }
+ }).result.then(
+ function(args) {
+ $scope.focusMe = true; // main barcode input
+ if (!args || !args.dummy_title) return;
+ args.precat = true;
+ performCheckout(args);
+ },
+ function() {
+ // dialog was closed without action
+ $scope.focusMe = true;
+ }
+ );
+ }
+
+ function openOverrideConfirmDialog(evt, args) {
+ $modal.open({
+ templateUrl: './circ/patron/t_event_override_dialog',
+ controller:
+ ['$scope', '$modalInstance',
+ function($scope, $modalInstance) {
+ $scope.evt = evt;
+ $scope.ok = function() { $modalInstance.close() }
+ $scope.cancel = function () { $modalInstance.dismiss() }
+ }]
+ }).result.then(
+ function() {
+ $scope.focusMe = true; // main barcode input
+ patronSvc.checkout_overrides[evt.textcode] = true;
+ performCheckout(args, true);
+ },
+ function() {
+ // dialog was closed without action
+ $scope.focusMe = true;
+ }
+ );
+ }
+
+}])
+
--- /dev/null
+/**
+ * List of patron items checked out
+ */
+
+angular.module('egPatronApp').controller('PatronItemsOutCtrl',
+
+ ['$scope','$q','$routeParams','egNet','egAuth','egUser','patronSvc','egPCRUD','egOrg','egGridDataProvider',
+function($scope, $q, $routeParams, egNet, egAuth, egUser, patronSvc, egPCRUD, egOrg , egGridDataProvider) {
+
+ $scope.initTab('items_out', $routeParams.id);
+
+ var provider = egGridDataProvider.instance({});
+ provider.itemFieldValue = provider.nestedItemFieldValue;
+ $scope.gridDataProvider = provider;
+
+ function fetchCircs(offset, count) {
+
+ // fetch the lot of circs and stream the results back via notify
+ return egPCRUD.search('circ',
+ {id : patronSvc.items_out_ids},
+ { flesh : 4,
+ flesh_fields : {
+ circ : ['target_copy'],
+ acp : ['call_number'],
+ acn : ['record'],
+ bre : ['simple_record']
+ },
+ // avoid fetching the MARC blob by specifying which
+ // fields on the bre to select. More may be needed.
+ // note that fleshed fields are explicitly selected.
+ select : { bre : ['id'] },
+ limit : count,
+ offset : offset,
+ // we need an order-by to support paging
+ order_by : {circ : ['xact_start']}
+
+ }).then(null, null, function(circ) {
+ circ.circ_lib(egOrg.get(circ.circ_lib())); // local fleshing
+
+ if (circ.target_copy().call_number().id() == -1) {
+ // dummy-up a record for precat items
+ circ.target_copy().call_number().record().simple_record({
+ title : function() {return circ.target_copy().dummy_title()},
+ author : function() {return circ.target_copy().dummy_author()},
+ isbn : function() {return circ.target_copy().dummy_isbn()}
+ })
+ }
+
+ patronSvc.items_out.push(circ); // toss it into the cache
+ return circ;
+ });
+ }
+
+ provider.get = function(offset, count) {
+
+ // see if we have the requested range cached
+ if (patronSvc.items_out[offset]) {
+ return patronSvc.arrayNotifier(
+ patronSvc.items_out.slice(offset, offset + count)
+ );
+ }
+
+ // see if we have the circ IDs for this range already loaded
+ if (patronSvc.items_out_ids[offset]) {
+ return fetchCircs(offset, count);
+ }
+
+
+ // avoid returning the request directly so the caller so the
+ // notify()'s from egNet.request don't leak into the
+ // final set of notifies (i.e. the real responses);
+
+ var deferred = $q.defer();
+ patronSvc.items_out_ids = [];
+
+ egNet.request(
+ 'open-ils.actor',
+ 'open-ils.actor.user.checked_out.authoritative',
+ egAuth.token(), $scope.patron_id
+
+ ).then(function(outs) {
+
+ patronSvc.items_out_ids = outs.out
+ .concat(outs.overdue)
+ .concat(outs.long_overdue)
+ .concat(outs.lost)
+ .concat(outs.claims_returned)
+
+ // no item out
+ if (!patronSvc.items_out_ids.length) {
+ deferred.resolve();
+ return;
+ }
+
+ // relay the notified circs back to the grid through
+ // our promise
+ fetchCircs(offset, count).then(null, null, deferred.notify);
+ });
+
+ return deferred.promise;
+ }
+
+
+ // true if circ is overdue, false otherwise
+ $scope.circIsOverdue = function(circ) {
+ // circ may not exist yet for rendered row
+ if (!circ) return false;
+
+ var date = new Date();
+ date.setTime(Date.parse(circ.due_date()));
+ return date < new Date();
+ }
+}]);
+
// this allows the caller to pass in initializtion
// values, like offset, for cases when the caller may
// be caching grid data between route loads.
- var conf = grid.dataProvider.initialize();
- if (conf) {
- angular.forEach(conf, function(val, key) {
- if (val !== null) grid[key] = val;
- });
+ if (angular.isFunction(grid.dataProvider.initialize)) {
+ var conf = grid.dataProvider.initialize();
+ if (conf) {
+ angular.forEach(conf, function(val, key) {
+ if (val !== null) grid[key] = val;
+ });
+ }
}
-
+
grid.compileSort();
$scope.grid = grid;
}
cols.columns.push(column);
if (fromIDL) return;
+ if (!cols.idlClass) return; // ad-hoc object
// lookup the matching IDL field
var idl_field = cols.idlFieldFromPath(column.path);
return Boolean(value == 't');
case 'timestamp':
// canned angular date filter FTW
- return $filter('date')(value);
+ return $filter('date')(value, 'shortDate');
default:
return value;
}