[BP-841] Add cyclic reporting form info component

Signed-off-by: Christopher Keim <keim@develop-group.de>
diff --git a/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.html b/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.html
new file mode 100644
index 0000000..9ee5de1
--- /dev/null
+++ b/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.html
@@ -0,0 +1,53 @@
+<!--
+/********************************************************************************
+ * 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="card bg-light cyclic-report-info">
+
+  <div class="cyclic-report-info-title">
+    Nächster Auslösezeitpunkt am {{nextTriggerDate | date : dateFormat}}:
+  </div>
+
+  <div class="cyclic-report-info-block">
+
+    <div>
+      Gültigkeit:
+    </div>
+    <div>
+      {{nextValidFromDate | date : dateFormat}} bis {{nextValidToDate | date : dateFormat}}
+    </div>
+
+    <div>
+      Betreff:
+    </div>
+    <div>
+      {{subject}}
+    </div>
+
+    <div>
+      Emailtext:
+    </div>
+    <div>
+      <ng-container *ngFor="let line of emailTextLines; let last = last;">
+        {{line}} <br *ngIf="!last || line === ''">
+      </ng-container>
+    </div>
+
+    <div>
+      Dateiname:
+    </div>
+    <div>
+      {{ fileName + '.' + data?.printFormat?.toLowerCase()}}
+    </div>
+
+  </div>
+
+</div>
diff --git a/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.scss b/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.scss
new file mode 100644
index 0000000..e30ce90
--- /dev/null
+++ b/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.scss
@@ -0,0 +1,34 @@
+/********************************************************************************
+ * 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
+ ********************************************************************************/
+
+:host {
+  display: flex;
+  flex-flow: column;
+  overflow: hidden;
+  justify-content: center;
+  align-items: center;
+}
+
+.cyclic-report-info {
+  width: calc(min(100%, 35em));
+  box-sizing: border-box;
+  padding: 1em;
+  display: flex;
+  flex-flow: column;
+  word-break: break-word;
+}
+
+.cyclic-report-info-block {
+  padding-top: 0.25em;
+  padding-left: 1.25em;
+  display: grid;
+  grid-template-columns: max-content auto;
+  gap: 0 0.5em;
+}
diff --git a/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.spec.ts b/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.spec.ts
new file mode 100644
index 0000000..abff32a
--- /dev/null
+++ b/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.spec.ts
@@ -0,0 +1,106 @@
+/********************************************************************************
+ * 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 {SimpleChange} from '@angular/core';
+import {async, ComponentFixture, TestBed} from '@angular/core/testing';
+import {CyclicReportObject} from '@shared/model/CyclicReportObject';
+import {Subject} from 'rxjs';
+import {CyclicReportFormInfoComponent} from './cyclic-report-form-info.component';
+
+describe('CyclicReportFormInfoComponent', () => {
+
+  const refreshInterval = new Subject<number>();
+
+  let component: CyclicReportFormInfoComponent;
+  let fixture: ComponentFixture<CyclicReportFormInfoComponent>;
+
+  let data: CyclicReportObject;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [
+        CyclicReportFormInfoComponent
+      ],
+      imports: [
+        CommonModule
+      ]
+    }).compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CyclicReportFormInfoComponent);
+    component = fixture.componentInstance;
+    component.refreshInterval = refreshInterval;
+    component.dateReplacementTokens = component.cyclicReportingUtilService.dateReplacementTokens;
+    data = {
+      id: 19,
+      name: 'Cyclic Report ' + 19,
+      fileNamePattern: '{Date}_{Time}_{Week}',
+      subject: '{Date}_{Time}_{Week}',
+      to: [],
+      emailText: '',
+
+      reportName: 'reportName',
+      printFormat: 'pdf',
+      standByListId: 19,
+      statusId: 2,
+
+      triggerWeekDay: 1,
+      triggerHour: 7,
+      triggerMinute: 30,
+      validFromDayOffset: 0,
+      validFromHour: 7,
+      validFromMinute: 30,
+      validToDayOffset: 3,
+      validToHour: 18,
+      validToMinute: 0
+    };
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeDefined();
+  });
+
+  it('should refresh on changes', () => {
+    const refreshSpy = spyOn(component, 'refresh').and.callThrough();
+    refreshSpy.calls.reset();
+    component.data = data;
+    component.ngOnChanges({ xyz: new SimpleChange(0, 1, false)});
+    expect(refreshSpy).not.toHaveBeenCalled();
+    component.ngOnChanges({ data: new SimpleChange(undefined, data, false)});
+    expect(refreshSpy).toHaveBeenCalled();
+  });
+
+  it('should refresh automatically after a specific time', () => {
+    const refreshSpy = spyOn(component, 'refresh').and.callThrough();
+    refreshSpy.calls.reset();
+    expect(refreshSpy).not.toHaveBeenCalled();
+    refreshInterval.next(0);
+    expect(refreshSpy).toHaveBeenCalled();
+  });
+
+  it('should ignore errors when refreshing', () => {
+    component.data = data;
+    spyOn(component.cyclicReportingUtilService, 'getNextTriggerDate').and.throwError('');
+    spyOn(console, 'error');
+    expect(() => component.refresh(new Date())).not.toThrow();
+    expect(component.cyclicReportingUtilService.getNextTriggerDate).toHaveBeenCalled();
+  });
+
+  it('should replace tokens', () => {
+    const date = new Date(2020, 11, 24, 19, 0);
+    expect(component.replaceTokens('{Date}', date)).toBe('20201224');
+    expect(component.replaceTokens(null, date)).toBe('');
+    expect(component.replaceTokens('{Date}', null)).toBe('');
+  });
+
+});
diff --git a/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.ts b/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.ts
new file mode 100644
index 0000000..cd41c06
--- /dev/null
+++ b/src/app/cyclic-reporting/components/form-info/cyclic-report-form-info.component.ts
@@ -0,0 +1,106 @@
+/********************************************************************************
+ * 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, Input, OnChanges, OnDestroy, OnInit, SimpleChanges} from '@angular/core';
+import {CyclicReportObject} from '@shared/model/CyclicReportObject';
+import {interval, Subscription} from 'rxjs';
+import {CyclicReportingUtilService} from '../../services/cyclic-reporting-util.service';
+
+@Component({
+  selector: 'ok-cyclic-report-form-info',
+  styleUrls: ['cyclic-report-form-info.component.scss'],
+  templateUrl: 'cyclic-report-form-info.component.html',
+  providers: [DatePipe]
+})
+export class CyclicReportFormInfoComponent implements OnInit, OnChanges, OnDestroy {
+
+  @Input()
+  public data: CyclicReportObject;
+
+  @Input()
+  public dateReplacementTokens: { [token: string]: string };
+
+  @Input()
+  public dateFormat = 'dd.MM.yyyy, HH:mm \'Uhr\'';
+
+  public nextTriggerDate: Date;
+
+  public nextValidFromDate: Date;
+
+  public nextValidToDate: Date;
+
+  public subject: string;
+
+  public fileName: string;
+
+  public emailTextLines: string[];
+
+  public refreshInterval = interval(1000 * 30);
+
+  private subscriptions: Subscription[] = [];
+
+  public constructor(
+    public datePipe: DatePipe,
+    public cyclicReportingUtilService: CyclicReportingUtilService
+  ) {
+
+  }
+
+  public ngOnInit() {
+    this.subscriptions.push(this.refreshInterval.subscribe(() => this.refresh(new Date())));
+  }
+
+  public ngOnChanges(changes: SimpleChanges) {
+    const keysToRefresh: Array<keyof CyclicReportFormInfoComponent> = [ 'data', 'dateReplacementTokens'];
+    if (keysToRefresh.some((key) => changes[key] != null)) {
+      this.refresh(new Date());
+    }
+  }
+
+  public ngOnDestroy() {
+    this.subscriptions.forEach((subscription) => subscription.unsubscribe());
+  }
+
+  public refresh(date: Date) {
+    try {
+      this.nextTriggerDate = undefined;
+      this.nextValidFromDate = undefined;
+      this.nextValidToDate = undefined;
+      this.subject = '';
+      this.fileName = '';
+
+      if (this.data == null) {
+        return;
+      }
+
+      this.nextTriggerDate = this.cyclicReportingUtilService.getNextTriggerDate(this.data, date);
+      this.nextValidFromDate = this.cyclicReportingUtilService.moveDateByDays(this.nextTriggerDate,
+        this.data.validFromDayOffset, this.data.validFromHour, this.data.validFromMinute);
+      this.nextValidToDate = this.cyclicReportingUtilService.moveDateByDays(this.nextTriggerDate,
+        this.data.validToDayOffset, this.data.validToHour, this.data.validToMinute);
+
+      this.subject = this.replaceTokens(this.data.subject, this.nextTriggerDate);
+      this.fileName = this.replaceTokens(this.data.fileNamePattern, this.nextTriggerDate);
+      this.emailTextLines = this.replaceTokens(this.data.emailText, this.nextTriggerDate).split('\n');
+    } catch (e) {
+      console.error(e);
+    }
+  }
+
+  public replaceTokens(value: string, date: Date) {
+    const replacements = Object.entries({...this.dateReplacementTokens})
+      .map(([token, dateFormat]) => ({token, value: date == null ? '' : this.datePipe.transform(date, dateFormat)}));
+    return replacements.reduce((result, replacement) => {
+      return result.replace(new RegExp(replacement.token, 'g'), replacement.value);
+    }, typeof value !== 'string' ? '' : value);
+  }
+
+}