]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/ui.js
LP#1642378 Webstaff org selector misc. repairs
[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                 scope.$apply(model.assign(scope, false));
23             })
24         }
25     };
26 }])
27
28 /**
29  * <input blur-me="pleaseBlurMe"/>
30  * $scope.pleaseBlurMe = true
31  * Useful for de-focusing when no other obvious focus target exists
32  */
33 .directive('blurMe', 
34        ['$timeout','$parse', 
35 function($timeout , $parse) {
36     return {
37         link: function(scope, element, attrs) {
38             var model = $parse(attrs.blurMe);
39             scope.$watch(model, function(value) {
40                 if(value === true) 
41                     $timeout(function() {element[0].blur()});
42             });
43             element.bind('focus', function() {
44                 scope.$apply(model.assign(scope, false));
45             })
46         }
47     };
48 }])
49
50
51 // <input select-me="iWantToBeSelected"/>
52 // $scope.iWantToBeSelected = true;
53 .directive('selectMe', 
54        ['$timeout','$parse', 
55 function($timeout , $parse) {
56     return {
57         link: function(scope, element, attrs) {
58             var model = $parse(attrs.selectMe);
59             scope.$watch(model, function(value) {
60                 if(value === true) 
61                     $timeout(function() {element[0].select()});
62             });
63             element.bind('blur', function() {
64                 scope.$apply(model.assign(scope, false));
65             })
66         }
67     };
68 }])
69
70
71 // 'reverse' filter 
72 // <div ng-repeat="item in items | reverse">{{item.name}}</div>
73 // http://stackoverflow.com/questions/15266671/angular-ng-repeat-in-reverse
74 // TODO: perhaps this should live elsewhere
75 .filter('reverse', function() {
76     return function(items) {
77         return items.slice().reverse();
78     };
79 })
80
81 /**
82  * Progress Dialog. 
83  *
84  * egProgressDialog.open();
85  * egProgressDialog.open({value : 0});
86  * egProgressDialog.open({value : 0, max : 123});
87  * egProgressDialog.increment();
88  * egProgressDialog.increment();
89  * egProgressDialog.close();
90  *
91  * Each dialog has 2 numbers, 'max' and 'value'.
92  * The content of these values determines how the dialog displays.  
93  *
94  * There are 3 flavors:
95  *
96  * -- value is set, max is set
97  * determinate: shows a progression with a percent complete.
98  *
99  * -- value is set, max is unset
100  * semi-determinate, with a value report.  Shows a value-less
101  * <progress/>, but shows the value as a number in the dialog.
102  *
103  * This is useful in cases where the total number of items to retrieve
104  * from the server is unknown, but we know how many items we've
105  * retrieved thus far.  It helps to reinforce that something specific
106  * is happening, but we don't know when it will end.
107  *
108  * -- value is unset
109  * indeterminate: shows a generic value-less <progress/> with no 
110  * clear indication of progress.
111  *
112  * Only 1 egProgressDialog instance will be activate at a time.
113  * Each invocation of .open() destroys any existing instance.
114  */
115
116 /* Simple storage class for egProgressDialog data maintenance.
117  * This data lives outside of egProgressDialog so it can be 
118  * directly imported into egProgressDialog's $uibModalInstance.
119  */
120 .factory('egProgressData', [
121     function() {
122         var service = {}; // max/value initially unset
123
124         service.reset = function() {
125             delete service.max;
126             delete service.value;
127         }
128
129         service.hasvalue = function() {
130             return Number.isInteger(service.value);
131         }
132
133         service.hasmax = function() {
134             return Number.isInteger(service.max);
135         }
136
137         service.percent = function() {
138             if (service.hasvalue()  && 
139                 service.hasmax()    && 
140                 service.max > 0     &&
141                 service.value <= service.max)
142                 return Math.floor((service.value / service.max) * 100);
143             return 100;
144         }
145
146         return service;
147     }
148 ])
149
150 .factory('egProgressDialog', [
151             'egProgressData','$uibModal', 
152     function(egProgressData , $uibModal) {
153     var service = {};
154
155     service.open = function(args) {
156         service.close(); // force-kill existing instances.
157
158         // Reset to an indeterminate progress bar, 
159         // overlay with caller values.
160         egProgressData.reset();
161         service.update(angular.extend({}, args));
162
163         return $uibModal.open({
164             templateUrl: './share/t_progress_dialog',
165             controller: ['$scope','$uibModalInstance','egProgressData',
166                 function( $scope , $uibModalInstance , egProgressData) {
167                   service.currentInstance = $uibModalInstance;
168                   $scope.data = egProgressData; // tiny service
169                 }
170             ]
171         });
172     };
173
174     service.close = function() {
175         if (service.currentInstance) {
176             service.currentInstance.close();
177             delete service.currentInstance;
178         }
179     }
180
181     // Set the current state of the progress bar.
182     service.update = function(args) {
183         if (args.max != undefined) 
184             egProgressData.max = args.max;
185         if (args.value != undefined) 
186             egProgressData.value = args.value;
187     }
188
189     // Increment the current value.  If no amount is specified,
190     // it increments by 1.  Calling increment() on an indetermite
191     // progress bar will force it to be a (semi-)determinate bar.
192     service.increment = function(amt) {
193         if (!Number.isInteger(amt)) amt = 1;
194
195         if (!egProgressData.hasvalue())
196             egProgressData.value = 0;
197
198         egProgressData.value += amt;
199     }
200
201     return service;
202 }])
203
204 /**
205  * egAlertDialog.open({message : 'hello {{name}}'}).result.then(
206  *     function() { console.log('alert closed') });
207  */
208 .factory('egAlertDialog', 
209
210         ['$uibModal','$interpolate',
211 function($uibModal , $interpolate) {
212     var service = {};
213
214     service.open = function(message, msg_scope) {
215         return $uibModal.open({
216             templateUrl: './share/t_alert_dialog',
217             controller: ['$scope', '$uibModalInstance',
218                 function($scope, $uibModalInstance) {
219                     $scope.message = $interpolate(message)(msg_scope);
220                     $scope.ok = function() {
221                         if (msg_scope && msg_scope.ok) msg_scope.ok();
222                         $uibModalInstance.close()
223                     }
224                 }
225             ]
226         });
227     }
228
229     return service;
230 }])
231
232 /**
233  * egConfirmDialog.open("some message goes {{here}}", {
234  *  here : 'foo', ok : function() {}, cancel : function() {}},
235  *  'OK', 'Cancel');
236  */
237 .factory('egConfirmDialog', 
238     
239        ['$uibModal','$interpolate',
240 function($uibModal, $interpolate) {
241     var service = {};
242
243     service.open = function(title, message, msg_scope, ok_button_label, cancel_button_label) {
244         return $uibModal.open({
245             templateUrl: './share/t_confirm_dialog',
246             controller: ['$scope', '$uibModalInstance',
247                 function($scope, $uibModalInstance) {
248                     $scope.title = $interpolate(title)(msg_scope);
249                     $scope.message = $interpolate(message)(msg_scope);
250                     $scope.ok_button_label = $interpolate(ok_button_label || '')(msg_scope);
251                     $scope.cancel_button_label = $interpolate(cancel_button_label || '')(msg_scope);
252                     $scope.ok = function() {
253                         if (msg_scope.ok) msg_scope.ok();
254                         $uibModalInstance.close()
255                     }
256                     $scope.cancel = function() {
257                         if (msg_scope.cancel) msg_scope.cancel();
258                         $uibModalInstance.dismiss();
259                     }
260                 }
261             ]
262         })
263     }
264
265     return service;
266 }])
267
268 /**
269  * egPromptDialog.open(
270  *    "prompt message goes {{here}}", 
271  *    promptValue,  // optional
272  *    {
273  *      here : 'foo',  
274  *      ok : function(value) {console.log(value)}, 
275  *      cancel : function() {console.log('prompt denied')}
276  *    }
277  *  );
278  */
279 .factory('egPromptDialog', 
280     
281        ['$uibModal','$interpolate',
282 function($uibModal, $interpolate) {
283     var service = {};
284
285     service.open = function(message, promptValue, msg_scope) {
286         return $uibModal.open({
287             templateUrl: './share/t_prompt_dialog',
288             controller: ['$scope', '$uibModalInstance',
289                 function($scope, $uibModalInstance) {
290                     $scope.message = $interpolate(message)(msg_scope);
291                     $scope.args = {value : promptValue || ''};
292                     $scope.focus = true;
293                     $scope.ok = function() {
294                         if (msg_scope.ok) msg_scope.ok($scope.args.value);
295                         $uibModalInstance.close()
296                     }
297                     $scope.cancel = function() {
298                         if (msg_scope.cancel) msg_scope.cancel();
299                         $uibModalInstance.dismiss();
300                     }
301                 }
302             ]
303         })
304     }
305
306     return service;
307 }])
308
309 /**
310  * egSelectDialog.open(
311  *    "message goes {{here}}", 
312  *    list,           // ['values','for','dropdown'],
313  *    selectedValue,  // optional
314  *    {
315  *      here : 'foo',
316  *      ok : function(value) {console.log(value)}, 
317  *      cancel : function() {console.log('prompt denied')}
318  *    }
319  *  );
320  */
321 .factory('egSelectDialog', 
322     
323        ['$uibModal','$interpolate',
324 function($uibModal, $interpolate) {
325     var service = {};
326
327     service.open = function(message, inputList, selectedValue, msg_scope) {
328         return $uibModal.open({
329             templateUrl: './share/t_select_dialog',
330             controller: ['$scope', '$uibModalInstance',
331                 function($scope, $uibModalInstance) {
332                     $scope.message = $interpolate(message)(msg_scope);
333                     $scope.args = {
334                         list  : inputList,
335                         value : selectedValue
336                     };
337                     $scope.focus = true;
338                     $scope.ok = function() {
339                         if (msg_scope.ok) msg_scope.ok($scope.args.value);
340                         $uibModalInstance.close()
341                     }
342                     $scope.cancel = function() {
343                         if (msg_scope.cancel) msg_scope.cancel();
344                         $uibModalInstance.dismiss();
345                     }
346                 }
347             ]
348         })
349     }
350
351     return service;
352 }])
353
354 /**
355  * Warn on page unload and give the user a chance to avoid navigating
356  * away from the current page.  
357  * Only one handler is supported per page.
358  * NOTE: we can't use an egUnloadDialog as the dialog builder, because
359  * it renders asynchronously, which allows the page to redirect before
360  * the dialog appears.
361  */
362 .factory('egUnloadPrompt', [
363         '$window','egStrings', 
364 function($window , egStrings) {
365     var service = {attached : false};
366
367     // attach a page/scope unload prompt
368     service.attach = function($scope, msg) {
369         if (service.attached) return;
370         service.attached = true;
371
372         // handle page change
373         $($window).on('beforeunload', function() { 
374             service.clear();
375             return msg || egStrings.EG_UNLOAD_PAGE_PROMPT_MSG;
376         });
377
378         if (!$scope) return;
379
380         // If a scope was provided, attach a scope-change handler,
381         // similar to the page-page prompt.
382         service.locChangeCancel = 
383             $scope.$on('$locationChangeStart', function(evt, next, current) {
384             if (confirm(msg || egStrings.EG_UNLOAD_CTRL_PROMPT_MSG)) {
385                 // user allowed the page to change.  
386                 // Clear the unload handler.
387                 service.clear();
388             } else {
389                 evt.preventDefault();
390             }
391         });
392     };
393
394     // remove the page unload prompt
395     service.clear = function() {
396         $($window).off('beforeunload');
397         if (service.locChangeCancel)
398             service.locChangeCancel();
399         service.attached = false;
400     }
401
402     return service;
403 }])
404
405 .directive('aDisabled', function() {
406     return {
407         restrict : 'A',
408         compile: function(tElement, tAttrs, transclude) {
409             //Disable ngClick
410             tAttrs["ngClick"] = ("ng-click", "!("+tAttrs["aDisabled"]+") && ("+tAttrs["ngClick"]+")");
411
412             //Toggle "disabled" to class when aDisabled becomes true
413             return function (scope, iElement, iAttrs) {
414                 scope.$watch(iAttrs["aDisabled"], function(newValue) {
415                     if (newValue !== undefined) {
416                         iElement.toggleClass("disabled", newValue);
417                     }
418                 });
419
420                 //Disable href on click
421                 iElement.on("click", function(e) {
422                     if (scope.$eval(iAttrs["aDisabled"])) {
423                         e.preventDefault();
424                     }
425                 });
426             };
427         }
428     };
429 })
430
431 .directive('egBasicComboBox', function() {
432     return {
433         restrict: 'E',
434         replace: true,
435         scope: {
436             list: "=", // list of strings
437             selected: "=",
438             egDisabled: "=",
439             allowAll: "@",
440         },
441         template:
442             '<div class="input-group">'+
443                 '<input type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()">'+
444                 '<div class="input-group-btn" dropdown ng-class="{open:isopen}">'+
445                     '<button type="button" ng-click="showAll()" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
446                     '<ul class="dropdown-menu dropdown-menu-right">'+
447                         '<li ng-repeat="item in list|filter:selected"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
448                         '<li ng-if="complete_list" class="divider"><span></span></li>'+
449                         '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
450                     '</ul>'+
451                 '</div>'+
452             '</div>',
453         controller: ['$scope','$filter',
454             function( $scope , $filter) {
455
456                 $scope.complete_list = false;
457                 $scope.isopen = false;
458                 $scope.clickedopen = false;
459                 $scope.clickedclosed = null;
460
461                 $scope.showAll = function () {
462
463                     $scope.clickedopen = !$scope.clickedopen;
464
465                     if ($scope.clickedclosed === null) {
466                         if (!$scope.clickedopen) {
467                             $scope.clickedclosed = true;
468                         }
469                     } else {
470                         $scope.clickedclosed = !$scope.clickedopen;
471                     }
472
473                     if ($scope.selected.length > 0) $scope.complete_list = true;
474                     if ($scope.selected.length == 0) $scope.complete_list = false;
475                     $scope.makeOpen();
476                 }
477
478                 $scope.makeOpen = function () {
479                     $scope.isopen = $scope.clickedopen || ($filter('filter')(
480                         $scope.list,
481                         $scope.selected
482                     ).length > 0 && $scope.selected.length > 0);
483                     if ($scope.clickedclosed) $scope.isopen = false;
484                 }
485
486                 $scope.changeValue = function (newVal) {
487                     $scope.selected = newVal;
488                     $scope.isopen = false;
489                     $scope.clickedclosed = null;
490                     $scope.clickedopen = false;
491                     if ($scope.selected.length == 0) $scope.complete_list = false;
492                 }
493
494             }
495         ]
496     };
497 })
498
499 /**
500  * Nested org unit selector modeled as a Bootstrap dropdown button.
501  */
502 .directive('egOrgSelector', function() {
503     return {
504         restrict : 'AE',
505         transclude : true,
506         replace : true, // makes styling easier
507         scope : {
508             selected : '=', // defaults to workstation or root org,
509                             // unless the nodefault attibute exists
510
511             // Each org unit is passed into this function and, for
512             // any org units where the response value is true, the
513             // org unit will not be added to the selector.
514             hiddenTest : '=',
515
516             // Each org unit is passed into this function and, for
517             // any org units where the response value is true, the
518             // org unit will not be available for selection.
519             disableTest : '=',
520
521             // if set to true, disable the UI element altogether
522             alldisabled : '@',
523
524             // Caller can either $watch(selected, ..) or register an
525             // onchange handler.
526             onchange : '=',
527
528             // optional primary drop-down button label
529             label : '@',
530
531             // optional name of settings key for persisting
532             // the last selected org unit
533             stickySetting : '@'
534         },
535
536         // any reason to move this into a TT2 template?
537         template : 
538             '<div class="btn-group eg-org-selector" uib-dropdown>'
539             + '<button type="button" class="btn btn-default" uib-dropdown-toggle ng-disabled="disable_button">'
540              + '<span style="padding-right: 5px;">{{getSelectedName()}}</span>'
541              + '<span class="caret"></span>'
542            + '</button>'
543            + '<ul uib-dropdown-menu class="scrollable-menu">'
544              + '<li ng-repeat="org in orgList" ng-hide="hiddenTest(org.id)">'
545                + '<a href ng-click="orgChanged(org)" a-disabled="disableTest(org.id)" '
546                  + 'style="padding-left: {{org.depth * 10 + 5}}px">'
547                  + '{{org.shortname}}'
548                + '</a>'
549              + '</li>'
550            + '</ul>'
551           + '</div>',
552
553         controller : ['$scope','$timeout','egCore','egStartup',
554               function($scope , $timeout , egCore , egStartup) {
555
556             if ($scope.alldisabled) {
557                 $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
558             } else {
559                 $scope.disable_button = false;
560             }
561
562             // avoid linking the full fleshed tree to the scope by 
563             // tossing in a flattened list.
564             // --
565             // Run-time code referencing post-start data should be run
566             // from within a startup block, otherwise accessing this
567             // module before startup completes will lead to failure.
568             //
569             // controller() runs before link().
570             // This post-startup code runs after link().
571             egStartup.go().then(function() {
572
573                 $scope.orgList = egCore.org.list().map(function(org) {
574                     return {
575                         id : org.id(),
576                         shortname : org.shortname(), 
577                         depth : org.ou_type().depth()
578                     }
579                 });
580
581                 // Apply default values
582
583                 if ($scope.stickySetting) {
584                     var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
585                     if (orgId) {
586                         $scope.selected = egCore.org.get(orgId);
587                     }
588                 }
589
590                 if (!$scope.selected && !$scope.nodefault) {
591                     $scope.selected = 
592                         egCore.org.get(egCore.auth.user().ws_ou());
593                 }
594
595                 fire_orgsel_onchange(); // no-op if nothing is selected
596             });
597
598             /**
599              * Fire onchange handler after a timeout, so the
600              * $scope.selected value has a chance to propagate to
601              * the page controllers before the onchange fires.  This
602              * way, the caller does not have to manually capture the
603              * $scope.selected value during onchange.
604              */
605             function fire_orgsel_onchange() {
606                 if (!$scope.selected || !$scope.onchange) return;
607                 $timeout(function() {
608                     console.debug(
609                         'egOrgSelector onchange('+$scope.selected.id()+')');
610                     $scope.onchange($scope.selected)
611                 });
612             }
613
614             $scope.getSelectedName = function() {
615                 if ($scope.selected && $scope.selected.shortname)
616                     return $scope.selected.shortname();
617                 return $scope.label;
618             }
619
620             $scope.orgChanged = function(org) {
621                 $scope.selected = egCore.org.get(org.id);
622                 if ($scope.stickySetting) {
623                     egCore.hatch.setLocalItem($scope.stickySetting, org.id);
624                 }
625                 fire_orgsel_onchange();
626             }
627
628         }],
629         link : function(scope, element, attrs, egGridCtrl) {
630
631             // boolean fields are presented as value-less attributes
632             angular.forEach(
633                 ['nodefault'],
634                 function(field) {
635                     if (angular.isDefined(attrs[field]))
636                         scope[field] = true;
637                     else
638                         scope[field] = false;
639                 }
640             );
641         }
642     }
643 })
644
645 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
646 .directive('egEnter', function () {
647     return function (scope, element, attrs) {
648         element.bind("keydown keypress", function (event) {
649             if(event.which === 13) {
650                 scope.$apply(function (){
651                     scope.$eval(attrs.egEnter);
652                 });
653  
654                 event.preventDefault();
655             }
656         });
657     };
658 })
659
660 /*
661 * Handy wrapper directive for uib-datapicker-popup
662 */
663 .directive(
664     'egDateInput', ['egStrings', 'egCore',
665     function(egStrings, egCore) {
666         return {
667             scope : {
668                 closeText : '@',
669                 ngModel : '=',
670                 ngChange : '=',
671                 ngBlur : '=',
672                 ngDisabled : '=',
673                 ngRequired : '=',
674                 hideDatePicker : '=',
675                 dateFormat : '=?'
676             },
677             require: 'ngModel',
678             templateUrl: './share/t_datetime',
679             replace: true,
680             link : function(scope, elm, attrs) {
681                 if (!scope.closeText)
682                     scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
683
684                 if ('showTimePicker' in attrs)
685                     scope.showTimePicker = true;
686
687                 var default_format = 'mediumDate';
688                 egCore.org.settings(['format.date']).then(function(set) {
689                     default_format = set['format.date'];
690                     scope.date_format = (scope.dateFormat) ?
691                         scope.dateFormat :
692                         default_format;
693                 });
694             }
695         };
696     }
697 ])
698
699 /*
700  *  egFmValueSelector - widget for selecting a value from list specified
701  *                      by IDL class
702  */
703 .directive('egFmValueSelector', function() {
704     return {
705         restrict : 'E',
706         transclude : true,
707         scope : {
708             idlClass : '@',
709             ngModel : '=',
710
711             // optional filter for refining the set of rows that
712             // get returned. Example:
713             //
714             // filter="{'column':{'=':null}}"
715             filter : '=',
716
717             // optional name of settings key for persisting
718             // the last selected value
719             stickySetting : '@',
720
721             // optional OU setting for fetching default value;
722             // used only if sticky setting not set
723             ouSetting : '@'
724         },
725         require: 'ngModel',
726         templateUrl : './share/t_fm_value_selector',
727         controller : ['$scope','egCore', function($scope , egCore) {
728
729             $scope.org = egCore.org; // for use in the link function
730             $scope.auth = egCore.auth; // for use in the link function
731             $scope.hatch = egCore.hatch // for use in the link function
732
733             function flatten_linked_values(cls, list) {
734                 var results = [];
735                 var fields = egCore.idl.classes[cls].fields;
736                 var id_field;
737                 var selector;
738                 angular.forEach(fields, function(fld) {
739                     if (fld.datatype == 'id') {
740                         id_field = fld.name;
741                         selector = fld.selector ? fld.selector : id_field;
742                         return;
743                     }
744                 });
745                 angular.forEach(list, function(item) {
746                     var rec = egCore.idl.toHash(item);
747                     results.push({
748                         id : rec[id_field],
749                         name : rec[selector]
750                     });
751                 });
752                 return results;
753             }
754
755             var search = {};
756             search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
757             if ($scope.filter) {
758                 angular.extend(search, $scope.filter);
759             }
760             egCore.pcrud.search(
761                 $scope.idlClass, search, {}, {atomic : true}
762             ).then(function(list) {
763                 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
764             });
765
766             $scope.handleChange = function(value) {
767                 if ($scope.stickySetting) {
768                     egCore.hatch.setLocalItem($scope.stickySetting, value);
769                 }
770             }
771
772         }],
773         link : function(scope, element, attrs) {
774             if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
775                 var value = scope.hatch.getLocalItem(scope.stickySetting);
776                 scope.ngModel = value;
777             }
778             if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
779                 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
780                 .then(function(set) {
781                     var value = parseInt(set[scope.ouSetting]);
782                     if (!isNaN(value))
783                         scope.ngModel = value;
784                 });
785             }
786         }
787     }
788 })
789
790 .factory('egWorkLog', ['egCore', function(egCore) {
791     var service = {};
792
793     service.retrieve_all = function() {
794         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
795         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
796
797         return { 'work_log' : workLog, 'patron_log' : patronLog };
798     }
799
800     service.record = function(message,data) {
801         var max_entries;
802         var max_patrons;
803         if (typeof egCore != 'undefined') {
804             if (typeof egCore.env != 'undefined') {
805                 if (typeof egCore.env.aous != 'undefined') {
806                     max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
807                     max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
808                 } else {
809                     console.log('worklog: missing egCore.env.aous');
810                 }
811             } else {
812                 console.log('worklog: missing egCore.env');
813             }
814         } else {
815             console.log('worklog: missing egCore');
816         }
817         if (!max_entries) {
818             if (typeof egCore.org != 'undefined') {
819                 if (typeof egCore.org.cachedSettings != 'undefined') {
820                     max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
821                 } else {
822                     console.log('worklog: missing egCore.org.cachedSettings');
823                 }
824             } else {
825                 console.log('worklog: missing egCore.org');
826             }
827         }
828         if (!max_patrons) {
829             if (typeof egCore.org != 'undefined') {
830                 if (typeof egCore.org.cachedSettings != 'undefined') {
831                     max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
832                 } else {
833                     console.log('worklog: missing egCore.org.cachedSettings');
834                 }
835             } else {
836                 console.log('worklog: missing egCore.org');
837             }
838         }
839         if (!max_entries) {
840             max_entries = 20;
841             console.log('worklog: defaulting to max_entries = ' + max_entries);
842         }
843         if (!max_patrons) {
844             max_patrons = 10;
845             console.log('worklog: defaulting to max_patrons = ' + max_patrons);
846         }
847
848         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
849         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
850         var entry = {
851             'when' : new Date(),
852             'msg' : message,
853             'data' : data,
854             'action' : data.action,
855             'actor' : egCore.auth.user().usrname()
856         };
857         if (data.action == 'checkin') {
858             entry['item'] = data.response.params.copy_barcode;
859             entry['item_id'] = data.response.data.acp.id();
860             if (data.response.data.au) {
861                 entry['user'] = data.response.data.au.family_name();
862                 entry['patron_id'] = data.response.data.au.id();
863             }
864         }
865         if (data.action == 'checkout') {
866             entry['item'] = data.response.params.copy_barcode;
867             entry['user'] = data.response.data.au.family_name();
868             entry['item_id'] = data.response.data.acp.id();
869             entry['patron_id'] = data.response.data.au.id();
870         }
871         if (data.action == 'renew') {
872             entry['item'] = data.response.params.copy_barcode;
873             entry['user'] = data.response.data.au.family_name();
874             entry['item_id'] = data.response.data.acp.id();
875             entry['patron_id'] = data.response.data.au.id();
876         }
877         if (data.action == 'requested_hold'
878             || data.action == 'edited_patron'
879             || data.action == 'registered_patron'
880             || data.action == 'paid_bill') {
881             entry['patron_id'] = data.patron_id;
882         }
883         if (data.action == 'paid_bill') {
884             entry['amount'] = data.total_amount;
885         }
886
887         workLog.push( entry );
888         if (workLog.length > max_entries) workLog.shift();
889         egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
890
891         if (entry['patron_id']) {
892             var temp = [];
893             for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
894                 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
895             }
896             temp.push( entry );
897             if (temp.length > max_patrons) temp.shift();
898             patronLog = temp;
899             egCore.hatch.setLocalItem('eg.patron_log',patronLog);
900         }
901
902         console.log('worklog',entry);
903     }
904
905     return service;
906 }]);