]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/circ/patron/app.js
lp1977877 action for unarchiving Notes in the staff UI
[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             return egCore.perm.hasPermFullPathAt('VIEW_USER')
74             .then(function(orgList) {
75                 hasPermAt['VIEW_USER'] = orgList;
76                 return go_promise;
77             });
78         });
79     }]};
80
81     $routeProvider.when('/circ/patron/search', {
82         templateUrl: './circ/patron/t_search',
83         controller: 'PatronSearchCtrl',
84         resolve : resolver
85     });
86
87     $routeProvider.when('/circ/patron/bcsearch', {
88         templateUrl: './circ/patron/t_bcsearch',
89         controller: 'PatronBarcodeSearchCtrl',
90         resolve : resolver
91     });
92
93     $routeProvider.when('/circ/patron/credentials', {
94         templateUrl: './circ/patron/t_credentials',
95         controller: 'PatronVerifyCredentialsCtrl',
96         resolve : resolver
97     });
98
99     $routeProvider.when('/circ/patron/last', {
100         templateUrl: './circ/patron/t_last_patron',
101         controller: 'PatronFetchLastCtrl',
102         resolve : resolver
103     });
104
105     // the following require a patron ID
106
107     $routeProvider.when('/circ/patron/:id/alerts', {
108         templateUrl: './circ/patron/t_alerts',
109         controller: 'PatronAlertsCtrl',
110         resolve : resolver
111     });
112
113     $routeProvider.when('/circ/patron/:id/checkout', {
114         templateUrl: './circ/patron/t_checkout',
115         controller: 'PatronCheckoutCtrl',
116         resolve : resolver
117     });
118
119     $routeProvider.when('/circ/patron/:id/items_out', {
120         templateUrl: './circ/patron/t_items_out',
121         controller: 'PatronItemsOutCtrl',
122         resolve : resolver
123     });
124
125     $routeProvider.when('/circ/patron/:id/holds', {
126         templateUrl: './circ/patron/t_holds',
127         controller: 'PatronHoldsCtrl',
128         resolve : resolver
129     });
130
131     $routeProvider.when('/circ/patron/:id/holds/create', {
132         templateUrl: './circ/patron/t_holds_create',
133         controller: 'PatronHoldsCreateCtrl',
134         resolve : resolver
135     });
136
137     $routeProvider.when('/circ/patron/:id/holds/:hold_id', {
138         templateUrl: './circ/patron/t_holds',
139         controller: 'PatronHoldsCtrl',
140         resolve : resolver
141     });
142
143     $routeProvider.when('/circ/patron/:id/hold/:hold_id', {
144         templateUrl: './circ/patron/t_hold_details',
145         controller: 'PatronHoldDetailsCtrl',
146         resolve : resolver
147     });
148
149     $routeProvider.when('/circ/patron/:id/bills', {
150         templateUrl: './circ/patron/t_bills',
151         controller: 'PatronBillsCtrl',
152         resolve : resolver
153     });
154
155     $routeProvider.when('/circ/patron/:id/bill/:xact_id/:xact_tab', {
156         templateUrl: './circ/patron/t_xact_details',
157         controller: 'XactDetailsCtrl',
158         resolve : resolver
159     });
160
161     $routeProvider.when('/circ/patron/:id/bill_history/:history_tab', {
162         templateUrl: './circ/patron/t_bill_history',
163         controller: 'BillHistoryCtrl',
164         resolve : resolver
165     });
166
167     $routeProvider.when('/circ/patron/:id/messages', {
168         templateUrl: './circ/patron/t_messages',
169         controller: 'PatronMessagesCtrl',
170         resolve : resolver
171     });
172
173     $routeProvider.when('/circ/patron/:id/edit', {
174         templateUrl: './circ/patron/t_edit',
175         controller: 'PatronRegCtrl',
176         resolve : resolver
177     });
178
179     $routeProvider.when('/circ/patron/:id/credentials', {
180         templateUrl: './circ/patron/t_credentials',
181         controller: 'PatronVerifyCredentialsCtrl',
182         resolve : resolver
183     });
184
185     $routeProvider.when('/circ/patron/:id/triggered_events', {
186         templateUrl: './circ/patron/t_triggered_events',
187         controller: 'PatronTriggeredEventsCtrl',
188         resolve : resolver
189     });
190
191     $routeProvider.when('/circ/patron/:id/message_center', {
192         templateUrl: './circ/patron/t_message_center',
193         controller: 'PatronMessageCenterCtrl',
194         resolve : resolver
195     });
196
197     $routeProvider.when('/circ/patron/:id/edit_perms', {
198         templateUrl: './circ/patron/t_edit_perms',
199         controller: 'PatronPermsCtrl',
200         resolve : resolver
201     });
202
203     $routeProvider.when('/circ/patron/:id/group', {
204         templateUrl: './circ/patron/t_group',
205         controller: 'PatronGroupCtrl',
206         resolve : resolver
207     });
208
209     $routeProvider.when('/circ/patron/:id/stat_cats', {
210         templateUrl: './circ/patron/t_stat_cats',
211         controller: 'PatronStatCatsCtrl',
212         resolve : resolver
213     });
214
215     $routeProvider.when('/circ/patron/:id/surveys', {
216         templateUrl: './circ/patron/t_surveys',
217         controller: 'PatronSurveyCtrl',
218         resolve : resolver
219     });
220
221     $routeProvider.when('/circ/patron/:id/hold_subscriptions', {
222         templateUrl: './circ/patron/t_hold_subscriptions',
223         controller: 'HoldSubscriptionsCtrl',
224         resolve : resolver
225     });
226
227     $routeProvider.otherwise({redirectTo : '/circ/patron/search'});
228 })
229
230 /**
231  * Manages tabbed patron view.
232  * This is the parent scope of all patron tab scopes.
233  *
234  * */
235 .controller('PatronCtrl',
236        ['$scope','$q','$location','$filter','egCore','egNet','egUser','egAlertDialog',
237         'egConfirmDialog','egPromptDialog','patronSvc','egCirc','hasPermAt','ngToast',
238 function($scope,  $q , $location , $filter , egCore , egNet , egUser , egAlertDialog ,
239          egConfirmDialog , egPromptDialog , patronSvc , egCirc , hasPermAt, ngToast) {
240
241     $scope.is_patron_edit = function() {
242         return Boolean($location.path().match(/patron\/\d+\/edit$/));
243     }
244
245     // To support the fixed position patron edit actions bar,
246     // its markup has to live outside the scope of the patron 
247     // edit controller.  Insert a scope blob here that can be
248     // modifed from within the patron edit controller.
249     $scope.edit_passthru = {};
250
251     // returns true if a redirect occurs
252     function redirectToAlertPanel() {
253
254         $scope.alert_penalties = 
255             function() {return patronSvc.alert_penalties}
256
257         if (patronSvc.alertsShown()) return false;
258
259         // if the patron has any unshown alerts, show them now
260         if (patronSvc.hasAlerts && 
261             !$location.path().match(/alerts$/)) {
262
263             $location
264                 .path('/circ/patron/' + patronSvc.current.id() + '/alerts')
265                 .search('card', null);
266             return true;
267         }
268
269         // no alert required.  If the patron has fines and the show-bills
270         // OUS is applied, direct to the bills page.
271         if ($scope.patron_stats().fines.balance_owed > 0 // TODO: != 0 ?
272             && egCore.env.aous['ui.circ.show_billing_tab_on_bills']
273             && !$location.path().match(/bills$/)) {
274
275             $scope.tab = 'bills';
276             $location
277                 .path('/circ/patron/' + patronSvc.current.id() + '/bills')
278                 .search('card', null);
279
280             return true;
281         }
282
283         return false;
284     }
285
286     // called after each route-specified controller is instantiated.
287     // this doubles as a way to inform the top-level controller that
288     // egStartup.go() has completed, which means we are clear to 
289     // fetch the patron, etc.
290     $scope.initTab = function(tab, patron_id) {
291         console.log('init tab ' + tab);
292         $scope.tab = tab;
293         $scope.aous = egCore.env.aous;
294         $scope.auth_user_id = egCore.auth.user().id();
295
296         if (tab == 'search') {
297             egCirc.reset(); // clear out auto-override and auto-skip selections when switching patrons
298         }
299
300         if (patron_id) {
301             $scope.patron_id = patron_id;
302             return patronSvc.setPrimary($scope.patron_id)
303             .then(function() {
304                 // the page title context label comes from the tab.
305                 egCore.strings.setPageTitle(
306                     egCore.strings.PAGE_TITLE_PATRON_NAME, 
307                     egCore.strings['PAGE_TITLE_PATRON_' + tab.toUpperCase()],
308                     {   lname : patronSvc.current.family_name(),
309                         fname : patronSvc.current.first_given_name(),
310                         mname : patronSvc.current.second_given_name()
311                     }
312                 );
313             })
314             .then(function() {return patronSvc.checkAlerts()})
315             .then(redirectToAlertPanel)
316             .then(function(){
317                 if ($scope.patron().locale() !== null) { 
318                     $scope.locale_name = $scope.patron().locale().name();
319                     $scope.hasLocaleName = $scope.locale_name.length > 0;
320                 }
321             })
322             .then(function(){
323                 $scope.ident_type_name = $scope.patron().ident_type().name();
324                 $scope.hasIdentTypeName = $scope.ident_type_name.length > 0;
325     });
326         } else {
327             // No patron, use the tab name as the page title.
328             egCore.strings.setPageTitle(
329                 egCore.strings['PAGE_TITLE_PATRON_' + tab.toUpperCase()]);
330         }
331         return $q.when();
332     }
333
334     $scope._show_dob = {};
335     $scope.show_dob = function (val) {
336         if ($scope.patron()) {
337             if (typeof val != 'undefined') $scope._show_dob[$scope.patron().id()] = val;
338             return $scope._show_dob[$scope.patron().id()];
339         }
340         return !egCore.env.aous['circ.obscure_dob'];
341     }
342         
343     $scope.obscure_dob = function() { 
344         return egCore.env.aous && egCore.env.aous['circ.obscure_dob'];
345     }
346     $scope.now_show_dob = function() { 
347         return egCore.env.aous && egCore.env.aous['circ.obscure_dob'] ?
348             $scope.show_dob() : true; 
349     }
350
351     $scope.patron = function() { return patronSvc.current }
352     $scope.visible_notes = function() {
353         var p = patronSvc.current;
354         if (p) {
355             var org_ids = hasPermAt['VIEW_USER'];
356             var filtered_notes = p.notes().filter(function(n) { return org_ids.indexOf(n.org_unit()) > -1; });
357             return filtered_notes;
358         }
359         return [];
360     }
361     $scope.patron_stats = function() { return patronSvc.patron_stats }
362     $scope.summary_stat_cats = function() { return patronSvc.summary_stat_cats }
363     $scope.hasAlerts = function() { return patronSvc.hasAlerts }
364     $scope.isPatronExpired = function() { return patronSvc.patronExpired }
365     $scope.doesPatronExpireSoon = function() { return patronSvc.patronExpiresSoon }
366
367     $scope.print_address = function(addr) {
368         egCore.print.print({
369             context : 'default', 
370             template : 'patron_address', 
371             scope : {
372                 patron : egCore.idl.toHash(patronSvc.current),
373                 address : egCore.idl.toHash(addr)
374             }
375         });
376     }
377
378     $scope.copy_address = function(addr) {
379         // Alas, navigator.clipboard is not yet supported in FF and others.
380         var lNode = document.querySelector('#patron-address-copy-' + addr.id());
381
382         // Un-hide the textarea just long enough to copy its data.
383         // Using node.style instead of ng-show/ng-hide in hopes it 
384         // will be quicker, so the user never sees the textarea.
385         lNode.style.visibility = 'visible';
386         lNode.focus();
387         lNode.select();
388
389         if (!document.execCommand('copy')) {
390             console.error('Copy command failed');
391         }
392
393         lNode.style.visibility = 'hidden';
394     }
395
396     $scope.toggle_expand_summary = function() {
397         if ($scope.collapsePatronSummary) {
398             $scope.collapsePatronSummary = false;
399             egCore.hatch.removeItem('eg.circ.patron.summary.collapse');
400         } else {
401             $scope.collapsePatronSummary = true;
402             egCore.hatch.setItem('eg.circ.patron.summary.collapse', true);
403         }
404     }
405     
406     // always expand the patron summary in the search UI, regardless
407     // of stored preference.
408     $scope.collapse_summary = function() {
409         return $scope.tab != 'search' && $scope.collapsePatronSummary;
410     }
411
412     function _purge_account(dest_usr,override) {
413         egNet.request(
414             'open-ils.actor',
415             'open-ils.actor.user.delete' + (override ? '.override' : ''),
416             egCore.auth.token(),
417             $scope.patron().id(),
418             dest_usr
419         ).then(function(resp){
420             if (evt = egCore.evt.parse(resp)) {
421                 if (evt.code == '2004' /* ACTOR_USER_DELETE_OPEN_XACTS */) {
422                     egConfirmDialog.open(
423                         egCore.strings.PATRON_PURGE_CONFIRM_TITLE, egCore.strings.PATRON_PURGE_OVERRIDE_PROMPT,
424                         {ok : function() {
425                             _purge_account(dest_usr,true);
426                         }}
427                     );
428                 } else {
429                     alert(js2JSON(evt));
430                 }
431             } else {
432                 location.href = egCore.env.basePath + '/circ/patron/search';
433             }
434         });
435     }
436
437     function _purge_account_with_destination(dest_barcode) {
438         egCore.pcrud.search('ac', {barcode : dest_barcode})
439         .then(function(card) {
440             if (!card) {
441                 egAlertDialog.open(egCore.strings.PATRON_PURGE_STAFF_BAD_BARCODE);
442             } else {
443                 _purge_account(card.usr());
444             }
445         });
446     }
447
448     $scope.purge_account = function() {
449         egConfirmDialog.open(
450             egCore.strings.PATRON_PURGE_CONFIRM_TITLE, egCore.strings.PATRON_PURGE_CONFIRM,
451             {ok : function() {
452                 egConfirmDialog.open(
453                     egCore.strings.PATRON_PURGE_CONFIRM_TITLE, egCore.strings.PATRON_PURGE_LAST_CHANCE,
454                     {ok : function() {
455                         egNet.request(
456                             'open-ils.actor',
457                             'open-ils.actor.user.has_work_perm_at',
458                             egCore.auth.token(), 'STAFF_LOGIN', $scope.patron().id()
459                         ).then(function(resp) {
460                             var is_staff = resp.length > 0;
461                             if (is_staff) {
462                                 egPromptDialog.open(
463                                     egCore.strings.PATRON_PURGE_STAFF_PROMPT,
464                                     null, // TODO: this would be cool if it worked: egCore.auth.user().card().barcode(),
465                                     {ok : function(barcode) {_purge_account_with_destination(barcode)}}
466                                 );
467                             } else {
468                                 _purge_account();
469                             }
470                         });
471                     }
472                 });
473             }
474         });
475     }
476
477     $scope.refreshPenalties = function() {
478
479         egNet.request(
480             'open-ils.actor',
481             'open-ils.actor.user.penalties.update',
482             egCore.auth.token(), $scope.patron().id()
483
484         ).then(function(resp) {
485
486             if (evt = egCore.evt.parse(resp)) {
487                 ngToast.warning(egCore.strings.PENALTY_REFRESH_FAILED);
488                 console.error(evt);
489             }
490
491             ngToast.create(egCore.strings.PENALTY_REFRESH_SUCCESS);
492
493             // Depending on which page we're on (e.g. Note/Messages) we
494             // may need to force a full data refresh.
495             setTimeout(function() { location.href = location.href; });
496         });
497     }
498
499     egCore.hatch.getItem('eg.circ.patron.summary.collapse')
500     .then(function(val) {$scope.collapsePatronSummary = Boolean(val)});
501 }])
502
503 .controller('PatronBarcodeSearchCtrl',
504        ['$scope','$location','egCore','egConfirmDialog','egUser','patronSvc','$uibModal','$q',
505 function($scope , $location , egCore , egConfirmDialog , egUser , patronSvc , $uibModal , $q) {
506     $scope.selectMe = true; // focus text input
507     patronSvc.clearPrimary(); // clear the default user
508
509     // jump to the patron checkout UI
510     function loadPatron(user_id) {
511         egCore.audio.play('success.patron.by_barcode');
512         $location
513         .path('/circ/patron/' + user_id + '/checkout')
514         .search('card', $scope.args.barcode);
515         patronSvc.search_barcode = $scope.args.barcode;
516     }
517
518     // create an opt-in=yes response for the loaded user
519     function createOptIn(user_id) {
520         egCore.net.request(
521             'open-ils.actor',
522             'open-ils.actor.user.org_unit_opt_in.create',
523             egCore.auth.token(), user_id).then(function(resp) {
524                 if (evt = egCore.evt.parse(resp)) return alert(evt);
525                 loadPatron(user_id);
526             }
527         );
528     }
529
530     $scope.submitBarcode = function(args) {
531         $scope.bcNotFound = null;
532         $scope.optInRestricted = false;
533         if (!args.barcode) return;
534         args.barcode = args.barcode.replace(/^\s/g,'');
535         args.barcode = args.barcode.replace(/\s$/g,'');
536         // blur so next time it's set to true it will re-apply select()
537         $scope.selectMe = false;
538
539         var user_id;
540
541         // given a scanned barcode, this function finds any matching users
542         // and handles multiple matches due to barcode completion
543         function handleBarcodeCompletion(scanned_barcode) {
544             var deferred = $q.defer();
545
546             egCore.net.request(
547                 'open-ils.actor',
548                 'open-ils.actor.get_barcodes',
549                 egCore.auth.token(), egCore.auth.user().ws_ou(), 
550                 'actor', scanned_barcode)
551
552             .then(function(resp) { // get_barcodes
553
554                 if (evt = egCore.evt.parse(resp)) {
555                     alert(evt); // FIXME
556                     deferred.reject();
557                     return;
558                 }
559
560                 if (!resp || !resp[0]) {
561                     $scope.bcNotFound = args.barcode;
562                     $scope.selectMe = true;
563                     egCore.audio.play('warning.patron.not_found');
564                     deferred.reject();
565                     return;
566                 }
567
568                 if (resp.length == 1) {
569                     // exactly one matching barcode: return it
570                     deferred.resolve();
571                     user_id = resp[0].id;
572                 } else {
573                     // multiple matching barcodes: let the user pick one 
574                     var barcode_map = {};
575                     var matches = [];
576                     var promises = [];
577                     var selected_barcode;
578                     angular.forEach(resp, function(match) {
579                         promises.push(
580                             egUser.get(match.id, {useFields : ['home_ou']}).then(function(user) {
581                                 barcode_map[match.barcode] = user.id();
582                                 matches.push( {
583                                     barcode: match.barcode,
584                                     title: user.first_given_name() + ' ' + user.family_name(),
585                                     org_name: user.home_ou().name(),
586                                     org_shortname: user.home_ou().shortname()
587                                 });
588                             })
589                         );
590                     });
591                     return $q.all(promises)
592                     .then(function() {
593                         $uibModal.open({
594                             templateUrl: './circ/share/t_barcode_choice_dialog',
595                             controller:
596                                 ['$scope', '$uibModalInstance',
597                                 function($scope, $uibModalInstance) {
598                                 $scope.matches = matches;
599                                 $scope.ok = function(barcode) {
600                                     $uibModalInstance.close();
601                                     selected_barcode = barcode;
602                                 }
603                                 $scope.cancel = function() {$uibModalInstance.dismiss()}
604                             }],
605                         }).result.then(function() {
606                             deferred.resolve();
607                             user_id = barcode_map[selected_barcode];
608                         });
609                     });
610                 }
611             });
612             return deferred.promise;
613         }
614
615         // call our function to lookup matching users for the scanned barcode
616         handleBarcodeCompletion(args.barcode).then(function() {
617
618             // see if an opt-in request is needed
619             return egCore.net.request(
620                 'open-ils.actor',
621                 'open-ils.actor.user.org_unit_opt_in.check',
622                 egCore.auth.token(), user_id
623             ).then(function(optInResp) { // opt_in_check
624
625                 if (evt = egCore.evt.parse(optInResp)) {
626                     alert(evt); // FIXME
627                     return;
628                 }
629
630                 if (optInResp == 2) {
631                     // opt-in disallowed at this location by patron's home library
632                     $scope.optInRestricted = true;
633                     $scope.selectMe = true;
634                     egCore.audio.play('warning.patron.opt_in_restricted');
635                     return;
636                 }
637             
638                 if (optInResp == 1) {
639                     // opt-in handled or not needed
640                     return loadPatron(user_id);
641                 }
642
643                 // opt-in needed, show the opt-in dialog
644                 egUser.get(user_id, {useFields : []})
645
646                 .then(function(user) { // retrieve user
647                     var org = egCore.org.get(user.home_ou());
648                     egConfirmDialog.open(
649                         egCore.strings.OPT_IN_DIALOG_TITLE,
650                         egCore.strings.OPT_IN_DIALOG,
651                         {   family_name : user.family_name(),
652                             first_given_name : user.first_given_name(),
653                             org_name : org.name(),
654                             org_shortname : org.shortname(),
655                             ok : function() { createOptIn(user.id()) },
656                             cancel : function() {}
657                         }
658                     );
659                 })
660             })
661         })
662     }
663 }])
664
665
666 /**
667  * Manages patron search
668  */
669 .controller('PatronSearchCtrl',
670        ['$scope','$q','$routeParams','$timeout','$window','$location','egCore','ngToast',
671        '$filter','egUser', 'patronSvc','egGridDataProvider','$document','bucketSvc',
672        'egPatronMerge','egProgressDialog','$controller','$interpolate','$uibModal',
673 function($scope,  $q,  $routeParams,  $timeout,  $window,  $location,  egCore , ngToast,
674          $filter,  egUser,  patronSvc , egGridDataProvider , $document , bucketSvc,
675         egPatronMerge , egProgressDialog , $controller , $interpolate , $uibModal) {
676
677     angular.extend(this, $controller('BasePatronSearchCtrl', {$scope : $scope}));
678     $scope.initTab('search');
679
680     $scope.gridControls = {
681         activateItem : function(item) {
682             $location.path('/circ/patron/' + item.id() + '/checkout');
683         },
684         selectedItems : function() { return [] }
685     }
686
687     $scope.bucketSvc = bucketSvc;
688     $scope.bucketSvc.fetchUserBuckets();
689     $scope.bucketSvc.fetchUserSubscriptions();
690     $scope.addToBucket = function(item, data, recs) {
691         if (recs.length == 0) return;
692         var added_count = 0;
693         var failed_count = 0;
694         var promise = $q.when();
695         angular.forEach(recs,
696             function(rec) {
697                 var item = new egCore.idl.cubi();
698                 item.bucket(data.id());
699                 item.target_user(rec.id());
700                 promise = promise.then(function() {
701                     return egCore.net.request(
702                         'open-ils.actor',
703                         'open-ils.actor.container.item.create',
704                         egCore.auth.token(), 'user', item, 1
705                     );
706                 }).then(
707                     function(){ added_count++ },
708                     function(){ failed_count++ }
709                 );
710             }
711         );
712
713         promise.then( function () {
714             if (added_count) ngToast.create($interpolate(egCore.strings.BUCKET_ADD_SUCCESS)({ count: ''+added_count, name: data.name()} ));
715             if (failed_count) ngToast.warning($interpolate(egCore.strings.BUCKET_ADD_FAIL)({ count: ''+failed_count, name: data.name() } ));
716         });
717     }
718
719     var temp_scope = $scope;
720     $scope.openCreateBucketDialog = function() {
721         $uibModal.open({
722             templateUrl: './circ/patron/bucket/t_bucket_create',
723             backdrop: 'static',
724             controller:
725                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
726                 $scope.focusMe = true;
727                 $scope.ok = function(args) { $uibModalInstance.close(args) }
728                 $scope.cancel = function () { $uibModalInstance.dismiss() }
729             }]
730         }).result.then(function (args) {
731             if (!args || !args.name) return;
732             bucketSvc.createBucket(args.name, args.desc).then(
733                 function(id) {
734                     if (id) {
735                         $scope.bucketSvc.fetchBucket(id).then(function (b) {
736                             $scope.addToBucket(
737                                 null,
738                                 b,
739                                 $scope.gridControls.selectedItems()
740                             );
741                             $scope.bucketSvc.fetchUserBuckets(true);
742                         });
743                     }
744                 }
745             );
746         });
747     }
748
749     $scope.$watch(
750         function() {return $scope.gridControls.selectedItems()},
751         function(list) {
752             if (list[0]) 
753                 patronSvc.setPrimary(null, list[0]);
754         },
755         true
756     );
757
758     $scope.need_one_selected = function() {
759         var items = $scope.gridControls.selectedItems();
760         return (items.length > 0) ? false : true;
761     }
762     $scope.need_two_selected = function() {
763         var items = $scope.gridControls.selectedItems();
764         return (items.length == 2) ? false : true;
765     }
766     $scope.merge_patrons = function() {
767         var items = $scope.gridControls.selectedItems();
768         if (items.length != 2) return false;
769
770         var patron_ids = [];
771         angular.forEach(items, function(i) {
772             patron_ids.push(i.id());
773         });
774         egPatronMerge.do_merge(patron_ids).then(
775             function() {
776                 // ensure that we're not drawing from cached
777                 // resuts, as a successful merge just deleted a
778                 // record
779                 delete patronSvc.lastSearch;
780                 $scope.gridControls.refresh();
781             },
782             function(evt) {
783                 if (evt && evt.textcode == 'MERGE_SELF_NOT_ALLOWED') {
784                     ngToast.warning(egCore.strings.MERGE_SELF_NOT_ALLOWED);
785                 }
786             }
787         );
788     }
789    
790 }])
791
792 /**
793  * Manages messages
794  */
795 .controller('PatronMessagesCtrl',
796        ['$scope','$q','$routeParams','egCore','$uibModal','patronSvc','egCirc','hasPermAt',
797 function($scope , $q , $routeParams,  egCore , $uibModal , patronSvc , egCirc , hasPermAt ) {
798     $scope.initTab('messages', $routeParams.id);
799     var usr_id = $routeParams.id;
800     var org_ids = hasPermAt.VIEW_USER;
801
802     // setup date filters
803     var start = new Date(); // now - 1 year
804     start.setFullYear(start.getFullYear() - 1),
805     $scope.dates = {
806         start_date : start,
807         end_date : new Date()
808     }
809
810     function date_range() {
811         var start = $scope.dates.start_date.toISOString().replace(/T.*/,'');
812         var end = $scope.dates.end_date.toISOString().replace(/T.*/,'');
813         var today = new Date().toISOString().replace(/T.*/,'');
814         if (end == today) end = 'now';
815         return [start, end];
816     }
817
818     // grid queries
819    
820     var activeGrid = $scope.activeGridControls = {
821         setSort : function() {
822             return [{'create_date' : 'DESC'}];
823         },
824         setQuery : function() {
825             return {
826                 usr : usr_id,
827                 org_unit : org_ids,
828                 '-or' : [
829                     {stop_date : null},
830                     {stop_date : {'>' : 'now'}}
831                 ]
832             }
833         }
834     }
835
836     var archiveGrid = $scope.archiveGridControls = {
837         setSort : function() {
838             return [{'create_date' : 'DESC'}];
839         },
840         watchQuery : function() {
841             return {
842                 usr : usr_id, 
843                 org_unit : org_ids,
844                 stop_date : {'<=' : 'now'},
845                 create_date : {between : date_range()}
846             };
847         }
848     };
849
850     $scope.test_for_disable_remove_penalty = function() {
851         var selected = $scope.activeGridControls.selectedItems();
852         var found_pub_and_read_and_not_deleted = false;
853         angular.forEach(selected, function(s) {
854             if (Boolean(s.pub == 't') && Boolean(s.read_date) && !Boolean(s.deleted == 't')) {
855                 found_pub_and_read_and_not_deleted = true;
856             }
857         });
858         return found_pub_and_read_and_not_deleted;
859     }
860
861     $scope.removePenalty = function(selected) {
862         if (selected.length == 0) return;
863
864         // TODO: need confirmation dialog
865
866         var promises = [];
867         // figure out the view components
868         var aum_ids = [];
869         var ausp_ids = [];
870         angular.forEach(selected, function(s) {
871             if (s.aum_id) { aum_ids.push(s.aum_id); }
872             if (s.ausp_id) { ausp_ids.push(s.ausp_id); }
873         });
874
875         // fetch all of them since trying to pull them
876         // off of patronSvc.current isn't reliable
877         if (ausp_ids.length > 0) {
878             promises.push(
879                 egCore.pcrud.search('ausp',
880                     {id : ausp_ids}, {},
881                     {atomic : true, authoritative : true}
882                 ).then(function(penalties) {
883                     return egCore.pcrud.remove(penalties);
884                 })
885             );
886         }
887         if (aum_ids.length > 0) {
888             promises.push(
889                 egCore.pcrud.search('aum',
890                     {id : aum_ids}, {},
891                     {atomic : true, authoritative : true}
892                 ).then(function(messages) {
893                     return egCore.pcrud.remove(messages);
894                 })
895             );
896         }
897         $q.all(promises).then(function() {
898             activeGrid.refresh();
899             archiveGrid.refresh();
900             // force a refresh of the user
901             patronSvc.setPrimary(patronSvc.current.id(), null, true);
902         });
903     }
904
905     $scope.archivePenalty = function(selected) {
906         if (selected.length == 0) return;
907
908         // TODO: need confirmation dialog
909
910         var promises = [];
911         // figure out the view components
912         var aum_ids = [];
913         var ausp_ids = [];
914         angular.forEach(selected, function(s) {
915             if (s.aum_id) { aum_ids.push(s.aum_id); }
916             if (s.ausp_id) { ausp_ids.push(s.ausp_id); }
917         });
918
919         // fetch all of them since trying to pull them
920         // off of patronSvc.current isn't reliable
921         if (ausp_ids.length > 0) {
922             promises.push(
923                 egCore.pcrud.search('ausp',
924                     {id : ausp_ids}, {},
925                     {atomic : true, authoritative : true}
926                 ).then(function(penalties) {
927                     angular.forEach(penalties, function(p) {
928                         p.stop_date('now');
929                     });
930                     return egCore.pcrud.update(penalties);
931                 })
932             );
933         }
934         if (aum_ids.length > 0) {
935             promises.push(
936                 egCore.pcrud.search('aum',
937                     {id : aum_ids}, {},
938                     {atomic : true, authoritative : true}
939                 ).then(function(messages) {
940                     angular.forEach(messages, function(m) {
941                         m.stop_date('now');
942                     });
943                     return egCore.pcrud.update(messages);
944                 })
945             );
946         }
947         $q.all(promises).then(function() {
948             activeGrid.refresh();
949             archiveGrid.refresh();
950             // force a refresh of the user
951             patronSvc.setPrimary(patronSvc.current.id(), null, true);
952         });
953     }
954
955     $scope.unarchivePenalty = function(selected) {
956         if (selected.length == 0) return;
957
958         // TODO: need confirmation dialog
959
960         var promises = [];
961         // figure out the view components
962         var aum_ids = [];
963         var ausp_ids = [];
964         angular.forEach(selected, function(s) {
965             if (s.aum_id) { aum_ids.push(s.aum_id); }
966             if (s.ausp_id) { ausp_ids.push(s.ausp_id); }
967         });
968
969         // fetch all of them since trying to pull them
970         // off of patronSvc.current isn't reliable
971         if (ausp_ids.length > 0) {
972             promises.push(
973                 egCore.pcrud.search('ausp',
974                     {id : ausp_ids}, {},
975                     {atomic : true, authoritative : true}
976                 ).then(function(penalties) {
977                     angular.forEach(penalties, function(p) {
978                         p.stop_date(null);
979                     });
980                     return egCore.pcrud.update(penalties);
981                 })
982             );
983         }
984         if (aum_ids.length > 0) {
985             promises.push(
986                 egCore.pcrud.search('aum',
987                     {id : aum_ids}, {},
988                     {atomic : true, authoritative : true}
989                 ).then(function(messages) {
990                     angular.forEach(messages, function(m) {
991                         m.stop_date(null);
992                     });
993                     return egCore.pcrud.update(messages);
994                 })
995             );
996         }
997         $q.all(promises).then(function() {
998             activeGrid.refresh();
999             archiveGrid.refresh();
1000             // force a refresh of the user
1001             patronSvc.setPrimary(patronSvc.current.id(), null, true);
1002         });
1003     }
1004
1005     // leverage egEnv for caching
1006     function fetchPenaltyTypes() {
1007         if (egCore.env.csp) 
1008             return $q.when(egCore.env.csp.list);
1009         return egCore.pcrud.search(
1010             // id <= 100 are reserved for system use
1011             'csp', {id : {'>': 100}}, {}, {atomic : true})
1012         .then(function(penalties) {
1013             egCore.env.absorbList(penalties, 'csp');
1014             return penalties;
1015         });
1016     }
1017
1018     $scope.createPenalty = function() {
1019         egCirc.create_penalty(usr_id).then(function() {
1020             activeGrid.refresh();
1021             // force a refresh of the user, since they may now
1022             // have blocking penalties, etc.
1023             patronSvc.setPrimary(patronSvc.current.id(), null, true);
1024         });
1025     }
1026
1027     $scope.editPenalty = function(selected) {
1028         if (selected.length == 0) return;
1029
1030         var promises = [];
1031         // figure out the view components
1032         var aum_ids = []; var aum_objs = {};
1033         var ausp_ids = []; var ausp_objs = {};
1034         var pairs = [];
1035         angular.forEach(selected, function(s) {
1036             if (s.aum_id) { aum_ids.push(s.aum_id); }
1037             if (s.ausp_id) { ausp_ids.push(s.ausp_id); }
1038             pairs.push( { aum_id : s.aum_id, ausp_id : s.ausp_id } );
1039         });
1040
1041         // fetch all of them since trying to pull them
1042         // off of patronSvc.current isn't reliable
1043         // (we want deleted user messages too)
1044         if (ausp_ids.length > 0) {
1045             promises.push(
1046                 egCore.pcrud.search('ausp',
1047                     {id : ausp_ids}, {},
1048                     {atomic : true, authoritative : true}
1049                 ).then(function(penalties) {
1050                     angular.forEach(penalties, function(p) {
1051                         ausp_objs[p.id()] = p;
1052                     });
1053                     return $q.when();
1054                 })
1055             );
1056         }
1057         if (aum_ids.length > 0) {
1058             promises.push(
1059                 egCore.pcrud.search('aum',
1060                     {id : aum_ids}, {
1061                         flesh : 1,
1062                         flesh_fields : {
1063                             aum : ['editor']
1064                         }
1065                     },
1066                     {atomic : true, authoritative : true}
1067                 ).then(function(messages) {
1068                     angular.forEach(messages, function(m) {
1069                         aum_objs[m.id()] = m;
1070                     });
1071                     return $q.when();
1072                 })
1073             );
1074         }
1075         $q.all(promises).then(function() {
1076             angular.forEach(pairs, function(pair) {
1077                 egCirc.edit_penalty(ausp_objs[pair.ausp_id],aum_objs[pair.aum_id]).then(function() {
1078                     activeGrid.refresh();
1079                     // force a refresh of the user, since they may now
1080                     // have blocking penalties, etc.
1081                     patronSvc.setPrimary(patronSvc.current.id(), null, true);
1082                 });
1083             });
1084         });
1085     }
1086 }])
1087
1088
1089 /**
1090  * Credentials tester
1091  */
1092 .controller('PatronVerifyCredentialsCtrl',
1093        ['$scope','$routeParams','$location','egCore',
1094 function($scope,  $routeParams , $location , egCore) {
1095     $scope.verified = null;
1096     $scope.focusMe = true;
1097
1098     // called with a patron, pre-populate the form args
1099     $scope.initTab('other', $routeParams.id).then(
1100         function() {
1101             if ($routeParams.id && $scope.patron()) {
1102                 $scope.prepop = true;
1103                 $scope.username = $scope.patron().usrname();
1104                 $scope.barcode = $scope.patron().card().barcode();
1105             } else {
1106                 $scope.username = '';
1107                 $scope.barcode = '';
1108                 $scope.password = '';
1109             }
1110         }
1111     );
1112
1113     // verify login credentials
1114     $scope.verify = function() {
1115         $scope.verified = null;
1116         $scope.notFound = false;
1117
1118         egCore.net.request(
1119             'open-ils.actor',
1120             'open-ils.actor.verify_user_password',
1121             egCore.auth.token(), $scope.barcode,
1122             $scope.username, hex_md5($scope.password || '')
1123
1124         ).then(function(resp) {
1125             $scope.focusMe = true;
1126             if (evt = egCore.evt.parse(resp)) {
1127                 alert(evt);
1128             } else if (resp == 1) {
1129                 $scope.verified = true;
1130             } else {
1131                 $scope.verified = false;
1132             }
1133         });
1134     }
1135
1136     // load the main patron UI for the provided username or barcode
1137     $scope.load = function($event) {
1138         $scope.notFound = false;
1139         $scope.verified = null;
1140
1141         egCore.net.request(
1142             'open-ils.actor',
1143             'open-ils.actor.user.retrieve_id_by_barcode_or_username',
1144             egCore.auth.token(), $scope.barcode, $scope.username
1145
1146         ).then(function(resp) {
1147
1148             if (Number(resp)) {
1149                 $location.path('/circ/patron/' + resp + '/checkout');
1150                 return;
1151             }
1152
1153             // something went wrong...
1154             $scope.focusMe = true;
1155             if (evt = egCore.evt.parse(resp)) {
1156                 if (evt.textcode == 'ACTOR_USR_NOT_FOUND') {
1157                     $scope.notFound = true;
1158                     return;
1159                 }
1160                 return alert(evt);
1161             } else {
1162                 alert(resp);
1163             }
1164         });
1165
1166         // load() button sits within the verify form.  
1167         // avoid submitting the verify() form action on load()
1168         $event.preventDefault();
1169     }
1170 }])
1171
1172 .controller('PatronAlertsCtrl',
1173        ['$scope','$routeParams','$location','egCore','patronSvc',
1174 function($scope,  $routeParams , $location , egCore , patronSvc) {
1175
1176     $scope.initTab('other', $routeParams.id)
1177     .then(function() {
1178         $scope.patronExpired = patronSvc.patronExpired;
1179         $scope.patronExpiresSoon = patronSvc.patronExpiresSoon;
1180         $scope.retrievedWithInactive = patronSvc.fetchedWithInactiveCard();
1181         $scope.invalidAddresses = patronSvc.invalidAddresses;
1182     });
1183
1184 }])
1185
1186 .controller('HoldSubscriptionsCtrl',
1187        ['$scope','$q','$routeParams','$location','egCore','patronSvc','bucketSvc','egGridDataProvider','egConfirmDialog','$timeout','$window',
1188 function($scope,  $q , $routeParams , $location , egCore , patronSvc,  bucketSvc,  egGridDataProvider,  egConfirmDialog,  $timeout,  $window) {
1189
1190     $scope.initTab('other', $routeParams.id);
1191
1192     $scope.bucket_ids = [];
1193     $scope.bucket_items = [];
1194     $scope.buckets = [];
1195
1196     $scope.gridControls = {
1197         activateItem : function (item) {
1198             var url = $location.absUrl().replace(
1199                 /\/circ\/patron\/.*/, 
1200                 '/cat/bucket/batch_hold/view/' + item.id());
1201             $window.open(url, '_blank').focus();
1202         }
1203     };
1204
1205     $scope.gridDataProvider = egGridDataProvider.instance({
1206         get : function(offset, count) {
1207             return this.arrayNotifier($scope.buckets, offset, count);
1208         }
1209     });
1210
1211     function fetchSubscriptions() {
1212         $scope.bucket_ids = [];
1213         $scope.bucket_items = [];
1214         $scope.buckets = [];
1215         egCore.pcrud.search('cubi',
1216             { target_user : $routeParams.id }
1217         ).then(
1218             function() {
1219                 if ($scope.bucket_ids.length > 0) {
1220                     egCore.pcrud.search('cub',
1221                         { id : $scope.bucket_ids, btype : 'hold_subscription' }
1222                     ).then(
1223                         function() { $scope.gridControls.refresh() },
1224                         null,
1225                         function(b) {
1226                             $scope.buckets.push(b);
1227                             b.items( $scope.bucket_items.filter(i => i.bucket() == b.id()) );
1228                         }
1229                     );
1230                 } else {
1231                     $scope.gridControls.refresh();
1232                 }
1233             },
1234             null,
1235             function(i) {
1236                 $scope.bucket_ids.push(i.bucket());
1237                 $scope.bucket_items.push(i);
1238             }
1239         )
1240     }
1241
1242     $scope.removeSubscriptions = function (buckets) {
1243         return egConfirmDialog.open(
1244             egCore.strings.REMOVE_HOLD_SUBSCRIPTIONS,'',{}
1245         ).result.then(function() {
1246             var promises = [];
1247
1248             angular.forEach(buckets, function(b) {
1249                 angular.forEach(b.items(), function (i) {
1250                     promises.push(bucketSvc.detachUser(i.id()));
1251                 })
1252             });
1253
1254             $q.all(promises).then(fetchSubscriptions);
1255         });
1256     }
1257
1258     $timeout(fetchSubscriptions);
1259
1260 }])
1261
1262 .controller('PatronGroupCtrl',
1263        ['$scope','$routeParams','$q','$window','$timeout','$location','egCore',
1264         'patronSvc','$uibModal','egPromptDialog','egConfirmDialog',
1265 function($scope,  $routeParams , $q , $window , $timeout,  $location , egCore ,
1266          patronSvc , $uibModal , egPromptDialog , egConfirmDialog) {
1267
1268     var usr_id = $routeParams.id;
1269
1270     $scope.totals = {owed : 0, total_out : 0, overdue : 0}
1271
1272     var grid = $scope.gridControls = {
1273         activateItem : function(item) {
1274             $location.path('/circ/patron/' + item.id + '/checkout');
1275         },
1276         itemRetrieved : function(item) {
1277
1278             if (item.id == patronSvc.current.id()) {
1279                 item.stats = patronSvc.patron_stats;
1280
1281             } else {
1282                 // flesh stats for other group members
1283                 patronSvc.getUserStats(item.id).then(function(stats) {
1284                     item.stats = stats;
1285                     $scope.totals.total_out += stats.checkouts.total_out; 
1286                     $scope.totals.overdue += stats.checkouts.overdue; 
1287                 });
1288             }
1289         },
1290         setSort : function() {
1291             return ['create_date'];
1292         },
1293         watchQuery: function() {
1294             if (patronSvc.current) {
1295                 return {
1296                     usrgroup : patronSvc.current.usrgroup(),
1297                     deleted : 'f'
1298                 };
1299             }
1300             return null;
1301         }
1302     }
1303
1304     $scope.initTab('other', $routeParams.id)
1305     .then(function(redirect) {
1306         // if we are redirecting to the alerts page, avoid updating the
1307         // grid query.
1308         if (redirect) return;
1309         // let initTab() fetch the user first so we can know the usrgroup
1310         $scope.totals.owed = patronSvc.patron_stats.fines.group_balance_owed;
1311     });
1312
1313     $scope.removeFromGroup = function(selected) {
1314         var promises = [];
1315         angular.forEach(selected, function(user) {
1316             console.debug('removing user ' + user.id + ' from group');
1317
1318             promises.push(
1319                 egCore.net.request(
1320                     'open-ils.actor',
1321                     'open-ils.actor.usergroup.new',
1322                     egCore.auth.token(), user.id, true
1323                 )
1324             );
1325         });
1326
1327         $q.all(promises).then(function() {grid.refresh()});
1328     }
1329
1330     function addUserToGroup(user) {
1331         user.usrgroup(patronSvc.current.usrgroup());
1332         user.ischanged(true);
1333         egCore.net.request(
1334             'open-ils.actor',
1335             'open-ils.actor.patron.update',
1336             egCore.auth.token(), user
1337
1338         ).then(function() {grid.refresh()});
1339     }
1340
1341     // fetch each user ("selected" has flattened users)
1342     // update the usrgroup, then update the user object
1343     // After all updates are complete, refresh the grid.
1344     function moveUsersToGroup(target_user, selected) {
1345         var promises = [];
1346
1347         angular.forEach(selected, function(user) {
1348             promises.push(
1349                 egCore.pcrud.retrieve('au', user.id)
1350                 .then(function(u) {
1351                     u.usrgroup(target_user.usrgroup());
1352                     u.ischanged(true);
1353                     return egCore.net.request(
1354                         'open-ils.actor',
1355                         'open-ils.actor.patron.update',
1356                         egCore.auth.token(), u
1357                     );
1358                 })
1359             );
1360         });
1361
1362         $q.all(promises).then(function() {grid.refresh()});
1363     }
1364
1365     function showMoveToGroupConfirm(barcode, selected, outbound) {
1366
1367         // find the user
1368         egCore.pcrud.search('ac', {barcode : barcode})
1369
1370         // fetch the fleshed user
1371         .then(function(card) {
1372
1373             if (!card) return; // TODO: warn user
1374
1375             egCore.pcrud.retrieve('au', card.usr())
1376             .then(function(user) {
1377                 user.card(card);
1378                 $uibModal.open({
1379                     templateUrl: './circ/patron/t_move_to_group_dialog',
1380                     backdrop: 'static',
1381                     controller: [
1382                                 '$scope','$uibModalInstance',
1383                         function($scope , $uibModalInstance) {
1384                             $scope.user = user;
1385                             $scope.selected = selected;
1386                             $scope.outbound = outbound;
1387                             $scope.ok = 
1388                                 function(count) { $uibModalInstance.close() }
1389                             $scope.cancel = 
1390                                 function () { $uibModalInstance.dismiss() }
1391                         }
1392                     ]
1393                 }).result.then(function() {
1394                     if (outbound) {
1395                         moveUsersToGroup(user, selected);
1396                     } else {
1397                         addUserToGroup(user);
1398                     }
1399                 });
1400             });
1401         });
1402     }
1403
1404     // selected == move selected patrons to another patron's group
1405     // !selected == patron from a different group moves into our group
1406     function moveToGroup(selected, outbound) {
1407         egPromptDialog.open(
1408             egCore.strings.GROUP_ADD_USER, '',
1409             {ok : function(value) {
1410                 if (value) 
1411                     showMoveToGroupConfirm(value, selected, outbound);
1412             }}
1413         );
1414     }
1415
1416     $scope.moveToGroup = function() { moveToGroup([], false) };
1417     $scope.moveToAnotherGroup = function(selected) { moveToGroup(selected, true) };
1418
1419     $scope.cloneUser = function(selected) {
1420         if (!selected.length) return;
1421         var url = $location.absUrl().replace(
1422             /\/patron\/.*/, 
1423             '/patron/register/clone/' + selected[0].id);
1424         $window.open(url, '_blank').focus();
1425     }
1426
1427     $scope.retrieveSelected = function(selected) {
1428         if (!selected.length) return;
1429         angular.forEach(selected, function(usr) {
1430             $timeout(function() {
1431                 var url = $location.absUrl().replace(
1432                     /\/patron\/.*/,
1433                     '/patron/' + usr.id + '/checkout');
1434                 $window.open(url, '_blank')
1435             });
1436         });
1437     }
1438
1439 }])
1440
1441 .controller('PatronStatCatsCtrl',
1442        ['$scope','$routeParams','$q','egCore','patronSvc',
1443 function($scope,  $routeParams , $q , egCore , patronSvc) {
1444     $scope.initTab('other', $routeParams.id)
1445     .then(function(redirect) {
1446         // Entries for org-visible stat cats are fleshed.  Any others
1447         // have to be fleshed within.
1448
1449         var to_flesh = {};
1450         angular.forEach(patronSvc.current.stat_cat_entries(), 
1451             function(entry) {
1452                 if (!angular.isObject(entry.stat_cat())) {
1453                     to_flesh[entry.stat_cat()] = entry;
1454                 }
1455             }
1456         );
1457
1458         if (!Object.keys(to_flesh).length) return;
1459
1460         egCore.pcrud.search('actsc', {id : Object.keys(to_flesh)})
1461         .then(null, null, function(cat) { // stream
1462             cat.owner(egCore.org.get(cat.owner())); // owner flesh
1463             to_flesh[cat.id()].stat_cat(cat);
1464         });
1465     });
1466 }])
1467
1468 .controller('PatronSurveyCtrl',
1469        ['$scope','$routeParams','$location','egCore','patronSvc',
1470 function($scope,  $routeParams , $location , egCore , patronSvc) {
1471     $scope.initTab('other', $routeParams.id);
1472     var usr_id = $routeParams.id;
1473     var org_ids = egCore.org.fullPath(egCore.auth.user().ws_ou(), true);
1474
1475     $scope.surveys = [];
1476     var svr_responses = {};
1477
1478     // fetch all survey responses for this user.
1479     egCore.pcrud.search('asvr',
1480         {usr : usr_id},
1481         {flesh : 2, flesh_fields : {asvr : ['survey','question','answer']}}
1482     ).then(
1483         function() {
1484             // All responses collected and deduplicated.
1485             // Create one collection of responses per survey.
1486
1487             angular.forEach(svr_responses, function(questions, survey_id) {
1488                 var collection = {responses : []};
1489                 angular.forEach(questions, function(response) {
1490                     collection.survey = response.survey(); // same for one.
1491                     collection.responses.push(response);
1492                 });
1493                 $scope.surveys.push(collection);
1494             });
1495         },
1496         null, 
1497         function(response) {
1498
1499             // Discard responses for out-of-scope surveys.
1500             if (org_ids.indexOf(response.survey().owner()) < 0) 
1501                 return;
1502
1503             // survey_id => question_id => response
1504             var svr_id = response.survey().id();
1505             var qst_id = response.question().id();
1506
1507             if (!svr_responses[svr_id]) 
1508                 svr_responses[svr_id] = [];
1509
1510             if (!svr_responses[svr_id][qst_id]) {
1511                 svr_responses[svr_id][qst_id] = response;
1512
1513             } else {
1514                 // We have multiple responses for the same question.
1515                 // For this UI we only care about the most recent response.
1516                 if (response.effective_date() > 
1517                     svr_responses[svr_id][qst_id].effective_date())
1518                     svr_responses[svr_id][qst_id] = response;
1519             }
1520         }
1521     );
1522 }])
1523
1524 .controller('PatronFetchLastCtrl',
1525        ['$scope','$location','egCore',
1526 function($scope , $location , egCore) {
1527
1528     var ids = egCore.hatch.getLoginSessionItem('eg.circ.recent_patrons') || [];
1529     if (ids.length) 
1530         return $location.path('/circ/patron/' + ids[0] + '/checkout');
1531
1532     $scope.no_last = true;
1533 }])
1534
1535 .controller('PatronTriggeredEventsCtrl',
1536        ['$scope','$routeParams','$location','egCore','patronSvc',
1537 function($scope,  $routeParams,  $location , egCore , patronSvc) {
1538     $scope.initTab('other', $routeParams.id);
1539
1540     var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
1541     url += '?patron_id=' + encodeURIComponent($routeParams.id);
1542
1543     $scope.triggered_events_url = url;
1544     $scope.funcs = {};
1545 }])
1546
1547 .controller('PatronMessageCenterCtrl',
1548        ['$scope','$routeParams','$location','egCore','patronSvc',
1549 function($scope,  $routeParams,  $location , egCore , patronSvc) {
1550     $scope.initTab('other', $routeParams.id);
1551
1552     var url = $location.protocol() + '://' + $location.host()
1553         + egCore.env.basePath.replace(/\/staff.*/,  '/actor/user/message');
1554     url += '/' + encodeURIComponent($routeParams.id);
1555
1556     $scope.message_center_url = url;
1557     $scope.funcs = {};
1558 }])
1559
1560 .controller('PatronPermsCtrl',
1561        ['$scope','$routeParams','$window','$location','egCore',
1562 function($scope , $routeParams , $window , $location , egCore) {
1563     $scope.initTab('other', $routeParams.id);
1564
1565     var url = $location.absUrl().replace(
1566         /\/eg\/staff.*/, '/xul/server/patron/user_edit.xhtml');
1567
1568     url += '?usr=' + encodeURIComponent($routeParams.id);
1569
1570     // user_edit does not load the session via cookie.  It uses URL 
1571     // params or xulG instead.  Pass via xulG.
1572     $scope.funcs = {
1573         ses : egCore.auth.token(),
1574         on_patron_save : function() {
1575             $scope.funcs.reload();
1576         }
1577     }
1578
1579     $scope.user_perms_url = url;
1580 }])
1581