LP#1721807: regression test
[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     // expose for testing
160     $scope._mergePaths = mergePaths;
161
162     $scope.constructTemplate = function () {
163         var param_counter = 0;
164         return {
165             version     : 5,
166             doc_url     : $scope.templateDocURL,
167             core_class  : egCore.idl.classTree.top.classname,
168             select      : dgrid.allItems().map(function (i) {
169                             return {
170                                 alias     : i.label,
171                                 path      : i.path[i.path.length - 1].classname + '-' + i.name,
172                                 field_doc : i.doc_text,
173                                 relation  : hex_md5(i.path_label),
174                                 column    : {
175                                     colname         : i.name,
176                                     transform       : i.transform ? i.transform.transform : '',
177                                     transform_label : i.transform ? i.transform.label : '',
178                                     aggregate       : !!i.transform.aggregate
179                                 }
180                             }
181                           }),
182             from        : mergePaths( dgrid.allItems().concat(fgrid.allItems()) ),
183             where       : fgrid.allItems().filter(function(i) {
184                             return !i.transform.aggregate;
185                           }).map(function (i) {
186                             var cond = {};
187                             if (
188                                 i.operator.op == 'is' ||
189                                 i.operator.op == 'is not' ||
190                                 i.operator.op == 'is blank' ||
191                                 i.operator.op == 'is not blank'
192                             ) {
193                                 cond[i.operator.op] = null;
194                             } else {
195                                 if (i.value === undefined) {
196                                     cond[i.operator.op] = '::P' + param_counter++;
197                                 }else {
198                                     cond[i.operator.op] = i.value;
199                                 }
200                             }
201                             return {
202                                 alias     : i.label,
203                                 path      : i.path[i.path.length - 1].classname + '-' + i.name,
204                                 field_doc : i.doc_text,
205                                 relation  : hex_md5(i.path_label),
206                                 column    : {
207                                     colname         : i.name,
208                                     transform       : i.transform.transform,
209                                     transform_label : i.transform.label,
210                                     aggregate       : 0
211                                 },
212                                 condition : cond // constructed above
213                             }
214                           }),
215             having      : fgrid.allItems().filter(function(i) {
216                             return !!i.transform.aggregate;
217                           }).map(function (i) {
218                             var cond = {};
219                             cond[i.operator.op] = '::P' + param_counter++;
220                             return {
221                                 alias     : i.label,
222                                 path      : i.path[i.path.length - 1].classname + '-' + i.name,
223                                 field_doc : i.doc_text,
224                                 relation  : hex_md5(i.path_label),
225                                 column    : {
226                                     colname         : i.name,
227                                     transform       : i.transform.transform,
228                                     transform_label : i.transform.label,
229                                     aggregate       : 1
230                                 },
231                                 condition : cond // constructed above
232                             }
233                           }),
234             display_cols: angular.copy( dgrid.allItems() ).map(strip_item),
235             filter_cols : angular.copy( fgrid.allItems() ).map(strip_item)
236         };
237
238         function strip_item (i) {
239             delete i.children;
240             i.path.forEach(function(p){
241                 delete p.children;
242                 delete p.fields;
243                 delete p.links;
244                 delete p.struct.permacrud;
245                 delete p.struct.field_map;
246                 delete p.struct.fields;
247             });
248             return i;
249         }
250
251     }
252
253     function loadTemplate () {
254         if (!template_id) return;
255         egCore.pcrud.retrieve( 'rt', template_id)
256         .then( function(template) {
257             template.data = angular.fromJson(template.data());
258             if (template.data.version < 5) { // redirect to old editor...
259                 $window.location.href = egCore.env.basePath + 'reporter/legacy/template/clone/'+folder_id + '/' + template_id;
260             // } else if (template.data.version < 5) { // redirect to old editor...
261             } else {
262                 $scope.templateName = template.name() + ' (clone)';
263                 $scope.templateDescription = template.description();
264                 $scope.templateDocURL = template.data.doc_url;
265
266                 $scope.changeCoreSource( template.data.core_class );
267
268                 egReportTemplateSvc.display_fields = template.data.display_cols;
269                 egReportTemplateSvc.filter_fields = template.data.filter_cols;
270
271                 $timeout(function(){
272                     dgrid.refresh();
273                     fgrid.refresh();
274                 });
275             }
276         });
277
278     }
279
280     $scope.saveTemplate = function () {
281         var tmpl = new egCore.idl.rt();
282         tmpl.name( $scope.templateName );
283         tmpl.description( $scope.templateDescription );
284         tmpl.owner(egCore.auth.user().id());
285         tmpl.folder(folder_id);
286         tmpl.data(angular.toJson($scope.constructTemplate()));
287
288         egConfirmDialog.open(tmpl.name(), egCore.strings.TEMPLATE_CONF_CONFIRM_SAVE,
289             {ok : function() {
290                 return egCore.pcrud.create( tmpl )
291                 .then(
292                     function() {
293                         ngToast.create(egCore.strings.TEMPLATE_CONF_SUCCESS_SAVE);
294                         return $timeout(
295                             function(){
296                                 $window.location.href = egCore.env.basePath + 'reporter/legacy/main';
297                             },
298                             1000
299                         );
300                     },
301                     function() {
302                         ngToast.warning(egCore.strings.TEMPLATE_CONF_FAIL_SAVE);
303                     }
304                 );
305             }}
306         );
307     }
308
309     $scope.addDisplayFields = function () {
310         var t = $scope.selected_transform;
311         if (!t) t = default_transform_obj;
312
313         egReportTemplateSvc.addFields(
314             'display_fields',
315             $scope.selected_source_field_list, 
316             t,
317             $scope.currentPathLabel,
318             $scope.currentPath
319         );
320         dgrid.refresh();
321     }
322
323     $scope.addFilterFields = function () {
324         var t = $scope.selected_transform;
325         if (!t) t = default_transform_obj;
326         f = default_filter_obj;
327
328         egReportTemplateSvc.addFields(
329             'filter_fields',
330             $scope.selected_source_field_list, 
331             t,
332             $scope.currentPathLabel,
333             $scope.currentPath,
334             f
335         );
336         fgrid.refresh();
337     }
338
339     $scope.moveDisplayFieldUp = function (items) {
340         items.reverse().forEach(function(item) {
341             egReportTemplateSvc.moveFieldUp('display_fields', item);
342         });
343         dgrid.refresh();
344     }
345
346     $scope.moveDisplayFieldDown = function (items) {
347         items.forEach(function(item) {
348             egReportTemplateSvc.moveFieldDown('display_fields', item);
349         });
350         dgrid.refresh();
351     }
352
353     $scope.removeDisplayField = function (items) {
354         items.forEach(function(item) {egReportTemplateSvc.removeField('display_fields', item)});
355         dgrid.refresh();
356     }
357
358     $scope.changeDisplayLabel = function (items) {
359         items.forEach(function(item) {
360             egPromptDialog.open(egCore.strings.TEMPLATE_CONF_PROMPT_CHANGE, item.label || '',
361                 {ok : function(value) {
362                     if (value) egReportTemplateSvc.display_fields[item.index].label = value;
363                 }}
364             );
365         });
366         dgrid.refresh();
367     }
368
369     $scope.changeDisplayFieldDoc = function (items) {
370         items.forEach(function(item) {
371             egPromptDialog.open(egCore.strings.TEMPLATE_FIELD_DOC_PROMPT_CHANGE, item.doc_text || '',
372                 {ok : function(value) {
373                     if (value) egReportTemplateSvc.display_fields[item.index].doc_text = value;
374                 }}
375             );
376         });
377         dgrid.refresh();
378     }
379
380     $scope.changeFilterFieldDoc = function (items) {
381         items.forEach(function(item) {
382             egPromptDialog.open(egCore.strings.TEMPLATE_FIELD_DOC_PROMPT_CHANGE, item.doc_text || '',
383                 {ok : function(value) {
384                     if (value) egReportTemplateSvc.filter_fields[item.index].doc_text = value;
385                 }}
386             );
387         });
388         fgrid.refresh();
389     }
390
391     $scope.changeFilterValue = function (items) {
392         items.forEach(function(item) {
393             var l = null;
394             egPromptDialog.open(egCore.strings.TEMPLATE_CONF_DEFAULT, item.value || '',
395                 {ok : function(value) {
396                     if (value) egReportTemplateSvc.filter_fields[item.index].value = value;
397                 }}
398             );
399         });
400         fgrid.refresh();
401     }
402
403     $scope.changeTransform = function (items) {
404
405         var f = items[0];
406
407         var tlist = [];
408         angular.forEach(egReportTemplateSvc.Transforms, function (o,n) {
409             if ( o.datatype.indexOf(f.datatype) > -1) {
410                 if (tlist.indexOf(o.label) == -1) tlist.push( o.label );
411             }
412         });
413         
414         items.forEach(function(item) {
415             egSelectDialog.open(
416                 egCore.strings.SELECT_TFORM, tlist, item.transform.label,
417                 {ok : function(value) {
418                     if (value) {
419                         var t = egReportTemplateSvc.getTransformByLabel(value);
420                         item.transform = {
421                             label     : value,
422                             transform : t,
423                             aggregate : egReportTemplateSvc.Transforms[t].aggregate ? true : false
424                         };
425                     }
426                 }}
427             );
428         });
429
430         fgrid.refresh();
431     }
432
433     $scope.changeOperator = function (items) {
434
435         var flist = [];
436         Object.keys(egReportTemplateSvc.Filters).forEach(function(k){
437             var v = egReportTemplateSvc.Filters[k];
438             if (flist.indexOf(v.label) == -1) flist.push(v.label);
439             if (v.labels && v.labels.length > 0) {
440                 v.labels.forEach(function(l) {
441                     if (flist.indexOf(l) == -1) flist.push(l);
442                 })
443             }
444         });
445
446         items.forEach(function(item) {
447             var l = item.operator ? item.operator.label : '';
448             egSelectDialog.open(
449                 egCore.strings.SELECT_OP, flist, l,
450                 {ok : function(value) {
451                     if (value) {
452                         var t = egReportTemplateSvc.getFilterByLabel(value);
453                         item.operator = { label: value, op : t };
454                     }
455                 }}
456             );
457         });
458
459         fgrid.refresh();
460     }
461
462     $scope.removeFilterValue = function (items) {
463         items.forEach(function(item) {delete egReportTemplateSvc.filter_fields[item.index].value});
464         fgrid.refresh();
465     }
466
467     $scope.removeFilterField = function (items) {
468         items.forEach(function(item) {egReportTemplateSvc.removeField('filter_fields', item)});
469         fgrid.refresh();
470     }
471
472     $scope.allSources = values(egCore.idl.classes).sort( function(a,b) {
473         if (a.core && !b.core) return -1;
474         if (b.core && !a.core) return 1;
475         aname = a.label ? a.label : a.name;
476         bname = b.label ? b.label : b.name;
477         if (aname > bname) return 1;
478         return -1;
479     });
480
481     $scope.class_tree = [];
482     $scope.selected_source = null;
483     $scope.selected_source_fields = [];
484     $scope.selected_source_field_list = [];
485     $scope.available_field_transforms = [];
486     $scope.coreSource = null;
487     $scope.coreSourceChosen = false;
488     $scope.currentPathLabel = '';
489
490     $scope.treeExpand = function (node, expanding) {
491         if (expanding) node.children.map(egCore.idl.classTree.fleshNode);
492     }
493
494     $scope.filterFields = function (n) {
495         return n.virtual ? false : true;
496         // should we hide links?
497         return n.datatype && n.datatype != 'link'
498     }
499
500     $scope.field_tree_opts = {
501         multiSelection: true,
502         equality      : function(node1, node2) {
503             return node1.name == node2.name;
504         }
505     }
506
507     $scope.field_transforms_tree_opts = {
508         equality : function(node1, node2) {
509             if (!node2) return false;
510             return node1.transform == node2.transform;
511         }
512     }
513
514     $scope.selectFields = function () {
515         while ($scope.available_field_transforms.length) {
516             $scope.available_field_transforms.pop();
517         }
518
519         angular.forEach( $scope.selected_source_field_list, function (f) {
520             angular.forEach(egReportTemplateSvc.Transforms, function (o,n) {
521                 if ( o.datatype.indexOf(f.datatype) > -1) {
522                     var include = true;
523
524                     angular.forEach($scope.available_field_transforms, function (t) {
525                         if (t.transform == n) include = false;
526                     });
527
528                     if (include) $scope.available_field_transforms.push({
529                         transform : n,
530                         label     : o.label,
531                         aggregate : o.aggregate ? true : false
532                     });
533                 }
534             });
535         });
536
537     }
538
539     $scope.selectSource = function (node, selected, $path) {
540
541         while ($scope.selected_source_field_list.length) {
542             $scope.selected_source_field_list.pop();
543         }
544         while ($scope.selected_source_fields.length) {
545             $scope.selected_source_fields.pop();
546         }
547
548         if (selected) {
549             $scope.currentPath = angular.copy( $path().reverse() );
550             $scope.selected_source = node;
551             $scope.currentPathLabel = $scope.currentPath.map(function(n,i){
552                 var l = n.label
553                 if (i) l += ' (' + n.jtype + ')';
554                 return l;
555             }).join( ' -> ' );
556             angular.forEach( node.fields, function (f) {
557                 $scope.selected_source_fields.push( f );
558             });
559         } else {
560             $scope.currentPathLabel = '';
561         }
562
563         // console.log($scope.selected_source);
564     }
565
566     $scope.changeCoreSource = function (new_core) {
567         console.log('changeCoreSource: '+new_core);
568         function change_core () {
569             if (new_core) $scope.coreSource = new_core;
570             $scope.coreSourceChosen = true;
571
572             $scope.class_tree.pop();
573             $scope.class_tree.push(
574                 egCore.idl.classTree.setTop($scope.coreSource)
575             );
576
577             while ($scope.selected_source_fields.length) {
578                 $scope.selected_source_fields.pop();
579             }
580
581             while ($scope.available_field_transforms.length) {
582                 $scope.available_field_transforms.pop();
583             }
584
585             $scope.currentPathLabel = '';
586         }
587
588         if ($scope.coreSourceChosen) {
589             egConfirmDialog.open(
590                 egCore.strings.FOLDERS_TEMPLATE,
591                 egCore.strings.SOURCE_SETUP_CONFIRM_EXIT,
592                 {ok : change_core}
593             );
594         } else {
595             change_core();
596         }
597     }
598
599     loadTemplate();
600 }])
601
602 ;