feat: add associated entities to anomalies

This commit is contained in:
Marcel Arndt 2025-06-26 09:04:53 +02:00
parent b31df96a44
commit d9b615fd47
38 changed files with 1094 additions and 133 deletions

View File

@ -3,19 +3,17 @@ import {
ErrorHandler,
importProvidersFrom,
provideZoneChangeDetection,
provideAppInitializer,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { MsAuthenticationModule } from './core/components/ms-authentication/ms-authentication.module';
import { environment } from '../environments/environment';
import { provideHttpClient } from '@angular/common/http';
import { graphqlProvider } from './graphql.provider';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHotToastConfig } from '@ngxpert/hot-toast';
import { faroInitializer } from './core/telemetry/faro-initializer';
import { environment } from '../environments/environment';
import { routes } from './app.routes';
import { MsAuthenticationModule } from './core/components/ms-authentication/ms-authentication.module';
import { GlobalErrorHandler } from './core/telemetry/global-error-handler';
import { graphqlProvider } from './graphql.provider';
export const appConfig: ApplicationConfig = {
providers: [
@ -28,7 +26,7 @@ export const appConfig: ApplicationConfig = {
useClass: GlobalErrorHandler,
},
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideRouter(routes, withComponentInputBinding()),
provideAnimationsAsync(),
importProvidersFrom(
MsAuthenticationModule.forRoot({

View File

@ -42,7 +42,6 @@ export type Anomaly = {
sleepSince?: Maybe<Scalars['DateTime']['output']>;
sleepTimer: Scalars['Int']['output'];
solution: Array<SolutionTarget>;
tour?: Maybe<Tour>;
type: Scalars['String']['output'];
workedOnBy: Scalars['String']['output'];
};
@ -76,12 +75,41 @@ export enum ApprovalState {
Unknown = 'UNKNOWN'
}
export type AssociatedEntities = {
__typename?: 'AssociatedEntities';
attendanceRegistration?: Maybe<Array<AttendanceRegistration>>;
employee?: Maybe<Array<Employee>>;
plannedTime?: Maybe<Array<PlannedTime>>;
tour?: Maybe<Array<Tour>>;
workTime?: Maybe<Array<WorkTime>>;
};
export type AttendanceRegistration = {
__typename?: 'AttendanceRegistration';
attendanceRegistrationId: Scalars['Int']['output'];
dateTime: Scalars['DateTime']['output'];
employeeId: Scalars['Int']['output'];
event: Scalars['String']['output'];
personnelNumber: Scalars['String']['output'];
};
export type DocumentInfo = {
__typename?: 'DocumentInfo';
approval: ApprovalInfo;
tdLocation?: Maybe<Scalars['String']['output']>;
};
export type Employee = {
__typename?: 'Employee';
id: Scalars['String']['output'];
name: Scalars['String']['output'];
occuptationalHealthExamination?: Maybe<Scalars['DateTime']['output']>;
personTransportCertificate?: Maybe<Scalars['DateTime']['output']>;
personnelNumber: Scalars['String']['output'];
qualification?: Maybe<Scalars['String']['output']>;
surname: Scalars['String']['output'];
};
export type HomeDashboardKpIs = {
__typename?: 'HomeDashboardKPIs';
anomalyCount: Scalars['Int']['output'];
@ -194,6 +222,20 @@ export type PaginatedTour = {
totalCount: Scalars['Int']['output'];
};
export type PlannedTime = {
__typename?: 'PlannedTime';
department: Scalars['String']['output'];
duration: Scalars['Int']['output'];
endDate: Scalars['DateTime']['output'];
firstname: Scalars['String']['output'];
id: Scalars['String']['output'];
note?: Maybe<Scalars['String']['output']>;
pause: Scalars['Int']['output'];
startDate: Scalars['DateTime']['output'];
surname: Scalars['String']['output'];
userId: Scalars['String']['output'];
};
export type Query = {
__typename?: 'Query';
accountingStateMeta: TourStateMeta;
@ -201,6 +243,7 @@ export type Query = {
anomalies: PaginatedAnomaly;
anomaliesProto: Array<Anomaly>;
anomaly: Anomaly;
associatedEntities: AssociatedEntities;
groupedAnomalies: PaginatedAnomalyGroup;
homeDashboardKpis: HomeDashboardKpIs;
managerKpi: KpiInfo;
@ -242,6 +285,11 @@ export type QueryAnomalyArgs = {
};
export type QueryAssociatedEntitiesArgs = {
id: Scalars['String']['input'];
};
export type QueryGroupedAnomaliesArgs = {
cursor?: InputMaybe<Scalars['String']['input']>;
take?: Scalars['Int']['input'];
@ -492,8 +540,28 @@ export type TourStateMeta = {
revenueSum?: Maybe<Scalars['Float']['output']>;
};
export type WorkTime = {
__typename?: 'WorkTime';
department: Scalars['String']['output'];
duration: Scalars['Int']['output'];
endDate: Scalars['DateTime']['output'];
firstname: Scalars['String']['output'];
id: Scalars['String']['output'];
pause: Scalars['Int']['output'];
startDate: Scalars['DateTime']['output'];
surname: Scalars['String']['output'];
userId: Scalars['String']['output'];
};
export type AnomalyAllFragment = { __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 GetAnomalyQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
export type GetAnomalyQuery = { __typename?: 'Query', anomaly: { __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 GetAnomaliesQueryVariables = Exact<{
cursor?: InputMaybe<Scalars['String']['input']>;
take: Scalars['Int']['input'];
@ -524,6 +592,23 @@ 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 EmployeeAllFragment = { __typename?: 'Employee', name: string, id: string, surname: string, personnelNumber: string, qualification?: string | null, occuptationalHealthExamination?: any | null, personTransportCertificate?: any | null };
export type PlannedTimeAllFragment = { __typename?: 'PlannedTime', id: string, userId: string, firstname: string, surname: string, startDate: any, endDate: any, department: string, pause: number, duration: number, note?: string | null };
export type WorkTimeAllFragment = { __typename?: 'WorkTime', id: string, userId: string, firstname: string, surname: string, startDate: any, endDate: any, department: string, pause: number, duration: number };
export type AttendanceRegistrationAllFragment = { __typename?: 'AttendanceRegistration', personnelNumber: string, attendanceRegistrationId: number, employeeId: number, dateTime: any, event: string };
export type TourAllFragment = { __typename?: 'Tour', billDate?: any | null, billNumber?: string | null, carName?: string | null, category?: string | null, check: number, codriverId?: string | null, codriverName?: string | null, consumptionCosts: number, createdAt?: any | null, deletedAt?: any | null, direction: string, done: boolean, driverId?: string | null, driverName?: string | null, empty?: string | null, hasInfection?: boolean | null, healthInsurance?: string | null, healthInsuranceNumber?: string | null, id: string, infectionName?: string | null, occupiedKm: number, operationId: string, ordinanceType?: string | null, patientCity?: string | null, patientId?: string | null, patientName?: string | null, patientStreet?: string | null, patientSurname?: string | null, patientZip?: string | null, rangeEndDate?: any | null, revenue: number, revenueDeviation: number, revenueDispoLive: number, startBegin?: string | null, startCity?: string | null, startDate?: any | null, startEnd?: string | null, startInstitution?: string | null, startStreet?: string | null, startZip?: string | null, target?: string | null, targetBegin?: string | null, targetCity?: string | null, targetInstitution?: string | null, targetStreet?: string | null, targetZip?: string | null, ticketId?: string | null, totalKm: number, transportType?: string | null, type: string, ticket?: { __typename?: 'Ticket', currentState: TicketValidationState, approvalState?: ApprovalState | null, createdAt: any } | null };
export type GetAnomaliesAssociatedEntitiesQueryVariables = Exact<{
id: Scalars['String']['input'];
}>;
export type GetAnomaliesAssociatedEntitiesQuery = { __typename?: 'Query', associatedEntities: { __typename?: 'AssociatedEntities', employee?: Array<{ __typename?: 'Employee', name: string, id: string, surname: string, personnelNumber: string, qualification?: string | null, occuptationalHealthExamination?: any | null, personTransportCertificate?: any | null }> | null, plannedTime?: Array<{ __typename?: 'PlannedTime', id: string, userId: string, firstname: string, surname: string, startDate: any, endDate: any, department: string, pause: number, duration: number, note?: string | null }> | null, workTime?: Array<{ __typename?: 'WorkTime', id: string, userId: string, firstname: string, surname: string, startDate: any, endDate: any, department: string, pause: number, duration: number }> | null, attendanceRegistration?: Array<{ __typename?: 'AttendanceRegistration', personnelNumber: string, attendanceRegistrationId: number, employeeId: number, dateTime: any, event: string }> | null, tour?: Array<{ __typename?: 'Tour', billDate?: any | null, billNumber?: string | null, carName?: string | null, category?: string | null, check: number, codriverId?: string | null, codriverName?: string | null, consumptionCosts: number, createdAt?: any | null, deletedAt?: any | null, direction: string, done: boolean, driverId?: string | null, driverName?: string | null, empty?: string | null, hasInfection?: boolean | null, healthInsurance?: string | null, healthInsuranceNumber?: string | null, id: string, infectionName?: string | null, occupiedKm: number, operationId: string, ordinanceType?: string | null, patientCity?: string | null, patientId?: string | null, patientName?: string | null, patientStreet?: string | null, patientSurname?: string | null, patientZip?: string | null, rangeEndDate?: any | null, revenue: number, revenueDeviation: number, revenueDispoLive: number, startBegin?: string | null, startCity?: string | null, startDate?: any | null, startEnd?: string | null, startInstitution?: string | null, startStreet?: string | null, startZip?: string | null, target?: string | null, targetBegin?: string | null, targetCity?: string | null, targetInstitution?: string | null, targetStreet?: string | null, targetZip?: string | null, ticketId?: string | null, totalKm: number, transportType?: string | null, type: string, ticket?: { __typename?: 'Ticket', currentState: TicketValidationState, approvalState?: ApprovalState | null, createdAt: any } | null }> | null } };
export type HomeDashboardQueryVariables = Exact<{ [key: string]: never; }>;
@ -669,6 +754,130 @@ export const AnomalyAllFragmentDoc = gql`
createdAt
}
`;
export const EmployeeAllFragmentDoc = gql`
fragment EmployeeAll on Employee {
name
id
surname
personnelNumber
qualification
occuptationalHealthExamination
personTransportCertificate
}
`;
export const PlannedTimeAllFragmentDoc = gql`
fragment PlannedTimeAll on PlannedTime {
id
userId
firstname
surname
startDate
endDate
department
pause
duration
note
}
`;
export const WorkTimeAllFragmentDoc = gql`
fragment WorkTimeAll on WorkTime {
id
userId
firstname
surname
startDate
endDate
department
pause
duration
}
`;
export const AttendanceRegistrationAllFragmentDoc = gql`
fragment AttendanceRegistrationAll on AttendanceRegistration {
personnelNumber
attendanceRegistrationId
employeeId
dateTime
event
}
`;
export const TourAllFragmentDoc = gql`
fragment TourAll on Tour {
billDate
billNumber
carName
category
check
codriverId
codriverName
consumptionCosts
createdAt
deletedAt
direction
done
driverId
driverName
empty
hasInfection
healthInsurance
healthInsuranceNumber
id
infectionName
occupiedKm
operationId
ordinanceType
patientCity
patientId
patientName
patientStreet
patientSurname
patientZip
rangeEndDate
revenue
revenueDeviation
revenueDispoLive
startBegin
startCity
startDate
startEnd
startInstitution
startStreet
startZip
target
targetBegin
targetCity
targetInstitution
targetStreet
targetZip
ticketId
ticket {
currentState
approvalState
createdAt
}
totalKm
transportType
type
}
`;
export const GetAnomalyDocument = gql`
query GetAnomaly($id: String!) {
anomaly(id: $id) {
...AnomalyAll
}
}
${AnomalyAllFragmentDoc}`;
@Injectable({
providedIn: 'root'
})
export class GetAnomalyGQL extends Apollo.Query<GetAnomalyQuery, GetAnomalyQueryVariables> {
override document = GetAnomalyDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
}
}
export const GetAnomaliesDocument = gql`
query GetAnomalies($cursor: String, $take: Int!) {
anomalies(cursor: $cursor, take: $take) {
@ -752,6 +961,42 @@ export const AcceptAnomalyDocument = gql`
export class AcceptAnomalyGQL extends Apollo.Mutation<AcceptAnomalyMutation, AcceptAnomalyMutationVariables> {
override document = AcceptAnomalyDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
}
}
export const GetAnomaliesAssociatedEntitiesDocument = gql`
query GetAnomaliesAssociatedEntities($id: String!) {
associatedEntities(id: $id) {
employee {
...EmployeeAll
}
plannedTime {
...PlannedTimeAll
}
workTime {
...WorkTimeAll
}
attendanceRegistration {
...AttendanceRegistrationAll
}
tour {
...TourAll
}
}
}
${EmployeeAllFragmentDoc}
${PlannedTimeAllFragmentDoc}
${WorkTimeAllFragmentDoc}
${AttendanceRegistrationAllFragmentDoc}
${TourAllFragmentDoc}`;
@Injectable({
providedIn: 'root'
})
export class GetAnomaliesAssociatedEntitiesGQL extends Apollo.Query<GetAnomaliesAssociatedEntitiesQuery, GetAnomaliesAssociatedEntitiesQueryVariables> {
override document = GetAnomaliesAssociatedEntitiesDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
}

View File

@ -21,6 +21,12 @@ fragment AnomalyAll on Anomaly {
createdAt
}
query GetAnomaly($id: String!) {
anomaly(id: $id) {
...AnomalyAll
}
}
query GetAnomalies($cursor: String, $take: Int!) {
anomalies(cursor: $cursor, take: $take) {
prevCursor

View File

@ -0,0 +1,120 @@
fragment EmployeeAll on Employee {
name
id
surname
personnelNumber
qualification
occuptationalHealthExamination
personTransportCertificate
}
fragment PlannedTimeAll on PlannedTime {
id
userId
firstname
surname
startDate
endDate
department
pause
duration
note
}
fragment WorkTimeAll on WorkTime {
id
userId
firstname
surname
startDate
endDate
department
pause
duration
}
fragment AttendanceRegistrationAll on AttendanceRegistration {
personnelNumber
attendanceRegistrationId
employeeId
dateTime
event
}
fragment TourAll on Tour {
billDate
billNumber
carName
category
check
codriverId
codriverName
consumptionCosts
createdAt
deletedAt
direction
done
driverId
driverName
empty
hasInfection
healthInsurance
healthInsuranceNumber
id
infectionName
occupiedKm
operationId
ordinanceType
patientCity
patientId
patientName
patientStreet
patientSurname
patientZip
rangeEndDate
revenue
revenueDeviation
revenueDispoLive
startBegin
startCity
startDate
startEnd
startInstitution
startStreet
startZip
target
targetBegin
targetCity
targetInstitution
targetStreet
targetZip
ticketId
ticket {
currentState
approvalState
createdAt
}
totalKm
transportType
type
}
query GetAnomaliesAssociatedEntities($id: String!) {
associatedEntities(id: $id) {
employee {
...EmployeeAll
}
plannedTime {
...PlannedTimeAll
}
workTime {
...WorkTimeAll
}
attendanceRegistration {
...AttendanceRegistrationAll
}
tour {
...TourAll
}
}
}

View File

@ -32,14 +32,14 @@ interface Anomaly {
}
@Component({
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
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,
})
export class AnomalyListItemComponent {
isHovering = false;
@ -76,7 +76,7 @@ export class AnomalyListItemComponent {
title: 'Ist dies kein Fehler?',
body: 'Sind sie sicher, dass diese Anomalie kein Fehler ist? Dann bestätigen Sie diesen Dialog',
},
width: 500
width: 500,
})
.afterClosed$.subscribe(
(confirmed) => confirmed && this.anomalyAccepted.emit(id)
@ -88,6 +88,7 @@ export class AnomalyListItemComponent {
data: {
title: this.anomaly.description,
solution: this.anomaly.solution,
anomalyId: this.anomaly.id,
},
});
dialogRef.afterClosed$.subscribe((result) => {

View File

@ -1,17 +1,20 @@
<h2>{{ ref.data.title }}</h2>
<mat-divider></mat-divider>
<article class="my-4 mx-2">
<div class="pb-4" *dksRole="['admin']">
<a [routerLink]="['/control-center', ref.data.anomalyId]">View Anomaly</a>
</div>
@for (target of ref.data.solution; track target) {
<div class="pb-4">
<span class="font-bold text-base">{{ target.name }}</span>
<ul class="ml-4">
@for (step of target.steps; track step) {
<li class="list-disc">
{{ step.description }}
</li>
}
</ul>
</div>
<div class="pb-4">
<span class="font-bold text-base">{{ target.name }}</span>
<ul class="ml-4">
@for (step of target.steps; track step) {
<li class="list-disc">
{{ step.description }}
</li>
}
</ul>
</div>
}
</article>
<mat-divider></mat-divider>

View File

@ -9,16 +9,19 @@ interface SolutionTarget {
}
@Component({
selector: 'dks-anomaly-solution-dialog',
templateUrl: './anomaly-solution-dialog.component.html',
styleUrls: ['./anomaly-solution-dialog.component.scss'],
host: {
class: 'm-4',
},
standalone: false
selector: 'dks-anomaly-solution-dialog',
templateUrl: './anomaly-solution-dialog.component.html',
styleUrls: ['./anomaly-solution-dialog.component.scss'],
host: {
class: 'm-4',
},
standalone: false,
})
export class AnomalySolutionDialogComponent {
constructor(
public ref: DialogRef<{ title: string; solution: SolutionTarget[] }, 'sleep' | 'accept'>
public ref: DialogRef<
{ title: string; solution: SolutionTarget[]; anomalyId: string },
'sleep' | 'accept'
>
) {}
}

View File

@ -0,0 +1,77 @@
<div class="w-full">
@if(anomaly()) {
<div>{{anomaly()?.description}}</div>
@for (target of anomaly()?.solution; track $index) {
<div class="pb-4">
<span class="font-bold text-base">{{ target.name }}</span>
<ul class="ml-4">
@for (step of target.steps; track step) {
<li class="list-disc">
{{ step.description }}
</li>
}
</ul>
</div>
}
}
<div class="flex m-4 justify-between overflow-y-hidden overflow-scroll [&>div]:flex-shrink-0">
@if(tours().length > 0) {
@for (tour of tours(); track $index) {
<div class="flex flex-col mx-2 my-4 p-4 rounded-2xl" style="box-shadow: var(--mat-sys-level2);">
@for (item of tour | keyvalue; track $index) {
<div>
<span>{{item.key}}</span>: <span>{{item.value}}</span>
</div>
}
</div>
}
}
@if(plannedTimes().length > 0) {
@for (plannedTime of plannedTimes(); track $index) {
<div class="flex flex-col mx-2 my-4 p-4 rounded-2xl" style="box-shadow: var(--mat-sys-level2);">
@for (item of plannedTime | keyvalue; track $index) {
<div>
<span>{{item.key}}</span>: <span>{{item.value}}</span>
</div>
}
</div>
}
}
@if(workTimes().length > 0) {
@for (workTime of workTimes(); track $index) {
<div class="flex flex-col mx-2 my-4 p-4 rounded-2xl" style="box-shadow: var(--mat-sys-level2);">
@for (item of workTime | keyvalue; track $index) {
<div>
<span>{{item.key}}</span>: <span>{{item.value}}</span>
</div>
}
</div>
}
}
@if(attendanceRegistrations().length > 0) {
<div class="flex flex-col">
@for (attendanceRegistration of attendanceRegistrations(); track $index) {
<div class="flex flex-col mx-2 my-4 p-4 rounded-2xl" style="box-shadow: var(--mat-sys-level2);">
@for (item of attendanceRegistration | keyvalue; track $index) {
<div>
<span>{{item.key}}</span>: <span>{{item.value}}</span>
</div>
}
</div>
}
</div>
}
@if(employees().length > 0) {
@for (employee of employees(); track $index) {
<div class="flex flex-col mx-2 my-4 p-4 rounded-2xl" style="box-shadow: var(--mat-sys-level2);">
@for (item of employee | keyvalue; track $index) {
<div>
<span>{{item.key}}</span>: <span>{{item.value}}</span>
</div>
}
</div>
}
}
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AnomalyViewComponent } from './anomaly-view.component';
describe('AnomalyViewComponent', () => {
let component: AnomalyViewComponent;
let fixture: ComponentFixture<AnomalyViewComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AnomalyViewComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AnomalyViewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,81 @@
import {
Component,
DestroyRef,
effect,
inject,
input,
signal,
} from '@angular/core';
import {
Anomaly,
AttendanceRegistration,
Employee,
GetAnomaliesAssociatedEntitiesGQL,
GetAnomalyGQL,
PlannedTime,
Tour,
WorkTime,
} from '../../../core/data-access/graphql/generated/generated';
import { map, tap } from 'rxjs';
import { KeyValuePipe } from '@angular/common';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'app-anomaly-view',
imports: [KeyValuePipe],
templateUrl: './anomaly-view.component.html',
styles: ``,
})
export class AnomalyViewComponent {
anomalyId = input.required<string>();
anomalyByIdGql = inject(GetAnomalyGQL);
associatedEntitiesGql = inject(GetAnomaliesAssociatedEntitiesGQL);
destroyRef = inject(DestroyRef);
anomaly = signal<Anomaly | undefined>(undefined);
tours = signal<Partial<Tour>[]>([]);
plannedTimes = signal<PlannedTime[]>([]);
workTimes = signal<WorkTime[]>([]);
attendanceRegistrations = signal<AttendanceRegistration[]>([]);
employees = signal<Employee[]>([]);
constructor() {
effect(() => {
this.anomalyByIdGql
.fetch({ id: this.anomalyId() })
.pipe(
takeUntilDestroyed(this.destroyRef),
tap((res) => this.anomaly.set(res.data.anomaly))
)
.subscribe();
this.associatedEntitiesGql
.fetch({ id: this.anomalyId() })
.pipe(
takeUntilDestroyed(this.destroyRef),
map((res) => res.data.associatedEntities),
tap(
({
tour,
attendanceRegistration,
plannedTime,
workTime,
employee,
}) => {
if (tour) this.tours.set(tour as Tour[]);
if (attendanceRegistration)
this.attendanceRegistrations.set(attendanceRegistration);
if (plannedTime) this.plannedTimes.set(plannedTime);
if (workTime) this.workTimes.set(workTime);
if (employee) this.employees.set(employee);
}
)
)
.subscribe();
});
}
ngOnInit() {
console.log(this.anomalyId());
}
}

View File

@ -1,42 +1 @@
<div class="flex flex-col m-4">
<div class="flex flex-wrap lg:flex-nowrap">
<dks-tours-per-time-kpi
class="w-full md:w-1/2 p-2"
[ordinanceType]="'BTW'"
></dks-tours-per-time-kpi>
<dks-tours-per-time-kpi
class="w-full md:w-1/2 p-2"
[ordinanceType]="'TSW'"
></dks-tours-per-time-kpi>
<dks-tours-per-time-kpi
class="w-full md:w-1/2 p-2"
[ordinanceType]="'KTW'"
></dks-tours-per-time-kpi>
</div>
<div class="flex lg:flex-nowrap flex-wrap-reverse">
<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>
<mat-card-content class="flex-grow">
<dks-anomaly-tree></dks-anomaly-tree>
</mat-card-content>
</mat-card>
<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>
<mat-card-content class="flex-grow">
<dks-anomaly-list></dks-anomaly-list>
</mat-card-content>
</mat-card>
<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>
</mat-card-content>
</mat-card>
</div>
</div>
<router-outlet></router-outlet>

View File

@ -1,22 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ControlCenterDashboardComponent } from './control-center-dashboard.component';
describe('ControlCenterDashboardComponent', () => {
let component: ControlCenterDashboardComponent;
let fixture: ComponentFixture<ControlCenterDashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ControlCenterDashboardComponent],
}).compileComponents();
fixture = TestBed.createComponent(ControlCenterDashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'dks-control-center-dashboard',
templateUrl: './control-center-dashboard.component.html',
styleUrls: ['./control-center-dashboard.component.scss'],
styles: [],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false
})

View File

@ -10,16 +10,19 @@ import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTreeModule } from '@angular/material/tree';
import { RouterModule } from '@angular/router';
import { ConfirmDialogComponent } from '../../core/components/confirm-dialog/confirm-dialog.component';
import { AnomaliesService } from './anomalies.service';
import { AnomalyListItemComponent } from './anomaly-list/anomaly-list-item/anomaly-list-item.component';
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 { AnomalyViewComponent } from './anomaly-view/anomaly-view.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';
import { ConfirmDialogComponent } from '../../core/components/confirm-dialog/confirm-dialog.component';
import { OverviewDashboardComponent } from './overview-dashboard/overview-dashboard.component';
import { RoleDirective } from '../../core/components/ms-authentication/role.directive';
@NgModule({
imports: [
@ -28,6 +31,21 @@ import { ConfirmDialogComponent } from '../../core/components/confirm-dialog/con
{
path: '',
component: ControlCenterDashboardComponent,
children: [
{
path: 'overview',
component: OverviewDashboardComponent,
},
{
path: ':anomalyId',
component: AnomalyViewComponent,
},
{
path: '',
pathMatch: 'full',
redirectTo: 'overview'
}
],
},
]),
MatGridListModule,
@ -40,9 +58,11 @@ import { ConfirmDialogComponent } from '../../core/components/confirm-dialog/con
MatTreeModule,
MatPaginatorModule,
ConfirmDialogComponent,
RoleDirective,
],
declarations: [
ControlCenterDashboardComponent,
OverviewDashboardComponent,
AnomalyListComponent,
AnomalyListItemComponent,
AnomalySolutionDialogComponent,

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OverviewDashboardComponent } from './overview-dashboard.component';
describe('OverviewDashboardComponent', () => {
let component: OverviewDashboardComponent;
let fixture: ComponentFixture<OverviewDashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [OverviewDashboardComponent]
})
.compileComponents();
fixture = TestBed.createComponent(OverviewDashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,58 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-overview-dashboard',
standalone: false,
template: `
<div class="flex flex-col m-4">
<div class="flex flex-wrap lg:flex-nowrap">
<dks-tours-per-time-kpi
class="w-full md:w-1/2 p-2"
[ordinanceType]="'BTW'"
></dks-tours-per-time-kpi>
<dks-tours-per-time-kpi
class="w-full md:w-1/2 p-2"
[ordinanceType]="'TSW'"
></dks-tours-per-time-kpi>
<dks-tours-per-time-kpi
class="w-full md:w-1/2 p-2"
[ordinanceType]="'KTW'"
></dks-tours-per-time-kpi>
</div>
<div class="flex lg:flex-nowrap flex-wrap-reverse">
<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>
<mat-card-content class="flex-grow">
<dks-anomaly-tree></dks-anomaly-tree>
</mat-card-content>
</mat-card>
<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>
<mat-card-content class="flex-grow">
<dks-anomaly-list></dks-anomaly-list>
</mat-card-content>
</mat-card>
<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>
</mat-card-content>
</mat-card>
</div>
</div>
`,
styles: ``,
})
export class OverviewDashboardComponent {}

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Anomaly" ADD COLUMN "associatedEntities" JSONB DEFAULT '{}';

View File

@ -508,7 +508,7 @@ model Employee {
}
model DyflexisEmployee {
id Int @id
id Int @id
firstname String
surname String
contractStart DateTime
@ -580,18 +580,19 @@ model Trace {
}
model Anomaly {
id String @id
type String
description String
groupKey String
groupDescription String
workedOnBy String
sleepTimer Int
sleepSince DateTime?
solution SolutionTarget[] @relation()
resolvedAt DateTime?
manuallyResolved Boolean @default(false)
createdAt DateTime @default(now())
id String @id
type String
description String
groupKey String
groupDescription String
associatedEntities Json? @default("{}")
workedOnBy String
sleepTimer Int
sleepSince DateTime?
solution SolutionTarget[] @relation()
resolvedAt DateTime?
manuallyResolved Boolean @default(false)
createdAt DateTime @default(now())
}
model SolutionTarget {

View File

@ -55,6 +55,22 @@ enum ApprovalState {
UNKNOWN
}
type AssociatedEntities {
attendanceRegistration: [AttendanceRegistration!]
employee: [Employee!]
plannedTime: [PlannedTime!]
tour: [Tour!]
workTime: [WorkTime!]
}
type AttendanceRegistration {
attendanceRegistrationId: Int!
dateTime: DateTime!
employeeId: Int!
event: String!
personnelNumber: String!
}
"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
@ -65,6 +81,16 @@ type DocumentInfo {
tdLocation: String
}
type Employee {
id: String!
name: String!
occuptationalHealthExamination: DateTime
personTransportCertificate: DateTime
personnelNumber: String!
qualification: String
surname: String!
}
type HomeDashboardKPIs {
anomalyCount: Int!
billableTourCount: Int!
@ -148,12 +174,26 @@ type PaginatedTour {
totalCount: Int!
}
type PlannedTime {
department: String!
duration: Int!
endDate: DateTime!
firstname: String!
id: String!
note: String
pause: Int!
startDate: DateTime!
surname: String!
userId: String!
}
type Query {
accountingStateMeta(state: AccountingState!): TourStateMeta!
accountingTours(cursor: String, filters: AccountingFilterArgs, state: AccountingState!, take: Int! = 10): PaginatedTour!
anomalies(cursor: String, take: Int! = 10): PaginatedAnomaly!
anomaliesProto: [Anomaly!]!
anomaly(id: String!): Anomaly!
associatedEntities(id: String!): AssociatedEntities!
groupedAnomalies(cursor: String, take: Int! = 10): PaginatedAnomalyGroup!
homeDashboardKpis: HomeDashboardKPIs!
managerKpi(ordinanceType: String!): KpiInfo!
@ -338,3 +378,15 @@ type TourStateMeta {
positiveRevenueDeviation: Float
revenueSum: Float
}
type WorkTime {
department: String!
duration: Int!
endDate: DateTime!
firstname: String!
id: String!
pause: Int!
startDate: DateTime!
surname: String!
userId: String!
}

View File

@ -48,6 +48,7 @@ export type Anomaly = {
description: string;
groupKey: string;
groupDescription: string;
associatedEntities: Generated<unknown | null>;
workedOnBy: string;
sleepTimer: number;
sleepSince: Timestamp | null;

View File

@ -1,6 +1,115 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Anomaly, SolutionTarget, SolutionTargetStep } from '@prisma/client';
import {
Anomaly,
AttendanceRegistration,
Employee,
PlannedTime,
SolutionTarget,
SolutionTargetStep,
WorkTime,
} from '@prisma/client';
import { JsonValue } from '@prisma/client/runtime/library';
import { Paginated } from 'src/core/base/pagination.input';
import { AnomalyAsssociatedEntities } from '../feat-business-objects/entities/anomaly/anomalies.dto';
import { TourObjectType } from '../app-accounting/tour.object-type';
@ObjectType('PlannedTime')
export class PlannedTimeObjectType implements PlannedTime {
@Field()
id: string;
@Field()
userId: string;
@Field()
firstname: string;
@Field()
surname: string;
@Field(() => Date)
startDate: Date;
@Field(() => Date)
endDate: Date;
@Field()
department: string;
@Field(() => Int)
pause: number;
@Field(() => Int)
duration: number;
@Field({nullable: true})
note: string;
}
@ObjectType('WorkTime')
export class WorkTimeObjectType implements WorkTime {
@Field()
id: string;
@Field()
userId: string;
@Field()
firstname: string;
@Field()
surname: string;
@Field(() => Date)
startDate: Date;
@Field(() => Date)
endDate: Date;
@Field()
department: string;
@Field(() => Int)
pause: number;
@Field(() => Int)
duration: number;
}
@ObjectType('Employee')
export class EmployeeObjectType implements Employee {
@Field()
name: string;
@Field()
id: string;
@Field()
surname: string;
@Field()
personnelNumber: string;
// @Field()
mobile: string;
@Field({ nullable: true })
qualification: string;
@Field(() => Date, { nullable: true })
occuptationalHealthExamination: Date;
@Field(() => Date, { nullable: true })
personTransportCertificate: Date;
}
@ObjectType('AttendanceRegistration')
export class AttendanceRegistrationObjectType
implements AttendanceRegistration
{
@Field()
personnelNumber: string;
@Field(() => Int)
attendanceRegistrationId: number;
@Field(() => Int)
employeeId: number;
@Field(() => Date)
dateTime: Date;
@Field()
event: string;
}
@ObjectType('AssociatedEntities')
export class AssociatedEntitiesObjectType
implements AnomalyAsssociatedEntities
{
@Field(() => [TourObjectType], { nullable: true })
tour?: TourObjectType[];
@Field(() => [EmployeeObjectType], { nullable: true })
employee?: EmployeeObjectType[];
@Field(() => [PlannedTimeObjectType], { nullable: true })
plannedTime?: PlannedTimeObjectType[];
@Field(() => [WorkTimeObjectType], { nullable: true })
workTime?: WorkTimeObjectType[];
@Field(() => [AttendanceRegistrationObjectType], { nullable: true })
attendanceRegistration?: AttendanceRegistrationObjectType[];
}
@ObjectType('SolutionTarget')
export class SolutionTargetObjectType implements SolutionTarget {
@ -32,6 +141,8 @@ export class AnomalyObjectType implements Anomaly {
groupKey: string;
@Field()
groupDescription: string;
// @Field()
associatedEntities: JsonValue;
@Field()
workedOnBy: string;
@Field(() => Int)

View File

@ -18,6 +18,7 @@ import { AnomalyService } from '../feat-business-objects/entities/anomaly/anomal
import { KpiService } from '../feat-kpis/kpi.service';
import {
AnomalyObjectType,
AssociatedEntitiesObjectType,
PaginatedAnomalyGroupObjectType,
PaginatedAnomalyObjectType,
SolutionTargetObjectType,
@ -99,6 +100,11 @@ export class ControlCenterResolver {
return this.anomalyService.acceptAnomaly(anomalyId);
}
@Query(() => AssociatedEntitiesObjectType)
async associatedEntities(@Args({ name: 'id' }) anomalyId: string) {
return this.anomalyService.getAssociatedEntities(anomalyId);
}
//! Used when creating anomaly list
// @ResolveField(() => TourObjectType, { nullable: true })
// tour(@Parent() anomaly: AnomalyObjectType) {

View File

@ -5,9 +5,15 @@ import { AnomalyDetectorService } from './anomaly-detector.service';
export type AnomalySkeleton = Pick<
Prisma.AnomalyCreateInput,
'createdAt' | 'workedOnBy' | 'sleepTimer' | 'sleepSince'
| 'associatedEntities'
| 'createdAt'
| 'workedOnBy'
| 'sleepTimer'
| 'sleepSince'
>;
export abstract class AnomalyDetector {
private associatedEntities: Record<string, { id: string }[]> = {};
constructor(protected readonly dataService: AnomalyDetectorService) {}
/**
* Unique identifier of the Anomaly Type which is implemented by the class
@ -25,8 +31,28 @@ export abstract class AnomalyDetector {
abstract createDescription(data: unknown): string;
abstract createSolution(data: unknown): Prisma.SolutionTargetCreateInput[];
protected addAssociatedEntity(type: string, id: string): void {
this.associatedEntities = {
...this.associatedEntities,
[type]: [
...(this.associatedEntities[type]?.length
? this.associatedEntities[type]
: []),
{ id },
],
};
}
protected flushAssociatedEntities(): void {
this.associatedEntities = {};
}
protected getAnomalySkeleton(): AnomalySkeleton {
const associatedEntities = this.associatedEntities;
this.flushAssociatedEntities();
return {
associatedEntities,
createdAt: new Date(),
workedOnBy: '',
sleepTimer: 60 * 60 * 1000,

View File

@ -89,7 +89,8 @@ export class AnomalyDetectorService {
],
): Promise<AttendanceRegistration[]> {
let startOfGivenDay: Date;
if (day.getHours() >= 0 && day.getHours() < 3) {
// TODO: Is there a better solution? This seems a bit flacky
if (day.getHours() >= 0 && day.getHours() < 2) {
startOfGivenDay = addDays(day, -1);
} else {
startOfGivenDay = startOfDay(day);

View File

@ -17,7 +17,14 @@ export class DriverCertificateDetector extends AnomalyDetector {
toursInRange
.filter(({ driverId }) => !!driverId)
.map(
async ({ driverId, startDate, driverName, carName, operationId }) => {
async ({
id: tourId,
driverId,
startDate,
driverName,
carName,
operationId,
}) => {
try {
const driverEmployee =
await this.dataService.findEmployeeById(driverId);
@ -37,6 +44,9 @@ export class DriverCertificateDetector extends AnomalyDetector {
startDate,
)
) {
this.addAssociatedEntity('employee', driverEmployee.id);
this.addAssociatedEntity('tour', tourId);
return this.createAnomaly(
`${operationId}-${driverId}-${this.type}`,
operationId,

View File

@ -17,8 +17,18 @@ export class DriverCodriverSameDetector extends AnomalyDetector {
const anomalies = await Promise.all(
toursInRange.map(
({ operationId, carName, startDate, driverId, codriverId }) => {
if (driverId === codriverId) {
({
id: tourId,
operationId,
carName,
startDate,
driverId,
codriverId,
}) => {
if (driverId === codriverId && driverId !== null) {
this.addAssociatedEntity('tour', tourId);
this.addAssociatedEntity('employee', driverId);
return this.createAnomaly(
`${operationId}-${this.type}`,
operationId,

View File

@ -152,6 +152,7 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
private async detectAnomalyForEmployee(
{
id: tourId,
driverId,
codriverId,
startDate,
@ -174,15 +175,15 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
const startOfTour = this.getStartOfTour(
new Date(startDate),
[startBegin, startEnd, target, targetBegin, empty].filter(
(val) => !!val,
)[0],
[startBegin, startEnd, target, targetBegin, empty]
.filter((val) => !!val)
.at(0),
);
if (!startOfTour) return Promise.resolve(undefined);
if (this.validateEmployee(employee)) {
const [isEmployeeLoggedIn, checkedInterval] =
const [isEmployeeLoggedIn, checkedInterval, associatedEntities] =
await this.isEmployeeLoggedInAtDateTime(
employee.personnelNumber,
startOfTour,
@ -190,8 +191,14 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
if (!isEmployeeLoggedIn) {
const employeeName = `${employee.name} ${employee.surname}`;
this.addAssociatedEntity('employee', employee.id);
this.addAssociatedEntity('tour', tourId);
Object.entries(associatedEntities).map(([type, entityIds]) =>
entityIds.map((id) => this.addAssociatedEntity(type, id)),
);
return this.createAnomaly(
`${operationId}-${driverId}-${this.type}`,
`${operationId}-${employeeId}-${this.type}`,
operationId,
employeeName,
employee.mobile,
@ -258,7 +265,7 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
private async isEmployeeLoggedInAtDateTime(
personnelNumber: string,
datetimeToCheck: Date,
): Promise<[boolean, Interval]> {
): Promise<[boolean, Interval, Record<string, string[]>]> {
const workTime = await this.dataService.findWorkTimeByPersonnelNumberForDay(
personnelNumber,
datetimeToCheck,
@ -288,6 +295,7 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
isWithinInterval(datetimeToCheckWithBuffer, interval) ||
isWithinInterval(datetimeToCheck, interval),
interval,
{ workTime: [workTime.id] },
];
} else if (
firstClockedIn &&
@ -300,6 +308,12 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
start: firstClockedIn.dateTime,
end: undefined,
},
{
attendanceRegistration: [
`${firstClockedIn.attendanceRegistrationId}`,
`${lastClockedOut.attendanceRegistrationId}`,
],
},
];
} else if (firstClockedIn && lastClockedOut) {
const interval = {
@ -310,6 +324,12 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
isWithinInterval(datetimeToCheckWithBuffer, interval) ||
isWithinInterval(datetimeToCheck, interval),
interval,
{
attendanceRegistration: [
`${firstClockedIn.attendanceRegistrationId}`,
`${lastClockedOut.attendanceRegistrationId}`,
],
},
];
} else if (firstClockedIn && !lastClockedOut) {
return [
@ -319,9 +339,14 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
datetimeToCheckWithBuffer,
),
{ start: firstClockedIn.dateTime, end: undefined },
{
attendanceRegistration: [
`${firstClockedIn.attendanceRegistrationId}`,
],
},
];
} else {
return [false, undefined];
return [false, undefined, {}];
}
} catch (error) {
this.logger.error(error, {
@ -330,7 +355,7 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
firstClockedIn,
lastClockedOut,
});
return [false, undefined];
return [false, undefined, {}];
}
}

View File

@ -57,6 +57,15 @@ export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetec
employee.id,
);
if (toursWithEmployee.length === 0) {
this.addAssociatedEntity(
'attendanceRegistration',
`${attendanceRegistrationId}`,
);
this.addAssociatedEntity('employee', employee.id);
plannedTime.map(({ id }) =>
this.addAssociatedEntity('plannedTime', id),
);
return this.createAnomaly(
`${attendanceRegistrationId}-${employee.id}`,
format(new Date(dateTime), 'yyyy-MM-dd'),

View File

@ -1,3 +1,11 @@
import {
AttendanceRegistration,
Employee,
PlannedTime,
Tour,
WorkTime,
} from '@prisma/client';
export interface AnomalyCreateDto {
id: string;
type: string;
@ -25,3 +33,11 @@ export interface AnomalyUpdateDto {
resolvedAt: Date;
manuallyResolved: boolean;
}
export interface AnomalyAsssociatedEntities {
tour?: Tour[];
plannedTime?: PlannedTime[];
workTime?: WorkTime[];
attendanceRegistration?: AttendanceRegistration[];
employee?: Employee[];
}

View File

@ -1,12 +1,26 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { Anomaly, SolutionTarget, SolutionTargetStep } from '@prisma/client';
import { AnomalyCreateDto } from './anomalies.dto';
import { AnomalyAsssociatedEntities, AnomalyCreateDto } from './anomalies.dto';
import { AnomalyTypes } from './anomaly-types.enum';
import { AnomalyRepository } from './anomaly.repository';
import { TourService } from '../tour/tour.service';
import { EmployeeService } from '../employee/employee.service';
import { PlannedTimeService } from '../planned-time/planned-time.service';
import { WorkTimeService } from '../work-time/work-time.service';
import { AttendanceRegistrationService } from '../attendance-registration/attendance-registration.service';
@Injectable()
export class AnomalyService {
constructor(private readonly repository: AnomalyRepository) {}
private logger = new Logger(AnomalyService.name);
constructor(
private readonly repository: AnomalyRepository,
private readonly attendanceRegistrationService: AttendanceRegistrationService,
private readonly employeeService: EmployeeService,
private readonly plannedTimeService: PlannedTimeService,
private readonly tourService: TourService,
private readonly workTimeService: WorkTimeService,
) {}
async createEvenIfExists(anomaly: AnomalyCreateDto): Promise<Anomaly> {
return this.repository.createEvenIfExists(anomaly);
@ -44,6 +58,54 @@ export class AnomalyService {
return this.repository.countVisibleAnomalies(types);
}
async getAssociatedEntities(
anomalyId: string,
): Promise<AnomalyAsssociatedEntities> {
const { associatedEntities } = await this.findById(anomalyId);
const entityList = await Promise.all(
Object.entries(associatedEntities).map(
async ([type, ids]: [string, { id: string }[]]) => {
switch (type) {
case 'tour':
return Promise.all(
ids.map(({ id }) => this.tourService.findById(id)),
).then((tour) => ({ tour }));
case 'plannedTime':
return Promise.all(
ids.map(({ id }) => this.plannedTimeService.findById(id)),
).then((plannedTime) => ({ plannedTime }));
case 'workTime':
return Promise.all(
ids.map(({ id }) => this.workTimeService.findById(id)),
).then((workTime) => ({ workTime }));
case 'attendanceRegistration':
return Promise.all(
ids.map(({ id }) =>
this.attendanceRegistrationService.findById(+id),
),
).then((attendanceRegistration) => ({ attendanceRegistration }));
case 'employee':
return Promise.all(
ids.map(({ id }) => this.employeeService.findById(id)),
).then((employee) => ({ employee }));
default:
this.logger.error(
`Associated Entity type not implemented: ${type}`,
);
}
},
),
);
return entityList.reduce(
(sum, cur) => ({
...sum,
...cur,
}),
{},
);
}
async findSolutionTargetsByAnomalyId(
anomalyId: string,
): Promise<SolutionTarget[]> {

View File

@ -7,6 +7,12 @@ import { PrismaService } from 'src/core/database/prisma.service';
export class PlannedTimeRepository {
constructor(private readonly prisma: PrismaService) {}
async findById(id: string): Promise<PlannedTime> {
return await this.prisma.plannedTime.findUnique({
where: { id },
});
}
async findByPersonnelNumberInInterval(
personnelNumber: string,
interval: Interval,

View File

@ -7,6 +7,10 @@ import { PlannedTimeRepository } from './planned-time.repository';
export class PlannedTimeService {
constructor(private readonly repository: PlannedTimeRepository) {}
async findById(id: string): Promise<PlannedTime> {
return this.repository.findById(id);
}
async findByPersonnelNumberInInterval(
personnelNumber: string,
interval: Interval,

View File

@ -80,6 +80,12 @@ export class TourRepository {
});
}
async findById(tourId: string): Promise<Tour> {
return await this.prisma.tour.findUnique({
where: { id: tourId },
});
}
async findInInterval(
interval: Interval,
ordinanceTypes: string[] = [],

View File

@ -28,6 +28,10 @@ export class TourService {
);
}
async findById(tourId: string): Promise<Tour> {
return this.repository.findById(tourId);
}
async findByOperationId(operationId: string): Promise<Tour> {
return this.repository.findByOperationId(operationId);
}

View File

@ -7,6 +7,12 @@ import { PrismaService } from 'src/core/database/prisma.service';
export class WorkTimeRepository {
constructor(private readonly prisma: PrismaService) {}
async findById(id: string): Promise<WorkTime> {
return await this.prisma.workTime.findUnique({
where: { id },
});
}
async findByPersonnelNumberInInterval(
personnelNumber: string,
interval: Interval,

View File

@ -7,6 +7,10 @@ import { WorkTimeRepository } from './work-time.repository';
export class WorkTimeService {
constructor(private readonly repository: WorkTimeRepository) {}
async findById(id: string): Promise<WorkTime> {
return this.repository.findById(id);
}
async findByPersonnelNumberInInterval(
personnelNumber: string,
interval: Interval,

View File

@ -316,6 +316,10 @@ export class KpiService {
include: {
state: true,
},
orderBy: {
id: 'asc'
},
take: 1000
});
const timePerTickets = movedTickets
.map(({ state }) => {