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