diff --git a/service/app-hub/dashboard/src/app/app.config.ts b/service/app-hub/dashboard/src/app/app.config.ts index c700c49..cd9ed05 100644 --- a/service/app-hub/dashboard/src/app/app.config.ts +++ b/service/app-hub/dashboard/src/app/app.config.ts @@ -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({ diff --git a/service/app-hub/dashboard/src/app/core/data-access/graphql/generated/generated.ts b/service/app-hub/dashboard/src/app/core/data-access/graphql/generated/generated.ts index 069bb95..d8c6ffe 100644 --- a/service/app-hub/dashboard/src/app/core/data-access/graphql/generated/generated.ts +++ b/service/app-hub/dashboard/src/app/core/data-access/graphql/generated/generated.ts @@ -42,7 +42,6 @@ export type Anomaly = { sleepSince?: Maybe; sleepTimer: Scalars['Int']['output']; solution: Array; - tour?: Maybe; type: Scalars['String']['output']; workedOnBy: Scalars['String']['output']; }; @@ -76,12 +75,41 @@ export enum ApprovalState { Unknown = 'UNKNOWN' } +export type AssociatedEntities = { + __typename?: 'AssociatedEntities'; + attendanceRegistration?: Maybe>; + employee?: Maybe>; + plannedTime?: Maybe>; + tour?: Maybe>; + workTime?: Maybe>; +}; + +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; }; +export type Employee = { + __typename?: 'Employee'; + id: Scalars['String']['output']; + name: Scalars['String']['output']; + occuptationalHealthExamination?: Maybe; + personTransportCertificate?: Maybe; + personnelNumber: Scalars['String']['output']; + qualification?: Maybe; + 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; + 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; + 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; take?: Scalars['Int']['input']; @@ -492,8 +540,28 @@ export type TourStateMeta = { revenueSum?: Maybe; }; +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; 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 { + 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 { 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 { + override document = GetAnomaliesAssociatedEntitiesDocument; + constructor(apollo: Apollo.Apollo) { super(apollo); } diff --git a/service/app-hub/dashboard/src/app/core/data-access/graphql/graphql/anomalies.graphql b/service/app-hub/dashboard/src/app/core/data-access/graphql/graphql/anomalies.graphql index c737250..c5c6d39 100644 --- a/service/app-hub/dashboard/src/app/core/data-access/graphql/graphql/anomalies.graphql +++ b/service/app-hub/dashboard/src/app/core/data-access/graphql/graphql/anomalies.graphql @@ -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 diff --git a/service/app-hub/dashboard/src/app/core/data-access/graphql/graphql/associated-entities.graphql b/service/app-hub/dashboard/src/app/core/data-access/graphql/graphql/associated-entities.graphql new file mode 100644 index 0000000..04b851a --- /dev/null +++ b/service/app-hub/dashboard/src/app/core/data-access/graphql/graphql/associated-entities.graphql @@ -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 + } + } +} diff --git a/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-list-item/anomaly-list-item.component.ts b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-list-item/anomaly-list-item.component.ts index 50e106f..a708060 100644 --- a/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-list-item/anomaly-list-item.component.ts +++ b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-list-item/anomaly-list-item.component.ts @@ -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) => { diff --git a/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.html b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.html index b92e716..f94a3e6 100644 --- a/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.html +++ b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.html @@ -1,17 +1,20 @@

{{ ref.data.title }}

+ @for (target of ref.data.solution; track target) { -
- {{ target.name }} -
    - @for (step of target.steps; track step) { -
  • - {{ step.description }} -
  • - } -
-
+
+ {{ target.name }} +
    + @for (step of target.steps; track step) { +
  • + {{ step.description }} +
  • + } +
+
}
diff --git a/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.ts b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.ts index 14f9744..50917e3 100644 --- a/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.ts +++ b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-list/anomaly-solution-dialog/anomaly-solution-dialog.component.ts @@ -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' + > ) {} } diff --git a/service/app-hub/dashboard/src/app/pages/control-center/anomaly-view/anomaly-view.component.html b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-view/anomaly-view.component.html new file mode 100644 index 0000000..3c969f0 --- /dev/null +++ b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-view/anomaly-view.component.html @@ -0,0 +1,77 @@ +
+ @if(anomaly()) { +
{{anomaly()?.description}}
+ @for (target of anomaly()?.solution; track $index) { +
+ {{ target.name }} +
    + @for (step of target.steps; track step) { +
  • + {{ step.description }} +
  • + } +
+
+ } + } + +
+ @if(tours().length > 0) { + @for (tour of tours(); track $index) { +
+ @for (item of tour | keyvalue; track $index) { +
+ {{item.key}}: {{item.value}} +
+ } +
+ } + } + @if(plannedTimes().length > 0) { + @for (plannedTime of plannedTimes(); track $index) { +
+ @for (item of plannedTime | keyvalue; track $index) { +
+ {{item.key}}: {{item.value}} +
+ } +
+ } + } + @if(workTimes().length > 0) { + @for (workTime of workTimes(); track $index) { +
+ @for (item of workTime | keyvalue; track $index) { +
+ {{item.key}}: {{item.value}} +
+ } +
+ } + } + @if(attendanceRegistrations().length > 0) { +
+ @for (attendanceRegistration of attendanceRegistrations(); track $index) { +
+ @for (item of attendanceRegistration | keyvalue; track $index) { +
+ {{item.key}}: {{item.value}} +
+ } +
+ } +
+ } + @if(employees().length > 0) { + @for (employee of employees(); track $index) { +
+ @for (item of employee | keyvalue; track $index) { +
+ {{item.key}}: {{item.value}} +
+ } +
+ } + } +
+
\ No newline at end of file diff --git a/service/app-hub/dashboard/src/app/pages/control-center/anomaly-view/anomaly-view.component.spec.ts b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-view/anomaly-view.component.spec.ts new file mode 100644 index 0000000..2058b86 --- /dev/null +++ b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-view/anomaly-view.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AnomalyViewComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AnomalyViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/service/app-hub/dashboard/src/app/pages/control-center/anomaly-view/anomaly-view.component.ts b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-view/anomaly-view.component.ts new file mode 100644 index 0000000..8276768 --- /dev/null +++ b/service/app-hub/dashboard/src/app/pages/control-center/anomaly-view/anomaly-view.component.ts @@ -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(); + + anomalyByIdGql = inject(GetAnomalyGQL); + associatedEntitiesGql = inject(GetAnomaliesAssociatedEntitiesGQL); + destroyRef = inject(DestroyRef); + + anomaly = signal(undefined); + tours = signal[]>([]); + plannedTimes = signal([]); + workTimes = signal([]); + attendanceRegistrations = signal([]); + employees = signal([]); + + 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()); + } +} diff --git a/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.html b/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.html index c738ce5..0680b43 100644 --- a/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.html +++ b/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.html @@ -1,42 +1 @@ -
-
- - - -
-
- - - Anomalien - - - - - - - - Mitarbeiter ohne Tour - - - - - - - - Nächste Touren ohne Anfahrt - - - - - -
-
+ diff --git a/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.scss b/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.spec.ts b/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.spec.ts deleted file mode 100644 index f01db34..0000000 --- a/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.spec.ts +++ /dev/null @@ -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; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ControlCenterDashboardComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(ControlCenterDashboardComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.ts b/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.ts index 389edda..7229500 100644 --- a/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.ts +++ b/service/app-hub/dashboard/src/app/pages/control-center/control-center-dashboard.component.ts @@ -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 }) diff --git a/service/app-hub/dashboard/src/app/pages/control-center/dashboard-control-center.module.ts b/service/app-hub/dashboard/src/app/pages/control-center/dashboard-control-center.module.ts index 1b63bf2..619f95f 100644 --- a/service/app-hub/dashboard/src/app/pages/control-center/dashboard-control-center.module.ts +++ b/service/app-hub/dashboard/src/app/pages/control-center/dashboard-control-center.module.ts @@ -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, diff --git a/service/app-hub/dashboard/src/app/pages/control-center/overview-dashboard/overview-dashboard.component.spec.ts b/service/app-hub/dashboard/src/app/pages/control-center/overview-dashboard/overview-dashboard.component.spec.ts new file mode 100644 index 0000000..45c9179 --- /dev/null +++ b/service/app-hub/dashboard/src/app/pages/control-center/overview-dashboard/overview-dashboard.component.spec.ts @@ -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; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [OverviewDashboardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(OverviewDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/service/app-hub/dashboard/src/app/pages/control-center/overview-dashboard/overview-dashboard.component.ts b/service/app-hub/dashboard/src/app/pages/control-center/overview-dashboard/overview-dashboard.component.ts new file mode 100644 index 0000000..693a6a7 --- /dev/null +++ b/service/app-hub/dashboard/src/app/pages/control-center/overview-dashboard/overview-dashboard.component.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-overview-dashboard', + standalone: false, + template: ` +
+
+ + + +
+
+ + + Anomalien + + + + + + + + Mitarbeiter ohne Tour + + + + + + + + Nächste Touren ohne Anfahrt + + + + + +
+
+ `, + styles: ``, +}) +export class OverviewDashboardComponent {} diff --git a/service/data-hub/data-connector/prisma/migrations/20250617104130_add_associated_entities/migration.sql b/service/data-hub/data-connector/prisma/migrations/20250617104130_add_associated_entities/migration.sql new file mode 100644 index 0000000..2ea35bd --- /dev/null +++ b/service/data-hub/data-connector/prisma/migrations/20250617104130_add_associated_entities/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Anomaly" ADD COLUMN "associatedEntities" JSONB DEFAULT '{}'; diff --git a/service/data-hub/data-connector/prisma/schema.prisma b/service/data-hub/data-connector/prisma/schema.prisma index d4b2619..907933c 100644 --- a/service/data-hub/data-connector/prisma/schema.prisma +++ b/service/data-hub/data-connector/prisma/schema.prisma @@ -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 { diff --git a/service/data-hub/data-connector/src/core/database/schema.gql b/service/data-hub/data-connector/src/core/database/schema.gql index 90ff5a4..5a3e9ed 100644 --- a/service/data-hub/data-connector/src/core/database/schema.gql +++ b/service/data-hub/data-connector/src/core/database/schema.gql @@ -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! @@ -337,4 +377,16 @@ type TourStateMeta { negativeRevenueDeviation: Float positiveRevenueDeviation: Float revenueSum: Float +} + +type WorkTime { + department: String! + duration: Int! + endDate: DateTime! + firstname: String! + id: String! + pause: Int! + startDate: DateTime! + surname: String! + userId: String! } \ No newline at end of file diff --git a/service/data-hub/data-connector/src/core/database/types.ts b/service/data-hub/data-connector/src/core/database/types.ts index 42a4f66..645cc30 100644 --- a/service/data-hub/data-connector/src/core/database/types.ts +++ b/service/data-hub/data-connector/src/core/database/types.ts @@ -48,6 +48,7 @@ export type Anomaly = { description: string; groupKey: string; groupDescription: string; + associatedEntities: Generated; workedOnBy: string; sleepTimer: number; sleepSince: Timestamp | null; diff --git a/service/data-hub/data-connector/src/modules/app-control-center/anomaly.object-type.ts b/service/data-hub/data-connector/src/modules/app-control-center/anomaly.object-type.ts index 61076be..d7e00c9 100644 --- a/service/data-hub/data-connector/src/modules/app-control-center/anomaly.object-type.ts +++ b/service/data-hub/data-connector/src/modules/app-control-center/anomaly.object-type.ts @@ -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) diff --git a/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts b/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts index 4a07a00..927619a 100644 --- a/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts +++ b/service/data-hub/data-connector/src/modules/app-control-center/control-center.resolver.ts @@ -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) { diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.class.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.class.ts index ccba2a6..8b20328 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.class.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.class.ts @@ -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 = {}; + 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, diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts index 4329927..2aeb1a3 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/anomaly-detector.service.ts @@ -89,7 +89,8 @@ export class AnomalyDetectorService { ], ): Promise { 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); diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts index d750cf4..bb9ccd1 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-certificate.detector.ts @@ -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, diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts index 643d56d..6650855 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/driver-codriver-same.detector.ts @@ -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, diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts index b14f8b5..38f7755 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/employees-logged-in.detector.ts @@ -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]> { 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, {}]; } } diff --git a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts index 199f553..7fbe7e1 100644 --- a/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts +++ b/service/data-hub/data-connector/src/modules/feat-anomalies/anomaly-detectors/working-employee-not-in-distribution-system.detector.ts @@ -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'), diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.dto.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.dto.ts index dcef961..ee35c43 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.dto.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomalies.dto.ts @@ -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[]; +} diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.service.ts index a41f4d7..9768b8b 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/anomaly/anomaly.service.ts @@ -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 { return this.repository.createEvenIfExists(anomaly); @@ -44,6 +58,54 @@ export class AnomalyService { return this.repository.countVisibleAnomalies(types); } + async getAssociatedEntities( + anomalyId: string, + ): Promise { + 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 { diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts index bcbdf0a..5379b8f 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.repository.ts @@ -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 { + return await this.prisma.plannedTime.findUnique({ + where: { id }, + }); + } + async findByPersonnelNumberInInterval( personnelNumber: string, interval: Interval, diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.service.ts index 06ee290..1c2e79e 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/planned-time/planned-time.service.ts @@ -7,6 +7,10 @@ import { PlannedTimeRepository } from './planned-time.repository'; export class PlannedTimeService { constructor(private readonly repository: PlannedTimeRepository) {} + async findById(id: string): Promise { + return this.repository.findById(id); + } + async findByPersonnelNumberInInterval( personnelNumber: string, interval: Interval, diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts index 1f10520..e306a82 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.repository.ts @@ -80,6 +80,12 @@ export class TourRepository { }); } + async findById(tourId: string): Promise { + return await this.prisma.tour.findUnique({ + where: { id: tourId }, + }); + } + async findInInterval( interval: Interval, ordinanceTypes: string[] = [], diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.service.ts index aca2841..518072f 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/tour/tour.service.ts @@ -28,6 +28,10 @@ export class TourService { ); } + async findById(tourId: string): Promise { + return this.repository.findById(tourId); + } + async findByOperationId(operationId: string): Promise { return this.repository.findByOperationId(operationId); } diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts index 87ac647..feeb4be 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.repository.ts @@ -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 { + return await this.prisma.workTime.findUnique({ + where: { id }, + }); + } + async findByPersonnelNumberInInterval( personnelNumber: string, interval: Interval, diff --git a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.service.ts b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.service.ts index 3c796d7..3ffd018 100644 --- a/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-business-objects/entities/work-time/work-time.service.ts @@ -7,6 +7,10 @@ import { WorkTimeRepository } from './work-time.repository'; export class WorkTimeService { constructor(private readonly repository: WorkTimeRepository) {} + async findById(id: string): Promise { + return this.repository.findById(id); + } + async findByPersonnelNumberInInterval( personnelNumber: string, interval: Interval, diff --git a/service/data-hub/data-connector/src/modules/feat-kpis/kpi.service.ts b/service/data-hub/data-connector/src/modules/feat-kpis/kpi.service.ts index 562f00c..227e1d4 100644 --- a/service/data-hub/data-connector/src/modules/feat-kpis/kpi.service.ts +++ b/service/data-hub/data-connector/src/modules/feat-kpis/kpi.service.ts @@ -316,6 +316,10 @@ export class KpiService { include: { state: true, }, + orderBy: { + id: 'asc' + }, + take: 1000 }); const timePerTickets = movedTickets .map(({ state }) => {