Simply put, it reports.
Sponsored-by: C/W MARS
Sponsored-by: Missouri Evergreen Consortium
Signed-off-by: Jason Boyer <JBoyer@equinoxOLI.org>
Signed-off-by: rfrasur <rfrasur@library.in.gov>
Signed-off-by: Mike Rylander <mrylander@gmail.com>
"postcss-selector-parser": "^6.0.4"
}
},
+ "ts-md5": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-1.2.9.tgz",
+ "integrity": "sha512-/Efr7ZfGf8P+d9HXh0PLQD1CDipqD8j9apCFG96pODDoEaFLxXpV4En6tAc6y3fWyfhFGrqtNBRBS+eLVIB2uQ=="
+ },
+ "ts-node": {
+ "version": "8.10.2",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz",
+ "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==",
+ "dev": true,
+ "requires": {
+ "browserslist": "^4.16.0",
+ "postcss-selector-parser": "^6.0.4"
+ }
+ },
"stylus": {
"version": "0.54.8",
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz",
"moment-timezone": "^0.5.33",
"ngx-cookie": "^5.0.2",
"rxjs": "^6.6.2",
- "zone.js": "^0.11.4"
+ "zone.js": "^0.11.4",
+ "ts-md5": "^1.2.9"
},
"//": "NOTE: version of angular/cli should be kept in sync with Open-ILS/src/extras/install/Makefile.common",
"devDependencies": {
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [
+ { path: 'simple',
+ loadChildren: () =>
+ import('./simple/simple-reporter.module').then(m => m.SimpleReporterModule)
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+
+export class ReporterRoutingModule {
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes, RouterStateSnapshot, ActivatedRouteSnapshot} from '@angular/router';
+import {SimpleReporterComponent} from './simple-reporter.component';
+import {SREditorComponent} from './sr-editor.component';
+import {SimpleReporterServiceResolver} from './simple-reporter.service';
+
+const routes: Routes = [
+ { path: '',
+ component: SimpleReporterComponent,
+ resolve: { srSvcResolver: SimpleReporterServiceResolver },
+ },
+ { path: 'new',
+ component: SREditorComponent,
+ resolve: { srSvcResolver: SimpleReporterServiceResolver },
+ canDeactivate: ['canLeaveEditor'],
+ runGuardsAndResolvers: 'always'
+ },
+ { path: 'edit/:id',
+ component: SREditorComponent,
+ resolve: { srSvcResolver: SimpleReporterServiceResolver },
+ canDeactivate: ['canLeaveEditor'],
+ runGuardsAndResolvers: 'always'
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+ providers: [SimpleReporterServiceResolver,
+ {
+ provide: 'canLeaveEditor',
+ useValue: (component: SREditorComponent, currentRoute: ActivatedRouteSnapshot,
+ currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) => component.canLeaveEditor()
+ }
+ ]
+})
+
+export class SimpleReporterRoutingModule {
+}
+
--- /dev/null
+<eg-staff-banner bannerText="Simple Reports" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="row" id="simple-reporter-main">
+ <div class="col-lg-12">
+ <ul ngbNav #simpleRptTabs="ngbNav" class="nav-tabs">
+ <li [ngbNavItem]="'myreports'">
+ <a ngbNavLink i18n>My Reports</a>
+ <ng-template ngbNavContent><eg-sr-reports></eg-sr-reports></ng-template>
+ </li>
+ <li [ngbNavItem]="'outputs'">
+ <a ngbNavLink i18n>My Outputs</a>
+ <ng-template ngbNavContent><eg-sr-outputs></eg-sr-outputs></ng-template>
+ </li>
+ </ul>
+ <div [ngbNavOutlet]="simpleRptTabs"></div>
+ </div>
+</div>
--- /dev/null
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router} from '@angular/router';
+import {NgbNav} from '@ng-bootstrap/ng-bootstrap';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {SimpleReporterService} from './simple-reporter.service';
+import {SROutputsComponent} from './sr-my-outputs.component';
+
+@Component({
+ templateUrl: './simple-reporter.component.html',
+})
+
+export class SimpleReporterComponent implements OnInit {
+
+ @ViewChild('simpleRptTabs', { static: true }) tabs: NgbNav;
+
+ constructor(
+ private router: Router,
+ private auth: AuthService,
+ private idl: IdlService,
+ private pcrud: PcrudService,
+ private srSvc: SimpleReporterService
+ ) {
+
+ }
+
+
+ ngOnInit() {
+
+ }
+
+}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {OrgFamilySelectModule} from '@eg/share/org-family-select/org-family-select.module';
+import {SimpleReporterComponent} from './simple-reporter.component';
+import {SROutputsComponent} from './sr-my-outputs.component';
+import {SRReportsComponent} from './sr-my-reports.component';
+import {SREditorComponent} from './sr-editor.component';
+import {SRFieldChooserComponent} from './sr-field-chooser.component';
+import {SRSortOrderComponent} from './sr-sort-order.component';
+import {SROutputOptionsComponent} from './sr-output-options.component';
+import {SRFieldComponent} from './sr-field.component';
+import {SimpleReporterRoutingModule} from './routing.module';
+
+@NgModule({
+ declarations: [
+ SimpleReporterComponent,
+ SROutputsComponent,
+ SRReportsComponent,
+ SRFieldChooserComponent,
+ SRSortOrderComponent,
+ SROutputOptionsComponent,
+ SRFieldComponent,
+ SREditorComponent
+ ],
+ imports: [
+ StaffCommonModule,
+ SimpleReporterRoutingModule,
+ OrgFamilySelectModule
+ ]
+})
+
+export class SimpleReporterModule {
+}
+
--- /dev/null
+import {Injectable} from '@angular/core';
+import {Router, Resolve, RouterStateSnapshot,
+ ActivatedRouteSnapshot} from '@angular/router';
+import * as moment from 'moment-timezone';
+import {Md5} from 'ts-md5';
+import {EMPTY} from 'rxjs';
+import {map, mergeMap, defaultIfEmpty, last} from 'rxjs/operators';
+import {Observable, of, from} from 'rxjs';
+import {AuthService} from '@eg/core/auth.service';
+import {PermService} from '@eg/core/perm.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {Pager} from '@eg/share/util/pager';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+
+const defaultFolderName = 'Simple Reporter';
+
+const transforms = [
+ {
+ name: 'Bare',
+ aggregate: false
+ },
+ {
+ name: 'upper',
+ aggregate: false,
+ datatypes: ['text']
+ },
+ {
+ name: 'lower',
+ aggregate: false,
+ datatypes: ['text']
+ },
+ {
+ name: 'substring',
+ aggregate: false,
+ datatypes: ['text']
+ },
+ {
+ name: 'day_name',
+ final_datatype: 'text',
+ aggregate: false,
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'month_name',
+ final_datatype: 'text',
+ aggregate: false,
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'doy',
+ aggregate: false,
+ final_datatype: 'number',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'woy',
+ aggregate: false,
+ final_datatype: 'number',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'moy',
+ aggregate: false,
+ final_datatype: 'number',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'qoy',
+ aggregate: false,
+ final_datatype: 'number',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'dom',
+ aggregate: false,
+ final_datatype: 'number',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'dow',
+ aggregate: false,
+ final_datatype: 'number',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'year_trunc',
+ aggregate: false,
+ final_datatype: 'number',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'month_trunc',
+ aggregate: false,
+ final_datatype: 'text',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'date_trunc',
+ aggregate: false,
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'hour_trunc',
+ aggregate: false,
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'quarter',
+ aggregate: false,
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'months_ago',
+ aggregate: false,
+ final_datatype: 'number',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'hod',
+ aggregate: false,
+ final_datatype: 'number',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'quarters_ago',
+ aggregate: false,
+ final_datatype: 'number',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'age',
+ aggregate: false,
+ final_datatype: 'interval',
+ datatypes: ['timestamp']
+ },
+ {
+ name: 'first',
+ aggregate: true
+ },
+ {
+ name: 'last',
+ aggregate: true
+ },
+ {
+ name: 'min',
+ aggregate: true
+ },
+ {
+ name: 'max',
+ aggregate: true
+ },
+ // "Simple" would be to only offer the choice that's almost always what you mean.
+ /*{
+ name: 'count',
+ aggregate: true
+ },*/
+ {
+ name: 'count_distinct',
+ final_datatype: 'number',
+ aggregate: true
+ },
+ {
+ name: 'sum',
+ aggregate: true,
+ datatypes: ['float', 'int', 'money', 'number']
+ },
+ {
+ name: 'average',
+ aggregate: true,
+ datatypes: ['float', 'int', 'money', 'number']
+ }
+];
+
+const operators = [
+ {
+ name: '=',
+ datatypes: ['link', 'text', 'timestamp', 'interval', 'float', 'int', 'money', 'number'],
+ arity: 1
+ },
+ {
+ arity: 1,
+ datatypes: ['bool', 'org_unit'],
+ name: '= any'
+ },
+ {
+ arity: 1,
+ datatypes: ['bool', 'org_unit'],
+ name: '<> any'
+ },
+ // If I had a dollar for every time someone wanted a case sensitive substring search, I might be able to buy a coffee.
+ /*{
+ name: 'like',
+ arity: 1,
+ datatypes: ['text']
+ },*/
+ {
+ name: 'ilike',
+ arity: 1,
+ datatypes: ['text']
+ },
+ {
+ name: '>',
+ arity: 1,
+ datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number']
+ },
+ {
+ name: '>=',
+ arity: 1,
+ datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number']
+ },
+ {
+ name: '<',
+ arity: 1,
+ datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number']
+
+ },
+ {
+ name: '<=',
+ arity: 1,
+ datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number']
+
+ },
+ {
+ name: 'in',
+ arity: 3,
+ datatypes: ['text', 'link', 'org_unit', 'float', 'int', 'money', 'number']
+ },
+ {
+ name: 'not in',
+ arity: 3,
+ datatypes: ['text', 'link', 'org_unit', 'float', 'int', 'money', 'number']
+
+ },
+ {
+ name: 'between',
+ arity: 2,
+ datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number']
+
+ },
+ {
+ name: 'not between',
+ arity: 2,
+ datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number']
+
+ },
+ {
+ arity: 0,
+ name: 'is'
+ },
+ {
+ arity: 0,
+ name: 'is not'
+ },
+ {
+ arity: 0,
+ name: 'is blank',
+ datatypes: ['text']
+ },
+ {
+ arity: 0,
+ name: 'is not blank',
+ datatypes: ['text']
+ }
+];
+
+const DEFAULT_TRANSFORM = 'Bare';
+const DEFAULT_OPERATOR = '=';
+
+export class SRTemplate {
+ id = -1;
+ rtIdl: IdlObject = null;
+ name = '';
+ description = ''; // description isn't currently used but someday could be
+ create_time = null;
+ fmClass = '';
+ displayFields: IdlObject[] = [];
+ orderByNames: string[] = [];
+ filterFields: IdlObject[] = [];
+ isNew = true;
+ recurring = false;
+ recurrence = null;
+ excelOutput = false;
+ csvOutput = true;
+ htmlOutput = true;
+ barCharts = false;
+ lineCharts = false;
+ email = '';
+ runNow = 'now';
+ runTime: moment.Moment = null;
+
+ constructor(idlObj: IdlObject = null) {
+ if ( idlObj !== null ) {
+ this.isNew = false;
+ this.id = Number(idlObj.id());
+ this.create_time = idlObj.create_time();
+ this.name = idlObj.name();
+ this.description = idlObj.description();
+
+ const simple_report = JSON.parse(idlObj.data()).simple_report;
+ this.fmClass = simple_report.fmClass;
+ this.displayFields = simple_report.displayFields;
+ this.orderByNames = simple_report.orderByNames;
+ this.filterFields = simple_report.filterFields;
+ if (idlObj.reports()?.length) {
+ const activeReport = idlObj.reports().reduce((prev, curr) =>
+ prev.create_time() > curr.create_time() ? prev : curr
+ );
+ if (activeReport) {
+ this.recurring = activeReport.recur() === 't';
+ this.recurrence = activeReport.recurrence();
+ }
+ // then fetch the most recent completed rs
+ if (activeReport.runs().length) {
+ const latestSched = activeReport.runs().reduce((prev, curr) =>
+ prev.run_time() > curr.run_time() ? prev : curr
+ );
+ if (latestSched) {
+ this.excelOutput = latestSched.excel_format() === 't';
+ this.csvOutput = latestSched.csv_format() === 't';
+ this.htmlOutput = latestSched.html_format() === 't';
+ this.barCharts = latestSched.chart_bar() === 't';
+ this.lineCharts = latestSched.chart_line() === 't';
+ this.email = latestSched.email();
+ this.runTime = latestSched.run_time().length ? moment(latestSched.run_time()) : moment();
+ this.runNow = this.runTime.isAfter(moment()) ? 'later' : 'now';
+ }
+ }
+ }
+ }
+ }
+}
+
+
+@Injectable({
+ providedIn: 'root'
+})
+export class SimpleReporterService {
+
+ templateFolder: IdlObject = null;
+ reportFolder: IdlObject = null;
+ outputFolder: IdlObject = null;
+
+ constructor (
+ private evt: EventService,
+ private auth: AuthService,
+ private idl: IdlService,
+ private pcrud: PcrudService
+ ) {
+ }
+
+ _initFolders(): Promise<any[]> {
+ if (this.templateFolder &&
+ this.reportFolder &&
+ this.outputFolder
+ ) {
+ return Promise.resolve([]);
+ }
+
+ return Promise.all([
+ new Promise((resolve, reject) => {
+ // Verify folders exist, create if not
+ this.getDefaultFolder('rtf')
+ .then(f => {
+ if (f) {
+ this.templateFolder = f;
+ resolve();
+ } else {
+ this.createDefaultFolder('rtf')
+ .then(n => {
+ this.templateFolder = n;
+ resolve();
+ });
+ }
+ });
+ }),
+ new Promise((resolve, reject) => {
+ this.getDefaultFolder('rrf')
+ .then(f => {
+ if (f) {
+ this.reportFolder = f;
+ resolve();
+ } else {
+ this.createDefaultFolder('rrf')
+ .then(n => {
+ this.reportFolder = n;
+ resolve();
+ });
+ }
+ });
+ }),
+ new Promise((resolve, reject) => {
+ this.getDefaultFolder('rof')
+ .then(f => {
+ if (f) {
+ resolve();
+ this.outputFolder = f;
+ } else {
+ this.createDefaultFolder('rof')
+ .then(n => {
+ this.outputFolder = n;
+ resolve();
+ });
+ }
+ });
+ })
+ ]);
+ }
+
+ getTransformsForDatatype(datatype: string) {
+ const ret = [];
+ transforms.forEach(el => {
+ if ( typeof el.datatypes === 'undefined' ||
+ (el.datatypes.findIndex(dt => dt === datatype) > -1) ) {
+ ret.push(el);
+ }
+ });
+ return ret;
+ }
+
+ getOperatorsForDatatype(datatype: string) {
+ const ret = [];
+ operators.forEach(el => {
+ if ( typeof el.datatypes === 'undefined' ||
+ (el.datatypes.findIndex(dt => dt === datatype) > -1) ) {
+ ret.push(el);
+ }
+ });
+ return ret;
+ }
+
+ defaultTransform() {
+ return this.getTransformByName(DEFAULT_TRANSFORM);
+ }
+
+ defaultOperator(dt) {
+ if (this.getOperatorByName(DEFAULT_OPERATOR).datatypes.indexOf(dt) >= 0) {
+ return this.getOperatorByName(DEFAULT_OPERATOR);
+ }
+ return this.getOperatorsForDatatype(dt)[0];
+ }
+
+ getTransformByName(name: string) {
+ return { ...transforms[transforms.findIndex(el => el.name === name)] };
+ }
+
+ getGenericTransformWithParams(ary: Array<any>) {
+ const copy = Array.from(ary);
+ const name = copy.shift();
+ const params = [];
+ const transform = { name: name, aggregate: false };
+
+ copy.forEach(el => {
+ switch (el) { // for now there's only user id for the current user, but in future ... ??
+ case 'SR__USER_ID':
+ params.push(this.auth.user().id());
+ break;
+ default:
+ params.push(el);
+ }
+ });
+
+ if ( params.length ) {
+ transform['params'] = params;
+ }
+
+ return transform;
+ }
+
+ getOperatorByName(name: string) {
+ return { ...operators[operators.findIndex(el => el.name === name)] };
+ }
+
+ getDefaultFolder(fmClass: string): Promise<IdlObject> {
+ return this.pcrud.search(fmClass,
+ { owner: this.auth.user().id(), 'simple_reporter': 't', name: defaultFolderName },
+ {}).toPromise();
+ }
+
+ createDefaultFolder(fmClass: string): Promise<IdlObject> {
+ const rf = this.idl.create(fmClass);
+ rf.isnew(true);
+ rf.owner(this.auth.user().id());
+ rf.simple_reporter(true);
+ rf.name(defaultFolderName);
+ return this.pcrud.create(rf).toPromise();
+ }
+
+ loadTemplate(id: number): Promise<IdlObject> {
+ const searchOps = {
+ flesh: 2,
+ flesh_fields: {
+ rt: ['reports'],
+ rr: ['runs']
+ }
+ };
+ return this.pcrud.search('rt', { id: id }, searchOps).toPromise();
+ }
+
+ saveTemplate(
+ templ: SRTemplate,
+ scheduleNow: boolean = false
+ ): Promise<any> { // IdlObject or Number? It depends!
+ const rtData = this.buildTemplateData(templ);
+
+ // gather our parameters
+ const rrData = {};
+ templ.filterFields.forEach((el, idx) => {
+ rrData[el.filter_placeholder] = el.force_filtervalues ? el.force_filtervalues : el.filter_value;
+ });
+
+ // Here's where we'd add rr-flags like __do_rollup to rrData
+
+ const rtIdl = this.idl.create('rt');
+ const rrIdl = this.idl.create('rr');
+
+ if ( templ.id === -1 ) {
+ rtIdl.isnew(true);
+ rrIdl.isnew(true);
+ } else {
+ rtIdl.isnew(false);
+ rrIdl.isnew(false);
+ rtIdl.id(templ.id);
+ rtIdl.create_time(templ.create_time);
+ }
+ rtIdl.name(templ.name);
+ rtIdl.description(templ.description);
+ rtIdl.data(JSON.stringify(rtData));
+ rtIdl.owner(this.auth.user().id());
+ rtIdl.folder(this.templateFolder.id());
+
+ rrIdl.name(templ.name);
+ rrIdl.data(JSON.stringify(rrData));
+ rrIdl.owner(this.auth.user().id());
+ rrIdl.folder(this.reportFolder.id());
+ rrIdl.template(templ.id);
+ rrIdl.create_time('now'); // rr create time is serving as the edit time
+ // of the SR template as a whole
+
+ rrIdl.recur(templ.recurring ? 't' : 'f');
+ rrIdl.recurrence(templ.recurrence);
+
+ return this.pcrud.search('rt', { name: rtIdl.name(), folder: rtIdl.folder() })
+ .pipe(defaultIfEmpty(rtIdl), map(existing => {
+ if (existing.id() !== rtIdl.id()) { // oh no! dup name
+ throw new Error(': Duplicate Report Name');
+ }
+
+ if ( templ.id === -1 ) {
+ return this.pcrud.create(rtIdl).pipe(mergeMap(rt => {
+ rrIdl.template(rt.id());
+ // after saving the rr, return an Observable of the rt
+ // to the caller
+ return this.pcrud.create(rrIdl).pipe(mergeMap(
+ rr => this.scheduleReport(templ, rr, scheduleNow).pipe(mergeMap(rs => of(rt)))
+ ));
+ })).toPromise();
+ } else {
+ const emptyRR = this.idl.create('rr');
+ emptyRR.id('no_rr');
+ return this.pcrud.update(rtIdl).pipe(mergeMap(rtId => {
+ // we may or may not have the rr already created, so
+ // test and act accordingly
+ return this.pcrud.search('rr', { template: rtId }).pipe(defaultIfEmpty(emptyRR), mergeMap(rr => {
+ if (rr.id() === 'no_rr') {
+ rrIdl.isnew(true);
+ return this.pcrud.create(rrIdl).pipe(mergeMap(rr2 =>
+ this.scheduleReport(templ, rr2, scheduleNow).pipe(mergeMap(rs => of(rtId)))
+ ));
+ } else {
+ rr.create_time('now'); // rr create time is serving as the
+ // edit time of the SR template as a whole
+ rr.recur(templ.recurring ? 't' : 'f');
+ rr.recurrence(templ.recurrence);
+ rr.data(rrIdl.data());
+ return this.pcrud.update(rr).pipe(mergeMap(
+ rr2 => this.scheduleReport(templ, rr, scheduleNow).pipe(mergeMap(rs => of(rtId) ))
+ ));
+ }
+ }));
+ })).toPromise();
+ }
+ })).toPromise();
+ }
+
+ scheduleReport(templ: SRTemplate, rr: IdlObject, scheduleNow: boolean): Observable<IdlObject> {
+ const rs = this.idl.create('rs');
+ if (!scheduleNow) {
+ return of(rs); // return a placeholder
+ }
+ rs.isnew(true);
+ rs.report(rr.id());
+ rs.folder(this.outputFolder.id());
+ rs.runner(rr.owner());
+ if (templ.runNow === 'now') {
+ rs.run_time('now');
+ } else {
+ rs.run_time(templ.runTime.toISOString());
+ }
+ rs.email(templ.email);
+ rs.excel_format(templ.excelOutput ? 't' : 'f');
+ rs.csv_format(templ.csvOutput ? 't' : 'f');
+ rs.html_format(templ.htmlOutput ? 't' : 'f');
+ rs.chart_line(templ.lineCharts ? 't' : 'f');
+ rs.chart_bar(templ.barCharts ? 't' : 'f');
+ rs.isnew(true);
+
+ // clear any un-run schedules, then add the new one
+ const emptyRS = this.idl.create('rs');
+ emptyRS.id('no_rs');
+ return this.pcrud.search('rs', { report: rr.id(), start_time: {'=' : null} }, {}, {atomic: true}).pipe(mergeMap(old_rs => {
+ if (old_rs.length > 0) {
+ old_rs.forEach(x => x.isdeleted(true));
+ old_rs.push(rs);
+ return this.pcrud.autoApply(old_rs).pipe(last()); // note that we don't care
+ // what the last one processed
+ // actually is
+ } else {
+ return this.pcrud.create(rs);
+ }
+ }));
+ }
+
+ // The template generated by this can obviously be trimmed to only those things
+ // that SQLBuilder.pm cares about, but for now it's basically the same as the
+ // existing template builder.
+ buildTemplateData(
+ templ: SRTemplate
+ ) {
+ const fmClass = templ.fmClass;
+ const sourceClass = this.idl.classes[fmClass];
+ const md5Name = Md5.hashStr(fmClass); // Just the one with SR since there are no joins
+ let conditionCount = 0;
+
+ // The simplified template that can be edited and re-saved
+ const simpleReport = {
+ name: templ.name,
+ fmClass: fmClass,
+ displayFields: templ.displayFields,
+ orderByNames: templ.orderByNames,
+ filterFields: templ.filterFields,
+ };
+ const reportTemplate = {
+ simple_report: simpleReport,
+ version: 5,
+ core_class: fmClass,
+ 'from': {
+ alias: md5Name,
+ path: fmClass + '-' + fmClass,
+ table: sourceClass.source,
+ idlclass: fmClass,
+ label: sourceClass.label
+ },
+ select: [],
+ where: [],
+ having: [],
+ order_by: []
+ };
+ // fill in select[] and display_cols[] simultaneously
+ templ.displayFields.forEach((el, idx) => {
+ reportTemplate.select.push({
+ alias: el.alias,
+ path: fmClass + '-' + el.name,
+ relation: md5Name,
+ column: {
+ colname: el.name,
+ transform: el.transform.name,
+ aggregate: el.transform.aggregate
+ }
+ });
+
+ }); // select[]
+
+ // where[] and having[] are the same save for aggregate == true
+ templ.filterFields.forEach((el, idx) => {
+ let whereObj = {};
+
+ whereObj = {
+ alias: el.alias,
+ path: fmClass + '-' + el.name,
+ relation: md5Name,
+ column: {
+ colname: el.name,
+ transform: el.transform.name,
+ aggregate: el.transform.aggregate
+ },
+ condition: {}
+ };
+
+ // No test for el.filterValue because currently all filter values are assigned at schedule time
+ whereObj['condition'][el.operator.name] = '::P' + conditionCount;
+ el.filter_placeholder = 'P' + conditionCount;
+ conditionCount++;
+
+ // handle force transforms
+ if (el.force_transform) {
+ whereObj['column']['params'] = el.transform.params;
+ }
+
+ if ( el.transform.aggregate ) {
+ reportTemplate.having.push(whereObj);
+ } else {
+ reportTemplate.where.push(whereObj);
+ }
+
+ }); // where[] and having[]
+
+ templ.orderByNames.forEach(ob => { // order_by and select have the same shape
+ const el = templ.displayFields[templ.displayFields.findIndex(fl => fl.name === ob)];
+ reportTemplate.order_by.push({
+ alias: el.alias,
+ path: fmClass + '-' + el.name,
+ relation: md5Name,
+ direction: el.direction ? el.direction : 'ascending',
+ column: {
+ colname: el.name,
+ transform: el.transform.name,
+ aggregate: el.transform.aggregate
+ }
+ });
+ });
+
+ return reportTemplate;
+ }
+
+ getOutputDatasource() {
+ const gridSource = new GridDataSource();
+
+ gridSource.sort = [{ name: 'complete_time', dir: 'DESC' }];
+
+ gridSource.getRows = (pager: Pager, sort: any[]) => {
+
+ // start setting up query
+ const base: Object = {};
+ base['runner'] = this.auth.user().id();
+ base['output_folder'] = this.outputFolder.id();
+ const query: any = new Array();
+ query.push(base);
+
+ // and add any filters
+ Object.keys(gridSource.filters).forEach(key => {
+ Object.keys(gridSource.filters[key]).forEach(key2 => {
+ query.push(gridSource.filters[key][key2]);
+ });
+ });
+
+ const orderBy: any = {};
+ if (sort.length) {
+ orderBy.rcr = sort[0].name + ' ' + sort[0].dir;
+ }
+
+ const searchOpts = {
+ flesh: 2,
+ flesh_fields: {
+ rcr: ['run'],
+ },
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy
+ };
+
+ return this.pcrud.search('rcr', query, searchOpts)
+ .pipe(map(row => {
+ if ( this.evt.parse(row) ) {
+ throw new Error(row);
+ } else {
+ return {
+ template_name: row.template_name(),
+ complete_time: row.complete_time(),
+ id: row.run().id(),
+ report_id: row.report(),
+ template_id: row.template(),
+ error_code: row.run().error_code(),
+ error_text: row.run().error_text(),
+ _rs: row.run()
+ };
+ }
+ }));
+
+ };
+
+ return gridSource;
+ }
+
+ getReportsDatasource() {
+ const gridSource = new GridDataSource();
+
+ gridSource.getRows = (pager: Pager, sort: any[]) => {
+ const orderBy: any = {};
+
+ if (sort.length) {
+ orderBy.rt = sort[0].name + ' ' + sort[0].dir;
+ } else {
+ orderBy.rt = 'create_time desc';
+ }
+
+ // start setting up query
+ const base: Object = {};
+ base['owner'] = this.auth.user().id();
+ base['folder'] = this.templateFolder.id();
+
+ const query: any = new Array();
+ query.push(base);
+
+ // and add any filters
+ Object.keys(gridSource.filters).forEach(key => {
+ Object.keys(gridSource.filters[key]).forEach(key2 => {
+ query.push(gridSource.filters[key][key2]);
+ });
+ });
+
+ const searchOps = {
+ flesh: 2,
+ flesh_fields: {
+ rt: ['reports'],
+ rr: ['runs']
+ },
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy
+ };
+
+ return this.pcrud.search('rt', query, searchOps).pipe(map(row => {
+ let edit_time = null;
+ let last_run = null;
+ let past = [];
+ let future = [];
+ let next_run = null;
+ let recurring = false;
+
+ // there should be exactly one rr associated with the template,
+ // but in case not, we'll just pick the one with the most
+ // recent create time
+ if (row.reports().length) {
+ const activeReport = row.reports().reduce((prev, curr) =>
+ prev.create_time() > curr.create_time() ? prev : curr
+ );
+ if (activeReport) {
+ // note that we're (ab)using the rr create_time
+ // to be the edit time of the SR rt + rr combo
+ edit_time = activeReport.create_time();
+ recurring = activeReport.recur() === 't';
+ }
+ // then fetch the most recent completed rs
+ if (activeReport.runs().length) {
+ let lastRun = null;
+ past = activeReport.runs().filter(el => el.start_time() !== null);
+ if (past.length) {
+ lastRun = past.reduce((prev, curr) =>
+ prev.complete_time() > curr.complete_time() ? prev : curr
+ );
+ }
+ if (lastRun) {
+ last_run = lastRun.complete_time();
+ }
+
+ // And the next rs not yet in progress
+ let nextRun = null;
+ future = activeReport.runs().filter(el => el.start_time() === null);
+ if (future.length) {
+ nextRun = future.reduce((prev, curr) =>
+ prev.run_time() < curr.run_time() ? prev : curr
+ );
+ }
+ if (nextRun) {
+ next_run = nextRun.run_time();
+ }
+ }
+ }
+ return {
+ name: row.name(),
+ rt_id: row.id(),
+ create_time: row.create_time(),
+ edit_time: edit_time,
+ last_run: last_run,
+ next_run: next_run,
+ recurring: recurring,
+ };
+ }));
+ };
+
+ return gridSource;
+ }
+}
+
+@Injectable()
+export class SimpleReporterServiceResolver implements Resolve<Promise<any[]>> {
+
+ constructor(
+ private router: Router,
+ private perm: PermService,
+ private svc: SimpleReporterService
+ ) {}
+
+ resolve(
+ route: ActivatedRouteSnapshot,
+ state: RouterStateSnapshot): Promise<any[]> {
+
+ return from(this.perm.hasWorkPermHere('RUN_SIMPLE_REPORTS')).pipe(mergeMap(
+ permResult => {
+ if (permResult['RUN_SIMPLE_REPORTS']) {
+ return Promise.all([
+ this.svc._initFolders()
+ ]);
+ } else {
+ this.router.navigate(['/staff/no_permission']);
+ return EMPTY;
+ }
+ }
+ )).toPromise();
+ }
+
+}
--- /dev/null
+.sr-name-empty {
+ border-left: 5px solid #FA787E;
+}
+
+.sr-name-notempty {
+ border-left: 5px solid #78FA89;
+}
--- /dev/null
+<eg-string #templateSaved i18n-text text="Report Saved Successfully"></eg-string>
+<eg-string #templateSaveError i18n-text text="Error Saving Report"></eg-string>
+<eg-string #newTitle i18n-text text="New Simple Report"></eg-string>
+<eg-string #editTitle i18n-text text="Edit Simple Report"></eg-string>
+
+<eg-staff-banner #banner [bannerText]="pageTitle">
+</eg-staff-banner>
+
+<eg-confirm-dialog
+ #changeTypeDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Report Type Change"
+ dialogBody="You have already started creating a report; changing the report type will remove your progress. Continue?"
+></eg-confirm-dialog>
+
+<eg-confirm-dialog
+ #closeFormDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Close Report Editor"
+ dialogBody="Close report editor, abandoning any unsaved changes?"
+></eg-confirm-dialog>
+
+<div class="row" id="sr-basic-info">
+ <div class="col-sm-2">
+ <label for="report-type" i18n>Report Type:</label>
+ </div>
+ <div class="col-lg-3">
+ <select class="form-control" id="report-type" [ngModelOptions]="{standalone: true}"
+ [(ngModel)]="rptType" (change)="changeReportType()" [disabled]="!isNew">
+ <option value="" disabled="disabled">Please Select a Report Type</option>
+ <option value="srcirc">Circulation</option>
+ <option value="srcp">Collections</option>
+ <option value="srwd">Weeding</option>
+ <option value="srusr">Patrons</option>
+ <option value="srbps">Billings and Payments Transaction Summary</option>
+ </select>
+ </div>
+ <div class="col-sm-2">
+ <label for="report-name" i18n>Report Name</label>
+ </div>
+ <div class="col-lg-3">
+ <input id="report-name" class="form-control sr-name-{{ (name !== '' && name !== null) ? 'not' : '' }}empty" [(ngModel)]="name" (ngModelChange)="dirty()" />
+ </div>
+ <div class="col-sm-2">
+ <button class="btn btn-success" (click)="saveTemplate(false)" [disabled]="!readyToSave()" i18n>Save</button>
+ <button class="btn btn-outline-dark ml-1" (click)="closeForm()" i18n>Close</button>
+ </div>
+</div>
+<div *ngIf="rptType != ''" class="row mt-2" id="sr-editor-main">
+ <div class="col-lg-12">
+ <ul ngbNav #srEditorTabs="ngbNav" class="nav-tabs">
+
+ <li [ngbNavItem]="'rptFields'">
+ <a ngbNavLink i18n>Display Fields</a>
+ <ng-template ngbNavContent>
+ <eg-sr-field-chooser
+ [fieldType]="'display'"
+ [allFields]="allFields"
+ [fieldGroups]="fieldGroups"
+ [(selectedFields)]="templ.displayFields"
+ (selectedFieldsChange)="dirty()"
+ [(orderByNames)]="templ.orderByNames"
+ (orderByNamesChange)="dirty()"
+ >
+ </eg-sr-field-chooser>
+ </ng-template>
+ </li>
+
+ <li [ngbNavItem]="'rptSortFields'">
+ <a ngbNavLink i18n>Output Order</a>
+ <ng-template ngbNavContent><eg-sr-sort-order [(fields)]="templ.displayFields" [(orderByNames)]="templ.orderByNames" (orderByNamesChange)="dirty()"></eg-sr-sort-order></ng-template>
+ </li>
+
+ <li [ngbNavItem]="'rptFilterFields'">
+ <a ngbNavLink i18n>Filters</a>
+ <ng-template ngbNavContent>
+ <eg-sr-field-chooser
+ [fieldType]="'filter'"
+ [allFields]="allFields"
+ [fieldGroups]="fieldGroups"
+ [(selectedFields)]="templ.filterFields"
+ (selectedFieldsChange)="dirty()"
+ [listFields]="templ.displayFields"
+ >
+ </eg-sr-field-chooser>
+ </ng-template>
+ </li>
+
+ <li [ngbNavItem]="'rptOutputOptions'">
+ <a ngbNavLink i18n>Output Options</a>
+ <ng-template ngbNavContent>
+ <eg-sr-output-options
+ [readyToSchedule]="readyToSchedule"
+ [saveTemplate]="saveTemplate"
+ [templ]="templ"
+ ></eg-sr-output-options>
+ </ng-template>
+ </li>
+ </ul>
+ <div [ngbNavOutlet]="srEditorTabs"></div>
+ </div>
+</div>
--- /dev/null
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute} from '@angular/router';
+import {Location} from '@angular/common';
+import {of} from 'rxjs';
+import {NgbNav} from '@ng-bootstrap/ng-bootstrap';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {SimpleReporterService, SRTemplate} from './simple-reporter.service';
+import {SRFieldChooserComponent} from './sr-field-chooser.component';
+import {SRSortOrderComponent} from './sr-sort-order.component';
+import {SROutputOptionsComponent} from './sr-output-options.component';
+
+@Component({
+ templateUrl: './sr-editor.component.html',
+ styleUrls: ['./sr-editor.component.css'],
+})
+
+export class SREditorComponent implements OnInit {
+
+ rptType = '';
+ oldRptType = '';
+ name = '';
+ templ: SRTemplate = null;
+ isNew = true;
+ sourceClass: IdlObject = null;
+ fieldGroups: IdlObject[] = [];
+ allFields: IdlObject[] = [];
+ pageTitle = '';
+ forcedFields = 0;
+ _isDirty = false;
+
+ @ViewChild('templateSaved', { static: true }) templateSavedString: StringComponent;
+ @ViewChild('templateSaveError', { static: true }) templateSaveErrorString: StringComponent;
+ @ViewChild('newTitle', { static: true }) newTitleString: StringComponent;
+ @ViewChild('editTitle', { static: true }) editTitleString: StringComponent;
+ @ViewChild('srEditorTabs', { static: true }) tabs: NgbNav;
+ @ViewChild('changeTypeDialog', { static: false }) changeTypeDialog: ConfirmDialogComponent;
+ @ViewChild('closeFormDialog', { static: false }) closeFormDialog: ConfirmDialogComponent;
+
+
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private location: Location,
+ private toast: ToastService,
+ private evt: EventService,
+ private idl: IdlService,
+ private pcrud: PcrudService,
+ private srSvc: SimpleReporterService
+ ) {
+ const id = this.route.snapshot.paramMap.get('id');
+ if ( id === null ) {
+ this.templ = new SRTemplate();
+ } else {
+ this.isNew = false;
+ this.loadTemplate(Number(id))
+ .then( x => this.reloadFields(this.templ.fmClass));
+ }
+
+ }
+
+ ngOnInit() {
+ this._setPageTitle();
+ }
+
+ _setPageTitle() {
+ if ( this.isNew ) {
+ this.newTitleString.current()
+ .then(str => this.pageTitle = str );
+ } else {
+ this.editTitleString.current()
+ .then(str => this.pageTitle = str );
+ }
+ }
+
+ reloadFields(fmClass: string) {
+ this.allFields = [];
+ this.forcedFields = 0;
+ this.sourceClass = this.idl.classes[fmClass];
+ ('field_groups' in this.sourceClass) ?
+ // grab a clone
+ this.fieldGroups = this.sourceClass.field_groups.map(x => ({...x})) :
+ this.fieldGroups = [];
+
+ this.sourceClass.fields.forEach(f => {
+
+ f.transform = this.srSvc.defaultTransform();
+ f.operator = this.srSvc.defaultOperator(f.datatype);
+
+ if ( f.suggest_transform ) {
+ f.transform = this.srSvc.getTransformByName(f.suggest_transform);
+ }
+
+ if ( f.suggest_operator ) {
+ f.operator = this.srSvc.getOperatorByName(f.suggest_operator);
+ }
+
+ if ( f.force_transform ) {
+ if ( typeof f.force_transform === 'string' ) {
+ f.transform = this.srSvc.getTransformByName(f.force_transform);
+ } else {
+ f.transform = this.srSvc.getGenericTransformWithParams(f.force_transform);
+ }
+ }
+
+ if ( f.force_operator ) {
+ f.operator = this.srSvc.getOperatorByName(f.force_operator);
+ }
+
+ if ( f.force_filter ) {
+ if ( this.templ.filterFields.findIndex(el => el.name === f.name) === -1 ) {
+ this.templ.filterFields.push(f);
+ this.forcedFields++;
+ }
+ }
+
+ this.allFields.push(f);
+ if ( 'field_groups' in f ) {
+ f.field_groups.forEach(g => {
+ const idx = this.fieldGroups.findIndex(el => el.name === g);
+ if ( idx > -1 ) {
+ if ( !('members' in this.fieldGroups[idx]) ) {
+ this.fieldGroups[idx].members = [];
+ }
+ this.fieldGroups[idx].members.push(f);
+ }
+ });
+ }
+ });
+
+ this.allFields.sort( (a, b) => a.label.localeCompare(b.label) );
+
+ }
+
+ changeReportType() {
+ if ( this.oldRptType === '' || (this.templ.displayFields.length === 0 && this.templ.filterFields.length === this.forcedFields) ) {
+ this.oldRptType = this.rptType;
+ this.templ = new SRTemplate();
+ this.templ.fmClass = this.rptType;
+ this.reloadFields(this.rptType);
+ this._isDirty = true;
+ } else {
+ return this.changeTypeDialog.open()
+ .subscribe(confirmed => {
+ if ( confirmed ) {
+ this.oldRptType = this.rptType;
+ this.templ = new SRTemplate();
+ this.templ.fmClass = this.rptType;
+ this.reloadFields(this.rptType);
+ this._isDirty = true;
+ } else {
+ this.rptType = this.oldRptType;
+ }
+ });
+ }
+ }
+
+ dirty() {
+ this._isDirty = true;
+ }
+
+ isDirty() {
+ return this._isDirty;
+ }
+
+ readyToSave() {
+ return ( this.sourceClass !== null && this.name !== '' );
+ }
+
+ readyToSchedule = () => {
+ return ( this.readyToSave() && this.templ.displayFields.length > 0 );
+ }
+
+ canLeaveEditor() {
+ if ( this.isDirty() ) {
+ return this.closeFormDialog.open();
+ } else {
+ return of(true);
+ }
+ }
+
+ loadTemplate(id: number) {
+ return this.srSvc.loadTemplate(id)
+ .then(idl => {
+ this.templ = new SRTemplate(idl);
+ this.name = this.templ.name;
+ this.rptType = this.templ.fmClass;
+ this.oldRptType = this.templ.fmClass;
+ });
+ }
+
+ saveTemplate = (scheduleNow) => {
+ this.templ.name = this.name;
+
+ this.srSvc.saveTemplate(this.templ, scheduleNow)
+ .then(rt => {
+ this._isDirty = false;
+ // It appears that calling pcrud.create will return the newly created object,
+ // while pcrud.update just gives you back the id of the updated object.
+ if ( typeof rt === 'object' ) {
+ this.templ = new SRTemplate(rt); // pick up the id and create_time fields
+ }
+ this.templateSavedString.current()
+ .then(str => {
+ this.toast.success(str);
+ });
+ if (scheduleNow) {
+ // we're done, so jump to the main page
+ this.router.navigate(['/staff/reporter/simple']);
+ } else if (this.isNew) {
+ // we've successfully saved, so we're no longer new
+ // adjust page title...
+ this.isNew = false;
+ this._setPageTitle();
+ // ... and make the URL say that we're editing
+ const url = this.router.createUrlTree(['/staff/reporter/simple/edit/' + this.templ.id]).toString();
+ this.location.go(url); // go without reloading
+ }
+ },
+ err => {
+ this.templateSaveErrorString.current()
+ .then(str => {
+ this.toast.danger(str + err);
+ console.error('Error saving template: %o', err);
+ });
+ });
+ }
+
+ closeForm() {
+ this.router.navigate(['/staff/reporter/simple']);
+ }
+
+}
+
--- /dev/null
+.chooser-row {
+ display: flex;
+ justify-content: space-between;
+}
+
+.sr-chooser-display-list {
+ font-size: 80%;
+}
+
--- /dev/null
+<div class="chooser-row">
+
+<ngb-accordion #fieldChooser="ngbAccordion" [closeOthers]="true" class="col-md-4">
+ <ngb-panel *ngIf="fieldType === 'filter'" id="suggested_filters" title="Suggested Filters" i18n-title>
+ <ng-template ngbPanelContent>
+ <ng-container *ngFor="let f of allFields">
+ <eg-sr-field
+ *ngIf="f.suggest_filter"
+ [field]=f
+ [withSelect]=true
+ [selected]="fieldIsSelected(f)"
+ (selectEvent)="toggleSelect(f)"
+ >
+ </eg-sr-field>
+ </ng-container>
+ </ng-template>
+ </ngb-panel>
+ <ngb-panel *ngFor="let g of fieldGroups" id="{{g.name}}" title="{{g.label}}">
+ <ng-template ngbPanelContent>
+ <ng-container *ngFor="let f of g.members">
+ <eg-sr-field
+ *ngIf="!hideField(f)"
+ [field]=f
+ [withSelect]=true
+ [selected]="fieldIsSelected(f)"
+ (selectEvent)="toggleSelect(f)"
+ >
+ </eg-sr-field>
+ </ng-container>
+ </ng-template>
+ </ngb-panel>
+ <ngb-panel *ngIf="allFields.length > 0" id="all" title="All Fields" i18n-title>
+ <ng-template ngbPanelContent>
+ <ng-container *ngFor="let f of allFields">
+ <eg-sr-field
+ *ngIf="!hideField(f) && !f.virtual"
+ [field]=f
+ [withSelect]=true
+ [selected]="fieldIsSelected(f)"
+ (selectEvent)="toggleSelect(f)"
+ >
+ </eg-sr-field>
+ </ng-container>
+ </ng-template>
+ </ngb-panel>
+</ngb-accordion>
+
+<ngb-accordion #selectedList="ngbAccordion" activeIds="display-field-list,sort-field-list" class="col-md-8">
+ <ngb-panel id="display-field-list" *ngIf="fieldType === 'filter' && listFields.length > 0">
+ <ng-template ngbPanelHeader let-opened=true>
+ <div class="d-flex align-items-center justify-content-between">
+ <h5 class="m-0" i18n>Fields Selected for Display</h5>
+ </div>
+ </ng-template>
+ <ng-template ngbPanelContent>
+ <span *ngFor="let f of listFields; index as idx" class="sr-chooser-display-list">{{f.alias}}{{idx === (listFields.length - 1) ? '' : ', '}}</span>
+ </ng-template>
+ </ngb-panel>
+ <ngb-panel id="sort-field-list">
+ <ng-template ngbPanelHeader let-opened=true>
+ <div class="d-flex align-items-center justify-content-between">
+ <h5 *ngIf="fieldType === 'display'" class="m-0" i18n>Field Display Order</h5>
+ <h5 *ngIf="fieldType === 'filter'" class="m-0" i18n>Filter Fields and Values</h5>
+ </div>
+ </ng-template>
+ <ng-template ngbPanelContent>
+
+ <ng-container *ngIf="fieldType === 'display'">
+ <ng-container *ngFor="let f of selectedFields; index as idx">
+ <eg-sr-field
+ *ngIf="!hideField(f)"
+ [field]=f
+ [withDeselect]=true
+ [withAlias]=true
+ [withTransforms]=true
+ [withUpDown]=true
+ [disableUp]="idx === 0"
+ [disableDown]="idx === (selectedFields.length - 1)"
+ (fieldChange)="updateField($event)"
+ (deselectEvent)="toggleSelect(f)"
+ (upEvent)="moveUp(idx)"
+ (downEvent)="moveDown(idx)"
+ >
+ </eg-sr-field>
+ </ng-container>
+ </ng-container>
+
+ <ng-container *ngIf="fieldType === 'filter'">
+ <ng-container *ngFor="let f of selectedFields; index as idx">
+ <eg-sr-field
+ *ngIf="!hideField(f)"
+ [field]=f
+ [withDeselect]=true
+ [withTransforms]=true
+ [withOperators]=true
+ [withValueInput]=true
+ (fieldChange)="updateField($event)"
+ (deselectEvent)="toggleSelect(f)"
+ >
+ </eg-sr-field>
+ </ng-container>
+ </ng-container>
+
+ </ng-template>
+ </ngb-panel>
+</ngb-accordion>
+
+</div>
--- /dev/null
+import {Component, Input, Output, EventEmitter, OnInit, ViewChild} from '@angular/core';
+import {NgbAccordion} from '@ng-bootstrap/ng-bootstrap';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {SimpleReporterService} from './simple-reporter.service';
+import {SRFieldComponent} from './sr-field.component';
+
+@Component({
+ selector: 'eg-sr-field-chooser',
+ styleUrls: ['./sr-field-chooser.component.css'],
+ templateUrl: './sr-field-chooser.component.html'
+})
+
+export class SRFieldChooserComponent implements OnInit {
+
+ @Input() fieldType = 'display';
+ @Input() allFields: IdlObject[] = [];
+ @Input() fieldGroups: IdlObject[] = [];
+ @Input() orderByNames: string[] = [];
+ @Output() orderByNamesChange = new EventEmitter<string[]>();
+ @Input() selectedFields: IdlObject[] = [];
+ @Output() selectedFieldsChange = new EventEmitter<IdlObject[]>();
+ @Input() listFields: IdlObject[] = [];
+
+ @ViewChild('fieldChooser', { static: false }) fieldChooser: NgbAccordion;
+ @ViewChild('selectedList', { static: false }) selectedList: NgbAccordion;
+
+ constructor(
+ private idl: IdlService,
+ private srSvc: SimpleReporterService
+ ) {
+ }
+
+ ngOnInit() {
+ }
+
+ fieldIsSelected(field: IdlObject) {
+ return this.selectedFields.findIndex(el => el.name === field.name) > -1;
+ }
+
+ hideField(field: IdlObject) {
+ if ( typeof field.hide_from === 'undefined' ) {
+ return false;
+ }
+ return (field.hide_from.indexOf(this.fieldType) > -1);
+ }
+
+ toggleSelect(field: IdlObject) {
+ const idx = this.selectedFields.findIndex(el => el.name === field.name);
+ if ( idx > -1 ) {
+ if ( field.forced_filter ) { return; } // These should just be hidden, but if not...
+ this.selectedFields.splice(idx, 1);
+ if ( this.fieldType === 'display' ) {
+ this.orderByNames.splice(this.orderByNames.findIndex(el => el === field.name), 1);
+ }
+ } else {
+ const f = { ...field };
+
+ if ( this.fieldType === 'display' ) {
+ f['alias'] = f.label; // can be edited
+ this.orderByNames.push(f.name);
+ }
+ this.selectedFields.push(f);
+ }
+
+ this.selectedFieldsChange.emit(this.selectedFields);
+
+ if ( this.fieldType === 'display' ) {
+ this.orderByNamesChange.emit(this.orderByNames);
+ }
+ }
+
+ updateField(field: IdlObject) {
+ const idx = this.selectedFields.findIndex(el => el.name === field.name);
+ this.selectedFields[idx] = field;
+ this.selectedFieldsChange.emit(this.selectedFields);
+ }
+
+ moveUp(idx: number) {
+ if ( idx > 0 ) { // should always be the case, but we check anyway
+ const hold: IdlObject = this.selectedFields[idx - 1];
+ this.selectedFields[idx - 1] = this.selectedFields[idx];
+ this.selectedFields[idx] = hold;
+ this.selectedFieldsChange.emit(this.selectedFields);
+ }
+ }
+
+ moveDown(idx: number) {
+ if ( idx < this.selectedFields.length ) { // see above comment
+ const hold: IdlObject = this.selectedFields[idx + 1];
+ this.selectedFields[idx + 1] = this.selectedFields[idx];
+ this.selectedFields[idx] = hold;
+ this.selectedFieldsChange.emit(this.selectedFields);
+ }
+ }
+
+}
--- /dev/null
+.sr-field-container {
+ border-bottom: 1px solid lightgray;
+ padding-bottom: .75em;
+ margin-bottom: .75em;
+}
+
+.sr-field-select {
+ display: flex;
+}
+
+.sr-checkbox {
+ margin-top: .3em;
+}
+
+.sr-field {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+
+.sr-field-explainer {
+ font-size: 80%;
+}
+
+.sr-field-disp-ind {
+ padding-left: .3em;
+ padding-bottom: .3em;
+}
+
--- /dev/null
+<div class="sr-field-container">
+
+ <div class="sr-field{{ withSelect === true ? '-select' : '' }}" (click)="selectAction()">
+
+ <div *ngIf="withSelect" class="sr-field-select col-md-1">
+ <input (change)="selectAction()" (click)="selectAction()" class="sr-checkbox" type="checkbox"
+ id="chk{{field.name}}" checked="{{ selected === true ? 'checked' : '' }}" />
+ </div>
+
+ <div *ngIf="withDeselect" class="sr-field-deselect col-md-1">
+ <button class="btn btn-sm material-icon-button p-1 sr-checkbox" title="Un-Select Field" (click)="deselectAction()" title-i18n>
+ <span class="material-icons">remove_circle_outline</span></button>
+ </div>
+
+ <div *ngIf="withAlias" class="sr-field-name col-md-auto">
+ <span class="sr-field-explainer" i18n>Name</span>
+ <input class="form-control" type="textbox" [disabled]="!editAlias" [(ngModel)]="field.alias" />
+ <span *ngIf="field.alias !== field.label" class="sr-field-explainer">({{field.label}})</span>
+ </div>
+ <div *ngIf="!withAlias" class="sr-field-name col-md-auto">
+ <span *ngIf="withTransforms || withOperators || withValueInput" class="sr-field-explainer"><br /></span>
+ <label *ngIf="withSelect" (click)="selectAction()" class="sr-field-label" for="chk{{field.name}}">{{field.label}}</label>
+ <label *ngIf="!withSelect" class="sr-field-label">{{field.label}}</label>
+ </div>
+
+ <div *ngIf="withTransforms || withOperators" class="sr-field-xform col-md-3">
+
+ <div *ngIf="withTransforms">
+ <span class="sr-field-explainer" i18n>Transform</span>
+ <select class="form-control" (change)="transformChange($event)" [disabled]="field.force_transform">
+ <option *ngFor="let t of transforms" value="{{t.name}}" selected="{{ field.transform.name === t.name ? 'selected' : '' }}">
+ <span *ngIf="t.name === 'Bare'" i18n>Raw Data</span>
+ <span *ngIf="t.name === 'upper'" i18n>Upper Case</span>
+ <span *ngIf="t.name === 'lower'" i18n>Lower Case</span>
+ <span *ngIf="t.name === 'substring'" i18n>Substring</span>
+ <span *ngIf="t.name === 'day_name'" i18n>Day Name</span>
+ <span *ngIf="t.name === 'month_name'" i18n>Month Name</span>
+ <span *ngIf="t.name === 'doy'" i18n>Day of Year</span>
+ <span *ngIf="t.name === 'woy'" i18n>Week of Year</span>
+ <span *ngIf="t.name === 'moy'" i18n>Month of Year</span>
+ <span *ngIf="t.name === 'qoy'" i18n>Quarter of Year</span>
+ <span *ngIf="t.name === 'dom'" i18n>Day of Month</span>
+ <span *ngIf="t.name === 'dow'" i18n>Day of Week</span>
+ <span *ngIf="t.name === 'year_trunc'" i18n>Year</span>
+ <span *ngIf="t.name === 'month_trunc'" i18n>Month</span>
+ <span *ngIf="t.name === 'date_trunc'" i18n>Date</span>
+ <span *ngIf="t.name === 'hour_trunc'" i18n>Hour</span>
+ <span *ngIf="t.name === 'quarter'" i18n>Quarter</span>
+ <span *ngIf="t.name === 'months_ago'" i18n>Months Ago</span>
+ <span *ngIf="t.name === 'hod'" i18n>Hour of Day</span>
+ <span *ngIf="t.name === 'quarters_ago'" i18n>Quarters Ago</span>
+ <span *ngIf="t.name === 'age'" i18n>Age</span>
+ <span *ngIf="t.name === 'first'" i18n>First Value</span>
+ <span *ngIf="t.name === 'last'" i18n>Last Value</span>
+ <span *ngIf="t.name === 'min'" i18n>Minimum Value</span>
+ <span *ngIf="t.name === 'max'" i18n>Maximum Value</span>
+ <span *ngIf="t.name === 'count'" i18n>Count</span>
+ <span *ngIf="t.name === 'count_distinct'" i18n>Count</span>{{ '' // This is currently the only Count transform offered; if that's changed this will need its 'Distinct' back. }}
+ <span *ngIf="t.name === 'sum'" i18n>Sum</span>
+ <span *ngIf="t.name === 'average'" i18n>Average</span>
+ </option>
+ </select>
+ </div>
+
+ <div *ngIf="withOperators">
+ <span class="sr-field-explainer" i18n>Operator</span>
+ <select class="form-control" (change)="operatorChange($event)" [disabled]="field.force_operator">
+ <option *ngFor="let o of operators" value="{{o.name}}" selected="{{ field.operator.name === o.name ? 'selected' : '' }}">
+ <span *ngIf="o.name === '= any'" i18n>Equals</span> {{ '' // this and the next are used for bools only }}
+ <span *ngIf="o.name === '<> any'" i18n>Does Not Equal</span>
+ <span *ngIf="o.name === '='" i18n>Equals</span>
+ <span *ngIf="o.name === 'like'" i18n>Contains Matching Substring (Case Sensitive)</span>{{ '' // This is on hiatus along with non-distinct counting }}
+ <span *ngIf="o.name === 'ilike'" i18n>Contains String</span>
+ <span *ngIf="o.name === '>' && (field.transform.final_datatype || field.datatype) === 'timestamp'" i18n>After</span>
+ <span *ngIf="o.name === '>' && (field.transform.final_datatype || field.datatype) !== 'timestamp'" i18n>Greater Than</span>
+ <span *ngIf="o.name === '>=' && (field.transform.final_datatype || field.datatype) === 'timestamp'" i18n>On or After</span>
+ <span *ngIf="o.name === '>=' && (field.transform.final_datatype || field.datatype) !== 'timestamp'" i18n>Greater Than or Equal to</span>
+ <span *ngIf="o.name === '<' && (field.transform.final_datatype || field.datatype) === 'timestamp'" i18n>Before</span>
+ <span *ngIf="o.name === '<' && (field.transform.final_datatype || field.datatype) !== 'timestamp'" i18n>Less Than</span>
+ <span *ngIf="o.name === '<=' && (field.transform.final_datatype || field.datatype) === 'timestamp'" i18n>On or Before</span>
+ <span *ngIf="o.name === '<=' && (field.transform.final_datatype || field.datatype) !== 'timestamp'" i18n>Less Than or Equal to</span>
+ <span *ngIf="o.name === 'in'" i18n>In List</span>
+ <span *ngIf="o.name === 'not in'" i18n>Not In List</span>
+ <span *ngIf="o.name === 'between'" i18n>Between</span>
+ <span *ngIf="o.name === 'not between'" i18n>Not Between</span>
+ <span *ngIf="o.name === 'is'" i18n>Is Null</span>
+ <span *ngIf="o.name === 'is not'" i18n>Is Not Null</span>
+ <span *ngIf="o.name === 'is blank'" i18n>Is Null or Blank</span>
+ <span *ngIf="o.name === 'is not blank'" i18n>Is Not Null or Blank</span>
+ </option>
+ </select>
+ </div>
+
+ </div>
+
+ <div *ngIf="withValueInput" class="sr-field-value col-md-5">
+ <span class="sr-field-explainer" i18n>Filter value</span>
+
+ <div *ngIf="field.operator.name.indexOf('in') > -1">
+ <div [ngSwitch]="field.transform.final_datatype || field.datatype">
+ <div *ngSwitchCase="'link'">
+ <eg-multi-select [linkedLibraryLabel]="field.org_filter_field" [idlBaseQuery]="linkedIdlBaseQuery" [idlClass]="field.class" [startValue]="getBracketListValue(field.filter_value)"
+ (onChange)="setBracketListValue($event)">
+ </eg-multi-select>
+ </div>
+ <div *ngSwitchCase="'org_unit'">
+ <eg-multi-select [idlClass]="'aou'" [startValue]="getBracketListValue(field.filter_value)"
+ (onChange)="setBracketListValue($event)">
+ </eg-multi-select>
+ </div>
+ <div *ngSwitchDefault>
+ <eg-text-multi-select [startValue]="field.filter_value"
+ (onChange)="setSingleValue($event)">
+ </eg-text-multi-select>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="field.operator.name.indexOf('between') > -1">
+ <div [ngSwitch]="field.transform.final_datatype || field.datatype">
+ <div *ngSwitchCase="'interval'">
+ <eg-interval-input [initialValue]="field.filter_value[0]" (onChange)="firstBetweenValue($event)"></eg-interval-input>
+ <span i18n>and</span>
+ <eg-interval-input [initialValue]="field.filter_value[1]" (onChange)="secondBetweenValue($event)"> </eg-interval-input>
+ </div>
+ <div *ngSwitchCase="'timestamp'">
+ <eg-date-select [initialIso]="field.filter_value[0]" (onChangeAsIso)="firstBetweenValue($event)"></eg-date-select>
+ <span i18n>and</span>
+ <eg-date-select [initialIso]="field.filter_value[1]" (onChangeAsIso)="secondBetweenValue($event)"></eg-date-select>
+ </div>
+ <div *ngSwitchCase="'link'">{{ '' }}</div>
+ <div *ngSwitchCase="'org_unit'">{{ '' }}</div>
+ <div *ngSwitchCase="'bool'">{{ '' }}</div>
+ <div *ngSwitchDefault>
+ <input class="form-control" type="textbox" value="{{field.filter_value[0]}}" (change)="firstBetweenValue($event.target.value)" />
+ <span i18n>and</span>
+ <input class="form-control" type="textbox" value="{{field.filter_value[1]}}" (change)="secondBetweenValue($event.target.value)" />
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="field.operator.name.indexOf('between') === -1 && field.operator.name.indexOf('in') === -1 && field.operator.name.indexOf('is') === -1">
+ <div [ngSwitch]="field.transform.final_datatype || field.datatype">
+ <div *ngSwitchCase="'org_unit'">
+ <eg-org-family-select
+ (onChange)="setOrgFamilyValue($event)"
+ [selectedOrgId]="field._org_family_primaryOrgId"
+ [ancestorSelectorChecked]="field._org_family_includeAncestors"
+ [descendantSelectorChecked]="field._org_family_includeDescendants">
+ </eg-org-family-select>
+ </div>
+ <div *ngSwitchCase="'link'">
+ <eg-combobox i18n-placeholder placeholder="Select..." idlClass="{{field.class}}"
+ id="{{field.name}}-{{field.class}}-{{field.key}}"
+ [idlBaseQuery]="linkedIdlBaseQuery"
+ [idlIncludeLibraryInLabel]="field.org_filter_field"
+ [asyncSupportsEmptyTermClick]="true"
+ [selectedId]="field.filter_value"
+ (onChange)="setSingleValue($event[field.key])">
+ </eg-combobox>
+ </div>
+ <div *ngSwitchCase="'timestamp'">
+ <eg-date-select [initialIso]="field.filter_value" (onChangeAsIso)="setSingleValue($event)"></eg-date-select>
+ </div>
+ <div *ngSwitchCase="'interval'">
+ <eg-interval-input [initialValue]="field.filter_value" (onChange)="setSingleValue($event)"></eg-interval-input>
+ </div>
+ <div *ngSwitchCase="'bool'">
+ <select class="form-control" (change)="setSingleValue($event.target.value)">
+ <option selected="{{ !field.filter_value ? 'selected' : '' }}" disabled="disabled" i18n>Select one</option>
+ <option value="{t}" selected="{{ field.filter_value === '{t}' ? 'selected' : '' }}" i18n>True</option>
+ <option value="{f}" selected="{{ field.filter_value === '{f}' ? 'selected' : '' }}" i18n>False</option>
+ <option value="{t,f}" selected="{{ (field.filter_value && (field.filter_value !== '{t}' && field.filter_value !== '{f}') ) ? 'selected' : '' }}" i18n>Don't Care</option>
+ </select>
+ </div>
+
+ <div *ngSwitchDefault>
+ <input class="form-control" type="textbox" value="{{field.filter_value}}" (change)="setSingleValue($event.target.value)" />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div *ngIf="withSortDirection" class="sr-field-sortdir col-md-5">
+ <span class="sr-field-explainer" i18n>Direction</span>
+ <select class="form-control" (change)="directionChange($event)">
+
+ <option *ngIf="field.datatype === 'timestamp'" value="ascending" selected="{{ field.direction === 'ascending' ? 'selected' : '' }}" i18n>Later dates at the bottom</option>
+ <option *ngIf="field.datatype === 'number' || field.datatype === 'int' || field.datatype === 'float' || field.datatype === 'money'"
+ value="ascending" selected="{{ field.direction === 'ascending' ? 'selected' : '' }}" i18n>Larger numbers at the bottom</option>
+ <option *ngIf="field.datatype !== 'timestamp' && field.datatype !== 'number' && field.datatype !== 'int' && field.datatype !== 'float' && field.datatype !== 'money'"
+ value="ascending" selected="{{ field.direction === 'ascending' ? 'selected' : '' }}" i18n>Ascending (1, 2, a, b, A, B)</option>
+
+ <option *ngIf="field.datatype === 'timestamp'" value="descending" selected="{{ field.direction === 'descending' ? 'selected' : '' }}" i18n>Later dates at the top</option>
+ <option *ngIf="field.datatype === 'number' || field.datatype === 'int' || field.datatype === 'float' || field.datatype === 'money'"
+ value="descending" selected="{{ field.direction === 'descending' ? 'selected' : '' }}" i18n>Larger numbers at the top</option>
+ <option *ngIf="field.datatype !== 'timestamp' && field.datatype !== 'number' && field.datatype !== 'int' && field.datatype !== 'float' && field.datatype !== 'money'"
+ value="descending" selected="{{ field.direction === 'descending' ? 'selected' : '' }}" i18n>Descending (B, A, b, a, 2, 1)</option>
+
+ </select>
+ </div>
+
+ <div *ngIf="withUpDown" class="sr-field-updown col-md-2">
+ <button (click)="upAction()" class="btn btn-outline-primary btn-sm" [disabled]="disableUp"><span class="material-icons">arrow_upward</span></button>
+ <button (click)="downAction()" class="btn btn-outline-primary btn-sm" [disabled]="disableDown"><span class="material-icons">arrow_downward</span></button>
+ </div>
+
+ </div>
+
+</div>
--- /dev/null
+import {Component, Input, Output, EventEmitter, OnInit} from '@angular/core';
+import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {SimpleReporterService} from './simple-reporter.service';
+
+@Component({
+ selector: 'eg-sr-field',
+ templateUrl: './sr-field.component.html',
+ styleUrls: ['./sr-field.component.css'],
+})
+export class SRFieldComponent implements OnInit {
+
+ operators = [];
+ transforms = [];
+ wsContextOrgs = [];
+ linkedIdlBaseQuery = {};
+
+ @Input() field: IdlObject = null;
+ @Output() fieldChange = new EventEmitter<IdlObject>();
+ @Input() withAlias = false;
+ @Input() editAlias = true;
+ @Input() withTransforms = false;
+ @Input() withOperators = false;
+ @Input() withValueInput = false;
+ @Input() withSelect = false;
+ @Input() withDeselect = false;
+ @Input() withSortDirection = false;
+ @Output() selectEvent = new EventEmitter();
+ @Output() deselectEvent = new EventEmitter();
+ @Input() selected = false;
+ @Input() withUpDown = false;
+ @Output() upEvent = new EventEmitter();
+ @Output() downEvent = new EventEmitter();
+ @Input() disableUp = false;
+ @Input() disableDown = false;
+
+ constructor(
+ private org: OrgService,
+ private auth: AuthService,
+ private srSvc: SimpleReporterService
+ ) {
+ }
+
+ ngOnInit() {
+
+ if ( this.withTransforms ) {
+ this.transforms = this.srSvc.getTransformsForDatatype(this.field.datatype);
+ }
+
+ if ( this.withOperators ) {
+ this.operators = this.srSvc.getOperatorsForDatatype(this.field.datatype);
+ }
+
+ this.wsContextOrgs = this.org.fullPath(this.auth.user().ws_ou(), true);
+ if (this.field.org_filter_field) {
+ this.linkedIdlBaseQuery[this.field.org_filter_field] = this.wsContextOrgs;
+ }
+ }
+
+ clearFilterValue() {
+ this.field.filter_value = this.field.operator.arity > 1 ? [] : null;
+ delete this.field._org_family_includeAncestors;
+ delete this.field._org_family_includeDescendants;
+ delete this.field._org_family_primaryOrgId;
+ }
+
+ operatorChange($event) {
+ const new_op = this.srSvc.getOperatorByName($event.target.value);
+ if (new_op.arity !== this.field.operator.arity) { // param count of the old and new ops are different
+ this.field.operator = new_op;
+ this.clearFilterValue(); // clear the filter value
+ } else {
+ this.field.operator = new_op;
+ }
+ this.fieldChange.emit(this.field);
+ }
+
+ transformChange($event) {
+ const new_transform = this.srSvc.getTransformByName($event.target.value);
+
+ if (new_transform.final_datatype) { // new has a final_datatype
+ if (this.field.transform.final_datatype) { // and so does old
+ if (new_transform.final_datatype !== this.field.transform.final_datatype) { // and they're different
+ this.clearFilterValue(); // clear
+ }
+ } else if (new_transform.final_datatype !== this.field.datatype) { // old does not, and base is different from new
+ this.clearFilterValue(); // clear
+ }
+ } else if (this.field.transform.final_datatype) {// old has a final_datatype, new doesn't
+ if (this.field.transform.final_datatype !== this.field.datatype) { // and it's different from the base type
+ this.clearFilterValue(); // clear
+ }
+ }
+
+ this.field.transform = new_transform;
+ if (new_transform.final_datatype) {
+ this.operators = this.srSvc.getOperatorsForDatatype(new_transform.final_datatype);
+ } else {
+ this.operators = this.srSvc.getOperatorsForDatatype(this.field.datatype);
+ }
+
+ this.selectEvent.emit();
+ this.fieldChange.emit(this.field);
+ }
+
+ firstBetweenValue($event) {
+ if (!Array.isArray(this.field.filter_value)) {
+ this.field.filter_value = [];
+ }
+ this.field.filter_value[0] = $event;
+ this.fieldChange.emit(this.field);
+ }
+
+ secondBetweenValue($event) {
+ if (!Array.isArray(this.field.filter_value)) {
+ this.field.filter_value = [];
+ }
+ this.field.filter_value[1] = $event;
+ this.fieldChange.emit(this.field);
+ }
+
+ setSingleValue($event) {
+ if (Array.isArray(this.field.filter_value)) {
+ this.field.filter_value = null;
+ }
+ this.field.filter_value = $event;
+ this.fieldChange.emit(this.field);
+ }
+
+ getBracketListValue(list_value) {
+ let output = '{';
+ if (Array.isArray(list_value)) {
+ list_value.forEach((v, i) => {
+ if (i > 0) {
+ output += ',';
+ }
+ output += v;
+ });
+ }
+ output += '}';
+ return output;
+ }
+
+ setOrgFamilyValue($event) {
+ this.field.filter_value = this.getBracketListValue($event.orgIds);
+ this.field._org_family_includeAncestors = $event.includeAncestors;
+ this.field._org_family_includeDescendants = $event.includeDescendants;
+ this.field._org_family_primaryOrgId = $event.primaryOrgId;
+ this.fieldChange.emit(this.field);
+ }
+
+ setBracketListValue($event) {
+ if (Array.isArray(this.field.filter_value)) {
+ this.field.filter_value = null;
+ }
+ let valstr = $event;
+ valstr = valstr.replace(/^{/, '');
+ valstr = valstr.replace(/}$/, '');
+ const ids = valstr.split(',');
+ this.field.filter_value = [...ids];
+ this.fieldChange.emit(this.field);
+ }
+
+ directionChange($event) {
+ this.field['direction'] = $event.target.value;
+ this.fieldChange.emit(this.field);
+ }
+
+ selectAction() {
+ this.selectEvent.emit();
+ }
+
+ deselectAction() {
+ this.deselectEvent.emit();
+ }
+
+ upAction() {
+ this.upEvent.emit();
+ }
+
+ downAction() {
+ this.downEvent.emit();
+ }
+
+}
+
--- /dev/null
+<ng-template #deletedOutputStringTmpl let-num="num" i18n>{num, plural, =1 {Output Deleted} other {{{num}} Outputs Deleted}}</ng-template>
+<eg-string #deleted [template]="deletedOutputStringTmpl"></eg-string>
+
+<ng-template #confirmDeleteOutputStringTmpl let-num="num" i18n>{num, plural, =1 {Are you sure you want to delete this output?} other {Are you sure you want to delete these {{num}} outputs?}}</ng-template>
+<eg-string #delete [template]="confirmDeleteOutputStringTmpl"></eg-string>
+
+<eg-confirm-dialog
+ #confirmDelete
+ i18n-dialogTitle
+ dialogTitle="Confirm Deletion"
+></eg-confirm-dialog>
+
+<ng-template #outputTmpl let-rpt="row">
+ <ul *ngIf="!rpt.error_code" class="list-group list-group-horizontal">
+ <li *ngIf="rpt._rs.html_format() === 't'" class="list-group-item">
+ <a href="{{outputPath(rpt, 'report-data.html.raw.html')}}" target="_blank" i18n>
+ HTML
+ </a>
+ </li>
+ <li *ngIf="rpt._rs.csv_format() === 't'" class="list-group-item">
+ <a href="{{outputPath(rpt, 'report-data.csv')}}" i18n>
+ CSV
+ </a>
+ </li>
+ <li *ngIf="rpt._rs.excel_format() === 't'" class="list-group-item">
+ <a href="{{outputPath(rpt, 'report-data.xlsx')}}" i18n>
+ Excel
+ </a>
+ </li>
+ <li *ngIf="rpt._rs.chart_line() === 't'" class="list-group-item">
+ <a href="{{outputPath(rpt, 'report-data.html.line.gif')}}" target="_blank" i18n>
+ Line Chart
+ </a>
+ </li>
+ <li *ngIf="rpt._rs.chart_bar() === 't'" class="list-group-item">
+ <a href="{{outputPath(rpt, 'report-data.html.bar.gif')}}" target="_blank" i18n>
+ Bar Chart
+ </a>
+ </li>
+ </ul>
+ <span *ngIf="rpt.error_code" i18n>
+ Error running report
+ </span>
+</ng-template>
+
+<div class="mt-2">
+ <eg-grid #srOutputsGrid
+ persistKey="reporter.simple.outputs"
+ [dataSource]="gridSource"
+ [stickyHeader]="true"
+ [filterable]="true"
+ [sortable]="true"
+ [cellTextGenerator]="cellTextGenerator"
+ [showDeclaredFieldsOnly]="true">
+
+ <eg-grid-toolbar-button label="Refresh" i18n-label
+ (onClick)="refreshGrid($event)">
+ </eg-grid-toolbar-button>
+
+ <eg-grid-toolbar-action label="Delete Output" i18n-label
+ (onClick)="deleteOutputs($event)"
+ [disableOnRows]="zeroSelectedRows">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-column path="id" [hidden]=true [index]="true" i18n-label label="Run ID" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <eg-grid-column path="template_name" i18n-label label="Report"></eg-grid-column>
+ <eg-grid-column path="complete_time" i18n-label label="Finish Time" datatype="timestamp" [datePlusTime]="true"></eg-grid-column>
+ <eg-grid-column path="_output" [cellTemplate]="outputTmpl" i18n-label label="Output" [sortable]="false" [filterable]="false" [disableTooltip]="true"></eg-grid-column>
+ <eg-grid-column path="error_text"[hidden]=true i18n-label label="Error Text"></eg-grid-column>
+
+ </eg-grid>
+</div>
--- /dev/null
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {AuthService} from '@eg/core/auth.service';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {Pager} from '@eg/share/util/pager';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {SimpleReporterService} from './simple-reporter.service';
+
+@Component({
+ selector: 'eg-sr-outputs',
+ templateUrl: 'sr-my-outputs.component.html',
+})
+
+export class SROutputsComponent implements OnInit {
+
+ gridSource: GridDataSource;
+ @ViewChild('srOutputsGrid', { static: true }) outputsGrid: GridComponent;
+ @ViewChild('confirmDelete', { static: false }) confirmDeleteDialog: ConfirmDialogComponent;
+ @ViewChild('deleted', { static: true} ) deletedString: StringComponent;
+ @ViewChild('delete', { static: true} ) confirmDeleteString: StringComponent;
+
+ cellTextGenerator: GridCellTextGenerator;
+
+ constructor(
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private idl: IdlService,
+ private toast: ToastService,
+ private srSvc: SimpleReporterService,
+ ) {
+ // These values are all replaced via custom templates and cause warnings if not specified here.
+ this.cellTextGenerator = {
+ _output: row => ''
+ };
+
+ }
+
+ ngOnInit() {
+ this.gridSource = this.srSvc.getOutputDatasource();
+
+ }
+
+ // Expects an rt object with fleshed report to grab the template id.
+ outputPath(row: any, file: string) {
+ return `/reporter/${row.template_id}/${row.report_id}/${row.id}/${file}?ses=${this.auth.token()}`;
+ }
+
+ zeroSelectedRows(rows: any) {
+ return rows.length === 0;
+ }
+
+ notOneSelectedRow(rows: any) {
+ return rows.length !== 1;
+ }
+
+ deleteOutputs(rows: any[]) {
+ if ( rows.length <= 0 ) { return; }
+ this.confirmDeleteString.current({ num: rows.length })
+ .then(str => {
+ this.confirmDeleteDialog.dialogBody = str;
+ this.confirmDeleteDialog.open()
+ .subscribe(confirmed => {
+ if ( confirmed ) { this.doDeleteOutputs(rows.map(x => x._rs)); }
+ });
+ });
+ }
+
+ doDeleteOutputs(outs: IdlObject[]) {
+ const deletedCount = outs.length;
+ this.pcrud.remove(outs).toPromise()
+ .then(res => {
+ this.outputsGrid.reload();
+ this.deletedString.current({num: outs.length})
+ .then(str => {
+ this.toast.success(str);
+ });
+ });
+
+ }
+
+ refreshGrid($event) {
+ this.outputsGrid.reload();
+ }
+
+}
+
--- /dev/null
+<ng-template #deleteSuccesstringTmpl let-ct="ct" i18n>{ct, plural, =1 {Deleted 1 Report} other {Deleted {{ct}} Reports}}</ng-template>
+<eg-string #deleteSuccess i18n-text [template]="deleteSuccesstringTmpl"></eg-string>
+<ng-template #deleteFailureStringTmpl let-ct="ct" i18n>{ct, plural, =1 {Failed to Delete 1 Report} other {Failed to Delete {{ct}} Reports}}</ng-template>
+<eg-string #deleteFailure i18n-text [template]="deleteFailureStringTmpl"></eg-string>
+<ng-template #mixedResultsStringTmpl let-fail="fail" let-success="success" i18n>{fail, plural, =1 {Failed to Delete 1 Report But Succeeded In Deleting {{success}}} other {Failed to Delete {{fail}} Reports But Succeeded in Deleting {{success}}}}</ng-template>
+<eg-string #mixedResults i18n-text [template]="mixedResultsStringTmpl"></eg-string>
+<ng-template #deleteStringTmpl let-ct="ct" i18n>{ct, plural, =1 {Are you sure you want to delete this report and its output?} other {Are you sure you want to delete these {{ct}} reports and their output?}}</ng-template>
+<eg-string #delete [template]="deleteStringTmpl"></eg-string>
+<ng-template #promptCloneOutputStringTmpl let-old="old" i18n>Enter a new name for the clone of: {{old}}</ng-template>
+<eg-string #clone [template]="promptCloneOutputStringTmpl"></eg-string>
+
+<eg-string #templateSaved i18n-text text="Report Saved Successfully"></eg-string>
+<eg-string #templateSaveError i18n-text text="Error Saving Report"></eg-string>
+
+<eg-confirm-dialog
+ #confirmDelete
+ i18n-dialogTitle
+ dialogTitle="Confirm Deletion"
+></eg-confirm-dialog>
+
+<eg-prompt-dialog
+ #promptClone
+ i18n-dialogTitle
+ dialogTitle="Clone Report"
+></eg-prompt-dialog>
+
+<div class="mt-2">
+ <eg-grid #srReportsGrid
+ persistKey="reporter.simple.reports"
+ [dataSource]="gridSource"
+ [stickyHeader]="true"
+ [filterable]="true"
+ [sortable]="true"
+ [cellTextGenerator]="cellTextGenerator"
+ [showDeclaredFieldsOnly]="true"
+ (onRowActivate)="editSelected([$event])">
+
+ <eg-grid-toolbar-button label="New" i18n-label
+ (onClick)="newReport($event)">
+ </eg-grid-toolbar-button>
+
+ <eg-grid-toolbar-action label="Edit" i18n-label
+ (onClick)="editSelected($event)"
+ [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action label="Delete" i18n-label
+ (onClick)="deleteSelected($event)"
+ [disableOnRows]="zeroSelectedRows">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-toolbar-action label="Clone" i18n-label
+ (onClick)="cloneSelected($event)"
+ [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-column path="rt_id" i18n-label label="Simple Report Template ID" [hidden]="true" [index]="true" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <eg-grid-column path="name" i18n-label label="Report Name"></eg-grid-column>
+ <eg-grid-column path="create_time" i18n-label label="Date Created" datatype="timestamp" [datePlusTime]="true"></eg-grid-column>
+ <eg-grid-column path="edit_time" i18n-label label="Last Edited" datatype="timestamp" [datePlusTime]="true" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <eg-grid-column path="last_run" i18n-label label="Last Run" datatype="timestamp" [datePlusTime]="true" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <eg-grid-column path="next_run" i18n-label label="Next Run" datatype="timestamp" [datePlusTime]="true" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <eg-grid-column path="recurring" i18n-label label="Recurring?" datatype="bool" [filterable]="false" [sortable]="false"></eg-grid-column>
+
+ </eg-grid>
+<div>
--- /dev/null
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute} from '@angular/router';
+import {map, concatMap} from 'rxjs/operators';
+import {from} from 'rxjs';
+import {AuthService} from '@eg/core/auth.service';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {Pager} from '@eg/share/util/pager';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {SimpleReporterService, SRTemplate} from './simple-reporter.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
+import {NetService} from '@eg/core/net.service';
+
+@Component({
+ selector: 'eg-sr-reports',
+ templateUrl: 'sr-my-reports.component.html',
+})
+
+export class SRReportsComponent implements OnInit {
+
+ gridSource: GridDataSource;
+ editSelected: ($event: any) => void;
+ newReport: ($event: any) => void;
+ @ViewChild('srReportsGrid', { static: true }) reportsGrid: GridComponent;
+ @ViewChild('confirmDelete', { static: true }) deleteDialog: ConfirmDialogComponent;
+ @ViewChild('promptClone', { static: true }) cloneDialog: PromptDialogComponent;
+ @ViewChild('delete', { static: true} ) deleteString: StringComponent;
+ @ViewChild('clone', { static: true} ) cloneString: StringComponent;
+ @ViewChild('deleteSuccess', { static: true} ) deleteSuccessString: StringComponent;
+ @ViewChild('deleteFailure', { static: true} ) deleteFailureString: StringComponent;
+ @ViewChild('mixedResults', { static: true} ) mixedResultsString: StringComponent;
+ @ViewChild('templateSaved', { static: true }) templateSavedString: StringComponent;
+ @ViewChild('templateSaveError', { static: true }) templateSaveErrorString: StringComponent;
+
+
+ cellTextGenerator: GridCellTextGenerator;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private idl: IdlService,
+ private srSvc: SimpleReporterService,
+ private toast: ToastService,
+ private net: NetService
+ ) {
+ }
+
+ ngOnInit() {
+ this.gridSource = this.srSvc.getReportsDatasource();
+
+ this.editSelected = ($event) => {
+ this.router.navigate(['edit', $event[0].rt_id], { relativeTo: this.route });
+ };
+
+ this.newReport = ($event) => {
+ this.router.navigate(['new'], { relativeTo: this.route });
+ };
+ }
+
+ zeroSelectedRows(rows: any) {
+ return rows.length === 0;
+ }
+
+ notOneSelectedRow(rows: any) {
+ return rows.length !== 1;
+ }
+
+ deleteSelected(rows: any) {
+ if ( rows.length <= 0 ) { return; }
+
+ let successes = 0;
+ let failures = 0;
+
+ this.deleteString.current({ct: rows.length})
+ .then(str => {
+ this.deleteDialog.dialogBody = str;
+ this.deleteDialog.open()
+ .subscribe(confirmed => {
+ if ( confirmed ) {
+ from(rows.map(x => x.rt_id)).pipe(concatMap(rt_id =>
+ this.net.request(
+ 'open-ils.reporter',
+ 'open-ils.reporter.template.delete.cascade',
+ this.auth.token(),
+ rt_id
+ ).pipe(map(res => ({
+ result: res,
+ rt_id: rt_id
+ })))
+ )).subscribe(
+ (res) => {
+ if (Number(res.result) === 2) {
+ successes++;
+ } else {
+ failures++;
+ }
+ },
+ (err) => {},
+ () => {
+ if (successes === rows.length) {
+ this.deleteSuccessString.current({ct: successes}).then(str2 => { this.toast.success(str2); });
+ } else if (failures && !successes) {
+ this.deleteFailureString.current({ct: failures}).then(str2 => { this.toast.danger(str2); });
+ } else {
+ this.mixedResultsString.current({fail: failures, success: successes})
+ .then(str2 => { this.toast.warning(str2); });
+ }
+ this.reportsGrid.reload();
+ }
+ );
+ }
+ });
+ });
+ }
+
+ cloneSelected(row: any) {
+ if ( row.length <= 0 ) { return; }
+ if ( row.length > 1 ) { return; }
+
+ const rt_row = row[0];
+
+ this.cloneString.current({old: rt_row.name})
+ .then(str => {
+ this.cloneDialog.dialogBody = str;
+ this.cloneDialog.promptValue = rt_row.name + ' (Clone)';
+ this.cloneDialog.open()
+ .subscribe(new_name => {
+ if ( new_name ) {
+ this.srSvc.loadTemplate(rt_row.rt_id)
+ .then(idl => {
+ // build a new clone
+ const new_templ = new SRTemplate(idl);
+ new_templ.name = new_name;
+ new_templ.id = -1;
+ new_templ.isNew = true;
+ new_templ.create_time = null;
+ new_templ.runNow = 'now';
+ new_templ.runTime = null;
+
+ // and save it
+ this.srSvc.saveTemplate(new_templ, false)
+ .then(rt => {
+ this.router.navigate(['edit', rt.id()], { relativeTo: this.route });
+ },
+ err => {
+ this.templateSaveErrorString.current()
+ .then(errstr => {
+ this.toast.danger(errstr + err);
+ console.error('Error saving template: %o', err);
+ });
+ });
+
+ });
+ }
+ });
+ });
+ }
+
+}
--- /dev/null
+<form #srOutputOptionsForm="ngForm" role="form" class="form-validated common-form">
+ <div class="form-group row">
+ <legend class="col-form-label col-sm-1 pt-0" i18n>Choose your output format(s)</legend>
+ <div class="col-sm-10">
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" id="srExcelOutput" name="srExcelOutput" [(ngModel)]="templ.excelOutput">
+ <label class="form-check-label" for="srExcelOutput" i18n>Excel Output</label>
+ </div>
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" id="srCsvOutput" name="srCsvOutput" [(ngModel)]="templ.csvOutput">
+ <label class="form-check-label" for="srCsvOutput" i18n>CSV Output</label>
+ </div>
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" id="srHtmlOutput" name="srHtmlOutput" [(ngModel)]="templ.htmlOutput">
+ <label class="form-check-label" for="srHtmlOutput" i18n>HTML Output</label>
+ </div>
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" id="srBarCharts" name="srBarCharts" [(ngModel)]="templ.barCharts">
+ <label class="form-check-label" for="srBarCharts" i18n>Bar Chart</label>
+ </div>
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" id="srLineCharts" name="srLineCharts" [(ngModel)]="templ.lineCharts">
+ <label class="form-check-label" for="srLineCharts" i18n>Line Chart</label>
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <legend class="col-form-label col-sm-1 pt-0" i18n>Recurrence</legend>
+ <div class="col-sm-10 form-inline">
+ <div class="form-check mr-sm-2">
+ <input class="form-check-input" type="checkbox" id="srRecurring" name="srRecurring" [(ngModel)]="templ.recurring">
+ <label class="form-check-label" for="srRecurring" i18n>Recurring Report?</label>
+ </div>
+ <label *ngIf="templ.recurring" class="mr-sm-2" for="srRecurrenceInterval">Recurrence Interval</label>
+ <eg-interval-input *ngIf="templ.recurring" [(ngModel)]="templ.recurrence" id="srRecurrenceInterval" name="srRecurrenceInterval">
+ </eg-interval-input>
+ </div>
+ </div>
+ <div class="form-group row">{{ '' // Can't use form-inline here because it breaks the calendar display }}
+ <legend class="col-form-label col-sm-1 pt-0" i18n>Scheduling</legend>
+ <div class="col-sm-10 ">
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="radio" id="srRunNow" name="srRun" value="now" [(ngModel)]="templ.runNow">
+ <label class="form-check-label" for="srRunNow" i18n>Run Report Now</label>
+ </div>
+ <div class="form-check form-check-inline">
+ <input class="form-check-input" type="radio" id="srScheduleLater" name="srRun" value="later" (change)="defaultTime()" [(ngModel)]="templ.runNow">
+ <label class="form-check-label" for="srScheduleLater" i18n>Schedule Report For Later</label>
+ </div>
+ <div class="col-sm-3">
+ <eg-datetime-select *ngIf="templ.runNow === 'later'" [(ngModel)]="templ.runTime" name="srRunTime"></eg-datetime-select>
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <legend class="col-form-label col-sm-1 pt-0" i18n>Email</legend>
+ <div class="col-sm-10 form-inline">
+ <label class="form-control-label mr-sm-2" for="srEmail" i18n>Email Address</label>
+ <input class="form-control" type="text" id="srEmail" name="srEmail" [(ngModel)]="templ.email">
+ </div>
+ </div>
+ <div class="form-group row">
+ <button class="btn btn-success" (click)="saveTemplate(true)" [disabled]="!readyToSchedule()" i18n>Save and Schedule Report</button>
+ </div>
+</form>
--- /dev/null
+import {Component, Input, Output, EventEmitter, OnInit, ViewChild} from '@angular/core';
+import {NgForm} from '@angular/forms';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {SimpleReporterService, SRTemplate} from './simple-reporter.service';
+import * as moment from 'moment-timezone';
+
+@Component({
+ selector: 'eg-sr-output-options',
+ templateUrl: './sr-output-options.component.html'
+})
+
+export class SROutputOptionsComponent implements OnInit {
+
+ @Input() templ: SRTemplate;
+ @Input() readyToSchedule: () => boolean;
+ @Input() saveTemplate: (args: any) => void;
+
+ constructor(
+ private idl: IdlService,
+ private srSvc: SimpleReporterService
+ ) { }
+
+ ngOnInit() {}
+
+ defaultTime() {
+ // When changing to Later for the first time default minutes to the quarter hour
+ if (this.templ.runNow === 'later' && this.templ.runTime === null) {
+ const now = moment();
+ const nextQ = now.add(15 - (now.minutes() % 15), 'minutes');
+ this.templ.runTime = nextQ;
+ }
+ }
+
+}
--- /dev/null
+.order-row {
+ display: flex;
+ justify-content: space-between;
+}
+
--- /dev/null
+<div class="order-row">
+
+<ngb-accordion #displayList="ngbAccordion" activeIds="display-field-list" class="col-md-6">
+
+ <ngb-panel id="display-field-list">
+ <ng-template ngbPanelHeader let-opened=true>
+ <h5 class="m-0" i18n>Field Display Order</h5>
+ </ng-template>
+ <ng-template ngbPanelContent>
+
+ <ng-container *ngFor="let f of fields; index as idx">
+ <eg-sr-field
+ [field]=f
+ [withAlias]=true
+ [withTransforms]=true
+ [withUpDown]=true
+ [disableUp]="idx === 0"
+ [disableDown]="idx === (fields.length - 1)"
+ (fieldChange)="updateField($event)"
+ (upEvent)="moveDisplayUp(idx)"
+ (downEvent)="moveDisplayDown(idx)"
+ >
+ </eg-sr-field>
+ </ng-container>
+
+ </ng-template>
+ </ngb-panel>
+
+</ngb-accordion>
+
+<ngb-accordion #orderList="ngbAccordion" activeIds="order-field-list" class="col-md-6">
+ <ngb-panel id="order-field-list">
+ <ng-template ngbPanelHeader let-opened=true>
+ <h5 class="m-0" i18n>Field Sort Order</h5>
+ </ng-template>
+ <ng-template ngbPanelContent>
+
+ <ng-container *ngFor="let f of fieldsInOrderByOrder(); index as idx">
+ <eg-sr-field
+ [field]=f
+ [withAlias]=true
+ [editAlias]=false
+ [withUpDown]=true
+ [withSortDirection]=true
+ [disableUp]="idx === 0"
+ [disableDown]="idx === (fields.length - 1)"
+ (fieldChange)="updateField($event)"
+ (upEvent)="moveOrderUp(idx)"
+ (downEvent)="moveOrderDown(idx)"
+ >
+ </eg-sr-field>
+ </ng-container>
+
+ </ng-template>
+ </ngb-panel>
+
+</ngb-accordion>
+
+</div>
--- /dev/null
+import {Component, Input, Output, EventEmitter, OnInit, ViewChild} from '@angular/core';
+import {NgbAccordion} from '@ng-bootstrap/ng-bootstrap';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {SimpleReporterService} from './simple-reporter.service';
+import {SRFieldComponent} from './sr-field.component';
+
+@Component({
+ selector: 'eg-sr-sort-order',
+ styleUrls: ['./sr-sort-order.component.css'],
+ templateUrl: './sr-sort-order.component.html'
+})
+
+export class SRSortOrderComponent implements OnInit {
+
+ @Input() fields: IdlObject[] = [];
+ @Output() fieldsChange = new EventEmitter<IdlObject[]>();
+ @Input() orderByNames: string[] = [];
+ @Output() orderByNamesChange = new EventEmitter<string[]>();
+
+ @ViewChild('displayList', { static: false }) displayList: NgbAccordion;
+ @ViewChild('orderList', { static: false }) orderList: NgbAccordion;
+
+ constructor(
+ private idl: IdlService,
+ private srSvc: SimpleReporterService
+ ) {
+ }
+
+ ngOnInit() {
+ }
+
+ updateField(field: IdlObject) {
+ const idx = this.fields.findIndex(el => el.name === field.name);
+ this.fields[idx] = field;
+ this.fieldsChange.emit(this.fields);
+ }
+
+ moveDisplayUp(idx: number) {
+ if ( idx > 0 ) { // should always be the case, but we check anyway
+ const hold: IdlObject = this.fields[idx - 1];
+ this.fields[idx - 1] = this.fields[idx];
+ this.fields[idx] = hold;
+ this.fieldsChange.emit(this.fields);
+ }
+ }
+
+ moveDisplayDown(idx: number) {
+ if ( idx < this.fields.length ) { // see above comment
+ const hold: IdlObject = this.fields[idx + 1];
+ this.fields[idx + 1] = this.fields[idx];
+ this.fields[idx] = hold;
+ this.fieldsChange.emit(this.fields);
+ }
+ }
+
+ moveOrderUp(idx: number) {
+ if ( idx > 0 ) {
+ const hold: string = this.orderByNames[idx - 1];
+ this.orderByNames[idx - 1] = this.orderByNames[idx];
+ this.orderByNames[idx] = hold;
+ this.orderByNamesChange.emit(this.orderByNames);
+ }
+ }
+
+ moveOrderDown(idx: number) {
+ if ( idx < this.orderByNames.length ) {
+ const hold: string = this.orderByNames[idx + 1];
+ this.orderByNames[idx + 1] = this.orderByNames[idx];
+ this.orderByNames[idx] = hold;
+ this.orderByNamesChange.emit(this.orderByNames);
+ }
+ }
+
+ fieldsInOrderByOrder() {
+ const sorted = [];
+ this.orderByNames.forEach(el => {
+ sorted.push(this.fields[this.fields.findIndex(fl => fl.name === el)]);
+ });
+ return sorted;
+ }
+
+}