]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/ui.js
LP1615805 No inputs after submit in patron search (AngularJS)
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / ui.js
1 /**
2   * UI tools and directives.
3   */
4 angular.module('egUiMod', ['egCoreMod', 'ui.bootstrap'])
5
6
7 /**
8  * <input focus-me="iAmOpen"/>
9  * $scope.iAmOpen = true;
10  */
11 .directive('focusMe', 
12        ['$timeout','$parse', 
13 function($timeout , $parse) {
14     return {
15         link: function(scope, element, attrs) {
16             var model = $parse(attrs.focusMe);
17             scope.$watch(model, function(value) {
18                 if(value === true) 
19                     $timeout(function() {element[0].focus()});
20             });
21             element.bind('blur', function() {
22                 $timeout(function() {
23                     if (model.assign && typeof model.assign == 'function')
24                         scope.$apply(model.assign(scope, false));
25                 });
26             })
27         }
28     };
29 }])
30
31 /**
32  * <input blur-me="pleaseBlurMe"/>
33  * $scope.pleaseBlurMe = true
34  * Useful for de-focusing when no other obvious focus target exists
35  */
36 .directive('blurMe', 
37        ['$timeout','$parse', 
38 function($timeout , $parse) {
39     return {
40         link: function(scope, element, attrs) {
41             var model = $parse(attrs.blurMe);
42             scope.$watch(model, function(value) {
43                 if(value === true) 
44                     $timeout(function() {element[0].blur()});
45             });
46             element.bind('focus', function() {
47                 $timeout(function() {
48                     scope.$apply(model.assign(scope, false));
49                 });
50             })
51         }
52     };
53 }])
54
55
56 // <input select-me="iWantToBeSelected"/>
57 // $scope.iWantToBeSelected = true;
58 .directive('selectMe', 
59        ['$timeout','$parse', 
60 function($timeout , $parse) {
61     return {
62         link: function(scope, element, attrs) {
63             var model = $parse(attrs.selectMe);
64             scope.$watch(model, function(value) {
65                 if(value === true) 
66                     $timeout(function() {element[0].select()});
67             });
68             element.bind('blur', function() {
69                 $timeout(function() {
70                     scope.$apply(model.assign(scope, false));
71                 });
72             })
73         }
74     };
75 }])
76
77 // <select int-to-str ><option value="1">Value</option></select>
78 // use integer models for string values
79 .directive('intToStr', function() {
80     return {
81         restrict: 'A',
82         require: 'ngModel',
83         link: function(scope, element, attrs, ngModel) {
84             ngModel.$parsers.push(function(value) {
85                 return parseInt(value);
86             });
87             ngModel.$formatters.push(function(value) {
88                 return '' + value;
89             });
90         }
91     };
92 })
93
94 // <input str-to-int value="10"/>
95 .directive('strToInt', function() {
96     return {
97         restrict: 'A',
98         require: 'ngModel',
99         link: function(scope, element, attrs, ngModel) {
100             ngModel.$parsers.push(function(value) {
101                 return '' + value;
102             });
103             ngModel.$formatters.push(function(value) {
104                 return parseInt(value);
105             });
106         }
107     };
108 })
109
110 // <input float-to-str
111 .directive('floatToStr', function() {
112     return {
113         restrict: 'A',
114         require: 'ngModel',
115         link: function(scope, element, attrs, ngModel) {
116             ngModel.$parsers.push(function(value) {
117                 return parseFloat(value);
118             });
119             ngModel.$formatters.push(function(value) {
120                 return '' + value;
121             });
122         }
123     };
124 })
125
126 .directive('strToFloat', function() {
127     return {
128         restrict: 'A',
129         require: 'ngModel',
130         link: function(scope, element, attrs, ngModel) {
131             ngModel.$parsers.push(function(value) {
132                 return '' + value;
133             });
134             ngModel.$formatters.push(function(value) {
135                 return parseFloat(value);
136             });
137         }
138     };
139 })
140
141 // 'reverse' filter 
142 // <div ng-repeat="item in items | reverse">{{item.name}}</div>
143 // http://stackoverflow.com/questions/15266671/angular-ng-repeat-in-reverse
144 // TODO: perhaps this should live elsewhere
145 .filter('reverse', function() {
146     return function(items) {
147         return items.slice().reverse();
148     };
149 })
150
151 // 'date' filter
152 // Overriding the core angular date filter with a moment-js based one for
153 // better timezone and formatting support.
154 .filter('date',function() {
155
156     var formatMap = {
157         short  : 'l LT',
158         medium : 'lll',
159         long   : 'LLL',
160         full   : 'LLLL',
161
162         shortDate  : 'l',
163         mediumDate : 'll',
164         longDate   : 'LL',
165         fullDate   : 'LL',
166
167         shortTime  : 'LT',
168         mediumTime : 'LTS'
169     };
170
171     var formatReplace = [
172         [ /yyyy/g, 'YYYY' ],
173         [ /yy/g,   'YY'   ],
174         [ /y/g,    'Y'    ],
175         [ /ww/g,   'WW'   ],
176         [ /w/g,    'W'    ],
177         [ /dd/g,   'DD'   ],
178         [ /d/g,    'D'    ],
179         [ /sss/g,  'SSS'  ],
180         [ /EEEE/g, 'dddd' ],
181         [ /EEE/g,  'ddd'  ],
182         [ /Z/g,    'ZZ'   ]
183     ];
184
185     return function (date, format, tz) {
186         if (!date) return '';
187
188         if (date == 'now') 
189             date = new Date().toISOString();
190
191         if (format) {
192             var fmt = formatMap[format] || format;
193             angular.forEach(formatReplace, function (r) {
194                 fmt = fmt.replace(r[0],r[1]);
195             });
196         }
197
198         var d = moment(date);
199         if (tz && tz !== '-') d.tz(tz);
200
201         return d.isValid() ? d.format(fmt) : '';
202     }
203
204 })
205
206 // 'egOrgDate' filter
207 // Uses moment.js and moment-timezone.js to put dates into the most appropriate
208 // timezone for a given (optional) org unit based on its lib.timezone setting
209 .filter('egOrgDate',['$filter','egCore',
210              function($filter , egCore) {
211
212     var tzcache = {};
213
214     function eg_date_filter (date, fmt, ouID) {
215         if (ouID) {
216             if (angular.isObject(ouID)) {
217                 if (angular.isFunction(ouID.id)) {
218                     ouID = ouID.id();
219                 } else {
220                     ouID = ouID.id;
221                 }
222             }
223     
224             if (!tzcache[ouID]) {
225                 tzcache[ouID] = '-';
226                 egCore.org.settings('lib.timezone', ouID)
227                 .then(function(s) {
228                     tzcache[ouID] = s['lib.timezone'] || OpenSRF.tz;
229                 });
230             }
231         }
232
233         return $filter('date')(date, fmt, tzcache[ouID]);
234     }
235
236     eg_date_filter.$stateful = true;
237
238     return eg_date_filter;
239 }])
240
241 // 'egOrgDateInContext' filter
242 // Uses the egOrgDate filter to make time and date location aware, and further
243 // modifies the format if one of [short, medium, long, full] to show only the
244 // date if the optional interval parameter is day-granular.  This is
245 // particularly useful for due dates on circulations.
246 .filter('egOrgDateInContext',['$filter','egCore',
247                       function($filter , egCore) {
248
249     function eg_context_date_filter (date, format, orgID, interval) {
250         var fmt = format;
251         if (!fmt) fmt = 'short';
252
253         // if this is a simple, one-word format, and it doesn't say "Date" in it...
254         if (['short','medium','long','full'].filter(function(x){return fmt == x}).length > 0 && interval) {
255             var secs = egCore.date.intervalToSeconds(interval);
256             if (secs !== null && secs % 86400 == 0) fmt += 'Date';
257         }
258
259         return $filter('egOrgDate')(date, fmt, orgID);
260     }
261
262     eg_context_date_filter.$stateful = true;
263
264     return eg_context_date_filter;
265 }])
266
267 // 'egDueDate' filter
268 // Uses the egOrgDateInContext filter to make time and date location aware, but
269 // only if the supplied interval is day-granular.  This is as wrapper for
270 // egOrgDateInContext to be used for circulation due date /only/.
271 .filter('egDueDate',['$filter','egCore',
272                       function($filter , egCore) {
273
274     function eg_context_due_date_filter (date, format, orgID, interval) {
275         if (interval) {
276             var secs = egCore.date.intervalToSeconds(interval);
277             if (secs === null || secs % 86400 != 0) {
278                 orgID = null;
279                 interval = null;
280             }
281         }
282         return $filter('egOrgDateInContext')(date, format, orgID, interval);
283     }
284
285     eg_context_due_date_filter.$stateful = true;
286
287     return eg_context_due_date_filter;
288 }])
289
290 // 'join' filter
291 // TODO: perhaps this should live elsewhere
292 .filter('join', function() {
293     return function(arr,sep) {
294         if (typeof arr == 'object' && arr.constructor == Array) {
295             return arr.join(sep || ',');
296         } else {
297             return '';
298         }
299     };
300 })
301
302 /**
303  * Progress Dialog. 
304  *
305  * egProgressDialog.open();
306  * egProgressDialog.open({value : 0});
307  * egProgressDialog.open({value : 0, max : 123});
308  * egProgressDialog.increment();
309  * egProgressDialog.increment();
310  * egProgressDialog.close();
311  *
312  * Each dialog has 2 numbers, 'max' and 'value'.
313  * The content of these values determines how the dialog displays.  
314  *
315  * There are 3 flavors:
316  *
317  * -- value is set, max is set
318  * determinate: shows a progression with a percent complete.
319  *
320  * -- value is set, max is unset
321  * semi-determinate, with a value report.  Shows a value-less
322  * <progress/>, but shows the value as a number in the dialog.
323  *
324  * This is useful in cases where the total number of items to retrieve
325  * from the server is unknown, but we know how many items we've
326  * retrieved thus far.  It helps to reinforce that something specific
327  * is happening, but we don't know when it will end.
328  *
329  * -- value is unset
330  * indeterminate: shows a generic value-less <progress/> with no 
331  * clear indication of progress.
332  *
333  * Only 1 egProgressDialog instance will be activate at a time.
334  * Each invocation of .open() destroys any existing instance.
335  */
336
337 /* Simple storage class for egProgressDialog data maintenance.
338  * This data lives outside of egProgressDialog so it can be 
339  * directly imported into egProgressDialog's $uibModalInstance.
340  */
341 .factory('egProgressData', [
342     function() {
343         var service = {}; // max/value initially unset
344
345         service.reset = function() {
346             delete service.max;
347             delete service.value;
348         }
349
350         service.hasvalue = function() {
351             return Number.isInteger(service.value);
352         }
353
354         service.hasmax = function() {
355             return Number.isInteger(service.max);
356         }
357
358         service.percent = function() {
359             if (service.hasvalue()  && 
360                 service.hasmax()    && 
361                 service.max > 0     &&
362                 service.value <= service.max)
363                 return Math.floor((service.value / service.max) * 100);
364             return 100;
365         }
366
367         return service;
368     }
369 ])
370
371 .factory('egProgressDialog', [
372             'egProgressData','$uibModal', 
373     function(egProgressData , $uibModal) {
374     var service = {};
375
376     service.open = function(args) {
377         return $uibModal.open({
378             templateUrl: './share/t_progress_dialog',
379             /* backdrop: 'static', */ /* allow 'cancelling' of progress dialog */
380             controller: ['$scope','$uibModalInstance','egProgressData',
381                 function( $scope , $uibModalInstance , egProgressData) {
382                     // Once the new modal instance is available, force-
383                     // kill any other instances
384                     service.close(true); 
385
386                     // Reset to an indeterminate progress bar, 
387                     // overlay with caller values.
388                     egProgressData.reset();
389                     service.update(angular.extend({}, args));
390
391                     service.currentInstance = $uibModalInstance;
392                     $scope.data = egProgressData; // tiny service
393                 }
394             ]
395         });
396     };
397
398     service.close = function(warn) {
399         if (service.currentInstance) {
400             if (warn) {
401                 console.warn("egProgressDialog replacing existing instance. "
402                     + "Only one may be open at a time.");
403             }
404             service.currentInstance.close();
405             delete service.currentInstance;
406         }
407     }
408
409     // Set the current state of the progress bar.
410     service.update = function(args) {
411         if (args.max != undefined) 
412             egProgressData.max = args.max;
413         if (args.value != undefined) 
414             egProgressData.value = args.value;
415         if (args.label != undefined) 
416             egProgressData.label = args.label;
417     }
418
419     // Increment the current value.  If no amount is specified,
420     // it increments by 1.  Calling increment() on an indetermite
421     // progress bar will force it to be a (semi-)determinate bar.
422     service.increment = function(amt) {
423         if (!Number.isInteger(amt)) amt = 1;
424
425         if (!egProgressData.hasvalue())
426             egProgressData.value = 0;
427
428         egProgressData.value += amt;
429     }
430
431     return service;
432 }])
433
434 /**
435  * egAlertDialog.open({message : 'hello {{name}}'}).result.then(
436  *     function() { console.log('alert closed') });
437  */
438 .factory('egAlertDialog', 
439
440         ['$uibModal','$interpolate',
441 function($uibModal , $interpolate) {
442     var service = {};
443
444     service.open = function(message, msg_scope) {
445         return $uibModal.open({
446             templateUrl: './share/t_alert_dialog',
447             backdrop: 'static',
448             controller: ['$scope', '$uibModalInstance',
449                 function($scope, $uibModalInstance) {
450                     $scope.message = $interpolate(message)(msg_scope);
451                     $scope.ok = function() {
452                         if (msg_scope && msg_scope.ok) msg_scope.ok();
453                         $uibModalInstance.close()
454                     }
455                 }
456             ]
457         });
458     }
459
460     return service;
461 }])
462
463 /**
464  * egConfirmDialog.open("some message goes {{here}}", {
465  *  here : 'foo', ok : function() {}, cancel : function() {}},
466  *  'OK', 'Cancel');
467  */
468 .factory('egConfirmDialog', 
469     
470        ['$uibModal','$interpolate',
471 function($uibModal, $interpolate) {
472     var service = {};
473
474     service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
475         msg_scope = msg_scope || {};
476         return $uibModal.open({
477             templateUrl: './share/t_confirm_dialog',
478             backdrop: 'static',
479             controller: ['$scope', '$uibModalInstance',
480                 function($scope, $uibModalInstance) {
481                     $scope.title = $interpolate(title)(msg_scope);
482                     $scope.message = $interpolate(message)(msg_scope);
483                     $scope.ok_button_label = $interpolate(ok_button_label || '')(msg_scope);
484                     $scope.cancel_button_label = $interpolate(cancel_button_label || '')(msg_scope);
485                     $scope.ok = function() {
486                         if (msg_scope.ok) msg_scope.ok();
487                         $uibModalInstance.close()
488                     }
489                     $scope.cancel = function() {
490                         if (msg_scope.cancel) msg_scope.cancel();
491                         $uibModalInstance.dismiss();
492                     }
493                 }
494             ]
495         })
496     }
497
498     return service;
499 }])
500
501 /**
502  * egPromptDialog.open(
503  *    "prompt message goes {{here}}", 
504  *    promptValue,  // optional
505  *    {
506  *      here : 'foo',  
507  *      ok : function(value) {console.log(value)}, 
508  *      cancel : function() {console.log('prompt denied')}
509  *    }
510  *  );
511  */
512 .factory('egPromptDialog', 
513     
514        ['$uibModal','$interpolate',
515 function($uibModal, $interpolate) {
516     var service = {};
517
518     service.open = function(message, promptValue, msg_scope) {
519         return $uibModal.open({
520             templateUrl: './share/t_prompt_dialog',
521             backdrop: 'static',
522             controller: ['$scope', '$uibModalInstance',
523                 function($scope, $uibModalInstance) {
524                     $scope.message = $interpolate(message)(msg_scope);
525                     $scope.args = {value : promptValue || ''};
526                     $scope.focus = true;
527                     $scope.ok = function() {
528                         if (msg_scope && msg_scope.ok) msg_scope.ok($scope.args.value);
529                         $uibModalInstance.close($scope.args);
530                     }
531                     $scope.cancel = function() {
532                         if (msg_scope && msg_scope.cancel) msg_scope.cancel();
533                         $uibModalInstance.dismiss();
534                     }
535                 }
536             ]
537         })
538     }
539
540     return service;
541 }])
542
543 /**
544  * egSelectDialog.open(
545  *    "message goes {{here}}", 
546  *    list,           // ['values','for','dropdown'],
547  *    selectedValue,  // optional
548  *    {
549  *      here : 'foo',
550  *      ok : function(value) {console.log(value)}, 
551  *      cancel : function() {console.log('prompt denied')}
552  *    }
553  *  );
554  */
555 .factory('egSelectDialog', 
556     
557        ['$uibModal','$interpolate',
558 function($uibModal, $interpolate) {
559     var service = {};
560
561     service.open = function(message, inputList, selectedValue, msg_scope) {
562         return $uibModal.open({
563             templateUrl: './share/t_select_dialog',
564             backdrop: 'static',
565             controller: ['$scope', '$uibModalInstance',
566                 function($scope, $uibModalInstance) {
567                     $scope.message = $interpolate(message)(msg_scope);
568                     $scope.args = {
569                         list  : inputList,
570                         value : selectedValue
571                     };
572                     $scope.focus = true;
573                     $scope.ok = function() {
574                         if (msg_scope.ok) msg_scope.ok($scope.args.value);
575                         $uibModalInstance.close()
576                     }
577                     $scope.cancel = function() {
578                         if (msg_scope.cancel) msg_scope.cancel();
579                         $uibModalInstance.dismiss();
580                     }
581                 }
582             ]
583         })
584     }
585
586     return service;
587 }])
588
589 /**
590  * Warn on page unload and give the user a chance to avoid navigating
591  * away from the current page.  
592  * Only one handler is supported per page.
593  * NOTE: we can't use an egUnloadDialog as the dialog builder, because
594  * it renders asynchronously, which allows the page to redirect before
595  * the dialog appears.
596  */
597 .factory('egUnloadPrompt', [
598         '$window','egStrings', 
599 function($window , egStrings) {
600     var service = {attached : false};
601
602     // attach a page/scope unload prompt
603     service.attach = function($scope, msg) {
604         if (service.attached) return;
605         service.attached = true;
606
607         // handle page change
608         $($window).on('beforeunload', function() { 
609             service.clear();
610             return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
611         });
612
613         if (!$scope) return;
614
615         // If a scope was provided, attach a scope-change handler,
616         // similar to the page-page prompt.
617         service.locChangeCancel = 
618             $scope.$on('$locationChangeStart', function(evt, next, current) {
619             if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
620                 // user allowed the page to change.  
621                 // Clear the unload handler.
622                 service.clear();
623             } else {
624                 evt.preventDefault();
625             }
626         });
627     };
628
629     // remove the page unload prompt
630     service.clear = function() {
631         $($window).off('beforeunload');
632         if (service.locChangeCancel)
633             service.locChangeCancel();
634         service.attached = false;
635     }
636
637     return service;
638 }])
639
640 /**
641  * egAddCopyAlertDialog - manage copy alerts
642  */
643 .factory('egAddCopyAlertDialog', 
644        ['$uibModal','$interpolate','egCore',
645 function($uibModal , $interpolate , egCore) {
646     var service = {};
647
648     service.open = function(args) {
649         return $uibModal.open({
650             templateUrl: './share/t_add_copy_alert_dialog',
651             controller: ['$scope','$q','$uibModalInstance',
652                 function( $scope , $q , $uibModalInstance) {
653
654                     $scope.copy_ids = args.copy_ids;
655                     egCore.pcrud.search('ccat',
656                         { active : 't' },
657                         {},
658                         { atomic : true }
659                     ).then(function (ccat) {
660                         $scope.alert_types = ccat;
661                     }); 
662
663                     $scope.copy_alert = {
664                         create_staff : egCore.auth.user().id(),
665                         note         : '',
666                         temp         : false
667                     };
668
669                     $scope.ok = function(copy_alert) {
670                         if (typeof(copy_alert.note) != 'undefined' &&
671                             copy_alert.note != '') {
672                             copy_alerts = [];
673                             angular.forEach($scope.copy_ids, function (cp_id) {
674                                 var a = new egCore.idl.aca();
675                                 a.isnew(1);
676                                 a.create_staff(copy_alert.create_staff);
677                                 a.note(copy_alert.note);
678                                 a.temp(copy_alert.temp ? 't' : 'f');
679                                 a.copy(cp_id);
680                                 a.ack_time(null);
681                                 a.alert_type(
682                                     $scope.alert_types.filter(function(at) {
683                                         return at.id() == copy_alert.alert_type;
684                                     })[0]
685                                 );
686                                 copy_alerts.push( a );
687                             });
688                             if (copy_alerts.length > 0) {
689                                 egCore.pcrud.apply(copy_alerts).finally(function() {
690                                     if (args.ok) args.ok();
691                                     $uibModalInstance.close()
692                                 });
693                             }
694                         } else {
695                             if (args.ok) args.ok();
696                             $uibModalInstance.close()
697                         }
698                     }
699                     $scope.cancel = function() {
700                         if (args.cancel) args.cancel();
701                         $uibModalInstance.dismiss();
702                     }
703                 }
704             ]
705         })
706     }
707
708     return service;
709 }])
710
711 /**
712  * egCopyAlertManagerDialog - manage copy alerts
713  */
714 .factory('egCopyAlertManagerDialog', 
715        ['$uibModal','$interpolate','egCore',
716 function($uibModal , $interpolate , egCore) {
717     var service = {};
718
719     service.get_user_copy_alerts = function(copy_id) {
720         return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
721             { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
722             { atomic : true }
723         );
724     }
725
726     service.open = function(args) {
727         return $uibModal.open({
728             templateUrl: './share/t_copy_alert_manager_dialog',
729             controller: ['$scope','$q','$uibModalInstance',
730                 function( $scope , $q , $uibModalInstance) {
731
732                     function init(args) {
733                         var defer = $q.defer();
734                         if (args.copy_id) {
735                             service.get_user_copy_alerts(args.copy_id).then(function(aca) {
736                                 defer.resolve(aca);
737                             });
738                         } else {
739                             defer.resolve(args.alerts);
740                         }
741                         return defer.promise;
742                     }
743
744                     // returns a promise resolved with the list of circ statuses
745                     $scope.get_copy_statuses = function() {
746                         if (egCore.env.ccs)
747                             return $q.when(egCore.env.ccs.list);
748
749                         return egCore.pcrud.retrieveAll('ccs', null, {atomic : true})
750                         .then(function(list) {
751                             egCore.env.absorbList(list, 'ccs');
752                             return list;
753                         });
754                     };
755
756                     $scope.mode = args.mode || 'checkin';
757
758                     var next_statuses = [];
759                     var seen_statuses = {};
760                     $scope.next_statuses = [];
761                     $scope.params = {
762                         'the_next_status' : null
763                     }
764                     init(args).then(function(copy_alerts) {
765                         $scope.alerts = copy_alerts;
766                         angular.forEach($scope.alerts, function(copy_alert) {
767                             var state = copy_alert.alert_type().state();
768                             copy_alert.evt = copy_alert.alert_type().event();
769
770                             copy_alert.message = copy_alert.note() ||
771                                 egCore.strings.ON_DEMAND_COPY_ALERT[copy_alert.evt][state];
772
773                             if (copy_alert.temp() == 't') {
774                                 angular.forEach(copy_alert.alert_type().next_status(), function (st) {
775                                     if (!seen_statuses[st]) {
776                                         seen_statuses[st] = true;
777                                         next_statuses.push(st);
778                                     }
779                                 });
780                             }
781                         });
782                         if ($scope.mode == 'checkin' && next_statuses.length > 0) {
783                             $scope.get_copy_statuses().then(function() {
784                                 angular.forEach(next_statuses, function(st) {
785                                     if (egCore.env.ccs.map[st])
786                                         $scope.next_statuses.push(egCore.env.ccs.map[st]);
787                                 });
788                                 $scope.params.the_next_status = $scope.next_statuses[0].id();
789                             });
790                         }
791                     });
792
793                     $scope.isAcknowledged = function(copy_alert) {
794                         return (copy_alert.acked);
795                     };
796                     $scope.canBeAcknowledged = function(copy_alert) {
797                         return (!copy_alert.ack_time() && copy_alert.temp() == 't');
798                     };
799                     $scope.canBeRemoved = function(copy_alert) {
800                         return (!copy_alert.ack_time() && copy_alert.temp() == 'f');
801                     };
802
803                     $scope.ok = function() {
804                         var acks = [];
805                         angular.forEach($scope.alerts, function (copy_alert) {
806                             if (copy_alert.acked) {
807                                 copy_alert.ack_time('now');
808                                 copy_alert.ack_staff(egCore.auth.user().id());
809                                 copy_alert.ischanged(true);
810                                 acks.push(copy_alert);
811                             }
812                         });
813                         if (acks.length > 0) {
814                             egCore.pcrud.apply(acks).finally(function() {
815                                 if (args.ok) args.ok($scope.params.the_next_status);
816                                 $uibModalInstance.close()
817                             });
818                         } else {
819                             if (args.ok) args.ok($scope.params.the_next_status);
820                             $uibModalInstance.close()
821                         }
822                     }
823                     $scope.cancel = function() {
824                         if (args.cancel) args.cancel();
825                         $uibModalInstance.dismiss();
826                     }
827                 }
828             ]
829         })
830     }
831
832     return service;
833 }])
834
835 /**
836  * egCopyAlertEditorDialog - manage copy alerts
837  */
838 .factory('egCopyAlertEditorDialog', 
839        ['$uibModal','$interpolate','egCore',
840 function($uibModal , $interpolate , egCore) {
841     var service = {};
842
843     service.get_user_copy_alerts = function(copy_id) {
844         return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
845             { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
846             { atomic : true }
847         );
848     }
849
850     service.get_copy_alert_types = function() {
851         return egCore.pcrud.search('ccat',
852             { active : 't' },
853             {},
854             { atomic : true }
855         );
856     };
857
858     service.open = function(args) {
859         return $uibModal.open({
860             templateUrl: './share/t_copy_alert_editor_dialog',
861             controller: ['$scope','$q','$uibModalInstance',
862                 function( $scope , $q , $uibModalInstance) {
863
864                     function init(args) {
865                         var defer = $q.defer();
866                         if (args.copy_id) {
867                             service.get_user_copy_alerts(args.copy_id).then(function(aca) {
868                                 defer.resolve(aca);
869                             });
870                         } else {
871                             defer.resolve(args.alerts);
872                         }
873                         return defer.promise;
874                     }
875
876                     init(args).then(function(copy_alerts) {
877                         $scope.copy_alert_list = copy_alerts;
878                     });
879                     service.get_copy_alert_types().then(function(ccat) {
880                         $scope.alert_types = ccat;
881                     });
882
883                     $scope.ok = function() {
884                         egCore.pcrud.apply($scope.copy_alert_list).finally(function() {
885                             $uibModalInstance.close();
886                         });
887                     }
888                     $scope.cancel = function() {
889                         if (args.cancel) args.cancel();
890                         $uibModalInstance.dismiss();
891                     }
892                 }
893             ]
894         })
895     }
896
897     return service;
898 }])
899 .directive('aDisabled', function() {
900     return {
901         restrict : 'A',
902         compile: function(tElement, tAttrs, transclude) {
903             //Disable ngClick
904             tAttrs["ngClick"] = ("ng-click", "!("+tAttrs["aDisabled"]+") && ("+tAttrs["ngClick"]+")");
905
906             //Toggle "disabled" to class when aDisabled becomes true
907             return function (scope, iElement, iAttrs) {
908                 scope.$watch(iAttrs["aDisabled"], function(newValue) {
909                     if (newValue !== undefined) {
910                         iElement.toggleClass("disabled", newValue);
911                     }
912                 });
913
914                 //Disable href on click
915                 iElement.on("click", function(e) {
916                     if (scope.$eval(iAttrs["aDisabled"])) {
917                         e.preventDefault();
918                     }
919                 });
920             };
921         }
922     };
923 })
924
925 .directive('egBasicComboBox', function() {
926     return {
927         restrict: 'E',
928         replace: true,
929         scope: {
930             list: "=", // list of strings
931             selected: "=",
932             onSelect: "=",
933             egDisabled: "=",
934             allowAll: "@",
935             placeholder: "@",
936             focusMe: "=?"
937         },
938         template:
939             '<div class="input-group">'+
940                 '<input placeholder="{{placeholder}}" type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()" focus-me="focusMe" ng-click="inputClick()">'+
941                 '<div class="input-group-btn" uib-dropdown ng-class="{open:isopen}">'+
942                     '<button type="button" ng-click="showAll()" ng-disabled="egDisabled" class="btn btn-default" uib-dropdown-toggle><span class="caret"></span></button>'+
943                     '<ul uib-dropdown-menu class="dropdown-menu-right">'+
944                         '<li ng-repeat="item in list|filter:selected:compare"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
945                         '<li ng-if="complete_list" class="divider"><span></span></li>'+
946                         '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
947                     '</ul>'+
948                 '</div>'+
949             '</div>',
950         controller: ['$scope','$filter',
951             function( $scope , $filter) {
952
953                 $scope.complete_list = false;
954                 $scope.isopen = false;
955                 $scope.clickedopen = false;
956                 $scope.clickedclosed = null;
957
958                 $scope.compare = function (ex, act) {
959                     if (act === null || act === undefined) return true;
960                     if (act.toString) {
961                         act = act.toString();
962                         act = act.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); // modify string to make sure characters like [ are accepted in the regex
963                     }
964                     return new RegExp(act.toLowerCase()).test(ex.toLowerCase());
965                 }
966
967                 $scope.showAll = function () {
968
969                     $scope.clickedopen = !$scope.clickedopen;
970
971                     if ($scope.clickedclosed === null) {
972                         if (!$scope.clickedopen) {
973                             $scope.clickedclosed = true;
974                         }
975                     } else {
976                         $scope.clickedclosed = !$scope.clickedopen;
977                     }
978
979                     if ($scope.selected && $scope.selected.length > 0) $scope.complete_list = true;
980                     if (!$scope.selected || $scope.selected.length == 0) $scope.complete_list = false;
981                     $scope.makeOpen();
982                 }
983
984                 $scope.inputClick = function() {
985                     if ($scope.isopen) {
986                         $scope.isopen = false;
987                         $scope.clickedclosed = null;
988                     }
989                 }
990
991                 $scope.makeOpen = function () {
992                     $scope.isopen = $scope.clickedopen || ($filter('filter')(
993                         $scope.list,
994                         $scope.selected
995                     ).length > 0 && $scope.selected && $scope.selected.length > 0);
996                     if ($scope.clickedclosed) {
997                         $scope.isopen = false;
998                         $scope.clickedclosed = null;
999                     }
1000                 }
1001
1002                 $scope.changeValue = function (newVal) {
1003                     $scope.selected = newVal;
1004                     $scope.isopen = false;
1005                     $scope.clickedclosed = null;
1006                     $scope.clickedopen = false;
1007                     if ($scope.selected.length == 0) $scope.complete_list = false;
1008                     if ($scope.onSelect) $scope.onSelect();
1009                 }
1010
1011             }
1012         ]
1013     };
1014 })
1015
1016 .directive('egListCounts', function() {
1017     return {
1018         restrict: 'E',
1019         replace: true,
1020         scope: {
1021             label: "@",
1022             list: "=", // list of things
1023             render: "=", // function to turn thing into string; default to stringification
1024             onSelect: "=" // function to fire when option selected. passed one copy of the selected value
1025         },
1026         templateUrl: './share/t_listcounts',
1027         controller: ['$scope','$timeout',
1028             function( $scope , $timeout ) {
1029
1030                 $scope.isopen = false;
1031                 $scope.count_hash = {};
1032
1033                 $scope.renderer = $scope.render ? $scope.render : function (x) { return ""+x };
1034
1035                 $scope.$watchCollection('list',function() {
1036                     $scope.count_hash = {};
1037                     angular.forEach($scope.list, function (item) {
1038                         var str = $scope.renderer(item);
1039                         if (!$scope.count_hash[str]) {
1040                             $scope.count_hash[str] = {
1041                                 count : 1,
1042                                 value : str,
1043                                 original : item
1044                             };
1045                         } else {
1046                             $scope.count_hash[str].count++;
1047                         }
1048                     });
1049                 });
1050
1051                 $scope.selectValue = function (item) {
1052                     if ($scope.onSelect) $scope.onSelect(item);
1053                 }
1054
1055             }
1056         ]
1057     };
1058 })
1059
1060 /**
1061  * Nested org unit selector modeled as a Bootstrap dropdown button.
1062  */
1063 .directive('egOrgSelector', function() {
1064     return {
1065         restrict : 'AE',
1066         transclude : true,
1067         replace : true, // makes styling easier
1068         scope : {
1069             selected : '=', // defaults to workstation or root org,
1070                             // unless the nodefault attibute exists
1071
1072             // Each org unit is passed into this function and, for
1073             // any org units where the response value is true, the
1074             // org unit will not be added to the selector.
1075             hiddenTest : '=',
1076
1077             // Each org unit is passed into this function and, for
1078             // any org units where the response value is true, the
1079             // org unit will not be available for selection.
1080             disableTest : '=',
1081
1082             // if set to true, disable the UI element altogether
1083             alldisabled : '@',
1084
1085             // Caller can either $watch(selected, ..) or register an
1086             // onchange handler.
1087             onchange : '=',
1088
1089             // optional typeahead placeholder text
1090             label : '@',
1091
1092             // optional name of settings key for persisting
1093             // the last selected org unit
1094             stickySetting : '@'
1095         },
1096
1097         templateUrl : './share/t_org_select',
1098
1099         controller : ['$scope','$timeout','egCore','egStartup','$q',
1100               function($scope , $timeout , egCore , egStartup , $q) {
1101
1102             // See emptyTypeahead directive below.
1103             var secretEmptyKey = '_INTERNAL_';
1104
1105             function formatName(org) {
1106                 return " ".repeat(org.ou_type().depth()) + org.shortname();
1107             }
1108
1109             // avoid linking the full fleshed tree to the scope by 
1110             // tossing in a flattened list.
1111             // --
1112             // Run-time code referencing post-start data should be run
1113             // from within a startup block, otherwise accessing this
1114             // module before startup completes will lead to failure.
1115             //
1116             // controller() runs before link().
1117             // This post-startup code runs after link().
1118             egStartup.go(
1119             ).then(
1120                 function() {
1121                     return egCore.env.classLoaders.aou();
1122                 }
1123             ).then(
1124                 function() {
1125
1126                     $scope.selecteName = '';
1127
1128                     $scope.shortNames = egCore.org.list()
1129                     .filter(function(org) {
1130                         return !(
1131                             $scope.hiddenTest && 
1132                             $scope.hiddenTest(org.id())
1133                         );
1134                     }).map(function(org) {
1135                         return formatName(org);
1136                     });
1137     
1138                     // Apply default values
1139     
1140                     if ($scope.stickySetting) {
1141                         var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
1142                         if (orgId) {
1143                             var org = egCore.org.get(orgId);
1144                             if (org) {
1145                                 $scope.selected = org;
1146                                 $scope.selectedName = org.shortname();
1147                             }
1148                         }
1149                     }
1150     
1151                     if (!$scope.selected && !$scope.nodefault && egCore.auth.user()) {
1152                         var org = egCore.org.get(egCore.auth.user().ws_ou());
1153                         $scope.selected = org;
1154                         $scope.selectedName = org.shortname();
1155                     }
1156     
1157                     fire_orgsel_onchange(); // no-op if nothing is selected
1158                     watch_external_changes();
1159                 }
1160             );
1161
1162             /**
1163              * Fire onchange handler after a timeout, so the
1164              * $scope.selected value has a chance to propagate to
1165              * the page controllers before the onchange fires.  This
1166              * way, the caller does not have to manually capture the
1167              * $scope.selected value during onchange.
1168              */
1169             function fire_orgsel_onchange() {
1170                 if (!$scope.selected || !$scope.onchange) return;
1171                 $timeout(function() {
1172                     console.debug(
1173                         'egOrgSelector onchange('+$scope.selected.id()+')');
1174                     $scope.onchange($scope.selected)
1175                 });
1176             }
1177
1178             // Force the compare filter to run when the input is
1179             // clicked.  This allows for displaying all values when
1180             // clicking on an empty input.
1181             $scope.handleClick = function (e) {
1182                 $timeout(function () {
1183                     var current = $scope.selectedName;
1184                     // HACK-CITY
1185                     // Force the input value to "" so when the compare 
1186                     // function runs it will see the special empty key
1187                     // instead of the selected value.
1188                     $(e.target).val('');
1189                     $(e.target).trigger('input');
1190                     // After the compare function runs, reset the the
1191                     // selected value.
1192                     $scope.selectedName = current;
1193                 });
1194             }
1195
1196             $scope.compare = function(shortName, inputValue) {
1197                 return inputValue === secretEmptyKey ||
1198                     (shortName || '').toLowerCase().trim()
1199                         .indexOf((inputValue || '').toLowerCase().trim()) > -1;
1200             }
1201
1202             // Trim leading tree-spaces before displaying selected value
1203             $scope.formatDisplayName = function(shortName) {
1204                 return ($scope.selectedName || '').trim();
1205             }
1206
1207             $scope.orgIsDisabled = function(shortName) {
1208                 if ($scope.alldisabled === 'true') return true;
1209                 if (shortName && $scope.disableTest) {
1210                     var org = egCore.org.list().filter(function(org) {
1211                         return org.shortname() === shortName.trim();
1212                     })[0];
1213
1214                     return org && $scope.disableTest(org.id());
1215                 }
1216                 return false;
1217             }
1218
1219             $scope.inputChanged = function(shortName) {
1220                 // Avoid watching for changes on $scope.selected while
1221                 // manually applying values below.
1222                 unwatch_external_changes();
1223
1224                 // Manually prevent selection of disabled orgs
1225                 if ($scope.selectedName && 
1226                     !$scope.orgIsDisabled($scope.selectedName)) {
1227                     $scope.selected = egCore.org.list().filter(function(org) {
1228                         return org.shortname() === $scope.selectedName.trim()
1229                     })[0];
1230                 } else {
1231                     $scope.selected = null;
1232                 }
1233                 if ($scope.selected && $scope.stickySetting) {
1234                     egCore.hatch.setLocalItem(
1235                         $scope.stickySetting, $scope.selected.id());
1236                 }
1237
1238                 fire_orgsel_onchange();
1239                 $timeout(watch_external_changes);
1240             }
1241
1242             // Propagate external changes on $scope.selected to the typeahead
1243             var dewatcher;
1244             function watch_external_changes() {
1245                 dewatcher = $scope.$watch('selected', function(newVal, oldVal) {
1246                     if (newVal) {
1247                         $scope.selectedName = newVal.shortname();
1248                     } else {
1249                         $scope.selectedName = '';
1250                     }
1251                 });
1252             }
1253
1254             function unwatch_external_changes() {
1255                 if (dewatcher) {
1256                     dewatcher();
1257                     dewatcher = null;
1258                 }
1259             }
1260         }],
1261
1262         link : function(scope, element, attrs, egGridCtrl) {
1263
1264             // boolean fields are presented as value-less attributes
1265             angular.forEach(
1266                 ['nodefault'],
1267                 function(field) {
1268                     if (angular.isDefined(attrs[field]))
1269                         scope[field] = true;
1270                     else
1271                         scope[field] = false;
1272                 }
1273             );
1274         }
1275     }
1276 })
1277
1278 /*
1279 https://stackoverflow.com/questions/24764802/angular-js-automatically-focus-input-and-show-typeahead-dropdown-ui-bootstra
1280 */
1281 .directive('emptyTypeahead', function () {
1282     return {
1283         require: 'ngModel',
1284         link: function(scope, element, attrs, modelCtrl) {
1285
1286             var secretEmptyKey = '_INTERNAL_';
1287
1288             // this parser run before typeahead's parser
1289             modelCtrl.$parsers.unshift(function (inputValue) {
1290                 // replace empty string with secretEmptyKey to bypass typeahead-min-length check
1291                 var value = (inputValue ? inputValue : secretEmptyKey);
1292                 // this $viewValue must match the inputValue pass to typehead directive
1293                 modelCtrl.$viewValue = value;
1294                 return value;
1295             });
1296
1297             // this parser run after typeahead's parser
1298             modelCtrl.$parsers.push(function (inputValue) {
1299                 // set the secretEmptyKey back to empty string
1300                 return inputValue === secretEmptyKey ? '' : inputValue;
1301             });
1302         }
1303     }
1304 })
1305
1306 .directive('nextOnEnter', function () {
1307     return function (scope, element, attrs) {
1308         element.bind("keydown keypress", function (event) {
1309             if(event.which === 13) {
1310                 $('#'+attrs.nextOnEnter).focus();
1311                 event.preventDefault();
1312             }
1313         });
1314     };
1315 })
1316
1317 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
1318 .directive('egEnter', function () {
1319     return function (scope, element, attrs) {
1320         element.bind("keydown keypress", function (event) {
1321             if(event.which === 13) {
1322                 scope.$apply(function (){
1323                     scope.$eval(attrs.egEnter);
1324                 });
1325  
1326                 event.preventDefault();
1327             }
1328         });
1329     };
1330 })
1331
1332 /*
1333 * Handy wrapper directive for uib-datapicker-popup
1334 */
1335 .directive(
1336     'egDateInput', ['egStrings', 'egCore',
1337     function(egStrings, egCore) {
1338         return {
1339             scope : {
1340                 id : '@',
1341                 closeText : '@',
1342                 ngModel : '=',
1343                 ngChange : '=',
1344                 ngBlur : '=',
1345                 minDate : '=?',
1346                 maxDate : '=?',
1347                 ngDisabled : '=',
1348                 ngRequired : '=',
1349                 hideDatePicker : '=',
1350                 hideTimePicker : '=?',
1351                 dateFormat : '=?',
1352                 outOfRange : '=?',
1353                 focusMe : '=?'
1354             },
1355             require: 'ngModel',
1356             templateUrl: './share/t_datetime',
1357             replace: true,
1358             controller : ['$scope', function($scope) {
1359                 $scope.options = {
1360                     minDate : $scope.minDate,
1361                     maxDate : $scope.maxDate
1362                 };
1363
1364                 var maxDateObj = $scope.maxDate ? new Date($scope.maxDate) : null;
1365                 var minDateObj = $scope.minDate ? new Date($scope.minDate) : null;
1366
1367                 if ($scope.outOfRange !== undefined && (maxDateObj || minDateObj)) {
1368                     $scope.$watch('ngModel', function (n,o) {
1369                         var bad = false;
1370                         var newdate = new Date(n);
1371                         if (isNaN(newdate.getTime())) bad = true;
1372                         if (maxDateObj && newdate.getTime() > maxDateObj.getTime()) bad = true;
1373                         if (minDateObj && newdate.getTime() < minDateObj.getTime()) bad = true;
1374                         $scope.outOfRange = bad;
1375                     });
1376                 }
1377             }],
1378             link : function(scope, elm, attrs) {
1379                 if (!scope.closeText)
1380                     scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
1381
1382                 if ('showTimePicker' in attrs)
1383                     scope.showTimePicker = true;
1384
1385                 var default_format = 'mediumDate';
1386                 egCore.org.settings(['format.date']).then(function(set) {
1387                     if (set) default_format = set['format.date'];
1388                     scope.date_format = (scope.dateFormat) ?
1389                         scope.dateFormat :
1390                         default_format;
1391                 });
1392             }
1393         };
1394     }
1395 ])
1396
1397 /*
1398  *  egFmValueSelector - widget for selecting a value from list specified
1399  *                      by IDL class
1400  */
1401 .directive('egFmValueSelector', function() {
1402     return {
1403         restrict : 'E',
1404         transclude : true,
1405         scope : {
1406             idlClass : '@',
1407             ngModel : '=',
1408
1409             // optional filter for refining the set of rows that
1410             // get returned. Example:
1411             //
1412             // filter="{'column':{'=':null}}"
1413             filter : '=',
1414
1415             // optional name of settings key for persisting
1416             // the last selected value
1417             stickySetting : '@',
1418
1419             // optional OU setting for fetching default value;
1420             // used only if sticky setting not set
1421             ouSetting : '@'
1422         },
1423         require: 'ngModel',
1424         templateUrl : './share/t_fm_value_selector',
1425         controller : ['$scope','egCore', function($scope , egCore) {
1426
1427             $scope.org = egCore.org; // for use in the link function
1428             $scope.auth = egCore.auth; // for use in the link function
1429             $scope.hatch = egCore.hatch // for use in the link function
1430
1431             function flatten_linked_values(cls, list) {
1432                 var results = [];
1433                 var fields = egCore.idl.classes[cls].fields;
1434                 var id_field;
1435                 var selector;
1436                 angular.forEach(fields, function(fld) {
1437                     if (fld.datatype == 'id') {
1438                         id_field = fld.name;
1439                         selector = fld.selector ? fld.selector : id_field;
1440                         return;
1441                     }
1442                 });
1443                 angular.forEach(list, function(item) {
1444                     var rec = egCore.idl.toHash(item);
1445                     results.push({
1446                         id : rec[id_field],
1447                         name : rec[selector]
1448                     });
1449                 });
1450                 return results;
1451             }
1452
1453             var search = {};
1454             search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
1455             if ($scope.filter) {
1456                 angular.extend(search, $scope.filter);
1457             }
1458             egCore.pcrud.search(
1459                 $scope.idlClass, search, {}, {atomic : true}
1460             ).then(function(list) {
1461                 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
1462             });
1463
1464             $scope.handleChange = function(value) {
1465                 if ($scope.stickySetting) {
1466                     egCore.hatch.setLocalItem($scope.stickySetting, value);
1467                 }
1468             }
1469
1470         }],
1471         link : function(scope, element, attrs) {
1472             if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1473                 var value = scope.hatch.getLocalItem(scope.stickySetting);
1474                 scope.ngModel = value;
1475             }
1476             if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1477                 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
1478                 .then(function(set) {
1479                     var value = parseInt(set[scope.ouSetting]);
1480                     if (!isNaN(value))
1481                         scope.ngModel = value;
1482                 });
1483             }
1484         }
1485     }
1486 })
1487
1488 /*
1489  *  egShareDepthSelector - widget for selecting a share depth
1490  */
1491 .directive('egShareDepthSelector', function() {
1492     return {
1493         restrict : 'E',
1494         transclude : true,
1495         scope : {
1496             ngModel : '=',
1497             useOpacLabel : '@',
1498             maxDepth : '@',
1499         },
1500         require: 'ngModel',
1501         templateUrl : './share/t_share_depth_selector',
1502         controller : ['$scope','egCore', function($scope , egCore) {
1503             $scope.values = [];
1504             egCore.pcrud.search('aout',
1505                 { id : {'!=' : null} },
1506                 { order_by : {aout : ['depth', 'name']} },
1507                 { atomic : true }
1508             ).then(function(list) {
1509                 var scratch = [];
1510                 angular.forEach(list, function(aout) {
1511                     var depth = parseInt(aout.depth());
1512                     if (typeof $scope.maxDepth == 'undefined' || depth <= $scope.maxDepth) {
1513                         var text = $scope.useOpacLabel ? aout.opac_label() : aout.name();
1514                         if (depth in scratch) {
1515                             scratch[depth].push(text);
1516                         } else {
1517                             scratch[depth] = [ text ]
1518                         }
1519                     }
1520                 });
1521                 scratch.forEach(function(val, idx) {
1522                     $scope.values.push({ id : idx,  name : scratch[idx].join(' / ') });
1523                 });
1524             });
1525         }],
1526         link : function(scope, elm, attrs) {
1527             if ('useOpacLabel' in attrs)
1528                 scope.useOpacLabel = true;
1529             if ('maxDepth' in attrs) // I feel like I'm doing this wrong :)
1530                 scope.maxDepth = parseInt(attrs.maxdepth);
1531         }
1532     }
1533 })
1534
1535 /*
1536  * egHelpPopover - a helpful widget
1537  */
1538 .directive('egHelpPopover', function() {
1539     return {
1540         restrict : 'E',
1541         transclude : true,
1542         scope : {
1543             helpText : '@',
1544             helpLink : '@'
1545         },
1546         templateUrl : './share/t_help_popover',
1547         controller : ['$scope','$sce', function($scope , $sce) {
1548             if ($scope.helpLink) {
1549                 $scope.helpHtml = $sce.trustAsHtml(
1550                     '<a target="_new" href="' + $scope.helpLink + '">' +
1551                     $scope.helpText + '</a>'
1552                 );
1553             }
1554         }]
1555     }
1556 })
1557
1558 .factory('egWorkLog', ['egCore', function(egCore) {
1559     var service = {};
1560
1561     service.retrieve_all = function() {
1562         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1563         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1564
1565         return { 'work_log' : workLog, 'patron_log' : patronLog };
1566     }
1567
1568     service.record = function(message,data) {
1569         var max_entries;
1570         var max_patrons;
1571         if (typeof egCore != 'undefined') {
1572             if (typeof egCore.env != 'undefined') {
1573                 if (typeof egCore.env.aous != 'undefined') {
1574                     max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
1575                     max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
1576                 } else {
1577                     console.log('worklog: missing egCore.env.aous');
1578                 }
1579             } else {
1580                 console.log('worklog: missing egCore.env');
1581             }
1582         } else {
1583             console.log('worklog: missing egCore');
1584         }
1585         if (!max_entries) {
1586             if (typeof egCore.org != 'undefined') {
1587                 if (typeof egCore.org.cachedSettings != 'undefined') {
1588                     max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
1589                 } else {
1590                     console.log('worklog: missing egCore.org.cachedSettings');
1591                 }
1592             } else {
1593                 console.log('worklog: missing egCore.org');
1594             }
1595         }
1596         if (!max_patrons) {
1597             if (typeof egCore.org != 'undefined') {
1598                 if (typeof egCore.org.cachedSettings != 'undefined') {
1599                     max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
1600                 } else {
1601                     console.log('worklog: missing egCore.org.cachedSettings');
1602                 }
1603             } else {
1604                 console.log('worklog: missing egCore.org');
1605             }
1606         }
1607         if (!max_entries) {
1608             max_entries = 20;
1609             console.log('worklog: defaulting to max_entries = ' + max_entries);
1610         }
1611         if (!max_patrons) {
1612             max_patrons = 10;
1613             console.log('worklog: defaulting to max_patrons = ' + max_patrons);
1614         }
1615
1616         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1617         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1618         var entry = {
1619             'when' : new Date(),
1620             'msg' : message,
1621             'action' : data.action,
1622             'actor' : egCore.auth.user().usrname()
1623         };
1624         if (data.action == 'checkin') {
1625             entry['item'] = data.response.params.copy_barcode;
1626             entry['item_id'] = data.response.data.acp.id();
1627             if (data.response.data.au) {
1628                 entry['user'] = data.response.data.au.family_name();
1629                 entry['patron_id'] = data.response.data.au.id();
1630             }
1631         }
1632         if (data.action == 'checkout') {
1633             entry['item'] = data.response.params.copy_barcode;
1634             entry['user'] = data.response.data.au.family_name();
1635             entry['item_id'] = data.response.data.acp.id();
1636             entry['patron_id'] = data.response.data.au.id();
1637         }
1638         if (data.action == 'noncat_checkout') {
1639             entry['user'] = data.response.data.au.family_name();
1640             entry['patron_id'] = data.response.data.au.id();
1641         }
1642         if (data.action == 'renew') {
1643             entry['item'] = data.response.params.copy_barcode;
1644             entry['user'] = data.response.data.au.family_name();
1645             entry['item_id'] = data.response.data.acp.id();
1646             entry['patron_id'] = data.response.data.au.id();
1647         }
1648         if (data.action == 'requested_hold'
1649             || data.action == 'edited_patron'
1650             || data.action == 'registered_patron'
1651             || data.action == 'paid_bill') {
1652             entry['patron_id'] = data.patron_id;
1653         }
1654         if (data.action == 'requested_hold') {
1655             entry['hold_id'] = data.hold_id;
1656         }
1657         if (data.action == 'paid_bill') {
1658             entry['amount'] = data.total_amount;
1659         }
1660
1661         workLog.push( entry );
1662         if (workLog.length > max_entries) workLog.shift();
1663         egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
1664
1665         if (entry['patron_id']) {
1666             var temp = [];
1667             for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
1668                 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
1669             }
1670             temp.push( entry );
1671             if (temp.length > max_patrons) temp.shift();
1672             patronLog = temp;
1673             egCore.hatch.setLocalItem('eg.patron_log',patronLog);
1674         }
1675
1676         console.log('worklog',entry);
1677     }
1678
1679     return service;
1680 }]);