]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/ui.js
LP#1756110: fix egBasicComboBox drop-down functionality
[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 = 'shortDate';
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     }
416
417     // Increment the current value.  If no amount is specified,
418     // it increments by 1.  Calling increment() on an indetermite
419     // progress bar will force it to be a (semi-)determinate bar.
420     service.increment = function(amt) {
421         if (!Number.isInteger(amt)) amt = 1;
422
423         if (!egProgressData.hasvalue())
424             egProgressData.value = 0;
425
426         egProgressData.value += amt;
427     }
428
429     return service;
430 }])
431
432 /**
433  * egAlertDialog.open({message : 'hello {{name}}'}).result.then(
434  *     function() { console.log('alert closed') });
435  */
436 .factory('egAlertDialog', 
437
438         ['$uibModal','$interpolate',
439 function($uibModal , $interpolate) {
440     var service = {};
441
442     service.open = function(message, msg_scope) {
443         return $uibModal.open({
444             templateUrl: './share/t_alert_dialog',
445             backdrop: 'static',
446             controller: ['$scope', '$uibModalInstance',
447                 function($scope, $uibModalInstance) {
448                     $scope.message = $interpolate(message)(msg_scope);
449                     $scope.ok = function() {
450                         if (msg_scope && msg_scope.ok) msg_scope.ok();
451                         $uibModalInstance.close()
452                     }
453                 }
454             ]
455         });
456     }
457
458     return service;
459 }])
460
461 /**
462  * egConfirmDialog.open("some message goes {{here}}", {
463  *  here : 'foo', ok : function() {}, cancel : function() {}},
464  *  'OK', 'Cancel');
465  */
466 .factory('egConfirmDialog', 
467     
468        ['$uibModal','$interpolate',
469 function($uibModal, $interpolate) {
470     var service = {};
471
472     service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
473         msg_scope = msg_scope || {};
474         return $uibModal.open({
475             templateUrl: './share/t_confirm_dialog',
476             backdrop: 'static',
477             controller: ['$scope', '$uibModalInstance',
478                 function($scope, $uibModalInstance) {
479                     $scope.title = $interpolate(title)(msg_scope);
480                     $scope.message = $interpolate(message)(msg_scope);
481                     $scope.ok_button_label = $interpolate(ok_button_label || '')(msg_scope);
482                     $scope.cancel_button_label = $interpolate(cancel_button_label || '')(msg_scope);
483                     $scope.ok = function() {
484                         if (msg_scope.ok) msg_scope.ok();
485                         $uibModalInstance.close()
486                     }
487                     $scope.cancel = function() {
488                         if (msg_scope.cancel) msg_scope.cancel();
489                         $uibModalInstance.dismiss();
490                     }
491                 }
492             ]
493         })
494     }
495
496     return service;
497 }])
498
499 /**
500  * egPromptDialog.open(
501  *    "prompt message goes {{here}}", 
502  *    promptValue,  // optional
503  *    {
504  *      here : 'foo',  
505  *      ok : function(value) {console.log(value)}, 
506  *      cancel : function() {console.log('prompt denied')}
507  *    }
508  *  );
509  */
510 .factory('egPromptDialog', 
511     
512        ['$uibModal','$interpolate',
513 function($uibModal, $interpolate) {
514     var service = {};
515
516     service.open = function(message, promptValue, msg_scope) {
517         return $uibModal.open({
518             templateUrl: './share/t_prompt_dialog',
519             backdrop: 'static',
520             controller: ['$scope', '$uibModalInstance',
521                 function($scope, $uibModalInstance) {
522                     $scope.message = $interpolate(message)(msg_scope);
523                     $scope.args = {value : promptValue || ''};
524                     $scope.focus = true;
525                     $scope.ok = function() {
526                         if (msg_scope && msg_scope.ok) msg_scope.ok($scope.args.value);
527                         $uibModalInstance.close($scope.args);
528                     }
529                     $scope.cancel = function() {
530                         if (msg_scope && msg_scope.cancel) msg_scope.cancel();
531                         $uibModalInstance.dismiss();
532                     }
533                 }
534             ]
535         })
536     }
537
538     return service;
539 }])
540
541 /**
542  * egSelectDialog.open(
543  *    "message goes {{here}}", 
544  *    list,           // ['values','for','dropdown'],
545  *    selectedValue,  // optional
546  *    {
547  *      here : 'foo',
548  *      ok : function(value) {console.log(value)}, 
549  *      cancel : function() {console.log('prompt denied')}
550  *    }
551  *  );
552  */
553 .factory('egSelectDialog', 
554     
555        ['$uibModal','$interpolate',
556 function($uibModal, $interpolate) {
557     var service = {};
558
559     service.open = function(message, inputList, selectedValue, msg_scope) {
560         return $uibModal.open({
561             templateUrl: './share/t_select_dialog',
562             backdrop: 'static',
563             controller: ['$scope', '$uibModalInstance',
564                 function($scope, $uibModalInstance) {
565                     $scope.message = $interpolate(message)(msg_scope);
566                     $scope.args = {
567                         list  : inputList,
568                         value : selectedValue
569                     };
570                     $scope.focus = true;
571                     $scope.ok = function() {
572                         if (msg_scope.ok) msg_scope.ok($scope.args.value);
573                         $uibModalInstance.close()
574                     }
575                     $scope.cancel = function() {
576                         if (msg_scope.cancel) msg_scope.cancel();
577                         $uibModalInstance.dismiss();
578                     }
579                 }
580             ]
581         })
582     }
583
584     return service;
585 }])
586
587 /**
588  * Warn on page unload and give the user a chance to avoid navigating
589  * away from the current page.  
590  * Only one handler is supported per page.
591  * NOTE: we can't use an egUnloadDialog as the dialog builder, because
592  * it renders asynchronously, which allows the page to redirect before
593  * the dialog appears.
594  */
595 .factory('egUnloadPrompt', [
596         '$window','egStrings', 
597 function($window , egStrings) {
598     var service = {attached : false};
599
600     // attach a page/scope unload prompt
601     service.attach = function($scope, msg) {
602         if (service.attached) return;
603         service.attached = true;
604
605         // handle page change
606         $($window).on('beforeunload', function() { 
607             service.clear();
608             return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
609         });
610
611         if (!$scope) return;
612
613         // If a scope was provided, attach a scope-change handler,
614         // similar to the page-page prompt.
615         service.locChangeCancel = 
616             $scope.$on('$locationChangeStart', function(evt, next, current) {
617             if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
618                 // user allowed the page to change.  
619                 // Clear the unload handler.
620                 service.clear();
621             } else {
622                 evt.preventDefault();
623             }
624         });
625     };
626
627     // remove the page unload prompt
628     service.clear = function() {
629         $($window).off('beforeunload');
630         if (service.locChangeCancel)
631             service.locChangeCancel();
632         service.attached = false;
633     }
634
635     return service;
636 }])
637
638 /**
639  * egAddCopyAlertDialog - manage copy alerts
640  */
641 .factory('egAddCopyAlertDialog', 
642        ['$uibModal','$interpolate','egCore',
643 function($uibModal , $interpolate , egCore) {
644     var service = {};
645
646     service.open = function(args) {
647         return $uibModal.open({
648             templateUrl: './share/t_add_copy_alert_dialog',
649             controller: ['$scope','$q','$uibModalInstance',
650                 function( $scope , $q , $uibModalInstance) {
651
652                     $scope.copy_ids = args.copy_ids;
653                     egCore.pcrud.search('ccat',
654                         { active : 't' },
655                         {},
656                         { atomic : true }
657                     ).then(function (ccat) {
658                         $scope.alert_types = ccat;
659                     }); 
660
661                     $scope.copy_alert = {
662                         create_staff : egCore.auth.user().id(),
663                         note         : '',
664                         temp         : false
665                     };
666
667                     $scope.ok = function(copy_alert) {
668                         if (typeof(copy_alert.note) != 'undefined' &&
669                             copy_alert.note != '') {
670                             copy_alerts = [];
671                             angular.forEach($scope.copy_ids, function (cp_id) {
672                                 var a = new egCore.idl.aca();
673                                 a.isnew(1);
674                                 a.create_staff(copy_alert.create_staff);
675                                 a.note(copy_alert.note);
676                                 a.temp(copy_alert.temp ? 't' : 'f');
677                                 a.copy(cp_id);
678                                 a.ack_time(null);
679                                 a.alert_type(
680                                     $scope.alert_types.filter(function(at) {
681                                         return at.id() == copy_alert.alert_type;
682                                     })[0]
683                                 );
684                                 copy_alerts.push( a );
685                             });
686                             if (copy_alerts.length > 0) {
687                                 egCore.pcrud.apply(copy_alerts).finally(function() {
688                                     if (args.ok) args.ok();
689                                     $uibModalInstance.close()
690                                 });
691                             }
692                         } else {
693                             if (args.ok) args.ok();
694                             $uibModalInstance.close()
695                         }
696                     }
697                     $scope.cancel = function() {
698                         if (args.cancel) args.cancel();
699                         $uibModalInstance.dismiss();
700                     }
701                 }
702             ]
703         })
704     }
705
706     return service;
707 }])
708
709 /**
710  * egCopyAlertManagerDialog - manage copy alerts
711  */
712 .factory('egCopyAlertManagerDialog', 
713        ['$uibModal','$interpolate','egCore',
714 function($uibModal , $interpolate , egCore) {
715     var service = {};
716
717     service.get_user_copy_alerts = function(copy_id) {
718         return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
719             { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
720             { atomic : true }
721         );
722     }
723
724     service.open = function(args) {
725         return $uibModal.open({
726             templateUrl: './share/t_copy_alert_manager_dialog',
727             controller: ['$scope','$q','$uibModalInstance',
728                 function( $scope , $q , $uibModalInstance) {
729
730                     function init(args) {
731                         var defer = $q.defer();
732                         if (args.copy_id) {
733                             service.get_user_copy_alerts(args.copy_id).then(function(aca) {
734                                 defer.resolve(aca);
735                             });
736                         } else {
737                             defer.resolve(args.alerts);
738                         }
739                         return defer.promise;
740                     }
741
742                     // returns a promise resolved with the list of circ statuses
743                     $scope.get_copy_statuses = function() {
744                         if (egCore.env.ccs)
745                             return $q.when(egCore.env.ccs.list);
746
747                         return egCore.pcrud.retrieveAll('ccs', null, {atomic : true})
748                         .then(function(list) {
749                             egCore.env.absorbList(list, 'ccs');
750                             return list;
751                         });
752                     };
753
754                     $scope.mode = args.mode || 'checkin';
755
756                     var next_statuses = [];
757                     var seen_statuses = {};
758                     $scope.next_statuses = [];
759                     $scope.params = {
760                         'the_next_status' : null
761                     }
762                     init(args).then(function(copy_alerts) {
763                         $scope.alerts = copy_alerts;
764                         angular.forEach($scope.alerts, function(copy_alert) {
765                             var state = copy_alert.alert_type().state();
766                             copy_alert.evt = copy_alert.alert_type().event();
767
768                             copy_alert.message = copy_alert.note() ||
769                                 egCore.strings.ON_DEMAND_COPY_ALERT[copy_alert.evt][state];
770
771                             if (copy_alert.temp() == 't') {
772                                 angular.forEach(copy_alert.alert_type().next_status(), function (st) {
773                                     if (!seen_statuses[st]) {
774                                         seen_statuses[st] = true;
775                                         next_statuses.push(st);
776                                     }
777                                 });
778                             }
779                         });
780                         if ($scope.mode == 'checkin' && next_statuses.length > 0) {
781                             $scope.get_copy_statuses().then(function() {
782                                 angular.forEach(next_statuses, function(st) {
783                                     if (egCore.env.ccs.map[st])
784                                         $scope.next_statuses.push(egCore.env.ccs.map[st]);
785                                 });
786                                 $scope.params.the_next_status = $scope.next_statuses[0].id();
787                             });
788                         }
789                     });
790
791                     $scope.isAcknowledged = function(copy_alert) {
792                         return (copy_alert.acked);
793                     };
794                     $scope.canBeAcknowledged = function(copy_alert) {
795                         return (!copy_alert.ack_time() && copy_alert.temp() == 't');
796                     };
797                     $scope.canBeRemoved = function(copy_alert) {
798                         return (!copy_alert.ack_time() && copy_alert.temp() == 'f');
799                     };
800
801                     $scope.ok = function() {
802                         var acks = [];
803                         angular.forEach($scope.alerts, function (copy_alert) {
804                             if (copy_alert.acked) {
805                                 copy_alert.ack_time('now');
806                                 copy_alert.ack_staff(egCore.auth.user().id());
807                                 copy_alert.ischanged(true);
808                                 acks.push(copy_alert);
809                             }
810                         });
811                         if (acks.length > 0) {
812                             egCore.pcrud.apply(acks).finally(function() {
813                                 if (args.ok) args.ok($scope.params.the_next_status);
814                                 $uibModalInstance.close()
815                             });
816                         } else {
817                             if (args.ok) args.ok($scope.params.the_next_status);
818                             $uibModalInstance.close()
819                         }
820                     }
821                     $scope.cancel = function() {
822                         if (args.cancel) args.cancel();
823                         $uibModalInstance.dismiss();
824                     }
825                 }
826             ]
827         })
828     }
829
830     return service;
831 }])
832
833 /**
834  * egCopyAlertEditorDialog - manage copy alerts
835  */
836 .factory('egCopyAlertEditorDialog', 
837        ['$uibModal','$interpolate','egCore',
838 function($uibModal , $interpolate , egCore) {
839     var service = {};
840
841     service.get_user_copy_alerts = function(copy_id) {
842         return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
843             { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
844             { atomic : true }
845         );
846     }
847
848     service.get_copy_alert_types = function() {
849         return egCore.pcrud.search('ccat',
850             { active : 't' },
851             {},
852             { atomic : true }
853         );
854     };
855
856     service.open = function(args) {
857         return $uibModal.open({
858             templateUrl: './share/t_copy_alert_editor_dialog',
859             controller: ['$scope','$q','$uibModalInstance',
860                 function( $scope , $q , $uibModalInstance) {
861
862                     function init(args) {
863                         var defer = $q.defer();
864                         if (args.copy_id) {
865                             service.get_user_copy_alerts(args.copy_id).then(function(aca) {
866                                 defer.resolve(aca);
867                             });
868                         } else {
869                             defer.resolve(args.alerts);
870                         }
871                         return defer.promise;
872                     }
873
874                     init(args).then(function(copy_alerts) {
875                         $scope.copy_alert_list = copy_alerts;
876                     });
877                     service.get_copy_alert_types().then(function(ccat) {
878                         $scope.alert_types = ccat;
879                     });
880
881                     $scope.ok = function() {
882                         egCore.pcrud.apply($scope.copy_alert_list).finally(function() {
883                             $uibModalInstance.close();
884                         });
885                     }
886                     $scope.cancel = function() {
887                         if (args.cancel) args.cancel();
888                         $uibModalInstance.dismiss();
889                     }
890                 }
891             ]
892         })
893     }
894
895     return service;
896 }])
897 .directive('aDisabled', function() {
898     return {
899         restrict : 'A',
900         compile: function(tElement, tAttrs, transclude) {
901             //Disable ngClick
902             tAttrs["ngClick"] = ("ng-click", "!("+tAttrs["aDisabled"]+") && ("+tAttrs["ngClick"]+")");
903
904             //Toggle "disabled" to class when aDisabled becomes true
905             return function (scope, iElement, iAttrs) {
906                 scope.$watch(iAttrs["aDisabled"], function(newValue) {
907                     if (newValue !== undefined) {
908                         iElement.toggleClass("disabled", newValue);
909                     }
910                 });
911
912                 //Disable href on click
913                 iElement.on("click", function(e) {
914                     if (scope.$eval(iAttrs["aDisabled"])) {
915                         e.preventDefault();
916                     }
917                 });
918             };
919         }
920     };
921 })
922
923 .directive('egBasicComboBox', function() {
924     return {
925         restrict: 'E',
926         replace: true,
927         scope: {
928             list: "=", // list of strings
929             selected: "=",
930             onSelect: "=",
931             egDisabled: "=",
932             allowAll: "@",
933             placeholder: "@",
934             focusMe: "=?"
935         },
936         template:
937             '<div class="input-group">'+
938                 '<input placeholder="{{placeholder}}" type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()" focus-me="focusMe">'+
939                 '<div class="input-group-btn" uib-dropdown ng-class="{open:isopen}">'+
940                     '<button type="button" ng-click="showAll()" ng-disabled="egDisabled" class="btn btn-default" uib-dropdown-toggle><span class="caret"></span></button>'+
941                     '<ul uib-dropdown-menu class="dropdown-menu-right">'+
942                         '<li ng-repeat="item in list|filter:selected:compare"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
943                         '<li ng-if="complete_list" class="divider"><span></span></li>'+
944                         '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
945                     '</ul>'+
946                 '</div>'+
947             '</div>',
948         controller: ['$scope','$filter',
949             function( $scope , $filter) {
950
951                 $scope.complete_list = false;
952                 $scope.isopen = false;
953                 $scope.clickedopen = false;
954                 $scope.clickedclosed = null;
955
956                 $scope.compare = function (ex, act) {
957                     if (act === null || act === undefined) return true;
958                     if (act.toString) act = act.toString();
959                     return new RegExp(act.toLowerCase()).test(ex)
960                 }
961
962                 $scope.showAll = function () {
963
964                     $scope.clickedopen = !$scope.clickedopen;
965
966                     if ($scope.clickedclosed === null) {
967                         if (!$scope.clickedopen) {
968                             $scope.clickedclosed = true;
969                         }
970                     } else {
971                         $scope.clickedclosed = !$scope.clickedopen;
972                     }
973
974                     if ($scope.selected && $scope.selected.length > 0) $scope.complete_list = true;
975                     if (!$scope.selected || $scope.selected.length == 0) $scope.complete_list = false;
976                     $scope.makeOpen();
977                 }
978
979                 $scope.makeOpen = function () {
980                     $scope.isopen = $scope.clickedopen || ($filter('filter')(
981                         $scope.list,
982                         $scope.selected
983                     ).length > 0 && $scope.selected.length > 0);
984                     if ($scope.clickedclosed) {
985                         $scope.isopen = false;
986                         $scope.clickedclosed = null;
987                     }
988                 }
989
990                 $scope.changeValue = function (newVal) {
991                     $scope.selected = newVal;
992                     $scope.isopen = false;
993                     $scope.clickedclosed = null;
994                     $scope.clickedopen = false;
995                     if ($scope.selected.length == 0) $scope.complete_list = false;
996                     if ($scope.onSelect) $scope.onSelect();
997                 }
998
999             }
1000         ]
1001     };
1002 })
1003
1004 /**
1005  * Nested org unit selector modeled as a Bootstrap dropdown button.
1006  */
1007 .directive('egOrgSelector', function() {
1008     return {
1009         restrict : 'AE',
1010         transclude : true,
1011         replace : true, // makes styling easier
1012         scope : {
1013             selected : '=', // defaults to workstation or root org,
1014                             // unless the nodefault attibute exists
1015
1016             // Each org unit is passed into this function and, for
1017             // any org units where the response value is true, the
1018             // org unit will not be added to the selector.
1019             hiddenTest : '=',
1020
1021             // Each org unit is passed into this function and, for
1022             // any org units where the response value is true, the
1023             // org unit will not be available for selection.
1024             disableTest : '=',
1025
1026             // if set to true, disable the UI element altogether
1027             alldisabled : '@',
1028
1029             // Caller can either $watch(selected, ..) or register an
1030             // onchange handler.
1031             onchange : '=',
1032
1033             // optional primary drop-down button label
1034             label : '@',
1035
1036             // optional name of settings key for persisting
1037             // the last selected org unit
1038             stickySetting : '@'
1039         },
1040
1041         // any reason to move this into a TT2 template?
1042         template : 
1043             '<div class="btn-group eg-org-selector" uib-dropdown>'
1044             + '<button type="button" class="btn btn-default" uib-dropdown-toggle ng-disabled="disable_button">'
1045              + '<span style="padding-right: 5px;">{{getSelectedName()}}</span>'
1046              + '<span class="caret"></span>'
1047            + '</button>'
1048            + '<ul uib-dropdown-menu class="scrollable-menu">'
1049              + '<li ng-repeat="org in orgList" ng-hide="hiddenTest(org.id)">'
1050                + '<a href ng-click="orgChanged(org)" a-disabled="disableTest(org.id)" '
1051                  + 'style="padding-left: {{org.depth * 10 + 5}}px">'
1052                  + '{{org.shortname}}'
1053                + '</a>'
1054              + '</li>'
1055            + '</ul>'
1056           + '</div>',
1057
1058         controller : ['$scope','$timeout','egCore','egStartup','egLovefield','$q',
1059               function($scope , $timeout , egCore , egStartup , egLovefield , $q) {
1060
1061             if ($scope.alldisabled) {
1062                 $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
1063             } else {
1064                 $scope.disable_button = false;
1065             }
1066
1067             // avoid linking the full fleshed tree to the scope by 
1068             // tossing in a flattened list.
1069             // --
1070             // Run-time code referencing post-start data should be run
1071             // from within a startup block, otherwise accessing this
1072             // module before startup completes will lead to failure.
1073             //
1074             // controller() runs before link().
1075             // This post-startup code runs after link().
1076             egStartup.go(
1077             ).then(
1078                 function() {
1079                     return egCore.env.classLoaders.aou();
1080                 }
1081             ).then(
1082                 function() {
1083
1084                     $scope.orgList = egCore.org.list().map(function(org) {
1085                         return {
1086                             id : org.id(),
1087                             shortname : org.shortname(), 
1088                             depth : org.ou_type().depth()
1089                         }
1090                     });
1091                     
1092     
1093                     // Apply default values
1094     
1095                     if ($scope.stickySetting) {
1096                         var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
1097                         if (orgId) {
1098                             $scope.selected = egCore.org.get(orgId);
1099                         }
1100                     }
1101     
1102                     if (!$scope.selected && !$scope.nodefault && egCore.auth.user()) {
1103                         $scope.selected = 
1104                             egCore.org.get(egCore.auth.user().ws_ou());
1105                     }
1106     
1107                     fire_orgsel_onchange(); // no-op if nothing is selected
1108                 }
1109             );
1110
1111             /**
1112              * Fire onchange handler after a timeout, so the
1113              * $scope.selected value has a chance to propagate to
1114              * the page controllers before the onchange fires.  This
1115              * way, the caller does not have to manually capture the
1116              * $scope.selected value during onchange.
1117              */
1118             function fire_orgsel_onchange() {
1119                 if (!$scope.selected || !$scope.onchange) return;
1120                 $timeout(function() {
1121                     console.debug(
1122                         'egOrgSelector onchange('+$scope.selected.id()+')');
1123                     $scope.onchange($scope.selected)
1124                 });
1125             }
1126
1127             $scope.getSelectedName = function() {
1128                 if ($scope.selected && $scope.selected.shortname)
1129                     return $scope.selected.shortname();
1130                 return $scope.label;
1131             }
1132
1133             $scope.orgChanged = function(org) {
1134                 $scope.selected = egCore.org.get(org.id);
1135                 if ($scope.stickySetting) {
1136                     egCore.hatch.setLocalItem($scope.stickySetting, org.id);
1137                 }
1138                 fire_orgsel_onchange();
1139             }
1140
1141         }],
1142         link : function(scope, element, attrs, egGridCtrl) {
1143
1144             // boolean fields are presented as value-less attributes
1145             angular.forEach(
1146                 ['nodefault'],
1147                 function(field) {
1148                     if (angular.isDefined(attrs[field]))
1149                         scope[field] = true;
1150                     else
1151                         scope[field] = false;
1152                 }
1153             );
1154         }
1155     }
1156 })
1157
1158 .directive('nextOnEnter', function () {
1159     return function (scope, element, attrs) {
1160         element.bind("keydown keypress", function (event) {
1161             if(event.which === 13) {
1162                 $('#'+attrs.nextOnEnter).focus();
1163                 event.preventDefault();
1164             }
1165         });
1166     };
1167 })
1168
1169 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
1170 .directive('egEnter', function () {
1171     return function (scope, element, attrs) {
1172         element.bind("keydown keypress", function (event) {
1173             if(event.which === 13) {
1174                 scope.$apply(function (){
1175                     scope.$eval(attrs.egEnter);
1176                 });
1177  
1178                 event.preventDefault();
1179             }
1180         });
1181     };
1182 })
1183
1184 /*
1185 * Handy wrapper directive for uib-datapicker-popup
1186 */
1187 .directive(
1188     'egDateInput', ['egStrings', 'egCore',
1189     function(egStrings, egCore) {
1190         return {
1191             scope : {
1192                 id : '@',
1193                 closeText : '@',
1194                 ngModel : '=',
1195                 ngChange : '=',
1196                 ngBlur : '=',
1197                 minDate : '=?',
1198                 maxDate : '=?',
1199                 ngDisabled : '=',
1200                 ngRequired : '=',
1201                 hideDatePicker : '=',
1202                 dateFormat : '=?',
1203                 outOfRange : '=?',
1204                 focusMe : '=?'
1205             },
1206             require: 'ngModel',
1207             templateUrl: './share/t_datetime',
1208             replace: true,
1209             controller : ['$scope', function($scope) {
1210                 $scope.options = {
1211                     minDate : $scope.minDate,
1212                     maxDate : $scope.maxDate
1213                 };
1214
1215                 var maxDateObj = $scope.maxDate ? new Date($scope.maxDate) : null;
1216                 var minDateObj = $scope.minDate ? new Date($scope.minDate) : null;
1217
1218                 if ($scope.outOfRange !== undefined && (maxDateObj || minDateObj)) {
1219                     $scope.$watch('ngModel', function (n,o) {
1220                         if (n && n != o) {
1221                             var bad = false;
1222                             var newdate = new Date(n);
1223                             if (maxDateObj && newdate.getTime() > maxDateObj.getTime()) bad = true;
1224                             if (minDateObj && newdate.getTime() < minDateObj.getTime()) bad = true;
1225                             $scope.outOfRange = bad;
1226                         }
1227                     });
1228                 }
1229             }],
1230             link : function(scope, elm, attrs) {
1231                 if (!scope.closeText)
1232                     scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
1233
1234                 if ('showTimePicker' in attrs)
1235                     scope.showTimePicker = true;
1236
1237                 var default_format = 'mediumDate';
1238                 egCore.org.settings(['format.date']).then(function(set) {
1239                     default_format = set['format.date'];
1240                     scope.date_format = (scope.dateFormat) ?
1241                         scope.dateFormat :
1242                         default_format;
1243                 });
1244             }
1245         };
1246     }
1247 ])
1248
1249 /*
1250  *  egFmValueSelector - widget for selecting a value from list specified
1251  *                      by IDL class
1252  */
1253 .directive('egFmValueSelector', function() {
1254     return {
1255         restrict : 'E',
1256         transclude : true,
1257         scope : {
1258             idlClass : '@',
1259             ngModel : '=',
1260
1261             // optional filter for refining the set of rows that
1262             // get returned. Example:
1263             //
1264             // filter="{'column':{'=':null}}"
1265             filter : '=',
1266
1267             // optional name of settings key for persisting
1268             // the last selected value
1269             stickySetting : '@',
1270
1271             // optional OU setting for fetching default value;
1272             // used only if sticky setting not set
1273             ouSetting : '@'
1274         },
1275         require: 'ngModel',
1276         templateUrl : './share/t_fm_value_selector',
1277         controller : ['$scope','egCore', function($scope , egCore) {
1278
1279             $scope.org = egCore.org; // for use in the link function
1280             $scope.auth = egCore.auth; // for use in the link function
1281             $scope.hatch = egCore.hatch // for use in the link function
1282
1283             function flatten_linked_values(cls, list) {
1284                 var results = [];
1285                 var fields = egCore.idl.classes[cls].fields;
1286                 var id_field;
1287                 var selector;
1288                 angular.forEach(fields, function(fld) {
1289                     if (fld.datatype == 'id') {
1290                         id_field = fld.name;
1291                         selector = fld.selector ? fld.selector : id_field;
1292                         return;
1293                     }
1294                 });
1295                 angular.forEach(list, function(item) {
1296                     var rec = egCore.idl.toHash(item);
1297                     results.push({
1298                         id : rec[id_field],
1299                         name : rec[selector]
1300                     });
1301                 });
1302                 return results;
1303             }
1304
1305             var search = {};
1306             search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
1307             if ($scope.filter) {
1308                 angular.extend(search, $scope.filter);
1309             }
1310             egCore.pcrud.search(
1311                 $scope.idlClass, search, {}, {atomic : true}
1312             ).then(function(list) {
1313                 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
1314             });
1315
1316             $scope.handleChange = function(value) {
1317                 if ($scope.stickySetting) {
1318                     egCore.hatch.setLocalItem($scope.stickySetting, value);
1319                 }
1320             }
1321
1322         }],
1323         link : function(scope, element, attrs) {
1324             if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1325                 var value = scope.hatch.getLocalItem(scope.stickySetting);
1326                 scope.ngModel = value;
1327             }
1328             if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
1329                 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
1330                 .then(function(set) {
1331                     var value = parseInt(set[scope.ouSetting]);
1332                     if (!isNaN(value))
1333                         scope.ngModel = value;
1334                 });
1335             }
1336         }
1337     }
1338 })
1339
1340 /*
1341  *  egShareDepthSelector - widget for selecting a share depth
1342  */
1343 .directive('egShareDepthSelector', function() {
1344     return {
1345         restrict : 'E',
1346         transclude : true,
1347         scope : {
1348             ngModel : '=',
1349         },
1350         require: 'ngModel',
1351         templateUrl : './share/t_share_depth_selector',
1352         controller : ['$scope','egCore', function($scope , egCore) {
1353             $scope.values = [];
1354             egCore.pcrud.search('aout',
1355                 { id : {'!=' : null} },
1356                 { order_by : {aout : ['depth', 'name']} },
1357                 { atomic : true }
1358             ).then(function(list) {
1359                 var scratch = [];
1360                 angular.forEach(list, function(aout) {
1361                     var depth = parseInt(aout.depth());
1362                     if (depth in scratch) {
1363                         scratch[depth].push(aout.name());
1364                     } else {
1365                         scratch[depth] = [ aout.name() ]
1366                     }
1367                 });
1368                 scratch.forEach(function(val, idx) {
1369                     $scope.values.push({ id : idx,  name : scratch[idx].join(' / ') });
1370                 });
1371             });
1372         }]
1373     }
1374 })
1375
1376 /*
1377  * egHelpPopover - a helpful widget
1378  */
1379 .directive('egHelpPopover', function() {
1380     return {
1381         restrict : 'E',
1382         transclude : true,
1383         scope : {
1384             helpText : '@',
1385             helpLink : '@'
1386         },
1387         templateUrl : './share/t_help_popover',
1388         controller : ['$scope','$sce', function($scope , $sce) {
1389             if ($scope.helpLink) {
1390                 $scope.helpHtml = $sce.trustAsHtml(
1391                     '<a target="_new" href="' + $scope.helpLink + '">' +
1392                     $scope.helpText + '</a>'
1393                 );
1394             }
1395         }]
1396     }
1397 })
1398
1399 .factory('egWorkLog', ['egCore', function(egCore) {
1400     var service = {};
1401
1402     service.retrieve_all = function() {
1403         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1404         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1405
1406         return { 'work_log' : workLog, 'patron_log' : patronLog };
1407     }
1408
1409     service.record = function(message,data) {
1410         var max_entries;
1411         var max_patrons;
1412         if (typeof egCore != 'undefined') {
1413             if (typeof egCore.env != 'undefined') {
1414                 if (typeof egCore.env.aous != 'undefined') {
1415                     max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
1416                     max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
1417                 } else {
1418                     console.log('worklog: missing egCore.env.aous');
1419                 }
1420             } else {
1421                 console.log('worklog: missing egCore.env');
1422             }
1423         } else {
1424             console.log('worklog: missing egCore');
1425         }
1426         if (!max_entries) {
1427             if (typeof egCore.org != 'undefined') {
1428                 if (typeof egCore.org.cachedSettings != 'undefined') {
1429                     max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
1430                 } else {
1431                     console.log('worklog: missing egCore.org.cachedSettings');
1432                 }
1433             } else {
1434                 console.log('worklog: missing egCore.org');
1435             }
1436         }
1437         if (!max_patrons) {
1438             if (typeof egCore.org != 'undefined') {
1439                 if (typeof egCore.org.cachedSettings != 'undefined') {
1440                     max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
1441                 } else {
1442                     console.log('worklog: missing egCore.org.cachedSettings');
1443                 }
1444             } else {
1445                 console.log('worklog: missing egCore.org');
1446             }
1447         }
1448         if (!max_entries) {
1449             max_entries = 20;
1450             console.log('worklog: defaulting to max_entries = ' + max_entries);
1451         }
1452         if (!max_patrons) {
1453             max_patrons = 10;
1454             console.log('worklog: defaulting to max_patrons = ' + max_patrons);
1455         }
1456
1457         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1458         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1459         var entry = {
1460             'when' : new Date(),
1461             'msg' : message,
1462             'action' : data.action,
1463             'actor' : egCore.auth.user().usrname()
1464         };
1465         if (data.action == 'checkin') {
1466             entry['item'] = data.response.params.copy_barcode;
1467             entry['item_id'] = data.response.data.acp.id();
1468             if (data.response.data.au) {
1469                 entry['user'] = data.response.data.au.family_name();
1470                 entry['patron_id'] = data.response.data.au.id();
1471             }
1472         }
1473         if (data.action == 'checkout') {
1474             entry['item'] = data.response.params.copy_barcode;
1475             entry['user'] = data.response.data.au.family_name();
1476             entry['item_id'] = data.response.data.acp.id();
1477             entry['patron_id'] = data.response.data.au.id();
1478         }
1479         if (data.action == 'noncat_checkout') {
1480             entry['user'] = data.response.data.au.family_name();
1481             entry['patron_id'] = data.response.data.au.id();
1482         }
1483         if (data.action == 'renew') {
1484             entry['item'] = data.response.params.copy_barcode;
1485             entry['user'] = data.response.data.au.family_name();
1486             entry['item_id'] = data.response.data.acp.id();
1487             entry['patron_id'] = data.response.data.au.id();
1488         }
1489         if (data.action == 'requested_hold'
1490             || data.action == 'edited_patron'
1491             || data.action == 'registered_patron'
1492             || data.action == 'paid_bill') {
1493             entry['patron_id'] = data.patron_id;
1494         }
1495         if (data.action == 'requested_hold') {
1496             entry['hold_id'] = data.hold_id;
1497         }
1498         if (data.action == 'paid_bill') {
1499             entry['amount'] = data.total_amount;
1500         }
1501
1502         workLog.push( entry );
1503         if (workLog.length > max_entries) workLog.shift();
1504         egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
1505
1506         if (entry['patron_id']) {
1507             var temp = [];
1508             for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
1509                 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
1510             }
1511             temp.push( entry );
1512             if (temp.length > max_patrons) temp.shift();
1513             patronLog = temp;
1514             egCore.hatch.setLocalItem('eg.patron_log',patronLog);
1515         }
1516
1517         console.log('worklog',entry);
1518     }
1519
1520     return service;
1521 }]);