(web): dashboard enhancements
This commit is contained in:
parent
d8a2930247
commit
44f627499a
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"@tailwindcss/postcss": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,8 +36,9 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"@angular/material/prebuilt-themes/azure-blue.css",
|
"src/m3-theme.scss",
|
||||||
"node_modules/@ngxpert/hot-toast/src/styles/styles.css",
|
"node_modules/@ngxpert/hot-toast/src/styles/styles.css",
|
||||||
|
"src/tailwind.css",
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"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",
|
"start": "ng serve",
|
||||||
"build": "ng build -c production",
|
"build": "ng build -c production",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test"
|
"test": "ng test",
|
||||||
|
"gql-generate": "graphql-codegen --config codegen.ts"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^19.2.6",
|
"@angular/animations": "^19.2.6",
|
||||||
"@angular/cdk": "^18.1.1",
|
"@angular/cdk": "^19.2.9",
|
||||||
"@angular/common": "^19.2.6",
|
"@angular/common": "^19.2.6",
|
||||||
"@angular/compiler": "^19.2.6",
|
"@angular/compiler": "^19.2.6",
|
||||||
"@angular/core": "^19.2.6",
|
"@angular/core": "^19.2.6",
|
||||||
"@angular/forms": "^19.2.6",
|
"@angular/forms": "^19.2.6",
|
||||||
"@angular/material": "^18.1.1",
|
"@angular/material": "^19.2.9",
|
||||||
"@angular/material-date-fns-adapter": "^18.1.1",
|
"@angular/material-date-fns-adapter": "^19.2.9",
|
||||||
"@angular/platform-browser": "^19.2.6",
|
"@angular/platform-browser": "^19.2.6",
|
||||||
"@angular/platform-browser-dynamic": "^19.2.6",
|
"@angular/platform-browser-dynamic": "^19.2.6",
|
||||||
"@angular/router": "^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-angular": "^3.0.22",
|
||||||
"@azure/msal-browser": "^3.19.1",
|
"@azure/msal-browser": "^3.19.1",
|
||||||
"@grafana/faro-web-sdk": "^1.15.0",
|
"@grafana/faro-web-sdk": "^1.15.0",
|
||||||
|
|
@ -33,16 +34,17 @@
|
||||||
"@opentelemetry/auto-instrumentations-web": "^0.45.1",
|
"@opentelemetry/auto-instrumentations-web": "^0.45.1",
|
||||||
"@opentelemetry/context-zone": "^1.30.1",
|
"@opentelemetry/context-zone": "^1.30.1",
|
||||||
"@opentelemetry/core": "^2.0.0",
|
"@opentelemetry/core": "^2.0.0",
|
||||||
"@opentelemetry/exporter-trace-otlp-http": "^0.57.2",
|
"@opentelemetry/exporter-trace-otlp-http": "^0.48.0",
|
||||||
"@opentelemetry/instrumentation": "^0.57.2",
|
"@opentelemetry/instrumentation": "^0.48.0",
|
||||||
"@opentelemetry/instrumentation-document-load": "^0.44.1",
|
"@opentelemetry/instrumentation-document-load": "^0.44.1",
|
||||||
|
"@opentelemetry/propagator-b3": "^1.30.1",
|
||||||
"@opentelemetry/resources": "^1.30.1",
|
"@opentelemetry/resources": "^1.30.1",
|
||||||
"@opentelemetry/sdk-trace-web": "^1.30.1",
|
"@opentelemetry/sdk-trace-web": "^1.30.1",
|
||||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||||
"apollo-angular": "^7.0.2",
|
"apollo-angular": "^10.0.3",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"graphql": "^16",
|
"graphql": "^16.11.0",
|
||||||
"graphql-ws": "^5.16.0",
|
"graphql-ws": "^5.16.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
|
|
@ -53,6 +55,12 @@
|
||||||
"@angular/cli": "^19.2.7",
|
"@angular/cli": "^19.2.7",
|
||||||
"@angular/compiler-cli": "^19.2.6",
|
"@angular/compiler-cli": "^19.2.6",
|
||||||
"@angular/localize": "^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",
|
"@types/jasmine": "~5.1.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"jasmine-core": "~5.1.0",
|
"jasmine-core": "~5.1.0",
|
||||||
|
|
@ -62,7 +70,7 @@
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.39",
|
||||||
"tailwindcss": "^3.4.6",
|
"tailwindcss": "^4.1.4",
|
||||||
"typescript": "~5.5.2"
|
"typescript": "~5.5.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ import { GlobalErrorHandler } from './core/telemetry/global-error-handler';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideAppInitializer(() => {
|
// provideAppInitializer(() => {
|
||||||
const initializerFn = faroInitializer();
|
// const initializerFn = faroInitializer();
|
||||||
return initializerFn();
|
// return initializerFn();
|
||||||
}),
|
// }),
|
||||||
{
|
{
|
||||||
provide: ErrorHandler,
|
provide: ErrorHandler,
|
||||||
useClass: GlobalErrorHandler,
|
useClass: GlobalErrorHandler,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,18 @@ import { MsalGuard } from '@azure/msal-angular';
|
||||||
import { roleGuard } from './core/components/ms-authentication/role.guard';
|
import { roleGuard } from './core/components/ms-authentication/role.guard';
|
||||||
|
|
||||||
export const routes: Routes = [
|
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',
|
path: 'control-center',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<mat-sidenav-container class="h-full">
|
<mat-sidenav-container class="h-full">
|
||||||
<mat-sidenav
|
<mat-sidenav
|
||||||
#drawer
|
#drawer
|
||||||
class="w-52"
|
class="w-24"
|
||||||
fixedInViewport
|
fixedInViewport
|
||||||
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
|
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
|
||||||
[mode]="(isHandset$ | async) ? 'over' : 'side'"
|
[mode]="(isHandset$ | async) ? 'over' : 'side'"
|
||||||
|
|
@ -9,27 +9,33 @@
|
||||||
>
|
>
|
||||||
<mat-toolbar color="primary">Navigation</mat-toolbar>
|
<mat-toolbar color="primary">Navigation</mat-toolbar>
|
||||||
<mat-nav-list>
|
<mat-nav-list>
|
||||||
|
<a
|
||||||
|
mat-list-item
|
||||||
|
routerLink="/home"
|
||||||
|
routerLinkActive="nav-link-active"
|
||||||
|
>Home</a
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
mat-list-item
|
mat-list-item
|
||||||
routerLink="/control-center"
|
routerLink="/control-center"
|
||||||
routerLinkActive="!bg-yellow-400"
|
routerLinkActive="nav-link-active"
|
||||||
>Dashboard Leitstelle</a
|
>Dashboard Leitstelle</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
*dksRole="['manager', 'admin']"
|
*dksRole="['manager', 'admin']"
|
||||||
mat-list-item
|
mat-list-item
|
||||||
routerLink="/managers"
|
routerLink="/managers"
|
||||||
routerLinkActive="!bg-yellow-400"
|
routerLinkActive="nav-link-active"
|
||||||
>Dashboard Manager</a
|
>Dashboard Manager</a
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
*dksRole="['manager', 'admin']"
|
*dksRole="['manager', 'admin']"
|
||||||
mat-list-item
|
mat-list-item
|
||||||
routerLink="/accounting"
|
routerLink="/accounting"
|
||||||
routerLinkActive="!bg-yellow-400"
|
routerLinkActive="nav-link-active"
|
||||||
>Abrechnung</a
|
>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
|
>Ticket System</a
|
||||||
>
|
>
|
||||||
</mat-nav-list>
|
</mat-nav-list>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,16 @@
|
||||||
:root {
|
@use '@angular/material' as mat;
|
||||||
--mat-toolbar-container-background-color: #17253D;
|
|
||||||
|
: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,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
selector: 'dks-avatar',
|
selector: 'dks-avatar',
|
||||||
template: `<div
|
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"
|
[matMenuTriggerFor]="menu"
|
||||||
|
style="background-color: var(--mat-sys-primary-container); color: var(--mat-sys-on-primary-container)"
|
||||||
>
|
>
|
||||||
{{ userInitials$ | async }}
|
{{ userInitials$ | async }}
|
||||||
</div>
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,12 +12,22 @@ export interface ConfirmDialogData {
|
||||||
<div class="text-lg font-bold pb-2">{{ ref.data.title }}</div>
|
<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="pb-4 border-b border-gray-400">{{ ref.data.body }}</div>
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<button mat-stroked-button class="mr-2" dialogClose>Abbrechen</button>
|
<button mat-button class="mr-2" dialogClose>Abbrechen</button>
|
||||||
<button mat-stroked-button color="warn" [dialogClose]="true">Bestätigen</button>
|
<button mat-button class="warn-btn" [dialogClose]="true">Bestätigen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
|
styles: `@use "@angular/material" as mat;
|
||||||
|
|
||||||
|
.warn-btn {
|
||||||
|
@include mat.button-overrides(
|
||||||
|
(
|
||||||
|
text-label-text-color: var(--mat-sys-error)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
`,
|
||||||
imports: [MatButtonModule, DialogCloseDirective],
|
imports: [MatButtonModule, DialogCloseDirective],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ConfirmDialogComponent {
|
export class ConfirmDialogComponent {
|
||||||
ref: DialogRef<ConfirmDialogData> = inject(DialogRef);
|
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 { MsalBroadcastService, MsalService } from '@azure/msal-angular';
|
||||||
import { AccountInfo, InteractionStatus } from '@azure/msal-browser';
|
import { AccountInfo, InteractionStatus } from '@azure/msal-browser';
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,12 +20,14 @@ export class AuthService implements OnDestroy {
|
||||||
activeAccount$ = this.account$.pipe(
|
activeAccount$ = this.account$.pipe(
|
||||||
tap((account) =>
|
tap((account) =>
|
||||||
this.msalService.instance.setActiveAccount(account ?? null)
|
this.msalService.instance.setActiveAccount(account ?? null)
|
||||||
)
|
),
|
||||||
|
tap((account) => this.activeAccount.set(account))
|
||||||
);
|
);
|
||||||
|
activeAccount = signal<AccountInfo | undefined>(undefined);
|
||||||
roles$ = this.activeAccount$.pipe(
|
roles$ = this.activeAccount$.pipe(
|
||||||
filter(Boolean),
|
filter(Boolean),
|
||||||
map((account) => account?.idTokenClaims?.roles),
|
map((account) => account?.idTokenClaims?.roles),
|
||||||
shareReplay(1),
|
shareReplay(1)
|
||||||
);
|
);
|
||||||
|
|
||||||
msalService = inject(MsalService);
|
msalService = inject(MsalService);
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,14 @@ import { EChartsOption } from 'echarts';
|
||||||
import { filter, interval, map, Observable, Subscription } from 'rxjs';
|
import { filter, interval, map, Observable, Subscription } from 'rxjs';
|
||||||
import * as echarts from 'echarts/core';
|
import * as echarts from 'echarts/core';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
import waldenTheme from './walden.theme.json';
|
||||||
|
import westerosTheme from './westeros.theme.json';
|
||||||
|
|
||||||
|
const CUSTOM_THEME_TOKEN = 'customTheme';
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[dksEcharts]',
|
selector: '[dksEcharts]',
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class EchartsDirective implements OnInit, OnDestroy, OnChanges {
|
export class EchartsDirective implements OnInit, OnDestroy, OnChanges {
|
||||||
@Input() options?: EChartsOption;
|
@Input() options?: EChartsOption;
|
||||||
|
|
@ -38,7 +42,11 @@ export class EchartsDirective implements OnInit, OnDestroy, OnChanges {
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
echarts.use([...this.extentions, CanvasRenderer]);
|
echarts.use([...this.extentions, CanvasRenderer]);
|
||||||
this.echartsInstance = echarts.init(this._el.nativeElement, this.theme, {
|
echarts.registerTheme(CUSTOM_THEME_TOKEN, westerosTheme.theme);
|
||||||
|
this.echartsInstance = echarts.init(
|
||||||
|
this._el.nativeElement,
|
||||||
|
CUSTOM_THEME_TOKEN,
|
||||||
|
{
|
||||||
width:
|
width:
|
||||||
this._el.nativeElement.clientWidth === 0
|
this._el.nativeElement.clientWidth === 0
|
||||||
? this.defaultWidth
|
? this.defaultWidth
|
||||||
|
|
@ -47,7 +55,8 @@ export class EchartsDirective implements OnInit, OnDestroy, OnChanges {
|
||||||
this._el.nativeElement.clientHeight === 0
|
this._el.nativeElement.clientHeight === 0
|
||||||
? this.defaultHeight
|
? this.defaultHeight
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
this.chartInit.emit(this.echartsInstance);
|
this.chartInit.emit(this.echartsInstance);
|
||||||
this.setParams();
|
this.setParams();
|
||||||
if (this.isResizable) {
|
if (this.isResizable) {
|
||||||
|
|
@ -93,7 +102,7 @@ export class EchartsDirective implements OnInit, OnDestroy, OnChanges {
|
||||||
|
|
||||||
private getWidthSensor(
|
private getWidthSensor(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
periodicityInMilliseconds: number = 2000
|
periodicityInMilliseconds: number = 1000
|
||||||
): Observable<number> {
|
): Observable<number> {
|
||||||
let oldWidth = element.clientWidth;
|
let oldWidth = element.clientWidth;
|
||||||
return interval(periodicityInMilliseconds).pipe(
|
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']>;
|
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 = {
|
export type Interval = {
|
||||||
__typename?: 'Interval';
|
__typename?: 'Interval';
|
||||||
end: Scalars['DateTime']['output'];
|
end: Scalars['DateTime']['output'];
|
||||||
|
|
@ -108,6 +121,19 @@ export type KpiMetadata = {
|
||||||
tourAmount: Scalars['Float']['output'];
|
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 = {
|
export type Mutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
acceptAnomaly: Anomaly;
|
acceptAnomaly: Anomaly;
|
||||||
|
|
@ -175,19 +201,19 @@ export type Query = {
|
||||||
anomalies: PaginatedAnomaly;
|
anomalies: PaginatedAnomaly;
|
||||||
anomaliesProto: Array<Anomaly>;
|
anomaliesProto: Array<Anomaly>;
|
||||||
anomaly: Anomaly;
|
anomaly: Anomaly;
|
||||||
anomalyAmountByType: Array<Report>;
|
|
||||||
averageAnomalyResolutionTimeReport: Array<Report>;
|
|
||||||
groupedAnomalies: PaginatedAnomalyGroup;
|
groupedAnomalies: PaginatedAnomalyGroup;
|
||||||
|
homeDashboardKpis: HomeDashboardKpIs;
|
||||||
|
managerKpi: KpiInfo;
|
||||||
searchTickets: Array<Ticket>;
|
searchTickets: Array<Ticket>;
|
||||||
ticket: Ticket;
|
ticket: Ticket;
|
||||||
ticketStateKeyFigures: TicketStateKeyFigures;
|
ticketStateKeyFigures: TicketStateKeyFigures;
|
||||||
|
ticketStateKpi: KpiInfo;
|
||||||
ticketStateMeta: TicketStateMeta;
|
ticketStateMeta: TicketStateMeta;
|
||||||
tickets: PaginatedTicket;
|
tickets: PaginatedTicket;
|
||||||
timePerTourMetricForDay: Array<TimePerTourMetric>;
|
timePerTourMetricForDay: Array<TimePerTourMetric>;
|
||||||
timePerTourMetricForMonth: Array<TimePerTourMetric>;
|
timePerTourMetricForMonth: Array<TimePerTourMetric>;
|
||||||
tour: Tour;
|
tourCostBreakdown: TourCostBreakdown;
|
||||||
toursPerTimeKPI: Array<TimePerTourKpiSummary>;
|
toursPerTimeKPI: Array<TimePerTourKpiSummary>;
|
||||||
upcomingTourNotificationReport: Array<Report>;
|
|
||||||
upcomingTours: Array<Tour>;
|
upcomingTours: Array<Tour>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -222,6 +248,11 @@ export type QueryGroupedAnomaliesArgs = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryManagerKpiArgs = {
|
||||||
|
ordinanceType: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QuerySearchTicketsArgs = {
|
export type QuerySearchTicketsArgs = {
|
||||||
query: Scalars['String']['input'];
|
query: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
@ -237,6 +268,11 @@ export type QueryTicketStateKeyFiguresArgs = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryTicketStateKpiArgs = {
|
||||||
|
state: TicketValidationState;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryTicketStateMetaArgs = {
|
export type QueryTicketStateMetaArgs = {
|
||||||
state: TicketValidationState;
|
state: TicketValidationState;
|
||||||
};
|
};
|
||||||
|
|
@ -262,7 +298,7 @@ export type QueryTimePerTourMetricForMonthArgs = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryTourArgs = {
|
export type QueryTourCostBreakdownArgs = {
|
||||||
operationId: Scalars['String']['input'];
|
operationId: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -271,10 +307,18 @@ export type QueryToursPerTimeKpiArgs = {
|
||||||
ordinanceTypes: Scalars['String']['input'];
|
ordinanceTypes: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Report = {
|
export type RevenueTrend = {
|
||||||
__typename?: 'Report';
|
__typename?: 'RevenueTrend';
|
||||||
label: Scalars['String']['output'];
|
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'];
|
value: Scalars['Float']['output'];
|
||||||
|
variant: Scalars['String']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SolutionTarget = {
|
export type SolutionTarget = {
|
||||||
|
|
@ -433,9 +477,18 @@ export type Tour = {
|
||||||
type: Scalars['String']['output'];
|
type: Scalars['String']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TourCostBreakdown = {
|
||||||
|
__typename?: 'TourCostBreakdown';
|
||||||
|
servicePositions: Array<ServicePosition>;
|
||||||
|
tariffDescription: Scalars['String']['output'];
|
||||||
|
tariffValidityPeriod: Scalars['String']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type TourStateMeta = {
|
export type TourStateMeta = {
|
||||||
__typename?: 'TourStateMeta';
|
__typename?: 'TourStateMeta';
|
||||||
count: Scalars['Int']['output'];
|
count: Scalars['Int']['output'];
|
||||||
|
negativeRevenueDeviation?: Maybe<Scalars['Float']['output']>;
|
||||||
|
positiveRevenueDeviation?: Maybe<Scalars['Float']['output']>;
|
||||||
revenueSum?: 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 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; }>;
|
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 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 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 GetAverageAnomalyResolutionTimeReportQueryVariables = Exact<{ [key: string]: never; }>;
|
|
||||||
|
|
||||||
|
|
||||||
export type GetAverageAnomalyResolutionTimeReportQuery = { __typename?: 'Query', averageAnomalyResolutionTimeReport: Array<{ __typename?: 'Report', label: string, value: number }> };
|
|
||||||
|
|
||||||
export type TicketsUpdatedSubscriptionVariables = Exact<{ [key: string]: never; }>;
|
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<{
|
export type GetAccountingToursQueryVariables = Exact<{
|
||||||
state: AccountingState;
|
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 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`
|
export const AnomalyAllFragmentDoc = gql`
|
||||||
fragment AnomalyAll on Anomaly {
|
fragment AnomalyAll on Anomaly {
|
||||||
id
|
id
|
||||||
|
|
@ -688,6 +752,36 @@ export const AcceptAnomalyDocument = gql`
|
||||||
export class AcceptAnomalyGQL extends Apollo.Mutation<AcceptAnomalyMutation, AcceptAnomalyMutationVariables> {
|
export class AcceptAnomalyGQL extends Apollo.Mutation<AcceptAnomalyMutation, AcceptAnomalyMutationVariables> {
|
||||||
override document = AcceptAnomalyDocument;
|
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) {
|
constructor(apollo: Apollo.Apollo) {
|
||||||
super(apollo);
|
super(apollo);
|
||||||
}
|
}
|
||||||
|
|
@ -844,58 +938,59 @@ export const GetTimePerTourMetricForMonthDocument = gql`
|
||||||
super(apollo);
|
super(apollo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const GetAnomalyByTypeReportDocument = gql`
|
export const TicketStateKpiDocument = gql`
|
||||||
query GetAnomalyByTypeReport {
|
query TicketStateKpi($state: TicketValidationState!) {
|
||||||
anomalyAmountByType {
|
ticketStateKpi(state: $state) {
|
||||||
|
main {
|
||||||
label
|
label
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
secondary {
|
||||||
|
label
|
||||||
|
value
|
||||||
|
}
|
||||||
|
additional {
|
||||||
|
label
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class GetAnomalyByTypeReportGQL extends Apollo.Query<GetAnomalyByTypeReportQuery, GetAnomalyByTypeReportQueryVariables> {
|
export class TicketStateKpiGQL extends Apollo.Query<TicketStateKpiQuery, TicketStateKpiQueryVariables> {
|
||||||
override document = GetAnomalyByTypeReportDocument;
|
override document = TicketStateKpiDocument;
|
||||||
|
|
||||||
constructor(apollo: Apollo.Apollo) {
|
constructor(apollo: Apollo.Apollo) {
|
||||||
super(apollo);
|
super(apollo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const GetUpcomingTourNotificationReportDocument = gql`
|
export const ManagerKpiDocument = gql`
|
||||||
query GetUpcomingTourNotificationReport {
|
query ManagerKpi($ordinanceType: String!) {
|
||||||
upcomingTourNotificationReport {
|
managerKpi(ordinanceType: $ordinanceType) {
|
||||||
|
main {
|
||||||
label
|
label
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
secondary {
|
||||||
|
label
|
||||||
|
value
|
||||||
|
}
|
||||||
|
additional {
|
||||||
|
label
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class GetUpcomingTourNotificationReportGQL extends Apollo.Query<GetUpcomingTourNotificationReportQuery, GetUpcomingTourNotificationReportQueryVariables> {
|
export class ManagerKpiGQL extends Apollo.Query<ManagerKpiQuery, ManagerKpiQueryVariables> {
|
||||||
override document = GetUpcomingTourNotificationReportDocument;
|
override document = ManagerKpiDocument;
|
||||||
|
|
||||||
constructor(apollo: Apollo.Apollo) {
|
|
||||||
super(apollo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const GetAverageAnomalyResolutionTimeReportDocument = gql`
|
|
||||||
query GetAverageAnomalyResolutionTimeReport {
|
|
||||||
averageAnomalyResolutionTimeReport {
|
|
||||||
label
|
|
||||||
value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class GetAverageAnomalyResolutionTimeReportGQL extends Apollo.Query<GetAverageAnomalyResolutionTimeReportQuery, GetAverageAnomalyResolutionTimeReportQueryVariables> {
|
|
||||||
override document = GetAverageAnomalyResolutionTimeReportDocument;
|
|
||||||
|
|
||||||
constructor(apollo: Apollo.Apollo) {
|
constructor(apollo: Apollo.Apollo) {
|
||||||
super(apollo);
|
super(apollo);
|
||||||
|
|
@ -1220,6 +1315,8 @@ export const GetAccountingStateMetaDocument = gql`
|
||||||
accountingStateMeta(state: $state) {
|
accountingStateMeta(state: $state) {
|
||||||
count
|
count
|
||||||
revenueSum
|
revenueSum
|
||||||
|
positiveRevenueDeviation
|
||||||
|
negativeRevenueDeviation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
@ -1306,6 +1403,30 @@ export const GetAccountingToursDocument = gql`
|
||||||
export class GetAccountingToursGQL extends Apollo.Query<GetAccountingToursQuery, GetAccountingToursQueryVariables> {
|
export class GetAccountingToursGQL extends Apollo.Query<GetAccountingToursQuery, GetAccountingToursQueryVariables> {
|
||||||
override document = GetAccountingToursDocument;
|
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) {
|
constructor(apollo: Apollo.Apollo) {
|
||||||
super(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) {
|
accountingStateMeta(state: $state) {
|
||||||
count
|
count
|
||||||
revenueSum
|
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,7 +12,12 @@ import { MatBadgeModule } from '@angular/material/badge';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { HotToastService } from '@ngxpert/hot-toast';
|
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({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
|
@ -44,10 +49,18 @@ import { Tour, Ticket, AccountingState, TicketValidationState } from '../../../c
|
||||||
tour.revenueDeviation | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE'
|
tour.revenueDeviation | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE'
|
||||||
}}
|
}}
|
||||||
</ng-template>`,
|
</ng-template>`,
|
||||||
|
host: {
|
||||||
|
class: 'flex flex-col rounded border p-2 m-1 bg-white cursor-pointer',
|
||||||
|
},
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
|
@use '@angular/material' as mat;
|
||||||
:host {
|
:host {
|
||||||
@apply flex flex-col rounded border p-2 m-1 bg-white cursor-pointer;
|
@include mat.icon-button-overrides(
|
||||||
|
(
|
||||||
|
icon-size: 14,
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
|
|
@ -59,7 +72,7 @@ import { Tour, Ticket, AccountingState, TicketValidationState } from '../../../c
|
||||||
NgIf,
|
NgIf,
|
||||||
NgClass,
|
NgClass,
|
||||||
CurrencyPipe,
|
CurrencyPipe,
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
export class AccountingItemComponent implements AfterViewInit {
|
export class AccountingItemComponent implements AfterViewInit {
|
||||||
toast = inject(HotToastService);
|
toast = inject(HotToastService);
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,11 @@ import { TourStateMeta } from '../../../core/data-access/graphql/generated/gener
|
||||||
| currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE'
|
| currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE'
|
||||||
}}
|
}}
|
||||||
</span>`,
|
</span>`,
|
||||||
styles: [
|
host: {
|
||||||
`
|
class: 'flex flex-col items-center justify-center p-2',
|
||||||
:host {
|
},
|
||||||
@apply flex flex-col items-center justify-center p-2;
|
styles: [``],
|
||||||
}
|
imports: [NgIf, CurrencyPipe],
|
||||||
`,
|
|
||||||
],
|
|
||||||
imports: [NgIf, AsyncPipe, CurrencyPipe]
|
|
||||||
})
|
})
|
||||||
export class AccountingLaneFooterComponent {
|
export class AccountingLaneFooterComponent {
|
||||||
@Input() tourStateMeta!: TourStateMeta;
|
@Input() tourStateMeta!: TourStateMeta;
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,11 @@ import {
|
||||||
Input,
|
Input,
|
||||||
Output,
|
Output,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
AccountingFilterArgs,
|
||||||
|
AccountingState,
|
||||||
|
} from '../../../core/data-access/graphql/generated/generated';
|
||||||
import { AccountingLaneHeaderFilterComponent } from './accounting-lane-header-filter.component';
|
import { AccountingLaneHeaderFilterComponent } from './accounting-lane-header-filter.component';
|
||||||
import { AccountingFilterArgs, AccountingState } from '../../../core/data-access/graphql/generated/generated';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
|
@ -26,14 +29,17 @@ import { AccountingFilterArgs, AccountingState } from '../../../core/data-access
|
||||||
[state]="state"
|
[state]="state"
|
||||||
(filterChange)="filterChanged($event)"
|
(filterChange)="filterChanged($event)"
|
||||||
></dks-accounting-lane-header-filter>`,
|
></dks-accounting-lane-header-filter>`,
|
||||||
|
host: {
|
||||||
|
class: 'flex items-center justify-between p-2 bg-secondary-container',
|
||||||
|
},
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
: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 {
|
export class AccountingLaneHeaderComponent {
|
||||||
@Input() state!: AccountingState;
|
@Input() state!: AccountingState;
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,19 @@
|
||||||
>
|
>
|
||||||
<dks-accounting-item
|
<dks-accounting-item
|
||||||
*cdkVirtualFor="let tour of tours; trackBy: trackByFn"
|
*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"
|
[accountingState]="accountingState"
|
||||||
[tour]="tour"
|
[tour]="tour"
|
||||||
(click)="openTourView(tour)"
|
(click)="openTourView(tour)"
|
||||||
>
|
>
|
||||||
</dks-accounting-item>
|
</dks-accounting-item>
|
||||||
</cdk-virtual-scroll-viewport>
|
</cdk-virtual-scroll-viewport>
|
||||||
<ng-container *ngIf="accountingStateMeta$ | async as accountingStateMeta">
|
<!-- <ng-container *ngIf="accountingStateMeta$ | async as accountingStateMeta">
|
||||||
<dks-accounting-lane-footer
|
<dks-accounting-lane-footer
|
||||||
[tourStateMeta]="accountingStateMeta"
|
[tourStateMeta]="accountingStateMeta"
|
||||||
[withTourCount]="true"
|
[withTourCount]="true"
|
||||||
[withRevenueSum]="accountingState !== AccountingState.Billed"
|
[withRevenueSum]="accountingState !== AccountingState.Billed"
|
||||||
></dks-accounting-lane-footer>
|
></dks-accounting-lane-footer>
|
||||||
</ng-container>
|
</ng-container> -->
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import {
|
||||||
CdkVirtualScrollViewport,
|
CdkVirtualScrollViewport,
|
||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
} from '@angular/cdk/scrolling';
|
} from '@angular/cdk/scrolling';
|
||||||
import { AsyncPipe, CurrencyPipe, NgClass, NgFor, NgIf } from '@angular/common';
|
import { AsyncPipe, NgIf } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
|
@ -32,32 +32,31 @@ import {
|
||||||
import { AccountingTourViewComponent } from '../accounting-tour-view/accounting-tour-view.component';
|
import { AccountingTourViewComponent } from '../accounting-tour-view/accounting-tour-view.component';
|
||||||
import { DashboardAccountingService } from '../dashboard-accounting.service';
|
import { DashboardAccountingService } from '../dashboard-accounting.service';
|
||||||
import { AccountingItemComponent } from './accounting-item.component';
|
import { AccountingItemComponent } from './accounting-item.component';
|
||||||
import { AccountingLaneFooterComponent } from './accounting-lane-footer.component';
|
|
||||||
import { AccountingLaneHeaderComponent } from './accounting-lane-header.component';
|
import { AccountingLaneHeaderComponent } from './accounting-lane-header.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'dks-accounting-lane',
|
selector: 'dks-accounting-lane',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
templateUrl: './accounting-lane.component.html',
|
templateUrl: './accounting-lane.component.html',
|
||||||
|
host: {
|
||||||
|
class: 'rounded h-full w-full grid',
|
||||||
|
},
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
: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;
|
grid-template-rows: auto 1fr auto;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
AccountingLaneHeaderComponent,
|
AccountingLaneHeaderComponent,
|
||||||
AccountingLaneFooterComponent,
|
|
||||||
AccountingItemComponent,
|
AccountingItemComponent,
|
||||||
NgFor,
|
|
||||||
NgIf,
|
NgIf,
|
||||||
NgClass,
|
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
CurrencyPipe,
|
|
||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
export class AccountingLaneComponent {
|
export class AccountingLaneComponent {
|
||||||
private dialog = inject(DialogService);
|
private dialog = inject(DialogService);
|
||||||
|
|
@ -91,7 +90,7 @@ export class AccountingLaneComponent {
|
||||||
this.accountingService.fetchAccountingStateMeta(accountingState)
|
this.accountingService.fetchAccountingStateMeta(accountingState)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
tours$: Observable<Tour[]>;
|
tours$: Observable<(Tour & { ticket: Ticket })[]>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const batchMap: Observable<Record<string, Tour & { ticket: Ticket }>> =
|
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,
|
NgFor,
|
||||||
NgSwitch,
|
NgSwitch,
|
||||||
NgSwitchCase,
|
NgSwitchCase,
|
||||||
DatePipe,
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AccountingTourTicketComponent {
|
export class AccountingTourTicketComponent {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
} from '../../../core/data-access/graphql/generated/generated';
|
} from '../../../core/data-access/graphql/generated/generated';
|
||||||
import { AccountingTourOverviewComponent } from './accounting-tour-overview.component';
|
import { AccountingTourOverviewComponent } from './accounting-tour-overview.component';
|
||||||
import { AccountingTourTicketComponent } from './accounting-tour-ticket.component';
|
import { AccountingTourTicketComponent } from './accounting-tour-ticket.component';
|
||||||
|
import { AccountingTourCostBreakdownComponent } from './accounting-tour-cost-breakdown.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
|
@ -19,6 +20,11 @@ import { AccountingTourTicketComponent } from './accounting-tour-ticket.componen
|
||||||
</h3>
|
</h3>
|
||||||
<hr />
|
<hr />
|
||||||
<mat-tab-group>
|
<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">
|
<mat-tab label="Übersicht">
|
||||||
<dks-accounting-tour-overview
|
<dks-accounting-tour-overview
|
||||||
[tour]="ref.data"
|
[tour]="ref.data"
|
||||||
|
|
@ -36,9 +42,10 @@ import { AccountingTourTicketComponent } from './accounting-tour-ticket.componen
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
AccountingTourOverviewComponent,
|
AccountingTourOverviewComponent,
|
||||||
AccountingTourTicketComponent,
|
AccountingTourTicketComponent,
|
||||||
]
|
AccountingTourCostBreakdownComponent
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AccountingTourViewComponent {
|
export class AccountingTourViewComponent {
|
||||||
ref: DialogRef<Tour & { ticket: Ticket }, undefined | any> =
|
ref =
|
||||||
inject(DialogRef);
|
inject<DialogRef<Tour & { ticket: Ticket }, undefined | any>>(DialogRef);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,38 @@
|
||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
import { AccountingLaneComponent } from './accounting-lane/accounting-lane.component';
|
|
||||||
import { AccountingState } from '../../core/data-access/graphql/generated/generated';
|
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({
|
@Component({
|
||||||
selector: 'dks-dashboard-accounting',
|
selector: 'dks-dashboard-accounting',
|
||||||
imports: [AccountingLaneComponent],
|
imports: [AccountingInfoCardComponent, AccountingLaneComponent],
|
||||||
template: `<section class="h-full p-6 grid grid-cols-1 md:grid-cols-3 gap-6">
|
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">
|
<div class="col-span-1 md:col-span-3 flex justify-end items-center">
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
</div>
|
</div>
|
||||||
|
<app-accounting-info-card
|
||||||
|
class="self-center"
|
||||||
|
[accountingState]="AccountingState.Billable"
|
||||||
|
></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
|
<dks-accounting-lane
|
||||||
[accountingState]="AccountingState.Billable"
|
[accountingState]="AccountingState.Billable"
|
||||||
></dks-accounting-lane>
|
></dks-accounting-lane>
|
||||||
|
</div>
|
||||||
<dks-accounting-lane
|
<dks-accounting-lane
|
||||||
[accountingState]="AccountingState.PreparedForBilling"
|
[accountingState]="AccountingState.PreparedForBilling"
|
||||||
></dks-accounting-lane>
|
></dks-accounting-lane>
|
||||||
<dks-accounting-lane [accountingState]="AccountingState.Billed"></dks-accounting-lane>
|
<dks-accounting-lane
|
||||||
|
[accountingState]="AccountingState.Billed"
|
||||||
|
></dks-accounting-lane>
|
||||||
</section>`,
|
</section>`,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
|
|
@ -24,11 +41,12 @@ import { AccountingState } from '../../core/data-access/graphql/generated/genera
|
||||||
height: calc(100% - 64px);
|
height: calc(100% - 64px);
|
||||||
}
|
}
|
||||||
section {
|
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 {
|
export class DashboardAccountingComponent {
|
||||||
AccountingState = AccountingState;
|
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',
|
selector: 'dks-anomaly-list-item',
|
||||||
templateUrl: './anomaly-list-item.component.html',
|
templateUrl: './anomaly-list-item.component.html',
|
||||||
styleUrls: ['./anomaly-list-item.component.css'],
|
styleUrls: ['./anomaly-list-item.component.css'],
|
||||||
|
host: {
|
||||||
|
class: 'w-full rounded flex items-center',
|
||||||
|
},
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
:host {
|
|
||||||
@apply flex flex-col h-full;
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@ import {
|
||||||
Component,
|
Component,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
ViewChild
|
ViewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||||
import {
|
import {
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
shareReplay,
|
shareReplay,
|
||||||
Subscription,
|
Subscription,
|
||||||
switchMap,
|
switchMap,
|
||||||
tap
|
tap,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
import { AnomaliesService } from '../anomalies.service';
|
import { AnomaliesService } from '../anomalies.service';
|
||||||
|
|
@ -20,9 +20,12 @@ import { AnomaliesService } from '../anomalies.service';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'dks-anomaly-list',
|
selector: 'dks-anomaly-list',
|
||||||
templateUrl: './anomaly-list.component.html',
|
templateUrl: './anomaly-list.component.html',
|
||||||
styleUrls: ['./anomaly-list.component.css'],
|
styleUrls: ['./anomaly-list.component.scss'],
|
||||||
|
host: {
|
||||||
|
class: 'flex flex-col h-full',
|
||||||
|
},
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class AnomalyListComponent implements OnInit, OnDestroy {
|
export class AnomalyListComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild(MatPaginator, { static: true }) private paginator!: MatPaginator;
|
@ViewChild(MatPaginator, { static: true }) private paginator!: MatPaginator;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
:host {
|
|
||||||
@apply m-4;
|
|
||||||
}
|
|
||||||
|
|
@ -12,10 +12,10 @@
|
||||||
</article>
|
</article>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div class="flex justify-end mt-2">
|
<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
|
<mat-icon>schedule</mat-icon> Stummschalten
|
||||||
</button>
|
</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
|
<mat-icon>check_circle_outline</mat-icon> Akzeptieren
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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({
|
@Component({
|
||||||
selector: 'dks-anomaly-solution-dialog',
|
selector: 'dks-anomaly-solution-dialog',
|
||||||
templateUrl: './anomaly-solution-dialog.component.html',
|
templateUrl: './anomaly-solution-dialog.component.html',
|
||||||
styleUrls: ['./anomaly-solution-dialog.component.css'],
|
styleUrls: ['./anomaly-solution-dialog.component.scss'],
|
||||||
|
host: {
|
||||||
|
class: 'm-4',
|
||||||
|
},
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AnomalySolutionDialogComponent {
|
export class AnomalySolutionDialogComponent {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
:host {
|
|
||||||
@apply flex flex-col h-full;
|
|
||||||
}
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
</mat-icon>
|
</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
{{ node.name }}
|
{{ 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-node>
|
||||||
</mat-tree>
|
</mat-tree>
|
||||||
<ng-template #emptyListTmpl>
|
<ng-template #emptyListTmpl>
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,10 @@ interface AnomalyFlatNode {
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'dks-anomaly-tree',
|
selector: 'dks-anomaly-tree',
|
||||||
templateUrl: './anomaly-tree.component.html',
|
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,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
></dks-tours-per-time-kpi>
|
></dks-tours-per-time-kpi>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex lg:flex-nowrap flex-wrap-reverse">
|
<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-header>
|
||||||
<mat-card-title> Anomalien </mat-card-title>
|
<mat-card-title> Anomalien </mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<dks-anomaly-tree></dks-anomaly-tree>
|
<dks-anomaly-tree></dks-anomaly-tree>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</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-header>
|
||||||
<mat-card-title> Mitarbeiter ohne Tour </mat-card-title>
|
<mat-card-title> Mitarbeiter ohne Tour </mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
|
|
@ -30,12 +30,12 @@
|
||||||
<dks-anomaly-list></dks-anomaly-list>
|
<dks-anomaly-list></dks-anomaly-list>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</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-header>
|
||||||
<mat-card-title> Nächste Touren ohne Anfahrt </mat-card-title>
|
<mat-card-title> Nächste Touren ohne Anfahrt </mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<!-- <dks-upcoming-tours></dks-upcoming-tours> -->
|
<dks-upcoming-tours></dks-upcoming-tours>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -15,7 +15,7 @@ import { AnomalyListItemComponent } from './anomaly-list/anomaly-list-item/anoma
|
||||||
import { AnomalyListComponent } from './anomaly-list/anomaly-list.component';
|
import { AnomalyListComponent } from './anomaly-list/anomaly-list.component';
|
||||||
import { AnomalySolutionDialogComponent } from './anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component';
|
import { AnomalySolutionDialogComponent } from './anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component';
|
||||||
import { AnomalyTreeComponent } from './anomaly-tree/anomaly-tree.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 { 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 { TourViewDialogComponent } from './upcoming-tours/tour-view-dialog/tour-view-dialog.component';
|
||||||
import { UpcomingToursComponent } from './upcoming-tours/upcoming-tours.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-header class="flex justify-center">
|
||||||
<mat-card-title>{{ ordinanceType }}</mat-card-title>
|
<mat-card-title>{{ ordinanceType }}</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
|
|
@ -6,17 +6,18 @@
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div
|
<div
|
||||||
*ngFor="let kpi of kpis; index as idx"
|
*ngFor="let kpi of kpis; index as idx"
|
||||||
class="w-1/3 flex flex-col justify-center items-center border-blue-400"
|
class="w-1/3 flex flex-col justify-center items-center"
|
||||||
[ngClass]="{'border-b-2': (currentTimeSlot$ | async) === idx}"
|
style="border-color: var(--mat-sys-primary);"
|
||||||
|
[ngClass]="{
|
||||||
|
'border-b-2': (currentTimeSlot$ | async) === idx,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div class="p-4 text-base">
|
||||||
class="p-4 text-base"
|
{{ kpi.kpi.value.toString().replace(".", ",") }}
|
||||||
>
|
|
||||||
{{ kpi.kpi.value.toString().replace('.', ',') }}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="font-bold">
|
<div class="font-bold">
|
||||||
{{ kpi.interval.start | date: 'HH:mm' }} -
|
{{ kpi.interval.start | date : "HH:mm" }} -
|
||||||
{{ kpi.interval.end | date: 'HH:mm' }}
|
{{ kpi.interval.end | date : "HH:mm" }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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',
|
selector: 'dks-tour-view-dialog',
|
||||||
templateUrl: './tour-view-dialog.component.html',
|
templateUrl: './tour-view-dialog.component.html',
|
||||||
styleUrls: ['./tour-view-dialog.component.css'],
|
styleUrls: ['./tour-view-dialog.component.css'],
|
||||||
|
host: {
|
||||||
|
class: 'm-4',
|
||||||
|
},
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
export class TourViewDialogComponent {
|
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 { MatTabsModule } from '@angular/material/tabs';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { de } from 'date-fns/locale';
|
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 { 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 { DayviewComponent } from './dayview/dayview.component';
|
||||||
import { ManagerDashboardComponent } from './manager-dashboard/manager-dashboard.component';
|
import { ManagerDashboardComponent } from './manager-dashboard/manager-dashboard.component';
|
||||||
import { MonthviewComponent } from './monthview/monthview.component';
|
import { MonthviewComponent } from './monthview/monthview.component';
|
||||||
import { OverviewComponent } from './overview/overview.component';
|
import { OverviewComponent } from './overview/overview.component';
|
||||||
import { TimePerTourByCarComponent } from './time-per-tour-by-car/time-per-tour-by-car.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 { 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({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -69,6 +70,7 @@ import { UtilNgxEchartsModule } from '../../core/components/ngx-echarts/util-ngx
|
||||||
UtilNgxEchartsModule,
|
UtilNgxEchartsModule,
|
||||||
MetricSummaryComponent,
|
MetricSummaryComponent,
|
||||||
ViewTypeSelectorComponent,
|
ViewTypeSelectorComponent,
|
||||||
|
ManagerInfoCardComponent,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
ManagerDashboardComponent,
|
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',
|
selector: 'dks-average-revenue-summary',
|
||||||
imports: [CommonModule, MatIconModule],
|
imports: [CommonModule, MatIconModule],
|
||||||
templateUrl: './average-revenue-summary.component.html',
|
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 {
|
export class AverageRevenueSummaryComponent {
|
||||||
@Input() set metrics(value: TimePerTourMetric[]) {
|
@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>
|
<mat-icon>account_balance</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
</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"
|
<span class="text-base"
|
||||||
>+{{
|
>+{{
|
||||||
summary.forecastRevenue | currency : 'EUR' : 'symbol' : '1.2-2'
|
summary.forecastRevenue | currency : 'EUR' : 'symbol' : '1.2-2'
|
||||||
|
|
@ -40,5 +41,5 @@
|
||||||
<div class="p-1 text-sm" title="Umsatz Vorausschau">
|
<div class="p-1 text-sm" title="Umsatz Vorausschau">
|
||||||
<mat-icon>account_balance</mat-icon>
|
<mat-icon>account_balance</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@ import { summarizeMetrics } from '../../util/summarize-metrics';
|
||||||
selector: 'dks-breakeven-summary',
|
selector: 'dks-breakeven-summary',
|
||||||
imports: [CommonModule, MatIconModule],
|
imports: [CommonModule, MatIconModule],
|
||||||
templateUrl: './breakeven-summary.component.html',
|
templateUrl: './breakeven-summary.component.html',
|
||||||
styleUrls: ['./breakeven-summary.component.css']
|
styleUrls: ['./breakeven-summary.component.css'],
|
||||||
|
host: {
|
||||||
|
class: 'flex justify-center items-center',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class BreakevenSummaryComponent {
|
export class BreakevenSummaryComponent {
|
||||||
@Input() set metrics(value: TimePerTourMetric[]) {
|
@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>
|
></dks-time-per-tour-summary>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngSwitchCase="DiagrammType.BreakEven">
|
<ng-container *ngSwitchCase="DiagrammType.Revenue">
|
||||||
<dks-breakeven-metric
|
<dks-breakeven-metric
|
||||||
class="col-span-3"
|
class="col-span-3"
|
||||||
[labelFormat]="getLabelFormatForMetricTimeScale(metricTimeScale)"
|
[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 { Component, Input } from '@angular/core';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { BehaviorSubject, map } from 'rxjs';
|
import { BehaviorSubject, map } from 'rxjs';
|
||||||
import { summarizeMetrics } from '../../util/summarize-metrics';
|
|
||||||
import { TimePerTourMetric } from '../../../../core/data-access/graphql/generated/generated';
|
import { TimePerTourMetric } from '../../../../core/data-access/graphql/generated/generated';
|
||||||
|
import { summarizeMetrics } from '../../util/summarize-metrics';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'dks-time-per-tour-summary',
|
selector: 'dks-time-per-tour-summary',
|
||||||
imports: [CommonModule, MatIconModule],
|
imports: [CommonModule, MatIconModule],
|
||||||
templateUrl: './time-per-tour-summary.component.html',
|
templateUrl: './time-per-tour-summary.component.html',
|
||||||
styleUrls: ['./time-per-tour-summary.component.css']
|
styleUrls: ['./time-per-tour-summary.component.css'],
|
||||||
|
host: {
|
||||||
|
class: 'flex justify-center items-center',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export class TimePerTourSummaryComponent {
|
export class TimePerTourSummaryComponent {
|
||||||
@Input() set metrics(value: TimePerTourMetric[]) {
|
@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]="{
|
[ngClass]="{
|
||||||
'border border-blue-400': selected
|
'!border': selected
|
||||||
}"
|
}"
|
||||||
|
style="border-color: var(--mat-sys-primary)"
|
||||||
(click)="setSelectedViewType()"
|
(click)="setSelectedViewType()"
|
||||||
*ngIf="metricSummary$ | async as summary"
|
*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">
|
<mat-card-content class="!flex justify-center">
|
||||||
<div
|
@switch (currentMetric) { @case (DiagrammType.TimePerTour) {
|
||||||
class="flex flex-col items-end"
|
<div class="w-1/4 flex flex-col items-end">
|
||||||
*ngSwitchCase="DiagrammType.TimePerTour"
|
|
||||||
>
|
|
||||||
<div class="flex items-center pb-2" title="Touren Menge">
|
<div class="flex items-center pb-2" title="Touren Menge">
|
||||||
{{ summary.tourAmount.toFixed(2) }}
|
{{ summary.tourAmount.toFixed(2) }}
|
||||||
<mat-icon class="mx-2">local_shipping</mat-icon>
|
<mat-icon class="mx-2">local_shipping</mat-icon>
|
||||||
|
|
@ -22,28 +26,28 @@
|
||||||
<mat-icon class="mx-2">departure_board</mat-icon>
|
<mat-icon class="mx-2">departure_board</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-end" *ngSwitchCase="DiagrammType.BreakEven">
|
} @case (DiagrammType.Revenue) {
|
||||||
<div class="flex items-center pb-2" title="Deckungsbeitrag">
|
<div class="w-1/4 flex flex-col items-end">
|
||||||
{{ summary.breakEven | currency:'EUR' }}
|
<div class="flex items-center pb-2" title="Umsatz">
|
||||||
<mat-icon class="mx-2">account_balance</mat-icon>
|
{{ summary.revenue | currency : "EUR" : "symbol" : ".2" }}
|
||||||
|
<mat-icon class="mx-2">euro</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center text-yellow-500" title="Erwarteter Deckungsbeitrag">
|
<div class="flex items-center" title="Deckungsbeitrag">
|
||||||
{{ summary.forecastRevenue | currency:'EUR' }}
|
{{ summary.breakEven | currency : "EUR" }}
|
||||||
<mat-icon class="mx-2">account_balance</mat-icon>
|
<mat-icon class="mx-2">account_balance</mat-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
} @case (DiagrammType.AverageRevenue) {
|
||||||
class="flex flex-col items-end"
|
<div class="w-1/4 flex flex-col items-end">
|
||||||
*ngSwitchCase="DiagrammType.AverageRevenue"
|
|
||||||
>
|
|
||||||
<div class="flex items-center pb-2" title="Durchschnittlicher Umsatz">
|
<div class="flex items-center pb-2" title="Durchschnittlicher Umsatz">
|
||||||
<span class="mr-1">Ø</span>
|
<span class="mr-1">Ø</span>
|
||||||
{{ summary.averageRevenue | currency:'EUR' }}
|
{{ summary.averageRevenue | currency : "EUR" }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center" title="Zeit pro Tour Index">
|
<div class="flex items-center" title="Zeit pro Tour Index">
|
||||||
{{ calculateIndex(summary).toFixed(2) }}
|
{{ calculateIndex(summary).toFixed(2) }}
|
||||||
<span class="mx-2">Index</span>
|
<span class="mx-2">Index</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
} }
|
||||||
</div>
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,28 @@ import {
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
CurrencyPipe,
|
CurrencyPipe,
|
||||||
NgClass,
|
NgClass,
|
||||||
NgIf,
|
NgIf
|
||||||
NgSwitch,
|
|
||||||
NgSwitchCase,
|
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { BehaviorSubject, map } from 'rxjs';
|
import { BehaviorSubject, map } from 'rxjs';
|
||||||
|
import { TimePerTourMetric } from '../../../../core/data-access/graphql/generated/generated';
|
||||||
import { DashboardViewType, DiagrammType } from '../../types';
|
import { DashboardViewType, DiagrammType } from '../../types';
|
||||||
import { summarizeMetrics } from '../../util/summarize-metrics';
|
import { summarizeMetrics } from '../../util/summarize-metrics';
|
||||||
import { TimePerTourMetric } from '../../../../core/data-access/graphql/generated/generated';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'dks-view-type-selector',
|
selector: 'dks-view-type-selector',
|
||||||
imports: [NgIf, NgSwitch, NgSwitchCase, NgClass, AsyncPipe, CurrencyPipe, MatIconModule],
|
imports: [
|
||||||
|
NgIf,
|
||||||
|
NgClass,
|
||||||
|
AsyncPipe,
|
||||||
|
CurrencyPipe,
|
||||||
|
MatIconModule,
|
||||||
|
MatCardModule,
|
||||||
|
],
|
||||||
templateUrl: './view-type-selector.component.html',
|
templateUrl: './view-type-selector.component.html',
|
||||||
styleUrls: ['./view-type-selector.component.css']
|
styleUrls: ['./view-type-selector.component.css'],
|
||||||
})
|
})
|
||||||
export class ViewTypeSelectorComponent {
|
export class ViewTypeSelectorComponent {
|
||||||
@Input() viewType!: DashboardViewType;
|
@Input() viewType!: DashboardViewType;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
</button>
|
</button>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="pr-2">
|
<span class="pr-2">
|
||||||
{{ vm.selectedDate | date : 'dd.MM.yyyy' }}
|
{{ vm.selectedDate | date : "dd.MM.yyyy" }}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
[formControl]="selectedDate"
|
[formControl]="selectedDate"
|
||||||
|
|
@ -27,18 +27,18 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<mat-chip-listbox (change)="changeSelectedType($event)">
|
<mat-chip-listbox (change)="changeSelectedType($event)">
|
||||||
|
<mat-chip-option
|
||||||
|
[value]="DiagrammType.Revenue"
|
||||||
|
[selected]="vm.selectedMetric === DiagrammType.Revenue"
|
||||||
|
>
|
||||||
|
Umsatz
|
||||||
|
</mat-chip-option>
|
||||||
<mat-chip-option
|
<mat-chip-option
|
||||||
[value]="DiagrammType.TimePerTour"
|
[value]="DiagrammType.TimePerTour"
|
||||||
[selected]="vm.selectedMetric === DiagrammType.TimePerTour"
|
[selected]="vm.selectedMetric === DiagrammType.TimePerTour"
|
||||||
>
|
>
|
||||||
Kapazität pro Tour
|
Kapazität pro Tour
|
||||||
</mat-chip-option>
|
</mat-chip-option>
|
||||||
<mat-chip-option
|
|
||||||
[value]="DiagrammType.BreakEven"
|
|
||||||
[selected]="vm.selectedMetric === DiagrammType.BreakEven"
|
|
||||||
>
|
|
||||||
Deckungsbeitrag
|
|
||||||
</mat-chip-option>
|
|
||||||
<mat-chip-option
|
<mat-chip-option
|
||||||
[value]="DiagrammType.AverageRevenue"
|
[value]="DiagrammType.AverageRevenue"
|
||||||
[selected]="vm.selectedMetric === DiagrammType.AverageRevenue"
|
[selected]="vm.selectedMetric === DiagrammType.AverageRevenue"
|
||||||
|
|
@ -47,6 +47,16 @@
|
||||||
</mat-chip-option>
|
</mat-chip-option>
|
||||||
</mat-chip-listbox>
|
</mat-chip-listbox>
|
||||||
</div>
|
</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
|
<div
|
||||||
class="my-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
|
class="my-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
|
||||||
>
|
>
|
||||||
|
|
@ -79,13 +89,16 @@
|
||||||
(viewTypeSelected)="setSelectedViewType(DashboardViewType.Gesamt)"
|
(viewTypeSelected)="setSelectedViewType(DashboardViewType.Gesamt)"
|
||||||
></dks-view-type-selector>
|
></dks-view-type-selector>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<!-- Visualisation -->
|
||||||
|
<mat-card
|
||||||
|
appearance="outlined"
|
||||||
class="grid grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
|
class="grid grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
|
||||||
[ngSwitch]="vm.selectedViewType"
|
[ngSwitch]="vm.selectedViewType"
|
||||||
>
|
>
|
||||||
|
<mat-card-content>
|
||||||
<div
|
<div
|
||||||
*ngSwitchCase="DashboardViewType.BTW"
|
*ngSwitchCase="DashboardViewType.BTW"
|
||||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
class="col-span-2 lg:col-span-4 w-full"
|
||||||
>
|
>
|
||||||
<dks-metric-summary
|
<dks-metric-summary
|
||||||
[metrics]="vm.btwMetrics"
|
[metrics]="vm.btwMetrics"
|
||||||
|
|
@ -96,7 +109,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
*ngSwitchCase="DashboardViewType.TSW"
|
*ngSwitchCase="DashboardViewType.TSW"
|
||||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
class="col-span-2 lg:col-span-4 w-full"
|
||||||
>
|
>
|
||||||
<dks-metric-summary
|
<dks-metric-summary
|
||||||
[metrics]="vm.tswMetrics"
|
[metrics]="vm.tswMetrics"
|
||||||
|
|
@ -107,7 +120,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
*ngSwitchCase="DashboardViewType.KTW"
|
*ngSwitchCase="DashboardViewType.KTW"
|
||||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
class="col-span-2 lg:col-span-4 w-full"
|
||||||
>
|
>
|
||||||
<dks-metric-summary
|
<dks-metric-summary
|
||||||
[metrics]="vm.ktwMetrics"
|
[metrics]="vm.ktwMetrics"
|
||||||
|
|
@ -118,7 +131,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
*ngSwitchCase="DashboardViewType.Gesamt"
|
*ngSwitchCase="DashboardViewType.Gesamt"
|
||||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
class="col-span-2 lg:col-span-4 w-full"
|
||||||
>
|
>
|
||||||
<dks-metric-summary
|
<dks-metric-summary
|
||||||
[metrics]="vm.combinedMetrics"
|
[metrics]="vm.combinedMetrics"
|
||||||
|
|
@ -127,14 +140,11 @@
|
||||||
[labelFormat]="'HH:mm'"
|
[labelFormat]="'HH:mm'"
|
||||||
></dks-metric-summary>
|
></dks-metric-summary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #loadingTmpl>
|
<ng-template #loadingTmpl>
|
||||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #viewTypeSelector let-typeAndSummary>
|
|
||||||
{{ typeAndSummary.type }}
|
|
||||||
</ng-template>
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export class DayviewComponent {
|
||||||
startWith(this.selectedDate.value),
|
startWith(this.selectedDate.value),
|
||||||
map((date) => (date?.getHours() === 0 ? addHours(date, 12) : date))
|
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>(
|
selectedViewType$ = new BehaviorSubject<DashboardViewType>(
|
||||||
DashboardViewType.Gesamt
|
DashboardViewType.Gesamt
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,15 @@ import {
|
||||||
throwError,
|
throwError,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
GetTourAmountPerTimeKpiManagerGQL,
|
|
||||||
GetTimePerTourMetricForDayGQL,
|
GetTimePerTourMetricForDayGQL,
|
||||||
GetTimePerTourMetricForMonthGQL,
|
|
||||||
TimePerTourUpdatedGQL,
|
|
||||||
GetTourAmountPerTimeKpiManagerQuery,
|
|
||||||
GetTimePerTourMetricForDayQuery,
|
GetTimePerTourMetricForDayQuery,
|
||||||
|
GetTimePerTourMetricForMonthGQL,
|
||||||
GetTimePerTourMetricForMonthQuery,
|
GetTimePerTourMetricForMonthQuery,
|
||||||
|
GetTourAmountPerTimeKpiManagerGQL,
|
||||||
|
GetTourAmountPerTimeKpiManagerQuery,
|
||||||
|
ManagerKpiGQL,
|
||||||
|
ManagerKpiQuery,
|
||||||
|
TimePerTourUpdatedGQL,
|
||||||
} from '../../core/data-access/graphql/generated/generated';
|
} from '../../core/data-access/graphql/generated/generated';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
|
|
@ -35,6 +37,7 @@ export class ManagerDashboardService {
|
||||||
private readonly getTimePerTourMetricForDayGql: GetTimePerTourMetricForDayGQL,
|
private readonly getTimePerTourMetricForDayGql: GetTimePerTourMetricForDayGQL,
|
||||||
private readonly getTimePerTourMetricForMonthGql: GetTimePerTourMetricForMonthGQL,
|
private readonly getTimePerTourMetricForMonthGql: GetTimePerTourMetricForMonthGQL,
|
||||||
private readonly timePerTourUpdatedGql: TimePerTourUpdatedGQL,
|
private readonly timePerTourUpdatedGql: TimePerTourUpdatedGQL,
|
||||||
|
private readonly managerKpiGql: ManagerKpiGQL,
|
||||||
private readonly toast: HotToastService
|
private readonly toast: HotToastService
|
||||||
) {
|
) {
|
||||||
this.timePerTourUpdate$ = this.timePerTourUpdatedGql.subscribe().pipe(
|
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(
|
getTimePerTourKPIByOrdinanceTypes(
|
||||||
ordinanceType: string
|
ordinanceType: string
|
||||||
): Observable<GetTourAmountPerTimeKpiManagerQuery['toursPerTimeKPI']> {
|
): Observable<GetTourAmountPerTimeKpiManagerQuery['toursPerTimeKPI']> {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
<nav mat-tab-nav-bar backgroundColor="accent" [tabPanel]="tabPanel">
|
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
|
||||||
|
@for (tab of tabs; track tab.route) {
|
||||||
<a
|
<a
|
||||||
#link
|
#link
|
||||||
mat-tab-link
|
mat-tab-link
|
||||||
*ngFor="let tab of tabs"
|
|
||||||
[routerLink]="tab.route"
|
[routerLink]="tab.route"
|
||||||
[routerLinkActive]="'active'"
|
[routerLinkActive]="'active'"
|
||||||
[active]="link.elementRef.nativeElement.classList.contains('active')"
|
[active]="link.elementRef.nativeElement.classList.contains('active')"
|
||||||
>{{ tab.title }}</a
|
>{{ tab.title }}</a
|
||||||
>
|
>
|
||||||
|
}
|
||||||
</nav>
|
</nav>
|
||||||
<mat-tab-nav-panel #tabPanel>
|
<mat-tab-nav-panel #tabPanel>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Component } from '@angular/core';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'dks-manager-dashboard',
|
selector: 'dks-manager-dashboard',
|
||||||
templateUrl: './manager-dashboard.component.html',
|
templateUrl: './manager-dashboard.component.html',
|
||||||
styleUrls: ['./manager-dashboard.component.css'],
|
styleUrls: ['./manager-dashboard.component.scss'],
|
||||||
standalone: false
|
standalone: false
|
||||||
})
|
})
|
||||||
export class ManagerDashboardComponent {
|
export class ManagerDashboardComponent {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
</button>
|
</button>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="pr-2">
|
<span class="pr-2">
|
||||||
{{ vm.selectedDate | date : 'MM.yyyy' }}
|
{{ vm.selectedDate | date : "MM.yyyy" }}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
[formControl]="selectedDate"
|
[formControl]="selectedDate"
|
||||||
|
|
@ -33,18 +33,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-chip-listbox (change)="changeSelectedType($event)">
|
<mat-chip-listbox (change)="changeSelectedType($event)">
|
||||||
|
<mat-chip-option
|
||||||
|
[value]="DiagrammType.Revenue"
|
||||||
|
[selected]="vm.selectedMetric === DiagrammType.Revenue"
|
||||||
|
>
|
||||||
|
Umsatz
|
||||||
|
</mat-chip-option>
|
||||||
<mat-chip-option
|
<mat-chip-option
|
||||||
[value]="DiagrammType.TimePerTour"
|
[value]="DiagrammType.TimePerTour"
|
||||||
[selected]="vm.selectedMetric === DiagrammType.TimePerTour"
|
[selected]="vm.selectedMetric === DiagrammType.TimePerTour"
|
||||||
>
|
>
|
||||||
Kapazität pro Tour
|
Kapazität pro Tour
|
||||||
</mat-chip-option>
|
</mat-chip-option>
|
||||||
<mat-chip-option
|
|
||||||
[value]="DiagrammType.BreakEven"
|
|
||||||
[selected]="vm.selectedMetric === DiagrammType.BreakEven"
|
|
||||||
>
|
|
||||||
Deckungsbeitrag
|
|
||||||
</mat-chip-option>
|
|
||||||
<mat-chip-option
|
<mat-chip-option
|
||||||
[value]="DiagrammType.AverageRevenue"
|
[value]="DiagrammType.AverageRevenue"
|
||||||
[selected]="vm.selectedMetric === DiagrammType.AverageRevenue"
|
[selected]="vm.selectedMetric === DiagrammType.AverageRevenue"
|
||||||
|
|
@ -85,14 +85,14 @@
|
||||||
(viewTypeSelected)="setSelectedViewType(DashboardViewType.Gesamt)"
|
(viewTypeSelected)="setSelectedViewType(DashboardViewType.Gesamt)"
|
||||||
></dks-view-type-selector>
|
></dks-view-type-selector>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<!-- Visualisation -->
|
||||||
|
<mat-card
|
||||||
|
appearance="outlined"
|
||||||
class="grid grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
|
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"
|
|
||||||
>
|
>
|
||||||
|
<mat-card-content>
|
||||||
|
@switch (vm.selectedViewType) { @case (DashboardViewType.BTW) {
|
||||||
|
<div class="col-span-2 lg:col-span-4 w-full">
|
||||||
<dks-metric-summary
|
<dks-metric-summary
|
||||||
[metrics]="vm.btwMetrics"
|
[metrics]="vm.btwMetrics"
|
||||||
[metricTimeScale]="metricTimeScale"
|
[metricTimeScale]="metricTimeScale"
|
||||||
|
|
@ -100,10 +100,8 @@
|
||||||
[labelFormat]="'d.M.yy'"
|
[labelFormat]="'d.M.yy'"
|
||||||
></dks-metric-summary>
|
></dks-metric-summary>
|
||||||
</div>
|
</div>
|
||||||
<div
|
} @case (DashboardViewType.TSW) {
|
||||||
*ngSwitchCase="DashboardViewType.TSW"
|
<div class="col-span-2 lg:col-span-4 w-full">
|
||||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
|
||||||
>
|
|
||||||
<dks-metric-summary
|
<dks-metric-summary
|
||||||
[metrics]="vm.tswMetrics"
|
[metrics]="vm.tswMetrics"
|
||||||
[metricTimeScale]="metricTimeScale"
|
[metricTimeScale]="metricTimeScale"
|
||||||
|
|
@ -111,9 +109,9 @@
|
||||||
[labelFormat]="'d.M.yy'"
|
[labelFormat]="'d.M.yy'"
|
||||||
></dks-metric-summary>
|
></dks-metric-summary>
|
||||||
</div>
|
</div>
|
||||||
|
} @case (DashboardViewType.KTW) {
|
||||||
<div
|
<div
|
||||||
*ngSwitchCase="DashboardViewType.KTW"
|
class="col-span-2 lg:col-span-4 w-full rounded"
|
||||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
|
||||||
>
|
>
|
||||||
<dks-metric-summary
|
<dks-metric-summary
|
||||||
[metrics]="vm.ktwMetrics"
|
[metrics]="vm.ktwMetrics"
|
||||||
|
|
@ -122,9 +120,9 @@
|
||||||
[labelFormat]="'d.M.yy'"
|
[labelFormat]="'d.M.yy'"
|
||||||
></dks-metric-summary>
|
></dks-metric-summary>
|
||||||
</div>
|
</div>
|
||||||
|
} @case (DashboardViewType.Gesamt) {
|
||||||
<div
|
<div
|
||||||
*ngSwitchCase="DashboardViewType.Gesamt"
|
class="col-span-2 lg:col-span-4 w-full"
|
||||||
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
|
|
||||||
>
|
>
|
||||||
<dks-metric-summary
|
<dks-metric-summary
|
||||||
[metrics]="vm.combinedMetrics"
|
[metrics]="vm.combinedMetrics"
|
||||||
|
|
@ -133,7 +131,9 @@
|
||||||
[labelFormat]="'d.M.yy'"
|
[labelFormat]="'d.M.yy'"
|
||||||
></dks-metric-summary>
|
></dks-metric-summary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
} }
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #loadingTmpl>
|
<ng-template #loadingTmpl>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { mergeMetrics } from '../util/merge-metric';
|
||||||
selector: 'dks-monthview',
|
selector: 'dks-monthview',
|
||||||
templateUrl: './monthview.component.html',
|
templateUrl: './monthview.component.html',
|
||||||
styleUrls: ['./monthview.component.css'],
|
styleUrls: ['./monthview.component.css'],
|
||||||
standalone: false
|
standalone: false,
|
||||||
})
|
})
|
||||||
export class MonthviewComponent {
|
export class MonthviewComponent {
|
||||||
OrdinanceTypes = OrdinanceTypes;
|
OrdinanceTypes = OrdinanceTypes;
|
||||||
|
|
@ -36,7 +36,7 @@ export class MonthviewComponent {
|
||||||
selectedDate$ = this.selectedDate.valueChanges.pipe(
|
selectedDate$ = this.selectedDate.valueChanges.pipe(
|
||||||
startWith(this.selectedDate.value)
|
startWith(this.selectedDate.value)
|
||||||
);
|
);
|
||||||
selectedMetric$ = new BehaviorSubject<DiagrammType>(DiagrammType.TimePerTour);
|
selectedMetric$ = new BehaviorSubject<DiagrammType>(DiagrammType.Revenue);
|
||||||
selectedViewType$ = new BehaviorSubject<DashboardViewType>(
|
selectedViewType$ = new BehaviorSubject<DashboardViewType>(
|
||||||
DashboardViewType.Gesamt
|
DashboardViewType.Gesamt
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,31 +5,31 @@
|
||||||
<div class="flex flex-wrap md:flex-nowrap">
|
<div class="flex flex-wrap md:flex-nowrap">
|
||||||
<!-- KPI -->
|
<!-- KPI -->
|
||||||
<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]="{
|
[ngClass]="{
|
||||||
'border border-blue-400':
|
'border': vm.selectedOrdinanceType === OrdinanceTypes.BTW
|
||||||
vm.selectedOrdinanceType === OrdinanceTypes.BTW
|
|
||||||
}"
|
}"
|
||||||
|
style="border-color: var(--mat-sys-primary);"
|
||||||
[text]="OrdinanceTypes.BTW"
|
[text]="OrdinanceTypes.BTW"
|
||||||
[kpis]="vm.btwKPI.summaries"
|
[kpis]="vm.btwKPI.summaries"
|
||||||
(click)="setSelectedOrdinanceType(OrdinanceTypes.BTW)"
|
(click)="setSelectedOrdinanceType(OrdinanceTypes.BTW)"
|
||||||
></dks-time-per-tour-kpi>
|
></dks-time-per-tour-kpi>
|
||||||
<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]="{
|
[ngClass]="{
|
||||||
'border border-blue-400':
|
'border': vm.selectedOrdinanceType === OrdinanceTypes.TSW
|
||||||
vm.selectedOrdinanceType === OrdinanceTypes.TSW
|
|
||||||
}"
|
}"
|
||||||
|
style="border-color: var(--mat-sys-primary);"
|
||||||
[text]="OrdinanceTypes.TSW"
|
[text]="OrdinanceTypes.TSW"
|
||||||
[kpis]="vm.tswKPI.summaries"
|
[kpis]="vm.tswKPI.summaries"
|
||||||
(click)="setSelectedOrdinanceType(OrdinanceTypes.TSW)"
|
(click)="setSelectedOrdinanceType(OrdinanceTypes.TSW)"
|
||||||
></dks-time-per-tour-kpi>
|
></dks-time-per-tour-kpi>
|
||||||
<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]="{
|
[ngClass]="{
|
||||||
'border border-blue-400':
|
'border': vm.selectedOrdinanceType === OrdinanceTypes.KTW
|
||||||
vm.selectedOrdinanceType === OrdinanceTypes.KTW
|
|
||||||
}"
|
}"
|
||||||
|
style="border-color: var(--mat-sys-primary);"
|
||||||
[text]="OrdinanceTypes.KTW"
|
[text]="OrdinanceTypes.KTW"
|
||||||
[kpis]="vm.ktwKPI.summaries"
|
[kpis]="vm.ktwKPI.summaries"
|
||||||
(click)="setSelectedOrdinanceType(OrdinanceTypes.KTW)"
|
(click)="setSelectedOrdinanceType(OrdinanceTypes.KTW)"
|
||||||
|
|
@ -40,6 +40,8 @@
|
||||||
*ngIf="vm.selectedOrdinanceType"
|
*ngIf="vm.selectedOrdinanceType"
|
||||||
>
|
>
|
||||||
<!-- Data Table -->
|
<!-- Data Table -->
|
||||||
|
<mat-card appearance="outlined" class="w-full">
|
||||||
|
<mat-card-content>
|
||||||
<ng-container [ngSwitch]="vm.selectedOrdinanceType">
|
<ng-container [ngSwitch]="vm.selectedOrdinanceType">
|
||||||
<dks-time-per-tour-by-car
|
<dks-time-per-tour-by-car
|
||||||
[columns]="vm.btwKPI.cars.columns"
|
[columns]="vm.btwKPI.cars.columns"
|
||||||
|
|
@ -60,6 +62,8 @@
|
||||||
*ngSwitchCase="OrdinanceTypes.KTW"
|
*ngSwitchCase="OrdinanceTypes.KTW"
|
||||||
></dks-time-per-tour-by-car>
|
></dks-time-per-tour-by-car>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #loadingTmpl>
|
<ng-template #loadingTmpl>
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,6 @@
|
||||||
<tr
|
<tr
|
||||||
mat-row
|
mat-row
|
||||||
*matRowDef="let carData; columns: displayedColumns"
|
*matRowDef="let carData; columns: displayedColumns"
|
||||||
class="[&_td]:border-b-0"
|
|
||||||
[ngClass]="{'hover:bg-gray-100': carData !== expandedCarData}"
|
|
||||||
(click)="setExpandedCarData(carData)"
|
(click)="setExpandedCarData(carData)"
|
||||||
></tr>
|
></tr>
|
||||||
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="!h-0"></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-header class="flex justify-center">
|
||||||
<mat-card-title>{{ text }}</mat-card-title>
|
<mat-card-title>{{ text }}</mat-card-title>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
|
|
@ -14,7 +14,8 @@
|
||||||
class="w-1/3 flex flex-col justify-center items-center"
|
class="w-1/3 flex flex-col justify-center items-center"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="font-bold border-blue-400"
|
class="font-bold"
|
||||||
|
style="border-color: var(--mat-sys-primary);"
|
||||||
[ngClass]="{ 'border-b-2': (currentTimeSlot$ | async) === idx }"
|
[ngClass]="{ 'border-b-2': (currentTimeSlot$ | async) === idx }"
|
||||||
>
|
>
|
||||||
{{ kpi.interval.start | date: 'HH:mm' }} -
|
{{ kpi.interval.start | date: 'HH:mm' }} -
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export enum OrdinanceTypes {
|
||||||
|
|
||||||
export enum DiagrammType {
|
export enum DiagrammType {
|
||||||
TimePerTour = 'TimePerTour',
|
TimePerTour = 'TimePerTour',
|
||||||
BreakEven = 'BreakEven',
|
Revenue = 'Revenue',
|
||||||
AverageRevenue = 'AverageRevenue',
|
AverageRevenue = 'AverageRevenue',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,18 @@ import CategoryColors from './category-colors';
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
selector: 'dks-ticket-item',
|
selector: 'dks-ticket-item',
|
||||||
templateUrl: './ticket-item.component.html',
|
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: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
: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 {
|
export class TicketItemComponent implements OnInit {
|
||||||
@Input() ticket!: Ticket & { tours: Tour[] };
|
@Input() ticket!: Ticket & { tours: Tour[] };
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { AsyncPipe, CurrencyPipe, NgIf } from '@angular/common';
|
import { NgIf } from '@angular/common';
|
||||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||||
import { TicketStateMeta } from '../../../core/data-access/graphql/generated/generated';
|
import { TicketStateMeta } from '../../../core/data-access/graphql/generated/generated';
|
||||||
|
|
||||||
|
|
@ -21,14 +21,11 @@ import { TicketStateMeta } from '../../../core/data-access/graphql/generated/gen
|
||||||
<span *ngIf="withTdNotCompanyCount" class="text-left text-xs">
|
<span *ngIf="withTdNotCompanyCount" class="text-left text-xs">
|
||||||
{{ ticketStateMeta.tdNotCompanyCount }} davon fehlender Transportschein
|
{{ ticketStateMeta.tdNotCompanyCount }} davon fehlender Transportschein
|
||||||
</span> `,
|
</span> `,
|
||||||
styles: [
|
host: {
|
||||||
`
|
class: 'flex flex-col items-center justify-center p-2',
|
||||||
:host {
|
},
|
||||||
@apply flex flex-col items-center justify-center p-2;
|
styles: [``],
|
||||||
}
|
imports: [NgIf],
|
||||||
`,
|
|
||||||
],
|
|
||||||
imports: [NgIf, AsyncPipe, CurrencyPipe]
|
|
||||||
})
|
})
|
||||||
export class TicketLaneFooterComponent {
|
export class TicketLaneFooterComponent {
|
||||||
@Input() ticketStateMeta!: TicketStateMeta;
|
@Input() ticketStateMeta!: TicketStateMeta;
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,14 @@ import { TicketLaneHeaderFilterComponent } from './ticket-lane-header-filter.com
|
||||||
[state]="state"
|
[state]="state"
|
||||||
(filterChange)="filterChanged($event)"
|
(filterChange)="filterChanged($event)"
|
||||||
></dks-ticket-lane-header-filter>`,
|
></dks-ticket-lane-header-filter>`,
|
||||||
|
host: {
|
||||||
|
class: 'flex items-center justify-between p-2',
|
||||||
|
},
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
: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
|
<dks-ticket-lane-header
|
||||||
class="shadow"
|
class="shadow"
|
||||||
[state]="ticketState"
|
[state]="ticketState"
|
||||||
|
|
@ -17,7 +16,7 @@
|
||||||
>
|
>
|
||||||
</dks-ticket-item>
|
</dks-ticket-item>
|
||||||
</cdk-virtual-scroll-viewport>
|
</cdk-virtual-scroll-viewport>
|
||||||
<ng-container *ngIf="ticketStateMeta$ | async as ticketStateMeta">
|
<!-- <ng-container *ngIf="ticketStateMeta$ | async as ticketStateMeta">
|
||||||
<dks-ticket-lane-footer
|
<dks-ticket-lane-footer
|
||||||
*dksRole="['manager', 'admin']"
|
*dksRole="['manager', 'admin']"
|
||||||
[ticketStateMeta]="ticketStateMeta"
|
[ticketStateMeta]="ticketStateMeta"
|
||||||
|
|
@ -33,5 +32,5 @@
|
||||||
*dksRole="['controlcenter']"
|
*dksRole="['controlcenter']"
|
||||||
[ticketStateMeta]="ticketStateMeta"
|
[ticketStateMeta]="ticketStateMeta"
|
||||||
></dks-ticket-lane-footer
|
></dks-ticket-lane-footer
|
||||||
></ng-container>
|
></ng-container> -->
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import {
|
||||||
CdkVirtualScrollViewport,
|
CdkVirtualScrollViewport,
|
||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
} from '@angular/cdk/scrolling';
|
} from '@angular/cdk/scrolling';
|
||||||
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
|
import { AsyncPipe, NgIf } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
|
@ -16,14 +16,11 @@ import {
|
||||||
Observable,
|
Observable,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
filter,
|
|
||||||
map,
|
map,
|
||||||
mergeMap,
|
mergeMap,
|
||||||
scan,
|
scan,
|
||||||
switchMap,
|
|
||||||
tap,
|
tap,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { RoleDirective } from '../../../core/components/ms-authentication/role.directive';
|
|
||||||
import {
|
import {
|
||||||
Ticket,
|
Ticket,
|
||||||
TicketFilterArgs,
|
TicketFilterArgs,
|
||||||
|
|
@ -33,32 +30,31 @@ import {
|
||||||
import { TicketItemComponent } from '../ticket-item/ticket-item.component';
|
import { TicketItemComponent } from '../ticket-item/ticket-item.component';
|
||||||
import { TicketViewComponent } from '../ticket-view/ticket-view.component';
|
import { TicketViewComponent } from '../ticket-view/ticket-view.component';
|
||||||
import { TicketsService } from '../tickets.service';
|
import { TicketsService } from '../tickets.service';
|
||||||
import { TicketLaneFooterComponent } from './ticket-lane-footer.component';
|
|
||||||
import { TicketLaneHeaderComponent } from './ticket-lane-header.component';
|
import { TicketLaneHeaderComponent } from './ticket-lane-header.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
selector: 'dks-ticket-lane',
|
selector: 'dks-ticket-lane',
|
||||||
templateUrl: './ticket-lane.component.html',
|
templateUrl: './ticket-lane.component.html',
|
||||||
|
host: {
|
||||||
|
class: 'rounded-sm h-full w-full grid',
|
||||||
|
},
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
: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;
|
grid-template-rows: auto 1fr auto;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
TicketLaneHeaderComponent,
|
TicketLaneHeaderComponent,
|
||||||
TicketLaneFooterComponent,
|
|
||||||
TicketItemComponent,
|
TicketItemComponent,
|
||||||
TicketViewComponent,
|
|
||||||
NgIf,
|
NgIf,
|
||||||
NgFor,
|
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
RoleDirective,
|
|
||||||
ScrollingModule,
|
ScrollingModule,
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
export class TicketLaneComponent {
|
export class TicketLaneComponent {
|
||||||
private dialog = inject(DialogService);
|
private dialog = inject(DialogService);
|
||||||
|
|
@ -87,12 +83,12 @@ export class TicketLaneComponent {
|
||||||
|
|
||||||
cursor$ = new BehaviorSubject<string>('');
|
cursor$ = new BehaviorSubject<string>('');
|
||||||
tickets$: Observable<(Ticket & { tours: Tour[] })[]>;
|
tickets$: Observable<(Ticket & { tours: Tour[] })[]>;
|
||||||
ticketStateMeta$ = this.ticketState$.pipe(
|
// ticketStateMeta$ = this.ticketState$.pipe(
|
||||||
filter(Boolean),
|
// filter(Boolean),
|
||||||
switchMap((ticketState) =>
|
// switchMap((ticketState) =>
|
||||||
this.ticketsService.fetchTicketStateMeta(ticketState)
|
// this.ticketsService.fetchTicketStateMeta(ticketState)
|
||||||
)
|
// )
|
||||||
);
|
// );
|
||||||
totalCount$ = new BehaviorSubject<number>(0);
|
totalCount$ = new BehaviorSubject<number>(0);
|
||||||
|
|
||||||
constructor() {
|
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: [
|
imports: [
|
||||||
MatListModule,
|
MatListModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
NgClass,
|
|
||||||
NgIf,
|
NgIf,
|
||||||
NgForOf,
|
NgForOf,
|
||||||
NgSwitch,
|
NgSwitch,
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue