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