LP1793196 Fix console error when none selected
[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">'+
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) act = act.toString();
961                     return new RegExp(act.toLowerCase()).test(ex)
962                 }
963
964                 $scope.showAll = function () {
965
966                     $scope.clickedopen = !$scope.clickedopen;
967
968                     if ($scope.clickedclosed === null) {
969                         if (!$scope.clickedopen) {
970                             $scope.clickedclosed = true;
971                         }
972                     } else {
973                         $scope.clickedclosed = !$scope.clickedopen;
974                     }
975
976                     if ($scope.selected && $scope.selected.length > 0) $scope.complete_list = true;
977                     if (!$scope.selected || $scope.selected.length == 0) $scope.complete_list = false;
978                     $scope.makeOpen();
979                 }
980
981                 $scope.makeOpen = function () {
982                     $scope.isopen = $scope.clickedopen || ($filter('filter')(
983                         $scope.list,
984                         $scope.selected
985                     ).length > 0 && $scope.selected && $scope.selected.length > 0);
986                     if ($scope.clickedclosed) {
987                         $scope.isopen = false;
988                         $scope.clickedclosed = null;
989                     }
990                 }
991
992                 $scope.changeValue = function (newVal) {
993                     $scope.selected = newVal;
994                     $scope.isopen = false;
995                     $scope.clickedclosed = null;
996                     $scope.clickedopen = false;
997                     if ($scope.selected.length == 0) $scope.complete_list = false;
998                     if ($scope.onSelect) $scope.onSelect();
999                 }
1000
1001             }
1002         ]
1003     };
1004 })
1005
1006 .directive('egListCounts', function() {
1007     return {
1008         restrict: 'E',
1009         replace: true,
1010         scope: {
1011             label: "@",
1012             list: "=", // list of things
1013             render: "=", // function to turn thing into string; default to stringification
1014             onSelect: "=" // function to fire when option selected. passed one copy of the selected value
1015         },
1016         templateUrl: './share/t_listcounts',
1017         controller: ['$scope','$timeout',
1018             function( $scope , $timeout ) {
1019
1020                 $scope.isopen = false;
1021                 $scope.count_hash = {};
1022
1023                 $scope.renderer = $scope.render ? $scope.render : function (x) { return ""+x };
1024
1025                 $scope.$watchCollection('list',function() {
1026                     $scope.count_hash = {};
1027                     angular.forEach($scope.list, function (item) {
1028                         var str = $scope.renderer(item);
1029                         if (!$scope.count_hash[str]) {
1030                             $scope.count_hash[str] = {
1031                                 count : 1,
1032                                 value : str,
1033                                 original : item
1034                             };
1035                         } else {
1036                             $scope.count_hash[str].count++;
1037                         }
1038                     });
1039                 });
1040
1041                 $scope.selectValue = function (item) {
1042                     if ($scope.onSelect) $scope.onSelect(item);
1043                 }
1044
1045             }
1046         ]
1047     };
1048 })
1049
1050 /**
1051  * Nested org unit selector modeled as a Bootstrap dropdown button.
1052  */
1053 .directive('egOrgSelector', function() {
1054     return {
1055         restrict : 'AE',
1056         transclude : true,
1057         replace : true, // makes styling easier
1058         scope : {
1059             selected : '=', // defaults to workstation or root org,
1060                             // unless the nodefault attibute exists
1061
1062             // Each org unit is passed into this function and, for
1063             // any org units where the response value is true, the
1064             // org unit will not be added to the selector.
1065             hiddenTest : '=',
1066
1067             // Each org unit is passed into this function and, for
1068             // any org units where the response value is true, the
1069             // org unit will not be available for selection.
1070             disableTest : '=',
1071
1072             // if set to true, disable the UI element altogether
1073             alldisabled : '@',
1074
1075             // Caller can either $watch(selected, ..) or register an
1076             // onchange handler.
1077             onchange : '=',
1078
1079             // optional typeahead placeholder text
1080             label : '@',
1081
1082             // optional name of settings key for persisting
1083             // the last selected org unit
1084             stickySetting : '@'
1085         },
1086
1087         templateUrl : './share/t_org_select',
1088
1089         controller : ['$scope','$timeout','egCore','egStartup','$q',
1090               function($scope , $timeout , egCore , egStartup , $q) {
1091
1092             // See emptyTypeahead directive below.
1093             var secretEmptyKey = '_INTERNAL_';
1094
1095             function formatName(org) {
1096                 return " ".repeat(org.ou_type().depth()) + org.shortname();
1097             }
1098
1099             // avoid linking the full fleshed tree to the scope by 
1100             // tossing in a flattened list.
1101             // --
1102             // Run-time code referencing post-start data should be run
1103             // from within a startup block, otherwise accessing this
1104             // module before startup completes will lead to failure.
1105             //
1106             // controller() runs before link().
1107             // This post-startup code runs after link().
1108             egStartup.go(
1109             ).then(
1110                 function() {
1111                     return egCore.env.classLoaders.aou();
1112                 }
1113             ).then(
1114                 function() {
1115
1116                     $scope.selecteName = '';
1117
1118                     $scope.shortNames = egCore.org.list()
1119                     .filter(function(org) {
1120                         return !(
1121                             $scope.hiddenTest && 
1122                             $scope.hiddenTest(org.id())
1123                         );
1124                     }).map(function(org) {
1125                         return formatName(org);
1126                     });
1127     
1128                     // Apply default values
1129     
1130                     if ($scope.stickySetting) {
1131                         var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
1132                         if (orgId) {
1133                             var org = egCore.org.get(orgId);
1134                             if (org) {
1135                                 $scope.selected = org;
1136                                 $scope.selectedName = org.shortname();
1137                             }
1138                         }
1139                     }
1140     
1141                     if (!$scope.selected && !$scope.nodefault && egCore.auth.user()) {
1142                         var org = egCore.org.get(egCore.auth.user().ws_ou());
1143                         $scope.selected = org;
1144                         $scope.selectedName = org.shortname();
1145                     }
1146     
1147                     fire_orgsel_onchange(); // no-op if nothing is selected
1148                     watch_external_changes();
1149                 }
1150             );
1151
1152             /**
1153              * Fire onchange handler after a timeout, so the
1154              * $scope.selected value has a chance to propagate to
1155              * the page controllers before the onchange fires.  This
1156              * way, the caller does not have to manually capture the
1157              * $scope.selected value during onchange.
1158              */
1159             function fire_orgsel_onchange() {
1160                 if (!$scope.selected || !$scope.onchange) return;
1161                 $timeout(function() {
1162                     console.debug(
1163                         'egOrgSelector onchange('+$scope.selected.id()+')');
1164                     $scope.onchange($scope.selected)
1165                 });
1166             }
1167
1168             // Force the compare filter to run when the input is
1169             // clicked.  This allows for displaying all values when
1170             // clicking on an empty input.
1171             $scope.handleClick = function (e) {
1172                 $timeout(function () {
1173                     var current = $scope.selectedName;
1174                     // HACK-CITY
1175                     // Force the input value to "" so when the compare 
1176                     // function runs it will see the special empty key
1177                     // instead of the selected value.
1178                     $(e.target).val('');
1179                     $(e.target).trigger('input');
1180                     // After the compare function runs, reset the the
1181                     // selected value.
1182                     $scope.selectedName = current;
1183                 });
1184             }
1185
1186             $scope.compare = function(shortName, inputValue) {
1187                 return inputValue === secretEmptyKey ||
1188                     (shortName || '').toLowerCase().trim()
1189                         .indexOf((inputValue || '').toLowerCase().trim()) > -1;
1190             }
1191
1192             // Trim leading tree-spaces before displaying selected value
1193             $scope.formatDisplayName = function(shortName) {
1194                 return ($scope.selectedName || '').trim();
1195             }
1196
1197             $scope.orgIsDisabled = function(shortName) {
1198                 if ($scope.alldisabled === 'true') return true;
1199                 if (shortName && $scope.disableTest) {
1200                     var org = egCore.org.list().filter(function(org) {
1201                         return org.shortname() === shortName.trim();
1202                     })[0];
1203
1204                     return org && $scope.disableTest(org.id());
1205                 }
1206                 return false;
1207             }
1208
1209             $scope.inputChanged = function(shortName) {
1210                 // Avoid watching for changes on $scope.selected while
1211                 // manually applying values below.
1212                 unwatch_external_changes();
1213
1214                 // Manually prevent selection of disabled orgs
1215                 if ($scope.selectedName && 
1216                     !$scope.orgIsDisabled($scope.selectedName)) {
1217                     $scope.selected = egCore.org.list().filter(function(org) {
1218                         return org.shortname() === $scope.selectedName.trim()
1219                     })[0];
1220                 } else {
1221                     $scope.selected = null;
1222                 }
1223                 if ($scope.selected && $scope.stickySetting) {
1224                     egCore.hatch.setLocalItem(
1225                         $scope.stickySetting, $scope.selected.id());
1226                 }
1227
1228                 fire_orgsel_onchange();
1229                 $timeout(watch_external_changes);
1230             }
1231
1232             // Propagate external changes on $scope.selected to the typeahead
1233             var dewatcher;
1234             function watch_external_changes() {
1235                 dewatcher = $scope.$watch('selected', function(newVal, oldVal) {
1236                     if (newVal) {
1237                         $scope.selectedName = newVal.shortname();
1238                     } else {
1239                         $scope.selectedName = '';
1240                     }
1241                 });
1242             }
1243
1244             function unwatch_external_changes() {
1245                 if (dewatcher) {
1246                     dewatcher();
1247                     dewatcher = null;
1248                 }
1249             }
1250         }],
1251
1252         link : function(scope, element, attrs, egGridCtrl) {
1253
1254             // boolean fields are presented as value-less attributes
1255             angular.forEach(
1256                 ['nodefault'],
1257                 function(field) {
1258                     if (angular.isDefined(attrs[field]))
1259                         scope[field] = true;
1260                     else
1261                         scope[field] = false;
1262                 }
1263             );
1264         }
1265     }
1266 })
1267
1268 /*
1269 https://stackoverflow.com/questions/24764802/angular-js-automatically-focus-input-and-show-typeahead-dropdown-ui-bootstra
1270 */
1271 .directive('emptyTypeahead', function () {
1272     return {
1273         require: 'ngModel',
1274         link: function(scope, element, attrs, modelCtrl) {
1275
1276             var secretEmptyKey = '_INTERNAL_';
1277
1278             // this parser run before typeahead's parser
1279             modelCtrl.$parsers.unshift(function (inputValue) {
1280                 // replace empty string with secretEmptyKey to bypass typeahead-min-length check
1281                 var value = (inputValue ? inputValue : secretEmptyKey);
1282                 // this $viewValue must match the inputValue pass to typehead directive
1283                 modelCtrl.$viewValue = value;
1284                 return value;
1285             });
1286
1287             // this parser run after typeahead's parser
1288             modelCtrl.$parsers.push(function (inputValue) {
1289                 // set the secretEmptyKey back to empty string
1290                 return inputValue === secretEmptyKey ? '' : inputValue;
1291             });
1292         }
1293     }
1294 })
1295
1296 .directive('nextOnEnter', function () {
1297     return function (scope, element, attrs) {
1298         element.bind("keydown keypress", function (event) {
1299             if(event.which === 13) {
1300                 $('#'+attrs.nextOnEnter).focus();
1301                 event.preventDefault();
1302             }
1303         });
1304     };
1305 })
1306
1307 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
1308 .directive('egEnter', function () {
1309     return function (scope, element, attrs) {
1310         element.bind("keydown keypress", function (event) {
1311             if(event.which === 13) {
1312                 scope.$apply(function (){
1313                     scope.$eval(attrs.egEnter);
1314                 });
1315  
1316                 event.preventDefault();
1317             }
1318         });
1319     };
1320 })
1321
1322 /*
1323 * Handy wrapper directive for uib-datapicker-popup
1324 */
1325 .directive(
1326     'egDateInput', ['egStrings', 'egCore',
1327     function(egStrings, egCore) {
1328         return {
1329             scope : {
1330                 id : '@',
1331                 closeText : '@',
1332                 ngModel : '=',
1333                 ngChange : '=',
1334                 ngBlur : '=',
1335                 minDate : '=?',
1336                 maxDate : '=?',
1337                 ngDisabled : '=',
1338                 ngRequired : '=',
1339                 hideDatePicker : '=',
1340                 hideTimePicker : '=?',
1341                 dateFormat : '=?',
1342                 outOfRange : '=?',
1343                 focusMe : '=?'
1344             },
1345             require: 'ngModel',
1346             templateUrl: './share/t_datetime',
1347             replace: true,
1348             controller : ['$scope', function($scope) {
1349                 $scope.options = {
1350                     minDate : $scope.minDate,
1351                     maxDate : $scope.maxDate
1352                 };
1353
1354                 var maxDateObj = $scope.maxDate ? new Date($scope.maxDate) : null;
1355                 var minDateObj = $scope.minDate ? new Date($scope.minDate) : null;
1356
1357                 if ($scope.outOfRange !== undefined && (maxDateObj || minDateObj)) {
1358                     $scope.$watch('ngModel', function (n,o) {
1359                         if (n && n != o) {
1360                             var bad = false;
1361                             var newdate = new Date(n);
1362                             if (maxDateObj && newdate.getTime() > maxDateObj.getTime()) bad = true;
1363                             if (minDateObj && newdate.getTime() < minDateObj.getTime()) bad = true;
1364                             $scope.outOfRange = bad;
1365                         }
1366                     });
1367                 }
1368             }],
1369             link : function(scope, elm, attrs) {
1370                 if (!scope.closeText)
1371                     scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
1372
1373                 if ('showTimePicker' in attrs)
1374                     scope.showTimePicker = true;
1375
1376                 var default_format = 'mediumDate';
1377                 egCore.org.settings(['format.date']).then(function(set) {
1378                     if (set) default_format = set['format.date'];
1379                     scope.date_format = (scope.dateFormat) ?
1380                         scope.dateFormat :
1381                         default_format;
1382                 });
1383             }
1384         };
1385     }
1386 ])
1387
1388 /*
1389  *  egFmValueSelector - widget for selecting a value from list specified
1390  *                      by IDL class
1391  */
1392 .directive('egFmValueSelector', function() {
1393     return {
1394         restrict : 'E',
1395         transclude : true,
1396         scope : {
1397             idlClass : '@',
1398             ngModel : '=',
1399
1400             // optional filter for refining the set of rows that
1401             // get returned. Example:
1402             //
1403             // filter="{'column':{'=':null}}"
1404             filter : '=',
1405
1406             // optional name of settings key for persisting
1407             // the last selected value
1408             stickySetting : '@',
1409
1410             // optional OU setting for fetching default value;
1411             // used only if sticky setting not set
1412             ouSetting : '@'
1413         },
1414         require: 'ngModel',
1415         templateUrl : './share/t_fm_value_selector',
1416         controller : ['$scope','egCore', function($scope , egCore) {
1417
1418             $scope.org = egCore.org; // for use in the link function
1419             $scope.auth = egCore.auth; // for use in the link function
1420             $scope.hatch = egCore.hatch // for use in the link function
1421
1422             function flatten_linked_values(cls, list) {
1423                 var results = [];
1424                 var fields = egCore.idl.classes[cls].fields;
1425                 var id_field;
1426                 var selector;
1427                 angular.forEach(fields, function(fld) {
1428                     if (fld.datatype == 'id') {
1429                         id_field = fld.name;
1430                         selector = fld.selector ? fld.selector : id_field;
1431                         return;
1432                     }
1433                 });
1434                 angular.forEach(list, function(item) {
1435                     var rec = egCore.idl.toHash(item);
1436                     results.push({
1437                         id : rec[id_field],
1438                         name : rec[selector]
1439                     });
1440                 });
1441                 return results;
1442             }
1443
1444             var search = {};
1445             search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
1446             if ($scope.filter) {
1447                 angular.extend(search, $scope.filter);
1448             }
1449             egCore.pcrud.search(
1450                 $scope.idlClass, search, {}, {atomic : true}
1451             ).then(function(list) {
1452                 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
1453             });
1454
1455             $scope.handleChange = function(value) {
1456                 if ($scope.stickySetting) {
1457                     egCore.hatch.setLocalItem($scope.stickySetting, value);
1458                 }
1459             }
1460
1461         }],
1462         link : function(scope, element, attrs) {
1463             if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1464                 var value = scope.hatch.getLocalItem(scope.stickySetting);
1465                 scope.ngModel = value;
1466             }
1467             if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1468                 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
1469                 .then(function(set) {
1470                     var value = parseInt(set[scope.ouSetting]);
1471                     if (!isNaN(value))
1472                         scope.ngModel = value;
1473                 });
1474             }
1475         }
1476     }
1477 })
1478
1479 /*
1480  *  egShareDepthSelector - widget for selecting a share depth
1481  */
1482 .directive('egShareDepthSelector', function() {
1483     return {
1484         restrict : 'E',
1485         transclude : true,
1486         scope : {
1487             ngModel : '=',
1488         },
1489         require: 'ngModel',
1490         templateUrl : './share/t_share_depth_selector',
1491         controller : ['$scope','egCore', function($scope , egCore) {
1492             $scope.values = [];
1493             egCore.pcrud.search('aout',
1494                 { id : {'!=' : null} },
1495                 { order_by : {aout : ['depth', 'name']} },
1496                 { atomic : true }
1497             ).then(function(list) {
1498                 var scratch = [];
1499                 angular.forEach(list, function(aout) {
1500                     var depth = parseInt(aout.depth());
1501                     if (depth in scratch) {
1502                         scratch[depth].push(aout.name());
1503                     } else {
1504                         scratch[depth] = [ aout.name() ]
1505                     }
1506                 });
1507                 scratch.forEach(function(val, idx) {
1508                     $scope.values.push({ id : idx,  name : scratch[idx].join(' / ') });
1509                 });
1510             });
1511         }]
1512     }
1513 })
1514
1515 /*
1516  * egHelpPopover - a helpful widget
1517  */
1518 .directive('egHelpPopover', function() {
1519     return {
1520         restrict : 'E',
1521         transclude : true,
1522         scope : {
1523             helpText : '@',
1524             helpLink : '@'
1525         },
1526         templateUrl : './share/t_help_popover',
1527         controller : ['$scope','$sce', function($scope , $sce) {
1528             if ($scope.helpLink) {
1529                 $scope.helpHtml = $sce.trustAsHtml(
1530                     '<a target="_new" href="' + $scope.helpLink + '">' +
1531                     $scope.helpText + '</a>'
1532                 );
1533             }
1534         }]
1535     }
1536 })
1537
1538 .factory('egWorkLog', ['egCore', function(egCore) {
1539     var service = {};
1540
1541     service.retrieve_all = function() {
1542         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1543         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1544
1545         return { 'work_log' : workLog, 'patron_log' : patronLog };
1546     }
1547
1548     service.record = function(message,data) {
1549         var max_entries;
1550         var max_patrons;
1551         if (typeof egCore != 'undefined') {
1552             if (typeof egCore.env != 'undefined') {
1553                 if (typeof egCore.env.aous != 'undefined') {
1554                     max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
1555                     max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
1556                 } else {
1557                     console.log('worklog: missing egCore.env.aous');
1558                 }
1559             } else {
1560                 console.log('worklog: missing egCore.env');
1561             }
1562         } else {
1563             console.log('worklog: missing egCore');
1564         }
1565         if (!max_entries) {
1566             if (typeof egCore.org != 'undefined') {
1567                 if (typeof egCore.org.cachedSettings != 'undefined') {
1568                     max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
1569                 } else {
1570                     console.log('worklog: missing egCore.org.cachedSettings');
1571                 }
1572             } else {
1573                 console.log('worklog: missing egCore.org');
1574             }
1575         }
1576         if (!max_patrons) {
1577             if (typeof egCore.org != 'undefined') {
1578                 if (typeof egCore.org.cachedSettings != 'undefined') {
1579                     max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
1580                 } else {
1581                     console.log('worklog: missing egCore.org.cachedSettings');
1582                 }
1583             } else {
1584                 console.log('worklog: missing egCore.org');
1585             }
1586         }
1587         if (!max_entries) {
1588             max_entries = 20;
1589             console.log('worklog: defaulting to max_entries = ' + max_entries);
1590         }
1591         if (!max_patrons) {
1592             max_patrons = 10;
1593             console.log('worklog: defaulting to max_patrons = ' + max_patrons);
1594         }
1595
1596         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1597         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1598         var entry = {
1599             'when' : new Date(),
1600             'msg' : message,
1601             'action' : data.action,
1602             'actor' : egCore.auth.user().usrname()
1603         };
1604         if (data.action == 'checkin') {
1605             entry['item'] = data.response.params.copy_barcode;
1606             entry['item_id'] = data.response.data.acp.id();
1607             if (data.response.data.au) {
1608                 entry['user'] = data.response.data.au.family_name();
1609                 entry['patron_id'] = data.response.data.au.id();
1610             }
1611         }
1612         if (data.action == 'checkout') {
1613             entry['item'] = data.response.params.copy_barcode;
1614             entry['user'] = data.response.data.au.family_name();
1615             entry['item_id'] = data.response.data.acp.id();
1616             entry['patron_id'] = data.response.data.au.id();
1617         }
1618         if (data.action == 'noncat_checkout') {
1619             entry['user'] = data.response.data.au.family_name();
1620             entry['patron_id'] = data.response.data.au.id();
1621         }
1622         if (data.action == 'renew') {
1623             entry['item'] = data.response.params.copy_barcode;
1624             entry['user'] = data.response.data.au.family_name();
1625             entry['item_id'] = data.response.data.acp.id();
1626             entry['patron_id'] = data.response.data.au.id();
1627         }
1628         if (data.action == 'requested_hold'
1629             || data.action == 'edited_patron'
1630             || data.action == 'registered_patron'
1631             || data.action == 'paid_bill') {
1632             entry['patron_id'] = data.patron_id;
1633         }
1634         if (data.action == 'requested_hold') {
1635             entry['hold_id'] = data.hold_id;
1636         }
1637         if (data.action == 'paid_bill') {
1638             entry['amount'] = data.total_amount;
1639         }
1640
1641         workLog.push( entry );
1642         if (workLog.length > max_entries) workLog.shift();
1643         egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
1644
1645         if (entry['patron_id']) {
1646             var temp = [];
1647             for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
1648                 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
1649             }
1650             temp.push( entry );
1651             if (temp.length > max_patrons) temp.shift();
1652             patronLog = temp;
1653             egCore.hatch.setLocalItem('eg.patron_log',patronLog);
1654         }
1655
1656         console.log('worklog',entry);
1657     }
1658
1659     return service;
1660 }]);