5dde15936fb4929b91ae290a37034fa11fd6bc85
[working/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                     // SQLBuilder relies on the first dash-separated component
123                     // of the join key to specify the column of left-hand relation
124                     // to join on; for has_many and might_have link types, we have to grab the
125                     // primary key of the left-hand table; otherwise, we can
126                     // just use the field/column name found in p.uplink.name.
127                     var uplink = (p.uplink.reltype == 'has_many' || p.uplink.reltype == 'might_have') ?
128                         egCore.idl.classes[p.from.split('.').slice(-1)[0]].pkey + '-' + p.uplink.name :
129                         p.uplink.name;
130                     join_path += '-' + uplink;
131                     alias = hex_md5(join_path);
132
133                     var uplink_alias = uplink + '-' + alias;
134
135                     if (!t.join) t.join = {};
136                     if (!t.join[uplink_alias]) t.join[uplink_alias] = {};
137
138                     t = t.join[uplink_alias];
139
140                     var djtype = 'inner';
141                     if (p.uplink.reltype != 'has_a') djtype = 'left';
142
143                     t.type = p.jtype || djtype;
144                     t.key = p.uplink.key;
145                 } else {
146                     join_path = p.classname + '-' + p.classname;
147                     alias = hex_md5(join_path);
148                 }
149
150                 if (!t.alias) t.alias = alias;
151                 t.path = join_path;
152
153                 t.table = p.struct.source ? p.struct.source : p.table;
154                 t.idlclass = p.classname;
155
156                 if (a.length == i + 1) { // end of the path array, need a predictable hash
157                     t.label = item.path_label;
158                     t.alias = hex_md5(item.path_label);
159                 }
160
161             });
162         });
163
164         return tree;
165     };
166     // expose for testing
167     $scope._mergePaths = mergePaths;
168
169     $scope.constructTemplate = function () {
170         var param_counter = 0;
171         return {
172             version     : 5,
173             doc_url     : $scope.templateDocURL,
174             core_class  : egCore.idl.classTree.top.classname,
175             select      : dgrid.allItems().map(function (i) {
176                             return {
177                                 alias     : i.label,
178                                 path      : i.path[i.path.length - 1].classname + '-' + i.name,
179                                 field_doc : i.doc_text,
180                                 relation  : hex_md5(i.path_label),
181                                 column    : {
182                                     colname         : i.name,
183                                     transform       : i.transform ? i.transform.transform : '',
184                                     transform_label : i.transform ? i.transform.label : '',
185                                     aggregate       : !!i.transform.aggregate
186                                 }
187                             }
188                           }),
189             from        : mergePaths( dgrid.allItems().concat(fgrid.allItems()) ),
190             where       : fgrid.allItems().filter(function(i) {
191                             return !i.transform.aggregate;
192                           }).map(function (i) {
193                             var cond = {};
194                             if (
195                                 i.operator.op == 'is' ||
196                                 i.operator.op == 'is not' ||
197                                 i.operator.op == 'is blank' ||
198                                 i.operator.op == 'is not blank'
199                             ) {
200                                 cond[i.operator.op] = null;
201                             } else {
202                                 if (i.value === undefined) {
203                                     cond[i.operator.op] = '::P' + param_counter++;
204                                 }else {
205                                     cond[i.operator.op] = i.value;
206                                 }
207                             }
208                             return {
209                                 alias     : i.label,
210                                 path      : i.path[i.path.length - 1].classname + '-' + i.name,
211                                 field_doc : i.doc_text,
212                                 relation  : hex_md5(i.path_label),
213                                 column    : {
214                                     colname         : i.name,
215                                     transform       : i.transform.transform,
216                                     transform_label : i.transform.label,
217                                     aggregate       : 0
218                                 },
219                                 condition : cond // constructed above
220                             }
221                           }),
222             having      : fgrid.allItems().filter(function(i) {
223                             return !!i.transform.aggregate;
224                           }).map(function (i) {
225                             var cond = {};
226                             cond[i.operator.op] = '::P' + param_counter++;
227                             return {
228                                 alias     : i.label,
229                                 path      : i.path[i.path.length - 1].classname + '-' + i.name,
230                                 field_doc : i.doc_text,
231                                 relation  : hex_md5(i.path_label),
232                                 column    : {
233                                     colname         : i.name,
234                                     transform       : i.transform.transform,
235                                     transform_label : i.transform.label,
236                                     aggregate       : 1
237                                 },
238                                 condition : cond // constructed above
239                             }
240                           }),
241             display_cols: angular.copy( dgrid.allItems() ).map(strip_item),
242             filter_cols : angular.copy( fgrid.allItems() ).map(strip_item)
243         };
244
245         function strip_item (i) {
246             delete i.children;
247             i.path.forEach(function(p){
248                 delete p.children;
249                 delete p.fields;
250                 delete p.links;
251                 delete p.struct.permacrud;
252                 delete p.struct.field_map;
253                 delete p.struct.fields;
254             });
255             return i;
256         }
257
258     }
259
260     $scope.upgradeTemplate = function(template) {
261         template.name(template.name() + ' (converted from XUL)');
262         template.data.version = 5;
263
264         var order_by;
265         var rels = [];
266         for (var key in template.data.rel_cache) {
267             if (key == 'order_by') {
268                 order_by = template.data.rel_cache[key];
269             } else {
270                 rels.push(template.data.rel_cache[key]);
271             }
272         }
273
274         template.data['display_cols'] = [];
275         template.data['filter_cols'] = [];
276
277         var dispcol_index = 0;
278
279         function _convertPath(orig, rel) {
280             var newPath = [];
281
282             var table_path = rel.path.split(/\./);
283             if (table_path.length > 1 || rel.path.indexOf('-') > -1) table_path.push( rel.idlclass );
284
285             var prev_type = '';
286             var prev_link = '';
287             table_path.forEach(function(link) {
288                 var cls = link.split(/-/)[0];
289                 var fld = link.split(/-/)[1];
290                 var args = {
291                     label : egCore.idl.classes[cls].label
292                 }
293                 if (prev_link != '') {
294                     args['from'] = prev_link.split(/-/)[0];
295                     var prev_col = prev_link.split(/-/)[1].split(/>/)[0];
296                     egCore.idl.classes[prev_link.split(/-/)[0]].fields.forEach(function(f) {
297                         if (prev_col == f.name) {
298                             args['link'] = f;
299                         }
300                     });
301                 }
302                 newPath.push(egCore.idl.classTree.buildNode(cls, args));
303                 prev_link = link;
304             });
305             return newPath;
306
307         }
308
309         rels.map(function(rel) {
310             for (var col in rel.fields.dis_tab) {
311                 var orig = rel.fields.dis_tab[col];
312                 var display_col = {
313                     name        : orig.colname,
314                     path        : _convertPath(orig, rel),
315                     index       : dispcol_index++,
316                     label       : orig.alias,
317                     datatype    : orig.datatype,
318                     doc_text    : orig.field_doc,
319                     transform   : {
320                                     label     : orig.transform_label,
321                                     transform : orig.transform,
322                                     aggregate : orig.aggregate
323                                   },
324                     path_label  : rel.label
325                 };
326                 template.data.display_cols.push(display_col);
327             }
328         });
329
330         rels.map(function(rel) {
331             for (var col in rel.fields.filter_tab) {
332                 var orig = rel.fields.filter_tab[col];
333                 var filter_col = {
334                     name        : orig.colname,
335                     path        : _convertPath(orig, rel),
336                     index       : dispcol_index++,
337                     label       : orig.alias,
338                     datatype    : orig.datatype,
339                     doc_text    : orig.field_doc,
340                     operator    : {
341                                     op        : orig.op,
342                                     label     : orig.op_label
343                                   },
344                     transform   : {
345                                     label     : orig.transform_label,
346                                     transform : orig.transform,
347                                     aggregate : orig.aggregate
348                                   },
349                     path_label  : rel.label
350                 };
351                 if ('value' in orig.op_value) {
352                     filter_col['value'] = orig.op_value.value;
353                 }
354                 template.data.filter_cols.push(filter_col);
355             }
356         });
357
358     }
359
360     function loadTemplate () {
361         if (!template_id) return;
362         egCore.pcrud.retrieve( 'rt', template_id)
363         .then( function(template) {
364             template.data = angular.fromJson(template.data());
365             if (template.data.version < 5) {
366                 $scope.upgradeTemplate(template);
367             }
368
369             $scope.templateName = template.name() + ' (clone)';
370             $scope.templateDescription = template.description();
371             $scope.templateDocURL = template.data.doc_url;
372
373             $scope.changeCoreSource( template.data.core_class );
374
375             egReportTemplateSvc.display_fields = template.data.display_cols;
376             egReportTemplateSvc.filter_fields = template.data.filter_cols;
377
378             $timeout(function(){
379                 dgrid.refresh();
380                 fgrid.refresh();
381             });
382         });
383
384     }
385
386     $scope.saveTemplate = function () {
387         var tmpl = new egCore.idl.rt();
388         tmpl.name( $scope.templateName );
389         tmpl.description( $scope.templateDescription );
390         tmpl.owner(egCore.auth.user().id());
391         tmpl.folder(folder_id);
392         tmpl.data(angular.toJson($scope.constructTemplate()));
393
394         egConfirmDialog.open(tmpl.name(), egCore.strings.TEMPLATE_CONF_CONFIRM_SAVE,
395             {ok : function() {
396                 return egCore.pcrud.create( tmpl )
397                 .then(
398                     function() {
399                         ngToast.create(egCore.strings.TEMPLATE_CONF_SUCCESS_SAVE);
400                         return $timeout(
401                             function(){
402                                 $window.location.href = egCore.env.basePath + 'reporter/legacy/main';
403                             },
404                             1000
405                         );
406                     },
407                     function() {
408                         ngToast.warning(egCore.strings.TEMPLATE_CONF_FAIL_SAVE);
409                     }
410                 );
411             }}
412         );
413     }
414
415     $scope.addDisplayFields = function () {
416         var t = $scope.selected_transform;
417         if (!t) t = default_transform_obj;
418
419         egReportTemplateSvc.addFields(
420             'display_fields',
421             $scope.selected_source_field_list, 
422             t,
423             $scope.currentPathLabel,
424             $scope.currentPath
425         );
426         dgrid.refresh();
427     }
428
429     $scope.addFilterFields = function () {
430         var t = $scope.selected_transform;
431         if (!t) t = default_transform_obj;
432         f = default_filter_obj;
433
434         egReportTemplateSvc.addFields(
435             'filter_fields',
436             $scope.selected_source_field_list, 
437             t,
438             $scope.currentPathLabel,
439             $scope.currentPath,
440             f
441         );
442         fgrid.refresh();
443     }
444
445     $scope.moveDisplayFieldUp = function (items) {
446         items.reverse().forEach(function(item) {
447             egReportTemplateSvc.moveFieldUp('display_fields', item);
448         });
449         dgrid.refresh();
450     }
451
452     $scope.moveDisplayFieldDown = function (items) {
453         items.forEach(function(item) {
454             egReportTemplateSvc.moveFieldDown('display_fields', item);
455         });
456         dgrid.refresh();
457     }
458
459     $scope.removeDisplayField = function (items) {
460         items.forEach(function(item) {egReportTemplateSvc.removeField('display_fields', item)});
461         dgrid.refresh();
462     }
463
464     $scope.changeDisplayLabel = function (items) {
465         items.forEach(function(item) {
466             egPromptDialog.open(egCore.strings.TEMPLATE_CONF_PROMPT_CHANGE, item.label || '',
467                 {ok : function(value) {
468                     if (value) egReportTemplateSvc.display_fields[item.index].label = value;
469                 }}
470             );
471         });
472         dgrid.refresh();
473     }
474
475     $scope.changeDisplayFieldDoc = function (items) {
476         items.forEach(function(item) {
477             egPromptDialog.open(egCore.strings.TEMPLATE_FIELD_DOC_PROMPT_CHANGE, item.doc_text || '',
478                 {ok : function(value) {
479                     if (value) egReportTemplateSvc.display_fields[item.index].doc_text = value;
480                 }}
481             );
482         });
483         dgrid.refresh();
484     }
485
486     $scope.changeFilterFieldDoc = function (items) {
487         items.forEach(function(item) {
488             egPromptDialog.open(egCore.strings.TEMPLATE_FIELD_DOC_PROMPT_CHANGE, item.doc_text || '',
489                 {ok : function(value) {
490                     if (value) egReportTemplateSvc.filter_fields[item.index].doc_text = value;
491                 }}
492             );
493         });
494         fgrid.refresh();
495     }
496
497     $scope.changeFilterValue = function (items) {
498         items.forEach(function(item) {
499             var l = null;
500             if (item.datatype == "bool") {
501                 var displayVal = typeof item.value === "undefined" ? egCore.strings.TEMPLATE_CONF_UNSET :
502                                  item.value === 't'                ? egCore.strings.TEMPLATE_CONF_TRUE :
503                                  item.value === 'f'                ? egCore.strings.TEMPLATE_CONF_FALSE :
504                                  item.value.toString();
505                 egConfirmDialog.open(egCore.strings.TEMPLATE_CONF_DEFAULT, displayVal,
506                     {ok : function() {
507                         egReportTemplateSvc.filter_fields[item.index].value = 't';
508                     },
509                     cancel : function() {
510                         egReportTemplateSvc.filter_fields[item.index].value = 'f';
511                     }}, egCore.strings.TEMPLATE_CONF_TRUE, egCore.strings.TEMPLATE_CONF_FALSE
512                 );
513             } else {
514                 egPromptDialog.open(egCore.strings.TEMPLATE_CONF_DEFAULT, item.value || '',
515                     {ok : function(value) {
516                         if (value) egReportTemplateSvc.updateFilterValue(item, value);
517                     }}
518                 );
519             }
520         });
521         fgrid.refresh();
522     }
523
524     $scope.changeTransform = function (items) {
525
526         var f = items[0];
527
528         var tlist = [];
529         angular.forEach(egReportTemplateSvc.Transforms, function (o,n) {
530             if ( o.datatype.indexOf(f.datatype) > -1) {
531                 if (tlist.indexOf(o.label) == -1) tlist.push( o.label );
532             }
533         });
534         
535         items.forEach(function(item) {
536             egSelectDialog.open(
537                 egCore.strings.SELECT_TFORM, tlist, item.transform.label,
538                 {ok : function(value) {
539                     if (value) {
540                         var t = egReportTemplateSvc.getTransformByLabel(value);
541                         item.transform = {
542                             label     : value,
543                             transform : t,
544                             aggregate : egReportTemplateSvc.Transforms[t].aggregate ? true : false
545                         };
546                     }
547                 }}
548             );
549         });
550
551         fgrid.refresh();
552     }
553
554     $scope.changeOperator = function (items) {
555
556         var flist = [];
557         Object.keys(egReportTemplateSvc.Filters).forEach(function(k){
558             var v = egReportTemplateSvc.Filters[k];
559             if (flist.indexOf(v.label) == -1) flist.push(v.label);
560             if (v.labels && v.labels.length > 0) {
561                 v.labels.forEach(function(l) {
562                     if (flist.indexOf(l) == -1) flist.push(l);
563                 })
564             }
565         });
566
567         items.forEach(function(item) {
568             var l = item.operator ? item.operator.label : '';
569             egSelectDialog.open(
570                 egCore.strings.SELECT_OP, flist, l,
571                 {ok : function(value) {
572                     if (value) {
573                         var t = egReportTemplateSvc.getFilterByLabel(value);
574                         item.operator = { label: value, op : t };
575
576                         //Update the filter value based on the new operator, because
577                         //  different operators treat the value differently
578                         egReportTemplateSvc.updateFilterValue(item, egReportTemplateSvc.filter_fields[item.index].value);
579                     }
580                 }}
581             );
582         });
583
584         fgrid.refresh();
585     }
586
587     $scope.removeFilterValue = function (items) {
588         items.forEach(function(item) {delete egReportTemplateSvc.filter_fields[item.index].value});
589         fgrid.refresh();
590     }
591
592     $scope.removeFilterField = function (items) {
593         items.forEach(function(item) {egReportTemplateSvc.removeField('filter_fields', item)});
594         fgrid.refresh();
595     }
596
597     $scope.allSources = values(egCore.idl.classes).sort( function(a,b) {
598         if (a.core && !b.core) return -1;
599         if (b.core && !a.core) return 1;
600         aname = a.label ? a.label : a.name;
601         bname = b.label ? b.label : b.name;
602         if (aname > bname) return 1;
603         return -1;
604     });
605
606     $scope.class_tree = [];
607     $scope.selected_source = null;
608     $scope.selected_source_fields = [];
609     $scope.selected_source_field_list = [];
610     $scope.available_field_transforms = [];
611     $scope.coreSource = null;
612     $scope.coreSourceChosen = false;
613     $scope.currentPathLabel = '';
614
615     $scope.treeExpand = function (node, expanding) {
616         if (expanding) node.children.map(egCore.idl.classTree.fleshNode);
617     }
618
619     $scope.filterFields = function (n) {
620         return n.virtual ? false : true;
621         // should we hide links?
622         return n.datatype && n.datatype != 'link'
623     }
624
625     $scope.field_tree_opts = {
626         multiSelection: true,
627         equality      : function(node1, node2) {
628             return node1.name == node2.name;
629         }
630     }
631
632     $scope.field_transforms_tree_opts = {
633         equality : function(node1, node2) {
634             if (!node2) return false;
635             return node1.transform == node2.transform;
636         }
637     }
638
639     $scope.selectFields = function () {
640         while ($scope.available_field_transforms.length) {
641             $scope.available_field_transforms.pop();
642         }
643
644         angular.forEach( $scope.selected_source_field_list, function (f) {
645             angular.forEach(egReportTemplateSvc.Transforms, function (o,n) {
646                 if ( o.datatype.indexOf(f.datatype) > -1) {
647                     var include = true;
648
649                     angular.forEach($scope.available_field_transforms, function (t) {
650                         if (t.transform == n) include = false;
651                     });
652
653                     if (include) $scope.available_field_transforms.push({
654                         transform : n,
655                         label     : o.label,
656                         aggregate : o.aggregate ? true : false
657                     });
658                 }
659             });
660         });
661
662     }
663
664     $scope.selectSource = function (node, selected, $path) {
665
666         while ($scope.selected_source_field_list.length) {
667             $scope.selected_source_field_list.pop();
668         }
669         while ($scope.selected_source_fields.length) {
670             $scope.selected_source_fields.pop();
671         }
672
673         if (selected) {
674             $scope.currentPath = angular.copy( $path().reverse() );
675             $scope.selected_source = node;
676             $scope.currentPathLabel = $scope.currentPath.map(function(n,i){
677                 var l = n.label
678                 if (i) l += ' (' + n.jtype + ')';
679                 return l;
680             }).join( ' -> ' );
681             angular.forEach( node.fields, function (f) {
682                 $scope.selected_source_fields.push( f );
683             });
684         } else {
685             $scope.currentPathLabel = '';
686         }
687
688         // console.log($scope.selected_source);
689     }
690
691     $scope.changeCoreSource = function (new_core) {
692         console.log('changeCoreSource: '+new_core);
693         function change_core () {
694             if (new_core) $scope.coreSource = new_core;
695             $scope.coreSourceChosen = true;
696
697             $scope.class_tree.pop();
698             $scope.class_tree.push(
699                 egCore.idl.classTree.setTop($scope.coreSource)
700             );
701
702             while ($scope.selected_source_fields.length) {
703                 $scope.selected_source_fields.pop();
704             }
705
706             while ($scope.available_field_transforms.length) {
707                 $scope.available_field_transforms.pop();
708             }
709
710             $scope.currentPathLabel = '';
711         }
712
713         if ($scope.coreSourceChosen) {
714             egConfirmDialog.open(
715                 egCore.strings.FOLDERS_TEMPLATE,
716                 egCore.strings.SOURCE_SETUP_CONFIRM_EXIT,
717                 {ok : change_core}
718             );
719         } else {
720             change_core();
721         }
722     }
723
724     loadTemplate();
725 }])
726
727 ;