2 * App to drive the offline UI
7 angular.module('egOffline', ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'ngToast', 'tableSort'])
10 ['$routeProvider','$locationProvider','$compileProvider',
11 function($routeProvider , $locationProvider , $compileProvider) {
13 $locationProvider.html5Mode(true);
14 $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/);
17 * Route resolvers allow us to run async commands
18 * before the page controller is instantiated.
20 var resolver = {delay : ['egCore', 'egLovefield',
21 function(egCore, egLovefield) {
22 // the 'offline' schema is only active in the offline UI.
23 egLovefield.activeSchemas.push('offline');
24 return egCore.startup.go();
28 $routeProvider.when('/offline-interface/:tab', {
29 templateUrl: 'offline-template',
30 controller: 'OfflineCtrl',
35 $routeProvider.otherwise({
36 templateUrl : 'offline-template',
37 controller : 'OfflineCtrl',
42 .controller('OfflineSessionCtrl',
43 ['$scope','$window','egCore','$routeParams','$http','$q','$timeout','egPromptDialog','ngToast','egProgressDialog',
44 function($scope , $window , egCore , $routeParams , $http , $q , $timeout , egPromptDialog , ngToast , egProgressDialog) {
45 $scope.active_session_tab = 'pending';
47 $scope.lookupNoncatTypeName = function (type) {
48 var nc = $scope.noncats.filter(function(n){ return n.id() == type })[0];
49 if (nc) return nc.name();
53 $scope.createDate = function (ts, epoch) {
55 if (epoch) ts = ts * 1000;
59 $scope.setSession = function (s, ind) {
60 $scope.current_session = s;
61 $scope.current_session_index = ind;
63 return $scope.refreshExceptions(s);
66 $scope.createSession = function () {
68 return egPromptDialog.open(
69 egCore.strings.OFFLINE_SESSION_DESC, '',
70 {ok : function(value) {
73 return $http.get(formURL({action:'create',desc:value})).then(function(res) {
74 if (res.data.ilsevent == "0") return $q.when(res.data.payload);
76 }).then(function (seskey) {
77 return $scope.refreshSessions().then(function() {
79 var s = $scope.sessions.filter(function(s){ s.key == seskey })[0];
80 var ind = $scope.sessions.length - 1; // sorted by create time, so new one is last
81 return $scope.setSession(s, ind);
85 ngToast.warning(egCore.strings.OFFLINE_SESSION_CREATE_FAILED);
92 $scope.processSession = function (s, ind) {
93 return $scope.setSession(s, ind).then(function() {
94 egProgressDialog.open();
97 formURL({action:'execute',seskey:$scope.current_session.key})
98 ).then(function(res) {
99 if (res.data.ilsevent == "0") return $q.when(res.data.payload);
101 }).then(function () {
102 egProgressDialog.close();
103 return $scope.refreshSessions()
104 .then(function(){ return $scope.refreshExceptions(s) });
106 egProgressDialog.close();
107 return $scope.refreshSessions().then(function() {
108 ngToast.warning(egCore.strings.OFFLINE_SESSION_PROCESSING_FAILED);
114 $scope.refreshExceptions = function (s) {
118 status_type : 'exceptions',
121 ).then(function(res) {
122 if (res.data.ilsevent) {
123 $scope.current_session.exceptions = [];
125 $scope.current_session.exceptions = res.data;
131 $scope.sessions = [];
132 $scope.refreshSessions = function () {
134 return $http.get(formURL({action:'status',status_type:'sessions'})).then(function(res) {
135 if (angular.isArray(res.data)) {
136 $scope.sessions = res.data;
141 var creator_list = [$q.when()];
142 angular.forEach($scope.sessions, function (s) {
144 s.org = egCore.org.get(s.org).shortname();
145 creator_list.push(egCore.pcrud.retrieve('au',s.creator).then(function(u) {
146 s.creator = u.family_name();
148 angular.forEach(s.scripts, function(sc) {
153 return $q.all(creator_list);
157 $scope.reprintLast = function () {
158 egCore.print.reprintLast();
162 $scope.uploadPending = function (s, ind) {
163 return $scope.setSession(s, ind).then(function() {
165 egProgressDialog.open();
166 return $scope.createOfflineXactBlob().then(function(blob) {
168 var form = new FormData();
169 form.append("ses", egCore.auth.token());
170 form.append("org", $scope.org.id());
171 form.append("ws", $scope.current_workstation_name());
172 form.append("wc", 1);
173 form.append("action", "load");
174 form.append("seskey", $scope.current_session.key);
175 form.append("file", blob, "file");
178 '/cgi-bin/offline/offline.pl?' + new Date().getTime(),
181 transformRequest: angular.identity,
182 headers: {'Content-Type': undefined}
184 ).then(function(res) {
185 egProgressDialog.close();
186 if (res.data.ilsevent == "0") {
187 return $scope.clear_pending(true).then(function() {
188 return $scope.refreshSessions();
191 ngToast.warning(egCore.strings.OFFLINE_SESSION_UPLOAD_FAILED);
192 return $scope.refreshSessions();
194 },function () { egProgressDialog.close() });
199 $scope.retrieveDetails = function (x) {
200 alert(JSON.stringify(x, null, 2)); // egAlertDialog kills pretty printing
203 $scope.retrieveItem = function (bc) {
204 return egCore.pcrud.search('acp',{deleted: 'f', barcode: bc}).then(function(copy) {
207 egCore.env.basePath +
208 '/cat/item/' + copy.id(),
213 ngToast.warning(egCore.strings.ITEM_NOT_FOUND);
217 $scope.retrievePatron = function (bc) {
218 return egCore.pcrud.search('ac',{barcode: bc}).then(function(card) {
221 egCore.env.basePath +
222 '/circ/patron/' + card.usr() + '/checkout',
227 ngToast.warning(egCore.strings.PATRON_NOT_FOUND);
231 function formURL (params) {
232 var url = '/cgi-bin/offline/offline.pl?' + new Date().getTime();
235 org : $scope.org ? $scope.org.id() : null,
236 ws : $scope.current_workstation_name(),
238 ses : egCore.auth.token()
241 angular.extend(params, defaults)
244 for (var k in params) {
245 url += '&' + k + '=' + window.encodeURIComponent(params[k]);
250 $scope.$watch('org',function(n){if (n) $scope.refreshSessions()});
255 .controller('OfflineCtrl',
256 ['$q','$scope','$window','$location','$rootScope','egCore',
257 'egLovefield','$routeParams','$timeout','$http','ngToast',
258 'egConfirmDialog','egUnloadPrompt','egProgressDialog', '$filter',
259 function($q , $scope , $window , $location , $rootScope , egCore ,
260 egLovefield , $routeParams , $timeout , $http , ngToast ,
261 egConfirmDialog , egUnloadPrompt, egProgressDialog, $filter) {
263 // Immediately redirect if we're really offline
264 if (!$window.navigator.onLine) {
265 if ($location.path().match(/session$/)) {
266 var path = $location.path();
267 console.log('internal redirect');
268 return $location.path(path.replace('session','checkout'));
272 var today = new Date();
276 today.setMilliseconds(0);
278 $scope.minDate = today;
279 $scope.blocked_patron = null;
280 $scope.bad_barcode = null;
281 $scope.barcode_type = 'barcode';
282 $scope.focusMe = true;
283 $scope.shared = { outOfRange : false, due_date : null, due_date_offset : '' };
284 $scope.workstation_obj = null;
285 $scope.workstation = '';
286 $scope.workstation_owner = '';
287 $scope.workstations = [];
289 $scope.do_print = Boolean($scope.active_tab == 'checkout');
290 $scope.do_print_changed = false;
291 $scope.printed = false;
292 $scope.imported_pending_xacts = { data : '' };
294 $scope.xact_page = { checkin:[], checkout:[], renew:[], in_house_use:[] };
295 $scope.all_xact = [];
298 $scope.checkout = { noncat_type : '' };
299 $scope.renew = { noncat_type : '' };
300 $scope.in_house_use = {count : 1};
301 $scope.checkin = { backdate : new Date() };
303 egLovefield.getOfflineBlockDate().then(
304 function(blockListDateResp) {
305 if (blockListDateResp) {
306 $scope.blockListDate =
307 Math.round(blockListDateResp.getTime() / 1000);
311 console.error("Error when retrieving block list download date");
315 $scope.current_workstation_owning_lib = function () {
316 return $scope.workstations.filter(function(w) {
317 return $scope.workstation == w.id
321 $scope.current_workstation_name = function () {
322 return $scope.workstations.filter(function(w) {
323 return $scope.workstation == w.id
327 $scope.$watch('workstation', function (n,o) {
329 $scope.org = egCore.org.get($scope.current_workstation_owning_lib());
332 $scope.changeCheck = function () {
333 $scope.strict_barcode = !$scope.strict_barcode;
334 $scope.do_check_changed = true;
335 egCore.hatch.setItem('eg.offline.strict_barcode', $scope.strict_barcode)
338 $scope.changePrint = function () {
339 $scope.do_print = !$scope.do_print;
340 $scope.do_print_changed = true;
341 egCore.hatch.setItem('eg.offline.print_receipt', $scope.do_print)
344 $scope.lookupNoncatTypeName = function (type) {
345 var nc = $scope.noncats.filter(function(n){ return n.id() == type })[0];
346 if (nc) return nc.name();
350 $scope.logged_in = egCore.auth.token() ? true : false;
353 $scope.active_tab = $routeParams.tab;
355 if (!$scope.logged_in) {
356 $scope.active_tab = 'checkout';
358 $scope.active_tab = 'session';
362 egCore.hatch.getItem('eg.offline.print_receipt')
363 .then(function(setting) {
364 $scope.do_print = setting;
365 if (setting !== undefined) $scope.do_print_changed = true;
368 egCore.hatch.getItem('eg.offline.strict_barcode')
369 .then(function(setting) {
370 $scope.strict_barcode = setting;
371 if (setting !== undefined) $scope.do_check_changed = true;
374 egCore.hatch.getWorkstations()
375 .then(function(all) {
376 if (all && all.length) {
377 $scope.workstations = all;
379 if (ws = $location.search().ws) {
380 // user requested a workstation via URL
381 var match = all.filter(
382 function(w) {return ws == w.name} )[0];
385 // requested WS registered on this client
386 $scope.workstation_obj = match;
387 $scope.workstation = match.id;
388 $scope.workstation_owner = match.owning_lib;
390 // the requested WS is not registered on this client
391 $scope.wsNotRegistered = true;
394 // no workstation requested; use the default
395 egCore.hatch.getDefaultWorkstation()
397 var ws_obj = all.filter(function(w) {
401 $scope.workstation_obj = ws_obj;
402 $scope.workstation = ws_obj.id;
403 $scope.workstation_owner = ws_obj.owning_lib;
405 return egLovefield.reconstituteList('cnct').then(function () {
406 $scope.noncats = egCore.env.cnct.list;
413 $scope.buildingBlockList = false;
414 $scope.downloadBlockList = function () {
415 $scope.buildingBlockList = true;
416 egProgressDialog.open();
417 egLovefield.populateBlockList().then(
419 egLovefield.setOfflineBlockDate();
420 ngToast.create(egCore.strings.OFFLINE_BLOCKLIST_SUCCESS);
423 ngToast.warning(egCore.strings.OFFLINE_BLOCKLIST_FAIL);
424 egCore.audio.play('warning.offline.blocklist_fail');
426 )['finally'](function() {
427 $scope.buildingBlockList = false;
428 egProgressDialog.close();
432 $scope.createOfflineXactBlob = function () {
433 return egLovefield.retrievePendingOfflineXacts().then(function(list) {
435 angular.forEach(list, function (i) {
436 flat_list.push(JSON.stringify(i) + '\n');
439 var blob = new Blob(flat_list, {type: 'text/plain'});
445 $scope.pending_xacts = [];
446 $scope.retrieve_pending = function () {
447 return egLovefield.retrievePendingOfflineXacts().then(function(list) {
448 $scope.pending_xacts = list;
449 return $q.when(list);
453 $scope.save = function () {
454 var promises = [$q.when()];
455 angular.forEach($scope.all_xact, function (x) {
456 promises.push(egLovefield.addOfflineXact(x));
459 var prints = [$q.when()];
460 if ($scope.do_print) {
461 angular.forEach(['checkin','checkout','renew','in_house_use'], function(xtype) {
462 if ($scope.xact_page[xtype].length > 0) {
463 prints.push(egCore.print.print({
465 template : 'offline_'+xtype,
467 transactions : $scope.xact_page[xtype]
474 return $q.all(promises.concat(prints)).finally(function() {
475 egUnloadPrompt.clear();
476 if (prints.length > 1) $scope.printed = true;
477 $scope.all_xact = [];
478 $scope.xact_page = { checkin:[], checkout:[], renew:[], in_house_use:[] };
479 angular.forEach(['checkout','renew'], function (xtype) {
480 $scope[xtype].patron_barcode = '';
482 $scope.retrieve_pending();
486 $rootScope.save_offline_xacts = function () { return $scope.save() };
487 $rootScope.active_tab = function (t) { $scope.active_tab = t };
489 $scope.logout = function () {
490 egCore.auth.logout();
491 $window.location.href = location.href;
494 $scope.clear_pending = function (skip_confirm) {
496 return egLovefield.destroyPendingOfflineXacts().then(function () {
497 return $scope.retrieve_pending();
500 return egConfirmDialog.open(
501 egCore.strings.CONFIRM_CLEAR_PENDING,
502 egCore.strings.CONFIRM_CLEAR_PENDING_BODY,
504 ).result.then(function() {
505 return egLovefield.destroyPendingOfflineXacts().then(function () {
506 return $scope.retrieve_pending();
512 $scope.retrieve_pending();
513 $scope.$watch('active_tab', function (n,o) {
514 console.log('watch caught change to active_tab: ' + o + ' -> ' + n);
515 if (n != o && !$scope.do_check_changed && n != 'checkout') $scope.strict_barcode = false;
516 if (n != o && !$scope.do_check_changed && n == 'checkout') $scope.strict_barcode = true;
517 if (n != o && !$scope.do_print_changed && n != 'checkout') $scope.do_print = false;
518 if (n != o && !$scope.do_print_changed && n == 'checkout') $scope.do_print = true;
519 if (n != o && n == 'session') $scope.retrieve_pending();
522 $scope.$watch('imported_pending_xacts.data', function (n, o) {
524 var lines = n.split('\n');
527 angular.forEach(lines, function (l) {
532 egLovefield.addOfflineXact(JSON.parse(l))
535 ngToast.warning(err);
539 $q.all(promises).then(function () { $scope.retrieve_pending() });
543 $scope.resetDueDate = function (xtype) {
544 $scope.shared.due_date = new Date();
545 $scope.shared.due_date.setDate($scope.shared.due_date.getDate() + parseInt($scope.shared.due_date_offset));
548 $scope.notEnough = function (xtype) {
550 if (xtype == 'checkout') {
551 if ($scope.shared.outOfRange) return true;
553 $scope.checkout.patron_barcode &&
554 ($scope.shared.due_date || $scope.shared.due_date_offset) &&
555 ($scope.checkout.barcode || ($scope.checkout.noncat_type && $scope.checkout.noncat_count))
560 if (xtype == 'renew') {
561 if ($scope.shared.outOfRange) return true;
563 $scope.renew.barcode &&
564 ($scope.shared.due_date || $scope.shared.due_date_offset)
569 if (xtype == 'in_house_use') {
571 $scope.in_house_use.barcode && $scope.in_house_use.count
576 if (xtype == 'checkin') {
578 $scope.checkin.barcode && $scope.checkin.backdate
584 $scope.clear = function (xtype) {
586 if (xtype=="in_house_use") $scope[xtype].count = 1;
589 $scope.add = function (xtype,next_focus) {
591 var barcode = $scope[xtype].barcode;
593 if ($scope.xact_page[xtype].filter(function(x){ return x.barcode == barcode }).length > 0) {
594 ngToast.warning(egCore.strings.DUPLICATE_BARCODE);
595 egCore.audio.play('warning.offline.duplicate_barcode');
596 $scope[xtype].barcode = '';
597 if (next_focus) $('#'+next_focus).focus();
602 var pbarcode = $scope[xtype].patron_barcode;
604 egLovefield.testOfflineBlock(pbarcode).then(function (blocked) {
606 egCore.audio.play('warning.offline.blocked_patron');
607 var default_format = 'mediumDate';
608 egCore.org.settings(['format.date']).then(function(set) {
609 if (set && set['format.date']) default_format = set['format.date'];
610 $scope.date_format = default_format;
611 var fBlockListDate = $scope.blockListDate ?
612 $filter('date')(($scope.blockListDate * 1000), $scope.date_format) :
614 egConfirmDialog.open(
615 egCore.strings.PATRON_BLOCKED,
616 egCore.strings.PATRON_BLOCKED_WHY[blocked],
617 {formatted_date: fBlockListDate, pbarcode: pbarcode},
618 egCore.strings.ALLOW,
619 egCore.strings.REJECT
621 function(){ // forced
622 $scope.blocked_patron = null;
623 _add_impl(xtype,true)
624 if (next_focus) $('#'+next_focus).focus();
625 },function(){ // stopped
626 $scope.blocked_patron = xtype;
627 if (next_focus) $('#'+next_focus).focus();
633 $scope.blocked_patron = null;
634 _add_impl(xtype,true)
635 if (next_focus) $('#'+next_focus).focus();
640 if (next_focus) $('#'+next_focus).focus();
644 function _add_impl (xtype,digest) {
645 var pbarcode = $scope[xtype].patron_barcode;
646 var backdate = $scope[xtype].backdate;
648 if ($scope.strict_barcode && pbarcode) {
649 if (!check_barcode(pbarcode)) {
650 $scope.bad_barcode = xtype;
651 egCore.audio.play('warning.offline.bad_barcode');
652 return egConfirmDialog.open(
653 egCore.strings.BAD_PATRON_BARCODE,
654 egCore.strings.BAD_PATRON_BARCODE_CD,
655 {}, egCore.strings.ALLOW, egCore.strings.REJECT
657 function(){ // forced
658 $scope.blocked_patron = null;
659 return _add_impl2(xtype,digest)
660 },function(){ // stopped
661 $scope.blocked_patron = xtype;
667 if ($scope.strict_barcode && $scope[xtype].barcode) {
668 if (!check_barcode($scope[xtype].barcode)) {
669 $scope.bad_barcode = xtype;
670 egCore.audio.play('warning.offline.bad_barcode');
671 return egConfirmDialog.open(
672 egCore.strings.BAD_BARCODE,
673 egCore.strings.BAD_BARCODE_CD,
674 {}, egCore.strings.ALLOW, egCore.strings.REJECT
676 function(){ // forced
677 $scope.blocked_patron = null;
678 return _add_impl2(xtype,digest)
679 },function(){ // stopped
680 $scope.blocked_patron = xtype;
686 return _add_impl2(xtype,digest);
689 function _add_impl2 (xtype,digest) {
690 var pbarcode = $scope[xtype].patron_barcode;
691 var backdate = $scope[xtype].backdate;
693 $scope.bad_barcode = null;
695 var now = new Date().getTime();
698 if ($scope[xtype].noncat_type) $scope[xtype].noncat = 1;
700 if ($scope.shared.due_date && (xtype == 'checkout' || xtype == 'renew')) {
701 $scope[xtype].due_date = $scope.shared.due_date.toISOString();
702 $scope[xtype].checkout_time = new Date().toISOString();
705 var xact = { timestamp : parseInt(now), type : xtype, delta : 0 };
707 $scope.xact_page[xtype].push(
708 angular.extend(xact, $scope[xtype])
711 $scope.all_xact.push(xact)
712 egUnloadPrompt.attach($rootScope);
716 if (pbarcode) $scope[xtype].patron_barcode = pbarcode;
717 if (backdate) $scope[xtype].backdate = backdate;
718 if (xtype=="in_house_use") $scope[xtype].count = 1;
720 if (digest) $timeout(function(){$scope.$apply()});
723 check_barcode = function(bc) {
724 if (bc != Number(bc)) return false;
726 // "16.00" == Number("16.00"), but the . is bad.
727 // Throw out any barcode that isn't just digits
728 if (bc.search(/\D/) != -1) return false;
729 var last_digit = bc.substr(bc.length-1);
730 var stripped_barcode = bc.substr(0,bc.length-1);
731 return barcode_checkdigit(stripped_barcode).toString() == last_digit;
734 barcode_checkdigit = function(bc) {
735 var reverse_barcode = bc.toString().split('').reverse();
736 var check_sum = 0; var multiplier = 2;
737 for (var i = 0; i < reverse_barcode.length; i++) {
738 var digit = reverse_barcode[i];
739 var product = digit * multiplier; product = product.toString();
741 for (var j = 0; j < product.length; j++) {
742 temp_sum += Number( product[j] );
744 check_sum += Number( temp_sum );
745 multiplier = ( multiplier == 2 ? 1 : 2 );
747 check_sum = check_sum.toString();
748 var next_multiple_of_10 = (check_sum.match(/(\d*)\d$/)[1] * 10) + 10;
749 var check_digit = next_multiple_of_10 - Number(check_sum);
750 if (check_digit == 10) check_digit = 0;
754 function fetch_org_after_tree_exists () {
757 $scope.org = egCore.org.get($scope.current_workstation_owning_lib());
759 fetch_org_after_tree_exists();
764 fetch_org_after_tree_exists();
768 // dummy service so standalone patron editor can reference it
769 .factory('patronSvc', function() { return { /* dummy */ } })
771 .factory('patronRegSvc', ['$q', 'egCore', 'egLovefield', function($q, egCore, egLovefield) {
773 egLovefield.isOffline = true;
776 org : null, // will come from workstation org
777 field_doc : {}, // config.idl_field_doc
778 profiles : [], // permission groups
779 edit_profiles : [], // perm groups we can modify
781 user_settings : {}, // applied user settings
782 user_setting_types : {}, // config.usr_setting_type
783 opt_in_setting_types : {}, // config.usr_setting_type for event-def opt-in
785 survey_questions : {},
787 survey_responses : {}, // survey.responses for loaded patron in progress
789 stat_cat_entry_maps : {}, // cat.id to selected value
790 virt_id : -1, // virtual ID for new objects
791 init_done : false // have we loaded our initialization data?
794 service.offlineMode = function () {
798 // launch a series of parallel data retrieval calls
799 service.init = function(scope) {
801 // Data loaded here only needs to be retrieved the first time this
802 // tab becomes active within the current instance of the patron app.
803 // In other words, navigating between patron tabs will not cause
804 // all of this data to be reloaded. Navigating to a separate app
805 // and returning will cause the data to be reloaded.
806 if (service.init_done) return $q.when();
807 service.init_done = true;
810 service.get_field_doc(),
811 service.get_perm_groups(),
812 service.get_ident_types(),
813 service.get_user_settings(),
814 service.get_org_settings(),
815 service.get_stat_cats(),
816 service.get_surveys(),
817 service.get_net_access_levels()
821 service.get_linked_addr_users = function(addrs) {
825 service.apply_secondary_groups = function(user_id, group_ids) {
826 return $q.when(true);
829 // See note above about not loading egUser.
831 service.format_name = function(last, first, middle) {
832 return last + ', ' + first + (middle ? ' ' + middle : '');
835 service.check_dupe_username = function(usrname) {
836 return $q.when(false);
839 // determine which user groups our user is not allowed to modify
840 service.set_edit_profiles = function() {
841 service.edit_profiles = egCore.env.pgt.list.filter(
842 function (p) { return p.application_perm() == 'group_application.user.patron' }
847 // resolves to a hash of perm-name => boolean value indicating
848 // wether the user has the permission at org_id.
849 service.has_perms_for_org = function(org_id) {
854 'CREATE_USER_GROUP_LINK',
855 'UPDATE_PATRON_COLLECTIONS_EXEMPT',
856 'UPDATE_PATRON_CLAIM_RETURN_COUNT',
857 'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
858 'UPDATE_PATRON_ACTIVE_CARD',
859 'UPDATE_PATRON_PRIMARY_CARD'
863 angular.forEach(perms_needed, function (p) {
867 return $q.when(hash);
870 service.get_surveys = function() {
871 return egLovefield.reconstituteList('asv').then(function(offline) {
872 return egLovefield.reconstituteList('asvq')
874 return egLovefield.reconstituteList('asva');
876 angular.forEach(egCore.env.asv.list, function (s) {
877 s.questions( egCore.env.asvq.list.filter( function (q) {
878 return q.survey().id == s.id();
882 angular.forEach(egCore.env.asvq.list, function (q) {
883 q.survey( egCore.env.asv.map[ q.survey().id ] );
884 q.answers( egCore.env.asva.list.filter( function (a) {
885 return q.id() == a.question();
889 angular.forEach(egCore.env.asva.list, function (a) {
890 a.question( egCore.env.asvq.map[ a.question().id ] );
893 service.surveys = egCore.env.asv.list;
894 service.survey_questions = egCore.env.asvq.list;
895 service.survey_answers = egCore.env.asva.list;
902 service.get_stat_cats = function() {
903 return egLovefield.getStatCatsCache().then(
905 service.stat_cats = cats;
911 service.get_org_settings = function() {
912 return egLovefield.getSettingsCache().then(
915 angular.forEach(list, function (s) {
916 hash[s.name] = s.value;
918 service.org_settings = hash;
919 if (egCore && egCore.env && !egCore.env.aous) {
920 egCore.env.aous = hash;
921 console.log('setting egCore.env.aous');
928 service.get_ident_types = function() {
929 return egLovefield.reconstituteList('cit').then(function() {
930 service.ident_types = egCore.env.cit.list;
935 service.get_net_access_levels = function() {
936 return egLovefield.reconstituteList('cnal').then(function() {
937 service.net_access_levels = egCore.env.cnal.list;
942 service.get_perm_groups = function() {
943 if (egCore.env.pgt) {
944 service.profiles = egCore.env.pgt.list;
945 return service.set_edit_profiles();
947 return egLovefield.reconstituteTree('pgt').then(function(offline) {
948 service.profiles = egCore.env.pgt.list;
949 return service.set_edit_profiles();
954 service.get_field_doc = function() {
955 return egLovefield.getListFromOfflineCache('fdoc').then(function (list) {
956 angular.forEach(list, function(doc) {
957 if (!service.field_doc[doc.fm_class()])
958 service.field_doc[doc.fm_class()] = {};
959 service.field_doc[doc.fm_class()][doc.field()] = doc;
965 service.get_user_settings = function() {
967 'circ.holds_behind_desk',
968 'circ.collections.exempt',
970 'opac.default_phone',
971 'opac.default_pickup_location',
972 'opac.default_sms_carrier',
973 'opac.default_sms_notify'];
975 angular.forEach(static_types, function (t) {
976 service.user_settings[t] = null;
979 return egLovefield.getListFromOfflineCache('cust').then(function (list) {
980 angular.forEach(list, function(stype) {
981 service.user_setting_types[stype.name()] = stype;
982 if (static_types.indexOf(stype.name()) == -1) {
983 service.opt_in_setting_types[stype.name()] = stype;
985 if (stype.reg_default() != undefined) {
986 service.user_settings[setting.name()] =
987 setting.reg_default();
994 service.invalidate_field = function(patron, field) {
998 service.dupe_patron_search = function(patron, type, value) {
999 return $q.when({ search : search, count : 0 });
1002 service.init_patron = function(current) {
1005 return service.init_new_patron();
1007 service.patron = current;
1008 return service.init_existing_patron(current)
1011 service.ingest_address = function(patron, addr) {
1012 addr.valid = addr.valid == 't';
1013 addr.within_city_limits = addr.within_city_limits == 't';
1014 addr._is_mailing = (patron.mailing_address &&
1015 addr.id == patron.mailing_address.id);
1016 addr._is_billing = (patron.billing_address &&
1017 addr.id == patron.billing_address.id);
1021 * Existing patron objects reqire some data munging before insertion
1024 * 1. Turn everything into a hash
1025 * 2. ... Except certain fields (selectors) whose widgets require objects
1026 * 3. Bools must be Boolean, not t/f.
1028 service.init_existing_patron = function(current) {
1030 service.existing_patron = current;
1032 var patron = egCore.idl.toHash(current);
1034 patron.home_ou = egCore.org.get(patron.home_ou.id);
1035 patron.expire_date = new Date(Date.parse(patron.expire_date));
1036 patron.dob = service.parse_dob(patron.dob);
1037 patron.profile = current.profile(); // pre-hash version
1038 patron.net_access_level = current.net_access_level();
1039 patron.ident_type = current.ident_type();
1040 patron.groups = current.groups(); // pre-hash
1043 ['juvenile', 'barred', 'active', 'master_account'],
1044 function(field) { patron[field] = patron[field] == 't'; }
1047 angular.forEach(patron.cards, function(card) {
1048 card.active = card.active == 't';
1049 if (card.id == patron.card.id) {
1051 card._primary = 'on';
1055 angular.forEach(patron.addresses,
1056 function(addr) { service.ingest_address(patron, addr) });
1058 service.get_linked_addr_users(patron.addresses);
1060 // Remove stat cat entries that link to out-of-scope stat
1061 // cats. With this, we avoid unnecessarily updating (or worse,
1062 // modifying) stat cat values that are not ours to modify.
1063 patron.stat_cat_entries = patron.stat_cat_entries.filter(
1066 // service.stat_cats only contains in-scope stat cats.
1067 service.stat_cats.filter(function(cat) {
1068 return (cat.id() == map.stat_cat.id) })[0]
1073 // toss entries for existing stat cat maps into our living
1074 // stat cat entry map, which is modified within the template.
1075 angular.forEach(patron.stat_cat_entries, function(map) {
1076 service.stat_cat_entry_maps[map.stat_cat.id] = map.stat_cat_entry;
1082 service.init_new_patron = function() {
1084 id : service.virt_id--,
1087 address_type : egCore.strings.REG_ADDR_TYPE,
1090 within_city_limits : false,
1091 country : service.org_settings['ui.patron.default_country'],
1095 id : service.virt_id--,
1101 var home_ou = egCore.org.get(service.org);
1109 stat_cat_entries : [],
1114 if (service.clone_user)
1115 service.copy_clone_data(user);
1117 if (service.stage_user)
1118 service.copy_stage_data(user);
1123 // dob is always YYYY-MM-DD
1124 // Dates of birth do not contain timezone info, which can lead to
1125 // inconcistent timezone handling, potentially representing
1126 // different points in time, depending on the implementation.
1127 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
1128 // See "Differences in assumed time zone"
1129 // TODO: move this into egDate ?
1130 service.parse_dob = function(dob) {
1131 if (!dob) return null;
1132 var parts = dob.split('-');
1133 var d = new Date(); // always local time zone, yay.
1134 d.setFullYear(parts[0]);
1135 d.setMonth(parts[1] - 1);
1136 d.setDate(parts[2]);
1140 service.copy_stage_data = function(user) {
1141 var cuser = service.stage_user;
1143 // copy the data into our new user object
1145 for (var key in egCore.idl.classes.stgu.field_map) {
1146 if (egCore.idl.classes.au.field_map[key] &&
1147 !egCore.idl.classes.stgu.field_map[key].virtual) {
1148 if (cuser.user[key]() !== null)
1149 user[key] = cuser.user[key]();
1153 if (user.home_ou) user.home_ou = egCore.org.get(user.home_ou);
1154 if (user.profile) user.profile = egCore.env.pgt.map[user.profile];
1155 if (user.ident_type)
1156 user.ident_type = egCore.env.cit.map[user.ident_type];
1157 user.dob = service.parse_dob(user.dob);
1159 // Clear the usrname if it looks like a UUID
1160 if (user.usrname.replace(/-/g,'').match(/[0-9a-f]{32}/))
1163 // Don't use stub address if we have one from the staged user.
1164 if (cuser.mailing_addresses.length || cuser.billing_addresses.length)
1165 user.addresses = [];
1167 // is_mailing=false implies is_billing
1168 function addr_from_stage(stage_addr) {
1169 if (!stage_addr) return;
1170 var cls = stage_addr.classname;
1173 id : service.virt_id--,
1177 _is_mailing : cls == 'stgma',
1178 _is_billing : cls == 'stgba'
1181 user.mailing_address = addr;
1182 user.addresses.push(addr);
1184 for (var key in egCore.idl.classes[cls].field_map) {
1185 if (egCore.idl.classes.aua.field_map[key] &&
1186 !egCore.idl.classes[cls].field_map[key].virtual) {
1187 if (stage_addr[key]() !== null)
1188 addr[key] = stage_addr[key]();
1193 addr_from_stage(cuser.mailing_addresses[0]);
1194 addr_from_stage(cuser.billing_addresses[0]);
1196 if (user.addresses.length == 1) {
1197 // If there is only one address,
1198 // use it as both mailing and billing.
1199 var addr = user.addresses[0];
1200 addr._is_mailing = addr._is_billing = true;
1201 user.mailing_address = user.billing_address = addr;
1204 if (cuser.cards.length) {
1206 id : service.virt_id--,
1207 barcode : cuser.cards[0].barcode(),
1213 user.cards.push(user.card);
1214 if (user.usrname == '')
1215 user.usrname = card.barcode;
1218 angular.forEach(cuser.settings, function(setting) {
1219 service.user_settings[setting.setting()] = Boolean(setting.value());
1223 // copy select values from the cloned user to the new user.
1225 service.copy_clone_data = function(user) {
1226 var clone_user = service.clone_user;
1228 // flesh the home org locally
1229 user.home_ou = egCore.org.get(clone_user.home_ou());
1230 if (user.profile) user.profile = egCore.env.pgt.map[user.profile];
1232 if (!clone_user.billing_address() &&
1233 !clone_user.mailing_address())
1234 return; // no addresses to copy or link
1236 // if the cloned user has any addresses, we don't need
1237 // the stub address created in init_new_patron.
1238 user.addresses = [];
1240 var copy_addresses =
1241 service.org_settings['circ.patron_edit.clone.copy_address'];
1243 var clone_fields = [
1250 angular.forEach(clone_fields, function(field) {
1251 user[field] = clone_user[field]();
1254 if (copy_addresses) {
1255 var bill_addr, mail_addr;
1257 // copy the billing and mailing addresses into new addresses
1258 function clone_addr(addr) {
1259 var new_addr = egCore.idl.toHash(addr);
1260 new_addr.id = service.virt_id--;
1261 new_addr.usr = user.id;
1262 new_addr.isnew = true;
1263 new_addr.valid = true;
1264 user.addresses.push(new_addr);
1268 if (bill_addr = clone_user.billing_address()) {
1269 var addr = clone_addr(bill_addr);
1270 addr._is_billing = true;
1271 user.billing_address = addr;
1274 if (mail_addr = clone_user.mailing_address()) {
1276 if (bill_addr && bill_addr.id() == mail_addr.id()) {
1277 user.mailing_address = user.billing_address;
1278 user.mailing_address._is_mailing = true;
1280 var addr = clone_addr(mail_addr);
1281 addr._is_mailing = true;
1282 user.mailing_address = addr;
1286 // if there is no billing addr, use the mailing addr
1287 user.billing_address = user.mailing_address;
1288 user.billing_address._is_billing = true;
1295 // link the billing and mailing addresses
1297 if (addr = clone_user.billing_address()) {
1298 user.billing_address = egCore.idl.toHash(addr);
1299 user.billing_address._is_billing = true;
1300 user.addresses.push(user.billing_address);
1301 user.billing_address._linked_owner_id = clone_user.id();
1302 user.billing_address._linked_owner = service.format_name(
1303 clone_user.family_name(),
1304 clone_user.first_given_name(),
1305 clone_user.second_given_name()
1309 if (addr = clone_user.mailing_address()) {
1310 if (user.billing_address &&
1311 addr.id() == user.billing_address.id) {
1312 // mailing matches billing
1313 user.mailing_address = user.billing_address;
1314 user.mailing_address._is_mailing = true;
1316 user.mailing_address = egCore.idl.toHash(addr);
1317 user.mailing_address._is_mailing = true;
1318 user.addresses.push(user.mailing_address);
1319 user.mailing_address._linked_owner_id = clone_user.id();
1320 user.mailing_address._linked_owner = service.format_name(
1321 clone_user.family_name(),
1322 clone_user.first_given_name(),
1323 clone_user.second_given_name()
1330 // translate the patron back into IDL form
1331 service.save_user = function(phash) {
1333 var patron = egCore.idl.fromHash('au', phash);
1335 patron.home_ou(patron.home_ou().id());
1336 patron.expire_date(patron.expire_date().toISOString());
1337 patron.profile(patron.profile().id());
1339 patron.dob(patron.dob().toISOString().replace(/T.*/,''));
1340 if (patron.ident_type())
1341 patron.ident_type(patron.ident_type().id());
1342 if (patron.net_access_level())
1343 patron.net_access_level(patron.net_access_level().id());
1346 ['juvenile', 'barred', 'active', 'master_account'],
1347 function(field) { patron[field](phash[field] ? 't' : 'f'); }
1350 var card_hashes = patron.cards();
1352 angular.forEach(card_hashes, function(chash) {
1353 var card = egCore.idl.fromHash('ac', chash)
1354 card.usr(patron.id());
1355 card.active(chash.active ? 't' : 'f');
1356 patron.cards().push(card);
1357 if (chash._primary) {
1362 var addr_hashes = patron.addresses();
1363 patron.addresses([]);
1364 angular.forEach(addr_hashes, function(addr_hash) {
1365 if (!addr_hash.isnew && !addr_hash.isdeleted)
1366 addr_hash.ischanged = true;
1367 var addr = egCore.idl.fromHash('aua', addr_hash);
1368 patron.addresses().push(addr);
1369 addr.valid(addr.valid() ? 't' : 'f');
1370 addr.within_city_limits(addr.within_city_limits() ? 't' : 'f');
1371 if (addr_hash._is_mailing) patron.mailing_address(addr);
1372 if (addr_hash._is_billing) patron.billing_address(addr);
1375 patron.survey_responses([]);
1376 angular.forEach(service.survey_responses, function(answer) {
1377 var question = service.survey_questions[answer.question()];
1378 var resp = new egCore.idl.asvr();
1380 resp.survey(question.survey());
1381 resp.question(question.id());
1382 resp.answer(answer.id());
1383 resp.usr(patron.id());
1384 resp.answer_date('now');
1385 patron.survey_responses().push(resp);
1388 // re-object-ify the patron stat cat entry maps
1390 angular.forEach(patron.stat_cat_entries(), function(entry) {
1391 var e = egCore.idl.fromHash('actscecm', entry);
1392 e.stat_cat(e.stat_cat().id);
1395 patron.stat_cat_entries(maps);
1397 // service.stat_cat_entry_maps maps stats to values
1398 // patron.stat_cat_entries is an array of stat_cat_entry_usr_map's
1400 service.stat_cat_entry_maps, function(value, cat_id) {
1402 // see if we already have a mapping for this entry
1403 var existing = patron.stat_cat_entries().filter(
1404 function(e) { return e.stat_cat() == cat_id })[0];
1406 if (existing) { // we have a mapping
1407 // if the existing mapping matches the new one,
1408 // there' nothing left to do
1409 if (existing.stat_cat_entry() == value) return;
1411 // mappings differ. delete the old one and create
1413 existing.isdeleted(true);
1416 var newmap = new egCore.idl.actscecm();
1417 newmap.target_usr(patron.id());
1419 newmap.stat_cat(cat_id);
1420 newmap.stat_cat_entry(value);
1421 patron.stat_cat_entries().push(newmap);
1424 if (!patron.isnew()) patron.ischanged(true);
1426 return egLovefield.addOfflineXact({
1427 user : egCore.idl.toHash(patron),
1428 timestamp : parseInt(new Date().getTime() / 1000),
1431 }).then(function (success) {
1432 if (success) return patron;
1436 service.remove_staged_user = function() {
1437 if (!service.stage_user) return $q.when();
1438 return egCore.net.request(
1440 'open-ils.actor.user.stage.delete',
1441 egCore.auth.token(),
1442 service.stage_user.user.row_id()
1446 service.save_user_settings = function(new_user, user_settings) {
1450 // Applies field-specific validation regex's from org settings
1451 // to form fields. Be careful not remove any pattern data we
1452 // are not explicitly over-writing in the provided patterns obj.
1453 service.set_field_patterns = function(patterns) {
1454 if (service.org_settings['opac.username_regex']) {
1455 patterns.au.usrname =
1456 new RegExp(service.org_settings['opac.username_regex']);
1459 if (service.org_settings['opac.barcode_regex']) {
1460 patterns.ac.barcode =
1461 new RegExp(service.org_settings['opac.barcode_regex']);
1464 if (service.org_settings['global.password_regex']) {
1465 patterns.au.passwd =
1466 new RegExp(service.org_settings['global.password_regex']);
1469 var phone_reg = service.org_settings['ui.patron.edit.phone.regex'];
1471 // apply generic phone regex first, replace below as needed.
1472 patterns.au.day_phone = new RegExp(phone_reg);
1473 patterns.au.evening_phone = new RegExp(phone_reg);
1474 patterns.au.other_phone = new RegExp(phone_reg);
1477 // the remaining patterns fit a well-known key name pattern
1479 angular.forEach(service.org_settings, function(val, key) {
1481 var parts = key.match(/ui.patron.edit\.(\w+)\.(\w+)\.regex/);
1484 var name = parts[2];
1485 patterns[cls][name] = new RegExp(val);
1492 .controller('PatronRegCtrl',
1493 ['$scope','$routeParams','$q','$uibModal','$window','egCore',
1494 'patronSvc','patronRegSvc','egUnloadPrompt','egAlertDialog',
1495 'egWorkLog','$timeout','egLovefield','$rootScope',
1496 function($scope , $routeParams , $q , $uibModal , $window , egCore ,
1497 patronSvc , patronRegSvc , egUnloadPrompt, egAlertDialog ,
1498 egWorkLog , $timeout , egLovefield , $rootScope) {
1500 $scope.rs = $rootScope;
1501 if ($scope.workstation_obj) patronRegSvc.org = $scope.workstation_obj.owning_lib;
1502 $scope.offline = true;
1504 $scope.page_data_loaded = false;
1505 $scope.clone_id = patronRegSvc.clone_id = $routeParams.clone_id;
1506 $scope.stage_username =
1507 patronRegSvc.stage_username = $routeParams.stage_username;
1509 patronRegSvc.patron_id = $routeParams.edit_id || $routeParams.id;
1511 // for existing patrons, disable barcode input by default
1512 $scope.disable_bc = $scope.focus_usrname = Boolean($scope.patron_id);
1513 $scope.focus_bc = !Boolean($scope.patron_id);
1514 $scope.address_alerts = [];
1515 $scope.dupe_counts = {};
1517 // map of perm name to true/false for perms the logged in user
1518 // has at the currently selected patron home org unit.
1521 $scope.edit_passthru = {};
1523 // 0=all, 1=suggested, 2=all
1524 $scope.edit_passthru.vis_level = 2;
1526 // Apply default values for new patrons during initial registration
1527 // prs is shorthand for patronSvc
1528 function set_new_patron_defaults(prs) {
1529 if (!$scope.patron.passwd) {
1530 // passsword may originate from staged user.
1531 $scope.generate_password();
1533 $scope.hold_notify_phone = true;
1534 $scope.hold_notify_email = true;
1536 // staged users may be loaded w/ a profile.
1537 $scope.set_expire_date();
1539 if (prs.org_settings['ui.patron.default_ident_type']) {
1540 // $scope.patron needs this field to be an object
1541 var id = prs.org_settings['ui.patron.default_ident_type'];
1542 var ident_type = $scope.ident_types.filter(
1543 function(type) { return type.id() == id })[0];
1544 $scope.patron.ident_type = ident_type;
1546 if (prs.org_settings['ui.patron.default_inet_access_level']) {
1547 // $scope.patron needs this field to be an object
1548 var id = prs.org_settings['ui.patron.default_inet_access_level'];
1549 var level = $scope.net_access_levels.filter(
1550 function(lvl) { return lvl.id() == id })[0];
1551 $scope.patron.net_access_level = level;
1553 if (prs.org_settings['ui.patron.default_country']) {
1554 $scope.patron.addresses[0].country =
1555 prs.org_settings['ui.patron.default_country'];
1559 // A null or undefined pattern leads to exceptions. Before the
1560 // patterns are loaded from the server, default all patterns
1561 // to an innocuous regex. To avoid re-creating numerous
1562 // RegExp objects, cache the stub RegExp after initial creation.
1563 // note: angular docs say ng-pattern accepts a regexp or string,
1564 // but as of writing, it only works with a regexp object.
1565 // (Likely an angular 1.2 vs. 1.4 issue).
1566 var field_patterns = {au : {}, ac : {}, aua : {}};
1567 $scope.field_pattern = function(cls, field) {
1568 if (!field_patterns[cls][field])
1569 field_patterns[cls][field] = new RegExp('.*');
1570 return field_patterns[cls][field];
1573 patronRegSvc.offlineMode($scope.offline); // force offline if ng-init'd to do so
1574 patronRegSvc.init().then(function() {
1575 // called after initTab and patronRegSvc.init have completed
1577 var prs = patronRegSvc; // brevity
1578 // in standalone mode, we have no patronSvc
1579 $scope.patron = prs.init_patron(patronSvc ? patronSvc.current : null);
1580 $scope.field_doc = prs.field_doc;
1581 $scope.edit_profiles = prs.edit_profiles;
1582 $scope.ident_types = prs.ident_types;
1583 $scope.net_access_levels = prs.net_access_levels;
1584 $scope.user_setting_types = prs.user_setting_types;
1585 $scope.opt_in_setting_types = prs.opt_in_setting_types;
1586 $scope.org_settings = prs.org_settings;
1587 $scope.sms_carriers = prs.sms_carriers;
1588 $scope.stat_cats = prs.stat_cats;
1589 $scope.surveys = prs.surveys;
1590 $scope.survey_responses = prs.survey_responses;
1591 $scope.stat_cat_entry_maps = prs.stat_cat_entry_maps;
1592 $scope.stage_user = prs.stage_user;
1593 $scope.stage_user_requestor = prs.stage_user_requestor;
1595 $scope.user_settings = prs.user_settings;
1596 // clone the user settings back into the patronRegSvc so
1597 // we have a copy of the original state of the settings.
1598 prs.user_settings = {};
1599 angular.forEach($scope.user_settings, function(val, key) {
1600 prs.user_settings[key] = val;
1603 extract_hold_notify();
1604 $scope.handle_home_org_changed();
1606 if ($scope.org_settings['ui.patron.edit.default_suggested'])
1607 $scope.edit_passthru.vis_level = 1;
1609 if ($scope.patron.isnew)
1610 set_new_patron_defaults(prs);
1612 $scope.page_data_loaded = true;
1614 prs.set_field_patterns(field_patterns);
1615 apply_username_regex();
1618 // update the currently displayed field documentation
1619 $scope.set_selected_field_doc = function(cls, field) {
1620 $scope.selected_field_doc = $scope.field_doc[cls][field];
1623 // returns the tree depth of the selected profile group tree node.
1624 $scope.pgt_depth = function(grp) {
1626 while (grp = egCore.env.pgt.map[grp.parent()]) d++;
1630 // IDL fields used for labels in the UI.
1631 $scope.idl_fields = {
1632 au : egCore.idl.classes.au.field_map,
1633 ac : egCore.idl.classes.ac.field_map,
1634 aua : egCore.idl.classes.aua.field_map
1637 // field visibility cache. Some fields are universally required.
1638 // 3 == value universally required
1639 // 2 == field is visible by default
1640 // 1 == field is suggested by default
1641 var field_visibility = {};
1642 var default_field_visibility = {
1646 'au.first_given_name' : 3,
1647 'au.family_name' : 3,
1648 'au.ident_type' : 3,
1651 'au.expire_date' : 3,
1652 'au.net_access_level' : 3,
1653 'aua.address_type' : 3,
1654 'aua.post_code' : 3,
1662 'aua.within_city_limits' : 2,
1667 // Returns true if the selected field should be visible
1668 // given the current required/suggested/all setting.
1669 // The visibility flag applied to each field as a result of calling
1670 // this function also sets (via the same flag) the requiredness state.
1671 $scope.show_field = function(field_key) {
1672 // org settings have not been received yet.
1673 if (!$scope.org_settings) return false;
1675 if (field_visibility[field_key] == undefined) {
1676 // compile and cache the visibility for the selected field
1678 var req_set = 'ui.patron.edit.' + field_key + '.require';
1679 var sho_set = 'ui.patron.edit.' + field_key + '.show';
1680 var sug_set = 'ui.patron.edit.' + field_key + '.suggest';
1682 if ($scope.org_settings[req_set]) {
1683 field_visibility[field_key] = 3;
1685 } else if ($scope.org_settings[sho_set]) {
1686 field_visibility[field_key] = 2;
1688 } else if ($scope.org_settings[sug_set]) {
1689 field_visibility[field_key] = 1;
1693 if (field_visibility[field_key] == undefined) {
1694 // No org settings were applied above. Use the default
1695 // settings if present or assume the field has no
1696 // visibility flags applied.
1697 field_visibility[field_key] =
1698 default_field_visibility[field_key] || 0;
1701 return field_visibility[field_key] >= $scope.edit_passthru.vis_level;
1704 // See $scope.show_field().
1705 // A field with visbility level 3 means it's required.
1706 $scope.field_required = function(cls, field) {
1708 // Value in the password field is not required
1709 // for existing patrons.
1710 if (field == 'passwd' && $scope.patron && !$scope.patron.isnew)
1713 return (field_visibility[cls + '.' + field] == 3 || default_field_visibility[cls + '.' + field] == 3);
1716 // generates a random 4-digit password
1717 $scope.generate_password = function() {
1718 $scope.patron.passwd = Math.floor(Math.random()*9000) + 1000;
1721 $scope.set_expire_date = function() {
1722 if (!$scope.patron.profile) return;
1723 var seconds = egCore.date.intervalToSeconds(
1724 $scope.patron.profile.perm_interval());
1725 var now_epoch = new Date().getTime();
1726 $scope.patron.expire_date = new Date(
1727 now_epoch + (seconds * 1000 /* milliseconds */))
1730 // grp is the pgt object
1731 $scope.set_profile = function(grp) {
1732 $scope.patron.profile = grp;
1733 $scope.set_expire_date();
1734 $scope.field_modified();
1737 $scope.invalid_profile = function() {
1740 $scope.patron.profile &&
1741 $scope.patron.profile.usergroup() == 't'
1745 $scope.new_address = function() {
1746 var addr = egCore.idl.toHash(new egCore.idl.aua());
1747 patronRegSvc.ingest_address($scope.patron, addr);
1748 addr.id = patronRegSvc.virt_id--;
1751 addr.within_city_limits = true;
1752 addr.country = $scope.org_settings['ui.patron.default_country'];
1753 $scope.patron.addresses.push(addr);
1756 // keep deleted addresses out of the patron object so
1757 // they won't appear in the UI. They'll be re-inserted
1758 // when the patron is updated.
1759 deleted_addresses = [];
1760 $scope.delete_address = function(id) {
1762 if ($scope.patron.isnew &&
1763 $scope.patron.addresses.length == 1 &&
1764 $scope.org_settings['ui.patron.registration.require_address']) {
1765 egAlertDialog.open(egCore.strings.REG_ADDR_REQUIRED);
1770 angular.forEach($scope.patron.addresses, function(addr) {
1771 if (addr.id == id) {
1773 addr.isdeleted = true;
1774 deleted_addresses.push(addr);
1777 addresses.push(addr);
1780 $scope.patron.addresses = addresses;
1783 $scope.post_code_changed = function(addr) {
1784 if ($scope.offline) return;
1786 'open-ils.search', 'open-ils.search.zip', addr.post_code)
1787 .then(function(resp) {
1789 if (resp.city) addr.city = resp.city;
1790 if (resp.state) addr.state = resp.state;
1791 if (resp.county) addr.county = resp.county;
1792 if (resp.alert) alert(resp.alert);
1796 $scope.replace_card = function() {
1797 $scope.patron.card.active = false;
1798 $scope.patron.card.ischanged = true;
1799 $scope.disable_bc = false;
1801 var new_card = egCore.idl.toHash(new egCore.idl.ac());
1802 new_card.id = patronRegSvc.virt_id--;
1803 new_card.isnew = true;
1804 new_card.active = true;
1805 new_card._primary = 'on';
1806 $scope.patron.card = new_card;
1807 $scope.patron.cards.push(new_card);
1810 $scope.day_phone_changed = function(phone) {
1811 if (phone && $scope.patron.isnew &&
1812 $scope.org_settings['patron.password.use_phone']) {
1813 $scope.patron.passwd = phone.substr(-4);
1817 $scope.barcode_changed = function(bc) {
1819 if (!$scope.patron.usrname)
1820 $scope.patron.usrname = bc;
1823 $scope.cards_dialog = function() {
1825 templateUrl: './circ/patron/t_patron_cards_dialog',
1828 ['$scope','$uibModalInstance','cards','perms',
1829 function($scope , $uibModalInstance , cards , perms) {
1830 // scope here is the modal-level scope
1831 $scope.args = {cards : cards};
1832 $scope.perms = perms;
1833 $scope.ok = function() { $uibModalInstance.close($scope.args) }
1834 $scope.cancel = function () { $uibModalInstance.dismiss() }
1837 cards : function() {
1838 // scope here is the controller-level scope
1839 return $scope.patron.cards;
1841 perms : function() {
1842 return $scope.perms;
1847 angular.forEach(args.cards, function(card) {
1848 card.ischanged = true; // assume cards need updating, OK?
1849 if (card._primary == 'on' &&
1850 card.id != $scope.patron.card.id) {
1851 $scope.patron.card = card;
1858 $scope.set_addr_type = function(addr, type) {
1859 var addrs = $scope.patron.addresses;
1860 if (addr['_is_'+type]) {
1861 angular.forEach(addrs, function(a) {
1862 if (a.id != addr.id) a['_is_'+type] = false;
1865 // unchecking mailing/billing means we have to randomly
1866 // select another address to fill that role. Select the
1867 // first address in the list (that does not match the
1869 for (var i = 0; i < addrs.length; i++) {
1870 if (addrs[i].id != addr.id) {
1871 addrs[i]['_is_' + type] = true;
1879 // Translate hold notify preferences from the form/scope back into a
1880 // single user setting value for opac.hold_notify.
1881 function compress_hold_notify() {
1882 var hold_notify = '';
1884 if ($scope.hold_notify_phone) {
1885 hold_notify = 'phone';
1888 if ($scope.hold_notify_email) {
1889 hold_notify = splitter + 'email';
1892 if ($scope.hold_notify_sms) {
1893 hold_notify = splitter + 'sms';
1896 $scope.user_settings['opac.hold_notify'] = hold_notify;
1899 // dialog for selecting additional permission groups
1900 $scope.secondary_groups_dialog = function() {
1902 templateUrl: './circ/patron/t_patron_groups_dialog',
1905 ['$scope','$uibModalInstance','linked_groups','pgt_depth',
1906 function($scope , $uibModalInstance , linked_groups , pgt_depth) {
1908 $scope.pgt_depth = pgt_depth;
1910 linked_groups : linked_groups,
1911 edit_profiles : patronRegSvc.edit_profiles,
1912 new_profile : patronRegSvc.edit_profiles[0]
1915 // add a new group to the linked groups list
1916 $scope.link_group = function($event, grp) {
1917 var found = false; // avoid duplicates
1918 angular.forEach($scope.args.linked_groups,
1919 function(g) {if (g.id() == grp.id()) found = true});
1920 if (!found) $scope.args.linked_groups.push(grp);
1921 $event.preventDefault(); // avoid close
1924 // remove a group from the linked groups list
1925 $scope.unlink_group = function($event, grp) {
1926 $scope.args.linked_groups =
1927 $scope.args.linked_groups.filter(function(g) {
1928 return g.id() != grp.id()
1930 $event.preventDefault(); // avoid close
1933 $scope.ok = function() { $uibModalInstance.close($scope.args) }
1934 $scope.cancel = function () { $uibModalInstance.dismiss() }
1937 linked_groups : function() { return $scope.patron.groups },
1938 pgt_depth : function() { return $scope.pgt_depth }
1943 if ($scope.patron.isnew) {
1944 // groups must be linked for new patrons after the
1945 // patron is created.
1946 $scope.patron.groups = args.linked_groups;
1950 // update links groups for existing users in real time.
1951 var ids = args.linked_groups.map(function(g) {return g.id()});
1952 patronRegSvc.apply_secondary_groups($scope.patron.id, ids)
1953 .then(function(success) {
1955 $scope.patron.groups = args.linked_groups;
1961 function extract_hold_notify() {
1962 notify = $scope.user_settings['opac.hold_notify'];
1963 if (!notify) return;
1964 $scope.hold_notify_phone = Boolean(notify.match(/phone/));
1965 $scope.hold_notify_email = Boolean(notify.match(/email/));
1966 $scope.hold_notify_sms = Boolean(notify.match(/sms/));
1969 $scope.invalidate_field = function(field) {
1970 patronRegSvc.invalidate_field($scope.patron, field);
1973 address_alert = function(addr) {
1974 if ($scope.offline) return;
1976 street1: addr.street1,
1977 street2: addr.street2,
1980 county: addr.county,
1981 country: addr.country,
1982 post_code: addr.post_code,
1983 mailing_address: addr._is_mailing,
1984 billing_address: addr._is_billing
1989 'open-ils.actor.address_alert.test',
1990 egCore.auth.token(), egCore.auth.user().ws_ou(), args
1991 ).then(function(res) {
1992 $scope.address_alerts = res;
1996 $scope.dupe_value_changed = function(type, value) {
1997 $scope.dupe_counts[type] = 0;
1998 patronRegSvc.dupe_patron_search($scope.patron, type, value)
1999 .then(function(res) {
2000 $scope.dupe_counts[type] = res.count;
2002 $scope.dupe_search_encoded =
2003 encodeURIComponent(js2JSON(res.search));
2005 $scope.dupe_search_encoded = '';
2010 // Dummy function in offline mode
2011 $scope.handle_home_org_changed = function() {}
2013 // This is called with every character typed in a form field,
2014 // since that's the only way to gaurantee something has changed.
2015 // See handle_field_changed for ng-change vs. ng-blur.
2016 $scope.field_modified = function() {
2017 // Call attach with every field change, regardless of whether
2018 // it's been called before. This will allow for re-attach after
2019 // the user clicks through the unload warning. egUnloadPrompt
2020 // will ensure we only attach once.
2021 egUnloadPrompt.attach($rootScope);
2024 // also monitor when form is changed *by the user*, as using
2025 // an ng-change handler doesn't work with eg-date-input
2026 $scope.$watch('reg_form.$pristine', function(newVal, oldVal) {
2027 if (!newVal) egUnloadPrompt.attach($rootScope);
2030 // username regex (if present) must be removed any time
2031 // the username matches the barcode to avoid firing the
2032 // invalid field handlers.
2033 function apply_username_regex() {
2034 var regex = $scope.org_settings['opac.username_regex'];
2036 if ($scope.patron.card.barcode) {
2037 // username must match the regex or the barcode
2038 field_patterns.au.usrname =
2040 regex + '|^' + $scope.patron.card.barcode + '$');
2042 // username must match the regex
2043 field_patterns.au.usrname = new RegExp(regex);
2046 // username can be any format.
2047 field_patterns.au.usrname = new RegExp('.*');
2051 // obj could be the patron, an address, etc.
2052 // This is called any time a form field achieves then loses focus.
2053 // It does not necessarily mean the field has changed.
2054 // The alternative is ng-change, but it's called with each character
2055 // typed, which would be overkill for many of the actions called here.
2056 $scope.handle_field_changed = function(obj, field_name) {
2059 var cls = obj.classname; // set by egIdl
2060 var value = obj[field_name];
2063 //console.log('changing field ' + field_name + ' to ' + value);
2065 switch (field_name) {
2067 if ($scope.patron.day_phone &&
2068 $scope.patron.isnew &&
2069 $scope.org_settings['patron.password.use_phone']) {
2070 $scope.patron.passwd = phone.substr(-4);
2075 apply_username_regex();
2076 $scope.barcode_changed(value);
2080 maintain_juvenile_flag();
2088 // patron.juvenile is set to true if the user was born after
2089 function maintain_juvenile_flag() {
2090 if ( !($scope.patron && $scope.patron.dob) ) return;
2093 $scope.org_settings['global.juvenile_age_threshold']
2096 var base = new Date();
2098 base.setTime(base.getTime() -
2099 Number(egCore.date.intervalToSeconds(juv_interval) + '000'));
2101 $scope.patron.juvenile = ($scope.patron.dob > base);
2104 // returns true (disable) for orgs that cannot have users.
2105 $scope.disable_home_org = function(org_id) {
2106 if (!org_id) return;
2107 var org = egCore.org.get(org_id);
2111 org.ou_type().can_have_users() == 'f'
2115 $scope.edit_passthru.self_edit_disallowed = function() {
2119 $scope.edit_passthru.group_edit_disallowed = function() {
2123 // Returns true if the Save and Save & Clone buttons should be disabled.
2124 $scope.edit_passthru.hide_save_actions = function() {
2128 // Returns true if any input elements are tagged as invalid
2129 // via Angular patterns or required attributes.
2130 function form_has_invalid_fields() {
2131 return $('#patron-reg-container .ng-invalid').length > 0;
2134 function form_is_incomplete() {
2136 $scope.dupe_username ||
2137 $scope.dupe_barcode ||
2138 form_has_invalid_fields()
2143 $scope.edit_passthru.save = function(save_args) {
2144 if (!save_args) save_args = {};
2146 if (form_is_incomplete()) {
2147 // User has not provided valid values for all required fields.
2148 return egAlertDialog.open(egCore.strings.REG_INVALID_FIELDS);
2151 // remove page unload warning prompt
2152 egUnloadPrompt.clear();
2154 // toss the deleted addresses back into the patron's list of
2155 // addresses so it's included in the update
2156 $scope.patron.addresses =
2157 $scope.patron.addresses.concat(deleted_addresses);
2159 compress_hold_notify();
2163 patronRegSvc.save_user($scope.patron)
2164 .then($scope.rs.save_offline_xacts)
2165 .then(function(new_user) {
2166 // reload the current page
2167 $window.location.href = location.href;