(web): dashboard enhancements
This commit is contained in:
parent
d8a2930247
commit
44f627499a
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"plugins": {
|
||||
"@tailwindcss/postcss": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,8 +36,9 @@
|
|||
}
|
||||
],
|
||||
"styles": [
|
||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
||||
"src/m3-theme.scss",
|
||||
"node_modules/@ngxpert/hot-toast/src/styles/styles.css",
|
||||
"src/tailwind.css",
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
|
||||
import type { CodegenConfig } from '@graphql-codegen/cli';
|
||||
|
||||
const config: CodegenConfig = {
|
||||
overwrite: true,
|
||||
schema: "/Users/d3r0/dev/repositories/active/avicenna/service/data-hub/data-connector/src/core/database/schema.gql",
|
||||
documents: "./src/app/core/data-access/**/*.graphql",
|
||||
generates: {
|
||||
"./src/app/core/data-access/graphql/generated/generated.ts": {
|
||||
plugins: [
|
||||
"typescript",
|
||||
"typescript-operations",
|
||||
"typescript-apollo-angular"
|
||||
],
|
||||
config: {
|
||||
addExplicitOverride: true,
|
||||
withResultType: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,22 +6,23 @@
|
|||
"start": "ng serve",
|
||||
"build": "ng build -c production",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
"test": "ng test",
|
||||
"gql-generate": "graphql-codegen --config codegen.ts"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^19.2.6",
|
||||
"@angular/cdk": "^18.1.1",
|
||||
"@angular/cdk": "^19.2.9",
|
||||
"@angular/common": "^19.2.6",
|
||||
"@angular/compiler": "^19.2.6",
|
||||
"@angular/core": "^19.2.6",
|
||||
"@angular/forms": "^19.2.6",
|
||||
"@angular/material": "^18.1.1",
|
||||
"@angular/material-date-fns-adapter": "^18.1.1",
|
||||
"@angular/material": "^19.2.9",
|
||||
"@angular/material-date-fns-adapter": "^19.2.9",
|
||||
"@angular/platform-browser": "^19.2.6",
|
||||
"@angular/platform-browser-dynamic": "^19.2.6",
|
||||
"@angular/router": "^19.2.6",
|
||||
"@apollo/client": "^3.0.0",
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@azure/msal-angular": "^3.0.22",
|
||||
"@azure/msal-browser": "^3.19.1",
|
||||
"@grafana/faro-web-sdk": "^1.15.0",
|
||||
|
|
@ -33,16 +34,17 @@
|
|||
"@opentelemetry/auto-instrumentations-web": "^0.45.1",
|
||||
"@opentelemetry/context-zone": "^1.30.1",
|
||||
"@opentelemetry/core": "^2.0.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.57.2",
|
||||
"@opentelemetry/instrumentation": "^0.57.2",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.48.0",
|
||||
"@opentelemetry/instrumentation": "^0.48.0",
|
||||
"@opentelemetry/instrumentation-document-load": "^0.44.1",
|
||||
"@opentelemetry/propagator-b3": "^1.30.1",
|
||||
"@opentelemetry/resources": "^1.30.1",
|
||||
"@opentelemetry/sdk-trace-web": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"apollo-angular": "^7.0.2",
|
||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||
"apollo-angular": "^10.0.3",
|
||||
"date-fns": "^3.6.0",
|
||||
"echarts": "^5.5.1",
|
||||
"graphql": "^16",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-ws": "^5.16.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
|
|
@ -53,6 +55,12 @@
|
|||
"@angular/cli": "^19.2.7",
|
||||
"@angular/compiler-cli": "^19.2.6",
|
||||
"@angular/localize": "^19.2.6",
|
||||
"@graphql-codegen/cli": "^5.0.5",
|
||||
"@graphql-codegen/typescript": "^4.1.6",
|
||||
"@graphql-codegen/typescript-apollo-angular": "^4.0.0",
|
||||
"@graphql-codegen/typescript-operations": "^4.6.0",
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@tailwindcss/postcss": "^4.1.4",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"jasmine-core": "~5.1.0",
|
||||
|
|
@ -62,7 +70,7 @@
|
|||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"typescript": "~5.5.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ import { GlobalErrorHandler } from './core/telemetry/global-error-handler';
|
|||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideAppInitializer(() => {
|
||||
const initializerFn = faroInitializer();
|
||||
return initializerFn();
|
||||
}),
|
||||
// provideAppInitializer(() => {
|
||||
// const initializerFn = faroInitializer();
|
||||
// return initializerFn();
|
||||
// }),
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useClass: GlobalErrorHandler,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,18 @@ import { MsalGuard } from '@azure/msal-angular';
|
|||
import { roleGuard } from './core/components/ms-authentication/role.guard';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'home',
|
||||
loadComponent: () =>
|
||||
import('./pages/home/home.component').then(
|
||||
(m) => m.HomeComponent
|
||||
),
|
||||
canActivate: [MsalGuard, roleGuard],
|
||||
data: {
|
||||
title: 'Home',
|
||||
roles: ['manager', 'admin'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'control-center',
|
||||
loadChildren: () =>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<mat-sidenav-container class="h-full">
|
||||
<mat-sidenav
|
||||
#drawer
|
||||
class="w-52"
|
||||
class="w-24"
|
||||
fixedInViewport
|
||||
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
|
||||
[mode]="(isHandset$ | async) ? 'over' : 'side'"
|
||||
|
|
@ -9,27 +9,33 @@
|
|||
>
|
||||
<mat-toolbar color="primary">Navigation</mat-toolbar>
|
||||
<mat-nav-list>
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="/home"
|
||||
routerLinkActive="nav-link-active"
|
||||
>Home</a
|
||||
>
|
||||
<a
|
||||
mat-list-item
|
||||
routerLink="/control-center"
|
||||
routerLinkActive="!bg-yellow-400"
|
||||
routerLinkActive="nav-link-active"
|
||||
>Dashboard Leitstelle</a
|
||||
>
|
||||
<a
|
||||
*dksRole="['manager', 'admin']"
|
||||
mat-list-item
|
||||
routerLink="/managers"
|
||||
routerLinkActive="!bg-yellow-400"
|
||||
routerLinkActive="nav-link-active"
|
||||
>Dashboard Manager</a
|
||||
>
|
||||
<a
|
||||
*dksRole="['manager', 'admin']"
|
||||
mat-list-item
|
||||
routerLink="/accounting"
|
||||
routerLinkActive="!bg-yellow-400"
|
||||
routerLinkActive="nav-link-active"
|
||||
>Abrechnung</a
|
||||
>
|
||||
<a mat-list-item routerLink="/tickets" routerLinkActive="!bg-yellow-400"
|
||||
<a mat-list-item routerLink="/tickets" routerLinkActive="nav-link-active"
|
||||
>Ticket System</a
|
||||
>
|
||||
</mat-nav-list>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,16 @@
|
|||
:root {
|
||||
--mat-toolbar-container-background-color: #17253D;
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
:host {
|
||||
@include mat.sidenav-overrides((
|
||||
container-width: 16rem,
|
||||
));
|
||||
}
|
||||
|
||||
.nav-link-active {
|
||||
background-color: var(--mat-sys-primary);
|
||||
@include mat.list-overrides((
|
||||
list-item-label-text-color: var(--mat-sys-on-primary),
|
||||
list-item-hover-label-text-color: var(--mat-sys-on-primary),
|
||||
list-item-focus-label-text-color: var(--mat-sys-on-primary)
|
||||
));
|
||||
}
|
||||
|
|
@ -8,8 +8,9 @@ import { AuthService } from '../ms-authentication/auth.service';
|
|||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-avatar',
|
||||
template: `<div
|
||||
class="flex items-center justify-center rounded-full h-10 w-10 bg-gray-100 text-blue-500 cursor-pointer transition ease-in-out hover:bg-white hover:shadow-md hover:scale-105 duration-200 "
|
||||
class="flex items-center justify-center rounded-full h-10 w-10 cursor-pointer transition ease-in-out hover:shadow-md hover:scale-105 duration-200 "
|
||||
[matMenuTriggerFor]="menu"
|
||||
style="background-color: var(--mat-sys-primary-container); color: var(--mat-sys-on-primary-container)"
|
||||
>
|
||||
{{ userInitials$ | async }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
import { Component, computed, effect, input, signal } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { GaugeChart } from 'echarts/charts';
|
||||
import { ECharts } from 'echarts/core';
|
||||
import { UtilNgxEchartsModule } from '../../ngx-echarts/util-ngx-echarts.module';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gauge-chart',
|
||||
imports: [UtilNgxEchartsModule],
|
||||
template: ` <div
|
||||
dksEcharts
|
||||
[defaultHeight]="75"
|
||||
[options]="options()"
|
||||
[extentions]="extentions()"
|
||||
(chartInit)="setChartInstance($event)"
|
||||
></div>`,
|
||||
styles: ``,
|
||||
})
|
||||
export class GaugeChartComponent {
|
||||
private chart?: ECharts;
|
||||
|
||||
value = input.required<number>();
|
||||
options = computed<EChartsOption>(() => ({
|
||||
series: [
|
||||
{
|
||||
type: 'gauge',
|
||||
center: ['50%', '100%'],
|
||||
startAngle: 180,
|
||||
endAngle: -40,
|
||||
radius: '200%',
|
||||
min: 0,
|
||||
max: 100,
|
||||
progress: {
|
||||
show: true,
|
||||
width: 4,
|
||||
},
|
||||
pointer: {
|
||||
show: false,
|
||||
},
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
show: false,
|
||||
},
|
||||
detail: {
|
||||
valueAnimation: true,
|
||||
width: '100%',
|
||||
lineHeight: 20,
|
||||
borderRadius: 8,
|
||||
offsetCenter: [0, '-30%'],
|
||||
fontSize: 28,
|
||||
fontWeight: 'normal',
|
||||
formatter: '{value}%',
|
||||
color: 'inherit',
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: this.value(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
extentions = signal([GaugeChart]);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (!!this.chart) {
|
||||
this.chart.setOption({
|
||||
series: [
|
||||
{
|
||||
data: [
|
||||
{
|
||||
value: this.value(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setChartInstance(chart: ECharts) {
|
||||
this.chart = chart;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,16 +8,26 @@ export interface ConfirmDialogData {
|
|||
}
|
||||
|
||||
@Component({
|
||||
template: `<div class="flex flex-col items-center justify-start p-4">
|
||||
template: `<div class="flex flex-col items-center justify-start p-4">
|
||||
<div class="text-lg font-bold pb-2">{{ ref.data.title }}</div>
|
||||
<div class="pb-4 border-b border-gray-400">{{ ref.data.body }}</div>
|
||||
<div class="pt-4">
|
||||
<button mat-stroked-button class="mr-2" dialogClose>Abbrechen</button>
|
||||
<button mat-stroked-button color="warn" [dialogClose]="true">Bestätigen</button>
|
||||
<button mat-button class="mr-2" dialogClose>Abbrechen</button>
|
||||
<button mat-button class="warn-btn" [dialogClose]="true">Bestätigen</button>
|
||||
</div>
|
||||
</div>`,
|
||||
imports: [MatButtonModule, DialogCloseDirective],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
styles: `@use "@angular/material" as mat;
|
||||
|
||||
.warn-btn {
|
||||
@include mat.button-overrides(
|
||||
(
|
||||
text-label-text-color: var(--mat-sys-error)
|
||||
)
|
||||
);
|
||||
}
|
||||
`,
|
||||
imports: [MatButtonModule, DialogCloseDirective],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ConfirmDialogComponent {
|
||||
ref: DialogRef<ConfirmDialogData> = inject(DialogRef);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<mat-card appearance="raised">
|
||||
@if(title() !== undefined || subtitle() !== undefined) {
|
||||
<mat-card-header>
|
||||
<mat-card-title class="!text-base">{{ title() }}</mat-card-title>
|
||||
<mat-card-subtitle>{{ subtitle() }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
}
|
||||
<mat-card-content>
|
||||
<div class="w-full flex flex-col">
|
||||
@if (chartLabel() !== undefined && chartValue() !== undefined) {
|
||||
<div class="flex flex-col relative">
|
||||
<div
|
||||
class="text-small absolute right-1/4 z-50 cursor-help"
|
||||
[matTooltip]="chartLabel()"
|
||||
>
|
||||
ⓘ
|
||||
</div>
|
||||
<app-gauge-chart [value]="chartValue()!"></app-gauge-chart>
|
||||
</div>
|
||||
} @if (chartValue() !== undefined) {
|
||||
<mat-divider class="py-2"></mat-divider>
|
||||
}
|
||||
<ng-content select="app-info-card-content" class="flex flex-col">
|
||||
</ng-content>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
@if (hasMainContentContent() && hasFooterContent()) {
|
||||
<mat-divider class="py-2"></mat-divider>
|
||||
}
|
||||
</mat-card-content>
|
||||
@if (hasFooterContent()) {
|
||||
<mat-card-footer class="mb-2 flex justify-around">
|
||||
<ng-content select="app-info-card-footer"></ng-content>
|
||||
</mat-card-footer>
|
||||
}
|
||||
</mat-card>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { InfoCardComponent } from './info-card.component';
|
||||
|
||||
describe('InfoCardComponent', () => {
|
||||
let component: InfoCardComponent;
|
||||
let fixture: ComponentFixture<InfoCardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [InfoCardComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(InfoCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import {
|
||||
AfterContentInit,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ContentChildren,
|
||||
input,
|
||||
NgModule,
|
||||
QueryList,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { GaugeChartComponent } from '../charts/gauge-chart/gauge-chart.component';
|
||||
import { UtilNgxEchartsModule } from '../ngx-echarts/util-ngx-echarts.module';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
|
||||
@Component({
|
||||
standalone: false,
|
||||
selector: 'app-info-card-footer',
|
||||
template: `<div class="text-md font-bold">{{ value() }}</div>
|
||||
<div class="text-xs font-light text-center">{{ label() }}</div>`,
|
||||
host: { class: 'flex flex-col items-center' },
|
||||
})
|
||||
export class InfoCardFooterValueGroup {
|
||||
value = input.required();
|
||||
label = input();
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: false,
|
||||
selector: 'app-info-card-content',
|
||||
template: ` <div
|
||||
class="p-4 pb-0 text-sm font-light text-center"
|
||||
style="color: var(--mat-sys-on-surface-variant)"
|
||||
>
|
||||
{{ label() }}
|
||||
</div>
|
||||
<div
|
||||
class="p-4 pt-0 text-center text-lg font-bold"
|
||||
style="color: var(--mat-sys-primary)"
|
||||
>
|
||||
{{ value() }}
|
||||
</div>`,
|
||||
host: { class: 'flex flex-col' },
|
||||
})
|
||||
export class InfoCardContentValueGroup {
|
||||
value = input.required();
|
||||
label = input();
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: false,
|
||||
selector: 'app-info-card',
|
||||
templateUrl: './info-card.component.html',
|
||||
styleUrl: './info-card.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InfoCardComponent implements AfterContentInit {
|
||||
@ContentChildren(InfoCardFooterValueGroup)
|
||||
projectedFooterItems!: QueryList<InfoCardFooterValueGroup>;
|
||||
hasFooterContent = signal(false);
|
||||
|
||||
@ContentChildren(InfoCardContentValueGroup)
|
||||
projectedMainContentItems!: QueryList<InfoCardContentValueGroup>;
|
||||
hasMainContentContent = signal(false);
|
||||
|
||||
chartValue = input<number>();
|
||||
chartLabel = input<string>();
|
||||
title = input<string>();
|
||||
subtitle = input<string>();
|
||||
|
||||
constructor(private readonly cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngAfterContentInit() {
|
||||
this.hasFooterContent.set(this.projectedFooterItems.length > 0);
|
||||
this.hasMainContentContent.set(this.projectedMainContentItems.length > 0);
|
||||
this.cdr.detectChanges();
|
||||
this.projectedFooterItems.changes.subscribe(() => {
|
||||
this.hasFooterContent.set(this.projectedFooterItems.length > 0);
|
||||
});
|
||||
this.projectedMainContentItems.changes.subscribe(() => {
|
||||
this.hasMainContentContent.set(this.projectedMainContentItems.length > 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
MatCardModule,
|
||||
MatDividerModule,
|
||||
MatTooltipModule,
|
||||
UtilNgxEchartsModule,
|
||||
GaugeChartComponent,
|
||||
],
|
||||
declarations: [
|
||||
InfoCardComponent,
|
||||
InfoCardContentValueGroup,
|
||||
InfoCardFooterValueGroup,
|
||||
],
|
||||
exports: [
|
||||
InfoCardComponent,
|
||||
InfoCardContentValueGroup,
|
||||
InfoCardFooterValueGroup,
|
||||
],
|
||||
})
|
||||
export class InfoCardModule {}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Injectable, OnDestroy, inject } from '@angular/core';
|
||||
import { Injectable, OnDestroy, inject, signal } from '@angular/core';
|
||||
import { MsalBroadcastService, MsalService } from '@azure/msal-angular';
|
||||
import { AccountInfo, InteractionStatus } from '@azure/msal-browser';
|
||||
import {
|
||||
|
|
@ -20,12 +20,14 @@ export class AuthService implements OnDestroy {
|
|||
activeAccount$ = this.account$.pipe(
|
||||
tap((account) =>
|
||||
this.msalService.instance.setActiveAccount(account ?? null)
|
||||
)
|
||||
),
|
||||
tap((account) => this.activeAccount.set(account))
|
||||
);
|
||||
activeAccount = signal<AccountInfo | undefined>(undefined);
|
||||
roles$ = this.activeAccount$.pipe(
|
||||
filter(Boolean),
|
||||
map((account) => account?.idTokenClaims?.roles),
|
||||
shareReplay(1),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
msalService = inject(MsalService);
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@ import { EChartsOption } from 'echarts';
|
|||
import { filter, interval, map, Observable, Subscription } from 'rxjs';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import waldenTheme from './walden.theme.json';
|
||||
import westerosTheme from './westeros.theme.json';
|
||||
|
||||
const CUSTOM_THEME_TOKEN = 'customTheme';
|
||||
|
||||
@Directive({
|
||||
selector: '[dksEcharts]',
|
||||
standalone: false
|
||||
selector: '[dksEcharts]',
|
||||
standalone: false,
|
||||
})
|
||||
export class EchartsDirective implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() options?: EChartsOption;
|
||||
|
|
@ -38,16 +42,21 @@ export class EchartsDirective implements OnInit, OnDestroy, OnChanges {
|
|||
|
||||
ngOnInit(): void {
|
||||
echarts.use([...this.extentions, CanvasRenderer]);
|
||||
this.echartsInstance = echarts.init(this._el.nativeElement, this.theme, {
|
||||
width:
|
||||
this._el.nativeElement.clientWidth === 0
|
||||
? this.defaultWidth
|
||||
: undefined,
|
||||
height:
|
||||
this._el.nativeElement.clientHeight === 0
|
||||
? this.defaultHeight
|
||||
: undefined,
|
||||
});
|
||||
echarts.registerTheme(CUSTOM_THEME_TOKEN, westerosTheme.theme);
|
||||
this.echartsInstance = echarts.init(
|
||||
this._el.nativeElement,
|
||||
CUSTOM_THEME_TOKEN,
|
||||
{
|
||||
width:
|
||||
this._el.nativeElement.clientWidth === 0
|
||||
? this.defaultWidth
|
||||
: undefined,
|
||||
height:
|
||||
this._el.nativeElement.clientHeight === 0
|
||||
? this.defaultHeight
|
||||
: undefined,
|
||||
}
|
||||
);
|
||||
this.chartInit.emit(this.echartsInstance);
|
||||
this.setParams();
|
||||
if (this.isResizable) {
|
||||
|
|
@ -93,7 +102,7 @@ export class EchartsDirective implements OnInit, OnDestroy, OnChanges {
|
|||
|
||||
private getWidthSensor(
|
||||
element: HTMLElement,
|
||||
periodicityInMilliseconds: number = 2000
|
||||
periodicityInMilliseconds: number = 1000
|
||||
): Observable<number> {
|
||||
let oldWidth = element.clientWidth;
|
||||
return interval(periodicityInMilliseconds).pipe(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,392 @@
|
|||
|
||||
{
|
||||
"color": [
|
||||
"#3fb1e3",
|
||||
"#6be6c1",
|
||||
"#626c91",
|
||||
"#a0a7e6",
|
||||
"#c4ebad",
|
||||
"#96dee8"
|
||||
],
|
||||
"backgroundColor": "rgba(252,252,252,0)",
|
||||
"textStyle": {},
|
||||
"title": {
|
||||
"textStyle": {
|
||||
"color": "#666666"
|
||||
},
|
||||
"subtextStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"line": {
|
||||
"itemStyle": {
|
||||
"borderWidth": "2"
|
||||
},
|
||||
"lineStyle": {
|
||||
"width": "3"
|
||||
},
|
||||
"symbolSize": "8",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": false
|
||||
},
|
||||
"radar": {
|
||||
"itemStyle": {
|
||||
"borderWidth": "2"
|
||||
},
|
||||
"lineStyle": {
|
||||
"width": "3"
|
||||
},
|
||||
"symbolSize": "8",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": false
|
||||
},
|
||||
"bar": {
|
||||
"itemStyle": {
|
||||
"barBorderWidth": 0,
|
||||
"barBorderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"pie": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"scatter": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"boxplot": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"parallel": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"sankey": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"funnel": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"gauge": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"candlestick": {
|
||||
"itemStyle": {
|
||||
"color": "#e6a0d2",
|
||||
"color0": "transparent",
|
||||
"borderColor": "#e6a0d2",
|
||||
"borderColor0": "#3fb1e3",
|
||||
"borderWidth": "2"
|
||||
}
|
||||
},
|
||||
"graph": {
|
||||
"itemStyle": {
|
||||
"borderWidth": 0,
|
||||
"borderColor": "#ccc"
|
||||
},
|
||||
"lineStyle": {
|
||||
"width": "1",
|
||||
"color": "#cccccc"
|
||||
},
|
||||
"symbolSize": "8",
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": false,
|
||||
"color": [
|
||||
"#3fb1e3",
|
||||
"#6be6c1",
|
||||
"#626c91",
|
||||
"#a0a7e6",
|
||||
"#c4ebad",
|
||||
"#96dee8"
|
||||
],
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
}
|
||||
},
|
||||
"map": {
|
||||
"itemStyle": {
|
||||
"areaColor": "#eeeeee",
|
||||
"borderColor": "#aaaaaa",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
},
|
||||
"emphasis": {
|
||||
"itemStyle": {
|
||||
"areaColor": "rgba(63,177,227,0.25)",
|
||||
"borderColor": "#3fb1e3",
|
||||
"borderWidth": 1
|
||||
},
|
||||
"label": {
|
||||
"color": "#3fb1e3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"geo": {
|
||||
"itemStyle": {
|
||||
"areaColor": "#eeeeee",
|
||||
"borderColor": "#aaaaaa",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
},
|
||||
"emphasis": {
|
||||
"itemStyle": {
|
||||
"areaColor": "rgba(63,177,227,0.25)",
|
||||
"borderColor": "#3fb1e3",
|
||||
"borderWidth": 1
|
||||
},
|
||||
"label": {
|
||||
"color": "#3fb1e3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"categoryAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"color": "#999999"
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"valueAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"color": "#999999"
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"logAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"color": "#999999"
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeAxis": {
|
||||
"axisLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": "#cccccc"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false,
|
||||
"lineStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true,
|
||||
"color": "#999999"
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true,
|
||||
"lineStyle": {
|
||||
"color": [
|
||||
"#eeeeee"
|
||||
]
|
||||
}
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false,
|
||||
"areaStyle": {
|
||||
"color": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"toolbox": {
|
||||
"iconStyle": {
|
||||
"borderColor": "#999999"
|
||||
},
|
||||
"emphasis": {
|
||||
"iconStyle": {
|
||||
"borderColor": "#666666"
|
||||
}
|
||||
}
|
||||
},
|
||||
"legend": {
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"axisPointer": {
|
||||
"lineStyle": {
|
||||
"color": "#cccccc",
|
||||
"width": 1
|
||||
},
|
||||
"crossStyle": {
|
||||
"color": "#cccccc",
|
||||
"width": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"lineStyle": {
|
||||
"color": "#626c91",
|
||||
"width": 1
|
||||
},
|
||||
"itemStyle": {
|
||||
"color": "#626c91",
|
||||
"borderWidth": 1
|
||||
},
|
||||
"controlStyle": {
|
||||
"color": "#626c91",
|
||||
"borderColor": "#626c91",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"checkpointStyle": {
|
||||
"color": "#3fb1e3",
|
||||
"borderColor": "#3fb1e3"
|
||||
},
|
||||
"label": {
|
||||
"color": "#626c91"
|
||||
},
|
||||
"emphasis": {
|
||||
"itemStyle": {
|
||||
"color": "#626c91"
|
||||
},
|
||||
"controlStyle": {
|
||||
"color": "#626c91",
|
||||
"borderColor": "#626c91",
|
||||
"borderWidth": 0.5
|
||||
},
|
||||
"label": {
|
||||
"color": "#626c91"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualMap": {
|
||||
"color": [
|
||||
"#2a99c9",
|
||||
"#afe8ff"
|
||||
]
|
||||
},
|
||||
"dataZoom": {
|
||||
"backgroundColor": "rgba(255,255,255,0)",
|
||||
"dataBackgroundColor": "rgba(222,222,222,1)",
|
||||
"fillerColor": "rgba(114,230,212,0.25)",
|
||||
"handleColor": "#cccccc",
|
||||
"handleSize": "100%",
|
||||
"textStyle": {
|
||||
"color": "#999999"
|
||||
}
|
||||
},
|
||||
"markPoint": {
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
},
|
||||
"emphasis": {
|
||||
"label": {
|
||||
"color": "#ffffff"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
{
|
||||
"version": 1,
|
||||
"themeName": "westeros",
|
||||
"theme": {
|
||||
"seriesCnt": "6",
|
||||
"backgroundColor": "rgba(0,0,0,0)",
|
||||
"titleColor": "#3956bf",
|
||||
"subtitleColor": "#1794b5",
|
||||
"textColorShow": false,
|
||||
"textColor": "#333",
|
||||
"markTextColor": "#eeeeee",
|
||||
"color": [
|
||||
"#3956bf",
|
||||
"#1794b5",
|
||||
"#93b7e3",
|
||||
"#a5e7f0",
|
||||
"#cbb0e3",
|
||||
"#edafda"
|
||||
],
|
||||
"borderColor": "#ccc",
|
||||
"borderWidth": 0,
|
||||
"visualMapColor": [
|
||||
"#516b91",
|
||||
"#59c4e6",
|
||||
"#a5e7f0"
|
||||
],
|
||||
"legendTextColor": "#999999",
|
||||
"kColor": "#edafda",
|
||||
"kColor0": "transparent",
|
||||
"kBorderColor": "#d680bc",
|
||||
"kBorderColor0": "#8fd3e8",
|
||||
"kBorderWidth": "2",
|
||||
"lineWidth": "2",
|
||||
"symbolSize": "6",
|
||||
"symbol": "emptyCircle",
|
||||
"symbolBorderWidth": "2",
|
||||
"lineSmooth": true,
|
||||
"graphLineWidth": 1,
|
||||
"graphLineColor": "#aaaaaa",
|
||||
"mapLabelColor": "#000",
|
||||
"mapLabelColorE": "#516b91",
|
||||
"mapBorderColor": "#516b91",
|
||||
"mapBorderColorE": "#516b91",
|
||||
"mapBorderWidth": 0.5,
|
||||
"mapBorderWidthE": 1,
|
||||
"mapAreaColor": "#f3f3f3",
|
||||
"mapAreaColorE": "#a5e7f0",
|
||||
"axes": [
|
||||
{
|
||||
"type": "all",
|
||||
"name": "通用坐标轴",
|
||||
"axisLineShow": true,
|
||||
"axisLineColor": "#cccccc",
|
||||
"axisTickShow": false,
|
||||
"axisTickColor": "#333",
|
||||
"axisLabelShow": true,
|
||||
"axisLabelColor": "#999999",
|
||||
"splitLineShow": true,
|
||||
"splitLineColor": [
|
||||
"#eeeeee"
|
||||
],
|
||||
"splitAreaShow": false,
|
||||
"splitAreaColor": [
|
||||
"rgba(250,250,250,0.05)",
|
||||
"rgba(200,200,200,0.02)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "category",
|
||||
"name": "类目坐标轴",
|
||||
"axisLineShow": true,
|
||||
"axisLineColor": "#333",
|
||||
"axisTickShow": true,
|
||||
"axisTickColor": "#333",
|
||||
"axisLabelShow": true,
|
||||
"axisLabelColor": "#333",
|
||||
"splitLineShow": false,
|
||||
"splitLineColor": [
|
||||
"#ccc"
|
||||
],
|
||||
"splitAreaShow": false,
|
||||
"splitAreaColor": [
|
||||
"rgba(250,250,250,0.3)",
|
||||
"rgba(200,200,200,0.3)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "value",
|
||||
"name": "数值坐标轴",
|
||||
"axisLineShow": true,
|
||||
"axisLineColor": "#333",
|
||||
"axisTickShow": true,
|
||||
"axisTickColor": "#333",
|
||||
"axisLabelShow": true,
|
||||
"axisLabelColor": "#333",
|
||||
"splitLineShow": true,
|
||||
"splitLineColor": [
|
||||
"#ccc"
|
||||
],
|
||||
"splitAreaShow": false,
|
||||
"splitAreaColor": [
|
||||
"rgba(250,250,250,0.3)",
|
||||
"rgba(200,200,200,0.3)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "log",
|
||||
"name": "对数坐标轴",
|
||||
"axisLineShow": true,
|
||||
"axisLineColor": "#333",
|
||||
"axisTickShow": true,
|
||||
"axisTickColor": "#333",
|
||||
"axisLabelShow": true,
|
||||
"axisLabelColor": "#333",
|
||||
"splitLineShow": true,
|
||||
"splitLineColor": [
|
||||
"#ccc"
|
||||
],
|
||||
"splitAreaShow": false,
|
||||
"splitAreaColor": [
|
||||
"rgba(250,250,250,0.3)",
|
||||
"rgba(200,200,200,0.3)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "time",
|
||||
"name": "时间坐标轴",
|
||||
"axisLineShow": true,
|
||||
"axisLineColor": "#333",
|
||||
"axisTickShow": true,
|
||||
"axisTickColor": "#333",
|
||||
"axisLabelShow": true,
|
||||
"axisLabelColor": "#333",
|
||||
"splitLineShow": true,
|
||||
"splitLineColor": [
|
||||
"#ccc"
|
||||
],
|
||||
"splitAreaShow": false,
|
||||
"splitAreaColor": [
|
||||
"rgba(250,250,250,0.3)",
|
||||
"rgba(200,200,200,0.3)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"axisSeperateSetting": false,
|
||||
"toolboxColor": "#999999",
|
||||
"toolboxEmphasisColor": "#666666",
|
||||
"tooltipAxisColor": "#cccccc",
|
||||
"tooltipAxisWidth": 1,
|
||||
"timelineLineColor": "#8fd3e8",
|
||||
"timelineLineWidth": 1,
|
||||
"timelineItemColor": "#8fd3e8",
|
||||
"timelineItemColorE": "#8fd3e8",
|
||||
"timelineCheckColor": "#8fd3e8",
|
||||
"timelineCheckBorderColor": "#8a7ca8",
|
||||
"timelineItemBorderWidth": 1,
|
||||
"timelineControlColor": "#8fd3e8",
|
||||
"timelineControlBorderColor": "#8fd3e8",
|
||||
"timelineControlBorderWidth": 0.5,
|
||||
"timelineLabelColor": "#8fd3e8",
|
||||
"datazoomBackgroundColor": "rgba(0,0,0,0)",
|
||||
"datazoomDataColor": "rgba(255,255,255,0.3)",
|
||||
"datazoomFillColor": "rgba(167,183,204,0.4)",
|
||||
"datazoomHandleColor": "#a7b7cc",
|
||||
"datazoomHandleWidth": "100",
|
||||
"datazoomLabelColor": "#333"
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +82,19 @@ export type DocumentInfo = {
|
|||
tdLocation?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type HomeDashboardKpIs = {
|
||||
__typename?: 'HomeDashboardKPIs';
|
||||
anomalyCount: Scalars['Int']['output'];
|
||||
billableTourCount: Scalars['Int']['output'];
|
||||
billableTourRevenueSum: Scalars['Float']['output'];
|
||||
billedTourCount: Scalars['Int']['output'];
|
||||
billedTourNegativeRevenueDeviation: Scalars['Float']['output'];
|
||||
billedTourPositiveRevenueDeviation: Scalars['Float']['output'];
|
||||
employeesWithoutTourCount: Scalars['Int']['output'];
|
||||
revenueTrend: Array<RevenueTrend>;
|
||||
toursWithoutApprovalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type Interval = {
|
||||
__typename?: 'Interval';
|
||||
end: Scalars['DateTime']['output'];
|
||||
|
|
@ -108,6 +121,19 @@ export type KpiMetadata = {
|
|||
tourAmount: Scalars['Float']['output'];
|
||||
};
|
||||
|
||||
export type KpiInfo = {
|
||||
__typename?: 'KpiInfo';
|
||||
additional: Array<Metric>;
|
||||
main: Metric;
|
||||
secondary: Metric;
|
||||
};
|
||||
|
||||
export type Metric = {
|
||||
__typename?: 'Metric';
|
||||
label: Scalars['String']['output'];
|
||||
value: Scalars['Float']['output'];
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
acceptAnomaly: Anomaly;
|
||||
|
|
@ -175,19 +201,19 @@ export type Query = {
|
|||
anomalies: PaginatedAnomaly;
|
||||
anomaliesProto: Array<Anomaly>;
|
||||
anomaly: Anomaly;
|
||||
anomalyAmountByType: Array<Report>;
|
||||
averageAnomalyResolutionTimeReport: Array<Report>;
|
||||
groupedAnomalies: PaginatedAnomalyGroup;
|
||||
homeDashboardKpis: HomeDashboardKpIs;
|
||||
managerKpi: KpiInfo;
|
||||
searchTickets: Array<Ticket>;
|
||||
ticket: Ticket;
|
||||
ticketStateKeyFigures: TicketStateKeyFigures;
|
||||
ticketStateKpi: KpiInfo;
|
||||
ticketStateMeta: TicketStateMeta;
|
||||
tickets: PaginatedTicket;
|
||||
timePerTourMetricForDay: Array<TimePerTourMetric>;
|
||||
timePerTourMetricForMonth: Array<TimePerTourMetric>;
|
||||
tour: Tour;
|
||||
tourCostBreakdown: TourCostBreakdown;
|
||||
toursPerTimeKPI: Array<TimePerTourKpiSummary>;
|
||||
upcomingTourNotificationReport: Array<Report>;
|
||||
upcomingTours: Array<Tour>;
|
||||
};
|
||||
|
||||
|
|
@ -222,6 +248,11 @@ export type QueryGroupedAnomaliesArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type QueryManagerKpiArgs = {
|
||||
ordinanceType: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QuerySearchTicketsArgs = {
|
||||
query: Scalars['String']['input'];
|
||||
};
|
||||
|
|
@ -237,6 +268,11 @@ export type QueryTicketStateKeyFiguresArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type QueryTicketStateKpiArgs = {
|
||||
state: TicketValidationState;
|
||||
};
|
||||
|
||||
|
||||
export type QueryTicketStateMetaArgs = {
|
||||
state: TicketValidationState;
|
||||
};
|
||||
|
|
@ -262,7 +298,7 @@ export type QueryTimePerTourMetricForMonthArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type QueryTourArgs = {
|
||||
export type QueryTourCostBreakdownArgs = {
|
||||
operationId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
|
@ -271,10 +307,18 @@ export type QueryToursPerTimeKpiArgs = {
|
|||
ordinanceTypes: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type Report = {
|
||||
__typename?: 'Report';
|
||||
label: Scalars['String']['output'];
|
||||
export type RevenueTrend = {
|
||||
__typename?: 'RevenueTrend';
|
||||
date: Scalars['DateTime']['output'];
|
||||
ordinanceType: Scalars['String']['output'];
|
||||
revenueSum: Scalars['Float']['output'];
|
||||
};
|
||||
|
||||
export type ServicePosition = {
|
||||
__typename?: 'ServicePosition';
|
||||
description: Scalars['String']['output'];
|
||||
value: Scalars['Float']['output'];
|
||||
variant: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type SolutionTarget = {
|
||||
|
|
@ -433,9 +477,18 @@ export type Tour = {
|
|||
type: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type TourCostBreakdown = {
|
||||
__typename?: 'TourCostBreakdown';
|
||||
servicePositions: Array<ServicePosition>;
|
||||
tariffDescription: Scalars['String']['output'];
|
||||
tariffValidityPeriod: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type TourStateMeta = {
|
||||
__typename?: 'TourStateMeta';
|
||||
count: Scalars['Int']['output'];
|
||||
negativeRevenueDeviation?: Maybe<Scalars['Float']['output']>;
|
||||
positiveRevenueDeviation?: Maybe<Scalars['Float']['output']>;
|
||||
revenueSum?: Maybe<Scalars['Float']['output']>;
|
||||
};
|
||||
|
||||
|
|
@ -471,6 +524,11 @@ export type AcceptAnomalyMutationVariables = Exact<{
|
|||
|
||||
export type AcceptAnomalyMutation = { __typename?: 'Mutation', acceptAnomaly: { __typename?: 'Anomaly', id: string, type: string, description: string, groupKey: string, groupDescription: string, sleepTimer: number, sleepSince?: any | null, workedOnBy: string, resolvedAt?: any | null, createdAt: any, solution: Array<{ __typename?: 'SolutionTarget', anomalyId: string, id: string, name: string, steps: Array<{ __typename?: 'SolutionTargetStep', description: string, id: string, solutionTargetId: string }> }> } };
|
||||
|
||||
export type HomeDashboardQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type HomeDashboardQuery = { __typename?: 'Query', homeDashboardKpis: { __typename?: 'HomeDashboardKPIs', anomalyCount: number, employeesWithoutTourCount: number, toursWithoutApprovalCount: number, billableTourCount: number, billableTourRevenueSum: number, billedTourCount: number, billedTourPositiveRevenueDeviation: number, billedTourNegativeRevenueDeviation: number, revenueTrend: Array<{ __typename?: 'RevenueTrend', date: any, ordinanceType: string, revenueSum: number }> } };
|
||||
|
||||
export type TimePerTourUpdatedSubscriptionVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
|
|
@ -506,20 +564,19 @@ export type GetTimePerTourMetricForMonthQueryVariables = Exact<{
|
|||
|
||||
export type GetTimePerTourMetricForMonthQuery = { __typename?: 'Query', timePerTourMetricForMonth: Array<{ __typename?: 'TimePerTourMetric', availableTime: number, tourAmount: number, breakEven: number, revenue: number, averageRevenue: number, personnelCost: number, consumptionCost: number, forecast: number, forecastRevenue: number, interval: { __typename?: 'Interval', start: any, end: any }, categoryAmounts: Array<{ __typename?: 'TimePerTourMetricCategoryAmount', category: string, amount: number }> }> };
|
||||
|
||||
export type GetAnomalyByTypeReportQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
export type TicketStateKpiQueryVariables = Exact<{
|
||||
state: TicketValidationState;
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAnomalyByTypeReportQuery = { __typename?: 'Query', anomalyAmountByType: Array<{ __typename?: 'Report', label: string, value: number }> };
|
||||
export type TicketStateKpiQuery = { __typename?: 'Query', ticketStateKpi: { __typename?: 'KpiInfo', main: { __typename?: 'Metric', label: string, value: number }, secondary: { __typename?: 'Metric', label: string, value: number }, additional: Array<{ __typename?: 'Metric', label: string, value: number }> } };
|
||||
|
||||
export type GetUpcomingTourNotificationReportQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
export type ManagerKpiQueryVariables = Exact<{
|
||||
ordinanceType: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetUpcomingTourNotificationReportQuery = { __typename?: 'Query', upcomingTourNotificationReport: Array<{ __typename?: 'Report', label: string, value: number }> };
|
||||
|
||||
export type GetAverageAnomalyResolutionTimeReportQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetAverageAnomalyResolutionTimeReportQuery = { __typename?: 'Query', averageAnomalyResolutionTimeReport: Array<{ __typename?: 'Report', label: string, value: number }> };
|
||||
export type ManagerKpiQuery = { __typename?: 'Query', managerKpi: { __typename?: 'KpiInfo', main: { __typename?: 'Metric', label: string, value: number }, secondary: { __typename?: 'Metric', label: string, value: number }, additional: Array<{ __typename?: 'Metric', label: string, value: number }> } };
|
||||
|
||||
export type TicketsUpdatedSubscriptionVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
|
@ -569,7 +626,7 @@ export type GetAccountingStateMetaQueryVariables = Exact<{
|
|||
}>;
|
||||
|
||||
|
||||
export type GetAccountingStateMetaQuery = { __typename?: 'Query', accountingStateMeta: { __typename?: 'TourStateMeta', count: number, revenueSum?: number | null } };
|
||||
export type GetAccountingStateMetaQuery = { __typename?: 'Query', accountingStateMeta: { __typename?: 'TourStateMeta', count: number, revenueSum?: number | null, positiveRevenueDeviation?: number | null, negativeRevenueDeviation?: number | null } };
|
||||
|
||||
export type GetAccountingToursQueryVariables = Exact<{
|
||||
state: AccountingState;
|
||||
|
|
@ -581,6 +638,13 @@ export type GetAccountingToursQueryVariables = Exact<{
|
|||
|
||||
export type GetAccountingToursQuery = { __typename?: 'Query', accountingTours: { __typename?: 'PaginatedTour', prevCursor: string, endCursor: string, hasNextPage: boolean, totalCount: number, nodes: Array<{ __typename?: 'Tour', id: string, done: boolean, direction: string, carName?: string | null, operationId: string, driverName?: string | null, codriverName?: string | null, patientName?: string | null, patientSurname?: string | null, patientStreet?: string | null, patientZip?: string | null, patientCity?: string | null, healthInsurance?: string | null, category?: string | null, transportType?: string | null, ordinanceType?: string | null, startStreet?: string | null, startZip?: string | null, startCity?: string | null, targetStreet?: string | null, targetZip?: string | null, targetCity?: string | null, startDate?: any | null, check: number, occupiedKm: number, totalKm: number, type: string, rangeEndDate?: any | null, startBegin?: string | null, startEnd?: string | null, target?: string | null, targetBegin?: string | null, empty?: string | null, billDate?: any | null, billNumber?: string | null, consumptionCosts: number, revenue: number, revenueDispoLive: number, revenueDeviation: number, startInstitution?: string | null, targetInstitution?: string | null, ticketId?: string | null, createdAt?: any | null, ticket?: { __typename?: 'Ticket', id: string, currentState: TicketValidationState, approvalState?: ApprovalState | null, errors: Array<string>, notes: Array<string>, isUrgent: boolean, urgency: any, createdAt: any } | null }> } };
|
||||
|
||||
export type GetTourCostBreakdownQueryVariables = Exact<{
|
||||
operationId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetTourCostBreakdownQuery = { __typename?: 'Query', tourCostBreakdown: { __typename?: 'TourCostBreakdown', tariffDescription: string, tariffValidityPeriod: string, servicePositions: Array<{ __typename?: 'ServicePosition', value: number, description: string, variant: string }> } };
|
||||
|
||||
export const AnomalyAllFragmentDoc = gql`
|
||||
fragment AnomalyAll on Anomaly {
|
||||
id
|
||||
|
|
@ -688,6 +752,36 @@ export const AcceptAnomalyDocument = gql`
|
|||
export class AcceptAnomalyGQL extends Apollo.Mutation<AcceptAnomalyMutation, AcceptAnomalyMutationVariables> {
|
||||
override document = AcceptAnomalyDocument;
|
||||
|
||||
constructor(apollo: Apollo.Apollo) {
|
||||
super(apollo);
|
||||
}
|
||||
}
|
||||
export const HomeDashboardDocument = gql`
|
||||
query HomeDashboard {
|
||||
homeDashboardKpis {
|
||||
anomalyCount
|
||||
employeesWithoutTourCount
|
||||
toursWithoutApprovalCount
|
||||
billableTourCount
|
||||
billableTourRevenueSum
|
||||
billedTourCount
|
||||
billedTourPositiveRevenueDeviation
|
||||
billedTourNegativeRevenueDeviation
|
||||
revenueTrend {
|
||||
date
|
||||
ordinanceType
|
||||
revenueSum
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HomeDashboardGQL extends Apollo.Query<HomeDashboardQuery, HomeDashboardQueryVariables> {
|
||||
override document = HomeDashboardDocument;
|
||||
|
||||
constructor(apollo: Apollo.Apollo) {
|
||||
super(apollo);
|
||||
}
|
||||
|
|
@ -844,11 +938,21 @@ export const GetTimePerTourMetricForMonthDocument = gql`
|
|||
super(apollo);
|
||||
}
|
||||
}
|
||||
export const GetAnomalyByTypeReportDocument = gql`
|
||||
query GetAnomalyByTypeReport {
|
||||
anomalyAmountByType {
|
||||
label
|
||||
value
|
||||
export const TicketStateKpiDocument = gql`
|
||||
query TicketStateKpi($state: TicketValidationState!) {
|
||||
ticketStateKpi(state: $state) {
|
||||
main {
|
||||
label
|
||||
value
|
||||
}
|
||||
secondary {
|
||||
label
|
||||
value
|
||||
}
|
||||
additional {
|
||||
label
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -856,46 +960,37 @@ export const GetAnomalyByTypeReportDocument = gql`
|
|||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GetAnomalyByTypeReportGQL extends Apollo.Query<GetAnomalyByTypeReportQuery, GetAnomalyByTypeReportQueryVariables> {
|
||||
override document = GetAnomalyByTypeReportDocument;
|
||||
export class TicketStateKpiGQL extends Apollo.Query<TicketStateKpiQuery, TicketStateKpiQueryVariables> {
|
||||
override document = TicketStateKpiDocument;
|
||||
|
||||
constructor(apollo: Apollo.Apollo) {
|
||||
super(apollo);
|
||||
}
|
||||
}
|
||||
export const GetUpcomingTourNotificationReportDocument = gql`
|
||||
query GetUpcomingTourNotificationReport {
|
||||
upcomingTourNotificationReport {
|
||||
label
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GetUpcomingTourNotificationReportGQL extends Apollo.Query<GetUpcomingTourNotificationReportQuery, GetUpcomingTourNotificationReportQueryVariables> {
|
||||
override document = GetUpcomingTourNotificationReportDocument;
|
||||
|
||||
constructor(apollo: Apollo.Apollo) {
|
||||
super(apollo);
|
||||
export const ManagerKpiDocument = gql`
|
||||
query ManagerKpi($ordinanceType: String!) {
|
||||
managerKpi(ordinanceType: $ordinanceType) {
|
||||
main {
|
||||
label
|
||||
value
|
||||
}
|
||||
secondary {
|
||||
label
|
||||
value
|
||||
}
|
||||
additional {
|
||||
label
|
||||
value
|
||||
}
|
||||
}
|
||||
export const GetAverageAnomalyResolutionTimeReportDocument = gql`
|
||||
query GetAverageAnomalyResolutionTimeReport {
|
||||
averageAnomalyResolutionTimeReport {
|
||||
label
|
||||
value
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GetAverageAnomalyResolutionTimeReportGQL extends Apollo.Query<GetAverageAnomalyResolutionTimeReportQuery, GetAverageAnomalyResolutionTimeReportQueryVariables> {
|
||||
override document = GetAverageAnomalyResolutionTimeReportDocument;
|
||||
export class ManagerKpiGQL extends Apollo.Query<ManagerKpiQuery, ManagerKpiQueryVariables> {
|
||||
override document = ManagerKpiDocument;
|
||||
|
||||
constructor(apollo: Apollo.Apollo) {
|
||||
super(apollo);
|
||||
|
|
@ -1220,6 +1315,8 @@ export const GetAccountingStateMetaDocument = gql`
|
|||
accountingStateMeta(state: $state) {
|
||||
count
|
||||
revenueSum
|
||||
positiveRevenueDeviation
|
||||
negativeRevenueDeviation
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -1306,6 +1403,30 @@ export const GetAccountingToursDocument = gql`
|
|||
export class GetAccountingToursGQL extends Apollo.Query<GetAccountingToursQuery, GetAccountingToursQueryVariables> {
|
||||
override document = GetAccountingToursDocument;
|
||||
|
||||
constructor(apollo: Apollo.Apollo) {
|
||||
super(apollo);
|
||||
}
|
||||
}
|
||||
export const GetTourCostBreakdownDocument = gql`
|
||||
query GetTourCostBreakdown($operationId: String!) {
|
||||
tourCostBreakdown(operationId: $operationId) {
|
||||
tariffDescription
|
||||
tariffValidityPeriod
|
||||
servicePositions {
|
||||
value
|
||||
description
|
||||
variant
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class GetTourCostBreakdownGQL extends Apollo.Query<GetTourCostBreakdownQuery, GetTourCostBreakdownQueryVariables> {
|
||||
override document = GetTourCostBreakdownDocument;
|
||||
|
||||
constructor(apollo: Apollo.Apollo) {
|
||||
super(apollo);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
query HomeDashboard {
|
||||
homeDashboardKpis {
|
||||
anomalyCount
|
||||
employeesWithoutTourCount
|
||||
toursWithoutApprovalCount
|
||||
billableTourCount
|
||||
billableTourRevenueSum
|
||||
billedTourCount
|
||||
billedTourPositiveRevenueDeviation
|
||||
billedTourNegativeRevenueDeviation
|
||||
revenueTrend {
|
||||
date
|
||||
ordinanceType
|
||||
revenueSum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -89,3 +89,37 @@ query GetTimePerTourMetricForMonth($day: DateTime!, $ordinanceType: String!) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
query TicketStateKpi($state: TicketValidationState!) {
|
||||
ticketStateKpi(state: $state) {
|
||||
main {
|
||||
label
|
||||
value
|
||||
}
|
||||
secondary {
|
||||
label
|
||||
value
|
||||
}
|
||||
additional {
|
||||
label
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query ManagerKpi($ordinanceType: String!) {
|
||||
managerKpi(ordinanceType: $ordinanceType) {
|
||||
main {
|
||||
label
|
||||
value
|
||||
}
|
||||
secondary {
|
||||
label
|
||||
value
|
||||
}
|
||||
additional {
|
||||
label
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
query GetAnomalyByTypeReport {
|
||||
anomalyAmountByType {
|
||||
label
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
query GetUpcomingTourNotificationReport {
|
||||
upcomingTourNotificationReport {
|
||||
label
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
query GetAverageAnomalyResolutionTimeReport {
|
||||
averageAnomalyResolutionTimeReport {
|
||||
label
|
||||
value
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,8 @@ query GetAccountingStateMeta($state: AccountingState!) {
|
|||
accountingStateMeta(state: $state) {
|
||||
count
|
||||
revenueSum
|
||||
positiveRevenueDeviation
|
||||
negativeRevenueDeviation
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,3 +107,15 @@ query GetAccountingTours(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
query GetTourCostBreakdown($operationId: String!) {
|
||||
tourCostBreakdown(operationId: $operationId) {
|
||||
tariffDescription
|
||||
tariffValidityPeriod
|
||||
servicePositions {
|
||||
value
|
||||
description
|
||||
variant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import { registerInstrumentations } from '@opentelemetry/instrumentation';
|
||||
import {
|
||||
WebTracerProvider,
|
||||
ConsoleSpanExporter,
|
||||
SimpleSpanProcessor,
|
||||
BatchSpanProcessor,
|
||||
ParentBasedSampler,
|
||||
TraceIdRatioBasedSampler,
|
||||
} from '@opentelemetry/sdk-trace-web';
|
||||
import { getWebAutoInstrumentations } from '@opentelemetry/auto-instrumentations-web';
|
||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
||||
import { Resource } from '@opentelemetry/resources';
|
||||
import { B3Propagator } from '@opentelemetry/propagator-b3';
|
||||
import {
|
||||
ATTR_SERVICE_NAME,
|
||||
ATTR_SERVICE_VERSION,
|
||||
} from '@opentelemetry/semantic-conventions';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
const resource = Resource.default().merge(
|
||||
new Resource({
|
||||
[ATTR_SERVICE_NAME]: environment.production ? 'avicenna.dashboard.prod' : 'avicenna.dashboard',
|
||||
// [ATTR_SERVICE_NAMESPACE]: 'avicenna',
|
||||
[ATTR_SERVICE_VERSION]: '1.0',
|
||||
environment: environment.production ? 'production' : 'staging'
|
||||
})
|
||||
);
|
||||
|
||||
const provider = new WebTracerProvider({
|
||||
resource,
|
||||
sampler: new ParentBasedSampler({
|
||||
root: new TraceIdRatioBasedSampler(0.1),
|
||||
}),
|
||||
spanProcessors: [
|
||||
new SimpleSpanProcessor(new ConsoleSpanExporter()),
|
||||
new BatchSpanProcessor(
|
||||
new OTLPTraceExporter({
|
||||
url: 'https://collector.genius.ceo/v1/traces',
|
||||
}),
|
||||
{
|
||||
maxQueueSize: 100,
|
||||
maxExportBatchSize: 10,
|
||||
scheduledDelayMillis: 500,
|
||||
}
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
provider.register({
|
||||
propagator: new B3Propagator(),
|
||||
});
|
||||
|
||||
registerInstrumentations({
|
||||
instrumentations: [
|
||||
getWebAutoInstrumentations({
|
||||
'@opentelemetry/instrumentation-document-load': {},
|
||||
'@opentelemetry/instrumentation-user-interaction': {},
|
||||
'@opentelemetry/instrumentation-fetch': {
|
||||
propagateTraceHeaderCorsUrls: /.+/,
|
||||
},
|
||||
'@opentelemetry/instrumentation-xml-http-request': {
|
||||
propagateTraceHeaderCorsUrls: /.+/,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, effect, inject, input, signal } from '@angular/core';
|
||||
import { tap } from 'rxjs';
|
||||
import { InfoCardModule } from '../../../core/components/info-card/info-card.component';
|
||||
import {
|
||||
AccountingState,
|
||||
TourStateMeta,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
import { DashboardAccountingService } from '../dashboard-accounting.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accounting-info-card',
|
||||
imports: [InfoCardModule, CurrencyPipe],
|
||||
template: `
|
||||
@if (kpi()) {
|
||||
<app-info-card>
|
||||
<div class="flex justify-around items-center">
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<div class="text-xl">{{ kpi()?.count }}</div>
|
||||
<div class="text-sm">Tickets</div>
|
||||
</div>
|
||||
@if(kpi()?.revenueSum && accountingState() !== AccountingState.Billed) {
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<div class="text-xl">
|
||||
{{ kpi()?.revenueSum | currency : 'EUR' : 'symbol' : '.2' }}
|
||||
</div>
|
||||
<div class="text-sm">Umsatz</div>
|
||||
</div>
|
||||
} @if(kpi()?.positiveRevenueDeviation && kpi()?.negativeRevenueDeviation
|
||||
&& accountingState() === AccountingState.Billed) {
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<div class="text-xl text-green-600">
|
||||
+{{
|
||||
kpi()?.positiveRevenueDeviation
|
||||
| currency : 'EUR' : 'symbol' : '.2'
|
||||
}}
|
||||
</div>
|
||||
<div class="text-xl text-red-700">
|
||||
{{
|
||||
kpi()?.negativeRevenueDeviation
|
||||
| currency : 'EUR' : 'symbol' : '.2'
|
||||
}}
|
||||
</div>
|
||||
<div class="text-sm">Umsatz</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</app-info-card>
|
||||
}
|
||||
<!-- @switch (accountingState()) { @case (AccountingState.Billable) {
|
||||
<app-info-card>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<div class="text-xl">124</div>
|
||||
<div class="text-sm">Tickets</div>
|
||||
</div>
|
||||
</app-info-card>
|
||||
} @case (AccountingState.PreparedForBilling) {
|
||||
<app-info-card>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<div class="text-xl">124</div>
|
||||
<div class="text-sm">Tickets</div>
|
||||
</div>
|
||||
</app-info-card>
|
||||
} @case (AccountingState.Billed) {
|
||||
<app-info-card>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<div class="text-xl">124</div>
|
||||
<div class="text-sm">Tickets</div>
|
||||
</div>
|
||||
</app-info-card>
|
||||
} } -->
|
||||
`,
|
||||
styles: ``,
|
||||
})
|
||||
export class AccountingInfoCardComponent {
|
||||
AccountingState = AccountingState;
|
||||
|
||||
private service = inject(DashboardAccountingService);
|
||||
|
||||
accountingState = input.required<AccountingState>();
|
||||
kpi = signal<TourStateMeta | undefined>(undefined);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.service
|
||||
.fetchAccountingStateMeta(this.accountingState())
|
||||
.pipe(tap((res) => this.kpi.set(res)))
|
||||
.subscribe();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -12,12 +12,17 @@ import { MatBadgeModule } from '@angular/material/badge';
|
|||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { HotToastService } from '@ngxpert/hot-toast';
|
||||
import { Tour, Ticket, AccountingState, TicketValidationState } from '../../../core/data-access/graphql/generated/generated';
|
||||
import {
|
||||
AccountingState,
|
||||
Ticket,
|
||||
TicketValidationState,
|
||||
Tour,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-accounting-item',
|
||||
template: ` <div class="flex justify-between">
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-accounting-item',
|
||||
template: ` <div class="flex justify-between">
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
mat-icon-button
|
||||
|
|
@ -44,22 +49,30 @@ import { Tour, Ticket, AccountingState, TicketValidationState } from '../../../c
|
|||
tour.revenueDeviation | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE'
|
||||
}}
|
||||
</ng-template>`,
|
||||
styles: [
|
||||
`
|
||||
host: {
|
||||
class: 'flex flex-col rounded border p-2 m-1 bg-white cursor-pointer',
|
||||
},
|
||||
styles: [
|
||||
`
|
||||
@use '@angular/material' as mat;
|
||||
:host {
|
||||
@apply flex flex-col rounded border p-2 m-1 bg-white cursor-pointer;
|
||||
@include mat.icon-button-overrides(
|
||||
(
|
||||
icon-size: 14,
|
||||
)
|
||||
);
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatBadgeModule,
|
||||
ClipboardModule,
|
||||
NgIf,
|
||||
NgClass,
|
||||
CurrencyPipe,
|
||||
]
|
||||
],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatBadgeModule,
|
||||
ClipboardModule,
|
||||
NgIf,
|
||||
NgClass,
|
||||
CurrencyPipe,
|
||||
],
|
||||
})
|
||||
export class AccountingItemComponent implements AfterViewInit {
|
||||
toast = inject(HotToastService);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
|||
import { TourStateMeta } from '../../../core/data-access/graphql/generated/generated';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-accounting-lane-footer',
|
||||
template: `<span *ngIf="withTourCount" class="text-left text-xs">
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-accounting-lane-footer',
|
||||
template: `<span *ngIf="withTourCount" class="text-left text-xs">
|
||||
{{ tourStateMeta.count }} Touren
|
||||
</span>
|
||||
<span *ngIf="withRevenueSum" class="text-left text-xs">
|
||||
|
|
@ -14,14 +14,11 @@ import { TourStateMeta } from '../../../core/data-access/graphql/generated/gener
|
|||
| currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE'
|
||||
}}
|
||||
</span>`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
@apply flex flex-col items-center justify-center p-2;
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [NgIf, AsyncPipe, CurrencyPipe]
|
||||
host: {
|
||||
class: 'flex flex-col items-center justify-center p-2',
|
||||
},
|
||||
styles: [``],
|
||||
imports: [NgIf, CurrencyPipe],
|
||||
})
|
||||
export class AccountingLaneFooterComponent {
|
||||
@Input() tourStateMeta!: TourStateMeta;
|
||||
|
|
|
|||
|
|
@ -6,13 +6,16 @@ import {
|
|||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
AccountingFilterArgs,
|
||||
AccountingState,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
import { AccountingLaneHeaderFilterComponent } from './accounting-lane-header-filter.component';
|
||||
import { AccountingFilterArgs, AccountingState } from '../../../core/data-access/graphql/generated/generated';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-accounting-lane-header',
|
||||
template: `<div></div>
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-accounting-lane-header',
|
||||
template: `<div></div>
|
||||
<ng-container [ngSwitch]="state">
|
||||
<div *ngSwitchCase="AccountingState.Billable">Abrechenbar</div>
|
||||
<div *ngSwitchCase="AccountingState.PreparedForBilling">
|
||||
|
|
@ -26,14 +29,17 @@ import { AccountingFilterArgs, AccountingState } from '../../../core/data-access
|
|||
[state]="state"
|
||||
(filterChange)="filterChanged($event)"
|
||||
></dks-accounting-lane-header-filter>`,
|
||||
styles: [
|
||||
`
|
||||
host: {
|
||||
class: 'flex items-center justify-between p-2 bg-secondary-container',
|
||||
},
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
@apply flex items-center justify-between p-2 bg-gray-50;
|
||||
background-color: var(--mat-sys-secondary-container);
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [NgSwitch, NgSwitchCase, AccountingLaneHeaderFilterComponent]
|
||||
],
|
||||
imports: [NgSwitch, NgSwitchCase, AccountingLaneHeaderFilterComponent],
|
||||
})
|
||||
export class AccountingLaneHeaderComponent {
|
||||
@Input() state!: AccountingState;
|
||||
|
|
|
|||
|
|
@ -10,18 +10,19 @@
|
|||
>
|
||||
<dks-accounting-item
|
||||
*cdkVirtualFor="let tour of tours; trackBy: trackByFn"
|
||||
class="mat-elevation-z4 animate-fadeIn"
|
||||
class="animate-fadeIn"
|
||||
style="box-shadow: var(--mat-sys-level2);"
|
||||
[accountingState]="accountingState"
|
||||
[tour]="tour"
|
||||
(click)="openTourView(tour)"
|
||||
>
|
||||
</dks-accounting-item>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
<ng-container *ngIf="accountingStateMeta$ | async as accountingStateMeta">
|
||||
<!-- <ng-container *ngIf="accountingStateMeta$ | async as accountingStateMeta">
|
||||
<dks-accounting-lane-footer
|
||||
[tourStateMeta]="accountingStateMeta"
|
||||
[withTourCount]="true"
|
||||
[withRevenueSum]="accountingState !== AccountingState.Billed"
|
||||
></dks-accounting-lane-footer>
|
||||
</ng-container>
|
||||
</ng-container> -->
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {
|
|||
CdkVirtualScrollViewport,
|
||||
ScrollingModule,
|
||||
} from '@angular/cdk/scrolling';
|
||||
import { AsyncPipe, CurrencyPipe, NgClass, NgFor, NgIf } from '@angular/common';
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
|
|
@ -32,32 +32,31 @@ import {
|
|||
import { AccountingTourViewComponent } from '../accounting-tour-view/accounting-tour-view.component';
|
||||
import { DashboardAccountingService } from '../dashboard-accounting.service';
|
||||
import { AccountingItemComponent } from './accounting-item.component';
|
||||
import { AccountingLaneFooterComponent } from './accounting-lane-footer.component';
|
||||
import { AccountingLaneHeaderComponent } from './accounting-lane-header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dks-accounting-lane',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './accounting-lane.component.html',
|
||||
styles: [
|
||||
`
|
||||
selector: 'dks-accounting-lane',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './accounting-lane.component.html',
|
||||
host: {
|
||||
class: 'rounded h-full w-full grid',
|
||||
},
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
@apply rounded bg-gray-200 h-full w-full grid;
|
||||
box-shadow: var(--mat-sys-level1);
|
||||
background-color: var(--mat-sys-surface-container-low);
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [
|
||||
AccountingLaneHeaderComponent,
|
||||
AccountingLaneFooterComponent,
|
||||
AccountingItemComponent,
|
||||
NgFor,
|
||||
NgIf,
|
||||
NgClass,
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
ScrollingModule,
|
||||
]
|
||||
],
|
||||
imports: [
|
||||
AccountingLaneHeaderComponent,
|
||||
AccountingItemComponent,
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
ScrollingModule,
|
||||
],
|
||||
})
|
||||
export class AccountingLaneComponent {
|
||||
private dialog = inject(DialogService);
|
||||
|
|
@ -91,7 +90,7 @@ export class AccountingLaneComponent {
|
|||
this.accountingService.fetchAccountingStateMeta(accountingState)
|
||||
)
|
||||
);
|
||||
tours$: Observable<Tour[]>;
|
||||
tours$: Observable<(Tour & { ticket: Ticket })[]>;
|
||||
|
||||
constructor() {
|
||||
const batchMap: Observable<Record<string, Tour & { ticket: Ticket }>> =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
import { CurrencyPipe, NgClass, NgStyle } from '@angular/common';
|
||||
import { Component, effect, input, signal } from '@angular/core';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { tap } from 'rxjs';
|
||||
import {
|
||||
GetTourCostBreakdownGQL,
|
||||
GetTourCostBreakdownQuery,
|
||||
Tour,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accounting-tour-cost-breakdown',
|
||||
imports: [MatDividerModule, CurrencyPipe, NgClass],
|
||||
template: `
|
||||
@if (costBreakdown() !== undefined) {
|
||||
<div class="px-4 py-2 flex justify-between">
|
||||
<div class="flex flex-col text-gray-600 text-sm">
|
||||
<span class="font-medium">Tarif</span>
|
||||
<span>
|
||||
{{ costBreakdown()?.tariffDescription }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col text-gray-600 text-sm">
|
||||
<span class="font-medium">Gültigkeitszeitraum</span>
|
||||
<span>
|
||||
{{ costBreakdown()?.tariffValidityPeriod }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="px-4 py-2 grid *:py-1">
|
||||
<div
|
||||
class="w-full justify-self-start"
|
||||
style="background-color: var(--mat-sys-secondary-container)"
|
||||
>
|
||||
Beschreibung
|
||||
</div>
|
||||
<!-- <div
|
||||
class="w-full justify-self-center"
|
||||
style="background-color: var(--mat-sys-secondary-container)"
|
||||
>
|
||||
Typ
|
||||
</div> -->
|
||||
<div
|
||||
class="w-full justify-self-end"
|
||||
style="background-color: var(--mat-sys-secondary-container)"
|
||||
>
|
||||
Wert
|
||||
</div>
|
||||
<div
|
||||
class="w-full justify-self-end"
|
||||
style="background-color: var(--mat-sys-secondary-container)"
|
||||
>
|
||||
Summe
|
||||
</div>
|
||||
@for (servicePosition of costBreakdown()?.servicePositions; track $index;
|
||||
let o = $odd) {
|
||||
<div class="w-full" [ngClass]="{ 'bg-gray-100': o }">
|
||||
{{ servicePosition.description }}
|
||||
</div>
|
||||
<!-- <div class="justify-self-center w-full break-all" [ngClass]="{ 'bg-gray-100': o }">
|
||||
{{ servicePosition.variant }}
|
||||
</div> -->
|
||||
<div class="justify-self-end w-full" [ngClass]="{ 'bg-gray-100': o }">
|
||||
{{
|
||||
servicePosition.value / 100
|
||||
| currency : 'EUR' : 'symbol' : '.2' : 'de'
|
||||
}}
|
||||
</div>
|
||||
<div class="justify-self-end w-full" [ngClass]="{ 'bg-gray-100': o }">
|
||||
{{
|
||||
sumServicePositionToIndex(costBreakdown()?.servicePositions, $index) /
|
||||
100 | currency : 'EUR' : 'symbol' : '.2' : 'de'
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
`,
|
||||
styles: `.grid {grid-template-columns: 3fr 1fr 1fr;}`,
|
||||
})
|
||||
export class AccountingTourCostBreakdownComponent {
|
||||
tour = input.required<Tour>();
|
||||
costBreakdown = signal<
|
||||
GetTourCostBreakdownQuery['tourCostBreakdown'] | undefined
|
||||
>(undefined);
|
||||
|
||||
constructor(private readonly tourCostBreakdownGql: GetTourCostBreakdownGQL) {
|
||||
effect(() => {
|
||||
tourCostBreakdownGql
|
||||
.fetch({ operationId: this.tour().operationId })
|
||||
.pipe(tap((res) => this.costBreakdown.set(res.data.tourCostBreakdown)))
|
||||
.subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
sumServicePositionToIndex(
|
||||
servicePositions: any[] | undefined,
|
||||
idx: number
|
||||
): number {
|
||||
console.log(servicePositions, idx);
|
||||
return (servicePositions ?? [])
|
||||
.slice(0, idx + 1)
|
||||
.reduce((sum, { value }) => (sum += value), 0);
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +95,6 @@ import { Ticket, TicketValidationState, Tour } from '../../../core/data-access/g
|
|||
NgFor,
|
||||
NgSwitch,
|
||||
NgSwitchCase,
|
||||
DatePipe,
|
||||
]
|
||||
})
|
||||
export class AccountingTourTicketComponent {
|
||||
|
|
|
|||
|
|
@ -8,17 +8,23 @@ import {
|
|||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
import { AccountingTourOverviewComponent } from './accounting-tour-overview.component';
|
||||
import { AccountingTourTicketComponent } from './accounting-tour-ticket.component';
|
||||
import { AccountingTourCostBreakdownComponent } from './accounting-tour-cost-breakdown.component';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-accounting-tour-view',
|
||||
template: `<h3 class="py-2 px-3 !mb-0">
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-accounting-tour-view',
|
||||
template: `<h3 class="py-2 px-3 !mb-0">
|
||||
{{ ref.data.ordinanceType }} -
|
||||
{{ ref.data.patientName }}
|
||||
{{ ref.data.patientSurname }}
|
||||
</h3>
|
||||
<hr />
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Umsatzberechnung">
|
||||
<app-accounting-tour-cost-breakdown
|
||||
[tour]="ref.data"
|
||||
></app-accounting-tour-cost-breakdown>
|
||||
</mat-tab>
|
||||
<mat-tab label="Übersicht">
|
||||
<dks-accounting-tour-overview
|
||||
[tour]="ref.data"
|
||||
|
|
@ -30,15 +36,16 @@ import { AccountingTourTicketComponent } from './accounting-tour-ticket.componen
|
|||
></dks-accounting-tour-ticket>
|
||||
</mat-tab>
|
||||
</mat-tab-group> `,
|
||||
styles: [],
|
||||
imports: [
|
||||
MatTabsModule,
|
||||
MatIconModule,
|
||||
AccountingTourOverviewComponent,
|
||||
AccountingTourTicketComponent,
|
||||
]
|
||||
styles: [],
|
||||
imports: [
|
||||
MatTabsModule,
|
||||
MatIconModule,
|
||||
AccountingTourOverviewComponent,
|
||||
AccountingTourTicketComponent,
|
||||
AccountingTourCostBreakdownComponent
|
||||
],
|
||||
})
|
||||
export class AccountingTourViewComponent {
|
||||
ref: DialogRef<Tour & { ticket: Ticket }, undefined | any> =
|
||||
inject(DialogRef);
|
||||
ref =
|
||||
inject<DialogRef<Tour & { ticket: Ticket }, undefined | any>>(DialogRef);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +1,52 @@
|
|||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { AccountingLaneComponent } from './accounting-lane/accounting-lane.component';
|
||||
import { AccountingState } from '../../core/data-access/graphql/generated/generated';
|
||||
import { AccountingInfoCardComponent } from './accounting-info-card/accounting-info-card.component';
|
||||
import { AccountingLaneComponent } from './accounting-lane/accounting-lane.component';
|
||||
|
||||
@Component({
|
||||
selector: 'dks-dashboard-accounting',
|
||||
imports: [AccountingLaneComponent],
|
||||
template: `<section class="h-full p-6 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="col-span-1 md:col-span-5 flex justify-end items-center">
|
||||
selector: 'dks-dashboard-accounting',
|
||||
imports: [AccountingInfoCardComponent, AccountingLaneComponent],
|
||||
template: `<section class="h-full p-6 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="col-span-1 md:col-span-3 flex justify-end items-center">
|
||||
<!-- Action Buttons -->
|
||||
</div>
|
||||
<dks-accounting-lane
|
||||
<app-accounting-info-card
|
||||
class="self-center"
|
||||
[accountingState]="AccountingState.Billable"
|
||||
></dks-accounting-lane>
|
||||
></app-accounting-info-card>
|
||||
<app-accounting-info-card
|
||||
class="self-center"
|
||||
[accountingState]="AccountingState.PreparedForBilling"
|
||||
></app-accounting-info-card>
|
||||
<app-accounting-info-card
|
||||
class="self-center"
|
||||
[accountingState]="AccountingState.Billed"
|
||||
></app-accounting-info-card>
|
||||
<div>
|
||||
<dks-accounting-lane
|
||||
[accountingState]="AccountingState.Billable"
|
||||
></dks-accounting-lane>
|
||||
</div>
|
||||
<dks-accounting-lane
|
||||
[accountingState]="AccountingState.PreparedForBilling"
|
||||
></dks-accounting-lane>
|
||||
<dks-accounting-lane [accountingState]="AccountingState.Billed"></dks-accounting-lane>
|
||||
<dks-accounting-lane
|
||||
[accountingState]="AccountingState.Billed"
|
||||
></dks-accounting-lane>
|
||||
</section>`,
|
||||
styles: [
|
||||
`
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: block;
|
||||
height: calc(100% - 64px);
|
||||
}
|
||||
section {
|
||||
grid-template-rows: 2rem 1fr;
|
||||
grid-template-rows: 2rem auto 1fr;
|
||||
/* grid-template-rows: 2rem 1fr; */
|
||||
}
|
||||
`,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DashboardAccountingComponent {
|
||||
AccountingState = AccountingState;
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
:host {
|
||||
@apply w-full rounded flex items-center;
|
||||
}
|
||||
|
|
@ -35,6 +35,9 @@ interface Anomaly {
|
|||
selector: 'dks-anomaly-list-item',
|
||||
templateUrl: './anomaly-list-item.component.html',
|
||||
styleUrls: ['./anomaly-list-item.component.css'],
|
||||
host: {
|
||||
class: 'w-full rounded flex items-center',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
:host {
|
||||
@apply flex flex-col h-full;
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ import {
|
|||
Component,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import {
|
||||
|
|
@ -12,17 +12,20 @@ import {
|
|||
shareReplay,
|
||||
Subscription,
|
||||
switchMap,
|
||||
tap
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AnomaliesService } from '../anomalies.service';
|
||||
|
||||
@Component({
|
||||
selector: 'dks-anomaly-list',
|
||||
templateUrl: './anomaly-list.component.html',
|
||||
styleUrls: ['./anomaly-list.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
selector: 'dks-anomaly-list',
|
||||
templateUrl: './anomaly-list.component.html',
|
||||
styleUrls: ['./anomaly-list.component.scss'],
|
||||
host: {
|
||||
class: 'flex flex-col h-full',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class AnomalyListComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(MatPaginator, { static: true }) private paginator!: MatPaginator;
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
:host {
|
||||
@apply m-4;
|
||||
}
|
||||
|
|
@ -12,10 +12,10 @@
|
|||
</article>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="flex justify-end mt-2">
|
||||
<button mat-button color="primary" (click)="ref.close('sleep')">
|
||||
<button mat-button (click)="ref.close('sleep')">
|
||||
<mat-icon>schedule</mat-icon> Stummschalten
|
||||
</button>
|
||||
<button mat-button color="warn" (click)="ref.close('accept')">
|
||||
<button mat-button class="warn-btn" (click)="ref.close('accept')">
|
||||
<mat-icon>check_circle_outline</mat-icon> Akzeptieren
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
@use "@angular/material" as mat;
|
||||
|
||||
.warn-btn {
|
||||
@include mat.button-overrides(
|
||||
(
|
||||
text-label-text-color: var(--mat-sys-error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -11,7 +11,10 @@ interface SolutionTarget {
|
|||
@Component({
|
||||
selector: 'dks-anomaly-solution-dialog',
|
||||
templateUrl: './anomaly-solution-dialog.component.html',
|
||||
styleUrls: ['./anomaly-solution-dialog.component.css'],
|
||||
styleUrls: ['./anomaly-solution-dialog.component.scss'],
|
||||
host: {
|
||||
class: 'm-4',
|
||||
},
|
||||
standalone: false
|
||||
})
|
||||
export class AnomalySolutionDialogComponent {
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
:host {
|
||||
@apply flex flex-col h-full;
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
</mat-icon>
|
||||
</button>
|
||||
{{ node.name }}
|
||||
<div class="ml-auto bg-blue-400 rounded-full w-5 h-5 flex justify-center items-center text-white font-bold">{{ node?.childCount }}</div>
|
||||
<div class="ml-auto rounded-full w-5 h-5 flex justify-center items-center font-bold" style="background-color: var(--mat-sys-primary-container); color: var(--mat-sys-on-primary-container)">{{ node?.childCount }}</div>
|
||||
</mat-tree-node>
|
||||
</mat-tree>
|
||||
<ng-template #emptyListTmpl>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@ interface AnomalyFlatNode {
|
|||
@Component({
|
||||
selector: 'dks-anomaly-tree',
|
||||
templateUrl: './anomaly-tree.component.html',
|
||||
styleUrls: ['./anomaly-tree.component.css'],
|
||||
styleUrls: ['./anomaly-tree.component.scss'],
|
||||
host: {
|
||||
class: 'flex flex-col h-full',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false
|
||||
})
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
></dks-tours-per-time-kpi>
|
||||
</div>
|
||||
<div class="flex lg:flex-nowrap flex-wrap-reverse">
|
||||
<mat-card class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col">
|
||||
<mat-card appearance="outlined" class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col">
|
||||
<mat-card-header>
|
||||
<mat-card-title> Anomalien </mat-card-title>
|
||||
</mat-card-header>
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
<dks-anomaly-tree></dks-anomaly-tree>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col">
|
||||
<mat-card appearance="outlined" class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col">
|
||||
<mat-card-header>
|
||||
<mat-card-title> Mitarbeiter ohne Tour </mat-card-title>
|
||||
</mat-card-header>
|
||||
|
|
@ -30,12 +30,12 @@
|
|||
<dks-anomaly-list></dks-anomaly-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card class="w-full lg:w-1/5 m-2">
|
||||
<mat-card appearance="outlined" class="w-full lg:w-1/5 m-2">
|
||||
<mat-card-header>
|
||||
<mat-card-title> Nächste Touren ohne Anfahrt </mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<!-- <dks-upcoming-tours></dks-upcoming-tours> -->
|
||||
<dks-upcoming-tours></dks-upcoming-tours>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
|
@ -15,7 +15,7 @@ import { AnomalyListItemComponent } from './anomaly-list/anomaly-list-item/anoma
|
|||
import { AnomalyListComponent } from './anomaly-list/anomaly-list.component';
|
||||
import { AnomalySolutionDialogComponent } from './anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component';
|
||||
import { AnomalyTreeComponent } from './anomaly-tree/anomaly-tree.component';
|
||||
import { ControlCenterDashboardComponent } from './control-center-dashboard/control-center-dashboard.component';
|
||||
import { ControlCenterDashboardComponent } from './control-center-dashboard.component';
|
||||
import { ToursPerTimeKPIComponent } from './kpis/tours-per-time-kpi/tours-per-time-kpi.component';
|
||||
import { TourViewDialogComponent } from './upcoming-tours/tour-view-dialog/tour-view-dialog.component';
|
||||
import { UpcomingToursComponent } from './upcoming-tours/upcoming-tours.component';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<mat-card>
|
||||
<mat-card appearance="raised">
|
||||
<mat-card-header class="flex justify-center">
|
||||
<mat-card-title>{{ ordinanceType }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
|
|
@ -6,17 +6,18 @@
|
|||
<div class="flex">
|
||||
<div
|
||||
*ngFor="let kpi of kpis; index as idx"
|
||||
class="w-1/3 flex flex-col justify-center items-center border-blue-400"
|
||||
[ngClass]="{'border-b-2': (currentTimeSlot$ | async) === idx}"
|
||||
class="w-1/3 flex flex-col justify-center items-center"
|
||||
style="border-color: var(--mat-sys-primary);"
|
||||
[ngClass]="{
|
||||
'border-b-2': (currentTimeSlot$ | async) === idx,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="p-4 text-base"
|
||||
>
|
||||
{{ kpi.kpi.value.toString().replace('.', ',') }}
|
||||
<div class="p-4 text-base">
|
||||
{{ kpi.kpi.value.toString().replace(".", ",") }}
|
||||
</div>
|
||||
<div class="font-bold">
|
||||
{{ kpi.interval.start | date: 'HH:mm' }} -
|
||||
{{ kpi.interval.end | date: 'HH:mm' }}
|
||||
{{ kpi.interval.start | date : "HH:mm" }} -
|
||||
{{ kpi.interval.end | date : "HH:mm" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
:host {
|
||||
@apply m-4;
|
||||
}
|
||||
|
|
@ -6,6 +6,9 @@ import { Tour } from '../../../../core/data-access/graphql/generated/generated';
|
|||
selector: 'dks-tour-view-dialog',
|
||||
templateUrl: './tour-view-dialog.component.html',
|
||||
styleUrls: ['./tour-view-dialog.component.css'],
|
||||
host: {
|
||||
class: 'm-4',
|
||||
},
|
||||
standalone: false
|
||||
})
|
||||
export class TourViewDialogComponent {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HomeComponent } from './home.component';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HomeComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, computed, effect, OnInit, signal } from '@angular/core';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { tap } from 'rxjs';
|
||||
import { InfoCardModule } from '../../core/components/info-card/info-card.component';
|
||||
import {
|
||||
HomeDashboardGQL,
|
||||
HomeDashboardQuery,
|
||||
TicketValidationState,
|
||||
} from '../../core/data-access/graphql/generated/generated';
|
||||
import { ManagerInfoCardComponent } from '../manager/components/manager-info-card/manager-info-card.component';
|
||||
import { TicketStateInfoCardComponent } from '../ticket-system/ticket-state-info-card/ticket-state-info-card.component';
|
||||
import { LinkCardComponent } from './link-card/link-card.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
imports: [
|
||||
ManagerInfoCardComponent,
|
||||
TicketStateInfoCardComponent,
|
||||
InfoCardModule,
|
||||
LinkCardComponent,
|
||||
MatDividerModule,
|
||||
CurrencyPipe,
|
||||
],
|
||||
template: `
|
||||
<div class="flex flex-col m-0 md:m-4">
|
||||
@if (kpis() !== undefined) {
|
||||
<div
|
||||
class="mb-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-9 gap-2 lg:gap-4"
|
||||
>
|
||||
<div class="col-span-9 text-lg">Übersicht</div>
|
||||
<app-link-card [target]="'/control-center'" class="col-span-3">
|
||||
<div class="flex justify-between">
|
||||
<div
|
||||
class="p-2 basis-1/2 flex flex-col justify-center items-center"
|
||||
>
|
||||
<div class="text-xl" style="color: var(--mat-sys-primary)">
|
||||
{{ kpis()?.anomalyCount }}
|
||||
</div>
|
||||
<div class="text-sm text-center">Aktuelle<br />Anomalien</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-2 basis-1/2 flex flex-col justify-center items-center"
|
||||
>
|
||||
<div class="text-xl" style="color: var(--mat-sys-primary)">
|
||||
{{ kpis()?.employeesWithoutTourCount }}
|
||||
</div>
|
||||
<div class="text-sm text-center">Mitarbeiter ohne Tour</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-link-card>
|
||||
<app-link-card
|
||||
[target]="'/managers/dayview'"
|
||||
class="row-span-1 col-span-3"
|
||||
>
|
||||
<div class="h-full flex flex-col justify-between">
|
||||
<div class="flex justify-between">
|
||||
@for (revenueItem of revenueByOrdinanceType(); track $index) {
|
||||
<div
|
||||
class="p-2 basis-1/2 flex flex-col justify-center items-center"
|
||||
>
|
||||
<div class="text-lg" style="color: var(--mat-sys-primary)">
|
||||
{{
|
||||
revenueItem.value
|
||||
| currency : 'EUR' : 'symbol-narrow' : '.2' : 'de'
|
||||
}}
|
||||
</div>
|
||||
<div class="text-sm">{{ revenueItem.label }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<!-- <div class="flex flex-col">
|
||||
<div class="text-small pb-2">Umsatzverlauf 10 Tage</div>
|
||||
<app-trend-graph [data]="kpis()?.revenueTrend"></app-trend-graph>
|
||||
</div> -->
|
||||
</div>
|
||||
</app-link-card>
|
||||
<app-link-card [target]="'/accounting'" class="col-span-3 row-span-2">
|
||||
<div class="h-full flex justify-around">
|
||||
<div class="flex flex-col h-full justify-around items-center">
|
||||
<div>Abrechenbar</div>
|
||||
<div
|
||||
class="p-2 basis-1/2 flex flex-col justify-center items-center"
|
||||
>
|
||||
<div class="text-xl" style="color: var(--mat-sys-primary)">
|
||||
{{ kpis()?.billableTourCount }}
|
||||
</div>
|
||||
<div class="text-sm text-center">Touren</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-2 basis-1/2 flex flex-col justify-center items-center"
|
||||
>
|
||||
<div class="text-xl" style="color: var(--mat-sys-primary)">
|
||||
{{
|
||||
kpis()?.billableTourRevenueSum
|
||||
| currency : 'EUR' : 'symbol-narrow' : '.2' : 'de'
|
||||
}}
|
||||
</div>
|
||||
<div class="text-sm text-center">Umsatz</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col h-full justify-around items-center">
|
||||
<div>Falsch abgerechnet</div>
|
||||
<div
|
||||
class="p-2 basis-1/2 flex flex-col justify-center items-center"
|
||||
>
|
||||
<div class="text-xl" style="color: var(--mat-sys-primary)">
|
||||
{{ kpis()?.billedTourCount }}
|
||||
</div>
|
||||
<div class="text-sm text-center">Touren</div>
|
||||
</div>
|
||||
<div
|
||||
class="p-2 basis-1/2 flex flex-col justify-center items-center"
|
||||
>
|
||||
<div class="text-xl text-green-700">
|
||||
+{{
|
||||
kpis()?.billedTourPositiveRevenueDeviation
|
||||
| currency : 'EUR' : 'symbol-narrow' : '.2' : 'de'
|
||||
}}
|
||||
</div>
|
||||
<div class="text-xl" style="color: var(--mat-sys-error)">
|
||||
{{
|
||||
kpis()?.billedTourNegativeRevenueDeviation
|
||||
| currency : 'EUR' : 'symbol-narrow' : '.2' : 'de'
|
||||
}}
|
||||
</div>
|
||||
<div class="text-sm text-center">Umsatz</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-link-card>
|
||||
<app-link-card [target]="'/tickets'" class="col-span-3">
|
||||
<div class="w-full h-full flex flex-col justify-center items-center">
|
||||
<div class="text-large text-center">
|
||||
<span class="font-bold" style="color: var(--mat-sys-error);">{{
|
||||
kpis()?.employeesWithoutTourCount
|
||||
}}</span>
|
||||
Touren starten in 3 Tagen ohne Genehmigung
|
||||
</div>
|
||||
</div>
|
||||
</app-link-card>
|
||||
</div>
|
||||
|
||||
}
|
||||
<div
|
||||
class="my-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 lg:gap-4"
|
||||
>
|
||||
<div class="col-span-4 text-lg">Manager System - Tagespotenzial</div>
|
||||
<app-manager-info-card ordinanceType="BTW"></app-manager-info-card>
|
||||
<app-manager-info-card ordinanceType="TSW"></app-manager-info-card>
|
||||
<app-manager-info-card ordinanceType="KTW"></app-manager-info-card>
|
||||
<!-- <app-manager-info-card ordinanceType="ALL"></app-manager-info-card> -->
|
||||
</div>
|
||||
<div
|
||||
class="my-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
|
||||
>
|
||||
<div class="col-span-4 text-lg">Ticket System - Bearbeitungsstatus</div>
|
||||
<app-ticket-state-info-card
|
||||
[ticketState]="TicketState.Created"
|
||||
></app-ticket-state-info-card>
|
||||
<app-ticket-state-info-card
|
||||
[ticketState]="TicketState.UserInteractionRequired"
|
||||
></app-ticket-state-info-card>
|
||||
<app-ticket-state-info-card
|
||||
[ticketState]="TicketState.TdUncertain"
|
||||
></app-ticket-state-info-card>
|
||||
<app-ticket-state-info-card
|
||||
[ticketState]="TicketState.DocumentsMissing"
|
||||
></app-ticket-state-info-card>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: ``,
|
||||
})
|
||||
export class HomeComponent implements OnInit {
|
||||
TicketState = TicketValidationState;
|
||||
|
||||
kpis = signal<HomeDashboardQuery['homeDashboardKpis'] | undefined>(undefined);
|
||||
|
||||
public readonly revenueByOrdinanceType = computed(() => {
|
||||
const currentData = this.kpis()?.revenueTrend;
|
||||
if (!currentData) return;
|
||||
|
||||
const groupedByOrdinanceType = [...currentData]
|
||||
.sort(
|
||||
({ date: aDate }, { date: bDate }) =>
|
||||
new Date(aDate).getTime() - new Date(bDate).getTime()
|
||||
)
|
||||
.reduce((sum, { ordinanceType, revenueSum }) => {
|
||||
return {
|
||||
...sum,
|
||||
[ordinanceType]: [
|
||||
...(sum[ordinanceType] ? sum[ordinanceType] : []),
|
||||
revenueSum,
|
||||
],
|
||||
};
|
||||
}, {} as Record<string, number[]>);
|
||||
|
||||
return Object.entries(groupedByOrdinanceType).map(([ot, revenues]) => {
|
||||
return {
|
||||
label: ot,
|
||||
value: revenues.at(-1), //.reduce((sum, cur) => (sum += cur), 0),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
constructor(private readonly homeDashboardGql: HomeDashboardGQL) {
|
||||
effect(() => {
|
||||
this.homeDashboardGql
|
||||
.fetch()
|
||||
.pipe(tap((res) => this.kpis.set(res.data.homeDashboardKpis)))
|
||||
.subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LinkCardComponent } from './link-card.component';
|
||||
|
||||
describe('LinkCardComponent', () => {
|
||||
let component: LinkCardComponent;
|
||||
let fixture: ComponentFixture<LinkCardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LinkCardComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LinkCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { Component, input } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-link-card',
|
||||
imports: [MatCardModule, RouterModule],
|
||||
template: `
|
||||
<mat-card [appearance]="appearance()" class="min-h-24 h-full">
|
||||
<mat-card-content class="h-full !p-0">
|
||||
<div class="h-full w-full flex justify-center items-stretch">
|
||||
<div class="p-2 h-full w-full">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div
|
||||
class="border-l pl-2 pr-2 basis-10 flex justify-center items-center cursor-pointer group"
|
||||
style="color: var(--mat-sys-on-background); border-color: var(--mat-sys-outline)"
|
||||
[routerLink]="target()"
|
||||
>
|
||||
<svg
|
||||
class="group-hover:scale-115 transition ease-in-out duration-200"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 30 100"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<path
|
||||
d="M5 10 L25 50 L5 90"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
`,
|
||||
styles: ``,
|
||||
})
|
||||
export class LinkCardComponent {
|
||||
appearance = input<'raised' | 'outlined'>('raised');
|
||||
target = input.required<string>();
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TrendGraphComponent } from './trend-graph.component';
|
||||
|
||||
describe('TrendGraphComponent', () => {
|
||||
let component: TrendGraphComponent;
|
||||
let fixture: ComponentFixture<TrendGraphComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TrendGraphComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TrendGraphComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
import { Component, computed, input, signal } from '@angular/core';
|
||||
import { format } from 'date-fns';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { LineChart } from 'echarts/charts';
|
||||
import {
|
||||
GridComponent,
|
||||
MarkPointComponent,
|
||||
TooltipComponent,
|
||||
} from 'echarts/components';
|
||||
import { ECharts } from 'echarts/core';
|
||||
import { UtilNgxEchartsModule } from '../../../core/components/ngx-echarts/util-ngx-echarts.module';
|
||||
|
||||
@Component({
|
||||
selector: 'app-trend-graph',
|
||||
imports: [UtilNgxEchartsModule],
|
||||
template: `<div
|
||||
dksEcharts
|
||||
[defaultHeight]="70"
|
||||
[options]="options()"
|
||||
[extentions]="extentions()"
|
||||
(chartInit)="setChartInstance($event)"
|
||||
></div>`,
|
||||
styles: ``,
|
||||
})
|
||||
export class TrendGraphComponent {
|
||||
data = input<{ date: Date; ordinanceType: string; revenueSum: number }[]>();
|
||||
|
||||
private chart?: ECharts;
|
||||
|
||||
public readonly options = computed<EChartsOption>(() => {
|
||||
console.log(this.data());
|
||||
return this.generateEchartsOptions();
|
||||
});
|
||||
|
||||
extentions = signal([
|
||||
LineChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
MarkPointComponent,
|
||||
]);
|
||||
|
||||
constructor() {
|
||||
// effect(() => {
|
||||
// console.log(`chartvalue changed`, this.data(), this.chart);
|
||||
// const currentData = this.data();
|
||||
// if (!currentData) return;
|
||||
// const groupedByOrdinanceType = [...currentData]
|
||||
// .sort(
|
||||
// ({ date: aDate }, { date: bDate }) =>
|
||||
// new Date(aDate).getTime() - new Date(bDate).getTime()
|
||||
// )
|
||||
// .reduce((sum, { ordinanceType, revenueSum }) => {
|
||||
// return {
|
||||
// ...sum,
|
||||
// [ordinanceType]: [
|
||||
// ...(sum[ordinanceType] ? sum[ordinanceType] : []),
|
||||
// revenueSum,
|
||||
// ],
|
||||
// };
|
||||
// }, {} as Record<string, number[]>);
|
||||
// setTimeout(() => {
|
||||
// if (!!this.chart) {
|
||||
// console.log({
|
||||
// xAxis: [
|
||||
// {
|
||||
// data: [
|
||||
// ...new Set(
|
||||
// this.data()?.map(({ date }) =>
|
||||
// format(new Date(date), 'EEE: dd.MM')
|
||||
// )
|
||||
// ),
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// series: [
|
||||
// ...Object.entries(groupedByOrdinanceType).map(
|
||||
// ([ot, revenues]) => {
|
||||
// return {
|
||||
// id: ot,
|
||||
// data: revenues,
|
||||
// };
|
||||
// }
|
||||
// ),
|
||||
// // {
|
||||
// // data: [
|
||||
// // {
|
||||
// // value: this.data(),
|
||||
// // },
|
||||
// // ],
|
||||
// // },
|
||||
// ],
|
||||
// });
|
||||
// this.chart.setOption({
|
||||
// xAxis: [
|
||||
// {
|
||||
// data: [
|
||||
// ...new Set(
|
||||
// this.data()?.map(({ date }) =>
|
||||
// format(new Date(date), 'EEEE: dd.MM')
|
||||
// )
|
||||
// ),
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// series: [
|
||||
// ...Object.entries(groupedByOrdinanceType).map(
|
||||
// ([ot, revenues]) => {
|
||||
// return {
|
||||
// id: ot,
|
||||
// data: [{ value: revenues }],
|
||||
// };
|
||||
// }
|
||||
// ),
|
||||
// // {
|
||||
// // data: [
|
||||
// // {
|
||||
// // value: this.data(),
|
||||
// // },
|
||||
// // ],
|
||||
// // },
|
||||
// ],
|
||||
// });
|
||||
// }
|
||||
// }, 1000);
|
||||
// });
|
||||
}
|
||||
|
||||
generateEchartsOptions(): EChartsOption {
|
||||
|
||||
const baseOptions: EChartsOption = {
|
||||
grid: {
|
||||
left: '30px',
|
||||
right: '30px',
|
||||
bottom: '3px',
|
||||
top: '15px',
|
||||
// containLabel: false,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
axisLabel: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: true },
|
||||
splitLine: { show: false },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { show: false },
|
||||
axisTick: { show: false },
|
||||
axisLine: { show: true },
|
||||
splitLine: { show: false },
|
||||
min: 0,
|
||||
max: 'dataMax',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
// axisPointer: {
|
||||
// type: 'cross',
|
||||
// label: {
|
||||
// precision: 2,
|
||||
// },
|
||||
// },
|
||||
formatter: function (params: any) {
|
||||
const param = params[0];
|
||||
const dateLabel = param.name;
|
||||
const revenueValue = param.value;
|
||||
|
||||
const formattedRevenue =
|
||||
typeof revenueValue === 'number'
|
||||
? revenueValue.toLocaleString('de-DE', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
})
|
||||
: revenueValue; // Fallback falls kein Zahlenwert
|
||||
|
||||
return `${dateLabel}<br/><b>Umsatz: ${formattedRevenue}</b>`;
|
||||
},
|
||||
backgroundColor: 'rgba(50,50,50,0.7)',
|
||||
borderColor: '#333',
|
||||
borderWidth: 0,
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{id: '1. KTW', data: []},
|
||||
{id: '2. TSW', data: []},
|
||||
{id: '3. BTW', data: []},
|
||||
// {
|
||||
// data: [],
|
||||
// type: 'line',
|
||||
// // smooth: true,
|
||||
// // showSymbol: false,
|
||||
// emphasis: {
|
||||
// focus: 'series',
|
||||
// },
|
||||
// markPoint: {
|
||||
// symbol: 'circle',
|
||||
// symbolSize: 25,
|
||||
// label: {
|
||||
// formatter: '{c}€',
|
||||
// },
|
||||
// data: [
|
||||
// { name: 'min', type: 'min' },
|
||||
// { name: 'max', type: 'max' },
|
||||
// { name: 'Current day', x: 'Di', y: 14744 },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
],
|
||||
};
|
||||
|
||||
// return baseOptions
|
||||
|
||||
const currentData = this.data();
|
||||
if (!currentData) return baseOptions;
|
||||
|
||||
const groupedByOrdinanceType = [...currentData]
|
||||
.sort(
|
||||
({ date: aDate }, { date: bDate }) =>
|
||||
new Date(aDate).getTime() - new Date(bDate).getTime()
|
||||
)
|
||||
.reduce((sum, { ordinanceType, revenueSum }) => {
|
||||
return {
|
||||
...sum,
|
||||
[ordinanceType]: [
|
||||
...(sum[ordinanceType] ? sum[ordinanceType] : []),
|
||||
revenueSum,
|
||||
],
|
||||
};
|
||||
}, {} as Record<string, number[]>);
|
||||
|
||||
return {
|
||||
...baseOptions,
|
||||
xAxis: {
|
||||
...baseOptions.xAxis,
|
||||
// data: [
|
||||
// ...new Set(
|
||||
// this.data()?.map(({ date }) => format(new Date(date), 'EEE: dd.MM'))
|
||||
// ),
|
||||
// ],
|
||||
},
|
||||
series: [
|
||||
...Object.entries(groupedByOrdinanceType).map(([ot, revenues]) => {
|
||||
return {
|
||||
id: ot,
|
||||
data: revenues,
|
||||
};
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
setChartInstance(chart: ECharts) {
|
||||
this.chart = chart;
|
||||
}
|
||||
}
|
||||
|
|
@ -15,15 +15,16 @@ import { MatTableModule } from '@angular/material/table';
|
|||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { de } from 'date-fns/locale';
|
||||
import { UtilNgxEchartsModule } from '../../core/components/ngx-echarts/util-ngx-echarts.module';
|
||||
import { ManagerInfoCardComponent } from './components/manager-info-card/manager-info-card.component';
|
||||
import { MetricSummaryComponent } from './components/metric-summary/metric-summary.component';
|
||||
import { ViewTypeSelectorComponent } from './components/view-type-selector/view-type-selector.component';
|
||||
import { DayviewComponent } from './dayview/dayview.component';
|
||||
import { ManagerDashboardComponent } from './manager-dashboard/manager-dashboard.component';
|
||||
import { MonthviewComponent } from './monthview/monthview.component';
|
||||
import { OverviewComponent } from './overview/overview.component';
|
||||
import { TimePerTourByCarComponent } from './time-per-tour-by-car/time-per-tour-by-car.component';
|
||||
import { TimePerTourKPIComponent } from './time-per-tour-kpi/time-per-tour-kpi.component';
|
||||
import { ViewTypeSelectorComponent } from './components/view-type-selector/view-type-selector.component';
|
||||
import { UtilNgxEchartsModule } from '../../core/components/ngx-echarts/util-ngx-echarts.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
|
@ -69,6 +70,7 @@ import { UtilNgxEchartsModule } from '../../core/components/ngx-echarts/util-ngx
|
|||
UtilNgxEchartsModule,
|
||||
MetricSummaryComponent,
|
||||
ViewTypeSelectorComponent,
|
||||
ManagerInfoCardComponent,
|
||||
],
|
||||
declarations: [
|
||||
ManagerDashboardComponent,
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
:host {
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
|
|
@ -9,7 +9,10 @@ import { summarizeMetrics } from '../../util/summarize-metrics';
|
|||
selector: 'dks-average-revenue-summary',
|
||||
imports: [CommonModule, MatIconModule],
|
||||
templateUrl: './average-revenue-summary.component.html',
|
||||
styleUrls: ['./average-revenue-summary.component.css']
|
||||
styleUrls: ['./average-revenue-summary.component.css'],
|
||||
host: {
|
||||
class: 'flex justify-center items-center',
|
||||
},
|
||||
})
|
||||
export class AverageRevenueSummaryComponent {
|
||||
@Input() set metrics(value: TimePerTourMetric[]) {
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
:host {
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
|
|
@ -31,7 +31,8 @@
|
|||
<mat-icon>account_balance</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end text-yellow-500">
|
||||
<!-- TODO: Add feature toggle for forecast -->
|
||||
<!-- <div class="flex items-center justify-end text-yellow-500">
|
||||
<span class="text-base"
|
||||
>+{{
|
||||
summary.forecastRevenue | currency : 'EUR' : 'symbol' : '1.2-2'
|
||||
|
|
@ -40,5 +41,5 @@
|
|||
<div class="p-1 text-sm" title="Umsatz Vorausschau">
|
||||
<mat-icon>account_balance</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,13 @@ import { TimePerTourMetric } from '../../../../core/data-access/graphql/generate
|
|||
import { summarizeMetrics } from '../../util/summarize-metrics';
|
||||
|
||||
@Component({
|
||||
selector: 'dks-breakeven-summary',
|
||||
imports: [CommonModule, MatIconModule],
|
||||
templateUrl: './breakeven-summary.component.html',
|
||||
styleUrls: ['./breakeven-summary.component.css']
|
||||
selector: 'dks-breakeven-summary',
|
||||
imports: [CommonModule, MatIconModule],
|
||||
templateUrl: './breakeven-summary.component.html',
|
||||
styleUrls: ['./breakeven-summary.component.css'],
|
||||
host: {
|
||||
class: 'flex justify-center items-center',
|
||||
},
|
||||
})
|
||||
export class BreakevenSummaryComponent {
|
||||
@Input() set metrics(value: TimePerTourMetric[]) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
@if (kpi()?.main?.label) {
|
||||
<app-info-card
|
||||
[chartValue]="kpi()?.main?.value!"
|
||||
[chartLabel]="kpi()?.main?.label!"
|
||||
[title]="ordinanceType()"
|
||||
>
|
||||
<app-info-card-content
|
||||
[value]="kpi()?.secondary?.value | number : '.0-1' : 'de'"
|
||||
[label]="kpi()?.secondary?.label"
|
||||
></app-info-card-content>
|
||||
@for (metric of kpi()?.additional; track $index) {
|
||||
<app-info-card-footer
|
||||
[value]="metric.value | number : '.0-2' : 'de'"
|
||||
[label]="metric.label"
|
||||
>
|
||||
</app-info-card-footer>
|
||||
}
|
||||
</app-info-card>
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ManagerInfoCardComponent } from './manager-info-card.component';
|
||||
|
||||
describe('ManagerInfoCardComponent', () => {
|
||||
let component: ManagerInfoCardComponent;
|
||||
let fixture: ComponentFixture<ManagerInfoCardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ManagerInfoCardComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ManagerInfoCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { DecimalPipe } from '@angular/common';
|
||||
import { Component, effect, inject, input, signal } from '@angular/core';
|
||||
import { tap } from 'rxjs';
|
||||
import { InfoCardModule } from '../../../../core/components/info-card/info-card.component';
|
||||
import {
|
||||
KpiInfo,
|
||||
Metric,
|
||||
} from '../../../../core/data-access/graphql/generated/generated';
|
||||
import { ManagerDashboardService } from '../../manager-dashboard.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manager-info-card',
|
||||
imports: [InfoCardModule, DecimalPipe],
|
||||
templateUrl: './manager-info-card.component.html',
|
||||
styleUrl: './manager-info-card.component.scss',
|
||||
})
|
||||
export class ManagerInfoCardComponent {
|
||||
private service = inject(ManagerDashboardService);
|
||||
|
||||
ordinanceType = input.required<string>();
|
||||
kpi = signal<KpiInfo | undefined>(undefined);
|
||||
metrics = signal<Metric[]>([]);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.service
|
||||
.fetchManagerKpi(this.ordinanceType())
|
||||
.pipe(
|
||||
tap((res) => this.metrics.set(res.additional)),
|
||||
tap((res) => this.kpi.set(res))
|
||||
)
|
||||
.subscribe();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
></dks-time-per-tour-summary>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="DiagrammType.BreakEven">
|
||||
<ng-container *ngSwitchCase="DiagrammType.Revenue">
|
||||
<dks-breakeven-metric
|
||||
class="col-span-3"
|
||||
[labelFormat]="getLabelFormatForMetricTimeScale(metricTimeScale)"
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
:host {
|
||||
@apply flex justify-center items-center;
|
||||
}
|
||||
|
|
@ -2,14 +2,17 @@ import { CommonModule } from '@angular/common';
|
|||
import { Component, Input } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { BehaviorSubject, map } from 'rxjs';
|
||||
import { summarizeMetrics } from '../../util/summarize-metrics';
|
||||
import { TimePerTourMetric } from '../../../../core/data-access/graphql/generated/generated';
|
||||
import { summarizeMetrics } from '../../util/summarize-metrics';
|
||||
|
||||
@Component({
|
||||
selector: 'dks-time-per-tour-summary',
|
||||
imports: [CommonModule, MatIconModule],
|
||||
templateUrl: './time-per-tour-summary.component.html',
|
||||
styleUrls: ['./time-per-tour-summary.component.css']
|
||||
selector: 'dks-time-per-tour-summary',
|
||||
imports: [CommonModule, MatIconModule],
|
||||
templateUrl: './time-per-tour-summary.component.html',
|
||||
styleUrls: ['./time-per-tour-summary.component.css'],
|
||||
host: {
|
||||
class: 'flex justify-center items-center',
|
||||
},
|
||||
})
|
||||
export class TimePerTourSummaryComponent {
|
||||
@Input() set metrics(value: TimePerTourMetric[]) {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
<div
|
||||
class="py-5 w-full flex flex-col justify-center items-center bg-white shadow rounded cursor-pointer transition ease-in-out hover:shadow-md hover:scale-105 duration-200"
|
||||
<!-- class="py-5 w-full flex flex-col justify-center items-center bg-white shadow rounded cursor-pointer transition ease-in-out hover:shadow-md hover:scale-105 duration-200" -->
|
||||
<mat-card
|
||||
appearance="raised"
|
||||
class="cursor-pointer transition ease-in-out hover:scale-105 duration-200"
|
||||
[ngClass]="{
|
||||
'border border-blue-400': selected
|
||||
'!border': selected
|
||||
}"
|
||||
style="border-color: var(--mat-sys-primary)"
|
||||
(click)="setSelectedViewType()"
|
||||
*ngIf="metricSummary$ | async as summary"
|
||||
>
|
||||
<span class="text-lg pb-4">{{ viewType }}</span>
|
||||
<mat-card-header class="!pb-2 justify-center">
|
||||
<mat-card-title>{{ viewType }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<!-- <span class="text-lg pb-4">{{ viewType }}</span> -->
|
||||
|
||||
<ng-container [ngSwitch]="currentMetric">
|
||||
<div
|
||||
class="flex flex-col items-end"
|
||||
*ngSwitchCase="DiagrammType.TimePerTour"
|
||||
>
|
||||
<mat-card-content class="!flex justify-center">
|
||||
@switch (currentMetric) { @case (DiagrammType.TimePerTour) {
|
||||
<div class="w-1/4 flex flex-col items-end">
|
||||
<div class="flex items-center pb-2" title="Touren Menge">
|
||||
{{ summary.tourAmount.toFixed(2) }}
|
||||
<mat-icon class="mx-2">local_shipping</mat-icon>
|
||||
|
|
@ -22,28 +26,28 @@
|
|||
<mat-icon class="mx-2">departure_board</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-end" *ngSwitchCase="DiagrammType.BreakEven">
|
||||
<div class="flex items-center pb-2" title="Deckungsbeitrag">
|
||||
{{ summary.breakEven | currency:'EUR' }}
|
||||
<mat-icon class="mx-2">account_balance</mat-icon>
|
||||
} @case (DiagrammType.Revenue) {
|
||||
<div class="w-1/4 flex flex-col items-end">
|
||||
<div class="flex items-center pb-2" title="Umsatz">
|
||||
{{ summary.revenue | currency : "EUR" : "symbol" : ".2" }}
|
||||
<mat-icon class="mx-2">euro</mat-icon>
|
||||
</div>
|
||||
<div class="flex items-center text-yellow-500" title="Erwarteter Deckungsbeitrag">
|
||||
{{ summary.forecastRevenue | currency:'EUR' }}
|
||||
<div class="flex items-center" title="Deckungsbeitrag">
|
||||
{{ summary.breakEven | currency : "EUR" }}
|
||||
<mat-icon class="mx-2">account_balance</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-end"
|
||||
*ngSwitchCase="DiagrammType.AverageRevenue"
|
||||
>
|
||||
} @case (DiagrammType.AverageRevenue) {
|
||||
<div class="w-1/4 flex flex-col items-end">
|
||||
<div class="flex items-center pb-2" title="Durchschnittlicher Umsatz">
|
||||
<span class="mr-1">Ø</span>
|
||||
{{ summary.averageRevenue | currency:'EUR' }}
|
||||
{{ summary.averageRevenue | currency : "EUR" }}
|
||||
</div>
|
||||
<div class="flex items-center" title="Zeit pro Tour Index">
|
||||
{{ calculateIndex(summary).toFixed(2) }}
|
||||
<span class="mx-2">Index</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
} }
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
|
|
|||
|
|
@ -2,22 +2,28 @@ import {
|
|||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
NgClass,
|
||||
NgIf,
|
||||
NgSwitch,
|
||||
NgSwitchCase,
|
||||
NgIf
|
||||
} from '@angular/common';
|
||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { BehaviorSubject, map } from 'rxjs';
|
||||
import { TimePerTourMetric } from '../../../../core/data-access/graphql/generated/generated';
|
||||
import { DashboardViewType, DiagrammType } from '../../types';
|
||||
import { summarizeMetrics } from '../../util/summarize-metrics';
|
||||
import { TimePerTourMetric } from '../../../../core/data-access/graphql/generated/generated';
|
||||
|
||||
@Component({
|
||||
selector: 'dks-view-type-selector',
|
||||
imports: [NgIf, NgSwitch, NgSwitchCase, NgClass, AsyncPipe, CurrencyPipe, MatIconModule],
|
||||
templateUrl: './view-type-selector.component.html',
|
||||
styleUrls: ['./view-type-selector.component.css']
|
||||
selector: 'dks-view-type-selector',
|
||||
imports: [
|
||||
NgIf,
|
||||
NgClass,
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
MatIconModule,
|
||||
MatCardModule,
|
||||
],
|
||||
templateUrl: './view-type-selector.component.html',
|
||||
styleUrls: ['./view-type-selector.component.css'],
|
||||
})
|
||||
export class ViewTypeSelectorComponent {
|
||||
@Input() viewType!: DashboardViewType;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</button>
|
||||
<div class="flex items-center">
|
||||
<span class="pr-2">
|
||||
{{ vm.selectedDate | date : 'dd.MM.yyyy' }}
|
||||
{{ vm.selectedDate | date : "dd.MM.yyyy" }}
|
||||
</span>
|
||||
<input
|
||||
[formControl]="selectedDate"
|
||||
|
|
@ -27,18 +27,18 @@
|
|||
</button>
|
||||
</div>
|
||||
<mat-chip-listbox (change)="changeSelectedType($event)">
|
||||
<mat-chip-option
|
||||
[value]="DiagrammType.Revenue"
|
||||
[selected]="vm.selectedMetric === DiagrammType.Revenue"
|
||||
>
|
||||
Umsatz
|
||||
</mat-chip-option>
|
||||
<mat-chip-option
|
||||
[value]="DiagrammType.TimePerTour"
|
||||
[selected]="vm.selectedMetric === DiagrammType.TimePerTour"
|
||||
>
|
||||
Kapazität pro Tour
|
||||
</mat-chip-option>
|
||||
<mat-chip-option
|
||||
[value]="DiagrammType.BreakEven"
|
||||
[selected]="vm.selectedMetric === DiagrammType.BreakEven"
|
||||
>
|
||||
Deckungsbeitrag
|
||||
</mat-chip-option>
|
||||
<mat-chip-option
|
||||
[value]="DiagrammType.AverageRevenue"
|
||||
[selected]="vm.selectedMetric === DiagrammType.AverageRevenue"
|
||||
|
|
@ -47,6 +47,16 @@
|
|||
</mat-chip-option>
|
||||
</mat-chip-listbox>
|
||||
</div>
|
||||
<!-- KPI Row -->
|
||||
<!-- <div
|
||||
class="my-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
|
||||
>
|
||||
<app-manager-info-card ordinanceType="BTW"></app-manager-info-card>
|
||||
<app-manager-info-card ordinanceType="TSW"></app-manager-info-card>
|
||||
<app-manager-info-card ordinanceType="KTW"></app-manager-info-card>
|
||||
<app-manager-info-card ordinanceType="ALL"></app-manager-info-card>
|
||||
</div> -->
|
||||
<!-- OrdinanceType Selector -->
|
||||
<div
|
||||
class="my-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
|
||||
>
|
||||
|
|
@ -79,62 +89,62 @@
|
|||
(viewTypeSelected)="setSelectedViewType(DashboardViewType.Gesamt)"
|
||||
></dks-view-type-selector>
|
||||
</div>
|
||||
<div
|
||||
<!-- Visualisation -->
|
||||
<mat-card
|
||||
appearance="outlined"
|
||||
class="grid grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
|
||||
[ngSwitch]="vm.selectedViewType"
|
||||
>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.BTW"
|
||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.btwMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'HH:mm'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.TSW"
|
||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.tswMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'HH:mm'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.KTW"
|
||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.ktwMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'HH:mm'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.Gesamt"
|
||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.combinedMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'HH:mm'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
</div>
|
||||
<mat-card-content>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.BTW"
|
||||
class="col-span-2 lg:col-span-4 w-full"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.btwMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'HH:mm'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.TSW"
|
||||
class="col-span-2 lg:col-span-4 w-full"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.tswMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'HH:mm'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.KTW"
|
||||
class="col-span-2 lg:col-span-4 w-full"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.ktwMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'HH:mm'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.Gesamt"
|
||||
class="col-span-2 lg:col-span-4 w-full"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.combinedMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'HH:mm'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #loadingTmpl>
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #viewTypeSelector let-typeAndSummary>
|
||||
{{ typeAndSummary.type }}
|
||||
</ng-template>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export class DayviewComponent {
|
|||
startWith(this.selectedDate.value),
|
||||
map((date) => (date?.getHours() === 0 ? addHours(date, 12) : date))
|
||||
);
|
||||
selectedMetric$ = new BehaviorSubject<DiagrammType>(DiagrammType.TimePerTour);
|
||||
selectedMetric$ = new BehaviorSubject<DiagrammType>(DiagrammType.Revenue);
|
||||
selectedViewType$ = new BehaviorSubject<DashboardViewType>(
|
||||
DashboardViewType.Gesamt
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,13 +15,15 @@ import {
|
|||
throwError,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
GetTourAmountPerTimeKpiManagerGQL,
|
||||
GetTimePerTourMetricForDayGQL,
|
||||
GetTimePerTourMetricForMonthGQL,
|
||||
TimePerTourUpdatedGQL,
|
||||
GetTourAmountPerTimeKpiManagerQuery,
|
||||
GetTimePerTourMetricForDayQuery,
|
||||
GetTimePerTourMetricForMonthGQL,
|
||||
GetTimePerTourMetricForMonthQuery,
|
||||
GetTourAmountPerTimeKpiManagerGQL,
|
||||
GetTourAmountPerTimeKpiManagerQuery,
|
||||
ManagerKpiGQL,
|
||||
ManagerKpiQuery,
|
||||
TimePerTourUpdatedGQL,
|
||||
} from '../../core/data-access/graphql/generated/generated';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
|
|
@ -35,6 +37,7 @@ export class ManagerDashboardService {
|
|||
private readonly getTimePerTourMetricForDayGql: GetTimePerTourMetricForDayGQL,
|
||||
private readonly getTimePerTourMetricForMonthGql: GetTimePerTourMetricForMonthGQL,
|
||||
private readonly timePerTourUpdatedGql: TimePerTourUpdatedGQL,
|
||||
private readonly managerKpiGql: ManagerKpiGQL,
|
||||
private readonly toast: HotToastService
|
||||
) {
|
||||
this.timePerTourUpdate$ = this.timePerTourUpdatedGql.subscribe().pipe(
|
||||
|
|
@ -50,6 +53,31 @@ export class ManagerDashboardService {
|
|||
);
|
||||
}
|
||||
|
||||
fetchManagerKpi(
|
||||
ordinanceType: string
|
||||
): Observable<ManagerKpiQuery['managerKpi']> {
|
||||
const managerKpiQuery = this.managerKpiGql.watch({
|
||||
ordinanceType,
|
||||
});
|
||||
|
||||
const update = this.timePerTourUpdate$.pipe(
|
||||
switchMap(() => managerKpiQuery.refetch()),
|
||||
startWith(undefined)
|
||||
);
|
||||
|
||||
return combineLatest([update, managerKpiQuery.valueChanges]).pipe(
|
||||
catchError((error) => {
|
||||
console.error(error);
|
||||
return throwError(() => error);
|
||||
}),
|
||||
retry({ delay: 1000, count: 3 }),
|
||||
map(([updatedData, firstData]) =>
|
||||
updatedData ? updatedData : firstData
|
||||
),
|
||||
map((response) => response.data.managerKpi)
|
||||
);
|
||||
}
|
||||
|
||||
getTimePerTourKPIByOrdinanceTypes(
|
||||
ordinanceType: string
|
||||
): Observable<GetTourAmountPerTimeKpiManagerQuery['toursPerTimeKPI']> {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
<nav mat-tab-nav-bar backgroundColor="accent" [tabPanel]="tabPanel">
|
||||
<a
|
||||
#link
|
||||
mat-tab-link
|
||||
*ngFor="let tab of tabs"
|
||||
[routerLink]="tab.route"
|
||||
[routerLinkActive]="'active'"
|
||||
[active]="link.elementRef.nativeElement.classList.contains('active')"
|
||||
>{{ tab.title }}</a
|
||||
>
|
||||
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||
@for (tab of tabs; track tab.route) {
|
||||
<a
|
||||
#link
|
||||
mat-tab-link
|
||||
[routerLink]="tab.route"
|
||||
[routerLinkActive]="'active'"
|
||||
[active]="link.elementRef.nativeElement.classList.contains('active')"
|
||||
>{{ tab.title }}</a
|
||||
>
|
||||
}
|
||||
</nav>
|
||||
<mat-tab-nav-panel #tabPanel>
|
||||
<router-outlet></router-outlet>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Component } from '@angular/core';
|
|||
@Component({
|
||||
selector: 'dks-manager-dashboard',
|
||||
templateUrl: './manager-dashboard.component.html',
|
||||
styleUrls: ['./manager-dashboard.component.css'],
|
||||
styleUrls: ['./manager-dashboard.component.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class ManagerDashboardComponent {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</button>
|
||||
<div class="flex items-center">
|
||||
<span class="pr-2">
|
||||
{{ vm.selectedDate | date : 'MM.yyyy' }}
|
||||
{{ vm.selectedDate | date : "MM.yyyy" }}
|
||||
</span>
|
||||
<input
|
||||
[formControl]="selectedDate"
|
||||
|
|
@ -33,18 +33,18 @@
|
|||
</div>
|
||||
|
||||
<mat-chip-listbox (change)="changeSelectedType($event)">
|
||||
<mat-chip-option
|
||||
[value]="DiagrammType.Revenue"
|
||||
[selected]="vm.selectedMetric === DiagrammType.Revenue"
|
||||
>
|
||||
Umsatz
|
||||
</mat-chip-option>
|
||||
<mat-chip-option
|
||||
[value]="DiagrammType.TimePerTour"
|
||||
[selected]="vm.selectedMetric === DiagrammType.TimePerTour"
|
||||
>
|
||||
Kapazität pro Tour
|
||||
</mat-chip-option>
|
||||
<mat-chip-option
|
||||
[value]="DiagrammType.BreakEven"
|
||||
[selected]="vm.selectedMetric === DiagrammType.BreakEven"
|
||||
>
|
||||
Deckungsbeitrag
|
||||
</mat-chip-option>
|
||||
<mat-chip-option
|
||||
[value]="DiagrammType.AverageRevenue"
|
||||
[selected]="vm.selectedMetric === DiagrammType.AverageRevenue"
|
||||
|
|
@ -85,55 +85,55 @@
|
|||
(viewTypeSelected)="setSelectedViewType(DashboardViewType.Gesamt)"
|
||||
></dks-view-type-selector>
|
||||
</div>
|
||||
<div
|
||||
<!-- Visualisation -->
|
||||
<mat-card
|
||||
appearance="outlined"
|
||||
class="grid grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
|
||||
[ngSwitch]="vm.selectedViewType"
|
||||
>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.BTW"
|
||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.btwMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'d.M.yy'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.TSW"
|
||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.tswMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'d.M.yy'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.KTW"
|
||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.ktwMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'d.M.yy'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
<div
|
||||
*ngSwitchCase="DashboardViewType.Gesamt"
|
||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.combinedMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'d.M.yy'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
</div>
|
||||
<mat-card-content>
|
||||
@switch (vm.selectedViewType) { @case (DashboardViewType.BTW) {
|
||||
<div class="col-span-2 lg:col-span-4 w-full">
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.btwMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'d.M.yy'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
} @case (DashboardViewType.TSW) {
|
||||
<div class="col-span-2 lg:col-span-4 w-full">
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.tswMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'d.M.yy'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
} @case (DashboardViewType.KTW) {
|
||||
<div
|
||||
class="col-span-2 lg:col-span-4 w-full rounded"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.ktwMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'d.M.yy'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
} @case (DashboardViewType.Gesamt) {
|
||||
<div
|
||||
class="col-span-2 lg:col-span-4 w-full"
|
||||
>
|
||||
<dks-metric-summary
|
||||
[metrics]="vm.combinedMetrics"
|
||||
[metricTimeScale]="metricTimeScale"
|
||||
[selectedMetric]="vm.selectedMetric"
|
||||
[labelFormat]="'d.M.yy'"
|
||||
></dks-metric-summary>
|
||||
</div>
|
||||
} }
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #loadingTmpl>
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ import {
|
|||
import { mergeMetrics } from '../util/merge-metric';
|
||||
|
||||
@Component({
|
||||
selector: 'dks-monthview',
|
||||
templateUrl: './monthview.component.html',
|
||||
styleUrls: ['./monthview.component.css'],
|
||||
standalone: false
|
||||
selector: 'dks-monthview',
|
||||
templateUrl: './monthview.component.html',
|
||||
styleUrls: ['./monthview.component.css'],
|
||||
standalone: false,
|
||||
})
|
||||
export class MonthviewComponent {
|
||||
OrdinanceTypes = OrdinanceTypes;
|
||||
|
|
@ -36,7 +36,7 @@ export class MonthviewComponent {
|
|||
selectedDate$ = this.selectedDate.valueChanges.pipe(
|
||||
startWith(this.selectedDate.value)
|
||||
);
|
||||
selectedMetric$ = new BehaviorSubject<DiagrammType>(DiagrammType.TimePerTour);
|
||||
selectedMetric$ = new BehaviorSubject<DiagrammType>(DiagrammType.Revenue);
|
||||
selectedViewType$ = new BehaviorSubject<DashboardViewType>(
|
||||
DashboardViewType.Gesamt
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,31 +5,31 @@
|
|||
<div class="flex flex-wrap md:flex-nowrap">
|
||||
<!-- KPI -->
|
||||
<dks-time-per-tour-kpi
|
||||
class="w-full md:w-1/3 m-2 rounded cursor-pointer transition ease-in-out hover:scale-105 duration-200"
|
||||
class="w-full md:w-1/3 m-2 cursor-pointer rounded-xl transition ease-in-out hover:scale-105 duration-200"
|
||||
[ngClass]="{
|
||||
'border border-blue-400':
|
||||
vm.selectedOrdinanceType === OrdinanceTypes.BTW
|
||||
'border': vm.selectedOrdinanceType === OrdinanceTypes.BTW
|
||||
}"
|
||||
style="border-color: var(--mat-sys-primary);"
|
||||
[text]="OrdinanceTypes.BTW"
|
||||
[kpis]="vm.btwKPI.summaries"
|
||||
(click)="setSelectedOrdinanceType(OrdinanceTypes.BTW)"
|
||||
></dks-time-per-tour-kpi>
|
||||
<dks-time-per-tour-kpi
|
||||
class="w-full md:w-1/3 m-2 rounded cursor-pointer transition ease-in-out hover:scale-105 duration-200"
|
||||
class="w-full md:w-1/3 m-2 cursor-pointer rounded-xl transition ease-in-out hover:scale-105 duration-200"
|
||||
[ngClass]="{
|
||||
'border border-blue-400':
|
||||
vm.selectedOrdinanceType === OrdinanceTypes.TSW
|
||||
'border': vm.selectedOrdinanceType === OrdinanceTypes.TSW
|
||||
}"
|
||||
style="border-color: var(--mat-sys-primary);"
|
||||
[text]="OrdinanceTypes.TSW"
|
||||
[kpis]="vm.tswKPI.summaries"
|
||||
(click)="setSelectedOrdinanceType(OrdinanceTypes.TSW)"
|
||||
></dks-time-per-tour-kpi>
|
||||
<dks-time-per-tour-kpi
|
||||
class="w-full md:w-1/3 m-2 rounded cursor-pointer transition ease-in-out hover:scale-105 duration-200"
|
||||
class="w-full md:w-1/3 m-2 cursor-pointer rounded-xl transition ease-in-out hover:scale-105 duration-200"
|
||||
[ngClass]="{
|
||||
'border border-blue-400':
|
||||
vm.selectedOrdinanceType === OrdinanceTypes.KTW
|
||||
'border': vm.selectedOrdinanceType === OrdinanceTypes.KTW
|
||||
}"
|
||||
style="border-color: var(--mat-sys-primary);"
|
||||
[text]="OrdinanceTypes.KTW"
|
||||
[kpis]="vm.ktwKPI.summaries"
|
||||
(click)="setSelectedOrdinanceType(OrdinanceTypes.KTW)"
|
||||
|
|
@ -40,26 +40,30 @@
|
|||
*ngIf="vm.selectedOrdinanceType"
|
||||
>
|
||||
<!-- Data Table -->
|
||||
<ng-container [ngSwitch]="vm.selectedOrdinanceType">
|
||||
<dks-time-per-tour-by-car
|
||||
[columns]="vm.btwKPI.cars.columns"
|
||||
[data]="vm.btwKPI.cars.data"
|
||||
class="w-full"
|
||||
*ngSwitchCase="OrdinanceTypes.BTW"
|
||||
></dks-time-per-tour-by-car>
|
||||
<dks-time-per-tour-by-car
|
||||
[columns]="vm.tswKPI.cars.columns"
|
||||
[data]="vm.tswKPI.cars.data"
|
||||
class="w-full"
|
||||
*ngSwitchCase="OrdinanceTypes.TSW"
|
||||
></dks-time-per-tour-by-car>
|
||||
<dks-time-per-tour-by-car
|
||||
[columns]="vm.ktwKPI.cars.columns"
|
||||
[data]="vm.ktwKPI.cars.data"
|
||||
class="w-full"
|
||||
*ngSwitchCase="OrdinanceTypes.KTW"
|
||||
></dks-time-per-tour-by-car>
|
||||
</ng-container>
|
||||
<mat-card appearance="outlined" class="w-full">
|
||||
<mat-card-content>
|
||||
<ng-container [ngSwitch]="vm.selectedOrdinanceType">
|
||||
<dks-time-per-tour-by-car
|
||||
[columns]="vm.btwKPI.cars.columns"
|
||||
[data]="vm.btwKPI.cars.data"
|
||||
class="w-full"
|
||||
*ngSwitchCase="OrdinanceTypes.BTW"
|
||||
></dks-time-per-tour-by-car>
|
||||
<dks-time-per-tour-by-car
|
||||
[columns]="vm.tswKPI.cars.columns"
|
||||
[data]="vm.tswKPI.cars.data"
|
||||
class="w-full"
|
||||
*ngSwitchCase="OrdinanceTypes.TSW"
|
||||
></dks-time-per-tour-by-car>
|
||||
<dks-time-per-tour-by-car
|
||||
[columns]="vm.ktwKPI.cars.columns"
|
||||
[data]="vm.ktwKPI.cars.data"
|
||||
class="w-full"
|
||||
*ngSwitchCase="OrdinanceTypes.KTW"
|
||||
></dks-time-per-tour-by-car>
|
||||
</ng-container>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #loadingTmpl>
|
||||
|
|
|
|||
|
|
@ -37,8 +37,6 @@
|
|||
<tr
|
||||
mat-row
|
||||
*matRowDef="let carData; columns: displayedColumns"
|
||||
class="[&_td]:border-b-0"
|
||||
[ngClass]="{'hover:bg-gray-100': carData !== expandedCarData}"
|
||||
(click)="setExpandedCarData(carData)"
|
||||
></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="!h-0"></tr>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<mat-card>
|
||||
<mat-card appearance="raised">
|
||||
<mat-card-header class="flex justify-center">
|
||||
<mat-card-title>{{ text }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
|
|
@ -14,7 +14,8 @@
|
|||
class="w-1/3 flex flex-col justify-center items-center"
|
||||
>
|
||||
<div
|
||||
class="font-bold border-blue-400"
|
||||
class="font-bold"
|
||||
style="border-color: var(--mat-sys-primary);"
|
||||
[ngClass]="{ 'border-b-2': (currentTimeSlot$ | async) === idx }"
|
||||
>
|
||||
{{ kpi.interval.start | date: 'HH:mm' }} -
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export enum OrdinanceTypes {
|
|||
|
||||
export enum DiagrammType {
|
||||
TimePerTour = 'TimePerTour',
|
||||
BreakEven = 'BreakEven',
|
||||
Revenue = 'Revenue',
|
||||
AverageRevenue = 'AverageRevenue',
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,17 +18,21 @@ import {
|
|||
import CategoryColors from './category-colors';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-item',
|
||||
templateUrl: './ticket-item.component.html',
|
||||
styles: [
|
||||
`
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-item',
|
||||
templateUrl: './ticket-item.component.html',
|
||||
host: {
|
||||
class:
|
||||
'flex flex-col rounded-sm border-l-2 border-r-8 p-2 m-2 bg-white cursor-pointer',
|
||||
},
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
@apply flex flex-col rounded border-l-2 border-r-8 p-2 m-2 bg-white cursor-pointer;
|
||||
box-shadow: var(--mat-sys-level2);
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [MatIconModule, MatBadgeModule, NgIf]
|
||||
],
|
||||
imports: [MatIconModule, MatBadgeModule, NgIf],
|
||||
})
|
||||
export class TicketItemComponent implements OnInit {
|
||||
@Input() ticket!: Ticket & { tours: Tour[] };
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { AsyncPipe, CurrencyPipe, NgIf } from '@angular/common';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { TicketStateMeta } from '../../../core/data-access/graphql/generated/generated';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-lane-footer',
|
||||
template: `<span *ngIf="withTicketCount" class="text-left text-xs">
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-lane-footer',
|
||||
template: `<span *ngIf="withTicketCount" class="text-left text-xs">
|
||||
{{ ticketStateMeta.count }} Tickets
|
||||
</span>
|
||||
<span *ngIf="withApprovalRequiredCount" class="text-left text-xs">
|
||||
|
|
@ -21,14 +21,11 @@ import { TicketStateMeta } from '../../../core/data-access/graphql/generated/gen
|
|||
<span *ngIf="withTdNotCompanyCount" class="text-left text-xs">
|
||||
{{ ticketStateMeta.tdNotCompanyCount }} davon fehlender Transportschein
|
||||
</span> `,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
@apply flex flex-col items-center justify-center p-2;
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [NgIf, AsyncPipe, CurrencyPipe]
|
||||
host: {
|
||||
class: 'flex flex-col items-center justify-center p-2',
|
||||
},
|
||||
styles: [``],
|
||||
imports: [NgIf],
|
||||
})
|
||||
export class TicketLaneFooterComponent {
|
||||
@Input() ticketStateMeta!: TicketStateMeta;
|
||||
|
|
|
|||
|
|
@ -33,10 +33,14 @@ import { TicketLaneHeaderFilterComponent } from './ticket-lane-header-filter.com
|
|||
[state]="state"
|
||||
(filterChange)="filterChanged($event)"
|
||||
></dks-ticket-lane-header-filter>`,
|
||||
host: {
|
||||
class: 'flex items-center justify-between p-2',
|
||||
},
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
@apply flex items-center justify-between p-2 bg-gray-50;
|
||||
color: var(--mat-sys-on-secondary-container);
|
||||
background-color: var(--mat-sys-secondary-container);
|
||||
}
|
||||
`,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
<!-- Key Figures -->
|
||||
<dks-ticket-lane-header
|
||||
class="shadow"
|
||||
[state]="ticketState"
|
||||
|
|
@ -17,7 +16,7 @@
|
|||
>
|
||||
</dks-ticket-item>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
<ng-container *ngIf="ticketStateMeta$ | async as ticketStateMeta">
|
||||
<!-- <ng-container *ngIf="ticketStateMeta$ | async as ticketStateMeta">
|
||||
<dks-ticket-lane-footer
|
||||
*dksRole="['manager', 'admin']"
|
||||
[ticketStateMeta]="ticketStateMeta"
|
||||
|
|
@ -33,5 +32,5 @@
|
|||
*dksRole="['controlcenter']"
|
||||
[ticketStateMeta]="ticketStateMeta"
|
||||
></dks-ticket-lane-footer
|
||||
></ng-container>
|
||||
></ng-container> -->
|
||||
</ng-container>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {
|
|||
CdkVirtualScrollViewport,
|
||||
ScrollingModule,
|
||||
} from '@angular/cdk/scrolling';
|
||||
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
|
|
@ -16,14 +16,11 @@ import {
|
|||
Observable,
|
||||
combineLatest,
|
||||
debounceTime,
|
||||
filter,
|
||||
map,
|
||||
mergeMap,
|
||||
scan,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
import { RoleDirective } from '../../../core/components/ms-authentication/role.directive';
|
||||
import {
|
||||
Ticket,
|
||||
TicketFilterArgs,
|
||||
|
|
@ -33,32 +30,31 @@ import {
|
|||
import { TicketItemComponent } from '../ticket-item/ticket-item.component';
|
||||
import { TicketViewComponent } from '../ticket-view/ticket-view.component';
|
||||
import { TicketsService } from '../tickets.service';
|
||||
import { TicketLaneFooterComponent } from './ticket-lane-footer.component';
|
||||
import { TicketLaneHeaderComponent } from './ticket-lane-header.component';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-lane',
|
||||
templateUrl: './ticket-lane.component.html',
|
||||
styles: [
|
||||
`
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 'dks-ticket-lane',
|
||||
templateUrl: './ticket-lane.component.html',
|
||||
host: {
|
||||
class: 'rounded-sm h-full w-full grid',
|
||||
},
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
@apply rounded bg-gray-200 h-full w-full grid;
|
||||
box-shadow: var(--mat-sys-level1);
|
||||
background-color: var(--mat-sys-surface-container-low);
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [
|
||||
TicketLaneHeaderComponent,
|
||||
TicketLaneFooterComponent,
|
||||
TicketItemComponent,
|
||||
TicketViewComponent,
|
||||
NgIf,
|
||||
NgFor,
|
||||
AsyncPipe,
|
||||
RoleDirective,
|
||||
ScrollingModule,
|
||||
]
|
||||
],
|
||||
imports: [
|
||||
TicketLaneHeaderComponent,
|
||||
TicketItemComponent,
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
ScrollingModule,
|
||||
],
|
||||
})
|
||||
export class TicketLaneComponent {
|
||||
private dialog = inject(DialogService);
|
||||
|
|
@ -87,12 +83,12 @@ export class TicketLaneComponent {
|
|||
|
||||
cursor$ = new BehaviorSubject<string>('');
|
||||
tickets$: Observable<(Ticket & { tours: Tour[] })[]>;
|
||||
ticketStateMeta$ = this.ticketState$.pipe(
|
||||
filter(Boolean),
|
||||
switchMap((ticketState) =>
|
||||
this.ticketsService.fetchTicketStateMeta(ticketState)
|
||||
)
|
||||
);
|
||||
// ticketStateMeta$ = this.ticketState$.pipe(
|
||||
// filter(Boolean),
|
||||
// switchMap((ticketState) =>
|
||||
// this.ticketsService.fetchTicketStateMeta(ticketState)
|
||||
// )
|
||||
// );
|
||||
totalCount$ = new BehaviorSubject<number>(0);
|
||||
|
||||
constructor() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
@if (kpi()?.main?.label) {
|
||||
<app-info-card
|
||||
[chartValue]="kpi()?.main?.value!"
|
||||
[chartLabel]="kpi()?.main?.label!"
|
||||
[title]="title()"
|
||||
>
|
||||
<app-info-card-content
|
||||
[value]="kpi()?.secondary?.value | number : '.0-1' : 'de'"
|
||||
[label]="kpi()?.secondary?.label"
|
||||
></app-info-card-content>
|
||||
@for (metric of kpi()?.additional; track $index) {
|
||||
<app-info-card-footer
|
||||
[value]="metric.value | number : '.0-2' : 'de'"
|
||||
[label]="metric.label"
|
||||
>
|
||||
</app-info-card-footer>
|
||||
}
|
||||
</app-info-card>
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TicketStateInfoCardComponent } from './ticket-state-info-card.component';
|
||||
|
||||
describe('TicketStateInfoCardComponent', () => {
|
||||
let component: TicketStateInfoCardComponent;
|
||||
let fixture: ComponentFixture<TicketStateInfoCardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TicketStateInfoCardComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TicketStateInfoCardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { DecimalPipe } from '@angular/common';
|
||||
import { Component, effect, inject, input, signal } from '@angular/core';
|
||||
import { tap } from 'rxjs';
|
||||
import { InfoCardModule } from '../../../core/components/info-card/info-card.component';
|
||||
import {
|
||||
KpiInfo,
|
||||
Metric,
|
||||
TicketValidationState,
|
||||
} from '../../../core/data-access/graphql/generated/generated';
|
||||
import { TicketsService } from '../tickets.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-ticket-state-info-card',
|
||||
imports: [InfoCardModule, DecimalPipe],
|
||||
templateUrl: './ticket-state-info-card.component.html',
|
||||
styleUrl: './ticket-state-info-card.component.scss',
|
||||
})
|
||||
export class TicketStateInfoCardComponent {
|
||||
private service = inject(TicketsService);
|
||||
|
||||
ticketState = input.required<TicketValidationState>();
|
||||
kpi = signal<KpiInfo | undefined>(undefined);
|
||||
metrics = signal<Metric[]>([]);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.service
|
||||
.fetchTicketStateKpi(this.ticketState())
|
||||
.pipe(
|
||||
tap((res) => this.metrics.set(res.additional)),
|
||||
tap((res) => this.kpi.set(res))
|
||||
)
|
||||
.subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
title(): string | undefined {
|
||||
switch (this.ticketState()) {
|
||||
case TicketValidationState.Created:
|
||||
return 'Prüfung Daten';
|
||||
case TicketValidationState.UserInteractionRequired:
|
||||
return 'Prüfung Genehmigungen';
|
||||
case TicketValidationState.TdUncertain:
|
||||
return 'Prüfung Transportscheine';
|
||||
case TicketValidationState.DocumentsMissing:
|
||||
return 'Prüfung Dokumente';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -86,7 +86,6 @@ import {
|
|||
imports: [
|
||||
MatListModule,
|
||||
MatIconModule,
|
||||
NgClass,
|
||||
NgIf,
|
||||
NgForOf,
|
||||
NgSwitch,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue