LP#1706107: Offline mode
[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','egLovefield','$q',
699               function($scope , $timeout , egCore , egStartup , egLovefield , $q) {
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(
717             ).then(
718                 function() {
719                     return egCore.env.classLoaders.aou();
720                 }
721             ).then(
722                 function() {
723
724                     $scope.orgList = egCore.org.list().map(function(org) {
725                         return {
726                             id : org.id(),
727                             shortname : org.shortname(), 
728                             depth : org.ou_type().depth()
729                         }
730                     });
731                     
732     
733                     // Apply default values
734     
735                     if ($scope.stickySetting) {
736                         var orgId = egCore.hatch.getLocalItem($scope.stickySetting);
737                         if (orgId) {
738                             $scope.selected = egCore.org.get(orgId);
739                         }
740                     }
741     
742                     if (!$scope.selected && !$scope.nodefault && egCore.auth.user()) {
743                         $scope.selected = 
744                             egCore.org.get(egCore.auth.user().ws_ou());
745                     }
746     
747                     fire_orgsel_onchange(); // no-op if nothing is selected
748                 }
749             );
750
751             /**
752              * Fire onchange handler after a timeout, so the
753              * $scope.selected value has a chance to propagate to
754              * the page controllers before the onchange fires.  This
755              * way, the caller does not have to manually capture the
756              * $scope.selected value during onchange.
757              */
758             function fire_orgsel_onchange() {
759                 if (!$scope.selected || !$scope.onchange) return;
760                 $timeout(function() {
761                     console.debug(
762                         'egOrgSelector onchange('+$scope.selected.id()+')');
763                     $scope.onchange($scope.selected)
764                 });
765             }
766
767             $scope.getSelectedName = function() {
768                 if ($scope.selected && $scope.selected.shortname)
769                     return $scope.selected.shortname();
770                 return $scope.label;
771             }
772
773             $scope.orgChanged = function(org) {
774                 $scope.selected = egCore.org.get(org.id);
775                 if ($scope.stickySetting) {
776                     egCore.hatch.setLocalItem($scope.stickySetting, org.id);
777                 }
778                 fire_orgsel_onchange();
779             }
780
781         }],
782         link : function(scope, element, attrs, egGridCtrl) {
783
784             // boolean fields are presented as value-less attributes
785             angular.forEach(
786                 ['nodefault'],
787                 function(field) {
788                     if (angular.isDefined(attrs[field]))
789                         scope[field] = true;
790                     else
791                         scope[field] = false;
792                 }
793             );
794         }
795     }
796 })
797
798 .directive('nextOnEnter', function () {
799     return function (scope, element, attrs) {
800         element.bind("keydown keypress", function (event) {
801             if(event.which === 13) {
802                 $('#'+attrs.nextOnEnter).focus();
803                 event.preventDefault();
804             }
805         });
806     };
807 })
808
809 /* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
810 .directive('egEnter', function () {
811     return function (scope, element, attrs) {
812         element.bind("keydown keypress", function (event) {
813             if(event.which === 13) {
814                 scope.$apply(function (){
815                     scope.$eval(attrs.egEnter);
816                 });
817  
818                 event.preventDefault();
819             }
820         });
821     };
822 })
823
824 /*
825 * Handy wrapper directive for uib-datapicker-popup
826 */
827 .directive(
828     'egDateInput', ['egStrings', 'egCore',
829     function(egStrings, egCore) {
830         return {
831             scope : {
832                 id : '@',
833                 closeText : '@',
834                 ngModel : '=',
835                 ngChange : '=',
836                 ngBlur : '=',
837                 minDate : '=?',
838                 maxDate : '=?',
839                 ngDisabled : '=',
840                 ngRequired : '=',
841                 hideDatePicker : '=',
842                 dateFormat : '=?',
843                 outOfRange : '=?'
844             },
845             require: 'ngModel',
846             templateUrl: './share/t_datetime',
847             replace: true,
848             controller : ['$scope', function($scope) {
849                 $scope.options = {
850                     minDate : $scope.minDate,
851                     maxDate : $scope.maxDate
852                 };
853
854                 var maxDateObj = $scope.maxDate ? new Date($scope.maxDate) : null;
855                 var minDateObj = $scope.minDate ? new Date($scope.minDate) : null;
856
857                 if ($scope.outOfRange !== undefined && (maxDateObj || minDateObj)) {
858                     $scope.$watch('ngModel', function (n,o) {
859                         if (n && n != o) {
860                             var bad = false;
861                             var newdate = new Date(n);
862                             if (maxDateObj && newdate.getTime() > maxDateObj.getTime()) bad = true;
863                             if (minDateObj && newdate.getTime() < minDateObj.getTime()) bad = true;
864                             $scope.outOfRange = bad;
865                         }
866                     });
867                 }
868             }],
869             link : function(scope, elm, attrs) {
870                 if (!scope.closeText)
871                     scope.closeText = egStrings.EG_DATE_INPUT_CLOSE_TEXT;
872
873                 if ('showTimePicker' in attrs)
874                     scope.showTimePicker = true;
875
876                 var default_format = 'mediumDate';
877                 egCore.org.settings(['format.date']).then(function(set) {
878                     default_format = set['format.date'];
879                     scope.date_format = (scope.dateFormat) ?
880                         scope.dateFormat :
881                         default_format;
882                 });
883             }
884         };
885     }
886 ])
887
888 /*
889  *  egFmValueSelector - widget for selecting a value from list specified
890  *                      by IDL class
891  */
892 .directive('egFmValueSelector', function() {
893     return {
894         restrict : 'E',
895         transclude : true,
896         scope : {
897             idlClass : '@',
898             ngModel : '=',
899
900             // optional filter for refining the set of rows that
901             // get returned. Example:
902             //
903             // filter="{'column':{'=':null}}"
904             filter : '=',
905
906             // optional name of settings key for persisting
907             // the last selected value
908             stickySetting : '@',
909
910             // optional OU setting for fetching default value;
911             // used only if sticky setting not set
912             ouSetting : '@'
913         },
914         require: 'ngModel',
915         templateUrl : './share/t_fm_value_selector',
916         controller : ['$scope','egCore', function($scope , egCore) {
917
918             $scope.org = egCore.org; // for use in the link function
919             $scope.auth = egCore.auth; // for use in the link function
920             $scope.hatch = egCore.hatch // for use in the link function
921
922             function flatten_linked_values(cls, list) {
923                 var results = [];
924                 var fields = egCore.idl.classes[cls].fields;
925                 var id_field;
926                 var selector;
927                 angular.forEach(fields, function(fld) {
928                     if (fld.datatype == 'id') {
929                         id_field = fld.name;
930                         selector = fld.selector ? fld.selector : id_field;
931                         return;
932                     }
933                 });
934                 angular.forEach(list, function(item) {
935                     var rec = egCore.idl.toHash(item);
936                     results.push({
937                         id : rec[id_field],
938                         name : rec[selector]
939                     });
940                 });
941                 return results;
942             }
943
944             var search = {};
945             search[egCore.idl.classes[$scope.idlClass].pkey] = {'!=' : null};
946             if ($scope.filter) {
947                 angular.extend(search, $scope.filter);
948             }
949             egCore.pcrud.search(
950                 $scope.idlClass, search, {}, {atomic : true}
951             ).then(function(list) {
952                 $scope.linked_values = flatten_linked_values($scope.idlClass, list);
953             });
954
955             $scope.handleChange = function(value) {
956                 if ($scope.stickySetting) {
957                     egCore.hatch.setLocalItem($scope.stickySetting, value);
958                 }
959             }
960
961         }],
962         link : function(scope, element, attrs) {
963             if (scope.stickySetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
964                 var value = scope.hatch.getLocalItem(scope.stickySetting);
965                 scope.ngModel = value;
966             }
967             if (scope.ouSetting && (angular.isUndefined(scope.ngModel) || (scope.ngModel === null))) {
968                 scope.org.settings([scope.ouSetting], scope.auth.user().ws_ou())
969                 .then(function(set) {
970                     var value = parseInt(set[scope.ouSetting]);
971                     if (!isNaN(value))
972                         scope.ngModel = value;
973                 });
974             }
975         }
976     }
977 })
978
979 .factory('egWorkLog', ['egCore', function(egCore) {
980     var service = {};
981
982     service.retrieve_all = function() {
983         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
984         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
985
986         return { 'work_log' : workLog, 'patron_log' : patronLog };
987     }
988
989     service.record = function(message,data) {
990         var max_entries;
991         var max_patrons;
992         if (typeof egCore != 'undefined') {
993             if (typeof egCore.env != 'undefined') {
994                 if (typeof egCore.env.aous != 'undefined') {
995                     max_entries = egCore.env.aous['ui.admin.work_log.max_entries'];
996                     max_patrons = egCore.env.aous['ui.admin.patron_log.max_entries'];
997                 } else {
998                     console.log('worklog: missing egCore.env.aous');
999                 }
1000             } else {
1001                 console.log('worklog: missing egCore.env');
1002             }
1003         } else {
1004             console.log('worklog: missing egCore');
1005         }
1006         if (!max_entries) {
1007             if (typeof egCore.org != 'undefined') {
1008                 if (typeof egCore.org.cachedSettings != 'undefined') {
1009                     max_entries = egCore.org.cachedSettings['ui.admin.work_log.max_entries'];
1010                 } else {
1011                     console.log('worklog: missing egCore.org.cachedSettings');
1012                 }
1013             } else {
1014                 console.log('worklog: missing egCore.org');
1015             }
1016         }
1017         if (!max_patrons) {
1018             if (typeof egCore.org != 'undefined') {
1019                 if (typeof egCore.org.cachedSettings != 'undefined') {
1020                     max_patrons = egCore.org.cachedSettings['ui.admin.patron_log.max_entries'];
1021                 } else {
1022                     console.log('worklog: missing egCore.org.cachedSettings');
1023                 }
1024             } else {
1025                 console.log('worklog: missing egCore.org');
1026             }
1027         }
1028         if (!max_entries) {
1029             max_entries = 20;
1030             console.log('worklog: defaulting to max_entries = ' + max_entries);
1031         }
1032         if (!max_patrons) {
1033             max_patrons = 10;
1034             console.log('worklog: defaulting to max_patrons = ' + max_patrons);
1035         }
1036
1037         var workLog = egCore.hatch.getLocalItem('eg.work_log') || [];
1038         var patronLog = egCore.hatch.getLocalItem('eg.patron_log') || [];
1039         var entry = {
1040             'when' : new Date(),
1041             'msg' : message,
1042             'action' : data.action,
1043             'actor' : egCore.auth.user().usrname()
1044         };
1045         if (data.action == 'checkin') {
1046             entry['item'] = data.response.params.copy_barcode;
1047             entry['item_id'] = data.response.data.acp.id();
1048             if (data.response.data.au) {
1049                 entry['user'] = data.response.data.au.family_name();
1050                 entry['patron_id'] = data.response.data.au.id();
1051             }
1052         }
1053         if (data.action == 'checkout') {
1054             entry['item'] = data.response.params.copy_barcode;
1055             entry['user'] = data.response.data.au.family_name();
1056             entry['item_id'] = data.response.data.acp.id();
1057             entry['patron_id'] = data.response.data.au.id();
1058         }
1059         if (data.action == 'noncat_checkout') {
1060             entry['user'] = data.response.data.au.family_name();
1061             entry['patron_id'] = data.response.data.au.id();
1062         }
1063         if (data.action == 'renew') {
1064             entry['item'] = data.response.params.copy_barcode;
1065             entry['user'] = data.response.data.au.family_name();
1066             entry['item_id'] = data.response.data.acp.id();
1067             entry['patron_id'] = data.response.data.au.id();
1068         }
1069         if (data.action == 'requested_hold'
1070             || data.action == 'edited_patron'
1071             || data.action == 'registered_patron'
1072             || data.action == 'paid_bill') {
1073             entry['patron_id'] = data.patron_id;
1074         }
1075         if (data.action == 'requested_hold') {
1076             entry['hold_id'] = data.hold_id;
1077         }
1078         if (data.action == 'paid_bill') {
1079             entry['amount'] = data.total_amount;
1080         }
1081
1082         workLog.push( entry );
1083         if (workLog.length > max_entries) workLog.shift();
1084         egCore.hatch.setLocalItem('eg.work_log',workLog); // hatch JSONifies the data, so should be okay re: memory leaks?
1085
1086         if (entry['patron_id']) {
1087             var temp = [];
1088             for (var i = 0; i < patronLog.length; i++) { // filter out any matching patron
1089                 if (patronLog[i]['patron_id'] != entry['patron_id']) temp.push(patronLog[i]);
1090             }
1091             temp.push( entry );
1092             if (temp.length > max_patrons) temp.shift();
1093             patronLog = temp;
1094             egCore.hatch.setLocalItem('eg.patron_log',patronLog);
1095         }
1096
1097         console.log('worklog',entry);
1098     }
1099
1100     return service;
1101 }]);