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