69c433f896a786114ba4a0cc0c8c6e90b17dd13b
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / reporter / template / app.js
1 /*
2  * Report template builder
3  */
4
5 angular.module('egReporter',
6     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egReportMod', 'treeControl', 'ngToast'])
7
8 .config(['ngToastProvider', function(ngToastProvider) {
9   ngToastProvider.configure({
10     verticalPosition: 'bottom',
11     animation: 'fade'
12   });
13 }])
14
15 .config(function($routeProvider, $locationProvider, $compileProvider) {
16     $locationProvider.html5Mode(true);
17     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
18         
19     var resolver = {delay : function(egStartup) {return egStartup.go()}};
20
21     $routeProvider.when('/reporter/template/clone/:folder/:id', {
22         templateUrl: './reporter/t_edit_template',
23         controller: 'ReporterTemplateEdit',
24         resolve : resolver
25     });
26
27     $routeProvider.when('/reporter/legacy/template/clone/:folder/:id', {
28         templateUrl: './reporter/t_legacy',
29         controller: 'ReporterTemplateLegacy',
30         resolve : resolver
31     });
32
33     $routeProvider.when('/reporter/template/new/:folder', {
34         templateUrl: './reporter/t_edit_template',
35         controller: 'ReporterTemplateEdit',
36         resolve : resolver
37     });
38
39     $routeProvider.when('/reporter/legacy/main', {
40         templateUrl: './reporter/t_legacy',
41         controller: 'ReporterTemplateLegacy',
42         resolve : resolver
43     });
44
45     // default page
46     $routeProvider.otherwise({redirectTo : '/reporter/legacy/main'});
47 })
48
49 /**
50  * controller for legacy template stuff
51  */
52 .controller('ReporterTemplateLegacy',
53        ['$scope','$routeParams','$location','egCore',
54 function($scope , $routeParams , $location , egCore) {
55
56     var template_id = $routeParams.id;
57     var folder_id = $routeParams.folder;
58
59     $scope.rurl = '/reports/oils_rpt.xhtml?ses=' + egCore.auth.token();
60
61     if (folder_id) {
62         $scope.rurl = '/reports/oils_rpt_builder.xhtml?ses=' +
63                         egCore.auth.token() + '&folder=' + folder_id;
64
65         if (template_id) $scope.rurl += '&ct=' + template_id;
66     }
67
68 }])
69
70 /**
71  * Uber-controller for template editing
72  */
73 .controller('ReporterTemplateEdit',
74        ['$scope','$q','$routeParams','$location','$timeout','$window','egCore','$uibModal','egPromptDialog',
75         'egGridDataProvider','egReportTemplateSvc','$uibModal','egConfirmDialog','egSelectDialog','ngToast',
76 function($scope , $q , $routeParams , $location , $timeout , $window,  egCore , $uibModal , egPromptDialog ,
77          egGridDataProvider , egReportTemplateSvc , $uibModal , egConfirmDialog , egSelectDialog , ngToast) {
78
79     function values(o) { return Object.keys(o).map(function(k){return o[k]}) };
80
81     var template_id = $routeParams.id;
82     var folder_id = $routeParams.folder;
83
84     $scope.grid_display_fields_provider = egGridDataProvider.instance({
85         get : function (offset, count) {
86             return this.arrayNotifier(egReportTemplateSvc.display_fields, offset, count);
87         }
88     });
89     $scope.grid_filter_fields_provider = egGridDataProvider.instance({
90         get : function (offset, count) {
91             return this.arrayNotifier(egReportTemplateSvc.filter_fields, offset, count);
92         }
93     });
94
95     var dgrid = $scope.display_grid_controls = {};
96     var fgrid = $scope.filter_grid_controls = {};
97
98     var default_filter_obj = {
99         op : '=',
100         label     : egReportTemplateSvc.Filters['='].label
101     };
102
103     var default_transform_obj = {
104         transform : 'Bare',
105         label     : egReportTemplateSvc.Transforms.Bare.label,
106         aggregate : false
107     };
108
109     function mergePaths (items) {
110         var tree = {};
111
112         items.forEach(function (item) {
113             var t = tree;
114             var join_path = '';
115
116             item.path.forEach(function (p, i, a) {
117                 var alias; // unpredictable hashes are fine for intermediate tables
118
119                 if (i) { // not at the top of the tree
120                     if (i == 1) join_path = join_path.split('-')[0];
121
122                     var uplink = p.uplink.name;
123                     join_path += '-' + uplink;
124                     alias = hex_md5(join_path);
125
126                     var uplink_alias = uplink + '-' + alias;
127
128                     if (!t.join) t.join = {};
129                     if (!t.join[uplink_alias]) t.join[uplink_alias] = {};
130
131                     t = t.join[uplink_alias];
132
133                     var djtype = 'inner';
134                     if (p.uplink.reltype != 'has_a') djtype = 'left';
135
136                     t.type = p.jtype || djtype;
137                     t.key = p.uplink.key;
138                 } else {
139                     join_path = p.classname + '-' + p.classname;
140                     alias = hex_md5(join_path);
141                 }
142
143                 if (!t.alias) t.alias = alias;
144                 t.path = join_path;
145
146                 t.table = p.struct.source ? p.struct.source : p.table;
147                 t.idlclass = p.classname;
148
149                 if (a.length == i + 1) { // end of the path array, need a predictable hash
150                     t.label = item.path_label;
151                     t.alias = hex_md5(item.path_label);
152                 }
153
154             });
155         });
156
157         return tree;
158     }
159
160     $scope.constructTemplate = function () {
161         var param_counter = 0;
162         return {
163             version     : 5,
164             doc_url     : $scope.templateDocURL,
165             core_class  : egCore.idl.classTree.top.classname,
166             select      : dgrid.allItems().map(function (i) {
167                             return {
168                                 alias     : i.label,
169                                 path      : i.path[i.path.length - 1].classname + '-' + i.name,
170                                 field_doc : i.doc_text,
171                                 relation  : hex_md5(i.path_label),
172                                 column    : {
173                                     colname         : i.name,
174                                     transform       : i.transform ? i.transform.transform : '',
175                                     transform_label : i.transform ? i.transform.label : '',
176                                     aggregate       : !!i.transform.aggregate
177                                 }
178                             }
179                           }),
180             from        : mergePaths( dgrid.allItems().concat(fgrid.allItems()) ),
181             where       : fgrid.allItems().filter(function(i) {
182                             return !i.transform.aggregate;
183                           }).map(function (i) {
184                             var cond = {};
185                             if (
186                                 i.operator.op == 'is' ||
187                                 i.operator.op == 'is not' ||
188                                 i.operator.op == 'is blank' ||
189                                 i.operator.op == 'is not blank'
190                             ) {
191                                 cond[i.operator.op] = null;
192                             } else {
193                                 if (i.value === undefined) {
194                                     cond[i.operator.op] = '::P' + param_counter++;
195                                 }else {
196                                     cond[i.operator.op] = i.value;
197                                 }
198                             }
199                             return {
200                                 alias     : i.label,
201                                 path      : i.path[i.path.length - 1].classname + '-' + i.name,
202                                 field_doc : i.doc_text,
203                                 relation  : hex_md5(i.path_label),
204                                 column    : {
205                                     colname         : i.name,
206                                     transform       : i.transform.transform,
207                                     transform_label : i.transform.label,
208                                     aggregate       : 0
209                                 },
210                                 condition : cond // constructed above
211                             }
212                           }),
213             having      : fgrid.allItems().filter(function(i) {
214                             return !!i.transform.aggregate;
215                           }).map(function (i) {
216                             var cond = {};
217                             cond[i.operator.op] = '::P' + param_counter++;
218                             return {
219                                 alias     : i.label,
220                                 path      : i.path[i.path.length - 1].classname + '-' + i.name,
221                                 field_doc : i.doc_text,
222                                 relation  : hex_md5(i.path_label),
223                                 column    : {
224                                     colname         : i.name,
225                                     transform       : i.transform.transform,
226                                     transform_label : i.transform.label,
227                                     aggregate       : 1
228                                 },
229                                 condition : cond // constructed above
230                             }
231                           }),
232             display_cols: angular.copy( dgrid.allItems() ).map(strip_item),
233             filter_cols : angular.copy( fgrid.allItems() ).map(strip_item)
234         };
235
236         function strip_item (i) {
237             delete i.children;
238             i.path.forEach(function(p){
239                 delete p.children;
240                 delete p.fields;
241                 delete p.links;
242                 delete p.struct.permacrud;
243                 delete p.struct.field_map;
244                 delete p.struct.fields;
245             });
246             return i;
247         }
248
249     }
250
251     function loadTemplate () {
252         if (!template_id) return;
253         egCore.pcrud.retrieve( 'rt', template_id)
254         .then( function(template) {
255             template.data = angular.fromJson(template.data());
256             if (template.data.version < 5) { // redirect to old editor...
257                 $window.location.href = egCore.env.basePath + 'reporter/legacy/template/clone/'+folder_id + '/' + template_id;
258             // } else if (template.data.version < 5) { // redirect to old editor...
259             } else {
260                 $scope.templateName = template.name() + ' (clone)';
261                 $scope.templateDescription = template.description();
262                 $scope.templateDocURL = template.data.doc_url;
263
264                 $scope.changeCoreSource( template.data.core_class );
265
266                 egReportTemplateSvc.display_fields = template.data.display_cols;
267                 egReportTemplateSvc.filter_fields = template.data.filter_cols;
268
269                 $timeout(function(){
270                     dgrid.refresh();
271                     fgrid.refresh();
272                 });
273             }
274         });
275
276     }
277
278     $scope.saveTemplate = function () {
279         var tmpl = new egCore.idl.rt();
280         tmpl.name( $scope.templateName );
281         tmpl.description( $scope.templateDescription );
282         tmpl.owner(egCore.auth.user().id());
283         tmpl.folder(folder_id);
284         tmpl.data(angular.toJson($scope.constructTemplate()));
285
286         egConfirmDialog.open(tmpl.name(), egCore.strings.TEMPLATE_CONF_CONFIRM_SAVE,
287             {ok : function() {
288                 return egCore.pcrud.create( tmpl )
289                 .then(
290                     function() {
291                         ngToast.create(egCore.strings.TEMPLATE_CONF_SUCCESS_SAVE);
292                         return $timeout(
293                             function(){
294                                 $window.location.href = egCore.env.basePath + 'reporter/legacy/main';
295                             },
296                             1000
297                         );
298                     },
299                     function() {
300                         ngToast.warning(egCore.strings.TEMPLATE_CONF_FAIL_SAVE);
301                     }
302                 );
303             }}
304         );
305     }
306
307     $scope.addDisplayFields = function () {
308         var t = $scope.selected_transform;
309         if (!t) t = default_transform_obj;
310
311         egReportTemplateSvc.addFields(
312             'display_fields',
313             $scope.selected_source_field_list, 
314             t,
315             $scope.currentPathLabel,
316             $scope.currentPath
317         );
318         dgrid.refresh();
319     }
320
321     $scope.addFilterFields = function () {
322         var t = $scope.selected_transform;
323         if (!t) t = default_transform_obj;
324         f = default_filter_obj;
325
326         egReportTemplateSvc.addFields(
327             'filter_fields',
328             $scope.selected_source_field_list, 
329             t,
330             $scope.currentPathLabel,
331             $scope.currentPath,
332             f
333         );
334         fgrid.refresh();
335     }
336
337     $scope.moveDisplayFieldUp = function (items) {
338         items.reverse().forEach(function(item) {
339             egReportTemplateSvc.moveFieldUp('display_fields', item);
340         });
341         dgrid.refresh();
342     }
343
344     $scope.moveDisplayFieldDown = function (items) {
345         items.forEach(function(item) {
346             egReportTemplateSvc.moveFieldDown('display_fields', item);
347         });
348         dgrid.refresh();
349     }
350
351     $scope.removeDisplayField = function (items) {
352         items.forEach(function(item) {egReportTemplateSvc.removeField('display_fields', item)});
353         dgrid.refresh();
354     }
355
356     $scope.changeDisplayLabel = function (items) {
357         items.forEach(function(item) {
358             egPromptDialog.open(egCore.strings.TEMPLATE_CONF_PROMPT_CHANGE, item.label || '',
359                 {ok : function(value) {
360                     if (value) egReportTemplateSvc.display_fields[item.index].label = value;
361                 }}
362             );
363         });
364         dgrid.refresh();
365     }
366
367     $scope.changeDisplayFieldDoc = function (items) {
368         items.forEach(function(item) {
369             egPromptDialog.open(egCore.strings.TEMPLATE_FIELD_DOC_PROMPT_CHANGE, item.doc_text || '',
370                 {ok : function(value) {
371                     if (value) egReportTemplateSvc.display_fields[item.index].doc_text = value;
372                 }}
373             );
374         });
375         dgrid.refresh();
376     }
377
378     $scope.changeFilterFieldDoc = function (items) {
379         items.forEach(function(item) {
380             egPromptDialog.open(egCore.strings.TEMPLATE_FIELD_DOC_PROMPT_CHANGE, item.doc_text || '',
381                 {ok : function(value) {
382                     if (value) egReportTemplateSvc.filter_fields[item.index].doc_text = value;
383                 }}
384             );
385         });
386         fgrid.refresh();
387     }
388
389     $scope.changeFilterValue = function (items) {
390         items.forEach(function(item) {
391             var l = null;
392             egPromptDialog.open(egCore.strings.TEMPLATE_CONF_DEFAULT, item.value || '',
393                 {ok : function(value) {
394                     if (value) egReportTemplateSvc.filter_fields[item.index].value = value;
395                 }}
396             );
397         });
398         fgrid.refresh();
399     }
400
401     $scope.changeTransform = function (items) {
402
403         var f = items[0];
404
405         var tlist = [];
406         angular.forEach(egReportTemplateSvc.Transforms, function (o,n) {
407             if ( o.datatype.indexOf(f.datatype) > -1) {
408                 if (tlist.indexOf(o.label) == -1) tlist.push( o.label );
409             }
410         });
411         
412         items.forEach(function(item) {
413             egSelectDialog.open(
414                 egCore.strings.SELECT_TFORM, tlist, item.transform.label,
415                 {ok : function(value) {
416                     if (value) {
417                         var t = egReportTemplateSvc.getTransformByLabel(value);
418                         item.transform = {
419                             label     : value,
420                             transform : t,
421                             aggregate : egReportTemplateSvc.Transforms[t].aggregate ? true : false
422                         };
423                     }
424                 }}
425             );
426         });
427
428         fgrid.refresh();
429     }
430
431     $scope.changeOperator = function (items) {
432
433         var flist = [];
434         Object.keys(egReportTemplateSvc.Filters).forEach(function(k){
435             var v = egReportTemplateSvc.Filters[k];
436             if (flist.indexOf(v.label) == -1) flist.push(v.label);
437             if (v.labels && v.labels.length > 0) {
438                 v.labels.forEach(function(l) {
439                     if (flist.indexOf(l) == -1) flist.push(l);
440                 })
441             }
442         });
443
444         items.forEach(function(item) {
445             var l = item.operator ? item.operator.label : '';
446             egSelectDialog.open(
447                 egCore.strings.SELECT_OP, flist, l,
448                 {ok : function(value) {
449                     if (value) {
450                         var t = egReportTemplateSvc.getFilterByLabel(value);
451                         item.operator = { label: value, op : t };
452                     }
453                 }}
454             );
455         });
456
457         fgrid.refresh();
458     }
459
460     $scope.removeFilterValue = function (items) {
461         items.forEach(function(item) {delete egReportTemplateSvc.filter_fields[item.index].value});
462         fgrid.refresh();
463     }
464
465     $scope.removeFilterField = function (items) {
466         items.forEach(function(item) {egReportTemplateSvc.removeField('filter_fields', item)});
467         fgrid.refresh();
468     }
469
470     $scope.allSources = values(egCore.idl.classes).sort( function(a,b) {
471         if (a.core && !b.core) return -1;
472         if (b.core && !a.core) return 1;
473         aname = a.label ? a.label : a.name;
474         bname = b.label ? b.label : b.name;
475         if (aname > bname) return 1;
476         return -1;
477     });
478
479     $scope.class_tree = [];
480     $scope.selected_source = null;
481     $scope.selected_source_fields = [];
482     $scope.selected_source_field_list = [];
483     $scope.available_field_transforms = [];
484     $scope.coreSource = null;
485     $scope.coreSourceChosen = false;
486     $scope.currentPathLabel = '';
487
488     $scope.treeExpand = function (node, expanding) {
489         if (expanding) node.children.map(egCore.idl.classTree.fleshNode);
490     }
491
492     $scope.filterFields = function (n) {
493         return n.virtual ? false : true;
494         // should we hide links?
495         return n.datatype && n.datatype != 'link'
496     }
497
498     $scope.field_tree_opts = {
499         multiSelection: true,
500         equality      : function(node1, node2) {
501             return node1.name == node2.name;
502         }
503     }
504
505     $scope.field_transforms_tree_opts = {
506         equality : function(node1, node2) {
507             if (!node2) return false;
508             return node1.transform == node2.transform;
509         }
510     }
511
512     $scope.selectFields = function () {
513         while ($scope.available_field_transforms.length) {
514             $scope.available_field_transforms.pop();
515         }
516
517         angular.forEach( $scope.selected_source_field_list, function (f) {
518             angular.forEach(egReportTemplateSvc.Transforms, function (o,n) {
519                 if ( o.datatype.indexOf(f.datatype) > -1) {
520                     var include = true;
521
522                     angular.forEach($scope.available_field_transforms, function (t) {
523                         if (t.transform == n) include = false;
524                     });
525
526                     if (include) $scope.available_field_transforms.push({
527                         transform : n,
528                         label     : o.label,
529                         aggregate : o.aggregate ? true : false
530                     });
531                 }
532             });
533         });
534
535     }
536
537     $scope.selectSource = function (node, selected, $path) {
538
539         while ($scope.selected_source_field_list.length) {
540             $scope.selected_source_field_list.pop();
541         }
542         while ($scope.selected_source_fields.length) {
543             $scope.selected_source_fields.pop();
544         }
545
546         if (selected) {
547             $scope.currentPath = angular.copy( $path().reverse() );
548             $scope.selected_source = node;
549             $scope.currentPathLabel = $scope.currentPath.map(function(n,i){
550                 var l = n.label
551                 if (i) l += ' (' + n.jtype + ')';
552                 return l;
553             }).join( ' -> ' );
554             angular.forEach( node.fields, function (f) {
555                 $scope.selected_source_fields.push( f );
556             });
557         } else {
558             $scope.currentPathLabel = '';
559         }
560
561         // console.log($scope.selected_source);
562     }
563
564     $scope.changeCoreSource = function (new_core) {
565         console.log('changeCoreSource: '+new_core);
566         function change_core () {
567             if (new_core) $scope.coreSource = new_core;
568             $scope.coreSourceChosen = true;
569
570             $scope.class_tree.pop();
571             $scope.class_tree.push(
572                 egCore.idl.classTree.setTop($scope.coreSource)
573             );
574
575             while ($scope.selected_source_fields.length) {
576                 $scope.selected_source_fields.pop();
577             }
578
579             while ($scope.available_field_transforms.length) {
580                 $scope.available_field_transforms.pop();
581             }
582
583             $scope.currentPathLabel = '';
584         }
585
586         if ($scope.coreSourceChosen) {
587             egConfirmDialog.open(
588                 egCore.strings.FOLDERS_TEMPLATE,
589                 egCore.strings.SOURCE_SETUP_CONFIRM_EXIT,
590                 {ok : change_core}
591             );
592         } else {
593             change_core();
594         }
595     }
596
597     loadTemplate();
598 }])
599
600 ;