[BP-841] Add cyclic reporting overview component

Signed-off-by: Christopher Keim <keim@develop-group.de>
diff --git a/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.html b/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.html
new file mode 100644
index 0000000..52d81b8
--- /dev/null
+++ b/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.html
@@ -0,0 +1,47 @@
+<!--
+/********************************************************************************
+ * Copyright © 2020 Basys GmbH.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+-->
+
+<div class="container-fluid">
+  <div class="row">
+    <div class="col-md-12">
+      <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
+        <h1 class="h2" id="overviewHeader">Zyklische Reports</h1>
+        <div class="btn-toolbar mb-2 mb-md-0">
+          <div class="btn-group mr-2">
+            <button class="btn btn-primary mr-1" routerLink="new">
+              Neuer Eintrag
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <div class="row">
+    <div class="col-md-12">
+      <ag-grid-angular
+        #gridNg2
+        [localeText]="localeText"
+        domLayout="autoHeight"
+        class="ag-theme-balham cyclic-reports-table"
+        [defaultColDef]="defaultColDef"
+        [rowData]="data"
+        [columnDefs]="columnDefs"
+        (gridReady)="onGridReady()"
+        (rowClicked)="selectEntry($event?.data)"
+        pagination="true"
+        paginationPageSize="30"
+        id="oragnisationGrid">
+      </ag-grid-angular>
+    </div>
+  </div>
+</div>
diff --git a/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.scss b/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.scss
new file mode 100644
index 0000000..c99c596
--- /dev/null
+++ b/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.scss
@@ -0,0 +1,9 @@
+/********************************************************************************
+ * Copyright © 2020 Basys GmbH.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
diff --git a/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.spec.ts b/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.spec.ts
new file mode 100644
index 0000000..e1f667a
--- /dev/null
+++ b/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.spec.ts
@@ -0,0 +1,126 @@
+/********************************************************************************
+ * Copyright © 2020 Basys GmbH.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {CommonModule} from '@angular/common';
+import {HttpClientTestingModule} from '@angular/common/http/testing';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {RouterTestingModule} from '@angular/router/testing';
+import {SharedModule} from '@shared/shared.module';
+import {StandbylistObject} from '@shared/model/StandbylistObject';
+import {CyclicReportObject} from '@shared/model/CyclicReportObject';
+import {RowNode} from 'ag-grid-community';
+import {of} from 'rxjs';
+import {CyclicReportsOverviewComponent} from './cyclic-reports-overview.component';
+
+function createMockData<T extends object>(data: Partial<T>): T {
+  return (data == null ? {} : data) as T;
+}
+
+describe('CyclicReportsOverviewComponent', () => {
+
+  let component: CyclicReportsOverviewComponent;
+  let fixture: ComponentFixture<CyclicReportsOverviewComponent>;
+
+  const standByListData: StandbylistObject[] = Array(42).fill(0)
+    .map((_, id) => createMockData<StandbylistObject>({ id, title: 'QVL ' + id }));
+
+  const reportData: CyclicReportObject[] = Array(100).fill(0)
+    .map((_, id) => createMockData<CyclicReportObject>({
+      id,
+      name: 'Cyclic Report ' + id,
+      fileNamePattern: '{Date}_{Time}_{Week}_' + id,
+      subject: 'Subject ' + id,
+      to: [],
+
+      reportName: 'ReportName ' + id,
+      printFormat: 'pdf',
+      standByListId: id % 42,
+      statusId: 2,
+
+      triggerWeekDay: (id + 6) % 7 + 1,
+      triggerHour: id % 24,
+      triggerMinute: (id % 12) * 5,
+      validFromDayOffset: - id % 7,
+      validFromHour: (id + 1) % 24,
+      validFromMinute: (id + 2) % 60,
+      validToDayOffset: 1 + id % 7,
+      validToHour: (id + 3) % 24,
+      validToMinute: (id + 4) % 24
+    }));
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [
+        CyclicReportsOverviewComponent
+      ],
+      imports: [
+        CommonModule,
+        SharedModule,
+        RouterTestingModule,
+        HttpClientTestingModule
+      ]
+    }).compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CyclicReportsOverviewComponent);
+    component = fixture.componentInstance;
+    spyOn(component.masterdataService, 'getStandbyListSelection').and.returnValue(of(standByListData));
+    spyOn(component.cyclicReportingService, 'getCyclicReports').and.returnValue(of(reportData));
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeDefined();
+  });
+
+  it('should compare trigger dates', () => {
+    const comparator = component.columnDefs.find((col) => col.colId === 'trigger').comparator;
+    const nodeA = createMockData<RowNode>({ data: reportData[0] });
+    const nodeB = createMockData<RowNode>({ data: reportData[1] });
+    expect(comparator('', '', nodeA, nodeA)).toBe(0);
+    expect(comparator('', '', nodeA, nodeB)).toBe(6);
+    expect(comparator('', '', nodeB, nodeA)).toBe(-6);
+    expect(comparator('', '', nodeB, nodeB)).toBe(0);
+  });
+
+  it('should sort table', async () => {
+    expect(() => component.sort('trigger', 'desc')).not.toThrow();
+    await fixture.whenStable();
+  });
+
+  it('should select entries', async () => {
+    const navigateSpy = spyOn(component.router, 'navigate');
+    navigateSpy.calls.reset();
+    await component.selectEntry(null);
+    await component.selectEntry(createMockData<CyclicReportObject>({}));
+    expect(navigateSpy).not.toHaveBeenCalled();
+    await component.selectEntry(createMockData<CyclicReportObject>({ id: 19 }));
+    expect(navigateSpy).toHaveBeenCalledWith([19], { relativeTo: component.activatedRoute });
+  });
+
+  it('should format data', () => {
+    component.fetch();
+    expect(component.formatData().length).toBe(reportData.length);
+    expect(component.formatData(null)).toEqual([]);
+    expect(component.formatData([ null, null ])).toEqual([]);
+  });
+
+  it('should format list name', () => {
+    component.fetch();
+    expect(component.data[0].listName).toBe('QVL 0');
+    expect(component.formatListName(null)).toEqual('');
+    expect(component.formatListName(createMockData<CyclicReportObject>({ standByListId: 19 }))).toEqual(standByListData[19].title);
+    expect(component.formatListName(createMockData<CyclicReportObject>({ standByListId: standByListData.length }))).toEqual('');
+    component.standByList = null;
+    expect(component.formatListName(createMockData<CyclicReportObject>({ standByListId: 19 }))).toEqual('');
+  });
+
+});
diff --git a/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.ts b/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.ts
new file mode 100644
index 0000000..7d8be30
--- /dev/null
+++ b/src/app/cyclic-reporting/components/overview/cyclic-reports-overview.component.ts
@@ -0,0 +1,143 @@
+/********************************************************************************
+ * Copyright © 2020 Basys GmbH.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ ********************************************************************************/
+
+import {DatePipe} from '@angular/common';
+import {Component, OnDestroy, ViewChild} from '@angular/core';
+import {ActivatedRoute, Router} from '@angular/router';
+import {MasterdataService} from '@masterdata/services/masterdata.service';
+import {StandbylistObject} from '@shared/model/StandbylistObject';
+import {CyclicReportObject} from '@shared/model/CyclicReportObject';
+import {AGGRID_LOCALETEXT, DEFAULT_COL_DEF} from '@shared/utils/list.util';
+import {AgGridNg2} from 'ag-grid-angular';
+import {ColDef} from 'ag-grid-community';
+import {Subscription} from 'rxjs';
+import {CyclicReportingUtilService} from '../../services/cyclic-reporting-util.service';
+import {CyclicReportingService} from '../../services/cyclic-reporting.service';
+
+export interface CyclicReportTableEntry extends CyclicReportObject {
+  trigger: string;
+  listName: string;
+}
+
+@Component({
+  selector: 'ok-cyclic-reports-overview',
+  styleUrls: ['cyclic-reports-overview.component.scss'],
+  templateUrl: 'cyclic-reports-overview.component.html',
+  providers: [
+    DatePipe
+  ]
+})
+export class CyclicReportsOverviewComponent implements OnDestroy {
+
+  public hourFormat = 'HH:mm';
+
+  public defaultColDef = DEFAULT_COL_DEF;
+
+  public columnDefs: ColDef[] = [
+    {
+      headerName: 'Name',
+      field: 'name',
+      colId: 'name'
+    },
+    {
+      headerName: 'Planart',
+      field: 'reportName',
+      colId: 'reportName',
+    },
+    {
+      headerName: 'Liste',
+      field: 'listName',
+      colId: 'listName',
+    },
+    {
+      headerName: 'Auslösezeitpunkt',
+      field: 'trigger',
+      colId: 'trigger',
+      comparator: (valueA, valueB, nodeA, nodeB, isInverted) => {
+        return this.cyclicReportingUtilService.cyclicReportTriggerComparator(nodeA.data, nodeB.data);
+      }
+    }
+  ];
+
+  public localeText = AGGRID_LOCALETEXT;
+
+  public data: CyclicReportTableEntry[];
+
+  public standByList: StandbylistObject[] = [];
+
+  private subscriptions: Subscription[] = [];
+
+  @ViewChild(AgGridNg2)
+  private grid: AgGridNg2;
+
+  public constructor(
+    public cyclicReportingService: CyclicReportingService,
+    public cyclicReportingUtilService: CyclicReportingUtilService,
+    public masterdataService: MasterdataService,
+    public router: Router,
+    public activatedRoute: ActivatedRoute,
+    public datePipe: DatePipe
+  ) {
+
+  }
+
+  public ngOnDestroy() {
+    this.subscriptions.forEach((subscription) => subscription.unsubscribe());
+  }
+
+  public onGridReady() {
+    this.fetch();
+    this.grid.api.sizeColumnsToFit();
+    this.sort('trigger');
+  }
+
+  public sort(colId: string, sort = 'asc') {
+    this.grid.api.setSortModel([{colId, sort}]);
+  }
+
+  public fetch() {
+    this.subscriptions.push(...[
+      this.cyclicReportingService.getCyclicReports().subscribe((data) => this.formatData(data)),
+      this.masterdataService.getStandbyListSelection().subscribe((data) => {
+        this.standByList = data;
+        this.formatData();
+      })
+    ]);
+  }
+
+  public async selectEntry(report: CyclicReportObject) {
+    const id = report != null ? report.id : undefined;
+    if (id != null) {
+      return this.router.navigate([id], { relativeTo: this.activatedRoute });
+    }
+  }
+
+  public formatData(data: CyclicReportObject[] = this.data) {
+    return this.data = (Array.isArray(data) ? data : [])
+      .filter((entry) => entry != null)
+      .map((entry) => {
+        const nextTriggerDate = this.cyclicReportingUtilService.getNextTriggerDate(entry);
+        const weekDayLabel = this.cyclicReportingUtilService.formatWeekDay(entry.triggerWeekDay);
+        return {
+          ...entry,
+          trigger: this.datePipe.transform(nextTriggerDate, `'${weekDayLabel}', ${this.hourFormat}`),
+          listName: this.formatListName(entry)
+        };
+      });
+  }
+
+  public formatListName(report: CyclicReportObject): string {
+    const standByList = Array.isArray(this.standByList) ? this.standByList : [];
+    const standByListId = report != null ? report.standByListId : undefined;
+    const selectedList = standByList.find((entry) => entry.id === standByListId);
+    return selectedList != null ? selectedList.title : '';
+  }
+
+}