--- /dev/null
+[%
+ WRAPPER "staff/base.tt2";
+ ctx.page_title = l("Curbside Pickup");
+ ctx.page_app = "egCurbsideApp";
+%]
+
+[% 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/circ/curbside/services/core.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/curbside/app.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/patron_search.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/curbside/directives/to_be_staged_manager.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/curbside/directives/staged_manager.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/curbside/directives/arrived_manager.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/curbside/directives/delivered_manager.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/curbside/directives/schedule_pickup.js"></script>
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+ s.CONFIRM_TAKE_OVER_STAGING_TITLE = "[% l('Take Over Claim for Staging Curbside Pickup Appointment') %]";
+ s.CONFIRM_TAKE_OVER_STAGING_BODY = "[% l('Take over staging pickup appointment [_1] from staff user [_2]?', '{{slot_id}}','{{other_staff}}') %]";
+ s.SUCCESS_CANCEL_APPOINTMENT = "[% l('Canceled curbside appointment {{slot_id}}') %]";
+ s.SUCCESS_CURBSIDE_CLAIM_STAGING = "[% l('Released claim on curbside appointment {{slot_id}} for staging') %]";
+ s.NOTFOUND_CURBSIDE_CLAIM_STAGING = "[% l('Could not find curbside appointment {{slot_id}} to release claim for staging') %]";
+ s.FAILED_CURBSIDE_CLAIM_STAGING = "[% l('Failed to release claim on curbside appointment {{slot_id}} for staging: {{evt_code}}') %]";
+ s.SUCCESS_CURBSIDE_UNCLAIM_STAGING = "[% l('Released claim on curbside appointment {{slot_id}} for staging') %]";
+ s.NOTFOUND_CURBSIDE_UNCLAIM_STAGING = "[% l('Could not find curbside appointment {{slot_id}} to release claim for staging') %]";
+ s.FAILED_CURBSIDE_UNCLAIM_STAGING = "[% l('Failed to release claim on curbside appointment {{slot_id}} for staging: {{evt_code}}') %]";
+ s.SUCCESS_CURBSIDE_MARK_STAGED = "[% l('Marked curbside appointment {{slot_id}} as staged') %]";
+ s.NOTFOUND_CURBSIDE_MARK_STAGED = "[% l('Could not find curbside appointment {{slot_id}} to mark as staged') %]";
+ s.FAILED_CURBSIDE_MARK_STAGED = "[% l('Failed to mark curbside appointment {{slot_id}} as staged: {{evt_code}}') %]";
+ s.SUCCESS_CURBSIDE_MARK_ARRIVED = "[% l('Marked curbside appointment {{slot_id}} as patron arrived') %]";
+ s.NOTFOUND_CURBSIDE_MARK_ARRIVED = "[% l('Could not find curbside appointment {{slot_id}} to mark as patron arrived') %]";
+ s.FAILED_CURBSIDE_MARK_ARRIVED = "[% l('Failed to mark curbside appointment {{slot_id}} as patron arrived: {{evt_code}}') %]";
+ s.SUCCESS_CURBSIDE_MARK_UNSTAGED = "[% l('Marked curbside appointment {{slot_id}} back to to-be-staged') %]";
+ s.NOTFOUND_CURBSIDE_MARK_UNSTAGED = "[% l('Could not find curbside appointment {{slot_id}} to mark as to-be-staged') %]";
+ s.FAILED_CURBSIDE_MARK_UNSTAGED = "[% l('Failed to mark curbside appointment {{slot_id}} as to-be-staged: {{evt_code}}') %]";
+ s.SUCCESS_CURBSIDE_MARK_DELIVERED = "[% l('Marked curbside appointment {{slot_id}} as delivered') %]";
+ s.NOTFOUND_CURBSIDE_MARK_DELIVERED = "[% l('Could not find curbside appointment {{slot_id}} to mark as delivered') %]";
+ s.FAILED_CURBSIDE_MARK_DELIVERED = "[% l('Failed to mark curbside appointment {{slot_id}} as delivered: {{evt_code}}') %]";
+ s.FAILED_CURBSIDE_CHECKOUT = "[% l('Failed to check out an item as part of curbside appointment {{slot_id}}: {{evt_code}}') %]";
+ s.CONFIRM_CANCEL_TITLE = "[% l('Cancel Curbside Pickup Appointment') %]";
+ s.CONFIRM_CANCEL_BODY = "[% l('Cancel curbside pickup appointment [_1]?', '{{slot_id}}') %]";
+ s.SUCCESS_CANCEL_APPOINTMENT = "[% l('Canceled curbside appointment {{slot_id}}') %]";
+ s.FAILED_CANCEL_APPOINTMENT = "[% l('Failed to cancel curbside appointment {{slot_id}} ({{evt_code}})') %]";
+ s.SUCCESS_SAVE_APPOINTMENT = "[% l('Saved curbside appointment {{slot_id}}') %]";
+ s.FAILED_SAVE_APPOINTMENT = "[% l('Failed to save changes to curbside appointment ({{evt_code}})') %]";
+ s.FAILED_SAVE_APPOINTMENT_TOO_MANY = "[% l('Time slot is full; please choose another.') %]";
+}]);
+</script>
+[% END %]
+
+<div ng-view></div>
+
+[% END %]
+
--- /dev/null
+<div>
+ <div ng-style="{visibility : refreshNeeded ? 'visible' : 'hidden'}" class="alert alert-warning">
+ [% l('Updates to the list of appointments whose patron has arrived are available. Please refresh.') %]
+ </div>
+ <eg-grid
+ id-field="slot_id"
+ features="-sort,-multisort,-picker,-multiselect"
+ items-provider="gridDataProvider"
+ grid-controls="gridControls"
+ dateformat="{{$root.egDateAndTimeFormat}}">
+
+ <eg-grid-menu-item handler="refresh_arrived" standalone="true"
+ label="[% l('Refresh List')%]"></eg-grid-menu-item>
+
+ <eg-grid-field label="[% l('Pickup Date/Time') %]" path="slot.slot" datatype="timestamp"></eg-grid-field>
+ <eg-grid-field label="[% l('Patron') %]" path="slot.patron" compiled handlers="gridCellHandlers">
+ <a href="./circ/patron/{{item.slot.patron().id()}}/holds" target="_blank">
+ {{item.slot.patron().family_name()}} / {{item.slot.patron().card().barcode()}}
+ <span class="glyphicon glyphicon-new-window"></span>
+ </a>
+ <br>
+ <span ng-show="item.slot.notes()">
+ <strong>[% l('Notes:') %]</strong> {{item.slot.notes()}}
+ </span>
+ <div class="alert alert-warning" ng-show="col.handlers.patronIsBlocked(item['slot'].patron())">
+ [% l('Patron is blocked from checkouts.') %]
+ </div>
+ </eg-grid-field>
+ <eg-grid-field label="[% l('Appointment ID') %]" path="slot.id"></eg-grid-field>
+ <eg-grid-field label="[% l('Items for Pickup') %]" path="holds" compiled>
+ <div class="alert alert-danger" ng-show="!item['slot'].staged()">
+ [% l('Items are not yet staged!') %]
+ </div>
+ <eg-curbside-holds-list holds="item.holds" bib-data="item.bib_data_by_hold" slot="item.slot"></eg-curbside-holds-list>
+ </eg-grid-field>
+ <eg-grid-field label="[% l('Action') %]" handlers="gridCellHandlers" compiled>
+ <button class="btn btn-sm btn-primary"
+ ng-disabled="col.handlers.wasHandled(item['slot_id']) || col.handlers.patronIsBlocked(item['slot'].patron())"
+ ng-click="col.handlers.mark_delivered(item['slot_id'])">
+ [% l('Check Out Items And Mark As Delivered') %]
+ </button>
+ </eg-grid-field>
+ </eg-grid>
+</div>
--- /dev/null
+<div>
+ <div ng-style="{visibility : refreshNeeded ? 'visible' : 'hidden'}" class="alert alert-warning">
+ [% l('Updates to the list of appointments whose items were delivered are available. Please refresh.') %]
+ </div>
+ <eg-grid
+ id-field="id"
+ features="-sort,-multisort,-picker,-multiselect"
+ items-provider="gridDataProvider"
+ grid-controls="gridControls"
+ dateformat="{{$root.egDateAndTimeFormat}}">
+
+ <eg-grid-menu-item handler="refresh_delivered" standalone="true"
+ label="[% l('Refresh List')%]"></eg-grid-menu-item>
+
+ <eg-grid-field label="[% l('Delivery Date/Time') %]" path="slot.delivered" datatype="timestamp"></eg-grid-field>
+ <eg-grid-field label="[% l('Patron') %]" path="slot.patron" compiled>
+ <a href="./circ/patron/{{item.slot.patron().id()}}/items_out" target="_blank">
+ {{item.slot.patron().family_name()}} / {{item.slot.patron().card().barcode()}}
+ <span class="glyphicon glyphicon-new-window"></span>
+ </a>
+ <br>
+ <span ng-show="item.slot.notes()">
+ <strong>[% l('Notes:') %]</strong> {{item.notes()}}
+ </span>
+ </eg-grid-field>
+ <eg-grid-field label="[% l('Appointment ID') %]" path="slot.id"></eg-grid-field>
+ <eg-grid-field label="[% l('Items Checked Out') %]" path="holds" compiled>
+ <eg-curbside-holds-list holds="item.holds" bib-data="item.bib_data_by_hold" slot="item.slot"></eg-curbside-holds-list>
+ </eg-grid-field>
+ </eg-grid>
+</div>
--- /dev/null
+<ul style="white-space: normal;">
+ <li ng-repeat="hold in holds">
+ {{bibData[hold.id()].title()}} / {{bibData[hold.id()].author()}}<br>
+ <a href="./cat/item/{{hold.current_copy().id()}}" target="_blank">
+ {{hold.current_copy().barcode()}}
+ <span class="glyphicon glyphicon-new-window"></span>
+ </a>
+ <div ng-if="slot.staged() && slot.staged() < hold.shelf_time()" class="alert alert-warning">
+ [% l('Check item; came in after appointment was staged.') %]
+ </div>
+ </li>
+</ul>
--- /dev/null
+<div class="container-fluid" style="text-align:center">
+ <div class="alert alert-info alert-less-pad strong-text-2">
+ <span>[% l('Curbside Pickup') %]</span>
+ </div>
+</div>
+
+<div class="row col-md-12 pad-vert">
+ <div class="col-md-12">
+ <uib-tabset active="active_tab">
+ <!-- note that non-numeric index values must be enclosed in single-quotes,
+ otherwise selecting the active table won't work cleanly -->
+ <uib-tab index="'to-be-staged'" heading="[% l('To Be Staged') %]">
+ <div class="container-fluid">
+ <eg-curbside-to-be-staged-manager ng-if="active_tab === 'to-be-staged'"></eg-curbside-to-be-staged-manager>
+ </div>
+ </uib-tab>
+ <uib-tab index="'staged'" heading="[% l('Staged And Ready') %]">
+ <div class="container-fluid">
+ <eg-curbside-staged-manager ng-if="active_tab === 'staged'"></eg-curbside-staged-manager>
+ </div>
+ </uib-tab>
+ <uib-tab index="'arrived'" heading="[% l('Patron Is Outside') %]">
+ <div class="container-fluid">
+ <eg-curbside-arrived-manager ng-if="active_tab === 'arrived'"></eg-curbside-arrived-manager>
+ </div>
+ </uib-tab>
+ <uib-tab index="'delivered'" heading="[% l('Delivered Today') %]">
+ <div class="container-fluid">
+ <eg-curbside-delivered-manager ng-if="active_tab === 'delivered'"></eg-curbside-delivered-manager>
+ </div>
+ </uib-tab>
+ <uib-tab index="'schedule'" heading="[% l('Schedule Pickup') %]">
+ <div class="container-fluid">
+ <eg-curbside-schedule-pickup ng-if="active_tab === 'schedule'"></eg-curbside-schedule-pickup>
+ </div>
+ </uib-tab>
+ </uib-tabset>
+ </div>
+</div>
--- /dev/null
+<div class="row">
+ <form ng-submit="submitBarcode(args)" role="form" class="form-inline" name="patronLookup">
+ <div class="input-group">
+
+ <label class="input-group-addon"
+ for="patron-curbside-barcode" >[% l('Patron Barcode') %]</label>
+
+ <input select-me="selectMe" class="form-control"
+ ng-model="args.barcode"
+ placeholder="[% l('Patron Barcode') %]"
+ id="patron-curbside-barcode" type="text"/>
+
+ </div>
+ <input class="btn btn-primary" type="submit" value="[% l('Submit') %]"/>
+ <button ng-click="patron_search()" class="btn btn-success">[% l('Patron Search') %]</button>
+ <button ng-click="clear()" class="btn btn-default">[% l('Clear') %]</button>
+ </form>
+</div>
+
+<br/>
+<div class="alert alert-warning" ng-show="bcNotFound">
+ [% l('Barcode Not Found: [_1]', '{{bcNotFound}}') %]
+</div>
+<div class="alert alert-warning" ng-show="optInRestricted">
+ [% l("This patron's record is not viewable at your library.") %]
+</div>
+
+<span ng-if="user_id">
+
+ <div class="row">
+ [% l('Patron: [_1] [_2], [_3] [_4] [_5]',
+ '{{patron().pref_prefix() || patron().prefix()}}',
+ '{{patron().pref_family_name() || patron().family_name()}}',
+ '{{patron().pref_first_given_name() || patron().first_given_name()}}',
+ '{{patron().pref_second_given_name() || patron().second_given_name()}}',
+ '{{patron().pref_suffix() || patron().suffix()}}')
+ %]
+ </div>
+ <div class="row">
+ [% l('Patron has [_1] ready holds at this location.', '{{readyHolds}}') %]
+ </div>
+
+ <div class="row">
+ <button ng-disabled="openAppointments.length > 0" ng-click="startNewAppointment()" class="btn btn-success">[% l('Make New Appointment') %]</button>
+ </div>
+ <br>
+ <div class="form-inline" ng-repeat="appt in openAppointments">
+ <ng-form name="forms['curbside' + appt.id]">
+ <div class="row">
+ <div class="col-md-1">
+ <label for="appointment-id">[% l('Appointment') %]</label>
+ <div id="appointment-id">{{appt.id}}</div>
+ </div>
+ <div class="col-md-2">
+ <label for="appointment-day">[% l('Date') %]</label>
+ <eg-date-input id="appointment-day" hide-time-picker ng-model="appt.slot_date"
+ required min-date="minDate">
+ </eg-date-input>
+ </div>
+ <div class="col-md-2">
+ <label for="appointment-time">[% l('Time') %]</label>
+ <select class="form-control" id="appointment-time" ng-model="appt.slot_time"
+ name="slot_time" style="display: block;"
+ required ng-focus="refreshAvailableTimes(appt)">
+ <option value=""></option>
+ <option ng-repeat="t in appt.available_times track by t.time" value="{{t.time}}"
+ ng-disabled="t.available === 0 && appt.original_slot_time !== t.time">
+ [% l('[_1] (Available: [_2])', '{{t.time_fmt}}', '{{t.available}}') %]
+ </option>
+ </select>
+ </div>
+ <div class="col-md-2">
+ <label for="appointment-notes">[% l('Notes') %]</label>
+ <input class="form-control" type="text" id="appointment-notes" ng-model="appt.notes" style="display: block;"></input>
+ </div>
+ <div class="col-md-2">
+ <label for="appointment-actions">[% l('Actions') %]</label>
+ <div id="appointment-actions">
+ <button ng-click="updateAppointment(appt)" ng-disabled="!forms['curbside' + appt.id].$valid" class="btn btn-primary">[% l('Save') %]</button>
+ <button ng-click="cancelAppointment(appt.id)" ng-disabled="!appt.id" class="btn btn-danger">[% l('Cancel Appointment') %]</button>
+ </div>
+ </div>
+ </div>
+ <div class="row pad-vert">
+ <div ng-if="appt.is_past" class="col-md-offset-1 col-md-4 alert alert-warning">
+ [% l('Appointment is in the past and may need to be rescheduled.') %]
+ </div>
+ </div>
+ </ng-form>
+ </div>
+</span>
--- /dev/null
+<div>
+ <div ng-style="{visibility : refreshNeeded ? 'visible' : 'hidden'}" class="alert alert-warning">
+ [% l('Updates to the list of staged and ready appointments are available. Please refresh.') %]
+ </div>
+ <eg-grid
+ id-field="slot_id"
+ features="-sort,-multisort,-picker,-multiselect"
+ items-provider="gridDataProvider"
+ grid-controls="gridControls"
+ dateformat="{{$root.egDateAndTimeFormat}}">
+
+ <eg-grid-menu-item handler="refresh_staged" standalone="true"
+ label="[% l('Refresh List')%]"></eg-grid-menu-item>
+
+ <eg-grid-field label="[% l('Pickup Date/Time') %]" path="slot.slot" datatype="timestamp"></eg-grid-field>
+ <eg-grid-field label="[% l('Patron') %]" path="slot.patron" compiled handlers="gridCellHandlers">
+ <a href="./circ/patron/{{item.slot.patron().id()}}/holds" target="_blank">
+ {{item.slot.patron().family_name()}} / {{item.slot.patron().card().barcode()}}
+ <span class="glyphicon glyphicon-new-window"></span>
+ </a>
+ <br>
+ <span ng-show="item.slot.notes()">
+ <strong>[% l('Notes:') %]</strong> {{item.slot.notes()}}
+ </span>
+ <div class="alert alert-warning" ng-show="col.handlers.patronIsBlocked(item['slot'].patron())">
+ [% l('Patron is blocked from checkouts.') %]
+ </div>
+ </eg-grid-field>
+ <eg-grid-field label="[% l('Appointment ID') %]" path="slot.id"></eg-grid-field>
+ <eg-grid-field label="[% l('Items for Pickup') %]" path="holds" compiled>
+ <eg-curbside-holds-list holds="item.holds" bib-data="item.bib_data_by_hold" slot="item.slot"></eg-curbside-holds-list>
+ </eg-grid-field>
+ <eg-grid-field label="[% l('Action') %]" handlers="gridCellHandlers" compiled>
+ <div class="row">
+ <div class="col-xs-12">
+ <button class="btn btn-sm btn-primary"
+ ng-disabled="col.handlers.wasHandled(item['slot_id']) || col.handlers.patronIsBlocked(item['slot'].patron())"
+ ng-click="col.handlers.mark_arrived(item['slot_id'])">
+ [% l('Mark As Patron Arrived') %]
+ </button>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-xs-12">
+ <button class="btn btn-sm btn-success"
+ ng-disabled="col.handlers.wasHandled(item['slot_id']) || col.handlers.patronIsBlocked(item['slot'].patron())"
+ ng-click="col.handlers.mark_delivered(item['slot_id'])">
+ [% l('Check Out Items And Mark As Delivered') %]
+ </button>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-xs-12">
+ <button class="btn btn-sm btn-warning"
+ ng-disabled="col.handlers.wasHandled(item['slot_id'])"
+ ng-click="col.handlers.mark_unstaged(item['slot_id'])">
+ [% l('Set Back to To Be Staged') %]
+ </button>
+ </div>
+ </div>
+ </eg-grid-field>
+ </eg-grid>
+</div>
--- /dev/null
+<div>
+ <div ng-style="{visibility : refreshNeeded ? 'visible' : 'hidden'}" class="alert alert-warning">
+ [% l('Updates to the curbside appointment list are available. Please refresh.') %]
+ </div>
+ <eg-grid
+ id-field="slot_id"
+ features="-sort,-multisort,-picker,-multiselect"
+ items-provider="gridDataProvider"
+ grid-controls="gridControls"
+ dateformat="{{$root.egDateAndTimeFormat}}">
+
+ <eg-grid-menu-item handler="refresh_staging" standalone="true"
+ label="[% l('Refresh List')%]"></eg-grid-menu-item>
+
+ <eg-grid-field label="[% l('Pickup Date/Time') %]" path="slot.slot" datatype="timestamp"></eg-grid-field>
+ <eg-grid-field label="[% l('Patron') %]" path="slot.patron" compiled handlers="gridCellHandlers">
+ <a href="./circ/patron/{{item.slot.patron().id()}}/holds" target="_blank">
+ {{item.slot.patron().family_name()}} / {{item.slot.patron().card().barcode()}}
+ <span class="glyphicon glyphicon-new-window"></span>
+ </a>
+ <br>
+ <span ng-show="item.slot.notes()">
+ <strong>[% l('Notes:') %]</strong> {{item.slot.notes()}}
+ </span>
+ <div class="alert alert-warning" ng-show="col.handlers.patronIsBlocked(item['slot'].patron())">
+ [% l('Patron is blocked from checkouts.') %]
+ </div>
+ <div class="alert alert-danger" ng-show="item['slot'].arrival()">
+ [% l('Patron has already arrived!') %]
+ </div>
+ </eg-grid-field>
+ <eg-grid-field label="[% l('Appointment ID') %]" path="slot.id"></eg-grid-field>
+ <eg-grid-field label="[% l('Items for Pickup') %]" path="holds" compiled>
+ <eg-curbside-holds-list holds="item.holds" bib-data="item.bib_data_by_hold" slot="item.slot"></eg-curbside-holds-list>
+ </eg-grid-field>
+ <eg-grid-field label="[% l('Staging Staff') %]" path="slot.stage_staff" handlers="gridCellHandlers" compiled>
+ {{item.slot.stage_staff().usrname()}}
+ <button class="btn btn-sm btn-default"
+ ng-show="col.handlers.canClaimStaging(item)"
+ ng-click="col.handlers.claim_staging(item)">
+ [% l('Claim') %]
+ </button>
+ <button class="btn btn-sm btn-default"
+ ng-show="col.handlers.canUnclaimStaging(item)"
+ ng-click="col.handlers.unclaim_staging(item)">
+ [% l('Release Claim') %]
+ </button>
+ </eg-grid-field>
+ <eg-grid-field label="[% l('Action') %]" handlers="gridCellHandlers" compiled>
+ <button class="btn btn-sm btn-primary"
+ ng-disabled="col.handlers.wasHandled(item['slot_id']) || col.handlers.patronIsBlocked(item['slot'].patron())"
+ ng-click="col.handlers.mark_staged(item['slot_id'])">
+ [% l('Mark As Staged And Ready') %]
+ </button>
+ </eg-grid-field>
+ </eg-grid>
+</div>
<span>[% l('Offline Circulation') %]</span>
</a>
</li>
+ <li ng-if="enableCurbside" class="divider"></li>
+ <li ng-if="enableCurbside">
+ <a href="./circ/curbside/index" target="_self" ng-class="{disabled : curbsideDisabled()}">
+ <span class="glyphicon glyphicon-road"></span>
+ <span>[% l('Curbside Pickup') %]</span>
+ </a>
+ </li>
</ul>
</li><!-- circ -->
--- /dev/null
+angular.module('egCurbsideApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod','ngToast','egCurbsideMod','egCurbsideAppDep']);
+angular.module('egCurbsideAppDep', ['egPatronSearchMod','egUserMod']);
+
+angular.module('egCurbsideApp')
+.config(['ngToastProvider', function(ngToastProvider) {
+ ngToastProvider.configure({
+ verticalPosition: 'bottom',
+ animation: 'fade'
+ });
+}])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+ $locationProvider.html5Mode(true);
+ $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
+
+ var resolver = {delay : ['egCore', function(egCore) {
+ egCore.env.classLoaders.aous = function() {
+ return egCore.org.settings([
+ 'circ.do_not_tally_claims_returned',
+ 'circ.tally_lost',
+ ]).then(function(settings) {
+ // local settings are cached within egOrg. Caching them
+ // again in egEnv just simplifies the syntax for access.
+ egCore.env.aous = settings;
+ });
+ };
+ egCore.env.loadClasses.push('aous');
+
+ return egCore.startup.go()
+ }]};
+
+ $routeProvider.when('/circ/curbside/index', {
+ templateUrl: './circ/curbside/t_main',
+ controller: 'CurbsideCtrl',
+ resolve : resolver
+ });
+
+ $routeProvider.when('/circ/curbside/:active_tab', {
+ templateUrl: './circ/curbside/t_main',
+ controller: 'CurbsideCtrl',
+ resolve : resolver
+ });
+
+ // default page
+ $routeProvider.otherwise({redirectTo : '/circ/curbside/index'});
+})
+
+.controller('CurbsideCtrl',
+ ['$scope','$routeParams','$location','egCurbsideCoreSvc',
+function($scope , $routeParams , $location , egCurbsideCoreSvc ) {
+ $scope.active_tab = $routeParams.active_tab ? $routeParams.active_tab : 'to-be-staged';
+
+ $scope.$watch('active_tab', function(newVal, oldVal) {
+ if (oldVal != newVal) {
+ var new_path = '/circ/curbside/' + $scope.active_tab;
+ $location.path(new_path);
+ }
+ });
+}])
--- /dev/null
+angular.module('egCurbsideAppDep')
+
+.directive('egCurbsideArrivedManager', function() {
+ return {
+ transclude: true,
+ restrict: 'E',
+ scope: { },
+ templateUrl: './circ/curbside/t_arrived_manager',
+ controller:
+ ['$scope','$q','egCurbsideCoreSvc','egCore','egGridDataProvider','egProgressDialog',
+ '$uibModal','$timeout','$location','egConfirmDialog','ngToast','$interval',
+function($scope , $q , egCurbsideCoreSvc , egCore , egGridDataProvider , egProgressDialog ,
+ $uibModal , $timeout , $location , egConfirmDialog , ngToast , $interval) {
+
+ $scope.gridControls = {};
+
+ $scope.wasHandled = {};
+ $scope.refreshNeeded = false;
+
+ latestTime = undefined;
+ var checkRefresh = undefined;
+ function startRefreshCheck() {
+ if (!angular.isDefined(checkRefresh)) {
+ checkRefresh = $interval(function() {
+ egCurbsideCoreSvc.get_latest_arrived().then(function(latest) {
+ if (angular.isDefined(latest)) {
+ if (angular.isDefined(latestTime) && latestTime != latest) {
+ $scope.refreshNeeded = true;
+ stopRefreshCheck();
+ }
+ latestTime = latest;
+ }
+ });
+ }, 15000);
+ }
+ }
+ function stopRefreshCheck() {
+ if (angular.isDefined(checkRefresh)) {
+ $interval.cancel(checkRefresh);
+ checkRefresh = undefined;
+ }
+ }
+ this.$onInit = function() {
+ startRefreshCheck();
+ }
+ this.$onDestroy = function() {
+ stopRefreshCheck();
+ }
+
+ $scope.gridDataProvider = egGridDataProvider.instance({
+ get : function(offset, count) {
+ $scope.wasHandled = {};
+ $scope.refreshNeeded = false;
+ startRefreshCheck();
+ return egCurbsideCoreSvc.get_arrived(offset, count);
+ }
+ });
+
+ $scope.refresh_arrived = function() {
+ $scope.gridControls.refresh();
+ }
+
+ $scope.gridCellHandlers = { };
+ $scope.gridCellHandlers.mark_delivered = function(id) {
+ var events_to_handle_later = [];
+ egProgressDialog.open();
+ egCurbsideCoreSvc.mark_delivered(id).then(function(resp) {
+ egProgressDialog.close();
+
+ events_to_handle_later.pop(); // last element is resp, our param
+ if (events_to_handle_later.length) { // this means we got at least one CO attempt
+
+ var bad_event;
+ angular.forEach(events_to_handle_later, function (evt) {
+ if (bad_event) return; // already warned staff, leave
+ if (angular.isArray(evt)) evt = evt[0]; // we only need to look at the first event from each CO response
+
+ evt = egCore.evt.parse(evt);
+ if (!bad_event && evt && evt.textcode != 'SUCCESS') { // at least one non-success event, show the first event.
+ bad_event = evt;
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_CURBSIDE_CHECKOUT,
+ { slot_id : id, evt_code : bad_event.code }
+ ));
+ }
+ });
+ }
+
+ if (evt = egCore.evt.parse(resp)) {
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_CURBSIDE_MARK_DELIVERED,
+ { slot_id : id, evt_code : evt.code }
+ ));
+ return;
+ }
+
+ if (!angular.isDefined(resp)) {
+ ngToast.warning(egCore.strings.$replace(
+ egCore.strings.NOTFOUND_CURBSIDE_MARK_DELIVERED,
+ { slot_id : id }
+ ));
+ return;
+ }
+
+ ngToast.success(egCore.strings.$replace(
+ egCore.strings.SUCCESS_CURBSIDE_MARK_DELIVERED,
+ { slot_id : id }
+ ));
+ $scope.wasHandled[id] = true;
+ $timeout(function() { $scope.refresh_arrived() }, 500);
+ },null, function (resp) {
+ events_to_handle_later.push(resp);
+ });
+ }
+ $scope.gridCellHandlers.wasHandled = function(id) {
+ return $scope.wasHandled[id];
+ }
+ $scope.gridCellHandlers.patronIsBlocked = function(usr) {
+ return egCurbsideCoreSvc.patron_blocked(usr);
+ }
+
+}]}});
--- /dev/null
+angular.module('egCurbsideAppDep')
+
+.directive('egCurbsideDeliveredManager', function() {
+ return {
+ transclude: true,
+ restrict: 'E',
+ scope: { },
+ templateUrl: './circ/curbside/t_delivered_manager',
+ controller:
+ ['$scope','$q','egCurbsideCoreSvc','egCore','egGridDataProvider',
+ '$uibModal','$timeout','$location','egConfirmDialog','ngToast','$interval',
+function($scope , $q , egCurbsideCoreSvc , egCore , egGridDataProvider ,
+ $uibModal , $timeout , $location , egConfirmDialog , ngToast , $interval) {
+
+ $scope.gridControls = {};
+
+ $scope.refreshNeeded = false;
+
+ latestTime = undefined;
+ var checkRefresh = undefined;
+ function startRefreshCheck() {
+ if (!angular.isDefined(checkRefresh)) {
+ checkRefresh = $interval(function() {
+ egCurbsideCoreSvc.get_latest_delivered().then(function(latest) {
+ if (angular.isDefined(latest)) {
+ if (angular.isDefined(latestTime) && latestTime != latest) {
+ $scope.refreshNeeded = true;
+ stopRefreshCheck();
+ }
+ latestTime = latest;
+ }
+ });
+ }, 15000);
+ }
+ }
+ function stopRefreshCheck() {
+ if (angular.isDefined(checkRefresh)) {
+ $interval.cancel(checkRefresh);
+ checkRefresh = undefined;
+ }
+ }
+ this.$onInit = function() {
+ startRefreshCheck();
+ }
+ this.$onDestroy = function() {
+ stopRefreshCheck();
+ }
+
+ $scope.gridDataProvider = egGridDataProvider.instance({
+ get : function(offset, count) {
+ $scope.refreshNeeded = false;
+ startRefreshCheck();
+ return egCurbsideCoreSvc.get_delivered(offset, count);
+ }
+ });
+
+ $scope.refresh_delivered = function() {
+ $scope.gridControls.refresh();
+ }
+
+ $scope.gridCellHandlers = { };
+
+}]}});
--- /dev/null
+angular.module('egCurbsideAppDep')
+
+.directive('egCurbsideSchedulePickup', function() {
+ return {
+ transclude: true,
+ restrict: 'E',
+ scope: { },
+ templateUrl: './circ/curbside/t_schedule_pickup',
+ controller:
+ ['$scope','$q','egCurbsideCoreSvc','egCore','patronSvc',
+ '$uibModal','$timeout','$location','egConfirmDialog','ngToast',
+function($scope , $q , egCurbsideCoreSvc , egCore , patronSvc ,
+ $uibModal , $timeout , $location , egConfirmDialog , ngToast) {
+
+ $scope.clear = function() {
+ $scope.user_id = undefined;
+ $scope.args = {};
+ $scope.readyHolds = 0;
+ $scope.openAppointments = [];
+ $scope.forms = [];
+ }
+ $scope.clear();
+
+ patron_search_dialog = function() {
+ return $uibModal.open({
+ templateUrl: './share/t_patron_selector',
+ backdrop: 'static',
+ size: 'lg',
+ animation: true,
+ controller:
+ ['$scope','$uibModalInstance','$controller',
+ function($scope , $uibModalInstance , $controller) {
+ angular.extend(this, $controller('BasePatronSearchCtrl', {$scope : $scope}));
+ $scope.clearForm();
+ $scope.need_one_selected = function() {
+ var items = $scope.gridControls.selectedItems();
+ return (items.length == 1) ? false : true
+ }
+ $scope.ok = function() {
+ var items = $scope.gridControls.selectedItems();
+ if (items.length == 1) {
+ $uibModalInstance.close(items[0].card().barcode());
+ } else {
+ $uibModalInstance.close()
+ }
+ }
+ $scope.cancel = function($event) {
+ $uibModalInstance.dismiss();
+ $event.preventDefault();
+ }
+ }]
+ });
+ }
+
+ $scope.patron_search = function() {
+ patron_search_dialog().result.then(function(barcode) {
+ $scope.args.barcode = barcode;
+ });
+ }
+
+ // this is blatantly copied from the patron app; if the AngularJS
+ // code had a longer life-expectancy, this would have been moved
+ // to a service.
+ $scope.submitBarcode = function(args) {
+ $scope.bcNotFound = null;
+ $scope.optInRestricted = false;
+ if (!args.barcode) return;
+ args.barcode = args.barcode.replace(/\s/g,'');
+ // blur so next time it's set to true it will re-apply select()
+ $scope.selectMe = false;
+
+ var user_id;
+
+ // given a scanned barcode, this function finds any matching users
+ // and handles multiple matches due to barcode completion
+ function handleBarcodeCompletion(scanned_barcode) {
+ var deferred = $q.defer();
+
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.get_barcodes',
+ egCore.auth.token(), egCore.auth.user().ws_ou(),
+ 'actor', scanned_barcode)
+
+ .then(function(resp) { // get_barcodes
+
+ if (evt = egCore.evt.parse(resp)) {
+ alert(evt); // FIXME
+ deferred.reject();
+ return;
+ }
+
+ if (!resp || !resp[0]) {
+ $scope.bcNotFound = args.barcode;
+ $scope.selectMe = true;
+ egCore.audio.play('warning.patron.not_found');
+ deferred.reject();
+ return;
+ }
+
+ if (resp.length == 1) {
+ // exactly one matching barcode: return it
+ deferred.resolve();
+ user_id = resp[0].id;
+ } else {
+ // multiple matching barcodes: let the user pick one
+ var barcode_map = {};
+ var matches = [];
+ var promises = [];
+ var selected_barcode;
+ angular.forEach(resp, function(match) {
+ promises.push(
+ egUser.get(match.id, {useFields : ['home_ou']}).then(function(user) {
+ barcode_map[match.barcode] = user.id();
+ matches.push( {
+ barcode: match.barcode,
+ title: user.first_given_name() + ' ' + user.family_name(),
+ org_name: user.home_ou().name(),
+ org_shortname: user.home_ou().shortname()
+ });
+ })
+ );
+ });
+ return $q.all(promises)
+ .then(function() {
+ $uibModal.open({
+ templateUrl: './circ/share/t_barcode_choice_dialog',
+ controller:
+ ['$scope', '$uibModalInstance',
+ function($scope, $uibModalInstance) {
+ $scope.matches = matches;
+ $scope.ok = function(barcode) {
+ $uibModalInstance.close();
+ selected_barcode = barcode;
+ }
+ $scope.cancel = function() {$uibModalInstance.dismiss()}
+ }],
+ }).result.then(function() {
+ deferred.resolve();
+ user_id = barcode_map[selected_barcode];
+ });
+ });
+ }
+ });
+ return deferred.promise;
+ }
+
+ // call our function to lookup matching users for the scanned barcode
+ handleBarcodeCompletion(args.barcode).then(function() {
+
+ // see if an opt-in request is needed
+ return egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.user.org_unit_opt_in.check',
+ egCore.auth.token(), user_id
+ ).then(function(optInResp) { // opt_in_check
+
+ if (evt = egCore.evt.parse(optInResp)) {
+ alert(evt); // FIXME
+ return;
+ }
+
+ if (optInResp == 2) {
+ // opt-in disallowed at this location by patron's home library
+ $scope.optInRestricted = true;
+ $scope.selectMe = true;
+ egCore.audio.play('warning.patron.opt_in_restricted');
+ return;
+ }
+
+ if (optInResp == 1) {
+ // opt-in handled or not needed
+ return loadPatron(user_id);
+ }
+
+ // opt-in needed, show the opt-in dialog
+ egUser.get(user_id, {useFields : []})
+
+ .then(function(user) { // retrieve user
+ var org = egCore.org.get(user.home_ou());
+ egConfirmDialog.open(
+ egCore.strings.OPT_IN_DIALOG_TITLE,
+ egCore.strings.OPT_IN_DIALOG,
+ { family_name : user.family_name(),
+ first_given_name : user.first_given_name(),
+ org_name : org.name(),
+ org_shortname : org.shortname(),
+ ok : function() { createOptIn(user.id()) },
+ cancel : function() {}
+ }
+ );
+ })
+ })
+ })
+ }
+
+ function countReadyHolds(user_id) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.patron.ready_holds_at_lib.count',
+ egCore.auth.token(),
+ user_id
+ ).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ return 0;
+ } else {
+ return resp;
+ }
+ });
+ }
+
+ function fetchOpenAppointments(user_id) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.open_user_appointments_at_lib.atomic',
+ egCore.auth.token(),
+ user_id
+ ).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ return 0;
+ } else {
+ return resp;
+ }
+ });
+ }
+
+ function mungeAvailableTimes(hash, times) {
+ var existing_present = false;
+ if (angular.isDefined(hash.slot_time) && hash.slot_time !== null) {
+ hash.original_slot_time = hash.slot_time;
+ }
+ hash.available_times = times.map(function(t) {
+ if (angular.isDefined(hash.slot_time) && hash.slot_time !== null && hash.slot_time === t[0]) {
+ existing_present = true;
+ }
+ return {
+ time: t[0],
+ available: t[1],
+ time_fmt: moment(t[0], [moment.ISO_8601, 'HH:mm:ss']).format('LT')
+ };
+ });
+ if (angular.isDefined(hash.slot_time) && hash.slot_time !== null && !existing_present) {
+ hash.available_times.unshift({
+ time: hash.slot_time,
+ available: 0,
+ time_fmt: moment(hash.slot_time, [moment.ISO_8601, 'HH:mm:ss']).format('LT')
+ });
+ }
+ }
+
+ function mungeOneAppointment(c, isNew) {
+ var hash = egCore.idl.toHash(c);
+ if (hash.slot === null) {
+ // coerce to today for the purpose of the
+ // form if no slot time has been set yet
+ hash.slot = new Date().toISOString();
+ hash.slot_time = null;
+ } else {
+ if (!isNew) {
+ hash.slot_time = hash.slot.substring(11, 19);
+ }
+ }
+ hash.slot_date = new Date(hash.slot);
+ if (!isNew) {
+ hash.is_past = (hash.slot_date < new Date());
+ }
+ hash.available_times = [];
+ egCore.net.request (
+ 'open-ils.curbside',
+ 'open-ils.curbside.times_for_date.atomic',
+ egCore.auth.token(),
+ hash.slot.substring(0, 10),
+ ).then(function(times) {
+ mungeAvailableTimes(hash, times);
+ });
+ return hash;
+ }
+
+ function mungeAppointmentList(list) {
+ $scope.openAppointments = list.map(function(c) {
+ var hash = mungeOneAppointment(c);
+ return hash;
+ });
+ }
+
+ function loadPatron(user_id) {
+ $scope.user_id = user_id;
+ patronSvc.getPrimary(user_id);
+ countReadyHolds(user_id).then(function(ct) { $scope.readyHolds = ct });
+ fetchOpenAppointments(user_id).then(function(list) {
+ mungeAppointmentList(list);
+ });
+ }
+
+
+ $scope.minDate = new Date();
+ $scope.refreshAvailableTimes = function(hash) {
+ var dateStr = (new Date(hash.slot_date)).toISOString().substring(0, 10);
+ egCore.net.request (
+ 'open-ils.curbside',
+ 'open-ils.curbside.times_for_date.atomic',
+ egCore.auth.token(),
+ dateStr,
+ ).then(function(times) {
+ mungeAvailableTimes(hash, times);
+ });
+ }
+
+ $scope.startNewAppointment = function() {
+ var slot = new egCore.idl.acsp();
+ slot.slot = new Date().toISOString();
+ slot.patron = $scope.user_id;
+ slot.org = egCore.auth.user().ws_ou();
+ $scope.openAppointments = [ mungeOneAppointment(slot, true) ];
+ }
+
+ $scope.updateAppointment = function(appt) {
+ var op = angular.isDefined(appt.id) ? 'update' : 'create';
+ egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.' + op + '_appointment',
+ egCore.auth.token(),
+ $scope.user_id,
+ (new Date(appt.slot_date)).toISOString().substring(0, 10),
+ appt.slot_time,
+ egCore.auth.user().ws_ou(),
+ appt.notes
+ ).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ if (evt.textcode === 'CURBSIDE_MAX_FOR_TIME') {
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_SAVE_APPOINTMENT_TOO_MANY,
+ { evt_code : evt.code }
+ ));
+ } else {
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_SAVE_APPOINTMENT,
+ { evt_code : evt.code }
+ ));
+ }
+ } else {
+ ngToast.success(egCore.strings.$replace(
+ egCore.strings.SUCCESS_SAVE_APPOINTMENT,
+ { slot_id : resp.id() }
+ ));
+ }
+ fetchOpenAppointments($scope.user_id).then(function(list) {
+ mungeAppointmentList(list);
+ });
+ });
+ }
+
+ function doCancel(id) {
+ egCore.net.request (
+ 'open-ils.curbside',
+ 'open-ils.curbside.delete_appointment',
+ egCore.auth.token(),
+ id
+ ).then(function(resp) {
+ if (!angular.isDefined(resp)) {
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_CANCEL_APPOINTMENT,
+ { slot_id : id, evt_code : 'NO_SUCH_APPOINTMENT' }
+ ));
+ } else if (evt = egCore.evt.parse(resp)) {
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_CANCEL_APPOINTMENT,
+ { slot_id : id, evt_code : evt.code }
+ ));
+ } else {
+ ngToast.success(egCore.strings.$replace(
+ egCore.strings.SUCCESS_CANCEL_APPOINTMENT,
+ { slot_id : id }
+ ));
+ }
+ fetchOpenAppointments($scope.user_id).then(function(list) {
+ mungeAppointmentList(list);
+ });
+ });
+ }
+ $scope.cancelAppointment = function(id) {
+ egConfirmDialog.open(
+ egCore.strings.CONFIRM_CANCEL_TITLE,
+ egCore.strings.CONFIRM_CANCEL_BODY,
+ { slot_id : id,
+ ok : function() { doCancel(id) },
+ cancel : function() {}
+ }
+ );
+ }
+
+ $scope.patron = function() {
+ return patronSvc.current;
+ }
+
+}]}});
--- /dev/null
+angular.module('egCurbsideAppDep')
+
+.directive('egCurbsideStagedManager', function() {
+ return {
+ transclude: true,
+ restrict: 'E',
+ scope: { },
+ templateUrl: './circ/curbside/t_staged_manager',
+ controller:
+ ['$scope','$q','egCurbsideCoreSvc','egCore','egGridDataProvider','egProgressDialog',
+ '$uibModal','$timeout','$location','egConfirmDialog','ngToast','$interval',
+function($scope , $q , egCurbsideCoreSvc , egCore , egGridDataProvider , egProgressDialog ,
+ $uibModal , $timeout , $location , egConfirmDialog , ngToast , $interval) {
+
+ $scope.gridControls = {};
+
+ $scope.wasHandled = {};
+ $scope.refreshNeeded = false;
+
+ latestTime = undefined;
+ var checkRefresh = undefined;
+ function startRefreshCheck() {
+ if (!angular.isDefined(checkRefresh)) {
+ checkRefresh = $interval(function() {
+ egCurbsideCoreSvc.get_latest_staged().then(function(latest) {
+ if (angular.isDefined(latest)) {
+ if (angular.isDefined(latestTime) && latestTime != latest) {
+ $scope.refreshNeeded = true;
+ stopRefreshCheck();
+ }
+ latestTime = latest;
+ }
+ });
+ }, 15000);
+ }
+ }
+ function stopRefreshCheck() {
+ if (angular.isDefined(checkRefresh)) {
+ $interval.cancel(checkRefresh);
+ checkRefresh = undefined;
+ }
+ }
+ this.$onInit = function() {
+ startRefreshCheck();
+ }
+ this.$onDestroy = function() {
+ stopRefreshCheck();
+ }
+
+ $scope.gridDataProvider = egGridDataProvider.instance({
+ get : function(offset, count) {
+ $scope.wasHandled = {};
+ $scope.refreshNeeded = false;
+ startRefreshCheck();
+ return egCurbsideCoreSvc.get_staged(offset, count);
+ }
+ });
+
+ $scope.refresh_staged = function() {
+ $scope.gridControls.refresh();
+ }
+
+ $scope.gridCellHandlers = { };
+ $scope.gridCellHandlers.mark_arrived = function(id) {
+ egCurbsideCoreSvc.mark_arrived(id).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_CURBSIDE_MARK_ARRIVED,
+ { slot_id : id, evt_code : evt.code }
+ ));
+ return;
+ }
+ if (!angular.isDefined(resp)) {
+ ngToast.warning(egCore.strings.$replace(
+ egCore.strings.NOTFOUND_CURBSIDE_MARK_ARRIVED,
+ { slot_id : id }
+ ));
+ return;
+ }
+ ngToast.success(egCore.strings.$replace(
+ egCore.strings.SUCCESS_CURBSIDE_MARK_ARRIVED,
+ { slot_id : id }
+ ));
+ $scope.wasHandled[id] = true;
+ $timeout(function() { $scope.refresh_staged() }, 500);
+ });
+ }
+ $scope.gridCellHandlers.mark_unstaged = function(id) {
+ egCurbsideCoreSvc.mark_unstaged(id).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_CURBSIDE_MARK_UNSTAGED,
+ { slot_id : id, evt_code : evt.code }
+ ));
+ return;
+ }
+ if (!angular.isDefined(resp)) {
+ ngToast.warning(egCore.strings.$replace(
+ egCore.strings.NOTFOUND_CURBSIDE_MARK_UNSTAGED,
+ { slot_id : id }
+ ));
+ return;
+ }
+ ngToast.success(egCore.strings.$replace(
+ egCore.strings.SUCCESS_CURBSIDE_MARK_UNSTAGED,
+ { slot_id : id }
+ ));
+ $scope.wasHandled[id] = true;
+ $timeout(function() { $scope.refresh_staged() }, 500);
+ });
+ }
+ $scope.gridCellHandlers.mark_delivered = function(id) {
+ egProgressDialog.open();
+ egCurbsideCoreSvc.mark_delivered(id).then(function(resp) {
+ egProgressDialog.close();
+ if (evt = egCore.evt.parse(resp)) {
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_CURBSIDE_MARK_DELIVERED,
+ { slot_id : id, evt_code : evt.code }
+ ));
+ return;
+ }
+ if (!angular.isDefined(resp)) {
+ ngToast.warning(egCore.strings.$replace(
+ egCore.strings.NOTFOUND_CURBSIDE_MARK_DELIVERED,
+ { slot_id : id }
+ ));
+ return;
+ }
+ ngToast.success(egCore.strings.$replace(
+ egCore.strings.SUCCESS_CURBSIDE_MARK_DELIVERED,
+ { slot_id : id }
+ ));
+ $scope.wasHandled[id] = true;
+ $timeout(function() { $scope.refresh_staged() }, 500);
+ });
+ }
+ $scope.gridCellHandlers.wasHandled = function(id) {
+ return $scope.wasHandled[id];
+ }
+ $scope.gridCellHandlers.patronIsBlocked = function(usr) {
+ return egCurbsideCoreSvc.patron_blocked(usr);
+ }
+
+}]}});
--- /dev/null
+angular.module('egCurbsideAppDep')
+
+.directive('egCurbsideToBeStagedManager', function() {
+ return {
+ transclude: true,
+ restrict: 'E',
+ scope: { },
+ templateUrl: './circ/curbside/t_to_be_staged_manager',
+ controller:
+ ['$scope','$q','egCurbsideCoreSvc','egCore','egGridDataProvider',
+ '$uibModal','$timeout','$location','egConfirmDialog','ngToast','$interval',
+function($scope , $q , egCurbsideCoreSvc , egCore , egGridDataProvider ,
+ $uibModal , $timeout , $location , egConfirmDialog , ngToast , $interval) {
+
+ $scope.gridControls = {};
+
+ $scope.wasHandled = {};
+ $scope.refreshNeeded = false;
+
+ latestTime = undefined;
+ var checkRefresh = undefined;
+ function startRefreshCheck() {
+ if (!angular.isDefined(checkRefresh)) {
+ checkRefresh = $interval(function() {
+ egCurbsideCoreSvc.get_latest_to_be_staged().then(function(latest) {
+ if (angular.isDefined(latest)) {
+ if (angular.isDefined(latestTime) && latestTime != latest) {
+ $scope.refreshNeeded = true;
+ stopRefreshCheck();
+ }
+ latestTime = latest;
+ }
+ });
+ }, 5000);
+ }
+ }
+ function stopRefreshCheck() {
+ if (angular.isDefined(checkRefresh)) {
+ $interval.cancel(checkRefresh);
+ checkRefresh = undefined;
+ }
+ }
+ this.$onInit = function() {
+ startRefreshCheck();
+ }
+ this.$onDestroy = function() {
+ stopRefreshCheck();
+ }
+
+ $scope.gridDataProvider = egGridDataProvider.instance({
+ get : function(offset, count) {
+ $scope.wasHandled = {};
+ $scope.refreshNeeded = false;
+ startRefreshCheck();
+ return egCurbsideCoreSvc.get_to_be_staged(offset, count);
+ }
+ });
+
+ $scope.refresh_staging = function() {
+ $scope.gridControls.refresh();
+ }
+
+ $scope.gridCellHandlers = { };
+ $scope.gridCellHandlers.mark_staged = function(id) {
+ egCurbsideCoreSvc.mark_staged(id).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_CURBSIDE_MARK_STAGED,
+ { slot_id : id, evt_code : evt.code }
+ ));
+ return;
+ }
+ if (!angular.isDefined(resp)) {
+ ngToast.warning(egCore.strings.$replace(
+ egCore.strings.NOTFOUND_CURBSIDE_MARK_STAGED,
+ { slot_id : id }
+ ));
+ return;
+ }
+ ngToast.success(egCore.strings.$replace(
+ egCore.strings.SUCCESS_CURBSIDE_MARK_STAGED,
+ { slot_id : id }
+ ));
+ $scope.wasHandled[id] = true;
+ $timeout(function() { $scope.refresh_staging() }, 500);
+ });
+ }
+ $scope.gridCellHandlers.wasHandled = function(id) {
+ return $scope.wasHandled[id];
+ }
+ $scope.gridCellHandlers.patronIsBlocked = function(usr) {
+ return egCurbsideCoreSvc.patron_blocked(usr);
+ }
+ $scope.gridCellHandlers.canClaimStaging = function(item) {
+ if ($scope.wasHandled[item.slot_id]) return false;
+ if (!item.slot.stage_staff()) return true;
+ if (item.slot.stage_staff().id() == egCore.auth.user().id()) return false;
+ return true;
+ }
+ $scope.gridCellHandlers.canUnclaimStaging = function(item) {
+ if ($scope.wasHandled[item.slot_id]) return false;
+ if (!item.slot.stage_staff()) return false;
+ if (item.slot.stage_staff().id() == egCore.auth.user().id()) return true;
+ return false;
+ }
+ $scope.gridCellHandlers.claim_staging = function(item) {
+ console.debug('claim');
+ }
+ doClaimStaging = function(item) {
+ var id = item.slot_id;
+ egCurbsideCoreSvc.claim_staging(id).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_CURBSIDE_CLAIM_STAGING,
+ { slot_id : id, evt_code : evt.code }
+ ));
+ return;
+ }
+ if (!angular.isDefined(resp)) {
+ ngToast.warning(egCore.strings.$replace(
+ egCore.strings.NOTFOUND_CURBSIDE_CLAIM_STAGING,
+ { slot_id : id }
+ ));
+ return;
+ }
+
+ item.slot = resp;
+
+ // attempt to avoid a spurious refresh prompt
+ egCurbsideCoreSvc.get_latest_to_be_staged().then(function(latest) {
+ if (angular.isDefined(latest)) {
+ latestTime = latest
+ }
+ });
+
+ ngToast.success(egCore.strings.$replace(
+ egCore.strings.SUCCESS_CURBSIDE_CLAIM_STAGING,
+ { slot_id : id }
+ ));
+ });
+ }
+ $scope.gridCellHandlers.claim_staging = function(item) {
+ if (item.slot.stage_staff() &&
+ item.slot.stage_staff().id() !== egCore.auth.user().id()) {
+ egConfirmDialog.open(
+ egCore.strings.CONFIRM_TAKE_OVER_STAGING_TITLE,
+ egCore.strings.CONFIRM_TAKE_OVER_STAGING_BODY,
+ { slot_id : item.slot_id,
+ other_staff : item.slot.stage_staff().usrname(),
+ ok : function() { doClaimStaging(item) },
+ cancel : function() {}
+ }
+ );
+ } else {
+ doClaimStaging(item);
+ }
+ }
+ $scope.gridCellHandlers.unclaim_staging = function(item) {
+ var id = item.slot_id;
+ egCurbsideCoreSvc.unclaim_staging(id).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ ngToast.danger(egCore.strings.$replace(
+ egCore.strings.FAILED_CURBSIDE_UNCLAIM_STAGING,
+ { slot_id : id, evt_code : evt.code }
+ ));
+ return;
+ }
+ if (!angular.isDefined(resp)) {
+ ngToast.warning(egCore.strings.$replace(
+ egCore.strings.NOTFOUND_CURBSIDE_UNCLAIM_STAGING,
+ { slot_id : id }
+ ));
+ return;
+ }
+
+ item.slot = resp;
+
+ // attempt to avoid a spurious refresh prompt
+ egCurbsideCoreSvc.get_latest_to_be_staged().then(function(latest) {
+ if (angular.isDefined(latest)) {
+ latestTime = latest
+ }
+ });
+
+ ngToast.success(egCore.strings.$replace(
+ egCore.strings.SUCCESS_CURBSIDE_UNCLAIM_STAGING,
+ { slot_id : id }
+ ));
+ });
+ }
+
+}]}});
--- /dev/null
+angular.module('egCurbsideMod', ['egCoreMod'])
+.factory('egCurbsideCoreSvc',
+ ['egCore','orderByFilter','$q','$filter','$uibModal','ngToast','egConfirmDialog',
+function(egCore , orderByFilter , $q , $filter , $uibModal , ngToast , egConfirmDialog) {
+ var service = { };
+
+ service.get_to_be_staged = function(offset, count) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.fetch_to_be_staged',
+ egCore.auth.token(),
+ egCore.auth.user().ws_ou(),
+ count, // yep, count first
+ offset
+ );
+ };
+ service.get_latest_to_be_staged = function() {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.fetch_to_be_staged.latest',
+ egCore.auth.token()
+ ).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ return undefined;
+ } else {
+ return resp;
+ }
+ });
+ }
+
+ service.get_staged = function(offset, count) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.fetch_staged',
+ egCore.auth.token(),
+ egCore.auth.user().ws_ou(),
+ count, // yep, count first
+ offset
+ );
+ };
+ service.get_latest_staged = function() {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.fetch_staged.latest',
+ egCore.auth.token()
+ ).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ return undefined;
+ } else {
+ return resp;
+ }
+ });
+ }
+
+ service.get_arrived = function(offset, count) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.fetch_arrived',
+ egCore.auth.token(),
+ egCore.auth.user().ws_ou(),
+ count, // yep, count first
+ offset
+ );
+ };
+ service.get_latest_arrived = function() {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.fetch_arrived.latest',
+ egCore.auth.token()
+ ).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ return undefined;
+ } else {
+ return resp;
+ }
+ });
+ }
+
+ service.get_delivered = function(offset, count) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.fetch_delivered',
+ egCore.auth.token(),
+ egCore.auth.user().ws_ou(),
+ count, // yep, count first
+ offset
+ );
+ };
+ service.get_latest_delivered = function() {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.fetch_delivered.latest',
+ egCore.auth.token()
+ ).then(function(resp) {
+ if (evt = egCore.evt.parse(resp)) {
+ return undefined;
+ } else {
+ return resp;
+ }
+ });
+ }
+
+ service.mark_staged = function(slot_id) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.mark_staged',
+ egCore.auth.token(),
+ slot_id
+ );
+ }
+ service.mark_unstaged = function(slot_id) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.mark_unstaged',
+ egCore.auth.token(),
+ slot_id
+ );
+ }
+ service.mark_arrived = function(slot_id) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.mark_arrived',
+ egCore.auth.token(),
+ slot_id
+ );
+ }
+ service.mark_delivered = function(slot_id) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.mark_delivered',
+ egCore.auth.token(),
+ slot_id
+ );
+ }
+
+ service.claim_staging = function(slot_id) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.claim_staging',
+ egCore.auth.token(),
+ slot_id
+ );
+ }
+ service.unclaim_staging = function(slot_id) {
+ return egCore.net.request(
+ 'open-ils.curbside',
+ 'open-ils.curbside.unclaim_staging',
+ egCore.auth.token(),
+ slot_id
+ );
+ }
+
+ service.patron_blocked = function(usr) {
+ if (usr.barred() == 't' ||
+ usr.active() == 'f') {
+ return true;
+ }
+ var expire = Date.parse(usr.expire_date());
+ if (expire < new Date()) {
+ return true;
+ }
+ var blocked_by_penalty = false;
+ angular.forEach(usr.standing_penalties(), function(penalty) {
+ if (blocked_by_penalty) return;
+ if (penalty.stop_date()) return;
+ if (!penalty.standing_penalty().block_list()) return;
+ if (penalty.standing_penalty().block_list().match(/CIRC/))
+ blocked_by_penalty = true;
+ });
+ return blocked_by_penalty;
+ }
+
+ return service;
+}])
+
+.directive('egCurbsideHoldsList', function() {
+ return {
+ restrict : 'E',
+ transclude: true,
+ templateUrl : './circ/curbside/t_holds_list',
+ scope : {
+ slot : '=',
+ holds : '=',
+ bibData : '='
+ },
+ controller : [
+ '$scope','egCore',
+ function($scope , egCore) {
+ }
+ ]
+ }
+});
egCore.org.settings([
'ui.staff.max_recent_patrons',
- 'ui.staff.angular_catalog.enabled'
+ 'ui.staff.angular_catalog.enabled',
+ 'circ.curbside'
]).then(function(s) {
var val = s['ui.staff.max_recent_patrons'];
$scope.showRecentPatron = val > 0;
$scope.showAngularCatalog =
s['ui.staff.angular_catalog.enabled'];
+ $scope.enableCurbside =
+ s['circ.curbside'];
}).then(function() {
// need to defer initialization of hotkeys to this point
// as it depends on various settings.