From 0900fa1ac157baa5b568486cb9eb917d50dbd7c5 Mon Sep 17 00:00:00 2001 From: "m.ilhan" Date: Tue, 3 Dec 2024 08:29:33 +0100 Subject: [PATCH] tv app --- src/app/app-routing.module.ts | 11 +- src/app/pages/aviso/aviso-routing.module.ts | 10 +- src/app/pages/aviso/aviso.module.ts | 4 +- src/app/pages/aviso/aviso.page.html | 69 +- src/app/pages/aviso/aviso.page.scss | 241 +++--- src/app/pages/aviso/aviso.page.ts | 850 +++++++++++--------- src/app/pages/aviso/aviso.resolver.ts | 52 ++ src/app/services/aviso-tv-settings.dto.ts | 2 +- src/app/services/aviso.dto.ts | 38 + src/app/services/aviso.service.ts | 18 +- src/assets/Logos/verag.png | Bin 0 -> 33039 bytes 11 files changed, 791 insertions(+), 504 deletions(-) create mode 100644 src/app/pages/aviso/aviso.resolver.ts create mode 100644 src/assets/Logos/verag.png diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index a477ecd..64e7075 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,19 +1,26 @@ + import { NgModule } from '@angular/core'; import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; +import { AvisoResolver } from './pages/aviso/aviso.resolver'; // Importieren Sie den Resolver const routes: Routes = [ { path: 'home', - loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule) + loadChildren: () => import('./pages/home/home.module').then(m => m.HomePageModule) }, { path: 'aviso', - loadChildren: () => import('./pages/aviso/aviso.module').then( m => m.AvisoPageModule) + loadChildren: () => import('./pages/aviso/aviso.module').then(m => m.AvisoPageModule), + resolve: { avisoData: AvisoResolver } // Hinzufügen des Resolvers zur Route }, { path: '', redirectTo: 'aviso', pathMatch: 'full' + }, + { + path: '**', + redirectTo: 'aviso' } ]; diff --git a/src/app/pages/aviso/aviso-routing.module.ts b/src/app/pages/aviso/aviso-routing.module.ts index 1a39cee..fcba62f 100644 --- a/src/app/pages/aviso/aviso-routing.module.ts +++ b/src/app/pages/aviso/aviso-routing.module.ts @@ -1,12 +1,14 @@ + import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; - +import { RouterModule, Routes } from '@angular/router'; import { AvisoPage } from './aviso.page'; +import { AvisoResolver } from './aviso.resolver'; // Pfad anpassen const routes: Routes = [ { path: '', - component: AvisoPage + component: AvisoPage, + resolve: { avisoData: AvisoResolver } } ]; @@ -14,4 +16,4 @@ const routes: Routes = [ imports: [RouterModule.forChild(routes)], exports: [RouterModule], }) -export class AvisoPageRoutingModule {} +export class AvisoPageRoutingModule { } diff --git a/src/app/pages/aviso/aviso.module.ts b/src/app/pages/aviso/aviso.module.ts index 9df1bf6..776f4df 100644 --- a/src/app/pages/aviso/aviso.module.ts +++ b/src/app/pages/aviso/aviso.module.ts @@ -1,14 +1,14 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; +import { AvisoPage } from './aviso.page'; import { IonicModule } from '@ionic/angular'; import { AvisoPageRoutingModule } from './aviso-routing.module'; import { AutoResizeTextDirective } from '../../directives/auto-resize-text.directive'; -import { AvisoPage } from './aviso.page'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; + import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @NgModule({ imports: [ diff --git a/src/app/pages/aviso/aviso.page.html b/src/app/pages/aviso/aviso.page.html index 2b1a311..5d7bb5c 100644 --- a/src/app/pages/aviso/aviso.page.html +++ b/src/app/pages/aviso/aviso.page.html @@ -1,14 +1,10 @@ - - - - - Ankünfte ({{ totalArrivals }}) Seite {{ currentPageIndex + 1 }} von {{ pages.length }} - - + + + @@ -28,10 +24,14 @@ -
- + + [ngClass]="getStatusClass(aviso.status, aviso.lkW_fertig)"> + + +
{{ getOverallIndex(i) }}
+ {{ aviso.lkW_Nr }} @@ -44,7 +44,7 @@ {{ aviso.ankunft }} - + {{ aviso.dauer }}

@@ -56,6 +56,7 @@

+ @@ -70,27 +71,45 @@
+ + + {{ currentDate | date: 'HH:mm:ss' }}
+ {{ currentDate | date: ' dd.MM.yyyy' }} + +
+ + + + + + ({{ totalArrivals }})
{{ currentPageIndex + 1 }} / {{ pages.length }}
+ +
- - + + - -
-
- - -
+
+
+ + + + + +
+
+ +
diff --git a/src/app/pages/aviso/aviso.page.scss b/src/app/pages/aviso/aviso.page.scss index 8098bba..ab9ad47 100644 --- a/src/app/pages/aviso/aviso.page.scss +++ b/src/app/pages/aviso/aviso.page.scss @@ -1,89 +1,54 @@ - - ion-content { --background: #f0f0f0; /* Heller Hintergrund für besseren Kontrast */ } -.title-card { - background-color: #3880ff; /* Beispiel: Blaue Hintergrundfarbe */ - color: white; - text-align: center; -} - -ion-card-content p { - font-size: 1.8em; /* Größere Schriftgröße für die Inhalte */ - margin: 10px 0; -} - .grid-container { display: grid; - grid-template-columns: repeat(auto-fill, minmax(15%, 1fr)); /* Feste Mindestbreite */ - gap: 1%; /* Abstand zwischen den Kacheln */ - padding: 0px; /* Optional: Innenabstand */ - justify-content: center; /* Zentriert die Kacheln */ - } + grid-template-columns: repeat(auto-fill, minmax(var(--tile-width-percent), 1fr)); + row-gap: 1vh; + column-gap: 1vh; + padding: 0px; + justify-content: center; +} .arrival-card { - width: 95%; /* Feste Breite */ - height: 24vh; /* Feste Höhe */ - border-radius: 15px; /* Abgerundete Ecken für ein moderneres Aussehen */ + height: var(--tile-height-vh); + border-radius: 15px; transition: box-shadow 0.3s, transform 0.3s; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 0; + padding: 0em; + box-sizing: border-box; /* Padding wird in der Höhe berücksichtigt */ + animation: fadeIn 0.2s ease-in-out; /* Kürzere Animationsdauer */ } .htmltext { - height: 30vh; + height: var(--text-balken-height-vh); width: 100%; white-space: pre-line; transition: box-shadow 0.3s, transform 0.3s; } -ion-content { - ion-grid.htmltext { - height: 30vh; - } -} - - - ion-icon { vertical-align: middle; - font-size: 1.2em; /* Größere Icons für bessere Sichtbarkeit */ + font-size: calc(var(--tile-height-vh) * 0.18); /* Anpassung der Icon-Größe */ + margin-right: 0.3em; /* Abstand zwischen Icon und Text */ } /* Statusklassen */ -.statusLKW_Erfasst { - border-left: 10px solid #2dd36f; /* Grün */ -} - -.statusLKW_Freigegeben { - border-left: 10px solid #3880ff; /* Blau */ -} - -.statusLKW_LKW_neu { - border-left: 10px solid #f78c40; /* Orange */ +.lkwfertig { + border-left: 10px solid green; + background-color: #e6ffe6; /* Leicht grüner Hintergrund für "fertig" */ } +.statusLKW_LKW_neu, .statusLKW_Ankunft { - border-left: 10px solid #f78c40; /* Orange */ -} - -.statusLKW_Vorbereitet { - border-left: 10px solid #c8c8c8; /* Grau */ -} - -.statusLKW_Vorgeschrieben { - border-left: 10px solid #a259ff; /* Violett */ -} - -.status-default { - border-left: 10px solid #c8c8c8; /* Grau */ -} + } /* Animationen für Ankunftskarten */ -.arrival-card { - animation: fadeIn 0.5s ease-in-out; -} - @keyframes fadeIn { from { opacity: 0; @@ -96,54 +61,138 @@ ion-icon { } } -@media (min-width: 1200px) { +/* Typografie */ +ion-title { + font-size: calc(var(--tile-height-vh) * 0.24); /* 4% der Kachelhöhe */ + font-weight: bold; + color: #003680; +} + +.title { + font-size: calc(var(--tile-height-vh) * 0.24); /* 4% der Kachelhöhe */ + font-weight: bold; + color: #003680; +} + +ion-card-title { + font-size: calc(var(--tile-height-vh) * 0.06); /* Erhöht auf 6% der Kachelhöhe */ + margin: 0; /* Abstand unter dem Titel */ + text-align: center; + white-space: nowrap; /* Erlaubt Zeilenumbrüche */ + overflow-wrap: normal; /* Bricht lange Wörter um */ + color: #002050; /* Dunkleres Blau für besseren Kontrast */ +} + +ion-card-content p { + font-size: calc(var(--tile-height-vh) * 0.04); /* Erhöht auf 4% der Kachelhöhe */ + margin: 0; + text-align: center; + white-space: nowrap; /* Erlaubt Zeilenumbrüche */ + overflow-wrap: normal; /* Bricht lange Wörter um */ + line-height: 1.5; /* Verbesserte Lesbarkeit */ + color: #333; /* Dunkle Schriftfarbe für besseren Kontrast */ +} + +.centered-content p { + margin-bottom: 0.5em; /* Abstand zwischen den Absätzen */ +} + +/* Icon-Text-Gruppe */ +.icon-text-group { + display: flex; + align-items: center; + margin-bottom: 0.5em; +} + +.icon-text-group ion-icon { + margin-right: 0.5em; +} + +/* Mobile Geräte */ +@media (max-width: 599px) { + .arrival-card { + height: auto; /* Höhe passt sich dem Inhalt an */ + } + ion-card-title { - font-size: 3.5em; + font-size: calc(var(--tile-height-vh) * 0.08); } ion-card-content p { - font-size: 2em; + font-size: calc(var(--tile-height-vh) * 0.05); } } -ion-text p { - font-size: 2em; -} +/* Tablets */ +@media (min-width: 600px) and (max-width: 1199px) { + ion-card-title { + font-size: var(--tile-title-font-size); + } -.card-title { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 2.5em; - font-weight: bold; -} - -.card-title ion-icon { - margin-left: auto; -} - -ion-footer { - background-color: #ffffff; - border-top: 1px solid #c8c8c8; -} - -ion-toolbar { - --padding-start: 0; - --padding-end: 0; -} - -ion-title { - font-size: 1.6em; - font-weight: bold; - color: #3880ff; -} - -@media (max-width: 600px) { - ion-title { - font-size: 1.4em; + ion-card-content p { + font-size: var(--tile-font-size-date-time); } } + +/* Desktops */ +@media (min-width: 1200px) { + ion-card-title { + font-size: var(--tile-title-font-size); + } + + ion-card-content p { + font-size: var(--tile-font-size-date-time); + } +} + +.logo { + max-height: calc(var(--tile-height-vh) * 0.40); + object-fit: contain; +} + +.logobar { + max-height: 10vh; +} + +.custom-progress-bar { + --progress-background: #003680; /* Fortschrittsfarbe (gefüllter Teil) */ + --background: #e0e0e0; /* Hintergrundfarbe der Leiste */ + --buffer-background: #003680; /* Hintergrundfarbe des Puffers */ +} +.card-number { + position: absolute; + top: 5px; + left: 5px; + font-size: calc(var(--tile-height-vh) * 0.18); + font-weight: bold; + background-color: transparent; + padding: 2px 5px; + border-radius: 3px; +} +/* Versteckt die Buttons und zeigt nur den Indikator-Strich an */ +.custom-segment-button { + --color: transparent; /* Text unsichtbar machen */ + --background: #003680; /* Hintergrund des Buttons transparent */ + min-height: 0; + padding: 0; + margin: 0; + border: none; + width: 200px; + --indicator-color: transparent; +} + +/* Styling für das ion-segment */ +.custom-segment { + --background: transparent; + height: 0.4vh; + position: relative; + margin: 0; + padding: 0; + +} + + diff --git a/src/app/pages/aviso/aviso.page.ts b/src/app/pages/aviso/aviso.page.ts index 525274e..d398f0e 100644 --- a/src/app/pages/aviso/aviso.page.ts +++ b/src/app/pages/aviso/aviso.page.ts @@ -1,383 +1,439 @@ +// src/app/pages/aviso/aviso.page.ts + import { - Component,OnInit,OnDestroy,HostListener,ChangeDetectorRef + Component, + OnInit, + OnDestroy, + HostListener, + ChangeDetectorRef, + HostBinding } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { AvisoService, AvisoArrivalsResponse } from '../../services/aviso.service'; -import { AvisoDto } from '../../services/aviso.dto'; -import { AvisoTvSettingsDto } from '../../services/aviso-tv-settings.dto'; -import { ToastController } from '@ionic/angular'; -import { interval, Subject } from 'rxjs'; -import { switchMap, takeUntil, startWith } from 'rxjs/operators'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; - +import { interval, Subject, throwError, lastValueFrom, Subscription } from 'rxjs'; +import { switchMap, takeUntil, distinctUntilChanged, tap, catchError, map, take } from 'rxjs/operators'; + +import { AvisoService, AvisoArrivalsResponse } from '../../services/aviso.service'; +import { AvisoDto, TvSettings } from '../../services/aviso.dto'; +import { AvisoTvTextSettingsDto } from '../../services/aviso-tv-settings.dto'; + +// Schnittstelle für die aufgelösten Daten vom Resolver +interface AvisoResolvedData { + avisoTvTextSettings: AvisoTvTextSettingsDto[]; + avisoTvSettings: TvSettings[]; + arrivals: AvisoArrivalsResponse; +} + +interface AvisoPageData { + avisoData: AvisoResolvedData; +} + +interface CssVariables { + tileWidthPercent: string; + tileHeightVh: string; + tileTitleFontSize: string; + tileFontSizeDateTime: string; + tileSeitenwechselInSek: string; + textBalkenHeightVh: string; +} + +const ARRIVALS_INTERVAL_MS = 10000; +const TVTEXTSETTINGS_INTERVAL_MS = 60000; +const TVSETTINGS_INTERVAL_MS = 10000; +const TOGGLE_DIV_INTERVAL_MS = 5000; + +interface StatusInfo { + class: string; + icon: string; + color: string; + text: string; +} + +const STATUS_MAP: { [key: string]: StatusInfo } = { + '3': { class: 'statusLKW_Ankunft', icon: 'time', color: 'medium', text: 'Ankunft' }, + 'lkwfertig': { class: 'lkwfertig', icon: 'time', color: 'warning', text: 'Ankunft' }, + 'default': { class: 'status-default', icon: 'help-circle', color: 'medium', text: 'Unbekannt' } +}; + +// Neues Interface für die Anzeige der Fixzeilen +interface SettingDisplayData { + nonEmptyFixeZeilen: { + sanitizedHtml: SafeHtml; + textAlign: string; + }[]; + currentDivIndex: number; +} + @Component({ selector: 'app-aviso', templateUrl: './aviso.page.html', styleUrls: ['./aviso.page.scss'], }) export class AvisoPage implements OnInit, OnDestroy { - - - standort: string = ''; - seiten: boolean = false; - onlyOK: boolean = false; - + + // HostBindings für CSS-Variablen + @HostBinding('style.--tile-width-percent') tileWidthPercent = '30vw'; + @HostBinding('style.--tile-height-vh') tileHeightVh = '20vh'; + @HostBinding('style.--tile-title-font-size') tileTitleFontSize = '2vh'; + @HostBinding('style.--tile-font-size-date-time') tileFontSizeDateTime = '1vh'; + @HostBinding('style.--text-balken-height-vh') textBalkenHeightVh = '5vh'; + + progressBarValue: number = 0; + private progressBarSubscription: Subscription = new Subscription(); + + + currentDate: Date = new Date(); + private dateSubscription: Subscription = new Subscription(); + + // Standort- und Filterinformationen + standort = ''; + standortID = 0; + seiten = false; + onlyOK = false; + // Datenmodelle arrivals: AvisoDto[] = []; - avisoTvSettings: AvisoTvSettingsDto[] = []; // Neues Datenmodell + avisoTvTextSettings: AvisoTvTextSettingsDto[] = []; + avisoTvSettings: TvSettings[] = []; // Paginierung pages: AvisoDto[][] = []; - currentPageIndex: number = 0; - tilesPerPage: number = 0; + currentPageIndex = 0; + tilesPerPage = 0; - // Ladezustand - loadingArrivals: boolean = false; - loadingSettings: boolean = false; // Ladezustand für Einstellungen + // Ladezustände + loadingArrivals = false; + loadingTextSettings = false; + loadingTvSettings = false; - // Gesamtanzahl - totalArrivals: number = 0; - totalSettings: number = 0; // Gesamtanzahl für Einstellungen + // Gesamtanzahlen + totalArrivals = 0; + totalTextSettings = 0; - // Fehlernachricht - errorMessage: string = ''; - settingsErrorMessage: string = ''; // Fehlernachricht für Einstellungen + // Fehlermeldungen + errorMessage = ''; + settingsErrorMessage = ''; - // Subjects zum Beenden der Subscriptions + // Sonstige Variablen private destroy$ = new Subject(); - private pageRotation$ = new Subject(); // Optional, für separate Page Rotation - - // Feste Kachelgrößen (entsprechend den CSS-Einstellungen) - private readonly tileWidth: number = 450; // in px - private readonly tileHeight: number = 100; // in px - private readonly gutter: number = 0; // Abstand zwischen den Kacheln in px - private readonly containerPadding: number = 0; // Innenabstand des Grid-Containers in px - public fontSettings: {textAlign: string}[] = []; private toggleDivInterval: any; - showFirstDiv: boolean = true; + private currentCssVariables: CssVariables | null = null; + + // Neues Array für die Anzeige der Fixzeilen + settingDisplayData: SettingDisplayData[] = []; constructor( private route: ActivatedRoute, private avisoService: AvisoService, - private toastController: ToastController, private cdr: ChangeDetectorRef, private sanitizer: DomSanitizer ) { } - - /** - * Sanitizes HTML, extracts font size and text align from span elements, - * and stores them in dataset attributes and the fontSettings array. - * @param html Das zu sanitizende HTML - * @param index Index der Einstellung zur Zuordnung der Stilinformationen - * @returns Sanitized HTML als SafeHtml - */ - sanitizeHtml(html: string, index: number): SafeHtml { - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - const spans = doc.querySelectorAll('div'); - - spans.forEach(span => { - const fontSize = span.style.fontSize; - - if (fontSize) { - - let fontSizePx: number; - if (fontSize.endsWith('pt')) { - fontSizePx = parseFloat(fontSize) * 1.333; // Umrechnung von pt zu px - } else if (fontSize.endsWith('px')) { - fontSizePx = parseFloat(fontSize); - } else { - // Andere Einheiten oder Standardfall - fontSizePx = parseFloat(fontSize); - } - - const textAlign = span.style.textAlign || 'left'; - - // Speichern der ursprünglichen Schriftgröße und Textausrichtung - this.fontSettings[index] = { - textAlign: textAlign - }; - - // Speichern in dataset - span.dataset['originalFontSizePx'] = fontSizePx.toString(); - span.dataset['originalTextAlign'] = textAlign; - - // Debugging: Loggen der ursprünglichen Schriftgröße - console.log(`Original Font Size for span: "${span.textContent}" = ${fontSizePx}px`); - } else { - // Falls keine Schriftgröße definiert ist - console.log(`No inline font size found for span: "${span.textContent}"`); - } - - // Entfernen der Inline-Schriftgröße - span.style.fontSize = ''; - - // Hinzufügen der Klasse für die dynamische Anpassung - span.classList.add('dynamic-font-size'); - }); - - // Debugging: Loggen des bearbeiteten HTML - console.log('Processed HTML:', doc.body.innerHTML); - - return this.sanitizer.bypassSecurityTrustHtml(doc.body.innerHTML); + * Konvertiert eine Schriftgröße in verschiedene Einheiten zu Pixel. + * @param fontSize Die Schriftgröße als String (z.B. '12pt', '16px') + * @returns Die Schriftgröße in Pixel + */ + private convertToPx(fontSize: string): number { + if (fontSize.endsWith('pt')) { + return parseFloat(fontSize) * 1.333; + } else if (fontSize.endsWith('px')) { + return parseFloat(fontSize); + } else { + return parseFloat(fontSize); + } } - getTextAlign(index: number): string { - const settings = this.fontSettings[index]; - return settings ? settings.textAlign : 'left'; - } - - ngOnInit() { - this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => { - this.standort = params['standort'] || 'SUB'; - this.seiten = params['seiten'] === 'true' || false; - this.onlyOK = params['onlyOK'] === 'true' || false; - - // Initiales Laden der Daten - this.loadArrivals(); - this.loadAvisoTvSettings(); - this.startDivToggle(); + this.currentDate = new Date(); + // Aktualisieren Sie das Datum jede Sekunde + this.dateSubscription = interval(1000).subscribe(() => { + this.currentDate = new Date(); }); - interval(1000000) // 10000 Millisekunden = 10 Sekunden + this.route.paramMap.subscribe(params => { + this.standort = params.get('standort') || 'SUB'; + this.standortID = parseInt(params.get('standortID') || '1', 10); + }); + + this.route.queryParamMap.subscribe(queryParams => { + this.seiten = queryParams.get('seiten') === 'true'; + this.onlyOK = queryParams.get('onlyOK') === 'true'; + }); + + // Zugriff auf die vom Resolver bereitgestellten Daten + this.route.data.pipe(takeUntil(this.destroy$)).subscribe((resolvedData) => { + const data = (resolvedData as any)['avisoData'] as AvisoResolvedData; + + this.avisoTvTextSettings = data.avisoTvTextSettings; + this.avisoTvSettings = data.avisoTvSettings; + this.arrivals = data.arrivals.avisos; + this.totalArrivals = data.arrivals.totalCount; + + // Process the HTML settings + this.preprocessHtmlSettings(); + + // Setzen der CSS-Variablen + this.setCSSVariables(); + + // Starten der Div-Umschaltung + this.startDivToggle(); + + // Starten der Seitenrotation + this.paginateArrivals(); + this.startPageRotation(); + }); + + + // Automatisches Aktualisieren der Arrivals alle 10 Sekunden + interval(ARRIVALS_INTERVAL_MS) .pipe( - startWith(0), // Sofortiger Start ohne Warten switchMap(() => { - // Nur den Ladeindikator anzeigen, wenn keine Daten vorhanden sind this.loadingArrivals = this.arrivals.length === 0; this.errorMessage = ''; return this.avisoService.getArrivals(this.standort, this.seiten, this.onlyOK); }), + distinctUntilChanged((prev, curr) => this.arrivalsAreEqual(prev.avisos, curr.avisos)), takeUntil(this.destroy$) ) .subscribe( - (data: AvisoArrivalsResponse) => { // Typisierung der empfangenen Daten - console.log('Received arrivals data:', data); // Zum Debuggen + (data: AvisoArrivalsResponse) => { this.arrivals = data.avisos; this.totalArrivals = data.totalCount; this.loadingArrivals = false; this.paginateArrivals(); - this.cdr.detectChanges(); // Manuelle Change Detection + this.cdr.detectChanges(); }, (error) => { - console.error('Fehler beim Laden der Arrivals:', error); - this.presentToast(`Fehler beim Laden der Arrivals: ${error}`); this.errorMessage = 'Fehler beim Laden der Arrivals. Bitte versuche es später erneut.'; this.loadingArrivals = false; } ); - - // Automatisches Aktualisieren der TV-Einstellungen alle 10 Sekunden - interval(1000000) // 10000 Millisekunden = 10 Sekunden + // Automatisches Aktualisieren der TV Settings alle 10 Sekunden + interval(TVSETTINGS_INTERVAL_MS) .pipe( - startWith(0), - switchMap(() => { - this.loadingSettings = this.avisoTvSettings.length === 0; - this.settingsErrorMessage = ''; - return this.avisoService.getAvisoTvSettings(this.standort); - }), + switchMap(() => this.loadAvisoTvSettings()), takeUntil(this.destroy$) ) - .subscribe( - (data: AvisoTvSettingsDto[]) => { // Typisierung als Array - console.log('Received TV settings data:', data); - this.avisoTvSettings = data; - this.totalSettings = data.length; // Setze totalSettings auf die Länge des Arrays - this.loadingSettings = false; - this.cdr.detectChanges(); // Manuelle Change Detection + .subscribe(); - // Debugging: Überprüfe jede Einstellung - this.avisoTvSettings.forEach(setting => { - console.log(`Setting ID: ${setting.tvTextBezeichnungID}, isActive: ${setting.isActive}, position: ${setting.position}, fixeZeile1: ${setting.fixeZeile1}`); - }); - }, - (error) => { - console.error('Fehler beim Laden der TV Settings:', error); - this.presentToast(`Fehler beim Laden der TV Settings: ${error}`); - this.settingsErrorMessage = 'Fehler beim Laden der TV Settings. Bitte versuche es später erneut.'; - this.loadingSettings = false; - } - ); - - // Starte die Page Rotation einmal nach dem Initialen Laden - // Optional: Falls du separate Subjects für Page Rotation verwendest - // this.startPageRotation(); + // Automatisches Aktualisieren der TV Text Settings alle 60 Sekunden + interval(TVTEXTSETTINGS_INTERVAL_MS) + .pipe( + switchMap(() => this.loadAvisoTvTextSettings()), + takeUntil(this.destroy$) + ) + .subscribe(); } + + private startProgressBar(seitenwechselInSek: number): void { + // Falls eine vorherige Subscription existiert, beenden wir sie + if (this.progressBarSubscription) { + this.progressBarSubscription.unsubscribe(); + } + + // Setzen des Fortschrittswerts auf 0 + this.progressBarValue = 0; + + const progressBarIntervalMs = 100; // Aktualisierung alle 100ms + const totalSteps = Math.floor((seitenwechselInSek * 1000) / progressBarIntervalMs); + + this.progressBarSubscription = interval(progressBarIntervalMs) + .pipe( + take(totalSteps), + map((step) => (step + 1) / totalSteps), + takeUntil(this.destroy$) + ) + .subscribe((progress) => { + this.progressBarValue = progress; + this.cdr.detectChanges(); + }); + } + + getOverallIndex(i: number): number { + let count = 0; + for (let j = 0; j < this.currentPageIndex; j++) { + count += this.pages[j].length; + } + return count + i + 1; + } + + + private preprocessHtmlSettings() { + this.settingDisplayData = this.avisoTvTextSettings.map((setting, index) => { + const nonEmptyFixeZeilen = []; + + // Verarbeiten von fixeZeile1 + if (setting.fixeZeile1) { + const { sanitizedHtml, textAlign } = this.processHtml(setting.fixeZeile1, index); + nonEmptyFixeZeilen.push({ sanitizedHtml, textAlign }); + } + + // Verarbeiten von fixeZeile2 + if (setting.fixeZeile2) { + const { sanitizedHtml, textAlign } = this.processHtml(setting.fixeZeile2, index); + nonEmptyFixeZeilen.push({ sanitizedHtml, textAlign }); + } + + // Verarbeiten von fixeZeile3 + if (setting.fixeZeile3) { + const { sanitizedHtml, textAlign } = this.processHtml(setting.fixeZeile3, index); + nonEmptyFixeZeilen.push({ sanitizedHtml, textAlign }); + } + + return { + nonEmptyFixeZeilen, + currentDivIndex: 0 + }; + }); + } + + private processHtml(html: string, index: number): { sanitizedHtml: SafeHtml; textAlign: string } { + // Vermeide das Modifizieren des HTML-Inhalts, um die Formatierung zu erhalten + const sanitizedHtml = this.sanitizer.bypassSecurityTrustHtml(html); + + // Extrahiere text-align aus dem ersten Div + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const firstDiv = doc.querySelector('div'); + let textAlign = 'left'; + if (firstDiv) { + textAlign = firstDiv.style.textAlign || 'left'; + } + return { sanitizedHtml, textAlign }; + } ngOnDestroy() { - // Beenden aller Subscriptions, um Speicherlecks zu vermeiden this.destroy$.next(); this.destroy$.complete(); - // Beende auch die Page Rotation Subscription, falls du separate Subjects verwendest - // this.pageRotation$.next(); - // this.pageRotation$.complete(); - } + if (this.toggleDivInterval) { + clearInterval(this.toggleDivInterval); + } - /** - * Lädt die Ankünfte (Arrivals) von der API. - */ - loadArrivals() { - this.loadingArrivals = true; - this.avisoService - .getArrivals(this.standort, this.seiten, this.onlyOK) - .pipe(takeUntil(this.destroy$)) - .subscribe( - (data: AvisoArrivalsResponse) => { - console.log('Received arrivals data:', data); - this.arrivals = data.avisos; - this.totalArrivals = data.totalCount; - this.loadingArrivals = false; - this.paginateArrivals(); - this.cdr.detectChanges(); - }, - (error) => { - console.error('Fehler beim Laden der Arrivals:', error); - this.presentToast(`Fehler beim Laden der Arrivals: ${error}`); - this.errorMessage = 'Fehler beim Laden der Arrivals. Bitte versuche es später erneut.'; - this.loadingArrivals = false; - } - ); - } + if (this.progressBarSubscription) { + this.progressBarSubscription.unsubscribe(); + } - /** - * Lädt die Aviso TV Einstellungen von der API. - */ - loadAvisoTvSettings() { - this.loadingSettings = true; - this.avisoService - .getAvisoTvSettings(this.standort) - .pipe(takeUntil(this.destroy$)) - .subscribe( - (data: AvisoTvSettingsDto[]) => { // Typisierung als Array - console.log('Received TV settings data:', data); // Zum Debuggen - this.avisoTvSettings = data; - this.totalSettings = data.length; // Setze totalSettings auf die Länge des Arrays - this.loadingSettings = false; - this.cdr.detectChanges(); // Manuelle Change Detection - showFirstDiv: true // Initialisiere alle auf true - - // Debugging: Überprüfe jede Einstellung - this.avisoTvSettings.forEach(setting => { - console.log(`Setting ID: ${setting.tvTextBezeichnungID}, isActive: ${setting.isActive}, position: ${setting.position}, fixeZeile1: ${setting.fixeZeile1}`); - }); - }, - (error) => { - console.error('Fehler beim Laden der TV Settings:', error); - this.presentToast(`Fehler beim Laden der TV Settings: ${error}`); - this.settingsErrorMessage = 'Fehler beim Laden der TV Settings. Bitte versuche es später erneut.'; - this.loadingSettings = false; - } - ); - } - - startDivToggle() { - this.toggleDivInterval = setInterval(() => { - this.showFirstDiv = !this.showFirstDiv; - this.cdr.detectChanges(); // Manuelle Change Detection, falls nötig - }, 5000); // 5000 Millisekunden = 5 Sekunden - } - /** - * Lädt Arrivals und TV-Einstellungen basierend auf aktuellen Filtern. - */ - applyFilters() { - this.loadArrivals(); - this.loadAvisoTvSettings(); - } - - /** - * Zeigt eine Toast-Nachricht an. - * @param message Die Nachricht, die angezeigt werden soll. - */ - async presentToast(message: string) { - const toast = await this.toastController.create({ - message: message, - duration: 3000, - position: 'bottom', - }); - toast.present(); - } - - // Methoden zur Bestimmung von Statusklassen und -farben - getStatusClass(status: number): string { - switch (status) { - case 0: - return 'statusLKW_Erfasst'; - case 1: - return 'statusLKW_Freigegeben'; - case 2: - return 'statusLKW_LKW_neu'; - case 3: - return 'statusLKW_Ankunft'; - case 4: - return 'statusLKW_Vorbereitet'; - case 5: - return 'statusLKW_Vorgeschrieben'; - default: - return 'status-default'; + if (this.dateSubscription) { + this.dateSubscription.unsubscribe(); } } + + private setCSSVariables(): void { + if (this.avisoTvSettings && this.avisoTvSettings.length > 0) { + const settings = this.avisoTvSettings[0]; + const newCssVariables: CssVariables = { + tileWidthPercent: `${settings.kachelWidthInPercent}vw`, + tileHeightVh: `${settings.kachelHeightInPercent}vh`, + tileTitleFontSize: `${settings.kachelFontSizeLkwNummer}vh`, + tileFontSizeDateTime: `${settings.kachelFontSizeDateTime}vh`, + tileSeitenwechselInSek: `${settings.seitenwechselInSek}s`, + textBalkenHeightVh: `${settings.textBalkenHeightInPercent}vh`, + }; + + if (this.currentCssVariables && this.hasCssVariablesChanged(newCssVariables)) { + this.reloadPageOnce(); + return; + } + + Object.assign(this, newCssVariables); + this.currentCssVariables = newCssVariables; + } else { + console.warn('avisoTvSettings ist nicht verfügbar oder leer'); + } + } + + private hasCssVariablesChanged(newVars: CssVariables): boolean { + return Object.keys(newVars).some(key => (newVars as any)[key] !== (this.currentCssVariables as any)[key]); + } + + private async loadAvisoTvSettings(): Promise { + this.loadingTvSettings = true; + try { + const data: TvSettings[] = await lastValueFrom( + this.avisoService.getAvisoTvSettings(this.avisoTvTextSettings[0]?.standortID || this.standortID).pipe( + tap((data: TvSettings[]) => { + this.avisoTvSettings = data; + this.loadingTvSettings = false; + this.setCSSVariables(); + }), + catchError((error: any) => this.handleSettingsError(error, 'TV Settings')) + ) + ); + } catch { + // Fehler wird bereits in handleSettingsError behandelt + } + } + + private async loadAvisoTvTextSettings(): Promise { + this.loadingTextSettings = true; + try { + const data: AvisoTvTextSettingsDto[] = await lastValueFrom( + this.avisoService.getAvisoTvTextSettings(this.standort).pipe( + tap((data: AvisoTvTextSettingsDto[]) => { + this.avisoTvTextSettings = data; + this.totalTextSettings = data.length; + this.loadingTextSettings = false; + // Process the new HTML settings + this.preprocessHtmlSettings(); + }), + catchError((error: any) => this.handleSettingsError(error, 'TV Text Settings')) + ) + ); + } catch { + // Fehler wird bereits in handleSettingsError behandelt + } + } + + private handleSettingsError(error: any, context: string) { + console.error(`Fehler beim Laden der ${context}:`, error); + this.settingsErrorMessage = `Fehler beim Laden der ${context}. Bitte versuche es später erneut.`; + this.loadingTvSettings = false; + this.loadingTextSettings = false; + return throwError(error); + } + + private startDivToggle(): void { + const shouldToggle = this.settingDisplayData.some(data => data.nonEmptyFixeZeilen.length > 1); + + if (shouldToggle) { + this.toggleDivInterval = setInterval(() => { + this.settingDisplayData.forEach(data => { + if (data.nonEmptyFixeZeilen.length > 1) { + data.currentDivIndex = (data.currentDivIndex + 1) % data.nonEmptyFixeZeilen.length; + } + }); + this.cdr.detectChanges(); + }, TOGGLE_DIV_INTERVAL_MS); + } else { + // Initialize currentDivIndex to 0 + this.settingDisplayData.forEach(data => data.currentDivIndex = 0); + } + } + + // Status-Methoden mithilfe des STATUS_MAP + getStatusClass(status: number, lkwFertig: boolean): string { + if (lkwFertig) { + return STATUS_MAP['lkwfertig'].class; + } + return STATUS_MAP[status.toString()]?.class || STATUS_MAP['default'].class; + } + + getStatusIcon(status: number): string { - switch (status) { - case 0: - return 'checkmark-circle'; // Erfasst - case 1: - return 'checkmark-circle'; // Freigegeben - case 2: - return 'help-circle'; // LKW n.e. - case 3: - return 'time'; // Ankunft - case 4: - return 'construct'; // Vorbereitet - case 5: - return 'bookmarks'; // Vorgeschrieben - default: - return 'help-circle'; // Unbekannt - } + return STATUS_MAP[status.toString()]?.icon || STATUS_MAP['default'].icon; } getStatusColor(status: number): string { - switch (status) { - case 0: - return 'success'; // Erfasst - Grün - case 1: - return 'primary'; // Freigegeben - Blau - case 2: - return 'warning'; // LKW n.e. - Gelb/Orange - case 3: - return 'warning'; // Ankunft - Gelb/Orange - case 4: - return 'secondary'; // Vorbereitet - Grau - case 5: - return 'tertiary'; // Vorgeschrieben - Violett - default: - return 'medium'; // Unbekannt - Grau - } + return STATUS_MAP[status.toString()]?.color || STATUS_MAP['default'].color; } getStatusText(status: number): string { - switch (status) { - case 0: - return 'Erfasst'; - case 1: - return 'Freigegeben'; - case 2: - return 'LKW n.e.'; - case 3: - return 'Ankunft'; - case 4: - return 'Vorbereitet'; - case 5: - return 'Vorgeschrieben'; - default: - return 'Unbekannt'; - } + return STATUS_MAP[status.toString()]?.text || STATUS_MAP['default'].text; } /** @@ -386,99 +442,155 @@ export class AvisoPage implements OnInit, OnDestroy { * @returns Eine CSS-Klassen-String */ getPositionClass(position: string): string { - const pos = position.toLowerCase(); // Konvertiere zu Kleinbuchstaben - switch (pos) { - case 'top': - return 'position-top'; - case 'middle': - return 'position-middle'; - case 'left': - return 'position-left'; - case 'right': - return 'position-right'; - default: - return ''; + const pos = position.toLowerCase(); + const positionClasses: { [key: string]: string } = { + 'top': 'position-top', + 'middle': 'position-middle', + 'left': 'position-left', + 'right': 'position-right' + }; + return positionClasses[pos] || ''; + } + + private reloadPageOnce(): void { + if (!sessionStorage.getItem('pageReloaded')) { + sessionStorage.setItem('pageReloaded', 'true'); + window.location.reload(); + } else { + sessionStorage.removeItem('pageReloaded'); } } // Paginierung der Arrivals - paginateArrivals() { - // Berechnung der verfügbaren Breite und Höhe - const windowWidth = window.innerWidth; - const windowHeight = window.innerHeight; - // Definiere die Prozentsätze für Layout-Parameter - const titleHeightPercent = 5; // z.B. 10% der Fensterhöhe - const htmltextPercent = this.totalArrivals > 0 ? 30 : 0; - - const containerPaddingPercent = 0; // z.B. 5% der Fensterbreite - const gutterPercent = 1; - const tileWidthPercent = 15; // z.B. 20% der Fensterbreite - const tileHeightPercent = 23; // z.B. 25% der Fensterhöhe - - - - const containerPadding = (containerPaddingPercent / 100) * windowWidth; - const gutter = (gutterPercent / 100) * windowWidth; - const tileWidth = (tileWidthPercent / 100) * windowWidth; - const tileHeight = (tileHeightPercent / 100) * windowHeight; - - // Berechnung der verfügbaren Breite für die Kacheln - const containerPaddingTotal = containerPadding * 2; // Links und rechts - const columns = Math.floor((windowWidth - containerPaddingTotal + gutter) / (tileWidth + gutter)); - - // Berechnung der verfügbaren Höhe für die Kacheln - const titleHeight = (titleHeightPercent / 100) * windowHeight; - let htmltext = 0; - if (this.totalArrivals > 0) { - htmltext = (htmltextPercent / 100) * windowHeight; + private paginateArrivals(): void { + if (!this.avisoTvSettings || this.avisoTvSettings.length === 0) { + return; } - const availableHeightForTiles = windowHeight - htmltext - titleHeight - containerPadding * 2 - gutter * (columns > 1 ? columns : 0); + const settings = this.avisoTvSettings[0]; + const windowWidth = window.outerWidth; + const windowHeight = window.outerHeight; - // Berechnung der maximal möglichen Kacheln pro Spalte - const rows = Math.floor((availableHeightForTiles + gutter) / (tileHeight + gutter)); + const tileWidth = (settings.kachelWidthInPercent / 100) * windowWidth; + const tileHeight = (settings.kachelHeightInPercent / 100) * windowHeight; + const logo = (10 / 100) * windowHeight; + + // Definieren Sie separate horizontale und vertikale Gutter + const gutterHorizontal = (1 / 100) * windowHeight; + const gutterVertical = (1 / 100) * windowHeight; + + const containerPadding = (0 / 100) * windowHeight; + + const containerPaddingTotal = containerPadding * 2; + + // Berechnung der Spalten unter Verwendung von gutterHorizontal + const columns = Math.floor( + (windowWidth - containerPaddingTotal + gutterHorizontal) / (tileWidth + gutterHorizontal) + ) || 1; + + const htmltext = this.totalArrivals > 0 ? (settings.textBalkenHeightInPercent / 100) * windowHeight : 0; + + // Verfügbare Höhe für Kacheln unter Verwendung von gutterVertical + const availableHeightForTiles = + windowHeight - + logo - + htmltext - + containerPadding * 2 - + gutterVertical * (columns > 1 ? columns : 0); + + // Berechnung der Reihen unter Verwendung von gutterVertical + const rows = Math.floor( + (availableHeightForTiles + gutterVertical) / (tileHeight + gutterVertical) + ) || 1; - // Berechnung der Kacheln pro Seite this.tilesPerPage = columns * rows; - - // Sicherstellen, dass mindestens eine Kachel pro Seite angezeigt wird this.tilesPerPage = this.tilesPerPage > 0 ? this.tilesPerPage : 1; - console.log('Columns:', columns); - console.log('Rows:', rows); - console.log('Tiles per Page:', this.tilesPerPage); - - // Aufteilung der Arrivals in Seiten this.pages = []; for (let i = 0; i < this.arrivals.length; i += this.tilesPerPage) { this.pages.push(this.arrivals.slice(i, i + this.tilesPerPage)); } - // Reset der aktuellen Seite, wenn nötig if (this.currentPageIndex >= this.pages.length) { this.currentPageIndex = 0; } - - console.log('Total Pages:', this.pages.length); } - // Automatisches Wechseln der Seiten alle 10 Sekunden - startPageRotation() { - interval(10000) // 10000 Millisekunden = 10 Sekunden - .pipe(takeUntil(this.destroy$)) - .subscribe(() => { - if (this.pages.length > 0) { - this.currentPageIndex = (this.currentPageIndex + 1) % this.pages.length; - console.log('Wechsel zu Seite:', this.currentPageIndex + 1); - } - }); + + // Automatisches Wechseln der Seiten alle seitenwechselInSek Sekunden + private startPageRotation(): void { + if (!this.avisoTvSettings || this.avisoTvSettings.length === 0) { + console.warn('avisoTvSettings ist nicht verfügbar oder leer'); + return; + } + + const seitenwechselInSek = this.avisoTvSettings[0].seitenwechselInSek; + console.log('Seitenwechselintervall (Sek):', seitenwechselInSek); + + if (typeof seitenwechselInSek === 'number' && seitenwechselInSek > 0) { + const intervalMs = seitenwechselInSek * 1000; + console.log('Seitenwechselintervall (ms):', intervalMs); + + // Starten Sie den Fortschrittsbalken + this.startProgressBar(seitenwechselInSek); + + interval(intervalMs) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + if (this.pages.length > 0) { + this.currentPageIndex = (this.currentPageIndex + 1) % this.pages.length; + console.log('Wechsel zu Seite:', this.currentPageIndex + 1); + this.cdr.detectChanges(); + + // Fortschrittsbalken neu starten + this.startProgressBar(seitenwechselInSek); + } else { + console.warn('Keine Seiten verfügbar zum Wechseln'); + } + }); + } else { + console.warn('seitenwechselInSek ist ungültig:', seitenwechselInSek); + } } + /** * Event Listener für Fenstergrößenänderungen, um die Paginierung anzupassen */ - @HostListener('window:resize', ['$event']) - onResize(event: Event) { + @HostListener('window:resize') + onResize(): void { this.paginateArrivals(); } + + /** + * Vergleicht zwei Arrivals-Arrays, um festzustellen, ob sie gleich sind. + * @param a Erstes Arrivals-Array + * @param b Zweites Arrivals-Array + * @returns `true`, wenn beide Arrays gleich sind, ansonsten `false` + */ + private arrivalsAreEqual(a: AvisoDto[], b: AvisoDto[]): boolean { + if (a.length !== b.length) { + return false; + } + + for (let i = 0; i < a.length; i++) { + if ( + a[i].avisoID !== b[i].avisoID || + a[i].status !== b[i].status || + a[i].lkW_Nr !== b[i].lkW_Nr || + a[i].ankunft !== b[i].ankunft || + a[i].dauer !== b[i].dauer || + a[i].letzterMitarbeiter !== b[i].letzterMitarbeiter || + a[i].weiterleitungTextTV !== b[i].weiterleitungTextTV || + a[i].imEx !== b[i].imEx || + a[i].zollDigitalEingereicht !== b[i].zollDigitalEingereicht || + a[i].buero !== b[i].buero || + a[i].avisoTVHinweis !== b[i].avisoTVHinweis + ) { + return false; + } + } + + return true; + } } diff --git a/src/app/pages/aviso/aviso.resolver.ts b/src/app/pages/aviso/aviso.resolver.ts new file mode 100644 index 0000000..b7f1407 --- /dev/null +++ b/src/app/pages/aviso/aviso.resolver.ts @@ -0,0 +1,52 @@ +// src/app/resolvers/aviso.resolver.ts + +import { Injectable } from '@angular/core'; +import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { Observable, forkJoin, of } from 'rxjs'; +import { catchError } from 'rxjs/operators'; +import { AvisoService, AvisoArrivalsResponse } from '../../services/aviso.service'; +import { AvisoTvTextSettingsDto, TvSettings } from '../../services/aviso.dto'; + +interface AvisoResolvedData { + avisoTvTextSettings: AvisoTvTextSettingsDto[]; + avisoTvSettings: TvSettings[]; + arrivals: AvisoArrivalsResponse; +} + +@Injectable({ + providedIn: 'root', +}) +export class AvisoResolver implements Resolve { + constructor(private avisoService: AvisoService) { } + + resolve( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ): Observable { + const standort = route.paramMap.get('standort') || 'SUB'; + const seiten = route.queryParamMap.get('seiten') === 'true'; + const onlyOK = route.queryParamMap.get('onlyOK') === 'true'; + const standortID = parseInt(route.paramMap.get('standortID') || '1', 10); + + return forkJoin({ + avisoTvTextSettings: this.avisoService.getAvisoTvTextSettings(standort).pipe( + catchError(error => { + console.error('Fehler beim Laden der TV Text Settings', error); + return of([]); + }) + ), + avisoTvSettings: this.avisoService.getAvisoTvSettings(standortID).pipe( + catchError(error => { + console.error('Fehler beim Laden der TV Settings', error); + return of([]); + }) + ), + arrivals: this.avisoService.getArrivals(standort, seiten, onlyOK).pipe( + catchError(error => { + console.error('Fehler beim Laden der Arrivals', error); + return of({ avisos: [], totalCount: 0 }); + }) + ) + }); + } +} diff --git a/src/app/services/aviso-tv-settings.dto.ts b/src/app/services/aviso-tv-settings.dto.ts index cc1e17f..396f617 100644 --- a/src/app/services/aviso-tv-settings.dto.ts +++ b/src/app/services/aviso-tv-settings.dto.ts @@ -1,4 +1,4 @@ -export interface AvisoTvSettingsDto { +export interface AvisoTvTextSettingsDto { tvTextBezeichnungID: number; standortID: number; tvTextBezeichnung: string; diff --git a/src/app/services/aviso.dto.ts b/src/app/services/aviso.dto.ts index c3212b4..e47d84e 100644 --- a/src/app/services/aviso.dto.ts +++ b/src/app/services/aviso.dto.ts @@ -2,6 +2,7 @@ export interface AvisoDto { avisoID: number; status: number; lkW_Nr: string; + lkW_fertig: boolean; ankunft: string | null; dauer: string; letzterMitarbeiter: string; @@ -11,3 +12,40 @@ export interface AvisoDto { buero: string; avisoTVHinweis: string; } + +export interface TvSettings { + TvSettingID: number; + StandortID: number; + kachelWidthInPercent: number; + kachelHeightInPercent: number; + kachelFontSizeLkwNummer: number; + kachelFontSizeDateTime: number; + seitenwechselInSek: number; + textBalkenHeightInPercent: number; + logo: string; +} + +export interface AvisoTvTextSettingsDto { + tvTextBezeichnungID: number; + standortID: number; + tvTextBezeichnung: string; + standort: string; + position: string; + fixeZeile1?: string; + fixeZeile2?: string; + fixeZeile3?: string; + art?: string; + startDate?: string | null; + endDate?: string | null; + startTime?: string | null; + endTime?: string | null; + isRecurring: boolean; + isMonday: boolean; + isTuesday: boolean; + isWednesday: boolean; + isThursday: boolean; + isFriday: boolean; + isSaturday: boolean; + isSunday: boolean; + isActive: boolean; +} diff --git a/src/app/services/aviso.service.ts b/src/app/services/aviso.service.ts index f26e3d9..e4c8c5b 100644 --- a/src/app/services/aviso.service.ts +++ b/src/app/services/aviso.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; -import { AvisoDto } from './aviso.dto'; // Importiere das Interface -import { AvisoTvSettingsDto } from './aviso-tv-settings.dto'; // Importiere das neue Interface +import { AvisoDto, TvSettings } from './aviso.dto'; // Importiere das Interface +import { AvisoTvTextSettingsDto } from './aviso-tv-settings.dto'; // Importiere das neue Interface // Definiere die Response Interfaces export interface AvisoArrivalsResponse { @@ -45,10 +45,18 @@ export class AvisoService { /** * Holt die Aviso TV Einstellungen basierend auf dem übergebenen Standort. * @param standort Standort zur Filterung der Einstellungen. - * @returns Ein Observable von AvisoTvSettingsDto[]. + * @returns Ein Observable von AvisoTvTextSettingsDto[]. */ - getAvisoTvSettings(standort: string): Observable { - return this.http.get(`${this.baseUrl}/${standort}`) + getAvisoTvTextSettings(standort: string): Observable { + return this.http.get(`${this.baseUrl}/${standort}`) + .pipe( + tap(data => console.log('AvisoService received TV Text settings data:', data)), // Logge die empfangenen Daten + catchError(this.handleError) + ); + } + + getAvisoTvSettings(standortID: number): Observable { + return this.http.get(`${this.baseUrl}/AvisoTvSettings/Standort/${standortID}`) .pipe( tap(data => console.log('AvisoService received TV settings data:', data)), // Logge die empfangenen Daten catchError(this.handleError) diff --git a/src/assets/Logos/verag.png b/src/assets/Logos/verag.png new file mode 100644 index 0000000000000000000000000000000000000000..a87a0f021a8293163f16d4bd4dfabb08cdf3e2de GIT binary patch literal 33039 zcmeFZXH?T!)HfPLU_>I$V3dv~qJq*17&=m-bVvdM(tBtcKnxw30Y!?Drqs|0RjGmq zD1!zNh*FfU5Q-q8LBLS&i8J%u_rts14|lD**8Ol?;aTJL~w&>yZSr3oxI}fWH8xUl@%NQ>Vtq#XmhW}28Uj(%ymj5l#oI0Lq_dlcm!CR( z^;tXoq?fBY+zM$dYkXbD&C~002*C{#VuE!E@peJE!ZkHcs@+ln1bp2Bolf5J_3;Z( zxup*OMOOv9X8l?Qe)88x0=?DY|1fpc*zBZ^Kf&!JQd&XEMOIGkq>_@foFY<5N$$c) zd09Dm8Cme3l9ZgH3Q|c$Uisv|KX9-c!PQ;GLRbIay}&zlxMyJCbrl(z;NW2CU? ze6JfD{|{lmfPV!AL?&~~>AH-Zw5*J;FY8>tJ{=Hf;r4&C@xT3a0G4>&O~%44z(0uK z0`9{d@!w(~yZ`e>tPcWiR7?q8;8L7?bp2g|eBJy4ujs17!C$0Zyjn1xckQMB<60FRf}2yIKLP9S@AIDqn0fjK`UiOW zUq7jXQ960f%FEByKR7^wbv^%_R@aT-b<@pNpWyF%@|S#7y#6=)Uqb09C@5c&Q$*?M zv(Bx3QSPF?66&I?f}*zKC6qq=-?gs)Ph=(on2}*A$N!=%|NRPRAnWD-s6Ke}KgQ$c z2P}pFOhXdaa0&tm*Sw;ujlDJYWs>Wmo!cX>Z!)J2D$$}t52RgAW`)D$v@#_c+1mjxa7^DhUEj2@f8P->mJn8V&i}!Q?)jB)!rk|1gxqn z1oWad_aX+?A`h@Vc(niMkitUS!tR+1wv89qe%UklA9hq1_TOs=&VzR7zc*!SF(&`L z)|wLtJn-Ke$pJFof3MAmoH_r!j_OfZJow)mY_9g%|6Y?BJSzXQU)uk_#s7B%rA(Rj z@A%VhzL^y@AP_B)N>OXgT65RuuN+M8sk#42&l{-etQ2@8Z0*u4R!1S>aAOLFh)t=@ z&0xKBHfnIx#Rg_;dBE*Up4P5vQEwST9Keb zUr5|BPlG_tFOQN3!5PDqBR&|<`Kr@?#x7an%=q{aR{A&1{u)>oy(q~rL3VUJy~waY zV(snQHI1P>(a+Mi<-{Cr1k>OCXqr8@7jn95^0Pl0M?WF@RrnOd<$EO+hF{Fg)4z$| z89l-ir{+`ZLKmR$`}A~g%Dy*;!sM&I-)NcrZU0LWmPc*(A|pmWqDmg(_+gz;IQWBF zEa`tfuUlP#aU5_`V;q}3hEf$E9DtOyc%9$5XvteTb1{Az`Cxx59ou^Hd)BMu*!(C` zHn9IO4Mrw44#&oR-rsFZyw=-wfjVd*_vK(vLxk&q{+=YS=3Li-4Ba+Tympye`2k35 z_n&UPA)D(1ED4|@Zjr{udz4>^i7~4r%bpG++6}r|JoMA^Hl|fz=FcGCr5eRKAqr7qa=hK`PoW6RZgCn%H=^d)XB0A z{BgA^Eq*rTrp>2?jFK78VlkeFVdih?l&HrZzy@2Wg->l$*@aw8iF8;`vzMes39wNpw?=jRSp=LEglZ_*}RfEtb7&4#q zYIA9`>;T?rn0gPfFU%~Zsz^p#Fx*Q`^)){H)F@c+0Uoi%%~>pBfX_niZyoQ7BIgP| z?C^vum12aB&4xhulXqsOL@6KcXD7$(W9YTiH`GA{ANF8Fqm3)r0l!O?!$%qP*4Tc5 zF_W=smmew*wyFpaZoj>%K=7OmjXRIvLYI_rK=@Pr`9tuduz+gAu7jwRT|>GTo_sr3 zn5m4c57eQv;~@}oCqqY$_{~dOGO6(yl3-6~QZ#uL*k;!n>6d>-+zORU+F~mgkF=k6 z`YMFjPl?}7_O9S~B_;M~H~-aw<*O1|R40VB#NR=W5Jm7LCOEcW_U505Vh&t;RkXmkEjyFI4-e4p;KXpl*2Fuc& z5@5Q~6G>Mj*&s`0GBGAV83F=!zS*y5JQxt!=R%sDVqW%4pBs^_Z!Tt}XpJY-=R^=aych!u3wqYk;Z*1{F z%v5);c}EiHq8a34wmD_*@&rmL`Bk4uZY;4R=jO(rkO8(=s;gvq2;0+0;jhn+WN3AT zA+;P@Wf{9zpP~tj2u;~EG3+cMf?#wSVulD83beV|m907xr(k#6V#6nFjcfO&8G%P% z71&id$fhNoZRkI#iKI1nR{256Q<_8W0ay0tpz^BaY$5X!l&Ah1X;kT@sGeKl^d;A4 zfJHvO?VasB(dI$7bK-f%8rskwZ}F55f9rb>;(%m1ssvFN2;sBayhZA5o^8oun$ zu$sQ-M;6=G;j6}7C%dMWq7265310L+=?&)~|t>U?Xw_%mb zuMrg$?2xkZyRchAjHR6{yLbzeYAVv@r_aOqS!vI&By6iAi0VO)wdsW0v}(qEFG0vNU#h1`sm`#7qyzpK1NaqXmmpMiEsTPv&(;k67p! zp3to}XNN$LYXw+g#F{)#>;2sJ^~1wvSDuwPFZ0j@o+-Dr-D;2|H{MJNJBylQgOriW zeYm#F=qee7sVGu6o*dU`*oF7X41N5)8RRKJ^*8AXq`I*7Lv}%z=@fifW$Ts6CpNMX z=wfHSH}2(8W8;nAnLZy5LCR9gc~qnlvp*72_g*sA0iomK%3EV7WkQM|1X9-Aq~CVb z8Uv%DRFVy6p`{KY#>XcEifo2^UE_o!LJ zkV1JW#yWMXJSuvllKR@>dkY&x%fYzv!}wYnwOHCvLLp`L;9|8^@eIe2%xxFC5ily* zs9ZrNLDIhU;46=kfnu9(9WU<+HUsPPj0UP-c~t&=)5wSUoDlN#8Ov|J3_S}J)2pM2 z5-t5GQ~0&ztJ~D)z=RyN=D5T(#2!_0^Vj*p{mpf%=%IeAX6*GyaYi%NhsYNDdWV_| zKDb4Rk1@$8ovCyf>y0s4E{~SJC|{cBSaGvi7bZ znGzN_%t2b99EdfEu=_qs3c|8MeykmSAejlXo@dmjz0Vb^;8gM1b*-}z2!Qia*dS4- zuHT|3;-kz%L+Hc!vf^RH*)q1w9yCFe%*k&fgr1^+^Ei~NECvSAU8GGVh-*n_-z*l5 zQuqrBoAdNE3V`{>Qb$Q=DQx;~ByRi)l=PayW?AbmnN)oscHA2gTEPZcjJpeq7-ux5 z7E7DkR)~)WFJ`|ko$>Ou>CFpBE@v?zk0dw|4xUI|e@R^%HuFw_1A2QVqIm`B9G*M)KCDuQrX&EAlhm zUNf~~-|3Ph9e!Z^tz+7U4|NF&(W)`7{5h^H!-(n~vJ^FXBK`3Dm^y=itPg7C210dy zt=EWSum6!rJ%Z=wKa7J!%Bj~@2u8u^T&G5GfCWRGSYQj1kkpGcXuG?iin!XQF?WA$G z#Gr3#fWZDA#B@O$o8ymw>Od~bB(<7BWeb{g8_ha9x@~jLltrIPbI`dcBYy>^#piF* z)xihR8fd#hY0=a(38omJPQpyzcX%3I8F`%X{VjDEXiii+^>u0JTd}<?|0zO`rUmTaRJ^_I|Zr!=Pu!oOo zS5O#rm!+B*D2>U6jd+E9VXqIsN+vaPLKa(EcvL2rh05PCen0Sx68&H#@r{L!?P~G_ z8Jyv#>e({L-n1pa}r#A7vPuiH-a`ZOx=%u>Qko!c)O0^NU6t5G|P@FWTpP z`eIq_MnbC=wEp`Cy4RpQxK~w4F zw}Jk}6wNLObqZ{$&t zt7NEW=;8Sd(7H+(c6NLsRcHv3@HUC$3(|%_j7Xegx}N*fz+yFVYrdWyd(ly2sBa%g z!hoZzU;h*91B-Q9TcQpQ$)++&vYPRZTD#t^zDUUIHv$X-nZ&;L)ttVwliX|7(@H&! zq{)iIUO#y|CW8>O00dHRlDYZiD@vSGlscDbRKDSksP3$f)8(9w*&CK@5Snq2#)wZ# zua$xQ?^_{A+P4zw>=13_4b=j8M=h^yO4O;fbgCtuA77zsV!_TnKiUlulZ^;1a^5}# z(XtvM(muyiyQG~Y>fjX=r^UIn!Mb7E$iBWtAy6Csw4M8XDZ?E{!z0x((4rYnQFO5U z|F!4C--FduG3cA4G|>m zu^9)?9kqww43H2Jh8pYK7Mt3o-tZGo6N>4lGLXZv^^GQ7HsLng`t&T8+~t^PnC!vE zJHiys^tB1<6y6c?B`ZEL2$JJCdkR8B@^O6(e<(!!Q0(LVocb2eZ*#VEhH}9ss~S{c z{B@IX%uq*^s+>FyTKRE^dJt(AuwQV19FvQ5${_NwuC4QA80mW!b^BV6#Z+#krI@}d z+P*lpQ3J-z2Ri5Y#?ml%!u|2eQPfpvf@3K)8c%+82EBI$s>M|WBNhP1;m4oqwbWWQ zuaQw#(>2%*{UlAq#Q3 z(LRq*QPLg~K%>k%X}7FhgOT;?FQ`GRlivt!vHqgK(z)et)O0+5{PNC0uxT}QbqG|PB2Ua+6_B!yM~q^Bp7WTcn&ZjtjeZBo zgSnu@A`6{4@?ksRCS56`p{l!^GfP`pN*)bNOzJx4X*LMCj^|I=%OxxYE4*&x8jdtW zgi;`j530_7k@qhLqB0Sx*De(&@!5@795HF!<*5x*%g@5E%pqj8+_VPkh*p2@89n%qErR zx-j-x0a6SwG$?7GmCpqJG8V3uBzr4GgrSWJ517mMBK};StQ5HOJ2_y>7c$9*G5? zp`WO&-dG)QNX{UdL?q(A)xF4maX;^AYBg!K!ue$O)Xy{bN8Hh(mMli}B2M84sjE2c z#5t}`A49*d>!haAndtNS>B^t;ghw4<*bfRgnlka|=d&bG_!@S_JW?)pE|cIzsTUNq z^8^RYEWeYoc+z}f|6!b8?lGc)WbB%idBA&LU{~b9Gth6_H*e76snXH`wue*dK5)G- zy!P6nIzH?gl;?gGW;gGh%Pj#F4*qLQz0)x!L%6A<5qAVLw{FaCFO9>eEzpMBdN=P~ z19Itk?Cd}B{%62PF3PRGnI5VX{BJ&Nh^p@-3^}|^_+%iZlFF{iXT^} zzmxWFJR0wD=-79m3ze6xFEHT9RCxuM?X;sA{ZuLN^^1?P5UOSQ>5RcKZ@rNupX~TV zo$+F4@+U8nF?PtDNg-y%P%3t`KIkG9gRV&?>Gf<*?5Wl!8;nJ$siUFTRPEBQ-JM2E zr!Qdx>b%NQxGQsNsqs}ds1r&N(!6u@_l6t(JNv#O$i(q2NnmWgG_D-!yJ1qbO@@;r zx2ej5)BWE|?DwRIV?_zQa0lSNWseXY$D+K#Cr4-U=4whEjxrbM9G^`-T;doSvhV(A za-KQ$mciRI7sum}D!e;rF!e{RKmA@g*_rX}g+6|OD=F|Kn#(GcO9l0 zBK$XYUp)O80D}Hgr&}t%vAx#}G-muGkwX>{lHs|61}J+D2t<*TTzAV#GWTP|HToAS zJ6@np3a14%zLQ-No1SF*X*KdVQ>LLlh)^q@utcooR++D$-1@L}C2=5F$0X*{3#wk? zDlbE>Xd2V~@M6mm)>c0`x6vfiXG6u=4<`aCg<&Z-bb$LHPA43In7xCN3gO9#VV6Z* zX1aZs&iMz1mI!9{$ZYI+{+X92((Irv(jU$La=#Bc36(Q z(~5+lr_85@4n2F51sno7CDyHM?oTR0I@s1UQMfGDW>k%F6Iq|;FaO~yC#1u3QV?~T zQA^Dml9ODfL}fnu1EmKwRu_IWOwZfZAe6+9WlsOR7*qcEt=AXE$N<7Zj{d_gh$k^D znVF3hb_tGC(RVWjAt*=jLX1iK<*v~W^%?#x536gETmSMd7aZ%!+t;)L#^my&nOcWX zE-z3N$|=k0gpZf!Q#R4n{@({q=KAh~CrlJ}Cx?r>@?zLG9yf$us zHL*wW*A7OnQ~Qi9S*q=jSsfkzOcig43~0L12coprB!yC79WQAlP(AD)UB**3>pK$b+$X z@_^DEBV1>h@IxE1PuhHrJnNMa)@N;S6}rbM`Kg0Kd7FUu zD`!DNF}6{y*;wR_VWUmc_S)B6bI@f-472i;nmZBdL5bEFx8Nvb^i$stYw@A=SsqYp z)kyf|CQ58GsMK)UGNOE;3SU;FgD^GMv1P991hQnp!CNxZ6=`E@%YmIp5uA2Ws1-si zBz|fSjawR1QL9Z9Ttvi}%*_=pnV7LlP27*~14DuzRM{gclQ8SWcNq`-jhf}`q=>!g zC$K%%N1Ls{MFw`G;mQ^H;y_PY!g%H+@t%_HkN0W(d(T$&G^*Ot8Q*i)hPA|e3vQe5 zTHneO^ql2^)hk{B@#UG*k5XQ33`qdZi`6b288KsnAx{x91#MI`7>gJ8wjK1QN?$dK z#g&#tYm91f6w~*uF5uY3!$FP@c`kS3&h+NKQP*g9eY(GqSWT<}^?aYmE^P`);$;KiVO&i}m_rp;kISp#B9 zN-U8kp?O!XH%?2!Kgov@+cu^_CQ8tf`p{$`&BJcAZ7LrMNOFrKD z+>7inCM$NSJRY(t?e}&pp$Nj(0SL{UFK%adUzaW^#_2m(dVQfRzs6*ZY16Zb$OEMY zL_LtgO}0F8Pt6``9InEMIkFexQ8M$FLuTkr*GMGIC8^qbK1`edLfq^N`IYG>1R-nKle^4Ov!qC z7#HB3Elui)EN9v2rae_de-CqsZ!T6bm06y#iHF zQ}*%&R+-LJ1J2V_^v*;jXIZN;+#jQZA+Eh^mEIjAUj29#)|9rLSI!yY3We4GfWc(- zmCHfuw9TUeYZGDC3XWwSeN+>hYpq_Z1uYI!BNT(gYB6OTD_h_uNxn3HM&?Tsu(T?> zo%JSfN81B?41wK{0Yg_ac~6Stp`wRaq^a#eyq<8X8bbFLM-E0%JcBkj<~oRmtvC zVl7N8wBM_ttSJs7)+F0uP=e)2;a=SN3at0>We%yFqr}H??bH53R zhy#0YHW($63)P-Ksb!KP6OC|shR<|2lCM?9C-o{jRSo&C4s=7kK^w1j8>f}`T&))P z*)41OZH=8a=BMWOdLu!HGDCFeZ?I~qhklGS0NymRd^opNSp$k~U}J?mDpg<7sm-J& zN$c21du_Wo(etfHG{ z3#hzyN$K`SiMfJg4?j?0L9lt+rSo5nyBgtRy&fht<*_$VDz(kPAqiOjYK^;!z$)p| zFxpumy(F9tCb?+I$HL|~>Xo?HdH_O-k|k+753oqm2i6(eFtc8iGL4ddxMD(WmO3a9c7$ zVKlC@XiIY)%xe5j^=<$6z{9twNzI*V(;0@?{hDs$JII(&L(rPcPjx|fS2$rox7n#T zlU006F3ORDh8{n##cCz5WD2hi_sn24Sz&q4q zc~s5&b4j}(_3(gevWhAYD!BFOiz*>2ERPVEecbBj3K;#w_71%(=~fC_RANo<&Dc3} zdb-h%-D~&=cW~Khn-{3yuaO?cWM~7 zt4?P$dXEp~hx(+0CYPY>B6i9OkpY?)$Onj5Jr~b0cz_z`QyW;LEuhB6i5CD*yr-2>pv|wu zD_eQNKcEE-{Xxwqodt=cgRaK4!lum~s*GJ@Izwr_4@kwS5Wo(GqO?ojGVSSy!BGrc z8$lYaT-0cxBe8G>Z4b`ts|0LW5Jp2(pg|+Qc!xIiPVo72rmuRxo416fma-L;2IH+@ znS=iWZhc>O*J}sU|zQbq;FxJUyra1&Pm4N#hr+f10jwv)F*gPN`Dq9=>{tlPl854;LxNb z=yin1E-ESoq#zsJTIFG;7csZO=+}XV52asVQMj=UVug~*+rxDTZ%RHzro{QYg`M5+ z%%a{8@6N+jE7(-rAF=dE6gUoJjii+@P$ML7X4S;*XCyNMuDPNP!#7zTojcURRT)`H zf9qPvH@W$g>cq;=u4;xurjmCwZqXh$U~oWS9gpP}6>l=HHgXNoEF)eLt&l~N2pKCK ziT1m#4j{?HP6&Zus79yfbF{3aQe8pV&sXGUk5ks2S|&99VD+c~6~~ACfr8I$5grx! zsVT8Ly8~HO{okC-Y&0T1p86$~tkbV8lmq_aUaSqnzSjhvXvb_Sn=K@g5fdBSTq!jN zo#A6S&W^`GG#W(p`Yfpxbs3oHLuiGSe(qc+IW_>f}@mRcP4%2GQ2$zVO0tfRFotj|irFuP@NY3~GeV&j_*KM_a=bG&9Twx|oAErgwfpn4Uv|of9b46F#ORIJEawYZ8g_MJ z6qV1yvXVJ*8uNQ1(gB9Vqnn=K9w4%dJSsu2bXcx+?r&<*sau+BJ>MdOn+>i1>HABb z(|mdzFB!{Fwn7_5CR{7wY&p&A?^7OIQhhJ%`fpPMHF|Ad2~B~leiw@z6_D$XL$re- zSMt5qo8SW&c#7Ru3+aR%EK7N)v6PE8%q{8@5)S)r!~>pO7A<|X{X6k;0P|CRu@0kc zzRINc8l$8{uLNY%V9e%DqXaOxeQmvZx|fGadA@CLexbHMeN(I^{HO}_tt1`msLSxY z#2slEuS{iW`a5YeHaq$Auuh?bFI*IUMd9OC+iN9N%IHQc@THI#`~z;4r5WsmV@SHB z%@`b}Y}YT?vFUq>9+>g8ehuEv>Q>wBwFUQKecWs+zP~4yd8JU5NFs=Fv}i@S+_sx! z(k7U4=Vka?>KGEohadZ1?CJF9(k*G(LG(O-b5FP&SR#azq+QzIqnysLEcGuds0!ox zn@I}g`m!6M=c=61l)B9tBSFd@RXsoiIO;7>qNKa06;zJ5HOru-MP6=h&x66$Y}jv# zLD%W^RAf0I=t#TRQoYtDz)T0Z+Bw4CQO`}87u)`l_Vx16_hS|5Lu2ZseJs1(w>O|i z>nX`QX(BeWsW~0lJQ$~qVdk!O|1wIm_Rqy@B^Bh<9s1-L`;Wa=iQ1}O8-q51&v!I| zCA0@1f-As0562;8$KM=sXL`Sj#23zAUmsZ;Bn&Qt4S+jTBaSk&L6&?R<*qfAq4pR# zE-oddiCgSC3J5fX2{&-@FUCg3tC_M|5h!iw=jS8`P!eXn2Diz7ku#^o!(DRZ&fu6i z3Cj6#_aV&s{oIAQd92zx(2k*TYm(}aR@_`Dt`XI+kxNa)U*;9J9UdP=)FuN{h47yS z1Qq};282pMP`OutQ6>6mYH?qp8f(B))?k}rd@Atu3)0FkZAhJzGW2a<<57Iyl43>^ z{uvh!FyoGI#(u$lSGrVg*r;BpbxWMy3yZlC-+Q%3<^h`)*x(=<{T0V9(r8211+!k8 z+AF?9V%C-%-qR}%47gc0Va6Z0R6VWVkI}KkU~c@d66~k$tphG(bLB{`0i$-v%0L=CXE z*W&0G8A1Al{i-yi#f?8j0EMA+?b0a#K&XkeIF8*}=^ej268y-3c)B?nEDySAFR3&p zSWC7&UhLedfY=TJBb9S2Q}j>uH>XnLXW`(8nuabUS zrZFE`%wnOKNK!tlV-h$pl^6<949@1(U(sZFH%tnGRV6{9TI3gpYm6u?G^-MdDKHMh;=xzi9}=F0W!9?;+( zWTuO@D8RVI84u#08xLIWN<7(vI!mYk_f58}zV}6tc}H@wc4v1z2}FfY)I)gwIvHJQ zP}(w|Qd5Rm*C7I~<2f2p2NIi|`0xaPh!N7R54NHoV{uZ|dX`a7HN|RY%o%qd9-_V5 z24{Ao zL-&u5_7|U%;w_b8d1>rO4(T(=7rvTw0e46}mCJMlOdTSPL>rfi97<=dnq;b?n$bL? zt}wv4`p+|s?LI-0XLik%pxiJy}a)BNZK zpz~!(TyU}E9T!QZ;d!V@?777Zwd*r_^pXC%7gCbWhz+9p;w%l-w%yY*r_81qlo#jk!%z9vjGc8#G{z^9G64)4)qRFyQ&L@#yo z3ba{6?_07&&Z^Y*w%zDDATyc83O}J4<|>Nh%%+?GbphES;tv#pQ9x}WW#~&AH7c8n z+=GL}Dh-Jk4dd_zU5Z}`vgE0nIE`~e=c1gjgb5<4Oa13_MoI}+MQgnw+yTe_qiclp zY)Gr?f;pR(L))1TN{lT1YRn^Of5!bmpi&*QP$S94i_h!7Z?-3e$upi%hnMn`8Ji>b zh9C{sv2=BkE$+pA8wb$oF$+(su1E?q8Vd)QhDFh^PUZ}C4QDsD0H0=sKWvW+<_T=G zoZ3JdB^eK05^GgqC?d6_DoH+p&QE>+ySm{UXnG*Bet1Pln%Mj3tZ{FsqCAG>w7tfQ zXQ)3~jeizUb0zc9B}FqYq{$jnM~T^j{1+S(08?ptOF;HKTjWuSGl^id$rUBl1_dO{ zHs{Flx0+|}u5BTPp|W6(2-pDqjyBIu=@XzFDVJ~dd7xa^od`jZe?Zr@1gmA9NzF?3-`g#F0+1nF9^TSt;H!BjH z=qsdg+@&}=TT9R|q@h8UaS18wnp<`|u!j4U+g)xj!X9`VG`w@^7BGIB;!bs6L6m+~ z@gyJW3SdiC_DKA-toXiWzM&TveMM0gP&4mPbZ#kUtkIPIBAe?gMBeR>FIZh#g+;8f zm{HCoRx>8po>9>c941s{7{%1sB}ZY#lW!QnOh%K6K!n=| zH64zG!9-yAi_$SB!}W>;(7x|cwmBa7@%bVXlc#qT03j%9|i|`Y>;;8X{`UISO6b zd3e%O;3-2LM@Lz^I|Ek{n4w*|#KOG9TD+D!SIL_E0!&qULt`&BV$Q&Q~u0US2ChAh5hq2HggF_V!0dy%WUtODp`bU@*zT zG4Q0da%|p0()6ch>MuiO@6Vr!5sUfz`STG=7(%-Ywv)7U*(2%1p2N z6`a-B09=ga^LUCeaCAlVaeDz`tf1q`xmlxRfCzEbgvlOB22C5SV6b=Fs%~2m@R=f` z9>@0gmPkyzP}(CBn&p0VmJ_JwhdO@yHKXRK|4eEC%6ZFLGU?l2Pp%rW`l)U-iYOzh zTOP>nB&>qb3lOEb`i0rOAb(+P->|bg5(kEhw_(Df#7aR&GALO-Z0+7JN1~T67&e#soPSq`XCEc%0{F;1BmCMb9kD!OhVx=B{@u&UI~3Kxa7)nhqEcmDI8IyR z$wdwDZBQ<-TGYb>l(yfc#A{&o;eREs4-U&q{tIm)|23PEsL_dcs+ALn-5r1|Et^W7 zTj?TYP@@0Xld8=G`?LTDwmb1V3OF!dVHBaceg8ex1TV4DM!f>01u+O)4Z0&3+aK_t z(Ax2Xx(wq0a%f~l5`bT{7SG_xe<9(oLB2y)225Z?1Gfr+bxvQc%!w~Ocf8C6%P^Q{ z^?4zwvg)H>6pYTC7D|bJNlDb?D-(+`DXfbC*zIFD+TWlx3qGX9%O6-?8j3gz<~u=f z|9iqBqt`@==Z1&j6q9z0f=LqgXBG|#npH37t~4CfZ5fSQRYMjtv2@x zHvSifz(o1}*3Sm+S958k-jq+%PU$tfZlqEcpk$M^{2jn+&`z}{>}0UCK`tbkn{(_5 z<-;||BKJ-?pKRHu{?bMY%|tJ5*achVuY;E!(>|LD@8_I zNjuJ6jg*Ui&bXkUAq9jcrc*p-IpOB2U(B z%Mj+fie@&{E$G>2#QWN%;J~=ON@j$g=e2U1*X_4k?g)S~%Wx>VS{WPt#6XfW_h!7> zSr~6J{Y06ts{Bn=isQ%ZcR#POd7kAJ7%=W~gbZ(DV1dS>wVJIYTSQ)R7?M#%wOgW8 zh6zt<>TUS$JZ&@pLBE?k`0-~1eTKSM?wFbcCRh-iB@#6#F|6Qw0C9YfjbsJ1^ljNn z+3o1sP(mIXe+~=rQf;m`x2wyc{^Sb%u|7kG&q`m~v`eQ~)1d2yr;^6{R4Vl*$pf7W z8s-F%1G~aHDxW-njts0%SS)&z&Dz0NTH@YYnhzI*9#ZP5`_Z++co)KcT;; z02|Ns^o^mGlelo^p)#gj>FiN&$J)Oyyxzo`doeH0X92)#K@CH;zdEsU@e`Kk)4nt; z#x(MB*W2L8zL~i_mB^JKQ^~JPcV4}(4qxB5sN#YHhUogdy}de4$zD4_qL*#X*M>+c z$yH7Ml-r+ZLAVxD-Milg;OL9`5Hf;C1)AGTvQlb8MOkX_Gg;F(a#f9ft|)UAs}^nQ z&HSqvv=>Ggy9JrAbP`zhlWufvA6CcEn?fE*x)M7a)&JoX1CPXxC8uz?M^DLtXX*m~)GmF|APZxc zdR#CVrUTS%mN5n3aGv;DZahft!Q*v-Jv-E6NbE=_>HHAnN5^y4aBETJA4yerkubv@ z+}Zc=To~>pSQm5<=l9$XDobY;0qRhuB=i9;K-t<%fZQx#hAoM@z!S-aSHm~$*ut# zE#BEig!<;HbmAFsq#o!$Z^0* zZCK#sA$-{dTghm@?Y~&yDA2SP*7&1YAFAW)PB#Zf(Lzfl?btwvCJYh#WKJ>{7)RJZ z(|%M@SBD{iZ-@Uq|8*+6v;sg~O5Y1(rMfhj-v^9G3jFXez&g+Y#tv)I7P$k+*%AEd zoPcVMGtKMPHR0w4T=?K%*7%@0ey0Y2?Ve))8S)BBGb`8q z%bYy<{lHM>>oXicO1&i|0pNwQYKlcgU2mqpn zES76hq6Eie0kYemmpyUd&INN0V|r+%<{h8H_;<(k_q3sDYj>$$-6t>5P`u)$pis2? zc_#bQgZI3VN(^}%PkkKXSG{f*+{9g$zq31&i9aW$spuXmv?9#MtQRyGm#TH3ua%dj zs@?xKMODKAG;0+&a5ESXXq`p1_+9LaS1X8(zUfDA!UNT>wkIik;v=xMZtx|zNL3^U{ZYPLcrRcpfAPtD+ zV?#O&bbe+@y~7>nuECg5g8FU|t~eyN{}D+QSpA??x}DqtFW^+Ak39ITMFO@^rErx2 zy`#X&H#>ea!DxFKg8V01?jfuh4K4Qlof!-kPXcR$B&2+D0OMbvu1zVJ8*c<+T(k)a zK$D`7wzJFP4Lt?VNGa9?n_lOtW| zF$?ShiGuw?vwAfZInxfkTB@Y_1F?mGrkfdxS1-woT~>e99z3@c2aF;xM*A#7&Lh&I zQ|n<)yb$QY0+hTC+|*LXi%D(S9`3lP_K@dm!U#)u%uo)=VFu8Sb90!pQy_UI+w!P9 zxrp))KYERi`4~7>$ki9Da^c)cC@Hxj`p8csx&<<|QJ;Pg=rpN2rnX~|o^tj$P~gSm zF($67@e?JnHdz87Hvvm4Kl*~Y+pMRSYMv;T&ljea*e?WN?K*Q4g2Y0>s0R$kqPS(Y zsA@2TY&MUWG&yayiF7q! zu#@hzyX`ia@j~RGUMqM8tFIXa^ASQ_gBIr;V?GyLyge=?=8&pzs@4pe78pz~rttG{ zTz!im=~#TX0p*;PK%g@bv~+Z#4Noo*$Bu#Jq@3({7qaK-E2`?S)`=V#GZ7esD&rau z(*S@9fV>TZ&;;WK+F{O|t5b!PtEI;3*N!z?{F0O70+kJjScJqiq;*1_9{@87A2g{L zo9T#$O9IGA2O;9R78u0K7*!cGg7TR9m=}EoN#l`_ejj3}B z)}##JHr>%0Wt=B>PBG%CI^_^ZNEew8aYNW<9f-^RpH1_q_F!Hvb|^Aca52A7P%Jn# zz{2%`pyc+#f)M;f#evwdP7*t!33%Q`@feeY)0%c0z&u7C01wDgsoPk(RVoVej>^RbbEa{g2tOg2+(~$^1 zi+(vM_RmGJ#@}Yv%;-1aDcW58`b5J|EEoR+Xme(>JmU~Bt{E!)pT%mwId1zC4}&%# zd-!$mfTL?+b{`yFmjCXnY1iWvz5|Z8TETNKKtm$H0QWqz`xSb~Oro^N89WUJfuv{T zl}{V7&<})00KaIh+Exah%ss-Kr5=JTD#V!76YS{iLuLZ) z$*qR}h;Ek!u~~UY)xndKMSa4AE&9jy5-R@MSy-w7E#TYxE~^?HNXpW7bY2?VG5@!B z7s~E@I2Rvfq3d$x3pCL_@$wM)N&cCeZ2Nd4pVJ#jb7w3Se-E{_mN$<_npUv&n|@7V8FKsU>5%_ z|NHUgd(T)&;nYmxDo_naFaI~z4hnCmyo5bd1+!?KfQWJd_Y3+};X_t)6_*l?N29#= z25k>W`tQ>Vt%eG3K2#8ZoUmWUvftxl1@6HI(B836juonkJP26b2>&8zeBilF>RNoZ zFG1r|$RP;-3GSSSi4_4;B?2k|p8ax}EH~h$rtF~#_?hl!=DIub{Jp6a=gp$^h?wc* zW0$)YJCq>0HkvJjJQRp`>Q}&E0)IviO>DD4_=NzqDjh;Zp+NjS-cA3JUl-;wncW1| zH)|{mXGY7_M&k8borXnI>zds8L+=qbx<{23W><(p=Vl*d^Ui;{U+3L#_2BnxP^lKv z?vBPEcHp=;*Og*a>h;gH)n-2f2fLd^Y=6g-K$^v@-9H6%I2(!VJef^eGevFoFq^Uu zD;`SFHR2UOaPEcoA4kaypsv(`T-PL@uL2>tYdXZErx_+(B)}5>D}9td5I4c8e_WB> zWib(D1+?OB!NU_?vD&CobI_@z6vt$bb+2Wm1{M ze`y<~*VeLw=f05YZuHj?+8!S3KLB9|2OnOa%XQti?+KRx z4xW5pAJp0QC!u)#>hj&4jsE`4ZGpZcxsdwfeL)*0pn3%Kb($3Yzt2RVZ{;-gYHz%} zb+7cw`(Yl$jqv`oOt$O*$4ho;0Xgnv784gUO9l4exn<9Y{3?2j*bK7PhyJUev3=d;2Q^*iPdk2j!H7b=GmMt)=|#hmxF zY5fKX6@i?4dB-GSW@bW`5*2WyhA!eHfz~~^$h0P%6Q&#{awx1mwlG!CQf-I4jscyR@+TIh|Cz9w(G23G*Zic z>^jtR?xdZ3$h$P($fs~5;*$N;Ui(jF^NwQ(J^PV)M)6+Q6+CyAG(J)c5_M!grW|W@DTy z%-!4-)L#F}CM4LW?gb&@Z2zZw;{#iy!vXyRsPhX68w)`J;Z7I~X5)w%@|R-#U)6p4 zKa+j@e}`M{bWnGS5-N8&6^W%ec0xJEMCGvJcA8Vn`7oVCj)|ffbKIKKj$ynf9JR;7)v5r|wJ+(&WZZklXq- z7)VLzwh(C7MKa}*;Zq`+W#}%Ee|7WKeXGuqXLZ0 zo9Gu!$&r?;W~^RpN5Z4OJ80cO@B6Uvg;S7tvRNK&qxPGsc`-zh-64QmY0}{J{Rqz zjb`Ii?OG0&Esv_kp2HrYiY~tO1gqbV{c9}=g~vK2zvv+P(z8wFeQGLXJHa)?HP%(9 zg^DLIJ^g+MTw`svmR|N36;BRI<2nD+>hf_2*4Hey)J~U$1!Jm@f>LOt z+#8+<&mDi&tnP{qg9?V47#~fBkGV8g&jo?e5??~r%}fgJKg2Fr*@|`z>Nw}1VisOl zM?D+uYq6ee>6=uw-R2^5bUhPXGl*UL(#_5D)EyK~!3(GH}A_PQ=ID zJxH;I{~A%ysnE+HQ(L;}!~&Mn0pvZ>>wUD7koG;5nCX3;orm(yv;^7rj$fEQw6|YM zN1PgVq{Rq-$9U{so#+VM$S%N!$JgI<`$|0$b0dJ2WelY*tLtmMo9zPH#?xEbCu*nb zem!C3u?4C1{vB-)Br041B%BtTBx?~f%S`Y$7KokL56GFj#OfCy?1(WQHSqYgOA65y z+scepR7&3Rj^f$qQx}#E2Y)6gN8c9F)bcR0V|_#EeJG$DHO_vz_;s%*-Pq@_0V!$2 zTRNDu8!c=g1+_h%J3p3ukJKP$-gqi66{(03*ruS2iRHpAQ6^?fA}y>0e??RoZOXlG zY|}sB^kGY*hXHpeXbJ~4%1K~9iA@!QM2v+0sB~m-RPiG8rR-mq>X76YhW54!uJeZ zBOhhmC6Nlwo)~!TrkmySY8@oG;N5S1vea-ih)5j-GZY%b=GXeLZ3 z9azuDzT<|&vpq(JrG3{cmGvl5X*&sG6Esr;B@*{Wen7c0&y_pg`mKdy=q&jbiKUNZ zyuLqrKUl}#?^q}iMF(Fh?5V)X)h)hoWp2;OEpRvG_AXc{wSg}|}Y@7oMfa>_wvq>y6=c@V|PwA*O=Ne*b2ct&E~w;FsejMfC+w+H3BH z=)_YRNA8k7RVA7K$L~WanVOp7nQ!;P_?ZAR+x5d(Bzo%_W9(Qt$yY%H-b8 zKxX~xKqPz&7UWKLXr0$|9jH<7x$HRfuWcpK+xKS34-=&GWT;zkaIjXS#69uf!|}43 z8+N*lFK9eUAg&9c+k?7O>xM7+EM|2K8yKlAk44*2>V2-3Sp(&M6D#q~wTWOrvhb%T zEMHC5ZYVY)k-jL(M%h#dY~Ghyd7R{Zn8YV7o<~JJB9i)L~mf+8P8>8tG9f&bP6M-m8v7|5S=6QC^fCmv#R#i1H#e-6IhBckn@S36|$_fniPECxAD%}yQX2z(Hi zw)D_yno9o+jQH@*-Vm0;I&QV%&}>+c9Zat_tV}WB01v1UQ`dc&C;sfDwdT0RgthWT zCS0+NYfC=4K-plq1-mCyCHg#_;u!%~)pCe_N@!i=^2HHT!8)i31?+kpT%kqd>s%wg>b8 z@(YoFfxS^esx3a&ih;+saS<>~CfASLkNfz}DQLWvt_5FAD|3cndTXuqJxWc<3?GE~ z*mjKr*{ad*lBpZ>aYN87G|E4P({%3#?~y&Pe-H&7irX*aC$@K>G#NU zJ1W!_ssEzghKdF#1b}IV^7M{dW8W0zmK z#}`?4C%0CnG67UWX$mHd}q<(N2FFL}l$m09DElo*a-xr(rMNTz~c$m&TO*fm_ zUPOKCjy^DCbw2tg^dmA++)!s%xGhZR1|_u$we z*>)I$tF#bqqAgdk3CoJ%w!H1ZJhuV2c~jCdyX}@ak7l>H7Z?A%nN0WR}(q;YvP2-Y?zJC=ZCr6^pyx(jNIuVV1$_5oVfG2;JI72dS{k zA?I~Zx~gZ*NT->#Rz8t$vFWiydEns0CVT{KF8Ib!&2$%lP(iG#s$zxk?bQ{(bogMgR_>H@kjH9wY~yFXKkw| z_TDuiGfka#vpU7p@^jesO{PMfl!&Nqt*t0n*BcVsyw$C`9(g&tWceZP(s_ z?y?8aDu127C|h3_NRcpYB#I6Yk3aA~7%n9(E@4p>mT`YfTbz;Brwk%>Kpq_b>j`h#cHM66Vy`8lF=2>O2#=x49Ah)anB_qlX9Dr{Tk&q zf{A{cHG!Wd;S$A4+7vBKMg*84x?yxG=ZqvIP~1N~BV)MZtkCM+-&FF)cG>E!1ByGn zU{Mhp#NcDC64IxXT3H4i*C3aee}y1wRYtrZz9gm+HM;6Mg{$Y~9wiu;uX zFq*=$C$-<0b#8+gLAWhnr1C#sz9|eR#N$7PxXAPBgNzOZ&t4yZz1!?f_t8e{SwAh& z^e&F>vbKANl#4T{k_YL|5>E_&pc%QK(_{3fHJ|*|anetj_Es-TT!v+olisw61$$ir zZs~L9ECCEPBFVbgMkRj8IkL0#f*EzxL@EjHGv*QW`Ig8`(r=KOnx1Mfe^?-8haXRv z@Bok4#*M@@Jx{F}XM$PZgFMME&Dm2+T0W9!q|#HjfAC&IFFFzuOUW!3Yx`z=*a5u5 zL*HL`OkmX`pq^HP#{KCETBg2Teeeemtz*2l0;n3 z>uorc!9Zen3 z#+-$yqp9N^i%hGf9S4!;Nj}>$AT<^`gZ->WbAO`;nd&o)Qb~v3gy{)>>h1J=6t2LS z*}eMJ@qLEDh0RUnH%N|QN`y1e91O<%0O9-Wu_~$B;gkWG&p`f%@#rdLKI(Aav7E}# z+;0of66F$4oEr%>D(ncRAzT@oTdtZ{(Q6Nyt6y!0Qv)FYsX$ zWf-H%Da28qx#=wr4JY&gbyhfImsngc5rmN#Z=qMq#Z43=D) z)den{_cJB0*|^rW{5_%~xx!pc`eKVqs@K93o<049sLeJ+rjQcfeghm}LmC;%s*jg? zpChWIIl3?9R{E*6WWSj1O9$-Y;ju%Z;ad~9G~Of@JAF2b8T^y{64KuV+36b-S`9h} zWGZ&eEf1sW)on%)xR0$4XjA~!=?*`zdmZz<+*xzKRMLEu29He=QAE4IsRM(cwMe8UWWnb zbghyErRd>=OI3ghP^i+WZC$$IoP?s`r&&mXXI)%yd(9GM_Cw1y@i>A%G0Mfs30=IT z_cZzir6YKN{F8-NtlFvt|M$G%5Ay2g!pEjY&N5`c=(|IqTyI$_asf*B9N=RhbXN;|20!|Fit{+XT=6A zu$#Sux4Y~`b1q7xB46Y*gf@?E zOX`)w0yI%&%Ld!VvG+uUYf;qc`8*%?h_SJ;?f4M^*#GQW8KHFh8Sa|KqY;0P;Z$T6 zOB927ER8uGrF4lxFtmh4QTtkCHENIBY$xs=G{s^Nm`V6n;RCep!R3F0+iqebHH72y zf+8qIw_pW#XRNxUIIPE7-T=j+&^ObXIvOdvcPPdG9@HJ9s{$~<= zQM+jkT>^kotE$s;w>6uWR*ovE=nXy`zEi|tY-u~k+@zN}=T~;hkr(i@F1BX>S$^|f zQeGL{sTh&FX#2g~_Xbt!Ls$`@yu};*F9-A=Kk!9rO0y`^k0>G^Qt|LyMq%s=tXS6M zD~|KQgzy|&zp|{jYeymCh|;G!+gbeEum-h3zf9ibPvBC+VHmd>|C{8Ox8#Paq2$*B zDN-yNHaJ`&4YBqK6(VYxb8YAWhX969Vgvya0@Pz!>{W9lI&t!$0>qMAz98+`L z!I1u;UGq*0y}#@KR+g|iS;Sh)W$N9nkSKLzvB&e^ko5%Rg!Z7@vZ5vA78?OEv`vO6Cp4bl|~C|X@>5$({Z47$q; zQ1+lHgCiL~Oi3y9TT*MdRbyrjwcYDXtoeM0QtnEQVQ66_zo!K= z8|E-i%Vb5LO%GV~#%lr^cHERMrIAH69Z?InFHh~xIXLL}tBh(Au|*dce82mWN$qhi zmCa|twELwxhF;7g2htG@Kf=SoAc5e#X3(gi<+7Lkug0tI{xB%r7qXJM38I`NFt zb#16TJ{u$E?NZre1oFNE(@THjr&-3|$!pneeDaKkv&1rN)PYMrBz@b)Hjo>De9Ioa zr^kzGDz60wQw46z*pUzOm85-D*M@d&+u)a0XeS?_Lyz02tr;#X-Ia_u8HEu+(+devi!YLWn zV7RMfG2TkcBRuHkJj|2~|BM1&TC^bf%zrap-%2B*&7KF?G@d;a)hDJtu zJIU)^px1PAw95G?2z8Z^F!xwOIS-%Mxl8?zMP)g{f@h?%CoG8Nr*r-R-jLVK-Z57h zr}E&6;$#l)J&jffR?AH!IeSg_ap&FV!I)M}-crPeJ5r>FY zgJ%?fK8HCLFd)hnXk(2Kli^c=hhFfLtIBt6D<9-u;SPI(lRpa zdKtvk~kWAH(o1*Wj&2m4typ& z(qJ2^`PWfuVtr93p>RlX=VJ#qJm+-RYyuD>h`sV__Aa8RmJ@LIS&Bo2i2A}{SMcGq z2^Q;p89#M(LUekb^>p8+kN9uH2Yy0rYCL9e`dt7GcxoR%PJ3BU;nwufw3KtvV#Ry zHOFdsXCw)3@^ztKV<>nPle#A;ZMQ_^Eld0M8Ws3(!>LA5>6sx_J)#|n!RbMWKAk(A zvRtA(*P(w6^;}!-w|>-h4a~R!hDt2~C&U;VJtP-F_)*hZR-s{Wx2aD?%I3#X#pWn= z4HFl-lF?dx{E3!WYtcvkmjtA&{Yv6vLWWxbb0ceTG&(JsEu^?3>bivqPoG5G5BAN0 zV$_AsrdS4}Gc+=_#dWQni^oT7(%Cgl^tE9}vUN_wRp?vUUMU^h$JI)1ydihnkL@Rh zSr5(VUi7c(dS+*?nM9cA_}$qXX8s%7<{VQapg$mGiYO;%u6kyu=r9riF+q?~n4?kL zv+k4e7Jfeg|AdADT`1?fDISsIuJYx;DHbJhwjw|IpBWw(!$SG;j2FMQ7gk-O`@3vX zC!Qc*w*`@>W0@5+r_?##* zIBX259_J2_7jl3&dUIE8{9UNF9}G6PCsMUj=8sAkyd1#}^Wjjy;m_y+wJ5J#r@snL zEo2=i?)eH%Fy)^BN8HV@Tly3YwFby)M;ui!pd?;*uTH~Zsm|pGHAN>}3O>(SrHR^Y zD=E;i%E5PLvnL>eJT9$38IhbV)QnHir2T(`qH%#A!zFL% zXn5Y#PVqClu1aGL4z+&L5nnqrlm5qJkj4l{y?9sT0~kTydPYQ^oF6<3qOK0%nrMXD z#Y!79-}T%ckB>S>)(Tdc#L%Yx;V89db9Ch^u?0F%4w7u(J{H< zLea0ncJ325SCJpHiKUkNu_gMhkNh6O*B{dIoHnEN0psae%Jh~h2>Lp{wsxwnv6LpF zdr&Cdb4YL`4L*YMvRpy-8IgRiB*}Vi*qP0`)rOG)VhX)=^cx4a6?G}bM^S2H8I(Mp z9y$4o4}_<{7XhklzS-+>K&g?CsExJ>tWs!+H*%E?0brY<7aBhCmG$Rgwn6N=tzH9rK6Yt<&2CsLuar`_HAN*qoEo$7L_nL z{~n1oK!0E__k=2|7iXj(#ak+>05n!JKq_wI;(<^?-Bm_uv>iPj_i>N+KfRhBwu6u5 zKoMR29-EvWhx@#nza-#gd#=HMwNvquTk>Loov&64)($A`Y_)S{wbZXkaX{I#pC}PB zx&5M-bh9SHMHcZp6Lt41QcD*jr0txH;;NvhjGuQ`;N~qNtQqcoZ0!dHFa1#3#1W`c zug0aVQiK)=v?dsNwX+fg`sMzwp3~!P1g9O5j%_|f6N{65T{MD-)vj!mW5~U)`=I4p zl;)KmCJ!UwFXXFTdfN1GvK}2fYAG&C;bebH8u0<|P_8AyIT>p%UkhU47 z!2RX7wwHS)*KdgmTiIMDjwA?1_K77S>c3bd^A;rE=)SlaPPI|9KrTZk;HZoZ_u zr?d};TH6BFZBh+LvmcD3_St@97on6ULp4BZ`7q01ofb&1#=o!uCD5Qqkj*K6k8%fl z;kgtPK+12LDkG2EMFI*G(@MEw2HQ(qT+s8)cWXTTar4rT81XwhU0;KX4f(@AS>qegM~yvC-$5&dY43bw ze|x|wki9Vcd2~gM46bE(Y|(pSk32Guy~3^D4Tv?TDa&>nEu76d%L-BZxEOOtUtGa? zNDHEpNLwPJNCy$#2Ml9y z1B#OO&=U8h;^ZvQ@5)Rfh+Mpn;#07y^LcEc`1*RHHh|O9+fS%#JwDthEzHRXK;ux5*zBEwW9t4GXl5cxjKeZLN zC{Z#PiCX)M88|PK1MaPaZfg3E+iVTT_pPyxJqwg1XIVbwDkDkWWMPMXyn$)i z$sFCVdg4ONxwJ?TfPPXrivVabTk9&L6M_sQh}BGNSE%F_$Dx%qE%`b!~>9n?Zs=;8kU%&K+qg|O^@acTy+S_7iP1tcNI(c!WA18NhuH!RU ziW06B3U_aXUVu94HI zD@zm1O_amFZib=g;%yCEMxE9Ne*S8YtuV`*o;_M0RMM$XEmZKmIXzihe4F7nuiA4*Seujy$@ulwf&w;S(FPF|2 z?oIVrNxNZkuF22N#q-B`pLI6HW(PA}4=1m`%HXq+dbBs?0Gsj&$rT^&x zis9l$U0=7>|B7!DN7~m~5356-MOe1uF)|DLThP=#ozKeD)EBu$DXhIWF|G zH%fTqN{O~Oty3-TX^=8$RUEDj>Hl2Z>FOzoW`~kWZkoBA%xRNQ9^K}*_h{ObS7AM3 z1x2^>fGL?Tz86#-cLC^L&(ele7_P3Xx+Z5c=EM9bbgP;>jO>g`iZOHfUa-jf4yud3 zZ3(oM{qs#W@ufLjy83|BcP{C#)6fH}K^~y99&3$&c)$Li-N3i2T312RVaX??2T;l- z+Sb_#2t|-Cq9UrPcBJO?BM*QA5J9(CXqx48F6w{~exd=aG~>$4emltThuMn0)L_lzVbdH*zpsdsjSe!NR}o-Fkwg(| z{xGd~9Y6suC^OQ+vQ=AY-6a_H^k&3bL{rCz4hV8Kz_FH7cMZQ3Ol}LJ@8Zomrw5mn7x@3J#aHhvJl0~hRa+D3Cm0;l$$5)Ft^apfqxi!LRr3|}DX)=l z{zKZ%jetUlA@Wan)WXO7WPMK=>kI;1fACXUJqeSxNzw~oYWyhh`H0rkbPeTsbbW34@9@!c3{D0iyM)lmK z6*Uu5+$$g`Wvm>^Pw^pKtjmyEs~Yh)aEzRVCc`tnIgqofGFJo4JbOrXZLYU#WH+j8 z$qfrNLokjIy;U)2tb*kA%P0XNtKVz=P)0q zO*0#=d;GzIZp%%3O$?^=U%inbHKww|!Rrcj#XPK&>U-jGI||FZQChOvRZG&q)XLjd zUTy9Lzckn&UGyrcUj`)o1O)_}4wJDRk5bv2`_&sx%&?DfGnCu-b8cXn<97NY=XA3+ zKI!+yxF$8cQ%NraOeDWhvfI7^XG1AUjL+~0Jgr66MyB@kqf3~=0v4QZptFcw(JeI8 zEZ0)gCUmX1l!E3Zm?haOI1VwV;*oa#tqwKfUQos<U{o{6bBhhZfw^_g3GapH03pCqr1NN4En|B zf(+!GSVT?%plma~it$-zBWPK&?qd8o49W_-B5m0U`XL9FB5|*iKf)haqNJcmD1n5T+eH~+~?pet_9F%#LeL~5u@HlX%k7tEDYEQ4e z+?p_xz+ctN0pfwKxazLsT)sR)Nobe-zt4(*d=MjNKqp{orAI`<<|lQr#<+Iz9rXJM zen_UU0Yq;o_5A8*^A64tBN)f(tv=7_3yVY>_gf{{BlB9>s^=|Oi( zPAy)z3l3mad1P0~VMeVlcWR5PQLe&9WDMz*^^!B+tbK+X2KPM({Bj#wX96Tm=8;p$ z7EY>Izy+pHI7ub5`v3=vLa^r4x$Rs4B7TKx+GTpn?y+Yia+K3EkoH~2hdw2+DgL;R zi%LSxl4U(>J~TX!45+$7LeJ*33xSo|RaqWR-Y*gE882=r>{V9SVGg-?B&T-}Nb!JK zsbwZShPFiNrou1R7XM!&RTszo2U*=O_rCsN#sH#ZOpHKcnukZ2n5}xJz33Qmdd$B@ z{-*rT$lYW9q3J2F%_c=+cYT7A`a0kogQ20+k!n)+xZcM71<+#~9$?BGo`=K+9v66g zVBer~&XKY1cst%%uu@0==SY7QpILydlq%|Nb8RSef1|M}{aW2b=0hz{2NCgK0guv5hrzk-FMA5JJ~p7C@){h-S*I?>o( z2wP;X@`!3nZ)Ea1C}JL?jlqaRdDvd?xiyVTBlqTu_gl_5b2=VGncb93n?rp7K&lG1 z^KK)uNbJ(mNmozmAyV%J+{q$2z0TMd1*f7>z>yEPy{s}FW|c9t_g0#2qxJ=5@n1C@ zw%jeG9k3O;i`AmC+>V0YUs50cnopv`w+IVCh$IWsL`W*DL=ZUP@%vB@Ys9Ca=%l5r zo_93X%xj+Om%N=F`rKCQA0e2fQ5J*`MEYG#P8iJq^Md_2RCN2YM;^53we0-1KDH@M z_Xarzj*-cvNDePE6V}4&C;arxI_}r3#SY0ak|I9v7kiAX9)1H+zz*A7j|;`$gA8Co z{ST|rusxZ5!oaD5U0<&JUZ+V-4aV&vZw2MT?Ibw4c(g*yI~&0^SJ6GyL8FmgrYgdn zNsR`}k|l;@?_;5%ho&?GcTk4_X=NNmMO6yY@jzg6ZoiIHYm|!F!Z?YjIwG)(vByjzH!Z9{h=2##EP3*e1@d+$mA7AZ>h0GYod zrh$O$Bl>7UX*_0hPpMQC_fOQToGgQX)ksHRTxv$GV*Xz74ov-cIDN62+!Bc7C+X z=uqU!=Tq`(qCMhgM+1j)-+9=+711dAF$QcFH6XpC_b0t`{`V@)H$$*GU8mGctwgD6 z{Hk{}$R=j3sVCO%%iw(0eoI-;;=%tKEf@)4Q%b)Q1G!(PA%6aILcF#0A>89kUuy~F zR=#}ny$r2=jMrxB+92s&lG-=I2$Tt~1UP)o#a2JC1Dy!)~h|_gShn&k__eaYelmwz%y7)E#36<1dc#tK) zF|@_VkGvw-S_?`GqB2M6)O}(V>t5;7wCRoxQ`OT9fdd{F?yAyC zpN=#DeJ}zaLUhRm@`J+;>JRii1lG+4*G5(A#v6ZTR2#(vR(+HD^!^V}|0vea0O3w{ z&CKr~SQPU4IJ9I6!sgLW5D%p zU#EyO+P9E%7LAW89N_*xxb4>|Z=7>6sS236TF*T^_YvANw8<1Uz&Bk(1t1V1ZgbAj z&G)MdwFHwbCcbAkQu-A@5wB^7GFf_AbJFmJa%7{FZi)<8;MeJ=-*Js&ke^y-KrK2} z!lBjx>?eok0x{6aiH!S%^{6-#H7?HK`chNXUK_1o&tz!d5eLZ~_*1 z*0Q;^UlFs$B1PM_z;w=W$Z8?x%eqMKJ-N5)OwaV!E$sF@~1%@Q<y3@ggA+s2_<{cB`vrGkfZKv_m%b$Bh z^0y6ni1I@2F#dVgpZozl;077U1KcJ3eRe0QmGNd@QdfELF%`Mxx_&!^^~DrJhHMc<_$kR@Ll zDg~pi-x{l}p~DCQTTj=Wja%;t$yqD|+3r;g5{94lcL@%znN0Ruu3lbq=sYMxU_`&; zh$Sf=l^$+UeQ9=QwWlXG-r`nPs7OykW0N2QkpF=>5-#Ge7|(Ja>_X7JLX)pq@R* zO&NXl56PB$J%9;bEw>eeI%WM$@{{4hE>8{!2C_e+O7Q5?4z1e{< zhUu9X*oxGk-m$+lu_$h_UCv#yTbp`mmwYmYGUx${q3NIJtG6P-&^x4_4>Z!!IwA>~ zI7mRvI*;LSIB}HF|CZC0aSU&r{wnZFAgw1Bwfd|lR`DpL`F&mgap)-}1Ja?LV|28# zaZ`)KFkv~5SqsmwaN(MXEeqNPlE?S8@q8qU7ls*&LW^RYnv*w9nwFwz84Lt#;}lMJZy^4` zIicXjBxKsV^P%?}k>3#@@kV3P>Qfesf>w@hv%^(`C26n3PdotnflMCe)79w81UE_r zIYL2{)i{+dl?qps-kNxd7rC2kKEvURkJ~hjAOS0L@t<{Uip%?C(WE=KjibwljbG4&rFUXFwZonD8KkAAuL!@4f;vn4ROpWP38HawO2 zPfVQn&ieOiYZ&I*o3xVUa?WcHPc-t}tF;f@dL|W$ZH-Bj@-EvMziPIt^vwEh0_DdYEa`TaRF`1cI{ s{rTD4@1FDL2l@ZM{(v98d*3b}r~iVlC8_>8WwGn7uF37