(web): dashboard enhancements

This commit is contained in:
Marcel Arndt 2025-06-05 15:24:05 +02:00
parent d8a2930247
commit 44f627499a
123 changed files with 8890 additions and 2052 deletions

View File

@ -0,0 +1,5 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View File

@ -36,8 +36,9 @@
}
],
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/m3-theme.scss",
"node_modules/@ngxpert/hot-toast/src/styles/styles.css",
"src/tailwind.css",
"src/styles.scss"
],
"scripts": []

View File

@ -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

View File

@ -6,22 +6,23 @@
"start": "ng serve",
"build": "ng build -c production",
"watch": "ng build --watch --configuration development",
"test": "ng test"
"test": "ng test",
"gql-generate": "graphql-codegen --config codegen.ts"
},
"private": true,
"dependencies": {
"@angular/animations": "^19.2.6",
"@angular/cdk": "^18.1.1",
"@angular/cdk": "^19.2.9",
"@angular/common": "^19.2.6",
"@angular/compiler": "^19.2.6",
"@angular/core": "^19.2.6",
"@angular/forms": "^19.2.6",
"@angular/material": "^18.1.1",
"@angular/material-date-fns-adapter": "^18.1.1",
"@angular/material": "^19.2.9",
"@angular/material-date-fns-adapter": "^19.2.9",
"@angular/platform-browser": "^19.2.6",
"@angular/platform-browser-dynamic": "^19.2.6",
"@angular/router": "^19.2.6",
"@apollo/client": "^3.0.0",
"@apollo/client": "^3.13.8",
"@azure/msal-angular": "^3.0.22",
"@azure/msal-browser": "^3.19.1",
"@grafana/faro-web-sdk": "^1.15.0",
@ -33,16 +34,17 @@
"@opentelemetry/auto-instrumentations-web": "^0.45.1",
"@opentelemetry/context-zone": "^1.30.1",
"@opentelemetry/core": "^2.0.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.57.2",
"@opentelemetry/instrumentation": "^0.57.2",
"@opentelemetry/exporter-trace-otlp-http": "^0.48.0",
"@opentelemetry/instrumentation": "^0.48.0",
"@opentelemetry/instrumentation-document-load": "^0.44.1",
"@opentelemetry/propagator-b3": "^1.30.1",
"@opentelemetry/resources": "^1.30.1",
"@opentelemetry/sdk-trace-web": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"apollo-angular": "^7.0.2",
"@opentelemetry/semantic-conventions": "^1.34.0",
"apollo-angular": "^10.0.3",
"date-fns": "^3.6.0",
"echarts": "^5.5.1",
"graphql": "^16",
"graphql": "^16.11.0",
"graphql-ws": "^5.16.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
@ -53,6 +55,12 @@
"@angular/cli": "^19.2.7",
"@angular/compiler-cli": "^19.2.6",
"@angular/localize": "^19.2.6",
"@graphql-codegen/cli": "^5.0.5",
"@graphql-codegen/typescript": "^4.1.6",
"@graphql-codegen/typescript-apollo-angular": "^4.0.0",
"@graphql-codegen/typescript-operations": "^4.6.0",
"@material/material-color-utilities": "^0.3.0",
"@tailwindcss/postcss": "^4.1.4",
"@types/jasmine": "~5.1.0",
"autoprefixer": "^10.4.19",
"jasmine-core": "~5.1.0",
@ -62,7 +70,7 @@
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.4.39",
"tailwindcss": "^3.4.6",
"tailwindcss": "^4.1.4",
"typescript": "~5.5.2"
}
}

View File

@ -19,10 +19,10 @@ import { GlobalErrorHandler } from './core/telemetry/global-error-handler';
export const appConfig: ApplicationConfig = {
providers: [
provideAppInitializer(() => {
const initializerFn = faroInitializer();
return initializerFn();
}),
// provideAppInitializer(() => {
// const initializerFn = faroInitializer();
// return initializerFn();
// }),
{
provide: ErrorHandler,
useClass: GlobalErrorHandler,

View File

@ -3,6 +3,18 @@ import { MsalGuard } from '@azure/msal-angular';
import { roleGuard } from './core/components/ms-authentication/role.guard';
export const routes: Routes = [
{
path: 'home',
loadComponent: () =>
import('./pages/home/home.component').then(
(m) => m.HomeComponent
),
canActivate: [MsalGuard, roleGuard],
data: {
title: 'Home',
roles: ['manager', 'admin'],
},
},
{
path: 'control-center',
loadChildren: () =>

View File

@ -1,7 +1,7 @@
<mat-sidenav-container class="h-full">
<mat-sidenav
#drawer
class="w-52"
class="w-24"
fixedInViewport
[attr.role]="(isHandset$ | async) ? 'dialog' : 'navigation'"
[mode]="(isHandset$ | async) ? 'over' : 'side'"
@ -9,27 +9,33 @@
>
<mat-toolbar color="primary">Navigation</mat-toolbar>
<mat-nav-list>
<a
mat-list-item
routerLink="/home"
routerLinkActive="nav-link-active"
>Home</a
>
<a
mat-list-item
routerLink="/control-center"
routerLinkActive="!bg-yellow-400"
routerLinkActive="nav-link-active"
>Dashboard Leitstelle</a
>
<a
*dksRole="['manager', 'admin']"
mat-list-item
routerLink="/managers"
routerLinkActive="!bg-yellow-400"
routerLinkActive="nav-link-active"
>Dashboard Manager</a
>
<a
*dksRole="['manager', 'admin']"
mat-list-item
routerLink="/accounting"
routerLinkActive="!bg-yellow-400"
routerLinkActive="nav-link-active"
>Abrechnung</a
>
<a mat-list-item routerLink="/tickets" routerLinkActive="!bg-yellow-400"
<a mat-list-item routerLink="/tickets" routerLinkActive="nav-link-active"
>Ticket System</a
>
</mat-nav-list>

View File

@ -1,3 +1,16 @@
:root {
--mat-toolbar-container-background-color: #17253D;
@use '@angular/material' as mat;
:host {
@include mat.sidenav-overrides((
container-width: 16rem,
));
}
.nav-link-active {
background-color: var(--mat-sys-primary);
@include mat.list-overrides((
list-item-label-text-color: var(--mat-sys-on-primary),
list-item-hover-label-text-color: var(--mat-sys-on-primary),
list-item-focus-label-text-color: var(--mat-sys-on-primary)
));
}

View File

@ -8,8 +8,9 @@ import { AuthService } from '../ms-authentication/auth.service';
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-avatar',
template: `<div
class="flex items-center justify-center rounded-full h-10 w-10 bg-gray-100 text-blue-500 cursor-pointer transition ease-in-out hover:bg-white hover:shadow-md hover:scale-105 duration-200 "
class="flex items-center justify-center rounded-full h-10 w-10 cursor-pointer transition ease-in-out hover:shadow-md hover:scale-105 duration-200 "
[matMenuTriggerFor]="menu"
style="background-color: var(--mat-sys-primary-container); color: var(--mat-sys-on-primary-container)"
>
{{ userInitials$ | async }}
</div>

View File

@ -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;
}
}

View File

@ -8,16 +8,26 @@ export interface ConfirmDialogData {
}
@Component({
template: `<div class="flex flex-col items-center justify-start p-4">
template: `<div class="flex flex-col items-center justify-start p-4">
<div class="text-lg font-bold pb-2">{{ ref.data.title }}</div>
<div class="pb-4 border-b border-gray-400">{{ ref.data.body }}</div>
<div class="pt-4">
<button mat-stroked-button class="mr-2" dialogClose>Abbrechen</button>
<button mat-stroked-button color="warn" [dialogClose]="true">Bestätigen</button>
<button mat-button class="mr-2" dialogClose>Abbrechen</button>
<button mat-button class="warn-btn" [dialogClose]="true">Bestätigen</button>
</div>
</div>`,
imports: [MatButtonModule, DialogCloseDirective],
changeDetection: ChangeDetectionStrategy.OnPush
styles: `@use "@angular/material" as mat;
.warn-btn {
@include mat.button-overrides(
(
text-label-text-color: var(--mat-sys-error)
)
);
}
`,
imports: [MatButtonModule, DialogCloseDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfirmDialogComponent {
ref: DialogRef<ConfirmDialogData> = inject(DialogRef);

View File

@ -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()"
>
&#9432;
</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>

View File

@ -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();
});
});

View File

@ -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 {}

View File

@ -1,4 +1,4 @@
import { Injectable, OnDestroy, inject } from '@angular/core';
import { Injectable, OnDestroy, inject, signal } from '@angular/core';
import { MsalBroadcastService, MsalService } from '@azure/msal-angular';
import { AccountInfo, InteractionStatus } from '@azure/msal-browser';
import {
@ -20,12 +20,14 @@ export class AuthService implements OnDestroy {
activeAccount$ = this.account$.pipe(
tap((account) =>
this.msalService.instance.setActiveAccount(account ?? null)
)
),
tap((account) => this.activeAccount.set(account))
);
activeAccount = signal<AccountInfo | undefined>(undefined);
roles$ = this.activeAccount$.pipe(
filter(Boolean),
map((account) => account?.idTokenClaims?.roles),
shareReplay(1),
shareReplay(1)
);
msalService = inject(MsalService);

View File

@ -13,10 +13,14 @@ import { EChartsOption } from 'echarts';
import { filter, interval, map, Observable, Subscription } from 'rxjs';
import * as echarts from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
import waldenTheme from './walden.theme.json';
import westerosTheme from './westeros.theme.json';
const CUSTOM_THEME_TOKEN = 'customTheme';
@Directive({
selector: '[dksEcharts]',
standalone: false
selector: '[dksEcharts]',
standalone: false,
})
export class EchartsDirective implements OnInit, OnDestroy, OnChanges {
@Input() options?: EChartsOption;
@ -38,16 +42,21 @@ export class EchartsDirective implements OnInit, OnDestroy, OnChanges {
ngOnInit(): void {
echarts.use([...this.extentions, CanvasRenderer]);
this.echartsInstance = echarts.init(this._el.nativeElement, this.theme, {
width:
this._el.nativeElement.clientWidth === 0
? this.defaultWidth
: undefined,
height:
this._el.nativeElement.clientHeight === 0
? this.defaultHeight
: undefined,
});
echarts.registerTheme(CUSTOM_THEME_TOKEN, westerosTheme.theme);
this.echartsInstance = echarts.init(
this._el.nativeElement,
CUSTOM_THEME_TOKEN,
{
width:
this._el.nativeElement.clientWidth === 0
? this.defaultWidth
: undefined,
height:
this._el.nativeElement.clientHeight === 0
? this.defaultHeight
: undefined,
}
);
this.chartInit.emit(this.echartsInstance);
this.setParams();
if (this.isResizable) {
@ -93,7 +102,7 @@ export class EchartsDirective implements OnInit, OnDestroy, OnChanges {
private getWidthSensor(
element: HTMLElement,
periodicityInMilliseconds: number = 2000
periodicityInMilliseconds: number = 1000
): Observable<number> {
let oldWidth = element.clientWidth;
return interval(periodicityInMilliseconds).pipe(

View File

@ -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"
}
}
}
}

View File

@ -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"
}
}

View File

@ -82,6 +82,19 @@ export type DocumentInfo = {
tdLocation?: Maybe<Scalars['String']['output']>;
};
export type HomeDashboardKpIs = {
__typename?: 'HomeDashboardKPIs';
anomalyCount: Scalars['Int']['output'];
billableTourCount: Scalars['Int']['output'];
billableTourRevenueSum: Scalars['Float']['output'];
billedTourCount: Scalars['Int']['output'];
billedTourNegativeRevenueDeviation: Scalars['Float']['output'];
billedTourPositiveRevenueDeviation: Scalars['Float']['output'];
employeesWithoutTourCount: Scalars['Int']['output'];
revenueTrend: Array<RevenueTrend>;
toursWithoutApprovalCount: Scalars['Int']['output'];
};
export type Interval = {
__typename?: 'Interval';
end: Scalars['DateTime']['output'];
@ -108,6 +121,19 @@ export type KpiMetadata = {
tourAmount: Scalars['Float']['output'];
};
export type KpiInfo = {
__typename?: 'KpiInfo';
additional: Array<Metric>;
main: Metric;
secondary: Metric;
};
export type Metric = {
__typename?: 'Metric';
label: Scalars['String']['output'];
value: Scalars['Float']['output'];
};
export type Mutation = {
__typename?: 'Mutation';
acceptAnomaly: Anomaly;
@ -175,19 +201,19 @@ export type Query = {
anomalies: PaginatedAnomaly;
anomaliesProto: Array<Anomaly>;
anomaly: Anomaly;
anomalyAmountByType: Array<Report>;
averageAnomalyResolutionTimeReport: Array<Report>;
groupedAnomalies: PaginatedAnomalyGroup;
homeDashboardKpis: HomeDashboardKpIs;
managerKpi: KpiInfo;
searchTickets: Array<Ticket>;
ticket: Ticket;
ticketStateKeyFigures: TicketStateKeyFigures;
ticketStateKpi: KpiInfo;
ticketStateMeta: TicketStateMeta;
tickets: PaginatedTicket;
timePerTourMetricForDay: Array<TimePerTourMetric>;
timePerTourMetricForMonth: Array<TimePerTourMetric>;
tour: Tour;
tourCostBreakdown: TourCostBreakdown;
toursPerTimeKPI: Array<TimePerTourKpiSummary>;
upcomingTourNotificationReport: Array<Report>;
upcomingTours: Array<Tour>;
};
@ -222,6 +248,11 @@ export type QueryGroupedAnomaliesArgs = {
};
export type QueryManagerKpiArgs = {
ordinanceType: Scalars['String']['input'];
};
export type QuerySearchTicketsArgs = {
query: Scalars['String']['input'];
};
@ -237,6 +268,11 @@ export type QueryTicketStateKeyFiguresArgs = {
};
export type QueryTicketStateKpiArgs = {
state: TicketValidationState;
};
export type QueryTicketStateMetaArgs = {
state: TicketValidationState;
};
@ -262,7 +298,7 @@ export type QueryTimePerTourMetricForMonthArgs = {
};
export type QueryTourArgs = {
export type QueryTourCostBreakdownArgs = {
operationId: Scalars['String']['input'];
};
@ -271,10 +307,18 @@ export type QueryToursPerTimeKpiArgs = {
ordinanceTypes: Scalars['String']['input'];
};
export type Report = {
__typename?: 'Report';
label: Scalars['String']['output'];
export type RevenueTrend = {
__typename?: 'RevenueTrend';
date: Scalars['DateTime']['output'];
ordinanceType: Scalars['String']['output'];
revenueSum: Scalars['Float']['output'];
};
export type ServicePosition = {
__typename?: 'ServicePosition';
description: Scalars['String']['output'];
value: Scalars['Float']['output'];
variant: Scalars['String']['output'];
};
export type SolutionTarget = {
@ -433,9 +477,18 @@ export type Tour = {
type: Scalars['String']['output'];
};
export type TourCostBreakdown = {
__typename?: 'TourCostBreakdown';
servicePositions: Array<ServicePosition>;
tariffDescription: Scalars['String']['output'];
tariffValidityPeriod: Scalars['String']['output'];
};
export type TourStateMeta = {
__typename?: 'TourStateMeta';
count: Scalars['Int']['output'];
negativeRevenueDeviation?: Maybe<Scalars['Float']['output']>;
positiveRevenueDeviation?: Maybe<Scalars['Float']['output']>;
revenueSum?: Maybe<Scalars['Float']['output']>;
};
@ -471,6 +524,11 @@ export type AcceptAnomalyMutationVariables = Exact<{
export type AcceptAnomalyMutation = { __typename?: 'Mutation', acceptAnomaly: { __typename?: 'Anomaly', id: string, type: string, description: string, groupKey: string, groupDescription: string, sleepTimer: number, sleepSince?: any | null, workedOnBy: string, resolvedAt?: any | null, createdAt: any, solution: Array<{ __typename?: 'SolutionTarget', anomalyId: string, id: string, name: string, steps: Array<{ __typename?: 'SolutionTargetStep', description: string, id: string, solutionTargetId: string }> }> } };
export type HomeDashboardQueryVariables = Exact<{ [key: string]: never; }>;
export type HomeDashboardQuery = { __typename?: 'Query', homeDashboardKpis: { __typename?: 'HomeDashboardKPIs', anomalyCount: number, employeesWithoutTourCount: number, toursWithoutApprovalCount: number, billableTourCount: number, billableTourRevenueSum: number, billedTourCount: number, billedTourPositiveRevenueDeviation: number, billedTourNegativeRevenueDeviation: number, revenueTrend: Array<{ __typename?: 'RevenueTrend', date: any, ordinanceType: string, revenueSum: number }> } };
export type TimePerTourUpdatedSubscriptionVariables = Exact<{ [key: string]: never; }>;
@ -506,20 +564,19 @@ export type GetTimePerTourMetricForMonthQueryVariables = Exact<{
export type GetTimePerTourMetricForMonthQuery = { __typename?: 'Query', timePerTourMetricForMonth: Array<{ __typename?: 'TimePerTourMetric', availableTime: number, tourAmount: number, breakEven: number, revenue: number, averageRevenue: number, personnelCost: number, consumptionCost: number, forecast: number, forecastRevenue: number, interval: { __typename?: 'Interval', start: any, end: any }, categoryAmounts: Array<{ __typename?: 'TimePerTourMetricCategoryAmount', category: string, amount: number }> }> };
export type GetAnomalyByTypeReportQueryVariables = Exact<{ [key: string]: never; }>;
export type TicketStateKpiQueryVariables = Exact<{
state: TicketValidationState;
}>;
export type GetAnomalyByTypeReportQuery = { __typename?: 'Query', anomalyAmountByType: Array<{ __typename?: 'Report', label: string, value: number }> };
export type TicketStateKpiQuery = { __typename?: 'Query', ticketStateKpi: { __typename?: 'KpiInfo', main: { __typename?: 'Metric', label: string, value: number }, secondary: { __typename?: 'Metric', label: string, value: number }, additional: Array<{ __typename?: 'Metric', label: string, value: number }> } };
export type GetUpcomingTourNotificationReportQueryVariables = Exact<{ [key: string]: never; }>;
export type ManagerKpiQueryVariables = Exact<{
ordinanceType: Scalars['String']['input'];
}>;
export type GetUpcomingTourNotificationReportQuery = { __typename?: 'Query', upcomingTourNotificationReport: Array<{ __typename?: 'Report', label: string, value: number }> };
export type GetAverageAnomalyResolutionTimeReportQueryVariables = Exact<{ [key: string]: never; }>;
export type GetAverageAnomalyResolutionTimeReportQuery = { __typename?: 'Query', averageAnomalyResolutionTimeReport: Array<{ __typename?: 'Report', label: string, value: number }> };
export type ManagerKpiQuery = { __typename?: 'Query', managerKpi: { __typename?: 'KpiInfo', main: { __typename?: 'Metric', label: string, value: number }, secondary: { __typename?: 'Metric', label: string, value: number }, additional: Array<{ __typename?: 'Metric', label: string, value: number }> } };
export type TicketsUpdatedSubscriptionVariables = Exact<{ [key: string]: never; }>;
@ -569,7 +626,7 @@ export type GetAccountingStateMetaQueryVariables = Exact<{
}>;
export type GetAccountingStateMetaQuery = { __typename?: 'Query', accountingStateMeta: { __typename?: 'TourStateMeta', count: number, revenueSum?: number | null } };
export type GetAccountingStateMetaQuery = { __typename?: 'Query', accountingStateMeta: { __typename?: 'TourStateMeta', count: number, revenueSum?: number | null, positiveRevenueDeviation?: number | null, negativeRevenueDeviation?: number | null } };
export type GetAccountingToursQueryVariables = Exact<{
state: AccountingState;
@ -581,6 +638,13 @@ export type GetAccountingToursQueryVariables = Exact<{
export type GetAccountingToursQuery = { __typename?: 'Query', accountingTours: { __typename?: 'PaginatedTour', prevCursor: string, endCursor: string, hasNextPage: boolean, totalCount: number, nodes: Array<{ __typename?: 'Tour', id: string, done: boolean, direction: string, carName?: string | null, operationId: string, driverName?: string | null, codriverName?: string | null, patientName?: string | null, patientSurname?: string | null, patientStreet?: string | null, patientZip?: string | null, patientCity?: string | null, healthInsurance?: string | null, category?: string | null, transportType?: string | null, ordinanceType?: string | null, startStreet?: string | null, startZip?: string | null, startCity?: string | null, targetStreet?: string | null, targetZip?: string | null, targetCity?: string | null, startDate?: any | null, check: number, occupiedKm: number, totalKm: number, type: string, rangeEndDate?: any | null, startBegin?: string | null, startEnd?: string | null, target?: string | null, targetBegin?: string | null, empty?: string | null, billDate?: any | null, billNumber?: string | null, consumptionCosts: number, revenue: number, revenueDispoLive: number, revenueDeviation: number, startInstitution?: string | null, targetInstitution?: string | null, ticketId?: string | null, createdAt?: any | null, ticket?: { __typename?: 'Ticket', id: string, currentState: TicketValidationState, approvalState?: ApprovalState | null, errors: Array<string>, notes: Array<string>, isUrgent: boolean, urgency: any, createdAt: any } | null }> } };
export type GetTourCostBreakdownQueryVariables = Exact<{
operationId: Scalars['String']['input'];
}>;
export type GetTourCostBreakdownQuery = { __typename?: 'Query', tourCostBreakdown: { __typename?: 'TourCostBreakdown', tariffDescription: string, tariffValidityPeriod: string, servicePositions: Array<{ __typename?: 'ServicePosition', value: number, description: string, variant: string }> } };
export const AnomalyAllFragmentDoc = gql`
fragment AnomalyAll on Anomaly {
id
@ -688,6 +752,36 @@ export const AcceptAnomalyDocument = gql`
export class AcceptAnomalyGQL extends Apollo.Mutation<AcceptAnomalyMutation, AcceptAnomalyMutationVariables> {
override document = AcceptAnomalyDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
}
}
export const HomeDashboardDocument = gql`
query HomeDashboard {
homeDashboardKpis {
anomalyCount
employeesWithoutTourCount
toursWithoutApprovalCount
billableTourCount
billableTourRevenueSum
billedTourCount
billedTourPositiveRevenueDeviation
billedTourNegativeRevenueDeviation
revenueTrend {
date
ordinanceType
revenueSum
}
}
}
`;
@Injectable({
providedIn: 'root'
})
export class HomeDashboardGQL extends Apollo.Query<HomeDashboardQuery, HomeDashboardQueryVariables> {
override document = HomeDashboardDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
}
@ -844,11 +938,21 @@ export const GetTimePerTourMetricForMonthDocument = gql`
super(apollo);
}
}
export const GetAnomalyByTypeReportDocument = gql`
query GetAnomalyByTypeReport {
anomalyAmountByType {
label
value
export const TicketStateKpiDocument = gql`
query TicketStateKpi($state: TicketValidationState!) {
ticketStateKpi(state: $state) {
main {
label
value
}
secondary {
label
value
}
additional {
label
value
}
}
}
`;
@ -856,46 +960,37 @@ export const GetAnomalyByTypeReportDocument = gql`
@Injectable({
providedIn: 'root'
})
export class GetAnomalyByTypeReportGQL extends Apollo.Query<GetAnomalyByTypeReportQuery, GetAnomalyByTypeReportQueryVariables> {
override document = GetAnomalyByTypeReportDocument;
export class TicketStateKpiGQL extends Apollo.Query<TicketStateKpiQuery, TicketStateKpiQueryVariables> {
override document = TicketStateKpiDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
}
}
export const GetUpcomingTourNotificationReportDocument = gql`
query GetUpcomingTourNotificationReport {
upcomingTourNotificationReport {
label
value
}
}
`;
@Injectable({
providedIn: 'root'
})
export class GetUpcomingTourNotificationReportGQL extends Apollo.Query<GetUpcomingTourNotificationReportQuery, GetUpcomingTourNotificationReportQueryVariables> {
override document = GetUpcomingTourNotificationReportDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
export const ManagerKpiDocument = gql`
query ManagerKpi($ordinanceType: String!) {
managerKpi(ordinanceType: $ordinanceType) {
main {
label
value
}
secondary {
label
value
}
additional {
label
value
}
}
export const GetAverageAnomalyResolutionTimeReportDocument = gql`
query GetAverageAnomalyResolutionTimeReport {
averageAnomalyResolutionTimeReport {
label
value
}
}
`;
@Injectable({
providedIn: 'root'
})
export class GetAverageAnomalyResolutionTimeReportGQL extends Apollo.Query<GetAverageAnomalyResolutionTimeReportQuery, GetAverageAnomalyResolutionTimeReportQueryVariables> {
override document = GetAverageAnomalyResolutionTimeReportDocument;
export class ManagerKpiGQL extends Apollo.Query<ManagerKpiQuery, ManagerKpiQueryVariables> {
override document = ManagerKpiDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
@ -1220,6 +1315,8 @@ export const GetAccountingStateMetaDocument = gql`
accountingStateMeta(state: $state) {
count
revenueSum
positiveRevenueDeviation
negativeRevenueDeviation
}
}
`;
@ -1306,6 +1403,30 @@ export const GetAccountingToursDocument = gql`
export class GetAccountingToursGQL extends Apollo.Query<GetAccountingToursQuery, GetAccountingToursQueryVariables> {
override document = GetAccountingToursDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
}
}
export const GetTourCostBreakdownDocument = gql`
query GetTourCostBreakdown($operationId: String!) {
tourCostBreakdown(operationId: $operationId) {
tariffDescription
tariffValidityPeriod
servicePositions {
value
description
variant
}
}
}
`;
@Injectable({
providedIn: 'root'
})
export class GetTourCostBreakdownGQL extends Apollo.Query<GetTourCostBreakdownQuery, GetTourCostBreakdownQueryVariables> {
override document = GetTourCostBreakdownDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
}

View File

@ -0,0 +1,17 @@
query HomeDashboard {
homeDashboardKpis {
anomalyCount
employeesWithoutTourCount
toursWithoutApprovalCount
billableTourCount
billableTourRevenueSum
billedTourCount
billedTourPositiveRevenueDeviation
billedTourNegativeRevenueDeviation
revenueTrend {
date
ordinanceType
revenueSum
}
}
}

View File

@ -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
}
}
}

View File

@ -1,20 +0,0 @@
query GetAnomalyByTypeReport {
anomalyAmountByType {
label
value
}
}
query GetUpcomingTourNotificationReport {
upcomingTourNotificationReport {
label
value
}
}
query GetAverageAnomalyResolutionTimeReport {
averageAnomalyResolutionTimeReport {
label
value
}
}

View File

@ -28,6 +28,8 @@ query GetAccountingStateMeta($state: AccountingState!) {
accountingStateMeta(state: $state) {
count
revenueSum
positiveRevenueDeviation
negativeRevenueDeviation
}
}
@ -105,3 +107,15 @@ query GetAccountingTours(
}
}
}
query GetTourCostBreakdown($operationId: String!) {
tourCostBreakdown(operationId: $operationId) {
tariffDescription
tariffValidityPeriod
servicePositions {
value
description
variant
}
}
}

View File

@ -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: /.+/,
},
}),
],
});

View File

@ -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();
});
}
}

View File

@ -12,12 +12,17 @@ import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { HotToastService } from '@ngxpert/hot-toast';
import { Tour, Ticket, AccountingState, TicketValidationState } from '../../../core/data-access/graphql/generated/generated';
import {
AccountingState,
Ticket,
TicketValidationState,
Tour,
} from '../../../core/data-access/graphql/generated/generated';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-accounting-item',
template: ` <div class="flex justify-between">
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-accounting-item',
template: ` <div class="flex justify-between">
<div class="flex items-center">
<button
mat-icon-button
@ -44,22 +49,30 @@ import { Tour, Ticket, AccountingState, TicketValidationState } from '../../../c
tour.revenueDeviation | currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE'
}}
</ng-template>`,
styles: [
`
host: {
class: 'flex flex-col rounded border p-2 m-1 bg-white cursor-pointer',
},
styles: [
`
@use '@angular/material' as mat;
:host {
@apply flex flex-col rounded border p-2 m-1 bg-white cursor-pointer;
@include mat.icon-button-overrides(
(
icon-size: 14,
)
);
}
`,
],
imports: [
MatButtonModule,
MatIconModule,
MatBadgeModule,
ClipboardModule,
NgIf,
NgClass,
CurrencyPipe,
]
],
imports: [
MatButtonModule,
MatIconModule,
MatBadgeModule,
ClipboardModule,
NgIf,
NgClass,
CurrencyPipe,
],
})
export class AccountingItemComponent implements AfterViewInit {
toast = inject(HotToastService);

View File

@ -3,9 +3,9 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { TourStateMeta } from '../../../core/data-access/graphql/generated/generated';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-accounting-lane-footer',
template: `<span *ngIf="withTourCount" class="text-left text-xs">
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-accounting-lane-footer',
template: `<span *ngIf="withTourCount" class="text-left text-xs">
{{ tourStateMeta.count }} Touren
</span>
<span *ngIf="withRevenueSum" class="text-left text-xs">
@ -14,14 +14,11 @@ import { TourStateMeta } from '../../../core/data-access/graphql/generated/gener
| currency : 'EUR' : 'symbol' : '1.2-2' : 'de-DE'
}}
</span>`,
styles: [
`
:host {
@apply flex flex-col items-center justify-center p-2;
}
`,
],
imports: [NgIf, AsyncPipe, CurrencyPipe]
host: {
class: 'flex flex-col items-center justify-center p-2',
},
styles: [``],
imports: [NgIf, CurrencyPipe],
})
export class AccountingLaneFooterComponent {
@Input() tourStateMeta!: TourStateMeta;

View File

@ -6,13 +6,16 @@ import {
Input,
Output,
} from '@angular/core';
import {
AccountingFilterArgs,
AccountingState,
} from '../../../core/data-access/graphql/generated/generated';
import { AccountingLaneHeaderFilterComponent } from './accounting-lane-header-filter.component';
import { AccountingFilterArgs, AccountingState } from '../../../core/data-access/graphql/generated/generated';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-accounting-lane-header',
template: `<div></div>
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-accounting-lane-header',
template: `<div></div>
<ng-container [ngSwitch]="state">
<div *ngSwitchCase="AccountingState.Billable">Abrechenbar</div>
<div *ngSwitchCase="AccountingState.PreparedForBilling">
@ -26,14 +29,17 @@ import { AccountingFilterArgs, AccountingState } from '../../../core/data-access
[state]="state"
(filterChange)="filterChanged($event)"
></dks-accounting-lane-header-filter>`,
styles: [
`
host: {
class: 'flex items-center justify-between p-2 bg-secondary-container',
},
styles: [
`
:host {
@apply flex items-center justify-between p-2 bg-gray-50;
background-color: var(--mat-sys-secondary-container);
}
`,
],
imports: [NgSwitch, NgSwitchCase, AccountingLaneHeaderFilterComponent]
],
imports: [NgSwitch, NgSwitchCase, AccountingLaneHeaderFilterComponent],
})
export class AccountingLaneHeaderComponent {
@Input() state!: AccountingState;

View File

@ -10,18 +10,19 @@
>
<dks-accounting-item
*cdkVirtualFor="let tour of tours; trackBy: trackByFn"
class="mat-elevation-z4 animate-fadeIn"
class="animate-fadeIn"
style="box-shadow: var(--mat-sys-level2);"
[accountingState]="accountingState"
[tour]="tour"
(click)="openTourView(tour)"
>
</dks-accounting-item>
</cdk-virtual-scroll-viewport>
<ng-container *ngIf="accountingStateMeta$ | async as accountingStateMeta">
<!-- <ng-container *ngIf="accountingStateMeta$ | async as accountingStateMeta">
<dks-accounting-lane-footer
[tourStateMeta]="accountingStateMeta"
[withTourCount]="true"
[withRevenueSum]="accountingState !== AccountingState.Billed"
></dks-accounting-lane-footer>
</ng-container>
</ng-container> -->
</ng-container>

View File

@ -2,7 +2,7 @@ import {
CdkVirtualScrollViewport,
ScrollingModule,
} from '@angular/cdk/scrolling';
import { AsyncPipe, CurrencyPipe, NgClass, NgFor, NgIf } from '@angular/common';
import { AsyncPipe, NgIf } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -32,32 +32,31 @@ import {
import { AccountingTourViewComponent } from '../accounting-tour-view/accounting-tour-view.component';
import { DashboardAccountingService } from '../dashboard-accounting.service';
import { AccountingItemComponent } from './accounting-item.component';
import { AccountingLaneFooterComponent } from './accounting-lane-footer.component';
import { AccountingLaneHeaderComponent } from './accounting-lane-header.component';
@Component({
selector: 'dks-accounting-lane',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './accounting-lane.component.html',
styles: [
`
selector: 'dks-accounting-lane',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './accounting-lane.component.html',
host: {
class: 'rounded h-full w-full grid',
},
styles: [
`
:host {
@apply rounded bg-gray-200 h-full w-full grid;
box-shadow: var(--mat-sys-level1);
background-color: var(--mat-sys-surface-container-low);
grid-template-rows: auto 1fr auto;
}
`,
],
imports: [
AccountingLaneHeaderComponent,
AccountingLaneFooterComponent,
AccountingItemComponent,
NgFor,
NgIf,
NgClass,
AsyncPipe,
CurrencyPipe,
ScrollingModule,
]
],
imports: [
AccountingLaneHeaderComponent,
AccountingItemComponent,
NgIf,
AsyncPipe,
ScrollingModule,
],
})
export class AccountingLaneComponent {
private dialog = inject(DialogService);
@ -91,7 +90,7 @@ export class AccountingLaneComponent {
this.accountingService.fetchAccountingStateMeta(accountingState)
)
);
tours$: Observable<Tour[]>;
tours$: Observable<(Tour & { ticket: Ticket })[]>;
constructor() {
const batchMap: Observable<Record<string, Tour & { ticket: Ticket }>> =

View File

@ -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);
}
}

View File

@ -95,7 +95,6 @@ import { Ticket, TicketValidationState, Tour } from '../../../core/data-access/g
NgFor,
NgSwitch,
NgSwitchCase,
DatePipe,
]
})
export class AccountingTourTicketComponent {

View File

@ -8,17 +8,23 @@ import {
} from '../../../core/data-access/graphql/generated/generated';
import { AccountingTourOverviewComponent } from './accounting-tour-overview.component';
import { AccountingTourTicketComponent } from './accounting-tour-ticket.component';
import { AccountingTourCostBreakdownComponent } from './accounting-tour-cost-breakdown.component';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-accounting-tour-view',
template: `<h3 class="py-2 px-3 !mb-0">
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-accounting-tour-view',
template: `<h3 class="py-2 px-3 !mb-0">
{{ ref.data.ordinanceType }} -
{{ ref.data.patientName }}
{{ ref.data.patientSurname }}
</h3>
<hr />
<mat-tab-group>
<mat-tab label="Umsatzberechnung">
<app-accounting-tour-cost-breakdown
[tour]="ref.data"
></app-accounting-tour-cost-breakdown>
</mat-tab>
<mat-tab label="Übersicht">
<dks-accounting-tour-overview
[tour]="ref.data"
@ -30,15 +36,16 @@ import { AccountingTourTicketComponent } from './accounting-tour-ticket.componen
></dks-accounting-tour-ticket>
</mat-tab>
</mat-tab-group> `,
styles: [],
imports: [
MatTabsModule,
MatIconModule,
AccountingTourOverviewComponent,
AccountingTourTicketComponent,
]
styles: [],
imports: [
MatTabsModule,
MatIconModule,
AccountingTourOverviewComponent,
AccountingTourTicketComponent,
AccountingTourCostBreakdownComponent
],
})
export class AccountingTourViewComponent {
ref: DialogRef<Tour & { ticket: Ticket }, undefined | any> =
inject(DialogRef);
ref =
inject<DialogRef<Tour & { ticket: Ticket }, undefined | any>>(DialogRef);
}

View File

@ -1,34 +1,52 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AccountingLaneComponent } from './accounting-lane/accounting-lane.component';
import { AccountingState } from '../../core/data-access/graphql/generated/generated';
import { AccountingInfoCardComponent } from './accounting-info-card/accounting-info-card.component';
import { AccountingLaneComponent } from './accounting-lane/accounting-lane.component';
@Component({
selector: 'dks-dashboard-accounting',
imports: [AccountingLaneComponent],
template: `<section class="h-full p-6 grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="col-span-1 md:col-span-5 flex justify-end items-center">
selector: 'dks-dashboard-accounting',
imports: [AccountingInfoCardComponent, AccountingLaneComponent],
template: `<section class="h-full p-6 grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="col-span-1 md:col-span-3 flex justify-end items-center">
<!-- Action Buttons -->
</div>
<dks-accounting-lane
<app-accounting-info-card
class="self-center"
[accountingState]="AccountingState.Billable"
></dks-accounting-lane>
></app-accounting-info-card>
<app-accounting-info-card
class="self-center"
[accountingState]="AccountingState.PreparedForBilling"
></app-accounting-info-card>
<app-accounting-info-card
class="self-center"
[accountingState]="AccountingState.Billed"
></app-accounting-info-card>
<div>
<dks-accounting-lane
[accountingState]="AccountingState.Billable"
></dks-accounting-lane>
</div>
<dks-accounting-lane
[accountingState]="AccountingState.PreparedForBilling"
></dks-accounting-lane>
<dks-accounting-lane [accountingState]="AccountingState.Billed"></dks-accounting-lane>
<dks-accounting-lane
[accountingState]="AccountingState.Billed"
></dks-accounting-lane>
</section>`,
styles: [
`
styles: [
`
:host {
display: block;
height: calc(100% - 64px);
}
section {
grid-template-rows: 2rem 1fr;
grid-template-rows: 2rem auto 1fr;
/* grid-template-rows: 2rem 1fr; */
}
`,
],
changeDetection: ChangeDetectionStrategy.OnPush
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DashboardAccountingComponent {
AccountingState = AccountingState;

View File

@ -1,3 +0,0 @@
:host {
@apply w-full rounded flex items-center;
}

View File

@ -35,6 +35,9 @@ interface Anomaly {
selector: 'dks-anomaly-list-item',
templateUrl: './anomaly-list-item.component.html',
styleUrls: ['./anomaly-list-item.component.css'],
host: {
class: 'w-full rounded flex items-center',
},
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
})

View File

@ -1,3 +0,0 @@
:host {
@apply flex flex-col h-full;
}

View File

@ -3,7 +3,7 @@ import {
Component,
OnDestroy,
OnInit,
ViewChild
ViewChild,
} from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import {
@ -12,17 +12,20 @@ import {
shareReplay,
Subscription,
switchMap,
tap
tap,
} from 'rxjs';
import { AnomaliesService } from '../anomalies.service';
@Component({
selector: 'dks-anomaly-list',
templateUrl: './anomaly-list.component.html',
styleUrls: ['./anomaly-list.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
selector: 'dks-anomaly-list',
templateUrl: './anomaly-list.component.html',
styleUrls: ['./anomaly-list.component.scss'],
host: {
class: 'flex flex-col h-full',
},
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class AnomalyListComponent implements OnInit, OnDestroy {
@ViewChild(MatPaginator, { static: true }) private paginator!: MatPaginator;

View File

@ -12,10 +12,10 @@
</article>
<mat-divider></mat-divider>
<div class="flex justify-end mt-2">
<button mat-button color="primary" (click)="ref.close('sleep')">
<button mat-button (click)="ref.close('sleep')">
<mat-icon>schedule</mat-icon> Stummschalten
</button>
<button mat-button color="warn" (click)="ref.close('accept')">
<button mat-button class="warn-btn" (click)="ref.close('accept')">
<mat-icon>check_circle_outline</mat-icon> Akzeptieren
</button>
</div>

View File

@ -0,0 +1,9 @@
@use "@angular/material" as mat;
.warn-btn {
@include mat.button-overrides(
(
text-label-text-color: var(--mat-sys-error)
)
);
}

View File

@ -11,7 +11,10 @@ interface SolutionTarget {
@Component({
selector: 'dks-anomaly-solution-dialog',
templateUrl: './anomaly-solution-dialog.component.html',
styleUrls: ['./anomaly-solution-dialog.component.css'],
styleUrls: ['./anomaly-solution-dialog.component.scss'],
host: {
class: 'm-4',
},
standalone: false
})
export class AnomalySolutionDialogComponent {

View File

@ -1,3 +0,0 @@
:host {
@apply flex flex-col h-full;
}

View File

@ -27,7 +27,7 @@
</mat-icon>
</button>
{{ node.name }}
<div class="ml-auto bg-blue-400 rounded-full w-5 h-5 flex justify-center items-center text-white font-bold">{{ node?.childCount }}</div>
<div class="ml-auto rounded-full w-5 h-5 flex justify-center items-center font-bold" style="background-color: var(--mat-sys-primary-container); color: var(--mat-sys-on-primary-container)">{{ node?.childCount }}</div>
</mat-tree-node>
</mat-tree>
<ng-template #emptyListTmpl>

View File

@ -43,7 +43,10 @@ interface AnomalyFlatNode {
@Component({
selector: 'dks-anomaly-tree',
templateUrl: './anomaly-tree.component.html',
styleUrls: ['./anomaly-tree.component.css'],
styleUrls: ['./anomaly-tree.component.scss'],
host: {
class: 'flex flex-col h-full',
},
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
})

View File

@ -14,7 +14,7 @@
></dks-tours-per-time-kpi>
</div>
<div class="flex lg:flex-nowrap flex-wrap-reverse">
<mat-card class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col">
<mat-card appearance="outlined" class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col">
<mat-card-header>
<mat-card-title> Anomalien </mat-card-title>
</mat-card-header>
@ -22,7 +22,7 @@
<dks-anomaly-tree></dks-anomaly-tree>
</mat-card-content>
</mat-card>
<mat-card class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col">
<mat-card appearance="outlined" class="w-full md:flex-grow lg:w-2/5 m-2 !flex flex-col">
<mat-card-header>
<mat-card-title> Mitarbeiter ohne Tour </mat-card-title>
</mat-card-header>
@ -30,12 +30,12 @@
<dks-anomaly-list></dks-anomaly-list>
</mat-card-content>
</mat-card>
<mat-card class="w-full lg:w-1/5 m-2">
<mat-card appearance="outlined" class="w-full lg:w-1/5 m-2">
<mat-card-header>
<mat-card-title> Nächste Touren ohne Anfahrt </mat-card-title>
</mat-card-header>
<mat-card-content>
<!-- <dks-upcoming-tours></dks-upcoming-tours> -->
<dks-upcoming-tours></dks-upcoming-tours>
</mat-card-content>
</mat-card>
</div>

View File

@ -15,7 +15,7 @@ import { AnomalyListItemComponent } from './anomaly-list/anomaly-list-item/anoma
import { AnomalyListComponent } from './anomaly-list/anomaly-list.component';
import { AnomalySolutionDialogComponent } from './anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component';
import { AnomalyTreeComponent } from './anomaly-tree/anomaly-tree.component';
import { ControlCenterDashboardComponent } from './control-center-dashboard/control-center-dashboard.component';
import { ControlCenterDashboardComponent } from './control-center-dashboard.component';
import { ToursPerTimeKPIComponent } from './kpis/tours-per-time-kpi/tours-per-time-kpi.component';
import { TourViewDialogComponent } from './upcoming-tours/tour-view-dialog/tour-view-dialog.component';
import { UpcomingToursComponent } from './upcoming-tours/upcoming-tours.component';

View File

@ -1,4 +1,4 @@
<mat-card>
<mat-card appearance="raised">
<mat-card-header class="flex justify-center">
<mat-card-title>{{ ordinanceType }}</mat-card-title>
</mat-card-header>
@ -6,17 +6,18 @@
<div class="flex">
<div
*ngFor="let kpi of kpis; index as idx"
class="w-1/3 flex flex-col justify-center items-center border-blue-400"
[ngClass]="{'border-b-2': (currentTimeSlot$ | async) === idx}"
class="w-1/3 flex flex-col justify-center items-center"
style="border-color: var(--mat-sys-primary);"
[ngClass]="{
'border-b-2': (currentTimeSlot$ | async) === idx,
}"
>
<div
class="p-4 text-base"
>
{{ kpi.kpi.value.toString().replace('.', ',') }}
<div class="p-4 text-base">
{{ kpi.kpi.value.toString().replace(".", ",") }}
</div>
<div class="font-bold">
{{ kpi.interval.start | date: 'HH:mm' }} -
{{ kpi.interval.end | date: 'HH:mm' }}
{{ kpi.interval.start | date : "HH:mm" }} -
{{ kpi.interval.end | date : "HH:mm" }}
</div>
</div>
</div>

View File

@ -6,6 +6,9 @@ import { Tour } from '../../../../core/data-access/graphql/generated/generated';
selector: 'dks-tour-view-dialog',
templateUrl: './tour-view-dialog.component.html',
styleUrls: ['./tour-view-dialog.component.css'],
host: {
class: 'm-4',
},
standalone: false
})
export class TourViewDialogComponent {

View File

@ -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();
});
});

View File

@ -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() {}
}

View File

@ -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();
});
});

View File

@ -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>();
}

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

@ -15,15 +15,16 @@ import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router';
import { de } from 'date-fns/locale';
import { UtilNgxEchartsModule } from '../../core/components/ngx-echarts/util-ngx-echarts.module';
import { ManagerInfoCardComponent } from './components/manager-info-card/manager-info-card.component';
import { MetricSummaryComponent } from './components/metric-summary/metric-summary.component';
import { ViewTypeSelectorComponent } from './components/view-type-selector/view-type-selector.component';
import { DayviewComponent } from './dayview/dayview.component';
import { ManagerDashboardComponent } from './manager-dashboard/manager-dashboard.component';
import { MonthviewComponent } from './monthview/monthview.component';
import { OverviewComponent } from './overview/overview.component';
import { TimePerTourByCarComponent } from './time-per-tour-by-car/time-per-tour-by-car.component';
import { TimePerTourKPIComponent } from './time-per-tour-kpi/time-per-tour-kpi.component';
import { ViewTypeSelectorComponent } from './components/view-type-selector/view-type-selector.component';
import { UtilNgxEchartsModule } from '../../core/components/ngx-echarts/util-ngx-echarts.module';
@NgModule({
imports: [
@ -69,6 +70,7 @@ import { UtilNgxEchartsModule } from '../../core/components/ngx-echarts/util-ngx
UtilNgxEchartsModule,
MetricSummaryComponent,
ViewTypeSelectorComponent,
ManagerInfoCardComponent,
],
declarations: [
ManagerDashboardComponent,

View File

@ -1,3 +0,0 @@
:host {
@apply flex justify-center items-center;
}

View File

@ -9,7 +9,10 @@ import { summarizeMetrics } from '../../util/summarize-metrics';
selector: 'dks-average-revenue-summary',
imports: [CommonModule, MatIconModule],
templateUrl: './average-revenue-summary.component.html',
styleUrls: ['./average-revenue-summary.component.css']
styleUrls: ['./average-revenue-summary.component.css'],
host: {
class: 'flex justify-center items-center',
},
})
export class AverageRevenueSummaryComponent {
@Input() set metrics(value: TimePerTourMetric[]) {

View File

@ -1,3 +0,0 @@
:host {
@apply flex justify-center items-center;
}

View File

@ -31,7 +31,8 @@
<mat-icon>account_balance</mat-icon>
</div>
</div>
<div class="flex items-center justify-end text-yellow-500">
<!-- TODO: Add feature toggle for forecast -->
<!-- <div class="flex items-center justify-end text-yellow-500">
<span class="text-base"
>+{{
summary.forecastRevenue | currency : 'EUR' : 'symbol' : '1.2-2'
@ -40,5 +41,5 @@
<div class="p-1 text-sm" title="Umsatz Vorausschau">
<mat-icon>account_balance</mat-icon>
</div>
</div>
</div> -->
</div>

View File

@ -6,10 +6,13 @@ import { TimePerTourMetric } from '../../../../core/data-access/graphql/generate
import { summarizeMetrics } from '../../util/summarize-metrics';
@Component({
selector: 'dks-breakeven-summary',
imports: [CommonModule, MatIconModule],
templateUrl: './breakeven-summary.component.html',
styleUrls: ['./breakeven-summary.component.css']
selector: 'dks-breakeven-summary',
imports: [CommonModule, MatIconModule],
templateUrl: './breakeven-summary.component.html',
styleUrls: ['./breakeven-summary.component.css'],
host: {
class: 'flex justify-center items-center',
},
})
export class BreakevenSummaryComponent {
@Input() set metrics(value: TimePerTourMetric[]) {

View File

@ -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>
}

View File

@ -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();
});
});

View File

@ -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();
});
}
}

View File

@ -18,7 +18,7 @@
></dks-time-per-tour-summary>
</div>
</ng-container>
<ng-container *ngSwitchCase="DiagrammType.BreakEven">
<ng-container *ngSwitchCase="DiagrammType.Revenue">
<dks-breakeven-metric
class="col-span-3"
[labelFormat]="getLabelFormatForMetricTimeScale(metricTimeScale)"

View File

@ -1,3 +0,0 @@
:host {
@apply flex justify-center items-center;
}

View File

@ -2,14 +2,17 @@ import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { BehaviorSubject, map } from 'rxjs';
import { summarizeMetrics } from '../../util/summarize-metrics';
import { TimePerTourMetric } from '../../../../core/data-access/graphql/generated/generated';
import { summarizeMetrics } from '../../util/summarize-metrics';
@Component({
selector: 'dks-time-per-tour-summary',
imports: [CommonModule, MatIconModule],
templateUrl: './time-per-tour-summary.component.html',
styleUrls: ['./time-per-tour-summary.component.css']
selector: 'dks-time-per-tour-summary',
imports: [CommonModule, MatIconModule],
templateUrl: './time-per-tour-summary.component.html',
styleUrls: ['./time-per-tour-summary.component.css'],
host: {
class: 'flex justify-center items-center',
},
})
export class TimePerTourSummaryComponent {
@Input() set metrics(value: TimePerTourMetric[]) {

View File

@ -1,18 +1,22 @@
<div
class="py-5 w-full flex flex-col justify-center items-center bg-white shadow rounded cursor-pointer transition ease-in-out hover:shadow-md hover:scale-105 duration-200"
<!-- class="py-5 w-full flex flex-col justify-center items-center bg-white shadow rounded cursor-pointer transition ease-in-out hover:shadow-md hover:scale-105 duration-200" -->
<mat-card
appearance="raised"
class="cursor-pointer transition ease-in-out hover:scale-105 duration-200"
[ngClass]="{
'border border-blue-400': selected
'!border': selected
}"
style="border-color: var(--mat-sys-primary)"
(click)="setSelectedViewType()"
*ngIf="metricSummary$ | async as summary"
>
<span class="text-lg pb-4">{{ viewType }}</span>
<mat-card-header class="!pb-2 justify-center">
<mat-card-title>{{ viewType }}</mat-card-title>
</mat-card-header>
<!-- <span class="text-lg pb-4">{{ viewType }}</span> -->
<ng-container [ngSwitch]="currentMetric">
<div
class="flex flex-col items-end"
*ngSwitchCase="DiagrammType.TimePerTour"
>
<mat-card-content class="!flex justify-center">
@switch (currentMetric) { @case (DiagrammType.TimePerTour) {
<div class="w-1/4 flex flex-col items-end">
<div class="flex items-center pb-2" title="Touren Menge">
{{ summary.tourAmount.toFixed(2) }}
<mat-icon class="mx-2">local_shipping</mat-icon>
@ -22,28 +26,28 @@
<mat-icon class="mx-2">departure_board</mat-icon>
</div>
</div>
<div class="flex flex-col items-end" *ngSwitchCase="DiagrammType.BreakEven">
<div class="flex items-center pb-2" title="Deckungsbeitrag">
{{ summary.breakEven | currency:'EUR' }}
<mat-icon class="mx-2">account_balance</mat-icon>
} @case (DiagrammType.Revenue) {
<div class="w-1/4 flex flex-col items-end">
<div class="flex items-center pb-2" title="Umsatz">
{{ summary.revenue | currency : "EUR" : "symbol" : ".2" }}
<mat-icon class="mx-2">euro</mat-icon>
</div>
<div class="flex items-center text-yellow-500" title="Erwarteter Deckungsbeitrag">
{{ summary.forecastRevenue | currency:'EUR' }}
<div class="flex items-center" title="Deckungsbeitrag">
{{ summary.breakEven | currency : "EUR" }}
<mat-icon class="mx-2">account_balance</mat-icon>
</div>
</div>
<div
class="flex flex-col items-end"
*ngSwitchCase="DiagrammType.AverageRevenue"
>
} @case (DiagrammType.AverageRevenue) {
<div class="w-1/4 flex flex-col items-end">
<div class="flex items-center pb-2" title="Durchschnittlicher Umsatz">
<span class="mr-1">Ø</span>
{{ summary.averageRevenue | currency:'EUR' }}
{{ summary.averageRevenue | currency : "EUR" }}
</div>
<div class="flex items-center" title="Zeit pro Tour Index">
{{ calculateIndex(summary).toFixed(2) }}
<span class="mx-2">Index</span>
</div>
</div>
</ng-container>
</div>
} }
</mat-card-content>
</mat-card>

View File

@ -2,22 +2,28 @@ import {
AsyncPipe,
CurrencyPipe,
NgClass,
NgIf,
NgSwitch,
NgSwitchCase,
NgIf
} from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { BehaviorSubject, map } from 'rxjs';
import { TimePerTourMetric } from '../../../../core/data-access/graphql/generated/generated';
import { DashboardViewType, DiagrammType } from '../../types';
import { summarizeMetrics } from '../../util/summarize-metrics';
import { TimePerTourMetric } from '../../../../core/data-access/graphql/generated/generated';
@Component({
selector: 'dks-view-type-selector',
imports: [NgIf, NgSwitch, NgSwitchCase, NgClass, AsyncPipe, CurrencyPipe, MatIconModule],
templateUrl: './view-type-selector.component.html',
styleUrls: ['./view-type-selector.component.css']
selector: 'dks-view-type-selector',
imports: [
NgIf,
NgClass,
AsyncPipe,
CurrencyPipe,
MatIconModule,
MatCardModule,
],
templateUrl: './view-type-selector.component.html',
styleUrls: ['./view-type-selector.component.css'],
})
export class ViewTypeSelectorComponent {
@Input() viewType!: DashboardViewType;

View File

@ -8,7 +8,7 @@
</button>
<div class="flex items-center">
<span class="pr-2">
{{ vm.selectedDate | date : 'dd.MM.yyyy' }}
{{ vm.selectedDate | date : "dd.MM.yyyy" }}
</span>
<input
[formControl]="selectedDate"
@ -27,18 +27,18 @@
</button>
</div>
<mat-chip-listbox (change)="changeSelectedType($event)">
<mat-chip-option
[value]="DiagrammType.Revenue"
[selected]="vm.selectedMetric === DiagrammType.Revenue"
>
Umsatz
</mat-chip-option>
<mat-chip-option
[value]="DiagrammType.TimePerTour"
[selected]="vm.selectedMetric === DiagrammType.TimePerTour"
>
Kapazität pro Tour
</mat-chip-option>
<mat-chip-option
[value]="DiagrammType.BreakEven"
[selected]="vm.selectedMetric === DiagrammType.BreakEven"
>
Deckungsbeitrag
</mat-chip-option>
<mat-chip-option
[value]="DiagrammType.AverageRevenue"
[selected]="vm.selectedMetric === DiagrammType.AverageRevenue"
@ -47,6 +47,16 @@
</mat-chip-option>
</mat-chip-listbox>
</div>
<!-- KPI Row -->
<!-- <div
class="my-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
>
<app-manager-info-card ordinanceType="BTW"></app-manager-info-card>
<app-manager-info-card ordinanceType="TSW"></app-manager-info-card>
<app-manager-info-card ordinanceType="KTW"></app-manager-info-card>
<app-manager-info-card ordinanceType="ALL"></app-manager-info-card>
</div> -->
<!-- OrdinanceType Selector -->
<div
class="my-5 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
>
@ -79,62 +89,62 @@
(viewTypeSelected)="setSelectedViewType(DashboardViewType.Gesamt)"
></dks-view-type-selector>
</div>
<div
<!-- Visualisation -->
<mat-card
appearance="outlined"
class="grid grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
[ngSwitch]="vm.selectedViewType"
>
<div
*ngSwitchCase="DashboardViewType.BTW"
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
>
<dks-metric-summary
[metrics]="vm.btwMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'HH:mm'"
></dks-metric-summary>
</div>
<div
*ngSwitchCase="DashboardViewType.TSW"
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
>
<dks-metric-summary
[metrics]="vm.tswMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'HH:mm'"
></dks-metric-summary>
</div>
<div
*ngSwitchCase="DashboardViewType.KTW"
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
>
<dks-metric-summary
[metrics]="vm.ktwMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'HH:mm'"
></dks-metric-summary>
</div>
<div
*ngSwitchCase="DashboardViewType.Gesamt"
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
>
<dks-metric-summary
[metrics]="vm.combinedMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'HH:mm'"
></dks-metric-summary>
</div>
</div>
<mat-card-content>
<div
*ngSwitchCase="DashboardViewType.BTW"
class="col-span-2 lg:col-span-4 w-full"
>
<dks-metric-summary
[metrics]="vm.btwMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'HH:mm'"
></dks-metric-summary>
</div>
<div
*ngSwitchCase="DashboardViewType.TSW"
class="col-span-2 lg:col-span-4 w-full"
>
<dks-metric-summary
[metrics]="vm.tswMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'HH:mm'"
></dks-metric-summary>
</div>
<div
*ngSwitchCase="DashboardViewType.KTW"
class="col-span-2 lg:col-span-4 w-full"
>
<dks-metric-summary
[metrics]="vm.ktwMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'HH:mm'"
></dks-metric-summary>
</div>
<div
*ngSwitchCase="DashboardViewType.Gesamt"
class="col-span-2 lg:col-span-4 w-full"
>
<dks-metric-summary
[metrics]="vm.combinedMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'HH:mm'"
></dks-metric-summary>
</div>
</mat-card-content>
</mat-card>
</div>
</ng-container>
<ng-template #loadingTmpl>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-template>
<ng-template #viewTypeSelector let-typeAndSummary>
{{ typeAndSummary.type }}
</ng-template>

View File

@ -36,7 +36,7 @@ export class DayviewComponent {
startWith(this.selectedDate.value),
map((date) => (date?.getHours() === 0 ? addHours(date, 12) : date))
);
selectedMetric$ = new BehaviorSubject<DiagrammType>(DiagrammType.TimePerTour);
selectedMetric$ = new BehaviorSubject<DiagrammType>(DiagrammType.Revenue);
selectedViewType$ = new BehaviorSubject<DashboardViewType>(
DashboardViewType.Gesamt
);

View File

@ -15,13 +15,15 @@ import {
throwError,
} from 'rxjs';
import {
GetTourAmountPerTimeKpiManagerGQL,
GetTimePerTourMetricForDayGQL,
GetTimePerTourMetricForMonthGQL,
TimePerTourUpdatedGQL,
GetTourAmountPerTimeKpiManagerQuery,
GetTimePerTourMetricForDayQuery,
GetTimePerTourMetricForMonthGQL,
GetTimePerTourMetricForMonthQuery,
GetTourAmountPerTimeKpiManagerGQL,
GetTourAmountPerTimeKpiManagerQuery,
ManagerKpiGQL,
ManagerKpiQuery,
TimePerTourUpdatedGQL,
} from '../../core/data-access/graphql/generated/generated';
@Injectable({ providedIn: 'root' })
@ -35,6 +37,7 @@ export class ManagerDashboardService {
private readonly getTimePerTourMetricForDayGql: GetTimePerTourMetricForDayGQL,
private readonly getTimePerTourMetricForMonthGql: GetTimePerTourMetricForMonthGQL,
private readonly timePerTourUpdatedGql: TimePerTourUpdatedGQL,
private readonly managerKpiGql: ManagerKpiGQL,
private readonly toast: HotToastService
) {
this.timePerTourUpdate$ = this.timePerTourUpdatedGql.subscribe().pipe(
@ -50,6 +53,31 @@ export class ManagerDashboardService {
);
}
fetchManagerKpi(
ordinanceType: string
): Observable<ManagerKpiQuery['managerKpi']> {
const managerKpiQuery = this.managerKpiGql.watch({
ordinanceType,
});
const update = this.timePerTourUpdate$.pipe(
switchMap(() => managerKpiQuery.refetch()),
startWith(undefined)
);
return combineLatest([update, managerKpiQuery.valueChanges]).pipe(
catchError((error) => {
console.error(error);
return throwError(() => error);
}),
retry({ delay: 1000, count: 3 }),
map(([updatedData, firstData]) =>
updatedData ? updatedData : firstData
),
map((response) => response.data.managerKpi)
);
}
getTimePerTourKPIByOrdinanceTypes(
ordinanceType: string
): Observable<GetTourAmountPerTimeKpiManagerQuery['toursPerTimeKPI']> {

View File

@ -1,13 +1,14 @@
<nav mat-tab-nav-bar backgroundColor="accent" [tabPanel]="tabPanel">
<a
#link
mat-tab-link
*ngFor="let tab of tabs"
[routerLink]="tab.route"
[routerLinkActive]="'active'"
[active]="link.elementRef.nativeElement.classList.contains('active')"
>{{ tab.title }}</a
>
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
@for (tab of tabs; track tab.route) {
<a
#link
mat-tab-link
[routerLink]="tab.route"
[routerLinkActive]="'active'"
[active]="link.elementRef.nativeElement.classList.contains('active')"
>{{ tab.title }}</a
>
}
</nav>
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>

View File

@ -3,7 +3,7 @@ import { Component } from '@angular/core';
@Component({
selector: 'dks-manager-dashboard',
templateUrl: './manager-dashboard.component.html',
styleUrls: ['./manager-dashboard.component.css'],
styleUrls: ['./manager-dashboard.component.scss'],
standalone: false
})
export class ManagerDashboardComponent {

View File

@ -8,7 +8,7 @@
</button>
<div class="flex items-center">
<span class="pr-2">
{{ vm.selectedDate | date : 'MM.yyyy' }}
{{ vm.selectedDate | date : "MM.yyyy" }}
</span>
<input
[formControl]="selectedDate"
@ -33,18 +33,18 @@
</div>
<mat-chip-listbox (change)="changeSelectedType($event)">
<mat-chip-option
[value]="DiagrammType.Revenue"
[selected]="vm.selectedMetric === DiagrammType.Revenue"
>
Umsatz
</mat-chip-option>
<mat-chip-option
[value]="DiagrammType.TimePerTour"
[selected]="vm.selectedMetric === DiagrammType.TimePerTour"
>
Kapazität pro Tour
</mat-chip-option>
<mat-chip-option
[value]="DiagrammType.BreakEven"
[selected]="vm.selectedMetric === DiagrammType.BreakEven"
>
Deckungsbeitrag
</mat-chip-option>
<mat-chip-option
[value]="DiagrammType.AverageRevenue"
[selected]="vm.selectedMetric === DiagrammType.AverageRevenue"
@ -85,55 +85,55 @@
(viewTypeSelected)="setSelectedViewType(DashboardViewType.Gesamt)"
></dks-view-type-selector>
</div>
<div
<!-- Visualisation -->
<mat-card
appearance="outlined"
class="grid grid-cols-2 lg:grid-cols-4 gap-2 lg:gap-4"
[ngSwitch]="vm.selectedViewType"
>
<div
*ngSwitchCase="DashboardViewType.BTW"
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
>
<dks-metric-summary
[metrics]="vm.btwMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'d.M.yy'"
></dks-metric-summary>
</div>
<div
*ngSwitchCase="DashboardViewType.TSW"
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
>
<dks-metric-summary
[metrics]="vm.tswMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'d.M.yy'"
></dks-metric-summary>
</div>
<div
*ngSwitchCase="DashboardViewType.KTW"
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
>
<dks-metric-summary
[metrics]="vm.ktwMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'d.M.yy'"
></dks-metric-summary>
</div>
<div
*ngSwitchCase="DashboardViewType.Gesamt"
class="col-span-2 lg:col-span-4 w-full rounded p-2 bg-white shadow"
>
<dks-metric-summary
[metrics]="vm.combinedMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'d.M.yy'"
></dks-metric-summary>
</div>
</div>
<mat-card-content>
@switch (vm.selectedViewType) { @case (DashboardViewType.BTW) {
<div class="col-span-2 lg:col-span-4 w-full">
<dks-metric-summary
[metrics]="vm.btwMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'d.M.yy'"
></dks-metric-summary>
</div>
} @case (DashboardViewType.TSW) {
<div class="col-span-2 lg:col-span-4 w-full">
<dks-metric-summary
[metrics]="vm.tswMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'d.M.yy'"
></dks-metric-summary>
</div>
} @case (DashboardViewType.KTW) {
<div
class="col-span-2 lg:col-span-4 w-full rounded"
>
<dks-metric-summary
[metrics]="vm.ktwMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'d.M.yy'"
></dks-metric-summary>
</div>
} @case (DashboardViewType.Gesamt) {
<div
class="col-span-2 lg:col-span-4 w-full"
>
<dks-metric-summary
[metrics]="vm.combinedMetrics"
[metricTimeScale]="metricTimeScale"
[selectedMetric]="vm.selectedMetric"
[labelFormat]="'d.M.yy'"
></dks-metric-summary>
</div>
} }
</mat-card-content>
</mat-card>
</div>
</ng-container>
<ng-template #loadingTmpl>

View File

@ -21,10 +21,10 @@ import {
import { mergeMetrics } from '../util/merge-metric';
@Component({
selector: 'dks-monthview',
templateUrl: './monthview.component.html',
styleUrls: ['./monthview.component.css'],
standalone: false
selector: 'dks-monthview',
templateUrl: './monthview.component.html',
styleUrls: ['./monthview.component.css'],
standalone: false,
})
export class MonthviewComponent {
OrdinanceTypes = OrdinanceTypes;
@ -36,7 +36,7 @@ export class MonthviewComponent {
selectedDate$ = this.selectedDate.valueChanges.pipe(
startWith(this.selectedDate.value)
);
selectedMetric$ = new BehaviorSubject<DiagrammType>(DiagrammType.TimePerTour);
selectedMetric$ = new BehaviorSubject<DiagrammType>(DiagrammType.Revenue);
selectedViewType$ = new BehaviorSubject<DashboardViewType>(
DashboardViewType.Gesamt
);

View File

@ -5,31 +5,31 @@
<div class="flex flex-wrap md:flex-nowrap">
<!-- KPI -->
<dks-time-per-tour-kpi
class="w-full md:w-1/3 m-2 rounded cursor-pointer transition ease-in-out hover:scale-105 duration-200"
class="w-full md:w-1/3 m-2 cursor-pointer rounded-xl transition ease-in-out hover:scale-105 duration-200"
[ngClass]="{
'border border-blue-400':
vm.selectedOrdinanceType === OrdinanceTypes.BTW
'border': vm.selectedOrdinanceType === OrdinanceTypes.BTW
}"
style="border-color: var(--mat-sys-primary);"
[text]="OrdinanceTypes.BTW"
[kpis]="vm.btwKPI.summaries"
(click)="setSelectedOrdinanceType(OrdinanceTypes.BTW)"
></dks-time-per-tour-kpi>
<dks-time-per-tour-kpi
class="w-full md:w-1/3 m-2 rounded cursor-pointer transition ease-in-out hover:scale-105 duration-200"
class="w-full md:w-1/3 m-2 cursor-pointer rounded-xl transition ease-in-out hover:scale-105 duration-200"
[ngClass]="{
'border border-blue-400':
vm.selectedOrdinanceType === OrdinanceTypes.TSW
'border': vm.selectedOrdinanceType === OrdinanceTypes.TSW
}"
style="border-color: var(--mat-sys-primary);"
[text]="OrdinanceTypes.TSW"
[kpis]="vm.tswKPI.summaries"
(click)="setSelectedOrdinanceType(OrdinanceTypes.TSW)"
></dks-time-per-tour-kpi>
<dks-time-per-tour-kpi
class="w-full md:w-1/3 m-2 rounded cursor-pointer transition ease-in-out hover:scale-105 duration-200"
class="w-full md:w-1/3 m-2 cursor-pointer rounded-xl transition ease-in-out hover:scale-105 duration-200"
[ngClass]="{
'border border-blue-400':
vm.selectedOrdinanceType === OrdinanceTypes.KTW
'border': vm.selectedOrdinanceType === OrdinanceTypes.KTW
}"
style="border-color: var(--mat-sys-primary);"
[text]="OrdinanceTypes.KTW"
[kpis]="vm.ktwKPI.summaries"
(click)="setSelectedOrdinanceType(OrdinanceTypes.KTW)"
@ -40,26 +40,30 @@
*ngIf="vm.selectedOrdinanceType"
>
<!-- Data Table -->
<ng-container [ngSwitch]="vm.selectedOrdinanceType">
<dks-time-per-tour-by-car
[columns]="vm.btwKPI.cars.columns"
[data]="vm.btwKPI.cars.data"
class="w-full"
*ngSwitchCase="OrdinanceTypes.BTW"
></dks-time-per-tour-by-car>
<dks-time-per-tour-by-car
[columns]="vm.tswKPI.cars.columns"
[data]="vm.tswKPI.cars.data"
class="w-full"
*ngSwitchCase="OrdinanceTypes.TSW"
></dks-time-per-tour-by-car>
<dks-time-per-tour-by-car
[columns]="vm.ktwKPI.cars.columns"
[data]="vm.ktwKPI.cars.data"
class="w-full"
*ngSwitchCase="OrdinanceTypes.KTW"
></dks-time-per-tour-by-car>
</ng-container>
<mat-card appearance="outlined" class="w-full">
<mat-card-content>
<ng-container [ngSwitch]="vm.selectedOrdinanceType">
<dks-time-per-tour-by-car
[columns]="vm.btwKPI.cars.columns"
[data]="vm.btwKPI.cars.data"
class="w-full"
*ngSwitchCase="OrdinanceTypes.BTW"
></dks-time-per-tour-by-car>
<dks-time-per-tour-by-car
[columns]="vm.tswKPI.cars.columns"
[data]="vm.tswKPI.cars.data"
class="w-full"
*ngSwitchCase="OrdinanceTypes.TSW"
></dks-time-per-tour-by-car>
<dks-time-per-tour-by-car
[columns]="vm.ktwKPI.cars.columns"
[data]="vm.ktwKPI.cars.data"
class="w-full"
*ngSwitchCase="OrdinanceTypes.KTW"
></dks-time-per-tour-by-car>
</ng-container>
</mat-card-content>
</mat-card>
</div>
</div>
<ng-template #loadingTmpl>

View File

@ -37,8 +37,6 @@
<tr
mat-row
*matRowDef="let carData; columns: displayedColumns"
class="[&_td]:border-b-0"
[ngClass]="{'hover:bg-gray-100': carData !== expandedCarData}"
(click)="setExpandedCarData(carData)"
></tr>
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="!h-0"></tr>

View File

@ -1,4 +1,4 @@
<mat-card>
<mat-card appearance="raised">
<mat-card-header class="flex justify-center">
<mat-card-title>{{ text }}</mat-card-title>
</mat-card-header>
@ -14,7 +14,8 @@
class="w-1/3 flex flex-col justify-center items-center"
>
<div
class="font-bold border-blue-400"
class="font-bold"
style="border-color: var(--mat-sys-primary);"
[ngClass]="{ 'border-b-2': (currentTimeSlot$ | async) === idx }"
>
{{ kpi.interval.start | date: 'HH:mm' }} -

View File

@ -6,7 +6,7 @@ export enum OrdinanceTypes {
export enum DiagrammType {
TimePerTour = 'TimePerTour',
BreakEven = 'BreakEven',
Revenue = 'Revenue',
AverageRevenue = 'AverageRevenue',
}

View File

@ -18,17 +18,21 @@ import {
import CategoryColors from './category-colors';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-ticket-item',
templateUrl: './ticket-item.component.html',
styles: [
`
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-ticket-item',
templateUrl: './ticket-item.component.html',
host: {
class:
'flex flex-col rounded-sm border-l-2 border-r-8 p-2 m-2 bg-white cursor-pointer',
},
styles: [
`
:host {
@apply flex flex-col rounded border-l-2 border-r-8 p-2 m-2 bg-white cursor-pointer;
box-shadow: var(--mat-sys-level2);
}
`,
],
imports: [MatIconModule, MatBadgeModule, NgIf]
],
imports: [MatIconModule, MatBadgeModule, NgIf],
})
export class TicketItemComponent implements OnInit {
@Input() ticket!: Ticket & { tours: Tour[] };

View File

@ -1,11 +1,11 @@
import { AsyncPipe, CurrencyPipe, NgIf } from '@angular/common';
import { NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { TicketStateMeta } from '../../../core/data-access/graphql/generated/generated';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-ticket-lane-footer',
template: `<span *ngIf="withTicketCount" class="text-left text-xs">
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-ticket-lane-footer',
template: `<span *ngIf="withTicketCount" class="text-left text-xs">
{{ ticketStateMeta.count }} Tickets
</span>
<span *ngIf="withApprovalRequiredCount" class="text-left text-xs">
@ -21,14 +21,11 @@ import { TicketStateMeta } from '../../../core/data-access/graphql/generated/gen
<span *ngIf="withTdNotCompanyCount" class="text-left text-xs">
{{ ticketStateMeta.tdNotCompanyCount }} davon fehlender Transportschein
</span> `,
styles: [
`
:host {
@apply flex flex-col items-center justify-center p-2;
}
`,
],
imports: [NgIf, AsyncPipe, CurrencyPipe]
host: {
class: 'flex flex-col items-center justify-center p-2',
},
styles: [``],
imports: [NgIf],
})
export class TicketLaneFooterComponent {
@Input() ticketStateMeta!: TicketStateMeta;

View File

@ -33,10 +33,14 @@ import { TicketLaneHeaderFilterComponent } from './ticket-lane-header-filter.com
[state]="state"
(filterChange)="filterChanged($event)"
></dks-ticket-lane-header-filter>`,
host: {
class: 'flex items-center justify-between p-2',
},
styles: [
`
:host {
@apply flex items-center justify-between p-2 bg-gray-50;
color: var(--mat-sys-on-secondary-container);
background-color: var(--mat-sys-secondary-container);
}
`,
],

View File

@ -1,4 +1,3 @@
<!-- Key Figures -->
<dks-ticket-lane-header
class="shadow"
[state]="ticketState"
@ -17,7 +16,7 @@
>
</dks-ticket-item>
</cdk-virtual-scroll-viewport>
<ng-container *ngIf="ticketStateMeta$ | async as ticketStateMeta">
<!-- <ng-container *ngIf="ticketStateMeta$ | async as ticketStateMeta">
<dks-ticket-lane-footer
*dksRole="['manager', 'admin']"
[ticketStateMeta]="ticketStateMeta"
@ -33,5 +32,5 @@
*dksRole="['controlcenter']"
[ticketStateMeta]="ticketStateMeta"
></dks-ticket-lane-footer
></ng-container>
></ng-container> -->
</ng-container>

View File

@ -2,7 +2,7 @@ import {
CdkVirtualScrollViewport,
ScrollingModule,
} from '@angular/cdk/scrolling';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe, NgIf } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
@ -16,14 +16,11 @@ import {
Observable,
combineLatest,
debounceTime,
filter,
map,
mergeMap,
scan,
switchMap,
tap,
} from 'rxjs';
import { RoleDirective } from '../../../core/components/ms-authentication/role.directive';
import {
Ticket,
TicketFilterArgs,
@ -33,32 +30,31 @@ import {
import { TicketItemComponent } from '../ticket-item/ticket-item.component';
import { TicketViewComponent } from '../ticket-view/ticket-view.component';
import { TicketsService } from '../tickets.service';
import { TicketLaneFooterComponent } from './ticket-lane-footer.component';
import { TicketLaneHeaderComponent } from './ticket-lane-header.component';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-ticket-lane',
templateUrl: './ticket-lane.component.html',
styles: [
`
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'dks-ticket-lane',
templateUrl: './ticket-lane.component.html',
host: {
class: 'rounded-sm h-full w-full grid',
},
styles: [
`
:host {
@apply rounded bg-gray-200 h-full w-full grid;
box-shadow: var(--mat-sys-level1);
background-color: var(--mat-sys-surface-container-low);
grid-template-rows: auto 1fr auto;
}
`,
],
imports: [
TicketLaneHeaderComponent,
TicketLaneFooterComponent,
TicketItemComponent,
TicketViewComponent,
NgIf,
NgFor,
AsyncPipe,
RoleDirective,
ScrollingModule,
]
],
imports: [
TicketLaneHeaderComponent,
TicketItemComponent,
NgIf,
AsyncPipe,
ScrollingModule,
],
})
export class TicketLaneComponent {
private dialog = inject(DialogService);
@ -87,12 +83,12 @@ export class TicketLaneComponent {
cursor$ = new BehaviorSubject<string>('');
tickets$: Observable<(Ticket & { tours: Tour[] })[]>;
ticketStateMeta$ = this.ticketState$.pipe(
filter(Boolean),
switchMap((ticketState) =>
this.ticketsService.fetchTicketStateMeta(ticketState)
)
);
// ticketStateMeta$ = this.ticketState$.pipe(
// filter(Boolean),
// switchMap((ticketState) =>
// this.ticketsService.fetchTicketStateMeta(ticketState)
// )
// );
totalCount$ = new BehaviorSubject<number>(0);
constructor() {

View File

@ -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>
}

View File

@ -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();
});
});

View File

@ -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;
}
}
}

View File

@ -86,7 +86,6 @@ import {
imports: [
MatListModule,
MatIconModule,
NgClass,
NgIf,
NgForOf,
NgSwitch,

Some files were not shown because too many files have changed in this diff Show More