browser staff : patron UI migrations..
authorBill Erickson <berick@esilibrary.com>
Fri, 25 Apr 2014 20:40:20 +0000 (16:40 -0400)
committerBill Erickson <berick@esilibrary.com>
Fri, 25 Apr 2014 20:40:20 +0000 (16:40 -0400)
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>
Open-ILS/src/templates/staff/circ/patron/index.tt2
Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2
Open-ILS/src/templates/staff/circ/patron/t_checkout_table.tt2 [deleted file]
Open-ILS/src/templates/staff/circ/patron/t_items_out.tt2
Open-ILS/src/templates/staff/circ/patron/t_items_out_actions.tt2 [deleted file]
Open-ILS/src/templates/staff/circ/patron/t_items_out_table.tt2 [deleted file]
Open-ILS/src/templates/staff/circ/patron/t_precat_dialog.tt2
Open-ILS/web/js/ui/default/staff/circ/patron/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/circ/patron/items_out.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/services/grid.js

index c5fab21..0edf08f 100644 (file)
@@ -11,6 +11,8 @@
 <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" -->
index b241ff9..9a37949 100644 (file)
       </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>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_checkout_table.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_checkout_table.tt2
deleted file mode 100644 (file)
index 7d718c2..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-
-[%
-# 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>
index 1e3e066..16d5785 100644 (file)
@@ -1,9 +1,18 @@
 <!-- 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' %]
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_items_out_actions.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_items_out_actions.tt2
deleted file mode 100644 (file)
index c0876d1..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-<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()">&laquo;</button>
-
-  <button type="button" class="btn btn-default" 
-    ng-class="{disabled : !items_out.hasNextPage()}"
-    ng-click="items_out.incrementPage();draw()">&raquo;</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>
-
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_items_out_table.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_items_out_table.tt2
deleted file mode 100644 (file)
index a62d91b..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-[%
-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()">&#x2713;</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()]">&#x2713;</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>
index 793f974..8676bf7 100644 (file)
@@ -1,6 +1,6 @@
 <!-- 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" 
index e3db2ad..48a368d 100644 (file)
@@ -100,8 +100,8 @@ angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap',
  * 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
@@ -110,29 +110,38 @@ function($q,  egList,  egNet,  egAuth,  egUser,  egEnv,  egOrg,  egList) {
         // 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) {
@@ -240,22 +249,6 @@ function($scope,  $q,  $routeParams,  $timeout,  $window,  $location,  egEnv,
     $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) {
@@ -276,7 +269,7 @@ function($scope,  $q,  $routeParams,  $timeout,  $window,  $location,  egEnv,
         }
     }
 
-    provider.get = function(offset, count, onitem) {
+    provider.get = function(offset, count) {
         var deferred = $q.defer();
 
         var search = compileSearch($scope.searchArgs);
@@ -464,350 +457,10 @@ function($scope,  $q,  $routeParams,  $timeout,  $window,  $location,  egEnv,
 .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
diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js b/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js
new file mode 100644 (file)
index 0000000..5e3f724
--- /dev/null
@@ -0,0 +1,266 @@
+/**
+ * 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;
+            }
+        );
+    }
+
+}])
+
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
new file mode 100644 (file)
index 0000000..679faa9
--- /dev/null
@@ -0,0 +1,114 @@
+/**
+ * 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();
+    }
+}]);
+
index a9f1918..f1ebb12 100644 (file)
@@ -119,13 +119,15 @@ angular.module('egGridMod',
                 // 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;
             }
@@ -629,6 +631,7 @@ angular.module('egGridMod',
             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);
@@ -947,7 +950,7 @@ angular.module('egGridMod',
                 return Boolean(value == 't');                                  
             case 'timestamp':                                                  
                 // canned angular date filter FTW                              
-                return $filter('date')(value);                                 
+                return $filter('date')(value, 'shortDate');                                 
             default:                                                           
                 return value;                                                  
         }