4 * Search, checkout, items out, holds, bills, edit, etc.
7 angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod',
8 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'ngToast',
11 .config(['ngToastProvider', function(ngToastProvider) {
12 ngToastProvider.configure({
13 verticalPosition: 'bottom',
18 .factory("hasPermAt",function(){
22 .config(function($routeProvider, $locationProvider, $compileProvider) {
23 $locationProvider.html5Mode(true);
24 $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
26 // data loaded at startup which only requires an authtoken goes
27 // here. this allows the requests to be run in parallel instead of
28 // waiting until startup has completed.
29 var resolver = {delay : ['egCore','egUser','hasPermAt', function(egCore , egUser , hasPermAt) {
31 // fetch the org settings we care about during egStartup
32 // and toss them into egCore.env as egCore.env.aous[name] = value.
33 // note: only load settings here needed by all tabs; load tab-
34 // specific settings from within their respective controllers
35 egCore.env.classLoaders.aous = function() {
36 return egCore.org.settings([
37 'ui.staff.require_initials.patron_info_notes',
38 'circ.do_not_tally_claims_returned',
41 'ui.circ.show_billing_tab_on_bills',
42 'circ.patron_expires_soon_warning',
43 'ui.circ.items_out.lost',
44 'ui.circ.items_out.longoverdue',
45 'ui.circ.items_out.claimsreturned'
46 ]).then(function(settings) {
47 // local settings are cached within egOrg. Caching them
48 // again in egEnv just simplifies the syntax for access.
49 egCore.env.aous = settings;
53 egCore.env.loadClasses.push('aous');
55 // app-globally modify the default flesh fields for
56 // fleshed user retrieval.
57 if (egUser.defaultFleshFields.indexOf('profile') == -1) {
58 egUser.defaultFleshFields = egUser.defaultFleshFields.concat([
69 return egCore.startup.go().then(function(go_promise) {
70 // FIXME: the following is really just for PatronMessagesCtrl
71 // and PatronCtrl, so we could refactor to avoid calling it
72 // for every controller
73 if (!egCore.auth.token()) return go_promise;
74 return egCore.perm.hasPermFullPathAt('VIEW_USER')
75 .then(function(orgList) {
76 hasPermAt['VIEW_USER'] = orgList;
82 $routeProvider.when('/circ/patron/search', {
83 templateUrl: './circ/patron/t_search',
84 controller: 'PatronSearchCtrl',
88 $routeProvider.when('/circ/patron/bcsearch', {
89 templateUrl: './circ/patron/t_bcsearch',
90 controller: 'PatronBarcodeSearchCtrl',
94 $routeProvider.when('/circ/patron/credentials', {
95 templateUrl: './circ/patron/t_credentials',
96 controller: 'PatronVerifyCredentialsCtrl',
100 $routeProvider.when('/circ/patron/last', {
101 templateUrl: './circ/patron/t_last_patron',
102 controller: 'PatronFetchLastCtrl',
106 // the following require a patron ID
108 $routeProvider.when('/circ/patron/:id/alerts', {
109 templateUrl: './circ/patron/t_alerts',
110 controller: 'PatronAlertsCtrl',
114 $routeProvider.when('/circ/patron/:id/checkout', {
115 templateUrl: './circ/patron/t_checkout',
116 controller: 'PatronCheckoutCtrl',
120 $routeProvider.when('/circ/patron/:id/items_out', {
121 templateUrl: './circ/patron/t_items_out',
122 controller: 'PatronItemsOutCtrl',
126 $routeProvider.when('/circ/patron/:id/holds', {
127 templateUrl: './circ/patron/t_holds',
128 controller: 'PatronHoldsCtrl',
132 $routeProvider.when('/circ/patron/:id/holds/create', {
133 templateUrl: './circ/patron/t_holds_create',
134 controller: 'PatronHoldsCreateCtrl',
138 $routeProvider.when('/circ/patron/:id/holds/:hold_id', {
139 templateUrl: './circ/patron/t_holds',
140 controller: 'PatronHoldsCtrl',
144 $routeProvider.when('/circ/patron/:id/hold/:hold_id', {
145 templateUrl: './circ/patron/t_hold_details',
146 controller: 'PatronHoldDetailsCtrl',
150 $routeProvider.when('/circ/patron/:id/bills', {
151 templateUrl: './circ/patron/t_bills',
152 controller: 'PatronBillsCtrl',
156 $routeProvider.when('/circ/patron/:id/bill/:xact_id/:xact_tab', {
157 templateUrl: './circ/patron/t_xact_details',
158 controller: 'XactDetailsCtrl',
162 $routeProvider.when('/circ/patron/:id/bill_history/:history_tab', {
163 templateUrl: './circ/patron/t_bill_history',
164 controller: 'BillHistoryCtrl',
168 $routeProvider.when('/circ/patron/:id/messages', {
169 templateUrl: './circ/patron/t_messages',
170 controller: 'PatronMessagesCtrl',
174 $routeProvider.when('/circ/patron/:id/edit', {
175 templateUrl: './circ/patron/t_edit',
176 controller: 'PatronRegCtrl',
180 $routeProvider.when('/circ/patron/:id/credentials', {
181 templateUrl: './circ/patron/t_credentials',
182 controller: 'PatronVerifyCredentialsCtrl',
186 $routeProvider.when('/circ/patron/:id/triggered_events', {
187 templateUrl: './circ/patron/t_triggered_events',
188 controller: 'PatronTriggeredEventsCtrl',
192 $routeProvider.when('/circ/patron/:id/message_center', {
193 templateUrl: './circ/patron/t_message_center',
194 controller: 'PatronMessageCenterCtrl',
198 $routeProvider.when('/circ/patron/:id/edit_perms', {
199 templateUrl: './circ/patron/t_edit_perms',
200 controller: 'PatronPermsCtrl',
204 $routeProvider.when('/circ/patron/:id/group', {
205 templateUrl: './circ/patron/t_group',
206 controller: 'PatronGroupCtrl',
210 $routeProvider.when('/circ/patron/:id/stat_cats', {
211 templateUrl: './circ/patron/t_stat_cats',
212 controller: 'PatronStatCatsCtrl',
216 $routeProvider.when('/circ/patron/:id/surveys', {
217 templateUrl: './circ/patron/t_surveys',
218 controller: 'PatronSurveyCtrl',
222 $routeProvider.when('/circ/patron/:id/hold_subscriptions', {
223 templateUrl: './circ/patron/t_hold_subscriptions',
224 controller: 'HoldSubscriptionsCtrl',
228 $routeProvider.otherwise({redirectTo : '/circ/patron/search'});
232 * Manages tabbed patron view.
233 * This is the parent scope of all patron tab scopes.
236 .controller('PatronCtrl',
237 ['$scope','$q','$location','$filter','egCore','egNet','egUser','egAlertDialog',
238 'egConfirmDialog','egPromptDialog','patronSvc','egCirc','hasPermAt','ngToast',
239 function($scope, $q , $location , $filter , egCore , egNet , egUser , egAlertDialog ,
240 egConfirmDialog , egPromptDialog , patronSvc , egCirc , hasPermAt, ngToast) {
242 $scope.is_patron_edit = function() {
243 return Boolean($location.path().match(/patron\/\d+\/edit$/));
246 $scope.alert_penalties = function() {
247 return patronSvc.alert_penalties;
250 // To support the fixed position patron edit actions bar,
251 // its markup has to live outside the scope of the patron
252 // edit controller. Insert a scope blob here that can be
253 // modifed from within the patron edit controller.
254 $scope.edit_passthru = {};
256 // returns true if a redirect occurs
257 function redirectToAlertPanel() {
259 if (patronSvc.alertsShown()) return false;
261 // if the patron has any unshown alerts, show them now
262 if (patronSvc.hasAlerts &&
263 !$location.path().match(/alerts$/)) {
266 .path('/circ/patron/' + patronSvc.current.id() + '/alerts')
267 .search('card', null);
271 // no alert required. If the patron has fines and the show-bills
272 // OUS is applied, direct to the bills page.
273 if ($scope.patron_stats().fines.balance_owed > 0 // TODO: != 0 ?
274 && egCore.env.aous['ui.circ.show_billing_tab_on_bills']
275 && !$location.path().match(/bills$/)) {
277 $scope.tab = 'bills';
279 .path('/circ/patron/' + patronSvc.current.id() + '/bills')
280 .search('card', null);
288 // called after each route-specified controller is instantiated.
289 // this doubles as a way to inform the top-level controller that
290 // egStartup.go() has completed, which means we are clear to
291 // fetch the patron, etc.
292 $scope.initTab = function(tab, patron_id) {
293 console.log('init tab ' + tab);
295 $scope.aous = egCore.env.aous;
296 $scope.auth_user_id = egCore.auth.user().id();
298 if (tab == 'search') {
299 egCirc.reset(); // clear out auto-override and auto-skip selections when switching patrons
303 $scope.patron_id = patron_id;
304 return patronSvc.setPrimary($scope.patron_id)
306 // the page title context label comes from the tab.
307 egCore.strings.setPageTitle(
308 egCore.strings.PAGE_TITLE_PATRON_NAME,
309 egCore.strings['PAGE_TITLE_PATRON_' + tab.toUpperCase()],
310 { lname : patronSvc.current.family_name(),
311 fname : patronSvc.current.first_given_name(),
312 mname : patronSvc.current.second_given_name()
316 .then(function() {return patronSvc.checkAlerts()})
317 .then(redirectToAlertPanel)
319 if ($scope.patron().locale() !== null) {
320 $scope.locale_name = $scope.patron().locale().name();
321 $scope.hasLocaleName = $scope.locale_name.length > 0;
325 $scope.ident_type_name = $scope.patron().ident_type().name();
326 $scope.hasIdentTypeName = $scope.ident_type_name.length > 0;
329 // No patron, use the tab name as the page title.
330 egCore.strings.setPageTitle(
331 egCore.strings['PAGE_TITLE_PATRON_' + tab.toUpperCase()]);
336 $scope._show_dob = {};
337 $scope.show_dob = function (val) {
338 if ($scope.patron()) {
339 if (typeof val != 'undefined') $scope._show_dob[$scope.patron().id()] = val;
340 return $scope._show_dob[$scope.patron().id()];
342 return !egCore.env.aous['circ.obscure_dob'];
345 $scope.obscure_dob = function() {
346 return egCore.env.aous && egCore.env.aous['circ.obscure_dob'];
348 $scope.now_show_dob = function() {
349 return egCore.env.aous && egCore.env.aous['circ.obscure_dob'] ?
350 $scope.show_dob() : true;
353 $scope.patron = function() { return patronSvc.current }
354 $scope.visible_notes = function() {
355 var p = patronSvc.current;
357 var org_ids = hasPermAt['VIEW_USER'];
358 var filtered_notes = p.notes().filter(function(n) { return org_ids.indexOf(n.org_unit()) > -1; });
359 return filtered_notes;
363 $scope.patron_stats = function() { return patronSvc.patron_stats }
364 $scope.summary_stat_cats = function() { return patronSvc.summary_stat_cats }
365 $scope.hasAlerts = function() { return patronSvc.hasAlerts }
366 $scope.isPatronExpired = function() { return patronSvc.patronExpired }
367 $scope.doesPatronExpireSoon = function() { return patronSvc.patronExpiresSoon }
369 $scope.print_address = function(addr) {
372 template : 'patron_address',
374 patron : egCore.idl.toHash(patronSvc.current),
375 address : egCore.idl.toHash(addr)
380 $scope.copy_address = function(addr) {
381 // Alas, navigator.clipboard is not yet supported in FF and others.
382 var lNode = document.querySelector('#patron-address-copy-' + addr.id());
384 // Un-hide the textarea just long enough to copy its data.
385 // Using node.style instead of ng-show/ng-hide in hopes it
386 // will be quicker, so the user never sees the textarea.
387 lNode.style.visibility = 'visible';
391 if (!document.execCommand('copy')) {
392 console.error('Copy command failed');
395 lNode.style.visibility = 'hidden';
398 $scope.toggle_expand_summary = function() {
399 if ($scope.collapsePatronSummary) {
400 $scope.collapsePatronSummary = false;
401 egCore.hatch.removeItem('eg.circ.patron.summary.collapse');
403 $scope.collapsePatronSummary = true;
404 egCore.hatch.setItem('eg.circ.patron.summary.collapse', true);
408 // always expand the patron summary in the search UI, regardless
409 // of stored preference.
410 $scope.collapse_summary = function() {
411 return $scope.tab != 'search' && $scope.collapsePatronSummary;
414 function _purge_account(dest_usr,override) {
417 'open-ils.actor.user.delete' + (override ? '.override' : ''),
419 $scope.patron().id(),
421 ).then(function(resp){
422 if (evt = egCore.evt.parse(resp)) {
423 if (evt.code == '2004' /* ACTOR_USER_DELETE_OPEN_XACTS */) {
424 egConfirmDialog.open(
425 egCore.strings.PATRON_PURGE_CONFIRM_TITLE, egCore.strings.PATRON_PURGE_OVERRIDE_PROMPT,
427 _purge_account(dest_usr,true);
434 location.href = egCore.env.basePath + '/circ/patron/search';
439 function _purge_account_with_destination(dest_barcode) {
440 egCore.pcrud.search('ac', {barcode : dest_barcode})
441 .then(function(card) {
443 egAlertDialog.open(egCore.strings.PATRON_PURGE_STAFF_BAD_BARCODE);
445 _purge_account(card.usr());
450 $scope.purge_account = function() {
451 egConfirmDialog.open(
452 egCore.strings.PATRON_PURGE_CONFIRM_TITLE, egCore.strings.PATRON_PURGE_CONFIRM,
454 egConfirmDialog.open(
455 egCore.strings.PATRON_PURGE_CONFIRM_TITLE, egCore.strings.PATRON_PURGE_LAST_CHANCE,
459 'open-ils.actor.user.has_work_perm_at',
460 egCore.auth.token(), 'STAFF_LOGIN', $scope.patron().id()
461 ).then(function(resp) {
462 var is_staff = resp.length > 0;
465 egCore.strings.PATRON_PURGE_STAFF_PROMPT,
466 null, // TODO: this would be cool if it worked: egCore.auth.user().card().barcode(),
467 {ok : function(barcode) {_purge_account_with_destination(barcode)}}
479 $scope.refreshPenalties = function() {
483 'open-ils.actor.user.penalties.update',
484 egCore.auth.token(), $scope.patron().id()
486 ).then(function(resp) {
488 if (evt = egCore.evt.parse(resp)) {
489 ngToast.warning(egCore.strings.PENALTY_REFRESH_FAILED);
493 ngToast.create(egCore.strings.PENALTY_REFRESH_SUCCESS);
495 // Depending on which page we're on (e.g. Note/Messages) we
496 // may need to force a full data refresh.
497 setTimeout(function() { location.href = location.href; });
501 egCore.hatch.getItem('eg.circ.patron.summary.collapse')
502 .then(function(val) {$scope.collapsePatronSummary = Boolean(val)});
505 .controller('PatronBarcodeSearchCtrl',
506 ['$scope','$location','egCore','egConfirmDialog','egUser','patronSvc','$uibModal','$q',
507 function($scope , $location , egCore , egConfirmDialog , egUser , patronSvc , $uibModal , $q) {
508 $scope.selectMe = true; // focus text input
509 patronSvc.clearPrimary(); // clear the default user
511 // jump to the patron checkout UI
512 function loadPatron(user_id) {
513 egCore.audio.play('success.patron.by_barcode');
515 .path('/circ/patron/' + user_id + '/checkout')
516 .search('card', $scope.args.barcode);
517 patronSvc.search_barcode = $scope.args.barcode;
520 // create an opt-in=yes response for the loaded user
521 function createOptIn(user_id) {
524 'open-ils.actor.user.org_unit_opt_in.create',
525 egCore.auth.token(), user_id).then(function(resp) {
526 if (evt = egCore.evt.parse(resp)) return alert(evt);
532 $scope.submitBarcode = function(args) {
533 $scope.bcNotFound = null;
534 $scope.optInRestricted = false;
535 if (!args.barcode) return;
536 args.barcode = args.barcode.replace(/^\s/g,'');
537 args.barcode = args.barcode.replace(/\s$/g,'');
538 // blur so next time it's set to true it will re-apply select()
539 $scope.selectMe = false;
543 // given a scanned barcode, this function finds any matching users
544 // and handles multiple matches due to barcode completion
545 function handleBarcodeCompletion(scanned_barcode) {
546 var deferred = $q.defer();
550 'open-ils.actor.get_barcodes',
551 egCore.auth.token(), egCore.auth.user().ws_ou(),
552 'actor', scanned_barcode)
554 .then(function(resp) { // get_barcodes
556 if (evt = egCore.evt.parse(resp)) {
562 if (!resp || !resp[0]) {
563 $scope.bcNotFound = args.barcode;
564 $scope.selectMe = true;
565 egCore.audio.play('warning.patron.not_found');
570 if (resp.length == 1) {
571 // exactly one matching barcode: return it
573 user_id = resp[0].id;
575 // multiple matching barcodes: let the user pick one
576 var barcode_map = {};
579 var selected_barcode;
580 angular.forEach(resp, function(match) {
582 egUser.get(match.id, {useFields : ['home_ou']}).then(function(user) {
583 barcode_map[match.barcode] = user.id();
585 barcode: match.barcode,
586 title: user.first_given_name() + ' ' + user.family_name(),
587 org_name: user.home_ou().name(),
588 org_shortname: user.home_ou().shortname()
593 return $q.all(promises)
596 templateUrl: './circ/share/t_barcode_choice_dialog',
598 ['$scope', '$uibModalInstance',
599 function($scope, $uibModalInstance) {
600 $scope.matches = matches;
601 $scope.ok = function(barcode) {
602 $uibModalInstance.close();
603 selected_barcode = barcode;
605 $scope.cancel = function() {$uibModalInstance.dismiss()}
607 }).result.then(function() {
609 user_id = barcode_map[selected_barcode];
614 return deferred.promise;
617 // call our function to lookup matching users for the scanned barcode
618 handleBarcodeCompletion(args.barcode).then(function() {
620 // see if an opt-in request is needed
621 return egCore.net.request(
623 'open-ils.actor.user.org_unit_opt_in.check',
624 egCore.auth.token(), user_id
625 ).then(function(optInResp) { // opt_in_check
627 if (evt = egCore.evt.parse(optInResp)) {
632 if (optInResp == 2) {
633 // opt-in disallowed at this location by patron's home library
634 $scope.optInRestricted = true;
635 $scope.selectMe = true;
636 egCore.audio.play('warning.patron.opt_in_restricted');
640 if (optInResp == 1) {
641 // opt-in handled or not needed
642 return loadPatron(user_id);
645 // opt-in needed, show the opt-in dialog
646 egUser.get(user_id, {useFields : []})
648 .then(function(user) { // retrieve user
649 var org = egCore.org.get(user.home_ou());
650 egConfirmDialog.open(
651 egCore.strings.OPT_IN_DIALOG_TITLE,
652 egCore.strings.OPT_IN_DIALOG,
653 { family_name : user.family_name(),
654 first_given_name : user.first_given_name(),
655 org_name : org.name(),
656 org_shortname : org.shortname(),
657 ok : function() { createOptIn(user.id()) },
658 cancel : function() {}
669 * Manages patron search
671 .controller('PatronSearchCtrl',
672 ['$scope','$q','$routeParams','$timeout','$window','$location','egCore','ngToast',
673 '$filter','egUser', 'patronSvc','egGridDataProvider','$document','bucketSvc',
674 'egPatronMerge','egProgressDialog','$controller','$interpolate','$uibModal',
675 function($scope, $q, $routeParams, $timeout, $window, $location, egCore , ngToast,
676 $filter, egUser, patronSvc , egGridDataProvider , $document , bucketSvc,
677 egPatronMerge , egProgressDialog , $controller , $interpolate , $uibModal) {
679 angular.extend(this, $controller('BasePatronSearchCtrl', {$scope : $scope}));
680 $scope.initTab('search');
682 $scope.gridControls = {
683 activateItem : function(item) {
684 $location.path('/circ/patron/' + item.id() + '/checkout');
686 selectedItems : function() { return [] }
689 $scope.bucketSvc = bucketSvc;
690 $scope.bucketSvc.fetchUserBuckets();
691 $scope.bucketSvc.fetchUserSubscriptions();
692 $scope.addToBucket = function(item, data, recs) {
693 if (recs.length == 0) return;
695 var failed_count = 0;
696 var promise = $q.when();
697 angular.forEach(recs,
699 var item = new egCore.idl.cubi();
700 item.bucket(data.id());
701 item.target_user(rec.id());
702 promise = promise.then(function() {
703 return egCore.net.request(
705 'open-ils.actor.container.item.create',
706 egCore.auth.token(), 'user', item, 1
709 function(){ added_count++ },
710 function(){ failed_count++ }
715 promise.then( function () {
716 if (added_count) ngToast.create($interpolate(egCore.strings.BUCKET_ADD_SUCCESS)({ count: ''+added_count, name: data.name()} ));
717 if (failed_count) ngToast.warning($interpolate(egCore.strings.BUCKET_ADD_FAIL)({ count: ''+failed_count, name: data.name() } ));
721 var temp_scope = $scope;
722 $scope.openCreateBucketDialog = function() {
724 templateUrl: './circ/patron/bucket/t_bucket_create',
727 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
728 $scope.focusMe = true;
729 $scope.ok = function(args) { $uibModalInstance.close(args) }
730 $scope.cancel = function () { $uibModalInstance.dismiss() }
732 }).result.then(function (args) {
733 if (!args || !args.name) return;
734 bucketSvc.createBucket(args.name, args.desc).then(
737 $scope.bucketSvc.fetchBucket(id).then(function (b) {
741 $scope.gridControls.selectedItems()
743 $scope.bucketSvc.fetchUserBuckets(true);
752 function() {return $scope.gridControls.selectedItems()},
755 patronSvc.setPrimary(null, list[0]);
760 $scope.need_one_selected = function() {
761 var items = $scope.gridControls.selectedItems();
762 return (items.length > 0) ? false : true;
764 $scope.need_two_selected = function() {
765 var items = $scope.gridControls.selectedItems();
766 return (items.length == 2) ? false : true;
768 $scope.merge_patrons = function() {
769 var items = $scope.gridControls.selectedItems();
770 if (items.length != 2) return false;
773 angular.forEach(items, function(i) {
774 patron_ids.push(i.id());
776 egPatronMerge.do_merge(patron_ids).then(
778 // ensure that we're not drawing from cached
779 // resuts, as a successful merge just deleted a
781 delete patronSvc.lastSearch;
782 $scope.gridControls.refresh();
785 if (evt && evt.textcode == 'MERGE_SELF_NOT_ALLOWED') {
786 ngToast.warning(egCore.strings.MERGE_SELF_NOT_ALLOWED);
797 .controller('PatronMessagesCtrl',
798 ['$scope','$q','$routeParams','egCore','$uibModal','egConfirmDialog','patronSvc','egCirc','hasPermAt',
799 function($scope , $q , $routeParams, egCore , $uibModal , egConfirmDialog , patronSvc , egCirc , hasPermAt ) {
800 $scope.initTab('messages', $routeParams.id);
801 var usr_id = $routeParams.id;
802 var org_ids = hasPermAt.VIEW_USER;
804 // setup date filters
805 var start = new Date(); // now - 1 year
806 start.setFullYear(start.getFullYear() - 1),
809 end_date : new Date()
812 function date_range() {
813 var start = $scope.dates.start_date.toISOString().replace(/T.*/,'');
814 var end = $scope.dates.end_date.toISOString().replace(/T.*/,'');
815 var today = new Date().toISOString().replace(/T.*/,'');
816 if (end == today) end = 'now';
822 var activeGrid = $scope.activeGridControls = {
823 setSort : function() {
824 return [{'create_date' : 'DESC'}];
826 setQuery : function() {
832 {stop_date : {'>' : 'now'}}
836 activateItem : function(selected) {
837 // activateItem returns a single row.
838 $scope.editPenalty([selected])
842 var archiveGrid = $scope.archiveGridControls = {
843 setSort : function() {
844 return [{'create_date' : 'DESC'}];
846 watchQuery : function() {
850 stop_date : {'<=' : 'now'},
851 create_date : {between : date_range()}
854 activateItem : function(selected) {
855 // activateItem returns a single row.
856 $scope.editPenalty([selected])
860 $scope.test_for_disable_remove_penalty = function() {
861 var selected = $scope.activeGridControls.selectedItems();
862 var found_pub_and_read_and_not_deleted = false;
863 angular.forEach(selected, function(s) {
864 if (Boolean(s.pub == 't') && Boolean(s.read_date) && !Boolean(s.deleted == 't')) {
865 found_pub_and_read_and_not_deleted = true;
868 return found_pub_and_read_and_not_deleted;
871 $scope.removePenalty = function(selected) {
872 if (selected.length == 0) return;
876 // figure out the view components
879 angular.forEach(selected, function(s) {
880 if (s.aum_id) { aum_ids.push(s.aum_id); }
881 if (s.ausp_id) { ausp_ids.push(s.ausp_id); }
884 // fetch all of them since trying to pull them
885 // off of patronSvc.current isn't reliable
886 if (ausp_ids.length > 0) {
888 egCore.pcrud.search('ausp',
890 {atomic : true, authoritative : true}
891 ).then(function(penalties) {
892 return egCore.pcrud.remove(penalties);
896 if (aum_ids.length > 0) {
898 egCore.pcrud.search('aum',
900 {atomic : true, authoritative : true}
901 ).then(function(messages) {
902 return egCore.pcrud.remove(messages);
906 $q.all(promises).then(function() {
907 activeGrid.refresh();
908 archiveGrid.refresh();
909 // force a refresh of the user
910 patronSvc.setPrimary(patronSvc.current.id(), null, true);
914 egCore.audio.play('warning.circ.remove_note');
915 egConfirmDialog.open(
916 egCore.strings.CONFIRM_REMOVE_NOTE, '',
917 { //xactIds : ''+ids,
925 $scope.archivePenalty = function(selected) {
926 if (selected.length == 0) return;
930 // figure out the view components
933 angular.forEach(selected, function(s) {
934 if (s.aum_id) { aum_ids.push(s.aum_id); }
935 if (s.ausp_id) { ausp_ids.push(s.ausp_id); }
938 // fetch all of them since trying to pull them
939 // off of patronSvc.current isn't reliable
940 if (ausp_ids.length > 0) {
942 egCore.pcrud.search('ausp',
944 {atomic : true, authoritative : true}
945 ).then(function(penalties) {
946 angular.forEach(penalties, function(p) {
949 return egCore.pcrud.update(penalties);
953 if (aum_ids.length > 0) {
955 egCore.pcrud.search('aum',
957 {atomic : true, authoritative : true}
958 ).then(function(messages) {
959 angular.forEach(messages, function(m) {
962 return egCore.pcrud.update(messages);
966 $q.all(promises).then(function() {
967 activeGrid.refresh();
968 archiveGrid.refresh();
969 // force a refresh of the user
970 patronSvc.setPrimary(patronSvc.current.id(), null, true);
974 egCore.audio.play('warning.circ.archive_note');
975 egConfirmDialog.open(
976 egCore.strings.CONFIRM_ARCHIVE_NOTE, '',
977 { //xactIds : ''+ids,
985 $scope.unarchivePenalty = function(selected) {
986 if (selected.length == 0) return;
990 // figure out the view components
993 angular.forEach(selected, function(s) {
994 if (s.aum_id) { aum_ids.push(s.aum_id); }
995 if (s.ausp_id) { ausp_ids.push(s.ausp_id); }
998 // fetch all of them since trying to pull them
999 // off of patronSvc.current isn't reliable
1000 if (ausp_ids.length > 0) {
1002 egCore.pcrud.search('ausp',
1003 {id : ausp_ids}, {},
1004 {atomic : true, authoritative : true}
1005 ).then(function(penalties) {
1006 angular.forEach(penalties, function(p) {
1009 return egCore.pcrud.update(penalties);
1013 if (aum_ids.length > 0) {
1015 egCore.pcrud.search('aum',
1017 {atomic : true, authoritative : true}
1018 ).then(function(messages) {
1019 angular.forEach(messages, function(m) {
1022 return egCore.pcrud.update(messages);
1026 $q.all(promises).then(function() {
1027 activeGrid.refresh();
1028 archiveGrid.refresh();
1029 // force a refresh of the user
1030 patronSvc.setPrimary(patronSvc.current.id(), null, true);
1034 egCore.audio.play('warning.circ.unarchive_note');
1035 egConfirmDialog.open(
1036 egCore.strings.CONFIRM_UNARCHIVE_NOTE, '',
1037 { //xactIds : ''+ids,
1045 // leverage egEnv for caching
1046 function fetchPenaltyTypes() {
1048 return $q.when(egCore.env.csp.list);
1049 return egCore.pcrud.search(
1050 // id <= 100 are reserved for system use
1051 'csp', {id : {'>': 100}}, {}, {atomic : true})
1052 .then(function(penalties) {
1053 egCore.env.absorbList(penalties, 'csp');
1058 $scope.createPenalty = function() {
1059 egCirc.create_penalty(usr_id).then(function() {
1060 activeGrid.refresh();
1061 // force a refresh of the user, since they may now
1062 // have blocking penalties, etc.
1063 patronSvc.setPrimary(patronSvc.current.id(), null, true);
1067 $scope.editPenalty = function(selected) {
1068 if (selected.length == 0) return;
1071 // figure out the view components
1072 var aum_ids = []; var aum_objs = {};
1073 var ausp_ids = []; var ausp_objs = {};
1075 angular.forEach(selected, function(s) {
1076 if (s.aum_id) { aum_ids.push(s.aum_id); }
1077 if (s.ausp_id) { ausp_ids.push(s.ausp_id); }
1078 pairs.push( { aum_id : s.aum_id, ausp_id : s.ausp_id } );
1081 // fetch all of them since trying to pull them
1082 // off of patronSvc.current isn't reliable
1083 // (we want deleted user messages too)
1084 if (ausp_ids.length > 0) {
1086 egCore.pcrud.search('ausp',
1087 {id : ausp_ids}, {},
1088 {atomic : true, authoritative : true}
1089 ).then(function(penalties) {
1090 angular.forEach(penalties, function(p) {
1091 ausp_objs[p.id()] = p;
1097 if (aum_ids.length > 0) {
1099 egCore.pcrud.search('aum',
1106 {atomic : true, authoritative : true}
1107 ).then(function(messages) {
1108 angular.forEach(messages, function(m) {
1109 aum_objs[m.id()] = m;
1115 $q.all(promises).then(function() {
1116 angular.forEach(pairs, function(pair) {
1117 egCirc.edit_penalty(ausp_objs[pair.ausp_id],aum_objs[pair.aum_id]).then(function() {
1118 activeGrid.refresh();
1119 // force a refresh of the user, since they may now
1120 // have blocking penalties, etc.
1121 patronSvc.setPrimary(patronSvc.current.id(), null, true);
1130 * Credentials tester
1132 .controller('PatronVerifyCredentialsCtrl',
1133 ['$scope','$routeParams','$location','egCore',
1134 function($scope, $routeParams , $location , egCore) {
1135 $scope.verified = null;
1136 $scope.focusMe = true;
1138 // called with a patron, pre-populate the form args
1139 $scope.initTab('other', $routeParams.id).then(
1141 if ($routeParams.id && $scope.patron()) {
1142 $scope.prepop = true;
1143 $scope.username = $scope.patron().usrname();
1144 $scope.barcode = $scope.patron().card().barcode();
1146 $scope.username = '';
1147 $scope.barcode = '';
1148 $scope.password = '';
1153 // verify login credentials
1154 $scope.verify = function() {
1155 $scope.verified = null;
1156 $scope.notFound = false;
1160 'open-ils.actor.verify_user_password',
1161 egCore.auth.token(), $scope.barcode,
1162 $scope.username, hex_md5($scope.password || '')
1164 ).then(function(resp) {
1165 $scope.focusMe = true;
1166 if (evt = egCore.evt.parse(resp)) {
1168 } else if (resp == 1) {
1169 $scope.verified = true;
1171 $scope.verified = false;
1176 // load the main patron UI for the provided username or barcode
1177 $scope.load = function($event) {
1178 $scope.notFound = false;
1179 $scope.verified = null;
1183 'open-ils.actor.user.retrieve_id_by_barcode_or_username',
1184 egCore.auth.token(), $scope.barcode, $scope.username
1186 ).then(function(resp) {
1189 $location.path('/circ/patron/' + resp + '/checkout');
1193 // something went wrong...
1194 $scope.focusMe = true;
1195 if (evt = egCore.evt.parse(resp)) {
1196 if (evt.textcode == 'ACTOR_USR_NOT_FOUND') {
1197 $scope.notFound = true;
1206 // load() button sits within the verify form.
1207 // avoid submitting the verify() form action on load()
1208 $event.preventDefault();
1212 .controller('PatronAlertsCtrl',
1213 ['$scope','$routeParams','$location','egCore','patronSvc',
1214 function($scope, $routeParams , $location , egCore , patronSvc) {
1216 $scope.initTab('other', $routeParams.id)
1218 $scope.patronExpired = patronSvc.patronExpired;
1219 $scope.patronExpiresSoon = patronSvc.patronExpiresSoon;
1220 $scope.retrievedWithInactive = patronSvc.fetchedWithInactiveCard();
1221 $scope.invalidAddresses = patronSvc.invalidAddresses;
1226 .controller('HoldSubscriptionsCtrl',
1227 ['$scope','$q','$routeParams','$location','egCore','patronSvc','bucketSvc','egGridDataProvider','egConfirmDialog','$timeout','$window',
1228 function($scope, $q , $routeParams , $location , egCore , patronSvc, bucketSvc, egGridDataProvider, egConfirmDialog, $timeout, $window) {
1230 $scope.initTab('other', $routeParams.id);
1232 $scope.bucket_ids = [];
1233 $scope.bucket_items = [];
1234 $scope.buckets = [];
1236 $scope.gridControls = {
1237 activateItem : function (item) {
1238 var url = $location.absUrl().replace(
1239 /\/circ\/patron\/.*/,
1240 '/cat/bucket/batch_hold/view/' + item.id());
1241 $window.open(url, '_blank').focus();
1245 $scope.gridDataProvider = egGridDataProvider.instance({
1246 get : function(offset, count) {
1247 return this.arrayNotifier($scope.buckets, offset, count);
1251 function fetchSubscriptions() {
1252 $scope.bucket_ids = [];
1253 $scope.bucket_items = [];
1254 $scope.buckets = [];
1255 egCore.pcrud.search('cubi',
1256 { target_user : $routeParams.id }
1259 if ($scope.bucket_ids.length > 0) {
1260 egCore.pcrud.search('cub',
1261 { id : $scope.bucket_ids, btype : 'hold_subscription' }
1263 function() { $scope.gridControls.refresh() },
1266 $scope.buckets.push(b);
1267 b.items( $scope.bucket_items.filter(i => i.bucket() == b.id()) );
1271 $scope.gridControls.refresh();
1276 $scope.bucket_ids.push(i.bucket());
1277 $scope.bucket_items.push(i);
1282 $scope.removeSubscriptions = function (buckets) {
1283 return egConfirmDialog.open(
1284 egCore.strings.REMOVE_HOLD_SUBSCRIPTIONS,'',{}
1285 ).result.then(function() {
1288 angular.forEach(buckets, function(b) {
1289 angular.forEach(b.items(), function (i) {
1290 promises.push(bucketSvc.detachUser(i.id()));
1294 $q.all(promises).then(fetchSubscriptions);
1298 $timeout(fetchSubscriptions);
1302 .controller('PatronGroupCtrl',
1303 ['$scope','$routeParams','$q','$window','$timeout','$location','egCore',
1304 'patronSvc','$uibModal','egPromptDialog','egConfirmDialog',
1305 function($scope, $routeParams , $q , $window , $timeout, $location , egCore ,
1306 patronSvc , $uibModal , egPromptDialog , egConfirmDialog) {
1308 var usr_id = $routeParams.id;
1310 $scope.totals = {owed : 0, total_out : 0, overdue : 0}
1312 var grid = $scope.gridControls = {
1313 activateItem : function(item) {
1314 $location.path('/circ/patron/' + item.id + '/checkout');
1316 itemRetrieved : function(item) {
1318 if (item.id == patronSvc.current.id()) {
1319 item.stats = patronSvc.patron_stats;
1322 // flesh stats for other group members
1323 patronSvc.getUserStats(item.id).then(function(stats) {
1325 $scope.totals.total_out += stats.checkouts.total_out;
1326 $scope.totals.overdue += stats.checkouts.overdue;
1330 setSort : function() {
1331 return ['create_date'];
1333 watchQuery: function() {
1334 if (patronSvc.current) {
1336 usrgroup : patronSvc.current.usrgroup(),
1344 $scope.initTab('other', $routeParams.id)
1345 .then(function(redirect) {
1346 // if we are redirecting to the alerts page, avoid updating the
1348 if (redirect) return;
1349 // let initTab() fetch the user first so we can know the usrgroup
1350 $scope.totals.owed = patronSvc.patron_stats.fines.group_balance_owed;
1353 $scope.removeFromGroup = function(selected) {
1355 angular.forEach(selected, function(user) {
1356 console.debug('removing user ' + user.id + ' from group');
1361 'open-ils.actor.usergroup.new',
1362 egCore.auth.token(), user.id, true
1367 $q.all(promises).then(function() {grid.refresh()});
1370 function addUserToGroup(user) {
1371 user.usrgroup(patronSvc.current.usrgroup());
1372 user.ischanged(true);
1375 'open-ils.actor.patron.update',
1376 egCore.auth.token(), user
1378 ).then(function() {grid.refresh()});
1381 // fetch each user ("selected" has flattened users)
1382 // update the usrgroup, then update the user object
1383 // After all updates are complete, refresh the grid.
1384 function moveUsersToGroup(target_user, selected) {
1387 angular.forEach(selected, function(user) {
1389 egCore.pcrud.retrieve('au', user.id)
1391 u.usrgroup(target_user.usrgroup());
1393 return egCore.net.request(
1395 'open-ils.actor.patron.update',
1396 egCore.auth.token(), u
1402 $q.all(promises).then(function() {grid.refresh()});
1405 function showMoveToGroupConfirm(barcode, selected, outbound) {
1408 egCore.pcrud.search('ac', {barcode : barcode})
1410 // fetch the fleshed user
1411 .then(function(card) {
1413 if (!card) return; // TODO: warn user
1415 egCore.pcrud.retrieve('au', card.usr())
1416 .then(function(user) {
1419 templateUrl: './circ/patron/t_move_to_group_dialog',
1422 '$scope','$uibModalInstance',
1423 function($scope , $uibModalInstance) {
1425 $scope.selected = selected;
1426 $scope.outbound = outbound;
1428 function(count) { $uibModalInstance.close() }
1430 function () { $uibModalInstance.dismiss() }
1433 }).result.then(function() {
1435 moveUsersToGroup(user, selected);
1437 addUserToGroup(user);
1444 // selected == move selected patrons to another patron's group
1445 // !selected == patron from a different group moves into our group
1446 function moveToGroup(selected, outbound) {
1447 egPromptDialog.open(
1448 egCore.strings.GROUP_ADD_USER, '',
1449 {ok : function(value) {
1451 showMoveToGroupConfirm(value, selected, outbound);
1456 $scope.moveToGroup = function() { moveToGroup([], false) };
1457 $scope.moveToAnotherGroup = function(selected) { moveToGroup(selected, true) };
1459 $scope.cloneUser = function(selected) {
1460 if (!selected.length) return;
1461 var url = $location.absUrl().replace(
1463 '/patron/register/clone/' + selected[0].id);
1464 $window.open(url, '_blank').focus();
1467 $scope.retrieveSelected = function(selected) {
1468 if (!selected.length) return;
1469 angular.forEach(selected, function(usr) {
1470 $timeout(function() {
1471 var url = $location.absUrl().replace(
1473 '/patron/' + usr.id + '/checkout');
1474 $window.open(url, '_blank')
1481 .controller('PatronStatCatsCtrl',
1482 ['$scope','$routeParams','$q','egCore','patronSvc',
1483 function($scope, $routeParams , $q , egCore , patronSvc) {
1484 $scope.initTab('other', $routeParams.id)
1485 .then(function(redirect) {
1486 // Entries for org-visible stat cats are fleshed. Any others
1487 // have to be fleshed within.
1490 angular.forEach(patronSvc.current.stat_cat_entries(),
1492 if (!angular.isObject(entry.stat_cat())) {
1493 to_flesh[entry.stat_cat()] = entry;
1498 if (!Object.keys(to_flesh).length) return;
1500 egCore.pcrud.search('actsc', {id : Object.keys(to_flesh)})
1501 .then(null, null, function(cat) { // stream
1502 cat.owner(egCore.org.get(cat.owner())); // owner flesh
1503 to_flesh[cat.id()].stat_cat(cat);
1508 .controller('PatronSurveyCtrl',
1509 ['$scope','$routeParams','$location','egCore','patronSvc',
1510 function($scope, $routeParams , $location , egCore , patronSvc) {
1511 $scope.initTab('other', $routeParams.id);
1512 var usr_id = $routeParams.id;
1513 var org_ids = egCore.org.fullPath(egCore.auth.user().ws_ou(), true);
1515 $scope.surveys = [];
1516 var svr_responses = {};
1518 // fetch all survey responses for this user.
1519 egCore.pcrud.search('asvr',
1521 {flesh : 2, flesh_fields : {asvr : ['survey','question','answer']}}
1524 // All responses collected and deduplicated.
1525 // Create one collection of responses per survey.
1527 angular.forEach(svr_responses, function(questions, survey_id) {
1528 var collection = {responses : []};
1529 angular.forEach(questions, function(response) {
1530 collection.survey = response.survey(); // same for one.
1531 collection.responses.push(response);
1533 $scope.surveys.push(collection);
1537 function(response) {
1539 // Discard responses for out-of-scope surveys.
1540 if (org_ids.indexOf(response.survey().owner()) < 0)
1543 // survey_id => question_id => response
1544 var svr_id = response.survey().id();
1545 var qst_id = response.question().id();
1547 if (!svr_responses[svr_id])
1548 svr_responses[svr_id] = [];
1550 if (!svr_responses[svr_id][qst_id]) {
1551 svr_responses[svr_id][qst_id] = response;
1554 // We have multiple responses for the same question.
1555 // For this UI we only care about the most recent response.
1556 if (response.effective_date() >
1557 svr_responses[svr_id][qst_id].effective_date())
1558 svr_responses[svr_id][qst_id] = response;
1564 .controller('PatronFetchLastCtrl',
1565 ['$scope','$location','egCore',
1566 function($scope , $location , egCore) {
1568 var ids = egCore.hatch.getLoginSessionItem('eg.circ.recent_patrons') || [];
1570 return $location.path('/circ/patron/' + ids[0] + '/checkout');
1572 $scope.no_last = true;
1575 .controller('PatronTriggeredEventsCtrl',
1576 ['$scope','$routeParams','$location','egCore','patronSvc',
1577 function($scope, $routeParams, $location , egCore , patronSvc) {
1578 $scope.initTab('other', $routeParams.id);
1580 var url = $location.absUrl().replace(/\/staff\/.*/, '/actor/user/event_log');
1581 url += '?patron_id=' + encodeURIComponent($routeParams.id);
1583 $scope.triggered_events_url = url;
1587 .controller('PatronMessageCenterCtrl',
1588 ['$scope','$routeParams','$location','egCore','patronSvc',
1589 function($scope, $routeParams, $location , egCore , patronSvc) {
1590 $scope.initTab('other', $routeParams.id);
1592 var url = $location.protocol() + '://' + $location.host()
1593 + egCore.env.basePath.replace(/\/staff\/.*/, '/actor/user/message');
1594 url += '/' + encodeURIComponent($routeParams.id);
1596 $scope.message_center_url = url;
1600 .controller('PatronPermsCtrl',
1601 ['$scope','$routeParams','$window','$location','egCore',
1602 function($scope , $routeParams , $window , $location , egCore) {
1603 $scope.initTab('other', $routeParams.id);
1605 var url = $location.absUrl().replace(
1606 /\/eg\/staff\/.*/, '/xul/server/patron/user_edit.xhtml');
1608 url += '?usr=' + encodeURIComponent($routeParams.id);
1610 // user_edit does not load the session via cookie. It uses URL
1611 // params or xulG instead. Pass via xulG.
1613 ses : egCore.auth.token(),
1614 on_patron_save : function() {
1615 $scope.funcs.reload();
1619 $scope.user_perms_url = url;