]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/circ/patron/app.js
LP1615805 No inputs after submit in patron search (AngularJS)
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / patron / app.js
1 /**
2  * Patron App
3  *
4  * Search, checkout, items out, holds, bills, edit, etc.
5  */
6
7 angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod', 
8     'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'ngToast',
9     'egPatronSearchMod'])
10
11 .config(['ngToastProvider', function(ngToastProvider) {
12     ngToastProvider.configure({
13         verticalPosition: 'bottom',
14         animation: 'fade'
15     });
16 }])
17
18 .factory("hasPermAt",function(){
19     return {};
20 })
21
22 .config(function($routeProvider, $locationProvider, $compileProvider) {
23     $locationProvider.html5Mode(true);
24     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
25         
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) {
30
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',
39                 'circ.tally_lost',
40                 'circ.obscure_dob',
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;
50             });
51         }
52
53         egCore.env.loadClasses.push('aous');
54
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([
59                 'profile',
60                 'net_access_level',
61                 'ident_type',
62                 'ident_type2',
63                 'locale',
64                 'cards',
65                 'groups'
66             ]);
67         }
68
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;
77                 return go_promise;
78             });
79         });
80     }]};
81
82     $routeProvider.when('/circ/patron/search', {
83         templateUrl: './circ/patron/t_search',
84         controller: 'PatronSearchCtrl',
85         resolve : resolver
86     });
87
88     $routeProvider.when('/circ/patron/bcsearch', {
89         templateUrl: './circ/patron/t_bcsearch',
90         controller: 'PatronBarcodeSearchCtrl',
91         resolve : resolver
92     });
93
94     $routeProvider.when('/circ/patron/credentials', {
95         templateUrl: './circ/patron/t_credentials',
96         controller: 'PatronVerifyCredentialsCtrl',
97         resolve : resolver
98     });
99
100     $routeProvider.when('/circ/patron/last', {
101         templateUrl: './circ/patron/t_last_patron',
102         controller: 'PatronFetchLastCtrl',
103         resolve : resolver
104     });
105
106     // the following require a patron ID
107
108     $routeProvider.when('/circ/patron/:id/alerts', {
109         templateUrl: './circ/patron/t_alerts',
110         controller: 'PatronAlertsCtrl',
111         resolve : resolver
112     });
113
114     $routeProvider.when('/circ/patron/:id/checkout', {
115         templateUrl: './circ/patron/t_checkout',
116         controller: 'PatronCheckoutCtrl',
117         resolve : resolver
118     });
119
120     $routeProvider.when('/circ/patron/:id/items_out', {
121         templateUrl: './circ/patron/t_items_out',
122         controller: 'PatronItemsOutCtrl',
123         resolve : resolver
124     });
125
126     $routeProvider.when('/circ/patron/:id/holds', {
127         templateUrl: './circ/patron/t_holds',
128         controller: 'PatronHoldsCtrl',
129         resolve : resolver
130     });
131
132     $routeProvider.when('/circ/patron/:id/holds/create', {
133         templateUrl: './circ/patron/t_holds_create',
134         controller: 'PatronHoldsCreateCtrl',
135         resolve : resolver
136     });
137
138     $routeProvider.when('/circ/patron/:id/holds/:hold_id', {
139         templateUrl: './circ/patron/t_holds',
140         controller: 'PatronHoldsCtrl',
141         resolve : resolver
142     });
143
144     $routeProvider.when('/circ/patron/:id/hold/:hold_id', {
145         templateUrl: './circ/patron/t_hold_details',
146         controller: 'PatronHoldDetailsCtrl',
147         resolve : resolver
148     });
149
150     $routeProvider.when('/circ/patron/:id/bills', {
151         templateUrl: './circ/patron/t_bills',
152         controller: 'PatronBillsCtrl',
153         resolve : resolver
154     });
155
156     $routeProvider.when('/circ/patron/:id/bill/:xact_id/:xact_tab', {
157         templateUrl: './circ/patron/t_xact_details',
158         controller: 'XactDetailsCtrl',
159         resolve : resolver
160     });
161
162     $routeProvider.when('/circ/patron/:id/bill_history/:history_tab', {
163         templateUrl: './circ/patron/t_bill_history',
164         controller: 'BillHistoryCtrl',
165         resolve : resolver
166     });
167
168     $routeProvider.when('/circ/patron/:id/messages', {
169         templateUrl: './circ/patron/t_messages',
170         controller: 'PatronMessagesCtrl',
171         resolve : resolver
172     });
173
174     $routeProvider.when('/circ/patron/:id/edit', {
175         templateUrl: './circ/patron/t_edit',
176         controller: 'PatronRegCtrl',
177         resolve : resolver
178     });
179
180     $routeProvider.when('/circ/patron/:id/credentials', {
181         templateUrl: './circ/patron/t_credentials',
182         controller: 'PatronVerifyCredentialsCtrl',
183         resolve : resolver
184     });
185
186     $routeProvider.when('/circ/patron/:id/triggered_events', {
187         templateUrl: './circ/patron/t_triggered_events',
188         controller: 'PatronTriggeredEventsCtrl',
189         resolve : resolver
190     });
191
192     $routeProvider.when('/circ/patron/:id/message_center', {
193         templateUrl: './circ/patron/t_message_center',
194         controller: 'PatronMessageCenterCtrl',
195         resolve : resolver
196     });
197
198     $routeProvider.when('/circ/patron/:id/edit_perms', {
199         templateUrl: './circ/patron/t_edit_perms',
200         controller: 'PatronPermsCtrl',
201         resolve : resolver
202     });
203
204     $routeProvider.when('/circ/patron/:id/group', {
205         templateUrl: './circ/patron/t_group',
206         controller: 'PatronGroupCtrl',
207         resolve : resolver
208     });
209
210     $routeProvider.when('/circ/patron/:id/stat_cats', {
211         templateUrl: './circ/patron/t_stat_cats',
212         controller: 'PatronStatCatsCtrl',
213         resolve : resolver
214     });
215
216     $routeProvider.when('/circ/patron/:id/surveys', {
217         templateUrl: './circ/patron/t_surveys',
218         controller: 'PatronSurveyCtrl',
219         resolve : resolver
220     });
221
222     $routeProvider.when('/circ/patron/:id/hold_subscriptions', {
223         templateUrl: './circ/patron/t_hold_subscriptions',
224         controller: 'HoldSubscriptionsCtrl',
225         resolve : resolver
226     });
227
228     $routeProvider.otherwise({redirectTo : '/circ/patron/search'});
229 })
230
231 /**
232  * Manages tabbed patron view.
233  * This is the parent scope of all patron tab scopes.
234  *
235  * */
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) {
241
242     $scope.is_patron_edit = function() {
243         return Boolean($location.path().match(/patron\/\d+\/edit$/));
244     }
245
246     $scope.alert_penalties = function() {
247         return patronSvc.alert_penalties;
248     }
249
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 = {};
255
256     // returns true if a redirect occurs
257     function redirectToAlertPanel() {
258
259         if (patronSvc.alertsShown()) return false;
260
261         // if the patron has any unshown alerts, show them now
262         if (patronSvc.hasAlerts && 
263             !$location.path().match(/alerts$/)) {
264
265             $location
266                 .path('/circ/patron/' + patronSvc.current.id() + '/alerts')
267                 .search('card', null);
268             return true;
269         }
270
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$/)) {
276
277             $scope.tab = 'bills';
278             $location
279                 .path('/circ/patron/' + patronSvc.current.id() + '/bills')
280                 .search('card', null);
281
282             return true;
283         }
284
285         return false;
286     }
287
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);
294         $scope.tab = tab;
295         $scope.aous = egCore.env.aous;
296         $scope.auth_user_id = egCore.auth.user().id();
297
298         if (tab == 'search') {
299             egCirc.reset(); // clear out auto-override and auto-skip selections when switching patrons
300         }
301
302         if (patron_id) {
303             $scope.patron_id = patron_id;
304             return patronSvc.setPrimary($scope.patron_id)
305             .then(function() {
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()
313                     }
314                 );
315             })
316             .then(function() {return patronSvc.checkAlerts()})
317             .then(redirectToAlertPanel)
318             .then(function(){
319                 if ($scope.patron().locale() !== null) { 
320                     $scope.locale_name = $scope.patron().locale().name();
321                     $scope.hasLocaleName = $scope.locale_name.length > 0;
322                 }
323             })
324             .then(function(){
325                 $scope.ident_type_name = $scope.patron().ident_type().name();
326                 $scope.hasIdentTypeName = $scope.ident_type_name.length > 0;
327     });
328         } else {
329             // No patron, use the tab name as the page title.
330             egCore.strings.setPageTitle(
331                 egCore.strings['PAGE_TITLE_PATRON_' + tab.toUpperCase()]);
332         }
333         return $q.when();
334     }
335
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()];
341         }
342         return !egCore.env.aous['circ.obscure_dob'];
343     }
344         
345     $scope.obscure_dob = function() { 
346         return egCore.env.aous && egCore.env.aous['circ.obscure_dob'];
347     }
348     $scope.now_show_dob = function() { 
349         return egCore.env.aous && egCore.env.aous['circ.obscure_dob'] ?
350             $scope.show_dob() : true; 
351     }
352
353     $scope.patron = function() { return patronSvc.current }
354     $scope.visible_notes = function() {
355         var p = patronSvc.current;
356         if (p) {
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;
360         }
361         return [];
362     }
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 }
368
369     $scope.print_address = function(addr) {
370         egCore.print.print({
371             context : 'default', 
372             template : 'patron_address', 
373             scope : {
374                 patron : egCore.idl.toHash(patronSvc.current),
375                 address : egCore.idl.toHash(addr)
376             }
377         });
378     }
379
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());
383
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';
388         lNode.focus();
389         lNode.select();
390
391         if (!document.execCommand('copy')) {
392             console.error('Copy command failed');
393         }
394
395         lNode.style.visibility = 'hidden';
396     }
397
398     $scope.toggle_expand_summary = function() {
399         if ($scope.collapsePatronSummary) {
400             $scope.collapsePatronSummary = false;
401             egCore.hatch.removeItem('eg.circ.patron.summary.collapse');
402         } else {
403             $scope.collapsePatronSummary = true;
404             egCore.hatch.setItem('eg.circ.patron.summary.collapse', true);
405         }
406     }
407     
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;
412     }
413
414     function _purge_account(dest_usr,override) {
415         egNet.request(
416             'open-ils.actor',
417             'open-ils.actor.user.delete' + (override ? '.override' : ''),
418             egCore.auth.token(),
419             $scope.patron().id(),
420             dest_usr
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,
426                         {ok : function() {
427                             _purge_account(dest_usr,true);
428                         }}
429                     );
430                 } else {
431                     alert(js2JSON(evt));
432                 }
433             } else {
434                 location.href = egCore.env.basePath + '/circ/patron/search';
435             }
436         });
437     }
438
439     function _purge_account_with_destination(dest_barcode) {
440         egCore.pcrud.search('ac', {barcode : dest_barcode})
441         .then(function(card) {
442             if (!card) {
443                 egAlertDialog.open(egCore.strings.PATRON_PURGE_STAFF_BAD_BARCODE);
444             } else {
445                 _purge_account(card.usr());
446             }
447         });
448     }
449
450     $scope.purge_account = function() {
451         egConfirmDialog.open(
452             egCore.strings.PATRON_PURGE_CONFIRM_TITLE, egCore.strings.PATRON_PURGE_CONFIRM,
453             {ok : function() {
454                 egConfirmDialog.open(
455                     egCore.strings.PATRON_PURGE_CONFIRM_TITLE, egCore.strings.PATRON_PURGE_LAST_CHANCE,
456                     {ok : function() {
457                         egNet.request(
458                             'open-ils.actor',
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;
463                             if (is_staff) {
464                                 egPromptDialog.open(
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)}}
468                                 );
469                             } else {
470                                 _purge_account();
471                             }
472                         });
473                     }
474                 });
475             }
476         });
477     }
478
479     $scope.refreshPenalties = function() {
480
481         egNet.request(
482             'open-ils.actor',
483             'open-ils.actor.user.penalties.update',
484             egCore.auth.token(), $scope.patron().id()
485
486         ).then(function(resp) {
487
488             if (evt = egCore.evt.parse(resp)) {
489                 ngToast.warning(egCore.strings.PENALTY_REFRESH_FAILED);
490                 console.error(evt);
491             }
492
493             ngToast.create(egCore.strings.PENALTY_REFRESH_SUCCESS);
494
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; });
498         });
499     }
500
501     egCore.hatch.getItem('eg.circ.patron.summary.collapse')
502     .then(function(val) {$scope.collapsePatronSummary = Boolean(val)});
503 }])
504
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
510
511     // jump to the patron checkout UI
512     function loadPatron(user_id) {
513         egCore.audio.play('success.patron.by_barcode');
514         $location
515         .path('/circ/patron/' + user_id + '/checkout')
516         .search('card', $scope.args.barcode);
517         patronSvc.search_barcode = $scope.args.barcode;
518     }
519
520     // create an opt-in=yes response for the loaded user
521     function createOptIn(user_id) {
522         egCore.net.request(
523             'open-ils.actor',
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);
527                 loadPatron(user_id);
528             }
529         );
530     }
531
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;
540
541         var user_id;
542
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();
547
548             egCore.net.request(
549                 'open-ils.actor',
550                 'open-ils.actor.get_barcodes',
551                 egCore.auth.token(), egCore.auth.user().ws_ou(), 
552                 'actor', scanned_barcode)
553
554             .then(function(resp) { // get_barcodes
555
556                 if (evt = egCore.evt.parse(resp)) {
557                     alert(evt); // FIXME
558                     deferred.reject();
559                     return;
560                 }
561
562                 if (!resp || !resp[0]) {
563                     $scope.bcNotFound = args.barcode;
564                     $scope.selectMe = true;
565                     egCore.audio.play('warning.patron.not_found');
566                     deferred.reject();
567                     return;
568                 }
569
570                 if (resp.length == 1) {
571                     // exactly one matching barcode: return it
572                     deferred.resolve();
573                     user_id = resp[0].id;
574                 } else {
575                     // multiple matching barcodes: let the user pick one 
576                     var barcode_map = {};
577                     var matches = [];
578                     var promises = [];
579                     var selected_barcode;
580                     angular.forEach(resp, function(match) {
581                         promises.push(
582                             egUser.get(match.id, {useFields : ['home_ou']}).then(function(user) {
583                                 barcode_map[match.barcode] = user.id();
584                                 matches.push( {
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()
589                                 });
590                             })
591                         );
592                     });
593                     return $q.all(promises)
594                     .then(function() {
595                         $uibModal.open({
596                             templateUrl: './circ/share/t_barcode_choice_dialog',
597                             controller:
598                                 ['$scope', '$uibModalInstance',
599                                 function($scope, $uibModalInstance) {
600                                 $scope.matches = matches;
601                                 $scope.ok = function(barcode) {
602                                     $uibModalInstance.close();
603                                     selected_barcode = barcode;
604                                 }
605                                 $scope.cancel = function() {$uibModalInstance.dismiss()}
606                             }],
607                         }).result.then(function() {
608                             deferred.resolve();
609                             user_id = barcode_map[selected_barcode];
610                         });
611                     });
612                 }
613             });
614             return deferred.promise;
615         }
616
617         // call our function to lookup matching users for the scanned barcode
618         handleBarcodeCompletion(args.barcode).then(function() {
619
620             // see if an opt-in request is needed
621             return egCore.net.request(
622                 'open-ils.actor',
623                 'open-ils.actor.user.org_unit_opt_in.check',
624                 egCore.auth.token(), user_id
625             ).then(function(optInResp) { // opt_in_check
626
627                 if (evt = egCore.evt.parse(optInResp)) {
628                     alert(evt); // FIXME
629                     return;
630                 }
631
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');
637                     return;
638                 }
639             
640                 if (optInResp == 1) {
641                     // opt-in handled or not needed
642                     return loadPatron(user_id);
643                 }
644
645                 // opt-in needed, show the opt-in dialog
646                 egUser.get(user_id, {useFields : []})
647
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() {}
659                         }
660                     );
661                 })
662             })
663         })
664     }
665 }])
666
667
668 /**
669  * Manages patron search
670  */
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) {
678
679     angular.extend(this, $controller('BasePatronSearchCtrl', {$scope : $scope}));
680     $scope.initTab('search');
681
682     $scope.gridControls = {
683         activateItem : function(item) {
684             $location.path('/circ/patron/' + item.id() + '/checkout');
685         },
686         selectedItems : function() { return [] }
687     }
688
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;
694         var added_count = 0;
695         var failed_count = 0;
696         var promise = $q.when();
697         angular.forEach(recs,
698             function(rec) {
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(
704                         'open-ils.actor',
705                         'open-ils.actor.container.item.create',
706                         egCore.auth.token(), 'user', item, 1
707                     );
708                 }).then(
709                     function(){ added_count++ },
710                     function(){ failed_count++ }
711                 );
712             }
713         );
714
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() } ));
718         });
719     }
720
721     var temp_scope = $scope;
722     $scope.openCreateBucketDialog = function() {
723         $uibModal.open({
724             templateUrl: './circ/patron/bucket/t_bucket_create',
725             backdrop: 'static',
726             controller:
727                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
728                 $scope.focusMe = true;
729                 $scope.ok = function(args) { $uibModalInstance.close(args) }
730                 $scope.cancel = function () { $uibModalInstance.dismiss() }
731             }]
732         }).result.then(function (args) {
733             if (!args || !args.name) return;
734             bucketSvc.createBucket(args.name, args.desc).then(
735                 function(id) {
736                     if (id) {
737                         $scope.bucketSvc.fetchBucket(id).then(function (b) {
738                             $scope.addToBucket(
739                                 null,
740                                 b,
741                                 $scope.gridControls.selectedItems()
742                             );
743                             $scope.bucketSvc.fetchUserBuckets(true);
744                         });
745                     }
746                 }
747             );
748         });
749     }
750
751     $scope.$watch(
752         function() {return $scope.gridControls.selectedItems()},
753         function(list) {
754             if (list[0]) 
755                 patronSvc.setPrimary(null, list[0]);
756         },
757         true
758     );
759
760     $scope.need_one_selected = function() {
761         var items = $scope.gridControls.selectedItems();
762         return (items.length > 0) ? false : true;
763     }
764     $scope.need_two_selected = function() {
765         var items = $scope.gridControls.selectedItems();
766         return (items.length == 2) ? false : true;
767     }
768     $scope.merge_patrons = function() {
769         var items = $scope.gridControls.selectedItems();
770         if (items.length != 2) return false;
771
772         var patron_ids = [];
773         angular.forEach(items, function(i) {
774             patron_ids.push(i.id());
775         });
776         egPatronMerge.do_merge(patron_ids).then(
777             function() {
778                 // ensure that we're not drawing from cached
779                 // resuts, as a successful merge just deleted a
780                 // record
781                 delete patronSvc.lastSearch;
782                 $scope.gridControls.refresh();
783             },
784             function(evt) {
785                 if (evt && evt.textcode == 'MERGE_SELF_NOT_ALLOWED') {
786                     ngToast.warning(egCore.strings.MERGE_SELF_NOT_ALLOWED);
787                 }
788             }
789         );
790     }
791    
792 }])
793
794 /**
795  * Manages messages
796  */
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;
803
804     // setup date filters
805     var start = new Date(); // now - 1 year
806     start.setFullYear(start.getFullYear() - 1),
807     $scope.dates = {
808         start_date : start,
809         end_date : new Date()
810     }
811
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';
817         return [start, end];
818     }
819
820     // grid queries
821    
822     var activeGrid = $scope.activeGridControls = {
823         setSort : function() {
824             return [{'create_date' : 'DESC'}];
825         },
826         setQuery : function() {
827             return {
828                 usr : usr_id,
829                 org_unit : org_ids,
830                 '-or' : [
831                     {stop_date : null},
832                     {stop_date : {'>' : 'now'}}
833                 ]
834             }
835         },
836         activateItem : function(selected) {
837             // activateItem returns a single row.
838             $scope.editPenalty([selected])
839         }
840     }
841
842     var archiveGrid = $scope.archiveGridControls = {
843         setSort : function() {
844             return [{'create_date' : 'DESC'}];
845         },
846         watchQuery : function() {
847             return {
848                 usr : usr_id, 
849                 org_unit : org_ids,
850                 stop_date : {'<=' : 'now'},
851                 create_date : {between : date_range()}
852             };
853         },
854         activateItem : function(selected) {
855             // activateItem returns a single row.
856             $scope.editPenalty([selected])
857         }
858     };
859
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;
866             }
867         });
868         return found_pub_and_read_and_not_deleted;
869     }
870
871     $scope.removePenalty = function(selected) {
872         if (selected.length == 0) return;
873
874         function doit() {
875             var promises = [];
876             // figure out the view components
877             var aum_ids = [];
878             var ausp_ids = [];
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); }
882             });
883
884             // fetch all of them since trying to pull them
885             // off of patronSvc.current isn't reliable
886             if (ausp_ids.length > 0) {
887                 promises.push(
888                     egCore.pcrud.search('ausp',
889                         {id : ausp_ids}, {},
890                         {atomic : true, authoritative : true}
891                     ).then(function(penalties) {
892                         return egCore.pcrud.remove(penalties);
893                     })
894                 );
895             }
896             if (aum_ids.length > 0) {
897                 promises.push(
898                     egCore.pcrud.search('aum',
899                         {id : aum_ids}, {},
900                         {atomic : true, authoritative : true}
901                     ).then(function(messages) {
902                         return egCore.pcrud.remove(messages);
903                     })
904                 );
905             }
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);
911             });
912         }
913
914         egCore.audio.play('warning.circ.remove_note');
915         egConfirmDialog.open(
916             egCore.strings.CONFIRM_REMOVE_NOTE, '',
917             {   //xactIds : ''+ids,
918                 ok : function() {
919                     doit();
920                 }
921             }
922         );
923     }
924
925     $scope.archivePenalty = function(selected) {
926         if (selected.length == 0) return;
927
928         function doit() {
929             var promises = [];
930             // figure out the view components
931             var aum_ids = [];
932             var ausp_ids = [];
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); }
936             });
937
938             // fetch all of them since trying to pull them
939             // off of patronSvc.current isn't reliable
940             if (ausp_ids.length > 0) {
941                 promises.push(
942                     egCore.pcrud.search('ausp',
943                         {id : ausp_ids}, {},
944                         {atomic : true, authoritative : true}
945                     ).then(function(penalties) {
946                         angular.forEach(penalties, function(p) {
947                             p.stop_date('now');
948                         });
949                         return egCore.pcrud.update(penalties);
950                     })
951                 );
952             }
953             if (aum_ids.length > 0) {
954                 promises.push(
955                     egCore.pcrud.search('aum',
956                         {id : aum_ids}, {},
957                         {atomic : true, authoritative : true}
958                     ).then(function(messages) {
959                         angular.forEach(messages, function(m) {
960                             m.stop_date('now');
961                         });
962                         return egCore.pcrud.update(messages);
963                     })
964                 );
965             }
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);
971             });
972         }
973
974         egCore.audio.play('warning.circ.archive_note');
975         egConfirmDialog.open(
976             egCore.strings.CONFIRM_ARCHIVE_NOTE, '', 
977             {   //xactIds : ''+ids,
978                 ok : function() {
979                     doit();
980                 }
981             }
982         );
983     }
984
985     $scope.unarchivePenalty = function(selected) {
986         if (selected.length == 0) return;
987
988         function doit() {
989             var promises = [];
990             // figure out the view components
991             var aum_ids = [];
992             var ausp_ids = [];
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); }
996             });
997
998             // fetch all of them since trying to pull them
999             // off of patronSvc.current isn't reliable
1000             if (ausp_ids.length > 0) {
1001                 promises.push(
1002                     egCore.pcrud.search('ausp',
1003                         {id : ausp_ids}, {},
1004                         {atomic : true, authoritative : true}
1005                     ).then(function(penalties) {
1006                         angular.forEach(penalties, function(p) {
1007                             p.stop_date(null);
1008                         });
1009                         return egCore.pcrud.update(penalties);
1010                     })
1011                 );
1012             }
1013             if (aum_ids.length > 0) {
1014                 promises.push(
1015                     egCore.pcrud.search('aum',
1016                         {id : aum_ids}, {},
1017                         {atomic : true, authoritative : true}
1018                     ).then(function(messages) {
1019                         angular.forEach(messages, function(m) {
1020                             m.stop_date(null);
1021                         });
1022                         return egCore.pcrud.update(messages);
1023                     })
1024                 );
1025             }
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);
1031             });
1032         }
1033
1034         egCore.audio.play('warning.circ.unarchive_note');
1035         egConfirmDialog.open(
1036             egCore.strings.CONFIRM_UNARCHIVE_NOTE, '', 
1037             {   //xactIds : ''+ids,
1038                 ok : function() {
1039                     doit();
1040                 }
1041             }
1042         );
1043     }
1044
1045     // leverage egEnv for caching
1046     function fetchPenaltyTypes() {
1047         if (egCore.env.csp) 
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');
1054             return penalties;
1055         });
1056     }
1057
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);
1064         });
1065     }
1066
1067     $scope.editPenalty = function(selected) {
1068         if (selected.length == 0) return;
1069
1070         var promises = [];
1071         // figure out the view components
1072         var aum_ids = []; var aum_objs = {};
1073         var ausp_ids = []; var ausp_objs = {};
1074         var pairs = [];
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 } );
1079         });
1080
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) {
1085             promises.push(
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;
1092                     });
1093                     return $q.when();
1094                 })
1095             );
1096         }
1097         if (aum_ids.length > 0) {
1098             promises.push(
1099                 egCore.pcrud.search('aum',
1100                     {id : aum_ids}, {
1101                         flesh : 1,
1102                         flesh_fields : {
1103                             aum : ['editor']
1104                         }
1105                     },
1106                     {atomic : true, authoritative : true}
1107                 ).then(function(messages) {
1108                     angular.forEach(messages, function(m) {
1109                         aum_objs[m.id()] = m;
1110                     });
1111                     return $q.when();
1112                 })
1113             );
1114         }
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);
1122                 });
1123             });
1124         });
1125     }
1126 }])
1127
1128
1129 /**
1130  * Credentials tester
1131  */
1132 .controller('PatronVerifyCredentialsCtrl',
1133        ['$scope','$routeParams','$location','egCore',
1134 function($scope,  $routeParams , $location , egCore) {
1135     $scope.verified = null;
1136     $scope.focusMe = true;
1137
1138     // called with a patron, pre-populate the form args
1139     $scope.initTab('other', $routeParams.id).then(
1140         function() {
1141             if ($routeParams.id && $scope.patron()) {
1142                 $scope.prepop = true;
1143                 $scope.username = $scope.patron().usrname();
1144                 $scope.barcode = $scope.patron().card().barcode();
1145             } else {
1146                 $scope.username = '';
1147                 $scope.barcode = '';
1148                 $scope.password = '';
1149             }
1150         }
1151     );
1152
1153     // verify login credentials
1154     $scope.verify = function() {
1155         $scope.verified = null;
1156         $scope.notFound = false;
1157
1158         egCore.net.request(
1159             'open-ils.actor',
1160             'open-ils.actor.verify_user_password',
1161             egCore.auth.token(), $scope.barcode,
1162             $scope.username, hex_md5($scope.password || '')
1163
1164         ).then(function(resp) {
1165             $scope.focusMe = true;
1166             if (evt = egCore.evt.parse(resp)) {
1167                 alert(evt);
1168             } else if (resp == 1) {
1169                 $scope.verified = true;
1170             } else {
1171                 $scope.verified = false;
1172             }
1173         });
1174     }
1175
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;
1180
1181         egCore.net.request(
1182             'open-ils.actor',
1183             'open-ils.actor.user.retrieve_id_by_barcode_or_username',
1184             egCore.auth.token(), $scope.barcode, $scope.username
1185
1186         ).then(function(resp) {
1187
1188             if (Number(resp)) {
1189                 $location.path('/circ/patron/' + resp + '/checkout');
1190                 return;
1191             }
1192
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;
1198                     return;
1199                 }
1200                 return alert(evt);
1201             } else {
1202                 alert(resp);
1203             }
1204         });
1205
1206         // load() button sits within the verify form.  
1207         // avoid submitting the verify() form action on load()
1208         $event.preventDefault();
1209     }
1210 }])
1211
1212 .controller('PatronAlertsCtrl',
1213        ['$scope','$routeParams','$location','egCore','patronSvc',
1214 function($scope,  $routeParams , $location , egCore , patronSvc) {
1215
1216     $scope.initTab('other', $routeParams.id)
1217     .then(function() {
1218         $scope.patronExpired = patronSvc.patronExpired;
1219         $scope.patronExpiresSoon = patronSvc.patronExpiresSoon;
1220         $scope.retrievedWithInactive = patronSvc.fetchedWithInactiveCard();
1221         $scope.invalidAddresses = patronSvc.invalidAddresses;
1222     });
1223
1224 }])
1225
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) {
1229
1230     $scope.initTab('other', $routeParams.id);
1231
1232     $scope.bucket_ids = [];
1233     $scope.bucket_items = [];
1234     $scope.buckets = [];
1235
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();
1242         }
1243     };
1244
1245     $scope.gridDataProvider = egGridDataProvider.instance({
1246         get : function(offset, count) {
1247             return this.arrayNotifier($scope.buckets, offset, count);
1248         }
1249     });
1250
1251     function fetchSubscriptions() {
1252         $scope.bucket_ids = [];
1253         $scope.bucket_items = [];
1254         $scope.buckets = [];
1255         egCore.pcrud.search('cubi',
1256             { target_user : $routeParams.id }
1257         ).then(
1258             function() {
1259                 if ($scope.bucket_ids.length > 0) {
1260                     egCore.pcrud.search('cub',
1261                         { id : $scope.bucket_ids, btype : 'hold_subscription' }
1262                     ).then(
1263                         function() { $scope.gridControls.refresh() },
1264                         null,
1265                         function(b) {
1266                             $scope.buckets.push(b);
1267                             b.items( $scope.bucket_items.filter(i => i.bucket() == b.id()) );
1268                         }
1269                     );
1270                 } else {
1271                     $scope.gridControls.refresh();
1272                 }
1273             },
1274             null,
1275             function(i) {
1276                 $scope.bucket_ids.push(i.bucket());
1277                 $scope.bucket_items.push(i);
1278             }
1279         )
1280     }
1281
1282     $scope.removeSubscriptions = function (buckets) {
1283         return egConfirmDialog.open(
1284             egCore.strings.REMOVE_HOLD_SUBSCRIPTIONS,'',{}
1285         ).result.then(function() {
1286             var promises = [];
1287
1288             angular.forEach(buckets, function(b) {
1289                 angular.forEach(b.items(), function (i) {
1290                     promises.push(bucketSvc.detachUser(i.id()));
1291                 })
1292             });
1293
1294             $q.all(promises).then(fetchSubscriptions);
1295         });
1296     }
1297
1298     $timeout(fetchSubscriptions);
1299
1300 }])
1301
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) {
1307
1308     var usr_id = $routeParams.id;
1309
1310     $scope.totals = {owed : 0, total_out : 0, overdue : 0}
1311
1312     var grid = $scope.gridControls = {
1313         activateItem : function(item) {
1314             $location.path('/circ/patron/' + item.id + '/checkout');
1315         },
1316         itemRetrieved : function(item) {
1317
1318             if (item.id == patronSvc.current.id()) {
1319                 item.stats = patronSvc.patron_stats;
1320
1321             } else {
1322                 // flesh stats for other group members
1323                 patronSvc.getUserStats(item.id).then(function(stats) {
1324                     item.stats = stats;
1325                     $scope.totals.total_out += stats.checkouts.total_out; 
1326                     $scope.totals.overdue += stats.checkouts.overdue; 
1327                 });
1328             }
1329         },
1330         setSort : function() {
1331             return ['create_date'];
1332         },
1333         watchQuery: function() {
1334             if (patronSvc.current) {
1335                 return {
1336                     usrgroup : patronSvc.current.usrgroup(),
1337                     deleted : 'f'
1338                 };
1339             }
1340             return null;
1341         }
1342     }
1343
1344     $scope.initTab('other', $routeParams.id)
1345     .then(function(redirect) {
1346         // if we are redirecting to the alerts page, avoid updating the
1347         // grid query.
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;
1351     });
1352
1353     $scope.removeFromGroup = function(selected) {
1354         var promises = [];
1355         angular.forEach(selected, function(user) {
1356             console.debug('removing user ' + user.id + ' from group');
1357
1358             promises.push(
1359                 egCore.net.request(
1360                     'open-ils.actor',
1361                     'open-ils.actor.usergroup.new',
1362                     egCore.auth.token(), user.id, true
1363                 )
1364             );
1365         });
1366
1367         $q.all(promises).then(function() {grid.refresh()});
1368     }
1369
1370     function addUserToGroup(user) {
1371         user.usrgroup(patronSvc.current.usrgroup());
1372         user.ischanged(true);
1373         egCore.net.request(
1374             'open-ils.actor',
1375             'open-ils.actor.patron.update',
1376             egCore.auth.token(), user
1377
1378         ).then(function() {grid.refresh()});
1379     }
1380
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) {
1385         var promises = [];
1386
1387         angular.forEach(selected, function(user) {
1388             promises.push(
1389                 egCore.pcrud.retrieve('au', user.id)
1390                 .then(function(u) {
1391                     u.usrgroup(target_user.usrgroup());
1392                     u.ischanged(true);
1393                     return egCore.net.request(
1394                         'open-ils.actor',
1395                         'open-ils.actor.patron.update',
1396                         egCore.auth.token(), u
1397                     );
1398                 })
1399             );
1400         });
1401
1402         $q.all(promises).then(function() {grid.refresh()});
1403     }
1404
1405     function showMoveToGroupConfirm(barcode, selected, outbound) {
1406
1407         // find the user
1408         egCore.pcrud.search('ac', {barcode : barcode})
1409
1410         // fetch the fleshed user
1411         .then(function(card) {
1412
1413             if (!card) return; // TODO: warn user
1414
1415             egCore.pcrud.retrieve('au', card.usr())
1416             .then(function(user) {
1417                 user.card(card);
1418                 $uibModal.open({
1419                     templateUrl: './circ/patron/t_move_to_group_dialog',
1420                     backdrop: 'static',
1421                     controller: [
1422                                 '$scope','$uibModalInstance',
1423                         function($scope , $uibModalInstance) {
1424                             $scope.user = user;
1425                             $scope.selected = selected;
1426                             $scope.outbound = outbound;
1427                             $scope.ok = 
1428                                 function(count) { $uibModalInstance.close() }
1429                             $scope.cancel = 
1430                                 function () { $uibModalInstance.dismiss() }
1431                         }
1432                     ]
1433                 }).result.then(function() {
1434                     if (outbound) {
1435                         moveUsersToGroup(user, selected);
1436                     } else {
1437                         addUserToGroup(user);
1438                     }
1439                 });
1440             });
1441         });
1442     }
1443
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) {
1450                 if (value) 
1451                     showMoveToGroupConfirm(value, selected, outbound);
1452             }}
1453         );
1454     }
1455
1456     $scope.moveToGroup = function() { moveToGroup([], false) };
1457     $scope.moveToAnotherGroup = function(selected) { moveToGroup(selected, true) };
1458
1459     $scope.cloneUser = function(selected) {
1460         if (!selected.length) return;
1461         var url = $location.absUrl().replace(
1462             /\/patron\/.*/, 
1463             '/patron/register/clone/' + selected[0].id);
1464         $window.open(url, '_blank').focus();
1465     }
1466
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(
1472                     /\/patron\/.*/,
1473                     '/patron/' + usr.id + '/checkout');
1474                 $window.open(url, '_blank')
1475             });
1476         });
1477     }
1478
1479 }])
1480
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.
1488
1489         var to_flesh = {};
1490         angular.forEach(patronSvc.current.stat_cat_entries(), 
1491             function(entry) {
1492                 if (!angular.isObject(entry.stat_cat())) {
1493                     to_flesh[entry.stat_cat()] = entry;
1494                 }
1495             }
1496         );
1497
1498         if (!Object.keys(to_flesh).length) return;
1499
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);
1504         });
1505     });
1506 }])
1507
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);
1514
1515     $scope.surveys = [];
1516     var svr_responses = {};
1517
1518     // fetch all survey responses for this user.
1519     egCore.pcrud.search('asvr',
1520         {usr : usr_id},
1521         {flesh : 2, flesh_fields : {asvr : ['survey','question','answer']}}
1522     ).then(
1523         function() {
1524             // All responses collected and deduplicated.
1525             // Create one collection of responses per survey.
1526
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);
1532                 });
1533                 $scope.surveys.push(collection);
1534             });
1535         },
1536         null, 
1537         function(response) {
1538
1539             // Discard responses for out-of-scope surveys.
1540             if (org_ids.indexOf(response.survey().owner()) < 0) 
1541                 return;
1542
1543             // survey_id => question_id => response
1544             var svr_id = response.survey().id();
1545             var qst_id = response.question().id();
1546
1547             if (!svr_responses[svr_id]) 
1548                 svr_responses[svr_id] = [];
1549
1550             if (!svr_responses[svr_id][qst_id]) {
1551                 svr_responses[svr_id][qst_id] = response;
1552
1553             } else {
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;
1559             }
1560         }
1561     );
1562 }])
1563
1564 .controller('PatronFetchLastCtrl',
1565        ['$scope','$location','egCore',
1566 function($scope , $location , egCore) {
1567
1568     var ids = egCore.hatch.getLoginSessionItem('eg.circ.recent_patrons') || [];
1569     if (ids.length) 
1570         return $location.path('/circ/patron/' + ids[0] + '/checkout');
1571
1572     $scope.no_last = true;
1573 }])
1574
1575 .controller('PatronTriggeredEventsCtrl',
1576        ['$scope','$routeParams','$location','egCore','patronSvc',
1577 function($scope,  $routeParams,  $location , egCore , patronSvc) {
1578     $scope.initTab('other', $routeParams.id);
1579
1580     var url = $location.absUrl().replace(/\/staff\/.*/, '/actor/user/event_log');
1581     url += '?patron_id=' + encodeURIComponent($routeParams.id);
1582
1583     $scope.triggered_events_url = url;
1584     $scope.funcs = {};
1585 }])
1586
1587 .controller('PatronMessageCenterCtrl',
1588        ['$scope','$routeParams','$location','egCore','patronSvc',
1589 function($scope,  $routeParams,  $location , egCore , patronSvc) {
1590     $scope.initTab('other', $routeParams.id);
1591
1592     var url = $location.protocol() + '://' + $location.host()
1593         + egCore.env.basePath.replace(/\/staff\/.*/,  '/actor/user/message');
1594     url += '/' + encodeURIComponent($routeParams.id);
1595
1596     $scope.message_center_url = url;
1597     $scope.funcs = {};
1598 }])
1599
1600 .controller('PatronPermsCtrl',
1601        ['$scope','$routeParams','$window','$location','egCore',
1602 function($scope , $routeParams , $window , $location , egCore) {
1603     $scope.initTab('other', $routeParams.id);
1604
1605     var url = $location.absUrl().replace(
1606         /\/eg\/staff\/.*/, '/xul/server/patron/user_edit.xhtml');
1607
1608     url += '?usr=' + encodeURIComponent($routeParams.id);
1609
1610     // user_edit does not load the session via cookie.  It uses URL 
1611     // params or xulG instead.  Pass via xulG.
1612     $scope.funcs = {
1613         ses : egCore.auth.token(),
1614         on_patron_save : function() {
1615             $scope.funcs.reload();
1616         }
1617     }
1618
1619     $scope.user_perms_url = url;
1620 }])
1621