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, ErrorHandler,
importProvidersFrom, importProvidersFrom,
provideZoneChangeDetection, provideZoneChangeDetection,
provideAppInitializer,
} from '@angular/core'; } 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 { 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 { 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 { GlobalErrorHandler } from './core/telemetry/global-error-handler';
import { graphqlProvider } from './graphql.provider';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@ -28,7 +26,7 @@ export const appConfig: ApplicationConfig = {
useClass: GlobalErrorHandler, useClass: GlobalErrorHandler,
}, },
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes), provideRouter(routes, withComponentInputBinding()),
provideAnimationsAsync(), provideAnimationsAsync(),
importProvidersFrom( importProvidersFrom(
MsAuthenticationModule.forRoot({ MsAuthenticationModule.forRoot({

View File

@ -42,7 +42,6 @@ export type Anomaly = {
sleepSince?: Maybe<Scalars['DateTime']['output']>; sleepSince?: Maybe<Scalars['DateTime']['output']>;
sleepTimer: Scalars['Int']['output']; sleepTimer: Scalars['Int']['output'];
solution: Array<SolutionTarget>; solution: Array<SolutionTarget>;
tour?: Maybe<Tour>;
type: Scalars['String']['output']; type: Scalars['String']['output'];
workedOnBy: Scalars['String']['output']; workedOnBy: Scalars['String']['output'];
}; };
@ -76,12 +75,41 @@ export enum ApprovalState {
Unknown = 'UNKNOWN' 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 = { export type DocumentInfo = {
__typename?: 'DocumentInfo'; __typename?: 'DocumentInfo';
approval: ApprovalInfo; approval: ApprovalInfo;
tdLocation?: Maybe<Scalars['String']['output']>; 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 = { export type HomeDashboardKpIs = {
__typename?: 'HomeDashboardKPIs'; __typename?: 'HomeDashboardKPIs';
anomalyCount: Scalars['Int']['output']; anomalyCount: Scalars['Int']['output'];
@ -194,6 +222,20 @@ export type PaginatedTour = {
totalCount: Scalars['Int']['output']; 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 = { export type Query = {
__typename?: 'Query'; __typename?: 'Query';
accountingStateMeta: TourStateMeta; accountingStateMeta: TourStateMeta;
@ -201,6 +243,7 @@ export type Query = {
anomalies: PaginatedAnomaly; anomalies: PaginatedAnomaly;
anomaliesProto: Array<Anomaly>; anomaliesProto: Array<Anomaly>;
anomaly: Anomaly; anomaly: Anomaly;
associatedEntities: AssociatedEntities;
groupedAnomalies: PaginatedAnomalyGroup; groupedAnomalies: PaginatedAnomalyGroup;
homeDashboardKpis: HomeDashboardKpIs; homeDashboardKpis: HomeDashboardKpIs;
managerKpi: KpiInfo; managerKpi: KpiInfo;
@ -242,6 +285,11 @@ export type QueryAnomalyArgs = {
}; };
export type QueryAssociatedEntitiesArgs = {
id: Scalars['String']['input'];
};
export type QueryGroupedAnomaliesArgs = { export type QueryGroupedAnomaliesArgs = {
cursor?: InputMaybe<Scalars['String']['input']>; cursor?: InputMaybe<Scalars['String']['input']>;
take?: Scalars['Int']['input']; take?: Scalars['Int']['input'];
@ -492,8 +540,28 @@ export type TourStateMeta = {
revenueSum?: Maybe<Scalars['Float']['output']>; 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 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<{ export type GetAnomaliesQueryVariables = Exact<{
cursor?: InputMaybe<Scalars['String']['input']>; cursor?: InputMaybe<Scalars['String']['input']>;
take: Scalars['Int']['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 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; }>; export type HomeDashboardQueryVariables = Exact<{ [key: string]: never; }>;
@ -669,6 +754,130 @@ export const AnomalyAllFragmentDoc = gql`
createdAt 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` export const GetAnomaliesDocument = gql`
query GetAnomalies($cursor: String, $take: Int!) { query GetAnomalies($cursor: String, $take: Int!) {
anomalies(cursor: $cursor, take: $take) { anomalies(cursor: $cursor, take: $take) {
@ -752,6 +961,42 @@ export const AcceptAnomalyDocument = gql`
export class AcceptAnomalyGQL extends Apollo.Mutation<AcceptAnomalyMutation, AcceptAnomalyMutationVariables> { export class AcceptAnomalyGQL extends Apollo.Mutation<AcceptAnomalyMutation, AcceptAnomalyMutationVariables> {
override document = AcceptAnomalyDocument; override document = AcceptAnomalyDocument;
constructor(apollo: Apollo.Apollo) {
super(apollo);
}
}
export const 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) { constructor(apollo: Apollo.Apollo) {
super(apollo); super(apollo);
} }

View File

@ -21,6 +21,12 @@ fragment AnomalyAll on Anomaly {
createdAt createdAt
} }
query GetAnomaly($id: String!) {
anomaly(id: $id) {
...AnomalyAll
}
}
query GetAnomalies($cursor: String, $take: Int!) { query GetAnomalies($cursor: String, $take: Int!) {
anomalies(cursor: $cursor, take: $take) { anomalies(cursor: $cursor, take: $take) {
prevCursor 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({ @Component({
selector: 'dks-anomaly-list-item', selector: 'dks-anomaly-list-item',
templateUrl: './anomaly-list-item.component.html', templateUrl: './anomaly-list-item.component.html',
styleUrls: ['./anomaly-list-item.component.css'], styleUrls: ['./anomaly-list-item.component.css'],
host: { host: {
class: 'w-full rounded flex items-center', class: 'w-full rounded flex items-center',
}, },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false standalone: false,
}) })
export class AnomalyListItemComponent { export class AnomalyListItemComponent {
isHovering = false; isHovering = false;
@ -76,7 +76,7 @@ export class AnomalyListItemComponent {
title: 'Ist dies kein Fehler?', title: 'Ist dies kein Fehler?',
body: 'Sind sie sicher, dass diese Anomalie kein Fehler ist? Dann bestätigen Sie diesen Dialog', body: 'Sind sie sicher, dass diese Anomalie kein Fehler ist? Dann bestätigen Sie diesen Dialog',
}, },
width: 500 width: 500,
}) })
.afterClosed$.subscribe( .afterClosed$.subscribe(
(confirmed) => confirmed && this.anomalyAccepted.emit(id) (confirmed) => confirmed && this.anomalyAccepted.emit(id)
@ -88,6 +88,7 @@ export class AnomalyListItemComponent {
data: { data: {
title: this.anomaly.description, title: this.anomaly.description,
solution: this.anomaly.solution, solution: this.anomaly.solution,
anomalyId: this.anomaly.id,
}, },
}); });
dialogRef.afterClosed$.subscribe((result) => { dialogRef.afterClosed$.subscribe((result) => {

View File

@ -1,17 +1,20 @@
<h2>{{ ref.data.title }}</h2> <h2>{{ ref.data.title }}</h2>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<article class="my-4 mx-2"> <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) { @for (target of ref.data.solution; track target) {
<div class="pb-4"> <div class="pb-4">
<span class="font-bold text-base">{{ target.name }}</span> <span class="font-bold text-base">{{ target.name }}</span>
<ul class="ml-4"> <ul class="ml-4">
@for (step of target.steps; track step) { @for (step of target.steps; track step) {
<li class="list-disc"> <li class="list-disc">
{{ step.description }} {{ step.description }}
</li> </li>
} }
</ul> </ul>
</div> </div>
} }
</article> </article>
<mat-divider></mat-divider> <mat-divider></mat-divider>

View File

@ -9,16 +9,19 @@ interface SolutionTarget {
} }
@Component({ @Component({
selector: 'dks-anomaly-solution-dialog', selector: 'dks-anomaly-solution-dialog',
templateUrl: './anomaly-solution-dialog.component.html', templateUrl: './anomaly-solution-dialog.component.html',
styleUrls: ['./anomaly-solution-dialog.component.scss'], styleUrls: ['./anomaly-solution-dialog.component.scss'],
host: { host: {
class: 'm-4', class: 'm-4',
}, },
standalone: false standalone: false,
}) })
export class AnomalySolutionDialogComponent { export class AnomalySolutionDialogComponent {
constructor( 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"> <router-outlet></router-outlet>
<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>

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({ @Component({
selector: 'dks-control-center-dashboard', selector: 'dks-control-center-dashboard',
templateUrl: './control-center-dashboard.component.html', templateUrl: './control-center-dashboard.component.html',
styleUrls: ['./control-center-dashboard.component.scss'], styles: [],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false standalone: false
}) })

View File

@ -10,16 +10,19 @@ import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTreeModule } from '@angular/material/tree'; import { MatTreeModule } from '@angular/material/tree';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ConfirmDialogComponent } from '../../core/components/confirm-dialog/confirm-dialog.component';
import { AnomaliesService } from './anomalies.service'; import { AnomaliesService } from './anomalies.service';
import { AnomalyListItemComponent } from './anomaly-list/anomaly-list-item/anomaly-list-item.component'; import { AnomalyListItemComponent } from './anomaly-list/anomaly-list-item/anomaly-list-item.component';
import { AnomalyListComponent } from './anomaly-list/anomaly-list.component'; import { AnomalyListComponent } from './anomaly-list/anomaly-list.component';
import { AnomalySolutionDialogComponent } from './anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component'; import { AnomalySolutionDialogComponent } from './anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component';
import { AnomalyTreeComponent } from './anomaly-tree/anomaly-tree.component'; import { AnomalyTreeComponent } from './anomaly-tree/anomaly-tree.component';
import { AnomalyViewComponent } from './anomaly-view/anomaly-view.component';
import { ControlCenterDashboardComponent } from './control-center-dashboard.component'; import { ControlCenterDashboardComponent } from './control-center-dashboard.component';
import { ToursPerTimeKPIComponent } from './kpis/tours-per-time-kpi/tours-per-time-kpi.component'; import { ToursPerTimeKPIComponent } from './kpis/tours-per-time-kpi/tours-per-time-kpi.component';
import { TourViewDialogComponent } from './upcoming-tours/tour-view-dialog/tour-view-dialog.component'; import { TourViewDialogComponent } from './upcoming-tours/tour-view-dialog/tour-view-dialog.component';
import { UpcomingToursComponent } from './upcoming-tours/upcoming-tours.component'; import { UpcomingToursComponent } from './upcoming-tours/upcoming-tours.component';
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({ @NgModule({
imports: [ imports: [
@ -28,6 +31,21 @@ import { ConfirmDialogComponent } from '../../core/components/confirm-dialog/con
{ {
path: '', path: '',
component: ControlCenterDashboardComponent, component: ControlCenterDashboardComponent,
children: [
{
path: 'overview',
component: OverviewDashboardComponent,
},
{
path: ':anomalyId',
component: AnomalyViewComponent,
},
{
path: '',
pathMatch: 'full',
redirectTo: 'overview'
}
],
}, },
]), ]),
MatGridListModule, MatGridListModule,
@ -40,9 +58,11 @@ import { ConfirmDialogComponent } from '../../core/components/confirm-dialog/con
MatTreeModule, MatTreeModule,
MatPaginatorModule, MatPaginatorModule,
ConfirmDialogComponent, ConfirmDialogComponent,
RoleDirective,
], ],
declarations: [ declarations: [
ControlCenterDashboardComponent, ControlCenterDashboardComponent,
OverviewDashboardComponent,
AnomalyListComponent, AnomalyListComponent,
AnomalyListItemComponent, AnomalyListItemComponent,
AnomalySolutionDialogComponent, 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 { model DyflexisEmployee {
id Int @id id Int @id
firstname String firstname String
surname String surname String
contractStart DateTime contractStart DateTime
@ -580,18 +580,19 @@ model Trace {
} }
model Anomaly { model Anomaly {
id String @id id String @id
type String type String
description String description String
groupKey String groupKey String
groupDescription String groupDescription String
workedOnBy String associatedEntities Json? @default("{}")
sleepTimer Int workedOnBy String
sleepSince DateTime? sleepTimer Int
solution SolutionTarget[] @relation() sleepSince DateTime?
resolvedAt DateTime? solution SolutionTarget[] @relation()
manuallyResolved Boolean @default(false) resolvedAt DateTime?
createdAt DateTime @default(now()) manuallyResolved Boolean @default(false)
createdAt DateTime @default(now())
} }
model SolutionTarget { model SolutionTarget {

View File

@ -55,6 +55,22 @@ enum ApprovalState {
UNKNOWN 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. 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 tdLocation: String
} }
type Employee {
id: String!
name: String!
occuptationalHealthExamination: DateTime
personTransportCertificate: DateTime
personnelNumber: String!
qualification: String
surname: String!
}
type HomeDashboardKPIs { type HomeDashboardKPIs {
anomalyCount: Int! anomalyCount: Int!
billableTourCount: Int! billableTourCount: Int!
@ -148,12 +174,26 @@ type PaginatedTour {
totalCount: Int! 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 { type Query {
accountingStateMeta(state: AccountingState!): TourStateMeta! accountingStateMeta(state: AccountingState!): TourStateMeta!
accountingTours(cursor: String, filters: AccountingFilterArgs, state: AccountingState!, take: Int! = 10): PaginatedTour! accountingTours(cursor: String, filters: AccountingFilterArgs, state: AccountingState!, take: Int! = 10): PaginatedTour!
anomalies(cursor: String, take: Int! = 10): PaginatedAnomaly! anomalies(cursor: String, take: Int! = 10): PaginatedAnomaly!
anomaliesProto: [Anomaly!]! anomaliesProto: [Anomaly!]!
anomaly(id: String!): Anomaly! anomaly(id: String!): Anomaly!
associatedEntities(id: String!): AssociatedEntities!
groupedAnomalies(cursor: String, take: Int! = 10): PaginatedAnomalyGroup! groupedAnomalies(cursor: String, take: Int! = 10): PaginatedAnomalyGroup!
homeDashboardKpis: HomeDashboardKPIs! homeDashboardKpis: HomeDashboardKPIs!
managerKpi(ordinanceType: String!): KpiInfo! managerKpi(ordinanceType: String!): KpiInfo!
@ -337,4 +377,16 @@ type TourStateMeta {
negativeRevenueDeviation: Float negativeRevenueDeviation: Float
positiveRevenueDeviation: Float positiveRevenueDeviation: Float
revenueSum: 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; description: string;
groupKey: string; groupKey: string;
groupDescription: string; groupDescription: string;
associatedEntities: Generated<unknown | null>;
workedOnBy: string; workedOnBy: string;
sleepTimer: number; sleepTimer: number;
sleepSince: Timestamp | null; sleepSince: Timestamp | null;

View File

@ -1,6 +1,115 @@
import { Field, Int, ObjectType } from '@nestjs/graphql'; 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 { 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') @ObjectType('SolutionTarget')
export class SolutionTargetObjectType implements SolutionTarget { export class SolutionTargetObjectType implements SolutionTarget {
@ -32,6 +141,8 @@ export class AnomalyObjectType implements Anomaly {
groupKey: string; groupKey: string;
@Field() @Field()
groupDescription: string; groupDescription: string;
// @Field()
associatedEntities: JsonValue;
@Field() @Field()
workedOnBy: string; workedOnBy: string;
@Field(() => Int) @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 { KpiService } from '../feat-kpis/kpi.service';
import { import {
AnomalyObjectType, AnomalyObjectType,
AssociatedEntitiesObjectType,
PaginatedAnomalyGroupObjectType, PaginatedAnomalyGroupObjectType,
PaginatedAnomalyObjectType, PaginatedAnomalyObjectType,
SolutionTargetObjectType, SolutionTargetObjectType,
@ -99,6 +100,11 @@ export class ControlCenterResolver {
return this.anomalyService.acceptAnomaly(anomalyId); return this.anomalyService.acceptAnomaly(anomalyId);
} }
@Query(() => AssociatedEntitiesObjectType)
async associatedEntities(@Args({ name: 'id' }) anomalyId: string) {
return this.anomalyService.getAssociatedEntities(anomalyId);
}
//! Used when creating anomaly list //! Used when creating anomaly list
// @ResolveField(() => TourObjectType, { nullable: true }) // @ResolveField(() => TourObjectType, { nullable: true })
// tour(@Parent() anomaly: AnomalyObjectType) { // tour(@Parent() anomaly: AnomalyObjectType) {

View File

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

View File

@ -89,7 +89,8 @@ export class AnomalyDetectorService {
], ],
): Promise<AttendanceRegistration[]> { ): Promise<AttendanceRegistration[]> {
let startOfGivenDay: Date; 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); startOfGivenDay = addDays(day, -1);
} else { } else {
startOfGivenDay = startOfDay(day); startOfGivenDay = startOfDay(day);

View File

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

View File

@ -17,8 +17,18 @@ export class DriverCodriverSameDetector extends AnomalyDetector {
const anomalies = await Promise.all( const anomalies = await Promise.all(
toursInRange.map( 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( return this.createAnomaly(
`${operationId}-${this.type}`, `${operationId}-${this.type}`,
operationId, operationId,

View File

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

View File

@ -57,6 +57,15 @@ export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetec
employee.id, employee.id,
); );
if (toursWithEmployee.length === 0) { if (toursWithEmployee.length === 0) {
this.addAssociatedEntity(
'attendanceRegistration',
`${attendanceRegistrationId}`,
);
this.addAssociatedEntity('employee', employee.id);
plannedTime.map(({ id }) =>
this.addAssociatedEntity('plannedTime', id),
);
return this.createAnomaly( return this.createAnomaly(
`${attendanceRegistrationId}-${employee.id}`, `${attendanceRegistrationId}-${employee.id}`,
format(new Date(dateTime), 'yyyy-MM-dd'), 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 { export interface AnomalyCreateDto {
id: string; id: string;
type: string; type: string;
@ -25,3 +33,11 @@ export interface AnomalyUpdateDto {
resolvedAt: Date; resolvedAt: Date;
manuallyResolved: boolean; 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 { Anomaly, SolutionTarget, SolutionTargetStep } from '@prisma/client';
import { AnomalyCreateDto } from './anomalies.dto'; import { AnomalyAsssociatedEntities, AnomalyCreateDto } from './anomalies.dto';
import { AnomalyTypes } from './anomaly-types.enum'; import { AnomalyTypes } from './anomaly-types.enum';
import { AnomalyRepository } from './anomaly.repository'; 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() @Injectable()
export class AnomalyService { 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> { async createEvenIfExists(anomaly: AnomalyCreateDto): Promise<Anomaly> {
return this.repository.createEvenIfExists(anomaly); return this.repository.createEvenIfExists(anomaly);
@ -44,6 +58,54 @@ export class AnomalyService {
return this.repository.countVisibleAnomalies(types); 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( async findSolutionTargetsByAnomalyId(
anomalyId: string, anomalyId: string,
): Promise<SolutionTarget[]> { ): Promise<SolutionTarget[]> {

View File

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

View File

@ -7,6 +7,10 @@ import { PlannedTimeRepository } from './planned-time.repository';
export class PlannedTimeService { export class PlannedTimeService {
constructor(private readonly repository: PlannedTimeRepository) {} constructor(private readonly repository: PlannedTimeRepository) {}
async findById(id: string): Promise<PlannedTime> {
return this.repository.findById(id);
}
async findByPersonnelNumberInInterval( async findByPersonnelNumberInInterval(
personnelNumber: string, personnelNumber: string,
interval: Interval, 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( async findInInterval(
interval: Interval, interval: Interval,
ordinanceTypes: string[] = [], 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> { async findByOperationId(operationId: string): Promise<Tour> {
return this.repository.findByOperationId(operationId); return this.repository.findByOperationId(operationId);
} }

View File

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

View File

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

View File

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