]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/ui.js
LP#1739286: Belt and suspenders on radio button flags
[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">'+
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.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 /**
1007  * Nested org unit selector modeled as a Bootstrap dropdown button.
1008  */
1009 .directive('egOrgSelector', function() {
1010     return {
1011         restrict : 'AE',
1012         transclude : true,
1013         replace : true, // makes styling easier
1014         scope : {
1015             selected : '=', // defaults to workstation or root org,
1016                             // unless the nodefault attibute exists
1017
1018             // Each org unit is passed into this function and, for
1019             // any org units where the response value is true, the
1020             // org unit will not be added to the selector.
1021             hiddenTest : '=',
1022
1023             // Each org unit is passed into this function and, for
1024             // any org units where the response value is true, the
1025             // org unit will not be available for selection.
1026             disableTest : '=',
1027
1028             // if set to true, disable the UI element altogether
1029             alldisabled : '@',
1030
1031             // Caller can either $watch(selected, ..) or register an
1032             // onchange handler.
1033             onchange : '=',
1034
1035             // optional primary drop-down button label
1036             label : '@',
1037
1038             // optional name of settings key for persisting
1039             // the last selected org unit
1040             stickySetting : '@'
1041         },
1042
1043         // any reason to move this into a TT2 template?
1044         template : 
1045             '<div class="btn-group eg-org-selector" uib-dropdown>'
1046             + '<button type="button" class="btn btn-default" uib-dropdown-toggle ng-disabled="disable_button">'
1047              + '<span style="padding-right: 5px;">{{getSelectedName()}}</span>'
1048              + '<span class="caret"></span>'
1049            + '</button>'
1050            + '<ul uib-dropdown-menu class="scrollable-menu">'
1051              + '<li ng-repeat="org in orgList" ng-hide="hiddenTest(org.id)">'
1052                + '<a href ng-click="orgChanged(org)" a-disabled="disableTest(org.id)" '
1053                  + 'style="padding-left: {{org.depth * 10 + 5}}px">'
1054                  + '{{org.shortname}}'
1055                + '</a>'
1056              + '</li>'
1057            + '</ul>'
1058           + '</div>',
1059
1060         controller : ['$scope','$timeout','egCore','egStartup','egLovefield','$q',
1061               function($scope , $timeout , egCore , egStartup , egLovefield , $q) {
1062
1063             if ($scope.alldisabled) {
1064                 $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
1065             } else {
1066                 $scope.disable_button = false;
1067             }
1068
1069             // avoid linking the full fleshed tree to the scope by 
1070             // tossing in a flattened list.
1071             // --
1072             // Run-time code referencing post-start data should be run
1073             // from within a startup block, otherwise accessing this
1074             // module before startup completes will lead to failure.
1075             //
1076             // controller() runs before link().
1077             // This post-startup code runs after link().
1078             egStartup.go(
1079             ).then(
1080                 function() {
1081                     return egCore.env.classLoaders.aou();
1082                 }
1083             ).then(
1084                 function() {
1085
1086                     $scope.orgList = egCore.org.list().map(function(org) {
1087                         return {
1088                             id : org.id(),
1089                             shortname : org.shortname(), 
1090                             depth : org.ou_type().depth()
1091                         }
1092                     });
1093                     
1094     
1095                     // Apply default values
1096     
1097                     if ($scope.stickySetting) {
1098                         var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
1099                         if (orgId) {
1100                             $scope.selected = egCore.org.get(orgId);
1101                         }
1102                     }
1103     
1104                     if (!$scope.selected && !$scope.nodefault && egCore.auth.user()) {
1105                         $scope.selected = 
1106                             egCore.org.get(egCore.auth.user().ws_ou());
1107                     }
1108     
1109                     fire_orgsel_onchange(); // no-op if nothing is selected
1110                 }
1111             );
1112
1113             /**
1114              * Fire onchange handler after a timeout, so the
1115              * $scope.selected value has a chance to propagate to
1116              * the page controllers before the onchange fires.  This
1117              * way, the caller does not have to manually capture the
1118              * $scope.selected value during onchange.
1119              */
1120             function fire_orgsel_onchange() {
1121                 if (!$scope.selected || !$scope.onchange) return;
1122                 $timeout(function() {
1123                     console.debug(
1124                         'egOrgSelector onchange('+$scope.selected.id()+')');
1125                     $scope.onchange($scope.selected)
1126                 });
1127             }
1128
1129             $scope.getSelectedName = function() {
1130                 if ($scope.selected && $scope.selected.shortname)
1131                     return $scope.selected.shortname();
1132                 return $scope.label;
1133             }
1134
1135             $scope.orgChanged = function(org) {
1136                 $scope.selected = egCore.org.get(org.id);
1137                 if ($scope.stickySetting) {
1138                     egCore.hatch.setLocalItem($scope.stickySetting, org.id);
1139                 }
1140                 fire_orgsel_onchange();
1141             }
1142
1143         }],
1144         link : function(scope, element, attrs, egGridCtrl) {
1145
1146             // boolean fields are presented as value-less attributes
1147             angular.forEach(
1148                 ['nodefault'],
1149                 function(field) {
1150                     if (angular.isDefined(attrs[field]))
1151                         scope[field] = true;
1152                     else
1153                         scope[field] = false;
1154                 }
1155             );
1156         }
1157     }
1158 })
1159
1160 .directive('nextOnEnter', function () {
1161     return function (scope, element, attrs) {
1162         element.bind("keydown keypress", function (event) {
1163             if(event.which === 13) {
1164                 $('#'+attrs.nextOnEnter).focus();
1165                 event.preventDefault();
1166             }
1167         });
1168     };
1169 })
1170
1171 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
1172 .directive('egEnter', function () {
1173     return function (scope, element, attrs) {
1174         element.bind("keydown keypress", function (event) {
1175             if(event.which === 13) {
1176                 scope.$apply(function (){
1177                     scope.$eval(attrs.egEnter);
1178                 });
1179  
1180                 event.preventDefault();
1181             }
1182         });
1183     };
1184 })
1185
1186 /*
1187 * Handy wrapper directive for uib-datapicker-popup
1188 */
1189 .directive(
1190     'egDateInput', ['egStrings', 'egCore',
1191     function(egStrings, egCore) {
1192         return {
1193             scope : {
1194                 id : '@',
1195                 closeText : '@',
1196                 ngModel : '=',
1197                 ngChange : '=',
1198                 ngBlur : '=',
1199                 minDate : '=?',
1200                 maxDate : '=?',
1201                 ngDisabled : '=',
1202                 ngRequired : '=',
1203                 hideDatePicker : '=',
1204                 hideTimePicker : '=?',
1205                 dateFormat : '=?',
1206                 outOfRange : '=?',
1207                 focusMe : '=?'
1208             },
1209             require: 'ngModel',
1210             templateUrl: './share/t_datetime',
1211             replace: true,
1212             controller : ['$scope', function($scope) {
1213                 $scope.options = {
1214                     minDate : $scope.minDate,
1215                     maxDate : $scope.maxDate
1216                 };
1217
1218                 var maxDateObj = $scope.maxDate ? new Date($scope.maxDate) : null;
1219                 var minDateObj = $scope.minDate ? new Date($scope.minDate) : null;
1220
1221                 if ($scope.outOfRange !== undefined && (maxDateObj || minDateObj)) {
1222                     $scope.$watch('ngModel', function (n,o) {
1223                         if (n && n != o) {
1224                             var bad = false;
1225                             var newdate = new Date(n);
1226                             if (maxDateObj && newdate.getTime() > maxDateObj.getTime()) bad = true;
1227                             if (minDateObj && newdate.getTime() < minDateObj.getTime()) bad = true;
1228                             $scope.outOfRange = bad;
1229                         }
1230                     });
1231                 }
1232             }],
1233             link : function(scope, elm, attrs) {
1234                 if (!scope.closeText)
1235                     scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
1236
1237                 if ('showTimePicker' in attrs)
1238                     scope.showTimePicker = true;
1239
1240                 var default_format = 'mediumDate';
1241                 egCore.org.settings(['format.date']).then(function(set) {
1242                     if (set) default_format = set['format.date'];
1243                     scope.date_format = (scope.dateFormat) ?
1244                         scope.dateFormat :
1245                         default_format;
1246                 });
1247             }
1248         };
1249     }
1250 ])
1251
1252 /*
1253  *  egFmValueSelector - widget for selecting a value from list specified
1254  *                      by IDL class
1255  */
1256 .directive('egFmValueSelector', function() {
1257     return {
1258         restrict : 'E',
1259         transclude : true,
1260         scope : {
1261             idlClass : '@',
1262             ngModel : '=',
1263
1264             // optional filter for refining the set of rows that
1265             // get returned. Example:
1266             //
1267             // filter="{'column':{'=':null}}"
1268             filter : '=',
1269
1270             // optional name of settings key for persisting
1271             // the last selected value
1272             stickySetting : '@',
1273
1274             // optional OU setting for fetching default value;
1275             // used only if sticky setting not set
1276             ouSetting : '@'
1277         },
1278         require: 'ngModel',
1279         templateUrl : './share/t_fm_value_selector',
1280         controller : ['$scope','egCore', function($scope , egCore) {
1281
1282             $scope.org = egCore.org; // for use in the link function
1283             $scope.auth = egCore.auth; // for use in the link function
1284             $scope.hatch = egCore.hatch // for use in the link function
1285
1286             function flatten_linked_values(cls, list) {
1287                 var results = [];
1288                 var fields = egCore.idl.classes[cls].fields;
1289                 var id_field;
1290                 var selector;
1291                 angular.forEach(fields, function(fld) {
1292                     if (fld.datatype == 'id') {
1293                         id_field = fld.name;
1294                         selector = fld.selector ? fld.selector : id_field;
1295                         return;
1296                     }
1297                 });
1298                 angular.forEach(list, function(item) {
1299                     var rec = egCore.idl.toHash(item);
1300                     results.push({
1301                         id : rec[id_field],
1302                         name : rec[selector]
1303                     });
1304                 });
1305                 return results;
1306             }
1307
1308             var search = {};
1309             search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
1310             if ($scope.filter) {
1311                 angular.extend(search, $scope.filter);
1312             }
1313             egCore.pcrud.search(
1314                 $scope.idlClass, search, {}, {atomic : true}
1315             ).then(function(list) {
1316                 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
1317             });
1318
1319             $scope.handleChange = function(value) {
1320                 if ($scope.stickySetting) {
1321                     egCore.hatch.setLocalItem($scope.stickySetting, value);
1322                 }
1323             }
1324
1325         }],
1326         link : function(scope, element, attrs) {
1327             if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1328                 var value = scope.hatch.getLocalItem(scope.stickySetting);
1329                 scope.ngModel = value;
1330             }
1331             if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1332                 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
1333                 .then(function(set) {
1334                     var value = parseInt(set[scope.ouSetting]);
1335                     if (!isNaN(value))
1336                         scope.ngModel = value;
1337                 });
1338             }
1339         }
1340     }
1341 })
1342
1343 /*
1344  *  egShareDepthSelector - widget for selecting a share depth
1345  */
1346 .directive('egShareDepthSelector', function() {
1347     return {
1348         restrict : 'E',
1349         transclude : true,
1350         scope : {
1351             ngModel : '=',
1352         },
1353         require: 'ngModel',
1354         templateUrl : './share/t_share_depth_selector',
1355         controller : ['$scope','egCore', function($scope , egCore) {
1356             $scope.values = [];
1357             egCore.pcrud.search('aout',
1358                 { id : {'!=' : null} },
1359                 { order_by : {aout : ['depth', 'name']} },
1360                 { atomic : true }
1361             ).then(function(list) {
1362                 var scratch = [];
1363                 angular.forEach(list, function(aout) {
1364                     var depth = parseInt(aout.depth());
1365                     if (depth in scratch) {
1366                         scratch[depth].push(aout.name());
1367                     } else {
1368                         scratch[depth] = [ aout.name() ]
1369                     }
1370                 });
1371                 scratch.forEach(function(val, idx) {
1372                     $scope.values.push({ id : idx,  name : scratch[idx].join(' / ') });
1373                 });
1374             });
1375         }]
1376     }
1377 })
1378
1379 /*
1380  * egHelpPopover - a helpful widget
1381  */
1382 .directive('egHelpPopover', function() {
1383     return {
1384         restrict : 'E',
1385         transclude : true,
1386         scope : {
1387             helpText : '@',
1388             helpLink : '@'
1389         },
1390         templateUrl : './share/t_help_popover',
1391         controller : ['$scope','$sce', function($scope , $sce) {
1392             if ($scope.helpLink) {
1393                 $scope.helpHtml = $sce.trustAsHtml(
1394                     '<a target="_new" href="' + $scope.helpLink + '">' +
1395                     $scope.helpText + '</a>'
1396                 );
1397             }
1398         }]
1399     }
1400 })
1401
1402 .factory('egWorkLog', ['egCore', function(egCore) {
1403     var service = {};
1404
1405     service.retrieve_all = function() {
1406         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1407         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1408
1409         return { 'work_log' : workLog, 'patron_log' : patronLog };
1410     }
1411
1412     service.record = function(message,data) {
1413         var max_entries;
1414         var max_patrons;
1415         if (typeof egCore != 'undefined') {
1416             if (typeof egCore.env != 'undefined') {
1417                 if (typeof egCore.env.aous != 'undefined') {
1418                     max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
1419                     max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
1420                 } else {
1421                     console.log('worklog: missing egCore.env.aous');
1422                 }
1423             } else {
1424                 console.log('worklog: missing egCore.env');
1425             }
1426         } else {
1427             console.log('worklog: missing egCore');
1428         }
1429         if (!max_entries) {
1430             if (typeof egCore.org != 'undefined') {
1431                 if (typeof egCore.org.cachedSettings != 'undefined') {
1432                     max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
1433                 } else {
1434                     console.log('worklog: missing egCore.org.cachedSettings');
1435                 }
1436             } else {
1437                 console.log('worklog: missing egCore.org');
1438             }
1439         }
1440         if (!max_patrons) {
1441             if (typeof egCore.org != 'undefined') {
1442                 if (typeof egCore.org.cachedSettings != 'undefined') {
1443                     max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
1444                 } else {
1445                     console.log('worklog: missing egCore.org.cachedSettings');
1446                 }
1447             } else {
1448                 console.log('worklog: missing egCore.org');
1449             }
1450         }
1451         if (!max_entries) {
1452             max_entries = 20;
1453             console.log('worklog: defaulting to max_entries = ' + max_entries);
1454         }
1455         if (!max_patrons) {
1456             max_patrons = 10;
1457             console.log('worklog: defaulting to max_patrons = ' + max_patrons);
1458         }
1459
1460         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1461         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1462         var entry = {
1463             'when' : new Date(),
1464             'msg' : message,
1465             'action' : data.action,
1466             'actor' : egCore.auth.user().usrname()
1467         };
1468         if (data.action == 'checkin') {
1469             entry['item'] = data.response.params.copy_barcode;
1470             entry['item_id'] = data.response.data.acp.id();
1471             if (data.response.data.au) {
1472                 entry['user'] = data.response.data.au.family_name();
1473                 entry['patron_id'] = data.response.data.au.id();
1474             }
1475         }
1476         if (data.action == 'checkout') {
1477             entry['item'] = data.response.params.copy_barcode;
1478             entry['user'] = data.response.data.au.family_name();
1479             entry['item_id'] = data.response.data.acp.id();
1480             entry['patron_id'] = data.response.data.au.id();
1481         }
1482         if (data.action == 'noncat_checkout') {
1483             entry['user'] = data.response.data.au.family_name();
1484             entry['patron_id'] = data.response.data.au.id();
1485         }
1486         if (data.action == 'renew') {
1487             entry['item'] = data.response.params.copy_barcode;
1488             entry['user'] = data.response.data.au.family_name();
1489             entry['item_id'] = data.response.data.acp.id();
1490             entry['patron_id'] = data.response.data.au.id();
1491         }
1492         if (data.action == 'requested_hold'
1493             || data.action == 'edited_patron'
1494             || data.action == 'registered_patron'
1495             || data.action == 'paid_bill') {
1496             entry['patron_id'] = data.patron_id;
1497         }
1498         if (data.action == 'requested_hold') {
1499             entry['hold_id'] = data.hold_id;
1500         }
1501         if (data.action == 'paid_bill') {
1502             entry['amount'] = data.total_amount;
1503         }
1504
1505         workLog.push( entry );
1506         if (workLog.length > max_entries) workLog.shift();
1507         egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
1508
1509         if (entry['patron_id']) {
1510             var temp = [];
1511             for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
1512                 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
1513             }
1514             temp.push( entry );
1515             if (temp.length > max_patrons) temp.shift();
1516             patronLog = temp;
1517             egCore.hatch.setLocalItem('eg.patron_log',patronLog);
1518         }
1519
1520         console.log('worklog',entry);
1521     }
1522
1523     return service;
1524 }]);