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