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