ref: add repository and service for all entities
This commit is contained in:
parent
5eb775523f
commit
51ba4a1f83
|
|
@ -63,6 +63,7 @@ export class AppController {
|
||||||
.then(() => this.anomaliesService.validateAnomalies());
|
.then(() => this.anomaliesService.validateAnomalies());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//! Used to create check 2 report
|
||||||
@Get('tour-calculation')
|
@Get('tour-calculation')
|
||||||
async tourCalculation(@Query('from') from: string, @Query('to') to: string) {
|
async tourCalculation(@Query('from') from: string, @Query('to') to: string) {
|
||||||
const interval = {
|
const interval = {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import { ApplicationTicketSystemModule } from './modules/app-ticket-system/app-t
|
||||||
import { AnomalyDetectionModule } from './modules/feat-anomalies/anomaly-detection.module';
|
import { AnomalyDetectionModule } from './modules/feat-anomalies/anomaly-detection.module';
|
||||||
import { FeatureEtlModule } from './modules/feat-etl/feat-etl.module';
|
import { FeatureEtlModule } from './modules/feat-etl/feat-etl.module';
|
||||||
import { FeatReportsModule } from './modules/feat-reports/feat-reports.module';
|
import { FeatReportsModule } from './modules/feat-reports/feat-reports.module';
|
||||||
import { TicketSystemModule } from './modules/feat-tickets/ticket-system.module';
|
import { FeatTicketSystemModule } from './modules/feat-tickets/ticket-system.module';
|
||||||
import { FeatCalculationModule } from './modules/feat-calculation/feat-calculation.module';
|
import { FeatCalculationModule } from './modules/feat-calculation/feat-calculation.module';
|
||||||
import { AppHomeModule } from './modules/app-home/app-home.module';
|
import { AppHomeModule } from './modules/app-home/app-home.module';
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ import { AppHomeModule } from './modules/app-home/app-home.module';
|
||||||
FeatureEtlModule,
|
FeatureEtlModule,
|
||||||
FeatReportsModule,
|
FeatReportsModule,
|
||||||
FeatCalculationModule,
|
FeatCalculationModule,
|
||||||
TicketSystemModule,
|
FeatTicketSystemModule,
|
||||||
AnomalyDetectionModule,
|
AnomalyDetectionModule,
|
||||||
ManagerModule,
|
ManagerModule,
|
||||||
ApplicationAccountingModule,
|
ApplicationAccountingModule,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ type Anomaly {
|
||||||
sleepSince: DateTime
|
sleepSince: DateTime
|
||||||
sleepTimer: Int!
|
sleepTimer: Int!
|
||||||
solution: [SolutionTarget!]!
|
solution: [SolutionTarget!]!
|
||||||
tour: Tour
|
|
||||||
type: String!
|
type: String!
|
||||||
workedOnBy: String!
|
workedOnBy: String!
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,11 @@ import {
|
||||||
ObjectType,
|
ObjectType,
|
||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
import { PaginationArgs } from 'src/core/base/pagination.input';
|
import { PaginationArgs } from 'src/core/base/pagination.input';
|
||||||
import { AccountingFilter } from './accounting-filter';
|
|
||||||
import { AccountingState } from './accounting-state';
|
import { AccountingState } from './accounting-state';
|
||||||
|
import { TourAccountingFilterDto } from '../feat-business-objects/entities/tour/tours.filter';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class AccountingFilterArgs implements AccountingFilter {
|
export class AccountingFilterArgs implements TourAccountingFilterDto {
|
||||||
@Field(() => [String], { nullable: true })
|
@Field(() => [String], { nullable: true })
|
||||||
deviation!: 'pos' | 'neg' | 'both'[];
|
deviation!: 'pos' | 'neg' | 'both'[];
|
||||||
@Field(() => [String], { nullable: true })
|
@Field(() => [String], { nullable: true })
|
||||||
|
|
|
||||||
|
|
@ -1,242 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
|
||||||
import { TourStateMetaObjectType } from './accounting-arg.types';
|
|
||||||
import {
|
|
||||||
AccountingFilter,
|
|
||||||
createPrismaQueryFromAccountingFilters,
|
|
||||||
} from './accounting-filter';
|
|
||||||
import { AccountingState } from './accounting-state';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AccountingValidationRepository {
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
async findByState(
|
|
||||||
state: AccountingState,
|
|
||||||
filter: AccountingFilter,
|
|
||||||
take?: number,
|
|
||||||
skip?: number,
|
|
||||||
) {
|
|
||||||
const checkValue = this.getCheckValueForAccountingState(state);
|
|
||||||
const query: Prisma.TourFindManyArgs = {
|
|
||||||
...{
|
|
||||||
...(skip ? { skip, take: 10 } : {}),
|
|
||||||
...(take ? { take } : {}),
|
|
||||||
...(take && !skip ? { skip: 0 } : {}),
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
check: checkValue,
|
|
||||||
...(checkValue === 2 && !filter?.deviation
|
|
||||||
? { revenueDeviation: { not: 0 } }
|
|
||||||
: {}),
|
|
||||||
empty: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
ticketId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
AND: [
|
|
||||||
...createPrismaQueryFromAccountingFilters(filter),
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
carName: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Storno',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Löschen',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Abgabe',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{ patientId: null },
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Pause
|
|
||||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Feierabend
|
|
||||||
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
};
|
|
||||||
|
|
||||||
const [tours, count] = await this.prisma.$transaction([
|
|
||||||
this.prisma.tour.findMany({ ...query }),
|
|
||||||
this.prisma.tour.count({ where: query.where }),
|
|
||||||
]);
|
|
||||||
return {
|
|
||||||
tours,
|
|
||||||
total: count,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async calculateTourStateMeta(
|
|
||||||
state: AccountingState,
|
|
||||||
): Promise<TourStateMetaObjectType> {
|
|
||||||
const checkValue = this.getCheckValueForAccountingState(state);
|
|
||||||
const ticketWithCurrentStateAndTours: Prisma.TourWhereInput = {
|
|
||||||
check: checkValue,
|
|
||||||
...(checkValue === 2 ? { revenueDeviation: { not: 0 } } : {}),
|
|
||||||
empty: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
ticketId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
carName: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Storno',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Löschen',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Abgabe',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{ patientId: null },
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Pause
|
|
||||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Feierabend
|
|
||||||
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const [
|
|
||||||
count,
|
|
||||||
revenueSum,
|
|
||||||
positiveRevenueDeviation,
|
|
||||||
negativeRevenueDeviation,
|
|
||||||
] = await this.prisma.$transaction([
|
|
||||||
this.prisma.tour.count({
|
|
||||||
where: { ...ticketWithCurrentStateAndTours },
|
|
||||||
}),
|
|
||||||
this.prisma.tour.aggregate({
|
|
||||||
_sum: {
|
|
||||||
revenue: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
...ticketWithCurrentStateAndTours,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.prisma.tour.aggregate({
|
|
||||||
_sum: {
|
|
||||||
revenueDeviation: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
...ticketWithCurrentStateAndTours,
|
|
||||||
revenueDeviation: {
|
|
||||||
gt: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
this.prisma.tour.aggregate({
|
|
||||||
_sum: {
|
|
||||||
revenueDeviation: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
...ticketWithCurrentStateAndTours,
|
|
||||||
revenueDeviation: {
|
|
||||||
lt: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
count,
|
|
||||||
revenueSum: revenueSum._sum.revenue ?? 0,
|
|
||||||
positiveRevenueDeviation:
|
|
||||||
positiveRevenueDeviation._sum.revenueDeviation ?? 0,
|
|
||||||
negativeRevenueDeviation:
|
|
||||||
negativeRevenueDeviation._sum.revenueDeviation ?? 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
findTicketByTicketId(ticketId: string) {
|
|
||||||
return this.prisma.ticket.findFirst({
|
|
||||||
where: {
|
|
||||||
id: ticketId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCheckValueForAccountingState(state: AccountingState): number {
|
|
||||||
switch (state) {
|
|
||||||
case AccountingState.Billable:
|
|
||||||
return 0;
|
|
||||||
case AccountingState.PreparedForBilling:
|
|
||||||
return 1;
|
|
||||||
case AccountingState.Billed:
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,27 +4,29 @@ import {
|
||||||
encodeCursor,
|
encodeCursor,
|
||||||
} from 'src/core/base/pagination.util';
|
} from 'src/core/base/pagination.util';
|
||||||
import { TourStateMetaObjectType } from './accounting-arg.types';
|
import { TourStateMetaObjectType } from './accounting-arg.types';
|
||||||
import { AccountingFilter } from './accounting-filter';
|
|
||||||
import { AccountingState } from './accounting-state';
|
import { AccountingState } from './accounting-state';
|
||||||
import { AccountingValidationRepository } from './accounting-validation.repository';
|
|
||||||
import { PaginatedTourObjectType } from './accounting.object-type';
|
import { PaginatedTourObjectType } from './accounting.object-type';
|
||||||
import { AccountingService } from './accounting.service';
|
import { AccountingService } from './accounting.service';
|
||||||
|
import { TicketService } from '../feat-business-objects/entities/ticket/ticket.service';
|
||||||
|
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
|
||||||
|
import { TourAccountingFilterDto } from '../feat-business-objects/entities/tour/tours.filter';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountingValidationService {
|
export class AccountingValidationService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly repository: AccountingValidationRepository,
|
|
||||||
private readonly accounting: AccountingService,
|
private readonly accounting: AccountingService,
|
||||||
|
private readonly ticketService: TicketService,
|
||||||
|
private readonly tourService: TourService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async paginateToursByState(
|
async paginateToursByState(
|
||||||
state: AccountingState,
|
state: AccountingState,
|
||||||
filter: AccountingFilter,
|
filter: TourAccountingFilterDto,
|
||||||
take: number = 10,
|
take: number = 10,
|
||||||
cursor?: string,
|
cursor?: string,
|
||||||
): Promise<PaginatedTourObjectType> {
|
): Promise<PaginatedTourObjectType> {
|
||||||
const skip = cursor ? decodeOffsetCursor(cursor) : 0;
|
const skip = cursor ? decodeOffsetCursor(cursor) : 0;
|
||||||
const { tours, total } = await this.repository.findByState(
|
const { tours, total } = await this.tourService.findByAccountingState(
|
||||||
state,
|
state,
|
||||||
filter,
|
filter,
|
||||||
take,
|
take,
|
||||||
|
|
@ -46,12 +48,12 @@ export class AccountingValidationService {
|
||||||
async getAccountingStateMeta(
|
async getAccountingStateMeta(
|
||||||
accountingState: AccountingState,
|
accountingState: AccountingState,
|
||||||
): Promise<TourStateMetaObjectType> {
|
): Promise<TourStateMetaObjectType> {
|
||||||
return this.repository.calculateTourStateMeta(accountingState);
|
return this.tourService.calculateAccountingStateMeta(accountingState);
|
||||||
}
|
}
|
||||||
|
|
||||||
findTicketByTicketId(ticketId?: string | null) {
|
findTicketByTicketId(ticketId?: string | null) {
|
||||||
if (!ticketId) return;
|
if (!ticketId) return;
|
||||||
return this.repository.findTicketByTicketId(ticketId);
|
return this.ticketService.findById(ticketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTourCostBreakdown(operationId: string) {
|
async getTourCostBreakdown(operationId: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,20 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
|
||||||
import { CalculationService } from '../feat-calculation/calculation.service';
|
import { CalculationService } from '../feat-calculation/calculation.service';
|
||||||
import { TourCostBreakdownObjectType } from './accounting.object-type';
|
import { TourCostBreakdownObjectType } from './accounting.object-type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AccountingService {
|
export class AccountingService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly calculation: CalculationService,
|
private readonly calculation: CalculationService,
|
||||||
|
private readonly tourService: TourService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async tourCostBreakdown(
|
async tourCostBreakdown(
|
||||||
operationId: string,
|
operationId: string,
|
||||||
): Promise<TourCostBreakdownObjectType> {
|
): Promise<TourCostBreakdownObjectType> {
|
||||||
const tour = await this.prisma.tour.findFirst({
|
const tour = await this.tourService.findByOperationId(operationId);
|
||||||
where: { operationId },
|
|
||||||
});
|
|
||||||
|
|
||||||
const { tariff, servicePositions } =
|
const { tariff, servicePositions } =
|
||||||
await this.calculation.listProvidedServicePositions(tour);
|
await this.calculation.listProvidedServicePositions(tour);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||||
|
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
|
||||||
import { FeatCalculationModule } from '../feat-calculation/feat-calculation.module';
|
import { FeatCalculationModule } from '../feat-calculation/feat-calculation.module';
|
||||||
import { AccountingValidationRepository } from './accounting-validation.repository';
|
|
||||||
import { AccountingValidationResolver } from './accounting-validation.resolver';
|
import { AccountingValidationResolver } from './accounting-validation.resolver';
|
||||||
import { AccountingValidationService } from './accounting-validation.service';
|
import { AccountingValidationService } from './accounting-validation.service';
|
||||||
import { AccountingService } from './accounting.service';
|
import { AccountingService } from './accounting.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, FeatCalculationModule],
|
imports: [PrismaModule, FeatCalculationModule, FeatBusinessObjectsModule],
|
||||||
providers: [
|
providers: [
|
||||||
AccountingValidationRepository,
|
|
||||||
AccountingValidationResolver,
|
AccountingValidationResolver,
|
||||||
AccountingValidationService,
|
AccountingValidationService,
|
||||||
AccountingService,
|
AccountingService,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
|
||||||
|
import { FeatKpiModule } from '../feat-kpis/feat-kpis.module';
|
||||||
import { ControlCenterResolver } from './control-center.resolver';
|
import { ControlCenterResolver } from './control-center.resolver';
|
||||||
import { SolutionTargetsResolver } from './solution-targets.resolver';
|
import { SolutionTargetsResolver } from './solution-targets.resolver';
|
||||||
import { AnomalyDetectionModule } from '../feat-anomalies/anomaly-detection.module';
|
|
||||||
import { FeatKpiModule } from '../feat-kpis/feat-kpis.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AnomalyDetectionModule, FeatKpiModule],
|
imports: [FeatBusinessObjectsModule, FeatKpiModule],
|
||||||
providers: [ControlCenterResolver, SolutionTargetsResolver],
|
providers: [ControlCenterResolver, SolutionTargetsResolver],
|
||||||
})
|
})
|
||||||
export class ApplicationControlCenterModule {}
|
export class ApplicationControlCenterModule {}
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,29 @@ import {
|
||||||
ResolveField,
|
ResolveField,
|
||||||
Resolver,
|
Resolver,
|
||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
|
import { Anomaly } from '@prisma/client';
|
||||||
import { PaginationArgs } from 'src/core/base/pagination.input';
|
import { PaginationArgs } from 'src/core/base/pagination.input';
|
||||||
import {
|
import {
|
||||||
decodeOffsetCursor,
|
decodeOffsetCursor,
|
||||||
encodeCursor,
|
encodeCursor,
|
||||||
} from 'src/core/base/pagination.util';
|
} from 'src/core/base/pagination.util';
|
||||||
import { TourObjectType } from '../app-accounting/tour.object-type';
|
import { TourObjectType } from '../app-accounting/tour.object-type';
|
||||||
import { AnomaliesService } from '../feat-anomalies/anomalies.service';
|
import { AnomalyTypes } from '../feat-business-objects/entities/anomaly/anomaly-types.enum';
|
||||||
import { AnomalyTypes } from '../feat-anomalies/anomaly-detectors/anomaly-types.enum';
|
import { AnomalyService } from '../feat-business-objects/entities/anomaly/anomaly.service';
|
||||||
|
import { KpiService } from '../feat-kpis/kpi.service';
|
||||||
import {
|
import {
|
||||||
AnomalyObjectType,
|
AnomalyObjectType,
|
||||||
PaginatedAnomalyGroupObjectType,
|
PaginatedAnomalyGroupObjectType,
|
||||||
PaginatedAnomalyObjectType,
|
PaginatedAnomalyObjectType,
|
||||||
SolutionTargetObjectType,
|
SolutionTargetObjectType,
|
||||||
} from './anomaly.object-type';
|
} from './anomaly.object-type';
|
||||||
import { KpiService } from '../feat-kpis/kpi.service';
|
|
||||||
|
|
||||||
@Resolver(() => AnomalyObjectType)
|
@Resolver(() => AnomalyObjectType)
|
||||||
export class ControlCenterResolver {
|
export class ControlCenterResolver {
|
||||||
constructor(private readonly anomaliesService: AnomaliesService, private readonly kpiService: KpiService) {}
|
constructor(
|
||||||
|
private readonly kpiService: KpiService,
|
||||||
|
private readonly anomalyService: AnomalyService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Query(() => [TourObjectType])
|
@Query(() => [TourObjectType])
|
||||||
async upcomingTours() {
|
async upcomingTours() {
|
||||||
|
|
@ -34,24 +37,24 @@ export class ControlCenterResolver {
|
||||||
|
|
||||||
@Query(() => AnomalyObjectType)
|
@Query(() => AnomalyObjectType)
|
||||||
async anomaly(@Args('id') id: string) {
|
async anomaly(@Args('id') id: string) {
|
||||||
return this.anomaliesService.findById(id);
|
return this.anomalyService.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => [AnomalyObjectType])
|
@Query(() => [AnomalyObjectType])
|
||||||
async anomaliesProto() {
|
async anomaliesProto() {
|
||||||
return this.anomaliesService.findActiveAnomalies();
|
return this.anomalyService.findAllVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => PaginatedAnomalyObjectType)
|
@Query(() => PaginatedAnomalyObjectType)
|
||||||
async anomalies(@Args() { cursor, take }: PaginationArgs) {
|
async anomalies(@Args() { cursor, take }: PaginationArgs) {
|
||||||
const skip = cursor ? decodeOffsetCursor(cursor) : 0;
|
const skip = cursor ? decodeOffsetCursor(cursor) : 0;
|
||||||
const activeAnomalies = await this.anomaliesService.findActiveAnomalies(
|
const activeAnomalies = await this.anomalyService.findAllVisible(
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
[AnomalyTypes.WorkingEmployeeNotInDistributionSystem],
|
[AnomalyTypes.WorkingEmployeeNotInDistributionSystem],
|
||||||
);
|
);
|
||||||
|
|
||||||
const totalCount = await this.anomaliesService.countActiveAnomalies([
|
const totalCount = await this.anomalyService.countVisibleAnomalies([
|
||||||
AnomalyTypes.WorkingEmployeeNotInDistributionSystem,
|
AnomalyTypes.WorkingEmployeeNotInDistributionSystem,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -72,10 +75,7 @@ export class ControlCenterResolver {
|
||||||
const skip = cursor ? decodeOffsetCursor(cursor) : 0;
|
const skip = cursor ? decodeOffsetCursor(cursor) : 0;
|
||||||
|
|
||||||
const { totalCount, anomalies: groupedActiveAnomalies } =
|
const { totalCount, anomalies: groupedActiveAnomalies } =
|
||||||
await this.anomaliesService.findActiveAnomaliesGroupedByGroupKey(
|
await this.findAnomaliesVisibleGroupedByGroupKey(skip, take);
|
||||||
skip,
|
|
||||||
take,
|
|
||||||
);
|
|
||||||
|
|
||||||
const prevPage = skip - take < 0 ? 0 : skip - take;
|
const prevPage = skip - take < 0 ? 0 : skip - take;
|
||||||
const nextPage = skip + take < totalCount ? skip + take : skip;
|
const nextPage = skip + take < totalCount ? skip + take : skip;
|
||||||
|
|
@ -91,24 +91,68 @@ export class ControlCenterResolver {
|
||||||
|
|
||||||
@Mutation(() => AnomalyObjectType)
|
@Mutation(() => AnomalyObjectType)
|
||||||
async setAnomalyToSleep(@Args({ name: 'id' }) anomalyId: string) {
|
async setAnomalyToSleep(@Args({ name: 'id' }) anomalyId: string) {
|
||||||
return this.anomaliesService.setAnomalyToSleep(anomalyId);
|
return this.anomalyService.snoozeAnomaly(anomalyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => AnomalyObjectType)
|
@Mutation(() => AnomalyObjectType)
|
||||||
async acceptAnomaly(@Args({ name: 'id' }) anomalyId: string) {
|
async acceptAnomaly(@Args({ name: 'id' }) anomalyId: string) {
|
||||||
return this.anomaliesService.acceptAnomaly(anomalyId);
|
return this.anomalyService.acceptAnomaly(anomalyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => TourObjectType, { nullable: true })
|
//! Used when creating anomaly list
|
||||||
tour(@Parent() anomaly: AnomalyObjectType) {
|
// @ResolveField(() => TourObjectType, { nullable: true })
|
||||||
const [operationId] = anomaly.groupKey.split('-').reverse();
|
// tour(@Parent() anomaly: AnomalyObjectType) {
|
||||||
return this.anomaliesService.findTourByOperationId(operationId);
|
// const [operationId] = anomaly.groupKey.split('-').reverse();
|
||||||
}
|
// return this.anomaliesService.findTourByOperationId(operationId);
|
||||||
|
// }
|
||||||
|
|
||||||
@ResolveField(() => [SolutionTargetObjectType])
|
@ResolveField(() => [SolutionTargetObjectType])
|
||||||
solution(
|
solution(
|
||||||
@Parent() anomaly: AnomalyObjectType,
|
@Parent() anomaly: AnomalyObjectType,
|
||||||
): Promise<SolutionTargetObjectType[]> {
|
): Promise<SolutionTargetObjectType[]> {
|
||||||
return this.anomaliesService.findSolutionTargetsByAnomalyId(anomaly.id);
|
return this.anomalyService.findSolutionTargetsByAnomalyId(anomaly.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findAnomaliesVisibleGroupedByGroupKey(
|
||||||
|
skip = 0,
|
||||||
|
take = 10,
|
||||||
|
): Promise<{
|
||||||
|
totalCount: number;
|
||||||
|
anomalies: { key: string; anomalies: Anomaly[] }[];
|
||||||
|
}> {
|
||||||
|
const activeAnomalies = await this.anomalyService.findAllVisible(
|
||||||
|
//! 2025-06-11: This is intended, we reduce the response at the end
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
[
|
||||||
|
AnomalyTypes.DriverCertificate,
|
||||||
|
AnomalyTypes.EmployeesLoggedIn,
|
||||||
|
AnomalyTypes.OneTourAtATime,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupedAnomalies = this.groupAnomaliesByGroupKey(activeAnomalies);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCount: Object.keys(groupedAnomalies).length,
|
||||||
|
anomalies: Object.entries(groupedAnomalies)
|
||||||
|
.map(([key, anomalies]) => ({
|
||||||
|
key,
|
||||||
|
anomalies,
|
||||||
|
}))
|
||||||
|
.slice(skip, skip + take),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private groupAnomaliesByGroupKey(
|
||||||
|
anomalies: Anomaly[],
|
||||||
|
): Record<string, Anomaly[]> {
|
||||||
|
return anomalies.reduce(
|
||||||
|
(grouped, anomaly) => ({
|
||||||
|
...grouped,
|
||||||
|
[anomaly.groupKey]: [...(grouped[anomaly.groupKey] ?? []), anomaly],
|
||||||
|
}),
|
||||||
|
{} as Record<string, Anomaly[]>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
|
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { AnomaliesService } from '../feat-anomalies/anomalies.service';
|
import { AnomalyService } from '../feat-business-objects/entities/anomaly/anomaly.service';
|
||||||
import {
|
import {
|
||||||
SolutionTargetObjectType,
|
SolutionTargetObjectType,
|
||||||
SolutionTargetStepObjectType,
|
SolutionTargetStepObjectType,
|
||||||
|
|
@ -8,13 +8,13 @@ import {
|
||||||
|
|
||||||
@Resolver(() => SolutionTargetObjectType)
|
@Resolver(() => SolutionTargetObjectType)
|
||||||
export class SolutionTargetsResolver {
|
export class SolutionTargetsResolver {
|
||||||
constructor(private readonly anomaliesService: AnomaliesService) {}
|
constructor(private readonly anomalyService: AnomalyService) {}
|
||||||
|
|
||||||
@ResolveField(() => [SolutionTargetStepObjectType!]!)
|
@ResolveField(() => [SolutionTargetStepObjectType!]!)
|
||||||
steps(
|
steps(
|
||||||
@Parent() solutionTarget: SolutionTargetObjectType,
|
@Parent() solutionTarget: SolutionTargetObjectType,
|
||||||
): Promise<SolutionTargetStepObjectType[]> {
|
): Promise<SolutionTargetStepObjectType[]> {
|
||||||
return this.anomaliesService.findSolutionTargetStepsBySolutionTargetId(
|
return this.anomalyService.findSolutionTargetStepsBySolutionTargetId(
|
||||||
solutionTarget.id,
|
solutionTarget.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
||||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||||
import { HomeResolver } from './home.resolver';
|
import { HomeResolver } from './home.resolver';
|
||||||
import { HomeService } from './home.service';
|
import { HomeService } from './home.service';
|
||||||
|
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule, FeatBusinessObjectsModule],
|
||||||
providers: [HomeResolver, HomeService],
|
providers: [HomeResolver, HomeService],
|
||||||
})
|
})
|
||||||
export class AppHomeModule {}
|
export class AppHomeModule {}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import { Injectable } from '@nestjs/common';
|
||||||
import { ApprovalState, Prisma, Tour } from '@prisma/client';
|
import { ApprovalState, Prisma, Tour } from '@prisma/client';
|
||||||
import { addDays, endOfDay, format, startOfDay, subDays } from 'date-fns';
|
import { addDays, endOfDay, format, startOfDay, subDays } from 'date-fns';
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
import { AnomalyTypes } from '../feat-anomalies/anomaly-detectors/anomaly-types.enum';
|
import { AnomalyTypes } from '../feat-business-objects/entities/anomaly/anomaly-types.enum';
|
||||||
|
import { AnomalyService } from '../feat-business-objects/entities/anomaly/anomaly.service';
|
||||||
import {
|
import {
|
||||||
HomeDashboardKPIsObjectType,
|
HomeDashboardKPIsObjectType,
|
||||||
RevenueTrendObjectType,
|
RevenueTrendObjectType,
|
||||||
|
|
@ -10,7 +11,10 @@ import {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HomeService {
|
export class HomeService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly anomalyService: AnomalyService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async homeDashboardKpis(): Promise<HomeDashboardKPIsObjectType> {
|
async homeDashboardKpis(): Promise<HomeDashboardKPIsObjectType> {
|
||||||
const anomalyCount = await this.getAnomalyCount();
|
const anomalyCount = await this.getAnomalyCount();
|
||||||
|
|
@ -39,20 +43,13 @@ export class HomeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAnomalyCount(): Promise<number> {
|
private async getAnomalyCount(): Promise<number> {
|
||||||
return await this.prisma.anomaly.count({
|
return await this.anomalyService.countVisibleAnomalies();
|
||||||
where: {
|
|
||||||
resolvedAt: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getEmployeesWithoutTourCount(): Promise<number> {
|
private async getEmployeesWithoutTourCount(): Promise<number> {
|
||||||
return await this.prisma.anomaly.count({
|
return await this.anomalyService.countVisibleAnomalies([
|
||||||
where: {
|
AnomalyTypes.WorkingEmployeeNotInDistributionSystem,
|
||||||
type: AnomalyTypes.WorkingEmployeeNotInDistributionSystem,
|
]);
|
||||||
resolvedAt: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getToursWithoutApprovalCount(): Promise<number> {
|
private async getToursWithoutApprovalCount(): Promise<number> {
|
||||||
|
|
@ -378,9 +375,9 @@ export class HomeService {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
AND: [
|
AND: [
|
||||||
{ordinanceType: {not: null}},
|
{ ordinanceType: { not: null } },
|
||||||
{ordinanceType: {not: 'Tragehilfe'}},
|
{ ordinanceType: { not: 'Tragehilfe' } },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
OR: [
|
OR: [
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,11 @@ import {
|
||||||
isSameDay,
|
isSameDay,
|
||||||
startOfDay,
|
startOfDay,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TourAmountForecastService {
|
export class TourAmountForecastService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly tourService: TourService) {}
|
||||||
|
|
||||||
calculateTourAmountForecastFourWeekPeriod(
|
calculateTourAmountForecastFourWeekPeriod(
|
||||||
ordinanceType: string | undefined,
|
ordinanceType: string | undefined,
|
||||||
|
|
@ -150,94 +150,9 @@ export class TourAmountForecastService {
|
||||||
interval: Interval,
|
interval: Interval,
|
||||||
ordinanceType: string | undefined,
|
ordinanceType: string | undefined,
|
||||||
) {
|
) {
|
||||||
const ordinanceTypeToQuery = ordinanceType.split(' ')[0];
|
return this.tourService.findInInterval(
|
||||||
return this.prisma.tour.findMany({
|
interval,
|
||||||
where: {
|
ordinanceType ? [ordinanceType] : [],
|
||||||
AND: [
|
);
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gte: new Date(interval.start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: new Date(interval.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(ordinanceType
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
ordinanceType: ordinanceTypeToQuery,
|
|
||||||
},
|
|
||||||
...(ordinanceType.indexOf('TSW') > -1
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
transportType:
|
|
||||||
ordinanceType.indexOf('1') > -1 ? 'TS1' : 'TS2',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
carName: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Storno',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Löschen',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Abgabe',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
check: {
|
|
||||||
not: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
patientId: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Pause
|
|
||||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Feierabend
|
|
||||||
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,14 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||||
import { TicketLifecycleService } from './ticket-lifecycle.service';
|
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
|
||||||
|
import { FeatTicketSystemModule } from '../feat-tickets/ticket-system.module';
|
||||||
import { TicketRepository } from './ticket.repository';
|
import { TicketRepository } from './ticket.repository';
|
||||||
import { TicketsResolver } from './tickets.resolver';
|
import { TicketsResolver } from './tickets.resolver';
|
||||||
import { TicketService } from './tickets.service';
|
import { TicketSystemService } from './ticket-system.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule, FeatTicketSystemModule, FeatBusinessObjectsModule],
|
||||||
providers: [
|
providers: [TicketRepository, TicketSystemService, TicketsResolver],
|
||||||
TicketRepository,
|
exports: [TicketSystemService],
|
||||||
TicketService,
|
|
||||||
TicketLifecycleService,
|
|
||||||
TicketsResolver,
|
|
||||||
],
|
|
||||||
exports: [TicketService],
|
|
||||||
})
|
})
|
||||||
export class ApplicationTicketSystemModule {}
|
export class ApplicationTicketSystemModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { ArgsType, Field, InputType } from '@nestjs/graphql';
|
import { ArgsType, Field, InputType } from '@nestjs/graphql';
|
||||||
import { ApprovalState, TicketValidationState } from '@prisma/client';
|
import { ApprovalState, TicketValidationState } from '@prisma/client';
|
||||||
import { PaginationArgs } from 'src/core/base/pagination.input';
|
import { PaginationArgs } from 'src/core/base/pagination.input';
|
||||||
import { TicketFilter } from './ticket-filter';
|
import { TicketFilter } from '../feat-business-objects/entities/ticket/ticket-filter';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class TicketFilterArgs implements TicketFilter {
|
export class TicketFilterArgs implements TicketFilter {
|
||||||
|
|
|
||||||
|
|
@ -1,266 +0,0 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
ApprovalState,
|
|
||||||
Ticket,
|
|
||||||
TicketValidationState,
|
|
||||||
Tour,
|
|
||||||
} from '@prisma/client';
|
|
||||||
import { Interval } from 'date-fns';
|
|
||||||
import { DocumentInfoArgs } from './ticket-arg.types';
|
|
||||||
import {
|
|
||||||
ValidationResult,
|
|
||||||
validatePatient,
|
|
||||||
validatePatientDataOnTours,
|
|
||||||
validateTour,
|
|
||||||
} from './ticket-validation.functions';
|
|
||||||
import { TicketRepository } from './ticket.repository';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TicketLifecycleService {
|
|
||||||
private readonly logger = new Logger(TicketLifecycleService.name);
|
|
||||||
|
|
||||||
constructor(private readonly repository: TicketRepository) {}
|
|
||||||
|
|
||||||
async createTickets(interval: Interval): Promise<void> {
|
|
||||||
const tours = await this.repository.findTours(interval);
|
|
||||||
|
|
||||||
for (const tour of tours) {
|
|
||||||
if (tour.ticketId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const ticketId = await this.repository.searchTicketIDForTour(tour);
|
|
||||||
if (ticketId) {
|
|
||||||
await this.repository.addToursToTicket(ticketId, [tour]);
|
|
||||||
} else {
|
|
||||||
await this.repository.createTicket([tour]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.validateCreatedTickets();
|
|
||||||
// await this.archiveClosedTickets();
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateDocumentInfoUpdate(
|
|
||||||
ticketId: string,
|
|
||||||
documentInfo: DocumentInfoArgs,
|
|
||||||
) {
|
|
||||||
const ticket = await this.repository.findById(ticketId);
|
|
||||||
|
|
||||||
switch (ticket.currentState) {
|
|
||||||
case TicketValidationState.USER_INTERACTION_REQUIRED:
|
|
||||||
const approvalStateUpdate = documentInfo.approval.isNeeded
|
|
||||||
? ApprovalState.REQUESTED
|
|
||||||
: ApprovalState.FREE;
|
|
||||||
|
|
||||||
let stateUpdate: TicketValidationState =
|
|
||||||
TicketValidationState.USER_INTERACTION_REQUIRED;
|
|
||||||
if (
|
|
||||||
documentInfo.tdLocation === 'company' &&
|
|
||||||
approvalStateUpdate === ApprovalState.FREE
|
|
||||||
)
|
|
||||||
stateUpdate = TicketValidationState.ARCHIVED;
|
|
||||||
else if (documentInfo.tdLocation === 'uncertain')
|
|
||||||
stateUpdate = TicketValidationState.TD_UNCERTAIN;
|
|
||||||
else stateUpdate = TicketValidationState.DOCUMENTS_MISSING;
|
|
||||||
|
|
||||||
return this.repository.updateTicket(ticketId, {
|
|
||||||
documentInfo,
|
|
||||||
approvalState: approvalStateUpdate,
|
|
||||||
state: stateUpdate,
|
|
||||||
});
|
|
||||||
|
|
||||||
case TicketValidationState.TD_UNCERTAIN:
|
|
||||||
return this.repository.updateTicket(ticketId, {
|
|
||||||
documentInfo,
|
|
||||||
state:
|
|
||||||
ticket.approvalState === ApprovalState.FREE &&
|
|
||||||
documentInfo.tdLocation === 'company'
|
|
||||||
? TicketValidationState.ARCHIVED
|
|
||||||
: TicketValidationState.DOCUMENTS_MISSING,
|
|
||||||
});
|
|
||||||
case TicketValidationState.DOCUMENTS_MISSING:
|
|
||||||
return this.repository.updateTicket(ticketId, {
|
|
||||||
documentInfo,
|
|
||||||
...((ticket.approvalState === ApprovalState.FREE ||
|
|
||||||
documentInfo.approval.location === 'company') &&
|
|
||||||
documentInfo.tdLocation === 'company'
|
|
||||||
? { state: TicketValidationState.ARCHIVED }
|
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async validateCreatedTickets(): Promise<void> {
|
|
||||||
const tickets = await this.repository.findAllCreated();
|
|
||||||
|
|
||||||
for (const ticket of tickets) {
|
|
||||||
const {
|
|
||||||
result: isValid,
|
|
||||||
errors,
|
|
||||||
notes,
|
|
||||||
} = await this.validateCreatedTicket(ticket);
|
|
||||||
|
|
||||||
if (isValid) {
|
|
||||||
const { result: approvalResult } = await this.isApprovalNeeded(ticket);
|
|
||||||
if (approvalResult) {
|
|
||||||
await this.repository.updateTicket(ticket.id, {
|
|
||||||
errors,
|
|
||||||
notes,
|
|
||||||
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
|
||||||
approvalState: ticket.tours.every(({ patientId }) => !patientId)
|
|
||||||
? ApprovalState.UNKNOWN
|
|
||||||
: ApprovalState.REQUIRED,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// NOTE: Condition required to get these tickets out of USER_INTERACTION_REQUIRED
|
|
||||||
if (
|
|
||||||
ticket.tours.every(
|
|
||||||
({ ordinanceType }) =>
|
|
||||||
ordinanceType.indexOf('BTW') > -1 ||
|
|
||||||
ordinanceType.indexOf('TSW') > -1,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
await this.repository.updateTicket(ticket.id, {
|
|
||||||
errors,
|
|
||||||
notes,
|
|
||||||
state: TicketValidationState.TD_UNCERTAIN,
|
|
||||||
approvalState: ApprovalState.FREE,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await this.repository.updateTicket(ticket.id, {
|
|
||||||
errors,
|
|
||||||
notes,
|
|
||||||
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
|
||||||
approvalState: ApprovalState.FREE,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.repository.setActionablesOnTicket(ticket.id, errors, notes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async archiveClosedTickets() {
|
|
||||||
const closedTickets = await this.repository.findAllClosed();
|
|
||||||
for (const closedTicket of closedTickets) {
|
|
||||||
await this.repository.setStateOfTicket(
|
|
||||||
closedTicket.id,
|
|
||||||
TicketValidationState.ARCHIVED,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async validateCreatedTicket(
|
|
||||||
ticket: Ticket & { tours: Tour[] },
|
|
||||||
): Promise<ValidationResult> {
|
|
||||||
const patientIds = [
|
|
||||||
...new Set(ticket.tours.map(({ patientId }) => patientId)),
|
|
||||||
];
|
|
||||||
if (patientIds.length > 1) {
|
|
||||||
return {
|
|
||||||
result: false,
|
|
||||||
errors: ['Ticket: Die Touren umfassen mehr als einen Patienten'],
|
|
||||||
notes: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const [patientId] = patientIds;
|
|
||||||
if (!patientId && ticket.tours.at(0)?.type !== 'Spontan') {
|
|
||||||
return {
|
|
||||||
result: false,
|
|
||||||
errors: [],
|
|
||||||
notes: ['Patient: Der Patient ist nicht in DispoLive hinterlegt'],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let patientResult: ValidationResult;
|
|
||||||
if (!patientId && ticket.tours.at(0)?.type === 'Spontan') {
|
|
||||||
patientResult = validatePatientDataOnTours(ticket.tours);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const patient = await this.repository.findPatientById(patientId);
|
|
||||||
patientResult = validatePatient(patient);
|
|
||||||
} catch {
|
|
||||||
patientResult = validatePatientDataOnTours(ticket.tours);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const tourResults = ticket.tours.map((tour) => validateTour(tour));
|
|
||||||
|
|
||||||
return [patientResult, ...tourResults].reduce(
|
|
||||||
(result, results) => ({
|
|
||||||
result: result.result && results.result,
|
|
||||||
errors: [...result.errors, ...results.errors],
|
|
||||||
notes: [...result.notes, ...results.notes],
|
|
||||||
}),
|
|
||||||
{ result: true, errors: [], notes: [] } as ValidationResult,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async isApprovalNeeded(
|
|
||||||
ticket: Ticket & { tours: Tour[] },
|
|
||||||
): Promise<ValidationResult> {
|
|
||||||
const approvalRequirements = await Promise.all(
|
|
||||||
ticket.tours.map((tour) => this.checkTourApprovalRequirement(tour)),
|
|
||||||
);
|
|
||||||
if(ticket.tours.some(({ordinanceType}) => ordinanceType.indexOf('TSW') > -1)) {
|
|
||||||
console.log(approvalRequirements)
|
|
||||||
}
|
|
||||||
const isApprovalRequired = approvalRequirements.reduce(
|
|
||||||
(result, requirement) => result || requirement,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: isApprovalRequired,
|
|
||||||
errors: [],
|
|
||||||
notes:
|
|
||||||
isApprovalRequired && ticket.tours.every(({ patientId }) => !patientId)
|
|
||||||
? [
|
|
||||||
'Patient nicht in Stammdaten: Prüfen Sie ob eine Genehmigung benötigt wird',
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkTourApprovalRequirement(tour: Tour): Promise<boolean> {
|
|
||||||
// NOTE: Requirement by Avicenna
|
|
||||||
if (
|
|
||||||
tour.ordinanceType.indexOf('BTW') > -1 ||
|
|
||||||
tour.ordinanceType.indexOf('TSW') > -1
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
[
|
|
||||||
'Einweisung',
|
|
||||||
'Entlassung',
|
|
||||||
'Entlassung Vorbestellung',
|
|
||||||
'Konsil KH - Dia',
|
|
||||||
'Privatfahrt Rechnung',
|
|
||||||
'Tagesklinik',
|
|
||||||
'Verlegung',
|
|
||||||
].includes(tour.category) ||
|
|
||||||
tour.healthInsurance === 'KOSTENTRÄGER PATIENT'
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
tour.patientId &&
|
|
||||||
['BTW', 'TSW'].includes(tour.ordinanceType) &&
|
|
||||||
tour.category !== 'Dialyse'
|
|
||||||
) {
|
|
||||||
const patient = await this.repository.findPatientById(tour.patientId);
|
|
||||||
if (
|
|
||||||
['3', '4', '5'].includes(patient.careDegree) &&
|
|
||||||
(['aG', 'Bi', 'H'].includes(patient.disabilityMark1) ||
|
|
||||||
['aG', 'Bi', 'H'].includes(patient.disabilityMark2))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
export enum TicketSystemEventTypes {
|
|
||||||
Validated = 'ticket-system.validated',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TicketSystemValidatedEvent {
|
|
||||||
constructor(public readonly count: number) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { EventEmitter2, OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
import { Ticket, TicketValidationState, Tour } from '@prisma/client';
|
import { Ticket, TicketValidationState, Tour } from '@prisma/client';
|
||||||
import { Interval } from 'date-fns';
|
|
||||||
import { PubSub } from 'graphql-subscriptions';
|
import { PubSub } from 'graphql-subscriptions';
|
||||||
import {
|
import {
|
||||||
decodeOffsetCursor,
|
decodeOffsetCursor,
|
||||||
encodeCursor,
|
encodeCursor,
|
||||||
} from 'src/core/base/pagination.util';
|
} from 'src/core/base/pagination.util';
|
||||||
import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types';
|
import { TicketService } from '../feat-business-objects/entities/ticket/ticket.service';
|
||||||
import { TicketLifecycleService } from './ticket-lifecycle.service';
|
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
|
||||||
|
import { TicketLifecycleService } from '../feat-tickets/ticket-lifecycle.service';
|
||||||
import {
|
import {
|
||||||
TicketSystemEventTypes,
|
TicketSystemEventTypes,
|
||||||
TicketSystemValidatedEvent,
|
TicketSystemValidatedEvent,
|
||||||
} from './ticket-system.events';
|
} from '../feat-tickets/ticket-system.events';
|
||||||
|
import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types';
|
||||||
import {
|
import {
|
||||||
PaginatedTicketObjectType,
|
PaginatedTicketObjectType,
|
||||||
TicketStateKeyFiguresObjectType,
|
TicketStateKeyFiguresObjectType,
|
||||||
|
|
@ -21,7 +22,7 @@ import {
|
||||||
import { TicketRepository } from './ticket.repository';
|
import { TicketRepository } from './ticket.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TicketService {
|
export class TicketSystemService {
|
||||||
public readonly pubSub = new PubSub();
|
public readonly pubSub = new PubSub();
|
||||||
|
|
||||||
@OnEvent(TicketSystemEventTypes.Validated, { async: true })
|
@OnEvent(TicketSystemEventTypes.Validated, { async: true })
|
||||||
|
|
@ -33,33 +34,25 @@ export class TicketService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly repository: TicketRepository,
|
private readonly repository: TicketRepository,
|
||||||
|
private readonly ticketService: TicketService,
|
||||||
|
private readonly tourService: TourService,
|
||||||
private readonly lifecycle: TicketLifecycleService,
|
private readonly lifecycle: TicketLifecycleService,
|
||||||
private readonly eventEmitter: EventEmitter2,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createTickets(interval: Interval): Promise<void> {
|
|
||||||
await this.lifecycle.createTickets(interval);
|
|
||||||
|
|
||||||
this.eventEmitter.emit(
|
|
||||||
TicketSystemEventTypes.Validated,
|
|
||||||
new TicketSystemValidatedEvent(0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateDocumentInfo(ticketId: string, documentInfo: DocumentInfoArgs) {
|
async updateDocumentInfo(ticketId: string, documentInfo: DocumentInfoArgs) {
|
||||||
return this.lifecycle.validateDocumentInfoUpdate(ticketId, documentInfo);
|
return this.lifecycle.validateDocumentInfoUpdate(ticketId, documentInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
findById(id: string): Promise<Ticket> {
|
findById(id: string): Promise<Ticket> {
|
||||||
return this.repository.findById(id);
|
return this.ticketService.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll(): Promise<Ticket[]> {
|
findAll(): Promise<Ticket[]> {
|
||||||
return this.repository.findAll() as Promise<Ticket[]>;
|
return this.ticketService.findAll() as Promise<Ticket[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
searchTickets(query: string): Promise<Ticket[]> {
|
searchTickets(query: string): Promise<Ticket[]> {
|
||||||
return this.repository.search(query);
|
return this.ticketService.search(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
async paginateTicketsByState(
|
async paginateTicketsByState(
|
||||||
|
|
@ -69,7 +62,7 @@ export class TicketService {
|
||||||
cursor?: string,
|
cursor?: string,
|
||||||
): Promise<PaginatedTicketObjectType> {
|
): Promise<PaginatedTicketObjectType> {
|
||||||
const skip = cursor ? decodeOffsetCursor(cursor) : 0;
|
const skip = cursor ? decodeOffsetCursor(cursor) : 0;
|
||||||
const { tickets, total } = await this.repository.findByState(
|
const { tickets, total } = await this.ticketService.findByState(
|
||||||
state,
|
state,
|
||||||
filter,
|
filter,
|
||||||
take,
|
take,
|
||||||
|
|
@ -88,12 +81,14 @@ export class TicketService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: obsolete
|
||||||
async getTicketStateMeta(
|
async getTicketStateMeta(
|
||||||
ticketState: TicketValidationState,
|
ticketState: TicketValidationState,
|
||||||
): Promise<TicketStateMetaObjectType> {
|
): Promise<TicketStateMetaObjectType> {
|
||||||
return this.repository.calculateTicketStateMeta(ticketState);
|
return this.repository.calculateTicketStateMeta(ticketState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: obsolete
|
||||||
getTicketStateKeyFigures(
|
getTicketStateKeyFigures(
|
||||||
ticketState: TicketValidationState,
|
ticketState: TicketValidationState,
|
||||||
): Promise<TicketStateKeyFiguresObjectType> {
|
): Promise<TicketStateKeyFiguresObjectType> {
|
||||||
|
|
@ -101,12 +96,12 @@ export class TicketService {
|
||||||
}
|
}
|
||||||
|
|
||||||
findToursByTicketId(ticketId: string): Promise<Tour[]> {
|
findToursByTicketId(ticketId: string): Promise<Tour[]> {
|
||||||
return this.repository.findToursByTicketId(ticketId);
|
return this.tourService.findByTicketId(ticketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
findStatesByTicketId(ticketId: string): Promise<TicketValidationState[]> {
|
findStatesByTicketId(ticketId: string): Promise<TicketValidationState[]> {
|
||||||
return this.repository
|
return this.ticketService
|
||||||
.findStatesByTicketId(ticketId)
|
.findStateHistoryByTicketId(ticketId)
|
||||||
.then((states) => states.map(({ state }) => state));
|
.then((states) => states.map(({ state }) => state));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
import { Patient, Tour } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface ValidationResult {
|
|
||||||
result: boolean;
|
|
||||||
notes: string[];
|
|
||||||
errors: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validatePatient(patient: Patient): ValidationResult {
|
|
||||||
const notes: string[] = [];
|
|
||||||
const errors: string[] = [];
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
if (!patient?.name) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push('Patient: Vorname ist nicht gefüllt');
|
|
||||||
}
|
|
||||||
if (!patient?.surname) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push('Patient: Nachname ist nicht gefüllt');
|
|
||||||
}
|
|
||||||
if (!patient?.birthday) {
|
|
||||||
isValid = false;
|
|
||||||
notes.push('Patient: Geburtstag ist nicht gefüllt');
|
|
||||||
}
|
|
||||||
if (!patient?.healthinsurance) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push('Patient: Krankenkasse ist nicht gefüllt');
|
|
||||||
}
|
|
||||||
// if (!patient?.careDegree) {
|
|
||||||
// notes.push('Patient: Pflegegrad ist nicht gefüllt');
|
|
||||||
// }
|
|
||||||
// if (!patient?.disabilityMark1) {
|
|
||||||
// notes.push('Patient: Merkzeichen 1 ist nicht gefüllt');
|
|
||||||
// }
|
|
||||||
// if (!patient?.disabilityMark2) {
|
|
||||||
// notes.push('Patient: Merkzeichen 2 ist nicht gefüllt');
|
|
||||||
// }
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: isValid,
|
|
||||||
errors,
|
|
||||||
notes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validatePatientDataOnTours(tours: Tour[]): ValidationResult {
|
|
||||||
const errors: string[] = [];
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
for (const tour of tours) {
|
|
||||||
if (!tour.patientName) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(
|
|
||||||
`Tour ${tour.operationId}: Patient Vorname ist nicht gefüllt`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!tour.patientSurname) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(
|
|
||||||
`Tour ${tour.operationId}: Patient Nachname ist nicht gefüllt`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!tour.healthInsurance) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(
|
|
||||||
`Tour ${tour.operationId}: Patient Krankenversicherung ist nicht gefüllt`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: isValid,
|
|
||||||
errors,
|
|
||||||
notes: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateTour(tour: Tour): ValidationResult {
|
|
||||||
const errors: string[] = [];
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
if (!tour.ordinanceType) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Verordnungsart ist nicht gefüllt`);
|
|
||||||
}
|
|
||||||
if (!tour.transportType) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Transportart ist nicht gefüllt`);
|
|
||||||
}
|
|
||||||
if (!tour.category) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Kategorie ist nicht gefüllt`);
|
|
||||||
}
|
|
||||||
if (!tour.startDate) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Startzeit ist nicht gefüllt`);
|
|
||||||
}
|
|
||||||
if (!tour.startStreet || !tour.startZip || !tour.startCity) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Start Adresse ist nicht gefüllt`);
|
|
||||||
}
|
|
||||||
if (!tour.targetStreet || !tour.targetZip || !tour.targetCity) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Ziel Adresse ist nicht gefüllt`);
|
|
||||||
}
|
|
||||||
if (tour.occupiedKm > 9990) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Besetzte Kilometer sind zu hoch`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: isValid,
|
|
||||||
errors,
|
|
||||||
notes: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateTransport(tour: Tour): ValidationResult {
|
|
||||||
const errors: string[] = [];
|
|
||||||
let isValid = true;
|
|
||||||
|
|
||||||
if (!tour.carName) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Fahrzeugname ist nicht angegeben`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tour.driverName) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Fahrer ist nicht angegeben`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tour.codriverName) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Beifahrer ist nicht angegeben`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tour.startBegin) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Anfahrt-Stempel fehlt`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tour.empty) {
|
|
||||||
isValid = false;
|
|
||||||
errors.push(`Tour ${tour.operationId}: Frei-Stempel fehlt`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
result: isValid,
|
|
||||||
errors,
|
|
||||||
notes: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,284 +1,17 @@
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
ApprovalState,
|
ApprovalState,
|
||||||
Patient,
|
|
||||||
Prisma,
|
Prisma,
|
||||||
TicketValidationState,
|
TicketValidationState
|
||||||
Tour,
|
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import {
|
import { differenceInBusinessDays } from 'date-fns';
|
||||||
differenceInBusinessDays,
|
|
||||||
endOfDay,
|
|
||||||
Interval,
|
|
||||||
startOfDay,
|
|
||||||
} from 'date-fns';
|
|
||||||
import { ExpressionBuilder, Kysely } from 'kysely';
|
|
||||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
|
||||||
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
|
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
import { DB } from 'src/core/database/types';
|
|
||||||
import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types';
|
|
||||||
import { createPrismaQueryFromTicketFilters } from './ticket-filter';
|
|
||||||
|
|
||||||
function withTours(eb: ExpressionBuilder<DB, 'Ticket'>) {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('Tour')
|
|
||||||
.selectAll('Tour')
|
|
||||||
.whereRef('Tour.ticketId', '=', 'Ticket.id')
|
|
||||||
.orderBy('Tour.operationId'),
|
|
||||||
).as('tours');
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TicketRepository {
|
export class TicketRepository {
|
||||||
private logger = new Logger(TicketRepository.name);
|
private logger = new Logger(TicketRepository.name);
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async createTicket(tours: Tour[]) {
|
|
||||||
const ticket = await this.db
|
|
||||||
.insertInto('Ticket')
|
|
||||||
.values({
|
|
||||||
currentState: TicketValidationState.CREATED,
|
|
||||||
})
|
|
||||||
.returningAll()
|
|
||||||
.executeTakeFirst();
|
|
||||||
|
|
||||||
await this.db
|
|
||||||
.insertInto('TicketStateHistory')
|
|
||||||
.values({
|
|
||||||
state: TicketValidationState.CREATED,
|
|
||||||
ticketId: ticket.id,
|
|
||||||
})
|
|
||||||
.executeTakeFirst();
|
|
||||||
|
|
||||||
await this.db
|
|
||||||
.updateTable('Tour')
|
|
||||||
.set({
|
|
||||||
ticketId: ticket.id,
|
|
||||||
})
|
|
||||||
.where(
|
|
||||||
'id',
|
|
||||||
'in',
|
|
||||||
tours.map(({ id }) => id),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return ticket;
|
|
||||||
}
|
|
||||||
|
|
||||||
async addToursToTicket(ticketId: string, tours: Tour[]) {
|
|
||||||
await this.db
|
|
||||||
.updateTable('Tour')
|
|
||||||
.set({
|
|
||||||
ticketId: ticketId,
|
|
||||||
})
|
|
||||||
.where(
|
|
||||||
'id',
|
|
||||||
'in',
|
|
||||||
tours.map(({ id }) => id),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return this.db
|
|
||||||
.selectFrom('Ticket')
|
|
||||||
.where('id', '=', ticketId)
|
|
||||||
.selectAll()
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
setActionablesOnTicket(ticketId: string, errors: string[], notes: string[]) {
|
|
||||||
return this.db
|
|
||||||
.updateTable('Ticket')
|
|
||||||
.set({ errors, notes })
|
|
||||||
.where('id', '=', ticketId)
|
|
||||||
.returningAll()
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
async setStateOfTicket(ticketId: string, state: TicketValidationState) {
|
|
||||||
await this.db
|
|
||||||
.updateTable('Ticket')
|
|
||||||
.set({
|
|
||||||
currentState: state,
|
|
||||||
})
|
|
||||||
.where('Ticket.id', '=', ticketId)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return this.db
|
|
||||||
.insertInto('TicketStateHistory')
|
|
||||||
.values({ state, ticketId })
|
|
||||||
.returningAll()
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTicket(
|
|
||||||
ticketId: string,
|
|
||||||
data: {
|
|
||||||
state?: TicketValidationState;
|
|
||||||
errors?: string[];
|
|
||||||
notes?: string[];
|
|
||||||
isUrgent?: boolean;
|
|
||||||
urgency?: Date;
|
|
||||||
approvalState?: ApprovalState;
|
|
||||||
documentInfo?: DocumentInfoArgs;
|
|
||||||
approvalPath?: string;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const { state, ...ticketData } = data;
|
|
||||||
|
|
||||||
await this.db
|
|
||||||
.updateTable('Ticket')
|
|
||||||
.set({ ...ticketData })
|
|
||||||
.where('id', '=', ticketId)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (state) {
|
|
||||||
await this.setStateOfTicket(ticketId, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.db
|
|
||||||
.selectFrom('Ticket')
|
|
||||||
.where('id', '=', ticketId)
|
|
||||||
.selectAll('Ticket')
|
|
||||||
.select((eb) => withTours(eb))
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
findById(id: string) {
|
|
||||||
return this.prisma.ticket.findUnique({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
tours: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findAll() {
|
|
||||||
return this.prisma.ticket.findMany({
|
|
||||||
where: {
|
|
||||||
state: {
|
|
||||||
none: {
|
|
||||||
state: TicketValidationState.ARCHIVED,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
tours: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findAllCreated() {
|
|
||||||
return this.prisma.ticket.findMany({
|
|
||||||
where: {
|
|
||||||
state: {
|
|
||||||
none: {
|
|
||||||
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
tours: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findAllClosed() {
|
|
||||||
return this.prisma.ticket.findMany({
|
|
||||||
where: {
|
|
||||||
tours: {
|
|
||||||
every: {
|
|
||||||
check: {
|
|
||||||
not: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
search(query: string) {
|
|
||||||
return this.prisma.ticket.findMany({
|
|
||||||
where: {
|
|
||||||
state: {
|
|
||||||
none: {
|
|
||||||
state: TicketValidationState.ARCHIVED,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tours: {
|
|
||||||
some: {
|
|
||||||
OR: [
|
|
||||||
...query.split(' ').flatMap((word) => [
|
|
||||||
{
|
|
||||||
patientName: {
|
|
||||||
contains: word,
|
|
||||||
mode: Prisma.QueryMode.insensitive,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientSurname: {
|
|
||||||
contains: word,
|
|
||||||
mode: Prisma.QueryMode.insensitive,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
operationId: {
|
|
||||||
contains: word,
|
|
||||||
mode: Prisma.QueryMode.insensitive,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
take: 5,
|
|
||||||
include: {
|
|
||||||
tours: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findByState(
|
|
||||||
state: TicketValidationState,
|
|
||||||
filter: TicketFilterArgs,
|
|
||||||
take?: number,
|
|
||||||
skip?: number,
|
|
||||||
) {
|
|
||||||
const query: Prisma.TicketFindManyArgs = {
|
|
||||||
...{
|
|
||||||
...(skip ? { skip, take: 10 } : {}),
|
|
||||||
...(take ? { take } : {}),
|
|
||||||
...(take && !skip ? { skip: 0 } : {}),
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
AND: createPrismaQueryFromTicketFilters(filter),
|
|
||||||
currentState: state,
|
|
||||||
tours: {
|
|
||||||
some: {
|
|
||||||
check: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
};
|
|
||||||
|
|
||||||
const [tickets, count] = await this.prisma.$transaction([
|
|
||||||
this.prisma.ticket.findMany({ ...query }),
|
|
||||||
this.prisma.ticket.count({ where: query.where }),
|
|
||||||
]);
|
|
||||||
return {
|
|
||||||
tickets,
|
|
||||||
total: count,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async calculateTicketStateMeta(ticketState: TicketValidationState) {
|
async calculateTicketStateMeta(ticketState: TicketValidationState) {
|
||||||
const ticketWithCurrentStateAndTours: Prisma.TicketWhereInput = {
|
const ticketWithCurrentStateAndTours: Prisma.TicketWhereInput = {
|
||||||
|
|
@ -350,6 +83,7 @@ export class TicketRepository {
|
||||||
|
|
||||||
async calculateTicketStateKeyFigures(ticketState: TicketValidationState) {
|
async calculateTicketStateKeyFigures(ticketState: TicketValidationState) {
|
||||||
const ticketWithCurrentStateAndTours: Prisma.TicketWhereInput = {
|
const ticketWithCurrentStateAndTours: Prisma.TicketWhereInput = {
|
||||||
|
currentState: ticketState,
|
||||||
tours: { some: { check: 0 } },
|
tours: { some: { check: 0 } },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -388,231 +122,4 @@ export class TicketRepository {
|
||||||
dwellTimesForTicketState.length,
|
dwellTimesForTicketState.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
findPatientById(patientId: string): Promise<Patient> {
|
|
||||||
return this.prisma.patient.findUnique({ where: { id: patientId } });
|
|
||||||
}
|
|
||||||
|
|
||||||
findStatesByTicketId(
|
|
||||||
ticketId: string,
|
|
||||||
): Promise<{ state: TicketValidationState }[]> {
|
|
||||||
return this.prisma.ticketStateHistory.findMany({
|
|
||||||
where: {
|
|
||||||
ticketId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
state: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findToursByTicketId(ticketId: string): Promise<Tour[]> {
|
|
||||||
return this.prisma.tour.findMany({
|
|
||||||
where: {
|
|
||||||
ticketId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchTicketIDForTour(tour: Tour): Promise<string | undefined> {
|
|
||||||
const tours = await Promise.all([
|
|
||||||
this.findFirstRecurringTour(tour),
|
|
||||||
this.findTourOnSameDayWithOppositeDirection(tour),
|
|
||||||
]);
|
|
||||||
const firstFoundTour = tours.filter((tour) => !!tour)[0];
|
|
||||||
if (firstFoundTour) {
|
|
||||||
const ticket = await this.prisma.ticket.findFirst({
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
tours: {
|
|
||||||
some: {
|
|
||||||
id: firstFoundTour.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return ticket?.id;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
findFirstRecurringTour(tour: Tour): Promise<Tour | null> {
|
|
||||||
const institutionStreet =
|
|
||||||
tour.direction === 'Hinfahrt' ? tour.targetStreet : tour.startStreet;
|
|
||||||
return this.prisma.tour.findFirst({
|
|
||||||
where: {
|
|
||||||
type: 'Serie',
|
|
||||||
category: tour.category,
|
|
||||||
ordinanceType: tour.ordinanceType,
|
|
||||||
patientId: tour.patientId,
|
|
||||||
patientName: tour.patientName,
|
|
||||||
patientSurname: tour.patientSurname,
|
|
||||||
patientStreet: tour.patientStreet,
|
|
||||||
patientCity: tour.patientCity,
|
|
||||||
patientZip: tour.patientZip,
|
|
||||||
OR: [
|
|
||||||
{ targetStreet: institutionStreet },
|
|
||||||
{ startStreet: institutionStreet },
|
|
||||||
],
|
|
||||||
rangeEndDate: {
|
|
||||||
gte: tour.startDate,
|
|
||||||
},
|
|
||||||
ticketId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
startDate: 'desc',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findTourOnSameDayWithOppositeDirection(tour: Tour): Promise<Tour | null> {
|
|
||||||
return this.prisma.tour.findFirst({
|
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gt: startOfDay(new Date(tour.startDate)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: endOfDay(new Date(tour.startDate)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startStreet: tour.targetStreet,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
targetStreet: tour.startStreet,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
direction: tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findToursWithTicket(interval: Interval): Promise<Tour[]> {
|
|
||||||
return this.prisma.tour.findMany({
|
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
ticketId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gte: new Date(interval.start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: new Date(interval.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTourFromTicket(tourId: string): Promise<Tour> {
|
|
||||||
return this.prisma.tour.update({
|
|
||||||
where: {
|
|
||||||
id: tourId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
Ticket: {
|
|
||||||
disconnect: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findTours(interval: Interval): Promise<Tour[]> {
|
|
||||||
return this.prisma.tour.findMany({
|
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gte: new Date(interval.start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: new Date(interval.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
carName: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Storno',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Löschen',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Abgabe',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
check: {
|
|
||||||
not: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ticketId: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Pause
|
|
||||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Feierabend
|
|
||||||
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteEmptyTickets(): Promise<Prisma.BatchPayload> {
|
|
||||||
return this.prisma.ticket.deleteMany({
|
|
||||||
where: {
|
|
||||||
tours: {
|
|
||||||
none: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@ import {
|
||||||
TicketStateMetaObjectType,
|
TicketStateMetaObjectType,
|
||||||
TicketsUpdatedObjectType,
|
TicketsUpdatedObjectType,
|
||||||
} from './ticket.object-type';
|
} from './ticket.object-type';
|
||||||
import { TicketService } from './tickets.service';
|
import { TicketSystemService } from './ticket-system.service';
|
||||||
|
|
||||||
@Resolver(() => TicketObjectType)
|
@Resolver(() => TicketObjectType)
|
||||||
export class TicketsResolver {
|
export class TicketsResolver {
|
||||||
constructor(private readonly ticketsService: TicketService) {}
|
constructor(private readonly ticketsService: TicketSystemService) {}
|
||||||
|
|
||||||
@Query(() => TicketObjectType)
|
@Query(() => TicketObjectType)
|
||||||
ticket(@Args('id') id: string) {
|
ticket(@Args('id') id: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,23 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import {
|
import { Anomaly } from '@prisma/client';
|
||||||
Anomaly,
|
|
||||||
Prisma,
|
|
||||||
SolutionTarget,
|
|
||||||
SolutionTargetStep,
|
|
||||||
Tour,
|
|
||||||
} from '@prisma/client';
|
|
||||||
import { Interval } from 'date-fns';
|
import { Interval } from 'date-fns';
|
||||||
|
|
||||||
import { TraceTypes } from 'src/core/base/trace.types';
|
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
|
||||||
import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service';
|
import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service';
|
||||||
import { AnomalyTypes } from './anomaly-detectors/anomaly-types.enum';
|
|
||||||
import { DriverCertificateDetector } from './anomaly-detectors/driver-certificate.detector';
|
import { DriverCertificateDetector } from './anomaly-detectors/driver-certificate.detector';
|
||||||
import { DriverCodriverSameDetector } from './anomaly-detectors/driver-codriver-same.detector';
|
import { DriverCodriverSameDetector } from './anomaly-detectors/driver-codriver-same.detector';
|
||||||
import { EmployeesLoggedInDetector } from './anomaly-detectors/employees-logged-in.detector';
|
import { EmployeesLoggedInDetector } from './anomaly-detectors/employees-logged-in.detector';
|
||||||
// import { OneTourAtATimeDetector } from './anomaly-detectors/one-tour-at-a-time.detector';
|
// import { OneTourAtATimeDetector } from './anomaly-detectors/one-tour-at-a-time.detector';
|
||||||
import { WorkingEmployeeNotInDistributionSystemDetector } from './anomaly-detectors/working-employee-not-in-distribution-system.detector';
|
import { AnomalyCreateDto } from '../feat-business-objects/entities/anomaly/anomalies.dto';
|
||||||
|
import { AnomalyService } from '../feat-business-objects/entities/anomaly/anomaly.service';
|
||||||
import { AnomalyDetector } from './anomaly-detectors/anomaly-detector.class';
|
import { AnomalyDetector } from './anomaly-detectors/anomaly-detector.class';
|
||||||
|
import { WorkingEmployeeNotInDistributionSystemDetector } from './anomaly-detectors/working-employee-not-in-distribution-system.detector';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AnomaliesService {
|
export class AnomaliesService {
|
||||||
private detectors: AnomalyDetector[];
|
private detectors: AnomalyDetector[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly anomalyDetectorService: AnomalyDetectorService,
|
private readonly anomalyDetectorService: AnomalyDetectorService,
|
||||||
|
private readonly anomalyService: AnomalyService,
|
||||||
) {
|
) {
|
||||||
this.detectors = [
|
this.detectors = [
|
||||||
new EmployeesLoggedInDetector(this.anomalyDetectorService),
|
new EmployeesLoggedInDetector(this.anomalyDetectorService),
|
||||||
|
|
@ -47,15 +39,9 @@ export class AnomaliesService {
|
||||||
for (const anomaly of anomalies) {
|
for (const anomaly of anomalies) {
|
||||||
if (anomaly) {
|
if (anomaly) {
|
||||||
anomalyEntities.push(
|
anomalyEntities.push(
|
||||||
await this.prisma.anomaly.upsert({
|
await this.anomalyService.createEvenIfExists(
|
||||||
where: {
|
anomaly as AnomalyCreateDto,
|
||||||
id: anomaly.id,
|
),
|
||||||
},
|
|
||||||
create: {
|
|
||||||
...anomaly,
|
|
||||||
},
|
|
||||||
update: {},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,11 +50,7 @@ export class AnomaliesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateAnomalies(): Promise<Anomaly[]> {
|
async validateAnomalies(): Promise<Anomaly[]> {
|
||||||
const anomalies = await this.prisma.anomaly.findMany({
|
const anomalies = await this.anomalyService.findAllUnsolved();
|
||||||
where: {
|
|
||||||
resolvedAt: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
anomalies.map((anomaly) =>
|
anomalies.map((anomaly) =>
|
||||||
|
|
@ -81,171 +63,10 @@ export class AnomaliesService {
|
||||||
const updatedAnomalies: Anomaly[] = [];
|
const updatedAnomalies: Anomaly[] = [];
|
||||||
for (const anomaly of validatedAnomalies) {
|
for (const anomaly of validatedAnomalies) {
|
||||||
updatedAnomalies.push(
|
updatedAnomalies.push(
|
||||||
await this.prisma.anomaly.update({
|
await this.anomalyService.resolveAnomaly(anomaly.id),
|
||||||
where: {
|
|
||||||
id: anomaly.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
resolvedAt: anomaly.resolvedAt,
|
|
||||||
manuallyResolved: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return updatedAnomalies;
|
return updatedAnomalies;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
countActiveAnomalies(types: AnomalyTypes[] = []): Promise<number> {
|
|
||||||
return this.prisma.anomaly.count({
|
|
||||||
where: this.getWhereActiveAnomalyQuery(types),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findActiveAnomalies(
|
|
||||||
skip?: number,
|
|
||||||
take?: number,
|
|
||||||
types: AnomalyTypes[] = [],
|
|
||||||
): Promise<Anomaly[]> {
|
|
||||||
return this.prisma.anomaly.findMany({
|
|
||||||
...{
|
|
||||||
...(skip ? { skip, take: 10 } : {}),
|
|
||||||
...(take ? { take } : {}),
|
|
||||||
...(take && !skip ? { skip: 0 } : {}),
|
|
||||||
},
|
|
||||||
where: this.getWhereActiveAnomalyQuery(types),
|
|
||||||
orderBy: {
|
|
||||||
groupKey: 'asc',
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
solution: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findActiveAnomaliesGroupedByGroupKey(
|
|
||||||
skip = 0,
|
|
||||||
take = 10,
|
|
||||||
): Promise<{
|
|
||||||
totalCount: number;
|
|
||||||
anomalies: { key: string; anomalies: Anomaly[] }[];
|
|
||||||
}> {
|
|
||||||
const activeAnomalies = await this.findActiveAnomalies(
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
[
|
|
||||||
AnomalyTypes.DriverCertificate,
|
|
||||||
AnomalyTypes.EmployeesLoggedIn,
|
|
||||||
AnomalyTypes.OneTourAtATime,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const groupedAnomalies = this.groupAnomaliesByGroupKey(activeAnomalies);
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalCount: Object.keys(groupedAnomalies).length,
|
|
||||||
anomalies: Object.entries(groupedAnomalies)
|
|
||||||
.map(([key, anomalies]) => ({
|
|
||||||
key,
|
|
||||||
anomalies,
|
|
||||||
}))
|
|
||||||
.slice(skip, skip + take),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
findById(id: string): Promise<Anomaly> {
|
|
||||||
return this.prisma.anomaly.findUnique({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findSolutionTargetsByAnomalyId(anomalyId: string): Promise<SolutionTarget[]> {
|
|
||||||
return this.prisma.solutionTarget.findMany({
|
|
||||||
where: {
|
|
||||||
anomalyId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findSolutionTargetStepsBySolutionTargetId(
|
|
||||||
solutionTargetId: string,
|
|
||||||
): Promise<SolutionTargetStep[]> {
|
|
||||||
return this.prisma.solutionTargetStep.findMany({
|
|
||||||
where: {
|
|
||||||
solutionTargetId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findTourByOperationId(operationId: string): Promise<Tour> {
|
|
||||||
return this.prisma.tour.findUnique({ where: { operationId } });
|
|
||||||
}
|
|
||||||
|
|
||||||
async setAnomalyToSleep(anomalyId: string): Promise<Anomaly> {
|
|
||||||
await this.prisma.trace.create({
|
|
||||||
data: {
|
|
||||||
entityKey: anomalyId,
|
|
||||||
type: TraceTypes.AnomalyIgnored,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return this.prisma.anomaly.update({
|
|
||||||
where: {
|
|
||||||
id: anomalyId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
sleepSince: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async acceptAnomaly(anomalyId: string): Promise<Anomaly> {
|
|
||||||
await this.prisma.trace.create({
|
|
||||||
data: {
|
|
||||||
entityKey: anomalyId,
|
|
||||||
type: TraceTypes.AnomalyAccepted,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return this.prisma.anomaly.update({
|
|
||||||
where: {
|
|
||||||
id: anomalyId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
resolvedAt: new Date(),
|
|
||||||
manuallyResolved: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getWhereActiveAnomalyQuery(
|
|
||||||
types: AnomalyTypes[] = [],
|
|
||||||
): Prisma.AnomalyWhereInput {
|
|
||||||
return {
|
|
||||||
...(types.length ? { type: { in: types } } : {}),
|
|
||||||
resolvedAt: null,
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
sleepSince: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sleepSince: {
|
|
||||||
lt: new Date(new Date().getTime() - 60 * 60 * 1000),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private groupAnomaliesByGroupKey(
|
|
||||||
anomalies: Anomaly[],
|
|
||||||
): Record<string, Anomaly[]> {
|
|
||||||
return anomalies.reduce(
|
|
||||||
(grouped, anomaly) => ({
|
|
||||||
...grouped,
|
|
||||||
[anomaly.groupKey]: [...(grouped[anomaly.groupKey] ?? []), anomaly],
|
|
||||||
}),
|
|
||||||
{} as Record<string, Anomaly[]>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
||||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||||
import { AnomaliesService } from './anomalies.service';
|
import { AnomaliesService } from './anomalies.service';
|
||||||
import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service';
|
import { AnomalyDetectorService } from './anomaly-detectors/anomaly-detector.service';
|
||||||
|
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule, FeatBusinessObjectsModule],
|
||||||
providers: [AnomaliesService, AnomalyDetectorService],
|
providers: [AnomaliesService, AnomalyDetectorService],
|
||||||
exports: [AnomaliesService],
|
exports: [AnomaliesService],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,12 @@ import {
|
||||||
WorkTime,
|
WorkTime,
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { Interval, addDays, addHours, endOfDay, startOfDay } from 'date-fns';
|
import { Interval, addDays, addHours, endOfDay, startOfDay } from 'date-fns';
|
||||||
import { AttendanceRegistrationEvents } from 'src/core/base/attendance-registration-events.enum';
|
import { AttendanceRegistrationEvents } from 'src/modules/feat-business-objects/entities/attendance-registration/attendance-registration-events.enum';
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
import { AttendanceRegistrationService } from 'src/modules/feat-business-objects/entities/attendance-registration/attendance-registration.service';
|
||||||
|
import { EmployeeService } from 'src/modules/feat-business-objects/entities/employee/employee.service';
|
||||||
|
import { PlannedTimeService } from 'src/modules/feat-business-objects/entities/planned-time/planned-time.service';
|
||||||
|
import { TourService } from 'src/modules/feat-business-objects/entities/tour/tour.service';
|
||||||
|
import { WorkTimeService } from 'src/modules/feat-business-objects/entities/work-time/work-time.service';
|
||||||
|
|
||||||
export type AnomalySkeleton = Pick<
|
export type AnomalySkeleton = Pick<
|
||||||
Prisma.AnomalyCreateInput,
|
Prisma.AnomalyCreateInput,
|
||||||
|
|
@ -23,7 +27,13 @@ export type AnomalySkeleton = Pick<
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AnomalyDetectorService {
|
export class AnomalyDetectorService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(
|
||||||
|
private readonly attendanceRegistrationService: AttendanceRegistrationService,
|
||||||
|
private readonly employeeService: EmployeeService,
|
||||||
|
private readonly plannedTimeService: PlannedTimeService,
|
||||||
|
private readonly tourService: TourService,
|
||||||
|
private readonly workTimeService: WorkTimeService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a list of tours in a given time interval
|
* Find a list of tours in a given time interval
|
||||||
|
|
@ -31,170 +41,26 @@ export class AnomalyDetectorService {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public findToursInInterval(
|
public findToursInInterval(
|
||||||
{ start, end }: Interval,
|
interval: Interval,
|
||||||
ordinanceTypes: string[] = [],
|
ordinanceTypes: string[] = [],
|
||||||
): Promise<Tour[]> {
|
): Promise<Tour[]> {
|
||||||
return this.prisma.tour.findMany({
|
return this.tourService.findInInterval(interval, ordinanceTypes);
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gte: new Date(start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: new Date(end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
check: {
|
|
||||||
lte: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ordinanceTypes.length
|
|
||||||
? { ordinanceType: { in: ordinanceTypes } }
|
|
||||||
: {},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
carName: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Storno',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Löschen',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Abgabe',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
patientId: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Pause
|
|
||||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Feierabend
|
|
||||||
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
startDate: 'asc',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findToursInIntervalWithEmployee(
|
public findToursInIntervalWithEmployee(
|
||||||
{ start, end }: Interval,
|
interval: Interval,
|
||||||
employeeId: string,
|
employeeId: string,
|
||||||
ordinanceType?: string,
|
ordinanceType?: string,
|
||||||
): Promise<Tour[]> {
|
): Promise<Tour[]> {
|
||||||
return this.prisma.tour.findMany({
|
return this.tourService.findInIntervalWithEmployee(
|
||||||
where: {
|
interval,
|
||||||
AND: [
|
employeeId,
|
||||||
{
|
ordinanceType ? [ordinanceType] : [],
|
||||||
OR: [
|
);
|
||||||
{
|
|
||||||
driverId: employeeId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
codriverId: employeeId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gte: new Date(start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: new Date(end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(ordinanceType ? [{ ordinanceType }] : []),
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
carName: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Storno',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Löschen',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Abgabe',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
patientId: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Pause
|
|
||||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Feierabend
|
|
||||||
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findTourByOperationId(operationId): Promise<Tour> {
|
public findTourByOperationId(operationId): Promise<Tour> {
|
||||||
return this.prisma.tour.findUnique({ where: { operationId } });
|
return this.tourService.findByOperationId(operationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findWorkTimeByPersonnelNumberForDay(
|
public findWorkTimeByPersonnelNumberForDay(
|
||||||
|
|
@ -208,23 +74,10 @@ export class AnomalyDetectorService {
|
||||||
startOfGivenDay = startOfDay(day);
|
startOfGivenDay = startOfDay(day);
|
||||||
}
|
}
|
||||||
const endOfGivenDay = endOfDay(startOfGivenDay);
|
const endOfGivenDay = endOfDay(startOfGivenDay);
|
||||||
return this.prisma.workTime.findFirst({
|
return this.workTimeService.findByPersonnelNumberInInterval(
|
||||||
where: {
|
personnelNumber,
|
||||||
userId: personnelNumber,
|
{ start: startOfGivenDay, end: endOfGivenDay },
|
||||||
AND: [
|
);
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gte: startOfGivenDay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lte: endOfGivenDay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findAttendanceRegistrationByPersonnelNumberForShift(
|
public findAttendanceRegistrationByPersonnelNumberForShift(
|
||||||
|
|
@ -242,29 +95,12 @@ export class AnomalyDetectorService {
|
||||||
startOfGivenDay = startOfDay(day);
|
startOfGivenDay = startOfDay(day);
|
||||||
}
|
}
|
||||||
const endOfShift = addHours(day, 12);
|
const endOfShift = addHours(day, 12);
|
||||||
return this.prisma.attendanceRegistration.findMany({
|
|
||||||
where: {
|
return this.attendanceRegistrationService.findByEmployeeInInterval(
|
||||||
employeeId: +personnelNumber,
|
+personnelNumber,
|
||||||
event: {
|
{ start: startOfGivenDay, end: endOfShift },
|
||||||
in: eventTypes,
|
eventTypes,
|
||||||
},
|
);
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
dateTime: {
|
|
||||||
gte: startOfGivenDay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dateTime: {
|
|
||||||
lt: endOfShift,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
dateTime: 'asc',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findPlannedTimeByPersonnelNumberForDay(
|
public findPlannedTimeByPersonnelNumberForDay(
|
||||||
|
|
@ -278,33 +114,18 @@ export class AnomalyDetectorService {
|
||||||
startOfGivenDay = startOfDay(day);
|
startOfGivenDay = startOfDay(day);
|
||||||
}
|
}
|
||||||
const endOfShift = addHours(day, 12);
|
const endOfShift = addHours(day, 12);
|
||||||
return this.prisma.plannedTime.findMany({
|
return this.plannedTimeService.findByPersonnelNumberInInterval(
|
||||||
where: {
|
personnelNumber,
|
||||||
userId: personnelNumber,
|
{ start: startOfGivenDay, end: endOfShift },
|
||||||
AND: [
|
);
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gte: startOfGivenDay,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: endOfShift,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findAttendanceRegistrationById(
|
public findAttendanceRegistrationById(
|
||||||
attendanceRegistrationId: number,
|
attendanceRegistrationId: number,
|
||||||
): Promise<AttendanceRegistration> {
|
): Promise<AttendanceRegistration> {
|
||||||
return this.prisma.attendanceRegistration.findUnique({
|
return this.attendanceRegistrationService.findById(
|
||||||
where: {
|
attendanceRegistrationId,
|
||||||
attendanceRegistrationId,
|
);
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findAttendanceRegistrationInInterval(
|
public findAttendanceRegistrationInInterval(
|
||||||
|
|
@ -314,37 +135,19 @@ export class AnomalyDetectorService {
|
||||||
AttendanceRegistrationEvents.ClockedOut,
|
AttendanceRegistrationEvents.ClockedOut,
|
||||||
],
|
],
|
||||||
): Promise<AttendanceRegistration[]> {
|
): Promise<AttendanceRegistration[]> {
|
||||||
return this.prisma.attendanceRegistration.findMany({
|
return this.attendanceRegistrationService.findInInterval(
|
||||||
where: {
|
interval,
|
||||||
event: {
|
eventTypes,
|
||||||
in: eventTypes,
|
);
|
||||||
},
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
dateTime: {
|
|
||||||
gte: startOfDay(interval.start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dateTime: {
|
|
||||||
lt: endOfDay(interval.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
dateTime: 'asc',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findEmployeeById(employeeId: string): Promise<Employee> {
|
public findEmployeeById(employeeId: string): Promise<Employee> {
|
||||||
return this.prisma.employee.findUnique({ where: { id: employeeId } });
|
return this.employeeService.findById(employeeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findEmployeeByPersonnelNumber(
|
public findEmployeeByPersonnelNumber(
|
||||||
personnelNumber: string,
|
personnelNumber: string,
|
||||||
): Promise<Employee> {
|
): Promise<Employee> {
|
||||||
return this.prisma.employee.findFirst({ where: { personnelNumber } });
|
return this.employeeService.findByPersonnelNumber(personnelNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ import { Anomaly, Prisma } from '@prisma/client';
|
||||||
import { Interval, format } from 'date-fns';
|
import { Interval, format } from 'date-fns';
|
||||||
import { toZonedTime } from 'date-fns-tz';
|
import { toZonedTime } from 'date-fns-tz';
|
||||||
|
|
||||||
|
import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum';
|
||||||
import { AnomalyDetector } from './anomaly-detector.class';
|
import { AnomalyDetector } from './anomaly-detector.class';
|
||||||
import { AnomalyTypes } from './anomaly-types.enum';
|
|
||||||
|
|
||||||
export class DriverCertificateDetector extends AnomalyDetector {
|
export class DriverCertificateDetector extends AnomalyDetector {
|
||||||
private readonly logger = new Logger(DriverCertificateDetector.name);
|
private readonly logger = new Logger(DriverCertificateDetector.name);
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { Logger } from '@nestjs/common';
|
||||||
import { Anomaly, Prisma } from '@prisma/client';
|
import { Anomaly, Prisma } from '@prisma/client';
|
||||||
import { Interval, format } from 'date-fns';
|
import { Interval, format } from 'date-fns';
|
||||||
import { toZonedTime } from 'date-fns-tz';
|
import { toZonedTime } from 'date-fns-tz';
|
||||||
|
import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum';
|
||||||
import { AnomalyDetector } from './anomaly-detector.class';
|
import { AnomalyDetector } from './anomaly-detector.class';
|
||||||
import { AnomalyTypes } from './anomaly-types.enum';
|
|
||||||
|
|
||||||
export class DriverCodriverSameDetector extends AnomalyDetector {
|
export class DriverCodriverSameDetector extends AnomalyDetector {
|
||||||
private readonly logger = new Logger(DriverCodriverSameDetector.name);
|
private readonly logger = new Logger(DriverCodriverSameDetector.name);
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import {
|
||||||
isWithinInterval,
|
isWithinInterval,
|
||||||
set,
|
set,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { toZonedTime, fromZonedTime } from 'date-fns-tz';
|
import { fromZonedTime, toZonedTime } from 'date-fns-tz';
|
||||||
|
|
||||||
|
import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum';
|
||||||
import { AnomalyDetector } from './anomaly-detector.class';
|
import { AnomalyDetector } from './anomaly-detector.class';
|
||||||
import { AnomalyTypes } from './anomaly-types.enum';
|
|
||||||
|
|
||||||
export class EmployeesLoggedInDetector extends AnomalyDetector {
|
export class EmployeesLoggedInDetector extends AnomalyDetector {
|
||||||
private readonly logger = new Logger(EmployeesLoggedInDetector.name);
|
private readonly logger = new Logger(EmployeesLoggedInDetector.name);
|
||||||
|
|
@ -200,11 +200,13 @@ export class EmployeesLoggedInDetector extends AnomalyDetector {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!employee.personnelNumber) {
|
if (!employee.personnelNumber) {
|
||||||
this.logger.warn(`Employee has no personnelNumber ${JSON.stringify({
|
this.logger.warn(
|
||||||
id: employee.id,
|
`Employee has no personnelNumber ${JSON.stringify({
|
||||||
surname: employee.surname,
|
id: employee.id,
|
||||||
name: employee.name,
|
surname: employee.surname,
|
||||||
})}`);
|
name: employee.name,
|
||||||
|
})}`,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import {
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { fromZonedTime, toZonedTime } from 'date-fns-tz';
|
import { fromZonedTime, toZonedTime } from 'date-fns-tz';
|
||||||
|
|
||||||
|
import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum';
|
||||||
import { AnomalyDetector } from './anomaly-detector.class';
|
import { AnomalyDetector } from './anomaly-detector.class';
|
||||||
import { AnomalyTypes } from './anomaly-types.enum';
|
|
||||||
|
|
||||||
export class OneTourAtATimeDetector extends AnomalyDetector {
|
export class OneTourAtATimeDetector extends AnomalyDetector {
|
||||||
private readonly logger = new Logger(OneTourAtATimeDetector.name);
|
private readonly logger = new Logger(OneTourAtATimeDetector.name);
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ import { Anomaly, Prisma } from '@prisma/client';
|
||||||
import { Interval, endOfDay, format, startOfDay } from 'date-fns';
|
import { Interval, endOfDay, format, startOfDay } from 'date-fns';
|
||||||
import { toZonedTime } from 'date-fns-tz';
|
import { toZonedTime } from 'date-fns-tz';
|
||||||
|
|
||||||
import { AttendanceRegistrationEvents } from 'src/core/base/attendance-registration-events.enum';
|
import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum';
|
||||||
|
import { AttendanceRegistrationEvents } from 'src/modules/feat-business-objects/entities/attendance-registration/attendance-registration-events.enum';
|
||||||
import { AnomalyDetector } from './anomaly-detector.class';
|
import { AnomalyDetector } from './anomaly-detector.class';
|
||||||
import { AnomalyTypes } from './anomaly-types.enum';
|
|
||||||
|
|
||||||
export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetector {
|
export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetector {
|
||||||
private readonly logger = new Logger(
|
private readonly logger = new Logger(
|
||||||
|
|
@ -83,6 +83,12 @@ export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetec
|
||||||
await this.dataService.findAttendanceRegistrationById(
|
await this.dataService.findAttendanceRegistrationById(
|
||||||
+attendanceRegistrationId,
|
+attendanceRegistrationId,
|
||||||
);
|
);
|
||||||
|
if (!attendanceRegistration) {
|
||||||
|
this.logger.error(
|
||||||
|
`Cannot verify anomaly ${anomaly.id}. AttendanceRegistration ${attendanceRegistrationId} couldn't be found`,
|
||||||
|
);
|
||||||
|
return anomaly;
|
||||||
|
}
|
||||||
const employeeId = employeeIdParts.join('-');
|
const employeeId = employeeIdParts.join('-');
|
||||||
const employee = await this.dataService.findEmployeeById(employeeId);
|
const employee = await this.dataService.findEmployeeById(employeeId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,139 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { PlannedTime, Tour } from '@prisma/client';
|
|
||||||
import { Interval } from 'date-fns';
|
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CarEntityRepository {
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
|
||||||
|
|
||||||
findToursInInterval(interval: Interval): Promise<Tour[]> {
|
|
||||||
return this.prisma.tour.findMany({
|
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gte: new Date(interval.start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: new Date(interval.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
carName: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Storno',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Löschen',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Abgabe',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
check: {
|
|
||||||
not: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
patientId: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Pause
|
|
||||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Feierabend
|
|
||||||
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findPlannedTimeByOrdinanceTypesInInterval(
|
|
||||||
ordinanceType: string,
|
|
||||||
interval: Interval,
|
|
||||||
): Promise<PlannedTime[]> {
|
|
||||||
return this.prisma.plannedTime.findMany({
|
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gte: new Date(interval.start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lte: new Date(interval.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
...(ordinanceType !== 'ALL'
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
department: {
|
|
||||||
contains: ordinanceType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
department: {
|
|
||||||
not: 'Leitstelle',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
department: {
|
|
||||||
not: 'Büro',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
department: {
|
|
||||||
not: 'Fortbildung',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
note: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
userId: {
|
|
||||||
not: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PlannedTime, Tour } from '@prisma/client';
|
import { PlannedTime, Tour } from '@prisma/client';
|
||||||
import { CarEntityRepository } from './car-entity.repository';
|
|
||||||
import { CarEntity } from './car.entity';
|
|
||||||
import { Interval } from 'date-fns';
|
import { Interval } from 'date-fns';
|
||||||
import { generateIntervalForTimeRange } from 'src/utils';
|
import { generateIntervalForTimeRange } from 'src/utils';
|
||||||
|
import { PlannedTimeService } from '../entities/planned-time/planned-time.service';
|
||||||
|
import { TourService } from '../entities/tour/tour.service';
|
||||||
|
import { CarEntity } from './car.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CarEntityService {
|
export class CarEntityService {
|
||||||
constructor(private readonly repository: CarEntityRepository) {}
|
constructor(
|
||||||
|
private readonly plannedTimeService: PlannedTimeService,
|
||||||
|
private readonly tourService: TourService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async getCarsInInterval(
|
async getCarsInInterval(
|
||||||
interval: Interval,
|
interval: Interval,
|
||||||
|
|
@ -41,10 +45,9 @@ export class CarEntityService {
|
||||||
interval: Interval,
|
interval: Interval,
|
||||||
ordinanceType: string,
|
ordinanceType: string,
|
||||||
): Promise<CarEntity[]> {
|
): Promise<CarEntity[]> {
|
||||||
const toursInFullRange =
|
const toursInFullRange = await this.tourService.findInInterval(interval);
|
||||||
await this.repository.findToursInInterval(interval);
|
|
||||||
const plannedTimeByOrdinanceType =
|
const plannedTimeByOrdinanceType =
|
||||||
await this.repository.findPlannedTimeByOrdinanceTypesInInterval(
|
await this.plannedTimeService.findByOrdinanceTypesInInterval(
|
||||||
ordinanceType,
|
ordinanceType,
|
||||||
interval,
|
interval,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
export interface AnomalyCreateDto {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
groupKey: string;
|
||||||
|
groupDescription: string;
|
||||||
|
workedOnBy: string;
|
||||||
|
sleepTimer: number;
|
||||||
|
sleepSince?: Date | string | null;
|
||||||
|
resolvedAt?: Date | string | null;
|
||||||
|
manuallyResolved?: boolean;
|
||||||
|
createdAt?: Date | string;
|
||||||
|
solution?: {
|
||||||
|
create: {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
steps?: {
|
||||||
|
create?: { description: string }[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnomalyUpdateDto {
|
||||||
|
resolvedAt: Date;
|
||||||
|
manuallyResolved: boolean;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export const ANOMALY_SLEEP_TIME_MS = 60 * 60 * 1000;
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
Anomaly,
|
||||||
|
Prisma,
|
||||||
|
SolutionTarget,
|
||||||
|
SolutionTargetStep,
|
||||||
|
} from '@prisma/client';
|
||||||
|
import { subMilliseconds } from 'date-fns';
|
||||||
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
|
import { AnomalyCreateDto } from './anomalies.dto';
|
||||||
|
import { ANOMALY_SLEEP_TIME_MS } from './anomalies.token';
|
||||||
|
import { AnomalyTypes } from './anomaly-types.enum';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AnomalyRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
// Create anomaly if ID is not found, otherwise continue without change
|
||||||
|
async createEvenIfExists(anomaly: AnomalyCreateDto): Promise<Anomaly> {
|
||||||
|
return await this.prisma.anomaly.upsert({
|
||||||
|
where: {
|
||||||
|
id: anomaly.id,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
...anomaly,
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolves the anomaly
|
||||||
|
async resolveAnomaly(anomalyId: string): Promise<Anomaly> {
|
||||||
|
return await this.prisma.anomaly.update({
|
||||||
|
where: {
|
||||||
|
id: anomalyId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
resolvedAt: new Date(),
|
||||||
|
manuallyResolved: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async snoozeAnomaly(anomalyId: string): Promise<Anomaly> {
|
||||||
|
return await this.prisma.anomaly.update({
|
||||||
|
where: {
|
||||||
|
id: anomalyId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
sleepSince: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptAnomaly(anomalyId: string): Promise<Anomaly> {
|
||||||
|
return await this.prisma.anomaly.update({
|
||||||
|
where: {
|
||||||
|
id: anomalyId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
resolvedAt: new Date(),
|
||||||
|
manuallyResolved: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all anomalies which are unresolved
|
||||||
|
async findAllUnsolved(): Promise<Anomaly[]> {
|
||||||
|
return await this.prisma.anomaly.findMany({
|
||||||
|
where: {
|
||||||
|
resolvedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllVisible(
|
||||||
|
skip?: number,
|
||||||
|
take?: number,
|
||||||
|
types: AnomalyTypes[] = [],
|
||||||
|
): Promise<Anomaly[]> {
|
||||||
|
return await this.prisma.anomaly.findMany({
|
||||||
|
...{
|
||||||
|
...(skip ? { skip, take: 10 } : {}),
|
||||||
|
...(take ? { take } : {}),
|
||||||
|
...(take && !skip ? { skip: 0 } : {}),
|
||||||
|
},
|
||||||
|
where: this.visibleAnomalyQuery(types),
|
||||||
|
orderBy: {
|
||||||
|
groupKey: 'asc',
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
solution: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(anomalyId: string): Promise<Anomaly> {
|
||||||
|
return await this.prisma.anomaly.findUnique({
|
||||||
|
where: {
|
||||||
|
id: anomalyId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async countVisibleAnomalies(types: AnomalyTypes[] = []): Promise<number> {
|
||||||
|
return await this.prisma.anomaly.count({
|
||||||
|
where: {
|
||||||
|
...this.visibleAnomalyQuery(types),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findSolutionTargetsByAnomalyId(
|
||||||
|
anomalyId: string,
|
||||||
|
): Promise<SolutionTarget[]> {
|
||||||
|
return await this.prisma.solutionTarget.findMany({
|
||||||
|
where: {
|
||||||
|
anomalyId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findSolutionTargetStepsBySolutionTargetId(
|
||||||
|
solutionTargetId: string,
|
||||||
|
): Promise<SolutionTargetStep[]> {
|
||||||
|
return await this.prisma.solutionTargetStep.findMany({
|
||||||
|
where: {
|
||||||
|
solutionTargetId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by `resolvedAt` and `sleepSince`
|
||||||
|
private visibleAnomalyQuery(types: AnomalyTypes[]): Prisma.AnomalyWhereInput {
|
||||||
|
return {
|
||||||
|
...(types.length ? { type: { in: types } } : {}),
|
||||||
|
resolvedAt: null,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
sleepSince: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sleepSince: {
|
||||||
|
lt: subMilliseconds(new Date(), ANOMALY_SLEEP_TIME_MS),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Anomaly, SolutionTarget, SolutionTargetStep } from '@prisma/client';
|
||||||
|
import { AnomalyCreateDto } from './anomalies.dto';
|
||||||
|
import { AnomalyTypes } from './anomaly-types.enum';
|
||||||
|
import { AnomalyRepository } from './anomaly.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AnomalyService {
|
||||||
|
constructor(private readonly repository: AnomalyRepository) {}
|
||||||
|
|
||||||
|
async createEvenIfExists(anomaly: AnomalyCreateDto): Promise<Anomaly> {
|
||||||
|
return this.repository.createEvenIfExists(anomaly);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveAnomaly(anomalyId: string): Promise<Anomaly> {
|
||||||
|
return this.repository.resolveAnomaly(anomalyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async snoozeAnomaly(anomalyId: string): Promise<Anomaly> {
|
||||||
|
return this.repository.snoozeAnomaly(anomalyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptAnomaly(anomalyId: string): Promise<Anomaly> {
|
||||||
|
return this.repository.acceptAnomaly(anomalyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllUnsolved(): Promise<Anomaly[]> {
|
||||||
|
return this.repository.findAllUnsolved();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllVisible(
|
||||||
|
skip?: number,
|
||||||
|
take?: number,
|
||||||
|
types: AnomalyTypes[] = [],
|
||||||
|
): Promise<Anomaly[]> {
|
||||||
|
return this.repository.findAllVisible(skip, take, types);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(anomalyId: string): Promise<Anomaly> {
|
||||||
|
return this.repository.findById(anomalyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async countVisibleAnomalies(types: AnomalyTypes[] = []): Promise<number> {
|
||||||
|
return this.repository.countVisibleAnomalies(types);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findSolutionTargetsByAnomalyId(
|
||||||
|
anomalyId: string,
|
||||||
|
): Promise<SolutionTarget[]> {
|
||||||
|
return this.repository.findSolutionTargetsByAnomalyId(anomalyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findSolutionTargetStepsBySolutionTargetId(
|
||||||
|
solutionTargetId: string,
|
||||||
|
): Promise<SolutionTargetStep[]> {
|
||||||
|
return this.repository.findSolutionTargetStepsBySolutionTargetId(
|
||||||
|
solutionTargetId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AttendanceRegistration } from '@prisma/client';
|
||||||
|
import { Interval } from 'date-fns';
|
||||||
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
|
import { AttendanceRegistrationEvents } from './attendance-registration-events.enum';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AttendanceRegistrationRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findById(
|
||||||
|
attendanceRegistrationId: number,
|
||||||
|
): Promise<AttendanceRegistration> {
|
||||||
|
return await this.prisma.attendanceRegistration.findUnique({
|
||||||
|
where: {
|
||||||
|
attendanceRegistrationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findInInterval(
|
||||||
|
interval: Interval,
|
||||||
|
eventTypes: AttendanceRegistrationEvents[] = [
|
||||||
|
AttendanceRegistrationEvents.ClockedIn,
|
||||||
|
AttendanceRegistrationEvents.ClockedOut,
|
||||||
|
],
|
||||||
|
): Promise<AttendanceRegistration[]> {
|
||||||
|
return await this.prisma.attendanceRegistration.findMany({
|
||||||
|
where: {
|
||||||
|
event: {
|
||||||
|
in: eventTypes,
|
||||||
|
},
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
dateTime: {
|
||||||
|
gte: new Date(interval.start),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dateTime: {
|
||||||
|
lt: new Date(interval.end),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
dateTime: 'asc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByEmployeeInInterval(
|
||||||
|
employeeId: number,
|
||||||
|
timeRange: Interval,
|
||||||
|
eventTypes: AttendanceRegistrationEvents[] = [
|
||||||
|
AttendanceRegistrationEvents.ClockedIn,
|
||||||
|
AttendanceRegistrationEvents.ClockedOut,
|
||||||
|
],
|
||||||
|
): Promise<AttendanceRegistration[]> {
|
||||||
|
return await this.prisma.attendanceRegistration.findMany({
|
||||||
|
where: {
|
||||||
|
employeeId,
|
||||||
|
event: {
|
||||||
|
in: eventTypes,
|
||||||
|
},
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
dateTime: {
|
||||||
|
gte: new Date(timeRange.start),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dateTime: {
|
||||||
|
lt: new Date(timeRange.end),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
dateTime: 'asc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AttendanceRegistrationRepository } from './attendance-registration.repository';
|
||||||
|
import { AttendanceRegistration } from '@prisma/client';
|
||||||
|
import { Interval } from 'date-fns';
|
||||||
|
import { AttendanceRegistrationEvents } from './attendance-registration-events.enum';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AttendanceRegistrationService {
|
||||||
|
constructor(private readonly repository: AttendanceRegistrationRepository) {}
|
||||||
|
|
||||||
|
async findById(
|
||||||
|
attendanceRegistrationId: number,
|
||||||
|
): Promise<AttendanceRegistration> {
|
||||||
|
return this.repository.findById(attendanceRegistrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findInInterval(
|
||||||
|
interval: Interval,
|
||||||
|
eventTypes: AttendanceRegistrationEvents[],
|
||||||
|
): Promise<AttendanceRegistration[]> {
|
||||||
|
return this.repository.findInInterval(interval, eventTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByEmployeeInInterval(
|
||||||
|
employeeId: number,
|
||||||
|
interval: Interval,
|
||||||
|
eventTypes: AttendanceRegistrationEvents[],
|
||||||
|
): Promise<AttendanceRegistration[]> {
|
||||||
|
return this.repository.findByEmployeeInInterval(
|
||||||
|
employeeId,
|
||||||
|
interval,
|
||||||
|
eventTypes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Employee } from '@prisma/client';
|
||||||
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EmployeeRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findById(employeeId: string): Promise<Employee> {
|
||||||
|
return await this.prisma.employee.findUnique({ where: { id: employeeId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByPersonnelNumber(personnelNumber: string): Promise<Employee> {
|
||||||
|
return await this.prisma.employee.findFirst({ where: { personnelNumber } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EmployeeRepository } from './employee.repository';
|
||||||
|
import { Employee } from '@prisma/client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EmployeeService {
|
||||||
|
constructor(private readonly repository: EmployeeRepository) {}
|
||||||
|
|
||||||
|
async findById(employeeId: string): Promise<Employee> {
|
||||||
|
return this.repository.findById(employeeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByPersonnelNumber(personnelNumber: string): Promise<Employee> {
|
||||||
|
return this.repository.findByPersonnelNumber(personnelNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Patient } from '@prisma/client';
|
||||||
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PatientRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findById(patientId: string): Promise<Patient> {
|
||||||
|
return this.prisma.patient.findUnique({ where: { id: patientId } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Patient } from '@prisma/client';
|
||||||
|
import { PatientRepository } from './patient.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PatientService {
|
||||||
|
constructor(private readonly repository: PatientRepository) {}
|
||||||
|
|
||||||
|
async findById(patientId: string): Promise<Patient> {
|
||||||
|
return this.repository.findById(patientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PlannedTime } from '@prisma/client';
|
||||||
|
import { Interval } from 'date-fns';
|
||||||
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PlannedTimeRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findByPersonnelNumberInInterval(
|
||||||
|
personnelNumber: string,
|
||||||
|
interval: Interval,
|
||||||
|
): Promise<PlannedTime[]> {
|
||||||
|
return await this.prisma.plannedTime.findMany({
|
||||||
|
where: {
|
||||||
|
userId: personnelNumber,
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
gte: new Date(interval.start),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
lt: new Date(interval.end),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByOrdinanceTypesInInterval(
|
||||||
|
ordinanceType: string | undefined,
|
||||||
|
interval: Interval,
|
||||||
|
): Promise<PlannedTime[]> {
|
||||||
|
const allOrdinanceTypes =
|
||||||
|
ordinanceType == undefined || ordinanceType === 'ALL' ? true : false;
|
||||||
|
return await this.prisma.plannedTime.findMany({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
gte: new Date(interval.start),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
lte: new Date(interval.end),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
...(allOrdinanceTypes
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
department: {
|
||||||
|
contains: ordinanceType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
department: {
|
||||||
|
not: 'Leitstelle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
department: {
|
||||||
|
not: 'Büro',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
department: {
|
||||||
|
not: 'Fortbildung',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
note: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: {
|
||||||
|
not: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PlannedTime } from '@prisma/client';
|
||||||
|
import { Interval } from 'date-fns';
|
||||||
|
import { PlannedTimeRepository } from './planned-time.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PlannedTimeService {
|
||||||
|
constructor(private readonly repository: PlannedTimeRepository) {}
|
||||||
|
|
||||||
|
async findByPersonnelNumberInInterval(
|
||||||
|
personnelNumber: string,
|
||||||
|
interval: Interval,
|
||||||
|
): Promise<PlannedTime[]> {
|
||||||
|
return this.repository.findByPersonnelNumberInInterval(
|
||||||
|
personnelNumber,
|
||||||
|
interval,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByOrdinanceTypesInInterval(
|
||||||
|
ordinanceType: string | undefined,
|
||||||
|
interval: Interval,
|
||||||
|
): Promise<PlannedTime[]> {
|
||||||
|
return this.repository.findByOrdinanceTypesInInterval(
|
||||||
|
ordinanceType,
|
||||||
|
interval,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AvicennaTenant, CostType, Tariff, TariffGroup } from '@prisma/client';
|
||||||
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TariffRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findTenant(
|
||||||
|
healthInsurance: string,
|
||||||
|
ordinanceType: string,
|
||||||
|
): Promise<AvicennaTenant> {
|
||||||
|
return await this.prisma.avicennaTenant.findFirst({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
name: healthInsurance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
contains: healthInsurance,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tariffGroups: {
|
||||||
|
some: {
|
||||||
|
ordinanceType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByTariffGroupDescription(
|
||||||
|
description: string,
|
||||||
|
ordinanceType: string,
|
||||||
|
dateValid: Date,
|
||||||
|
): Promise<Tariff & { tariffGroup: TariffGroup; costTypes: CostType[] }> {
|
||||||
|
return await this.prisma.tariff.findFirst({
|
||||||
|
where: {
|
||||||
|
tariffGroup: {
|
||||||
|
description: {
|
||||||
|
contains: description,
|
||||||
|
},
|
||||||
|
ordinanceType,
|
||||||
|
},
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{ validTo: null },
|
||||||
|
{
|
||||||
|
validFrom: {
|
||||||
|
lte: dateValid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{ validTo: { gte: dateValid } },
|
||||||
|
{ validFrom: { lte: dateValid } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
tariffGroup: true,
|
||||||
|
costTypes: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByTenantId(
|
||||||
|
tenantId: string,
|
||||||
|
ordinanceType: string,
|
||||||
|
dateValid: Date,
|
||||||
|
): Promise<Tariff & { tariffGroup: TariffGroup; costTypes: CostType[] }> {
|
||||||
|
return await this.prisma.tariff.findFirst({
|
||||||
|
where: {
|
||||||
|
tariffGroup: {
|
||||||
|
tenants: {
|
||||||
|
some: {
|
||||||
|
id: tenantId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ordinanceType,
|
||||||
|
},
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{ validTo: null },
|
||||||
|
{
|
||||||
|
validFrom: {
|
||||||
|
lte: dateValid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{ validTo: { gte: dateValid } },
|
||||||
|
{ validFrom: { lte: dateValid } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
tariffGroup: true,
|
||||||
|
costTypes: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AvicennaTenant, CostType, Tariff, TariffGroup } from '@prisma/client';
|
||||||
|
import { TariffRepository } from './tariff.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TariffService {
|
||||||
|
constructor(private readonly repository: TariffRepository) {}
|
||||||
|
|
||||||
|
async findTenant(
|
||||||
|
healthInsurance: string,
|
||||||
|
ordinanceType: string,
|
||||||
|
): Promise<AvicennaTenant> {
|
||||||
|
return this.repository.findTenant(healthInsurance, ordinanceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByTariffGroupDescription(
|
||||||
|
description: string,
|
||||||
|
ordinanceType: string,
|
||||||
|
dateValid: Date,
|
||||||
|
): Promise<Tariff & { tariffGroup: TariffGroup; costTypes: CostType[] }> {
|
||||||
|
return this.repository.findByTariffGroupDescription(
|
||||||
|
description,
|
||||||
|
ordinanceType,
|
||||||
|
dateValid,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByTenantId(
|
||||||
|
tenantId: string,
|
||||||
|
ordinanceType: string,
|
||||||
|
dateValid: Date,
|
||||||
|
): Promise<Tariff & { tariffGroup: TariffGroup; costTypes: CostType[] }> {
|
||||||
|
return this.repository.findByTenantId(tenantId, ordinanceType, dateValid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,315 @@
|
||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApprovalState,
|
||||||
|
Prisma,
|
||||||
|
Ticket,
|
||||||
|
TicketStateHistory,
|
||||||
|
TicketValidationState,
|
||||||
|
Tour,
|
||||||
|
} from '@prisma/client';
|
||||||
|
import { ExpressionBuilder, Kysely } from 'kysely';
|
||||||
|
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||||
|
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
|
||||||
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
|
import { DB } from 'src/core/database/types';
|
||||||
|
import { createPrismaQueryFromTicketFilters } from './ticket-filter';
|
||||||
|
import { TicketFilterDto, UpdateDocumentInfoDto } from './tickets.dto';
|
||||||
|
|
||||||
|
function withTours(eb: ExpressionBuilder<DB, 'Ticket'>) {
|
||||||
|
return jsonArrayFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom('Tour')
|
||||||
|
.selectAll('Tour')
|
||||||
|
.whereRef('Tour.ticketId', '=', 'Ticket.id')
|
||||||
|
.orderBy('Tour.operationId'),
|
||||||
|
).as('tours');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TicketRepository {
|
||||||
|
private logger = new Logger(TicketRepository.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findById(ticketId: string): Promise<Ticket> {
|
||||||
|
return await this.prisma.ticket.findUnique({
|
||||||
|
where: { id: ticketId },
|
||||||
|
include: { tours: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllCreated(): Promise<(Ticket & { tours: Tour[] })[]> {
|
||||||
|
return await this.prisma.ticket.findMany({
|
||||||
|
where: {
|
||||||
|
state: {
|
||||||
|
none: {
|
||||||
|
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
tours: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllClosed(): Promise<Ticket[]> {
|
||||||
|
return await this.prisma.ticket.findMany({
|
||||||
|
where: {
|
||||||
|
tours: {
|
||||||
|
every: {
|
||||||
|
check: {
|
||||||
|
not: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<(Ticket & { tours: Tour[] })[]> {
|
||||||
|
return await this.prisma.ticket.findMany({
|
||||||
|
where: {
|
||||||
|
state: {
|
||||||
|
none: {
|
||||||
|
state: TicketValidationState.ARCHIVED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
tours: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByState(
|
||||||
|
state: TicketValidationState,
|
||||||
|
filter: TicketFilterDto,
|
||||||
|
take?: number,
|
||||||
|
skip?: number,
|
||||||
|
) {
|
||||||
|
const query: Prisma.TicketFindManyArgs = {
|
||||||
|
...{
|
||||||
|
...(skip ? { skip, take: 10 } : {}),
|
||||||
|
...(take ? { take } : {}),
|
||||||
|
...(take && !skip ? { skip: 0 } : {}),
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
AND: createPrismaQueryFromTicketFilters(filter),
|
||||||
|
currentState: state,
|
||||||
|
tours: {
|
||||||
|
some: {
|
||||||
|
check: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const [tickets, count] = await this.prisma.$transaction([
|
||||||
|
this.prisma.ticket.findMany({ ...query }),
|
||||||
|
this.prisma.ticket.count({ where: query.where }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
tickets,
|
||||||
|
total: count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findStateHistoryByTicketId(
|
||||||
|
ticketId: string,
|
||||||
|
): Promise<{ state: TicketValidationState }[]> {
|
||||||
|
return await this.prisma.ticketStateHistory.findMany({
|
||||||
|
where: {
|
||||||
|
ticketId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
state: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findIdByTourId(tourId: string): Promise<{ id: string }> {
|
||||||
|
return await this.prisma.ticket.findFirst({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
tours: {
|
||||||
|
some: {
|
||||||
|
id: tourId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query: string): Promise<(Ticket & { tours: Tour[] })[]> {
|
||||||
|
return this.prisma.ticket.findMany({
|
||||||
|
where: {
|
||||||
|
state: {
|
||||||
|
none: {
|
||||||
|
state: TicketValidationState.ARCHIVED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tours: {
|
||||||
|
some: {
|
||||||
|
OR: [
|
||||||
|
...query.split(' ').flatMap((word) => [
|
||||||
|
{
|
||||||
|
patientName: {
|
||||||
|
contains: word,
|
||||||
|
mode: Prisma.QueryMode.insensitive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patientSurname: {
|
||||||
|
contains: word,
|
||||||
|
mode: Prisma.QueryMode.insensitive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
operationId: {
|
||||||
|
contains: word,
|
||||||
|
mode: Prisma.QueryMode.insensitive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
take: 5,
|
||||||
|
include: {
|
||||||
|
tours: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTicket(tours: Tour[]): Promise<Ticket> {
|
||||||
|
this.logger.log(`Creating Ticket for ${tours.length} tours`);
|
||||||
|
try {
|
||||||
|
return await this.db.transaction().execute(async (trx) => {
|
||||||
|
const ticket = await trx
|
||||||
|
.insertInto('Ticket')
|
||||||
|
.values({
|
||||||
|
currentState: TicketValidationState.CREATED,
|
||||||
|
})
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.insertInto('TicketStateHistory')
|
||||||
|
.values({
|
||||||
|
state: TicketValidationState.CREATED,
|
||||||
|
ticketId: ticket.id,
|
||||||
|
})
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.updateTable('Tour')
|
||||||
|
.set({
|
||||||
|
ticketId: ticket.id,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
'id',
|
||||||
|
'in',
|
||||||
|
tours.map(({ id }) => id),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return ticket as Ticket;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addToursToTicket(ticketId: string, tours: Tour[]): Promise<Ticket> {
|
||||||
|
await this.db
|
||||||
|
.updateTable('Tour')
|
||||||
|
.set({
|
||||||
|
ticketId: ticketId,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
'id',
|
||||||
|
'in',
|
||||||
|
tours.map(({ id }) => id),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return (await this.db
|
||||||
|
.selectFrom('Ticket')
|
||||||
|
.where('id', '=', ticketId)
|
||||||
|
.selectAll()
|
||||||
|
.executeTakeFirst()) as Ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setActionablesOnTicket(
|
||||||
|
ticketId: string,
|
||||||
|
errors: string[],
|
||||||
|
notes: string[],
|
||||||
|
): Promise<Ticket> {
|
||||||
|
return (await this.db
|
||||||
|
.updateTable('Ticket')
|
||||||
|
.set({ errors, notes })
|
||||||
|
.where('id', '=', ticketId)
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirst()) as Ticket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setStateOfTicket(
|
||||||
|
ticketId: string,
|
||||||
|
state: TicketValidationState,
|
||||||
|
): Promise<TicketStateHistory> {
|
||||||
|
await this.db
|
||||||
|
.updateTable('Ticket')
|
||||||
|
.set({
|
||||||
|
currentState: state,
|
||||||
|
})
|
||||||
|
.where('Ticket.id', '=', ticketId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return await this.db
|
||||||
|
.insertInto('TicketStateHistory')
|
||||||
|
.values({ state, ticketId })
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTicket(
|
||||||
|
ticketId: string,
|
||||||
|
data: {
|
||||||
|
state?: TicketValidationState;
|
||||||
|
errors?: string[];
|
||||||
|
notes?: string[];
|
||||||
|
isUrgent?: boolean;
|
||||||
|
urgency?: Date;
|
||||||
|
approvalState?: ApprovalState;
|
||||||
|
documentInfo?: UpdateDocumentInfoDto;
|
||||||
|
approvalPath?: string;
|
||||||
|
},
|
||||||
|
): Promise<Ticket & { tours: Tour[] }> {
|
||||||
|
const { state, ...ticketData } = data;
|
||||||
|
|
||||||
|
await this.db
|
||||||
|
.updateTable('Ticket')
|
||||||
|
.set({ ...ticketData })
|
||||||
|
.where('id', '=', ticketId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
await this.setStateOfTicket(ticketId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await this.db
|
||||||
|
.selectFrom('Ticket')
|
||||||
|
.where('id', '=', ticketId)
|
||||||
|
.selectAll('Ticket')
|
||||||
|
.select((eb) => withTours(eb))
|
||||||
|
.executeTakeFirst()) as Ticket & { tours: Tour[] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApprovalState,
|
||||||
|
Ticket,
|
||||||
|
TicketStateHistory,
|
||||||
|
TicketValidationState,
|
||||||
|
Tour,
|
||||||
|
} from '@prisma/client';
|
||||||
|
import {
|
||||||
|
TicketFilterDto,
|
||||||
|
UpdateDocumentInfoDto,
|
||||||
|
} from 'src/modules/feat-business-objects/entities/ticket/tickets.dto';
|
||||||
|
import { TicketRepository } from './ticket.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TicketService {
|
||||||
|
constructor(private readonly repository: TicketRepository) {}
|
||||||
|
|
||||||
|
async findById(ticketId: string): Promise<Ticket> {
|
||||||
|
return this.repository.findById(ticketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllCreated(): Promise<(Ticket & { tours: Tour[] })[]> {
|
||||||
|
return this.repository.findAllCreated();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllClosed(): Promise<Ticket[]> {
|
||||||
|
return this.repository.findAllClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(): Promise<(Ticket & { tours: Tour[] })[]> {
|
||||||
|
return this.repository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByState(
|
||||||
|
state: TicketValidationState,
|
||||||
|
filter: TicketFilterDto,
|
||||||
|
take?: number,
|
||||||
|
skip?: number,
|
||||||
|
) {
|
||||||
|
return this.repository.findByState(state, filter, take, skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findStateHistoryByTicketId(
|
||||||
|
ticketId: string,
|
||||||
|
): Promise<{ state: TicketValidationState }[]> {
|
||||||
|
return this.repository.findStateHistoryByTicketId(ticketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findIdByTourId(tourId: string): Promise<{ id: string }> {
|
||||||
|
return this.repository.findIdByTourId(tourId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query: string): Promise<(Ticket & { tours: Tour[] })[]> {
|
||||||
|
return this.repository.search(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTicket(tours: Tour[]): Promise<Ticket> {
|
||||||
|
return this.repository.createTicket(tours);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addToursToTicket(ticketId: string, tours: Tour[]): Promise<Ticket> {
|
||||||
|
return this.repository.addToursToTicket(ticketId, tours);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setActionablesOnTicket(
|
||||||
|
ticketId: string,
|
||||||
|
errors: string[],
|
||||||
|
notes: string[],
|
||||||
|
): Promise<Ticket> {
|
||||||
|
return this.repository.setActionablesOnTicket(ticketId, errors, notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setStateOfTicket(
|
||||||
|
ticketId: string,
|
||||||
|
state: TicketValidationState,
|
||||||
|
): Promise<TicketStateHistory> {
|
||||||
|
return this.repository.setStateOfTicket(ticketId, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTicket(
|
||||||
|
ticketId: string,
|
||||||
|
data: {
|
||||||
|
state?: TicketValidationState;
|
||||||
|
errors?: string[];
|
||||||
|
notes?: string[];
|
||||||
|
isUrgent?: boolean;
|
||||||
|
urgency?: Date;
|
||||||
|
approvalState?: ApprovalState;
|
||||||
|
documentInfo?: UpdateDocumentInfoDto;
|
||||||
|
approvalPath?: string;
|
||||||
|
},
|
||||||
|
): Promise<Ticket & { tours: Tour[] }> {
|
||||||
|
return this.repository.updateTicket(ticketId, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ApprovalState } from "@prisma/client";
|
||||||
|
|
||||||
|
export interface UpdateApprovalInfoDto {
|
||||||
|
isNeeded?: boolean;
|
||||||
|
whoRequested?: string;
|
||||||
|
answeredFrom?: string;
|
||||||
|
location?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDocumentInfoDto {
|
||||||
|
approval: UpdateApprovalInfoDto;
|
||||||
|
tdLocation?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TicketFilterDto {
|
||||||
|
errors?: string[];
|
||||||
|
approvalStates?: ApprovalState[];
|
||||||
|
documentInfo?: ('approval' | 'transport_document')[]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Prisma, Tour } from '@prisma/client';
|
||||||
|
import { endOfDay, Interval, startOfDay } from 'date-fns';
|
||||||
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
|
import { AccountingState } from 'src/modules/app-accounting/accounting-state';
|
||||||
|
import {
|
||||||
|
createPrismaQueryFromAccountingFilters,
|
||||||
|
TourAccountingFilterDto,
|
||||||
|
} from './tours.filter';
|
||||||
|
|
||||||
|
function onlyRealTours(): Prisma.TourWhereInput[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
carName: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
carName: {
|
||||||
|
not: 'Storno',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carName: {
|
||||||
|
not: 'Löschen',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
carName: {
|
||||||
|
not: 'Abgabe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
check: {
|
||||||
|
not: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
patientId: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
patientId: {
|
||||||
|
// Pause
|
||||||
|
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
patientId: {
|
||||||
|
// Feierabend
|
||||||
|
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TourRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findByOperationId(operationId: string): Promise<Tour> {
|
||||||
|
return await this.prisma.tour.findFirst({
|
||||||
|
where: { operationId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findInInterval(
|
||||||
|
interval: Interval,
|
||||||
|
ordinanceTypes: string[] = [],
|
||||||
|
): Promise<Tour[]> {
|
||||||
|
return await this.prisma.tour.findMany({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
gte: new Date(interval.start),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
lt: new Date(interval.end),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
...(ordinanceTypes.length
|
||||||
|
? ordinanceTypes.map((ot) => ({
|
||||||
|
ordinanceType: { contains: ot },
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...onlyRealTours(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
startDate: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public findInIntervalWithEmployee(
|
||||||
|
{ start, end }: Interval,
|
||||||
|
employeeId: string,
|
||||||
|
ordinanceTypes: string[] = [],
|
||||||
|
): Promise<Tour[]> {
|
||||||
|
return this.prisma.tour.findMany({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
driverId: employeeId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codriverId: employeeId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
gte: new Date(start),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
lt: new Date(end),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
OR: [
|
||||||
|
...(ordinanceTypes.length
|
||||||
|
? ordinanceTypes.map((ot) => ({
|
||||||
|
ordinanceType: { contains: ot },
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...onlyRealTours(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByTicketId(ticketId: string): Promise<Tour[]> {
|
||||||
|
return await this.prisma.tour.findMany({
|
||||||
|
where: { ticketId },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Ticket System ------------------- //
|
||||||
|
async findFirstRecurring(tour: Tour): Promise<Tour | null> {
|
||||||
|
const institutionStreet =
|
||||||
|
tour.direction === 'Hinfahrt' ? tour.targetStreet : tour.startStreet;
|
||||||
|
return await this.prisma.tour.findFirst({
|
||||||
|
where: {
|
||||||
|
type: 'Serie',
|
||||||
|
category: tour.category,
|
||||||
|
ordinanceType: tour.ordinanceType,
|
||||||
|
patientId: tour.patientId,
|
||||||
|
patientName: tour.patientName,
|
||||||
|
patientSurname: tour.patientSurname,
|
||||||
|
patientStreet: tour.patientStreet,
|
||||||
|
patientCity: tour.patientCity,
|
||||||
|
patientZip: tour.patientZip,
|
||||||
|
OR: [
|
||||||
|
{ targetStreet: institutionStreet },
|
||||||
|
{ startStreet: institutionStreet },
|
||||||
|
],
|
||||||
|
rangeEndDate: {
|
||||||
|
gte: tour.startDate,
|
||||||
|
},
|
||||||
|
ticketId: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
startDate: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOnSameDayWithOppositeDirection(tour: Tour): Promise<Tour | null> {
|
||||||
|
return await this.prisma.tour.findFirst({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
gt: startOfDay(new Date(tour.startDate)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
lt: endOfDay(new Date(tour.startDate)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startStreet: tour.targetStreet,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targetStreet: tour.startStreet,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
direction: tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findWithTicket(interval: Interval): Promise<Tour[]> {
|
||||||
|
return await this.prisma.tour.findMany({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
ticketId: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
gte: new Date(interval.start),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
lt: new Date(interval.end),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeTourFromTicket(tourId: string): Promise<Tour> {
|
||||||
|
return await this.prisma.tour.update({
|
||||||
|
where: {
|
||||||
|
id: tourId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
Ticket: {
|
||||||
|
disconnect: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------- Accounting --------------------- //
|
||||||
|
async findByAccountingState(
|
||||||
|
state: AccountingState,
|
||||||
|
filter: TourAccountingFilterDto,
|
||||||
|
take?: number,
|
||||||
|
skip?: number,
|
||||||
|
): Promise<{ tours: Tour[]; total: number }> {
|
||||||
|
const checkValue = this.getCheckValueForAccountingState(state);
|
||||||
|
const query: Prisma.TourFindManyArgs = {
|
||||||
|
...{
|
||||||
|
...(skip ? { skip, take: 10 } : {}),
|
||||||
|
...(take ? { take } : {}),
|
||||||
|
...(take && !skip ? { skip: 0 } : {}),
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
check: checkValue,
|
||||||
|
...(checkValue === 2 && !filter?.deviation
|
||||||
|
? { revenueDeviation: { not: 0 } }
|
||||||
|
: {}),
|
||||||
|
empty: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
ticketId: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
AND: [
|
||||||
|
...createPrismaQueryFromAccountingFilters(filter),
|
||||||
|
...onlyRealTours(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const [tours, count] = await this.prisma.$transaction([
|
||||||
|
this.prisma.tour.findMany({ ...query }),
|
||||||
|
this.prisma.tour.count({ where: query.where }),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
tours,
|
||||||
|
total: count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async calculateAccountingStateMeta(state: AccountingState): Promise<{
|
||||||
|
count: number;
|
||||||
|
revenueSum?: number;
|
||||||
|
positiveRevenueDeviation?: number;
|
||||||
|
negativeRevenueDeviation?: number;
|
||||||
|
}> {
|
||||||
|
const checkValue = this.getCheckValueForAccountingState(state);
|
||||||
|
const ticketWithCurrentStateAndTours: Prisma.TourWhereInput = {
|
||||||
|
check: checkValue,
|
||||||
|
...(checkValue === 2 ? { revenueDeviation: { not: 0 } } : {}),
|
||||||
|
empty: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
ticketId: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
AND: [...onlyRealTours()],
|
||||||
|
};
|
||||||
|
|
||||||
|
const [
|
||||||
|
count,
|
||||||
|
revenueSum,
|
||||||
|
positiveRevenueDeviation,
|
||||||
|
negativeRevenueDeviation,
|
||||||
|
] = await this.prisma.$transaction([
|
||||||
|
this.prisma.tour.count({
|
||||||
|
where: { ...ticketWithCurrentStateAndTours },
|
||||||
|
}),
|
||||||
|
this.prisma.tour.aggregate({
|
||||||
|
_sum: {
|
||||||
|
revenue: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
...ticketWithCurrentStateAndTours,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.prisma.tour.aggregate({
|
||||||
|
_sum: {
|
||||||
|
revenueDeviation: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
...ticketWithCurrentStateAndTours,
|
||||||
|
revenueDeviation: {
|
||||||
|
gt: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
this.prisma.tour.aggregate({
|
||||||
|
_sum: {
|
||||||
|
revenueDeviation: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
...ticketWithCurrentStateAndTours,
|
||||||
|
revenueDeviation: {
|
||||||
|
lt: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
count,
|
||||||
|
revenueSum: revenueSum._sum.revenue ?? 0,
|
||||||
|
positiveRevenueDeviation:
|
||||||
|
positiveRevenueDeviation._sum.revenueDeviation ?? 0,
|
||||||
|
negativeRevenueDeviation:
|
||||||
|
negativeRevenueDeviation._sum.revenueDeviation ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCheckValueForAccountingState(state: AccountingState): number {
|
||||||
|
switch (state) {
|
||||||
|
case AccountingState.Billable:
|
||||||
|
return 0;
|
||||||
|
case AccountingState.PreparedForBilling:
|
||||||
|
return 1;
|
||||||
|
case AccountingState.Billed:
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Tour } from '@prisma/client';
|
||||||
|
import { Interval } from 'date-fns';
|
||||||
|
import { TourRepository } from './tour.repository';
|
||||||
|
import { AccountingState } from 'src/modules/app-accounting/accounting-state';
|
||||||
|
import { TourAccountingFilterDto } from './tours.filter';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TourService {
|
||||||
|
constructor(private readonly repository: TourRepository) {}
|
||||||
|
|
||||||
|
async findInInterval(
|
||||||
|
interval: Interval,
|
||||||
|
ordinanceTypes?: string[],
|
||||||
|
): Promise<Tour[]> {
|
||||||
|
return this.repository.findInInterval(interval, ordinanceTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public findInIntervalWithEmployee(
|
||||||
|
interval: Interval,
|
||||||
|
employeeId: string,
|
||||||
|
ordinanceTypes: string[],
|
||||||
|
): Promise<Tour[]> {
|
||||||
|
return this.repository.findInIntervalWithEmployee(
|
||||||
|
interval,
|
||||||
|
employeeId,
|
||||||
|
ordinanceTypes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByOperationId(operationId: string): Promise<Tour> {
|
||||||
|
return this.repository.findByOperationId(operationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByTicketId(ticketId: string): Promise<Tour[]> {
|
||||||
|
return this.repository.findByTicketId(ticketId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findFirstRecurring(tour: Tour): Promise<Tour | null> {
|
||||||
|
return this.repository.findFirstRecurring(tour);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOnSameDayWithOppositeDirection(tour: Tour): Promise<Tour | null> {
|
||||||
|
return this.repository.findOnSameDayWithOppositeDirection(tour);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findToursWithTicket(interval: Interval): Promise<Tour[]> {
|
||||||
|
return this.repository.findWithTicket(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeTourFromTicket(tourId: string): Promise<Tour> {
|
||||||
|
return this.repository.removeTourFromTicket(tourId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByAccountingState(
|
||||||
|
state: AccountingState,
|
||||||
|
filter: TourAccountingFilterDto,
|
||||||
|
take?: number,
|
||||||
|
skip?: number,
|
||||||
|
): Promise<{ tours: Tour[]; total: number }> {
|
||||||
|
return this.repository.findByAccountingState(state, filter, take, skip);
|
||||||
|
}
|
||||||
|
|
||||||
|
async calculateAccountingStateMeta(state: AccountingState): Promise<{
|
||||||
|
count: number;
|
||||||
|
revenueSum?: number;
|
||||||
|
positiveRevenueDeviation?: number;
|
||||||
|
negativeRevenueDeviation?: number;
|
||||||
|
}> {
|
||||||
|
return this.repository.calculateAccountingStateMeta(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { Prisma, TicketValidationState } from '@prisma/client';
|
import { Prisma, TicketValidationState } from '@prisma/client';
|
||||||
import { Interval } from 'date-fns';
|
import { Interval } from 'date-fns';
|
||||||
|
|
||||||
export interface AccountingFilter {
|
export interface TourAccountingFilterDto {
|
||||||
deviation?: 'pos' | 'neg' | 'both'[];
|
deviation?: 'pos' | 'neg' | 'both'[];
|
||||||
missing?: 'information' | 'documents'[];
|
missing?: 'information' | 'documents'[];
|
||||||
dateRange?: Interval;
|
dateRange?: Interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPrismaQueryFromAccountingFilters(
|
export function createPrismaQueryFromAccountingFilters(
|
||||||
filter: AccountingFilter,
|
filter: TourAccountingFilterDto,
|
||||||
): Prisma.TourWhereInput[] {
|
): Prisma.TourWhereInput[] {
|
||||||
const conditions: Prisma.TourWhereInput[] = [];
|
const conditions: Prisma.TourWhereInput[] = [];
|
||||||
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { WorkTime } from '@prisma/client';
|
||||||
|
import { Interval } from 'date-fns';
|
||||||
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WorkTimeRepository {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async findByPersonnelNumberInInterval(
|
||||||
|
personnelNumber: string,
|
||||||
|
interval: Interval,
|
||||||
|
): Promise<WorkTime> {
|
||||||
|
return await this.prisma.workTime.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: personnelNumber,
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
gte: new Date(interval.start),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startDate: {
|
||||||
|
lte: new Date(interval.end),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { WorkTime } from '@prisma/client';
|
||||||
|
import { Interval } from 'date-fns';
|
||||||
|
import { WorkTimeRepository } from './work-time.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WorkTimeService {
|
||||||
|
constructor(private readonly repository: WorkTimeRepository) {}
|
||||||
|
|
||||||
|
async findByPersonnelNumberInInterval(
|
||||||
|
personnelNumber: string,
|
||||||
|
interval: Interval,
|
||||||
|
): Promise<WorkTime> {
|
||||||
|
return this.repository.findByPersonnelNumberInInterval(
|
||||||
|
personnelNumber,
|
||||||
|
interval,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,59 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from '@nestjs/common';
|
||||||
import { PrismaModule } from "src/core/database/prisma.module";
|
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||||
import { CarEntityRepository } from "./car-entity/car-entity.repository";
|
import { CarEntityService } from './car-entity/car-entity.service';
|
||||||
import { CarEntityService } from "./car-entity/car-entity.service";
|
import { AnomalyRepository } from './entities/anomaly/anomaly.repository';
|
||||||
|
import { AnomalyService } from './entities/anomaly/anomaly.service';
|
||||||
|
import { AttendanceRegistrationRepository } from './entities/attendance-registration/attendance-registration.repository';
|
||||||
|
import { AttendanceRegistrationService } from './entities/attendance-registration/attendance-registration.service';
|
||||||
|
import { EmployeeRepository } from './entities/employee/employee.repository';
|
||||||
|
import { EmployeeService } from './entities/employee/employee.service';
|
||||||
|
import { PatientRepository } from './entities/patient/patient.repository';
|
||||||
|
import { PatientService } from './entities/patient/patient.service';
|
||||||
|
import { PlannedTimeRepository } from './entities/planned-time/planned-time.repository';
|
||||||
|
import { PlannedTimeService } from './entities/planned-time/planned-time.service';
|
||||||
|
import { TariffRepository } from './entities/tariff/tariff.repository';
|
||||||
|
import { TariffService } from './entities/tariff/tariff.service';
|
||||||
|
import { TicketRepository } from './entities/ticket/ticket.repository';
|
||||||
|
import { TicketService } from './entities/ticket/ticket.service';
|
||||||
|
import { TourRepository } from './entities/tour/tour.repository';
|
||||||
|
import { TourService } from './entities/tour/tour.service';
|
||||||
|
import { WorkTimeRepository } from './entities/work-time/work-time.repository';
|
||||||
|
import { WorkTimeService } from './entities/work-time/work-time.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
exports: [CarEntityService],
|
exports: [
|
||||||
providers: [CarEntityRepository, CarEntityService]
|
CarEntityService,
|
||||||
|
AnomalyService,
|
||||||
|
AttendanceRegistrationService,
|
||||||
|
EmployeeService,
|
||||||
|
PatientService,
|
||||||
|
PlannedTimeService,
|
||||||
|
TariffService,
|
||||||
|
TicketService,
|
||||||
|
TourService,
|
||||||
|
WorkTimeService,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
CarEntityService,
|
||||||
|
AnomalyRepository,
|
||||||
|
AnomalyService,
|
||||||
|
AttendanceRegistrationRepository,
|
||||||
|
AttendanceRegistrationService,
|
||||||
|
EmployeeRepository,
|
||||||
|
EmployeeService,
|
||||||
|
PatientRepository,
|
||||||
|
PatientService,
|
||||||
|
PlannedTimeRepository,
|
||||||
|
PlannedTimeService,
|
||||||
|
TariffRepository,
|
||||||
|
TariffService,
|
||||||
|
TicketRepository,
|
||||||
|
TicketService,
|
||||||
|
TourRepository,
|
||||||
|
TourService,
|
||||||
|
WorkTimeRepository,
|
||||||
|
WorkTimeService,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class FeatBusinessObjectsModule {}
|
export class FeatBusinessObjectsModule {}
|
||||||
|
|
@ -8,8 +8,8 @@ import {
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { Interval, addDays, isSaturday, isWithinInterval } from 'date-fns';
|
import { Interval, addDays, isSaturday, isWithinInterval } from 'date-fns';
|
||||||
|
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
|
||||||
import { generateIntervalForTimeRange } from 'src/utils';
|
import { generateIntervalForTimeRange } from 'src/utils';
|
||||||
|
import { TariffService } from '../feat-business-objects/entities/tariff/tariff.service';
|
||||||
import { BILLING_INFORMATION_BY_ORDINANCE_TYPE } from './billing-information-by-ordinance-type';
|
import { BILLING_INFORMATION_BY_ORDINANCE_TYPE } from './billing-information-by-ordinance-type';
|
||||||
import {
|
import {
|
||||||
calculateDesinfektionszuschlag,
|
calculateDesinfektionszuschlag,
|
||||||
|
|
@ -36,8 +36,8 @@ export class CalculationService {
|
||||||
private healthInsurancesWithNoTenant = [];
|
private healthInsurancesWithNoTenant = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly personnelCostCalculationService: PersonnelCostCalculationService,
|
private readonly personnelCostCalculationService: PersonnelCostCalculationService,
|
||||||
|
private readonly tariffService: TariffService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async calculateTourRevenue(tour: Tour): Promise<number> {
|
async calculateTourRevenue(tour: Tour): Promise<number> {
|
||||||
|
|
@ -305,127 +305,29 @@ export class CalculationService {
|
||||||
tourDate: Date,
|
tourDate: Date,
|
||||||
) {
|
) {
|
||||||
if (!healthInsurance || healthInsurance === '') {
|
if (!healthInsurance || healthInsurance === '') {
|
||||||
return this.prisma.tariff.findFirst({
|
return this.tariffService.findByTariffGroupDescription(
|
||||||
where: {
|
'AOK',
|
||||||
tariffGroup: {
|
ordinanceType,
|
||||||
description: {
|
tourDate,
|
||||||
contains: 'AOK',
|
);
|
||||||
},
|
|
||||||
ordinanceType,
|
|
||||||
},
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{ validTo: null },
|
|
||||||
{
|
|
||||||
validFrom: {
|
|
||||||
lte: tourDate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{ validTo: { gte: tourDate } },
|
|
||||||
{ validFrom: { lte: tourDate } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
tariffGroup: true,
|
|
||||||
costTypes: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const tenant = await this.prisma.avicennaTenant.findFirst({
|
const tenant = await this.tariffService.findTenant(
|
||||||
where: {
|
healthInsurance,
|
||||||
OR: [
|
ordinanceType,
|
||||||
{
|
);
|
||||||
name: healthInsurance,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: {
|
|
||||||
contains: healthInsurance,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
tariffGroups: {
|
|
||||||
some: {
|
|
||||||
ordinanceType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!tenant) {
|
if (!tenant) {
|
||||||
this.healthInsurancesWithNoTenant.push(healthInsurance);
|
this.healthInsurancesWithNoTenant.push(healthInsurance);
|
||||||
return this.prisma.tariff.findFirst({
|
return this.tariffService.findByTariffGroupDescription(
|
||||||
where: {
|
'AOK',
|
||||||
tariffGroup: {
|
ordinanceType,
|
||||||
description: {
|
tourDate,
|
||||||
contains: 'AOK',
|
);
|
||||||
},
|
|
||||||
ordinanceType,
|
|
||||||
},
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{ validTo: null },
|
|
||||||
{
|
|
||||||
validFrom: {
|
|
||||||
lte: tourDate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{ validTo: { gte: tourDate } },
|
|
||||||
{ validFrom: { lte: tourDate } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
tariffGroup: true,
|
|
||||||
costTypes: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return this.prisma.tariff.findFirst({
|
return this.tariffService.findByTenantId(
|
||||||
where: {
|
tenant.id,
|
||||||
tariffGroup: {
|
ordinanceType,
|
||||||
tenants: {
|
tourDate,
|
||||||
some: {
|
);
|
||||||
id: tenant.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ordinanceType,
|
|
||||||
},
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{ validTo: null },
|
|
||||||
{
|
|
||||||
validFrom: {
|
|
||||||
lte: tourDate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{ validTo: { gte: tourDate } },
|
|
||||||
{ validFrom: { lte: tourDate } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
tariffGroup: true,
|
|
||||||
costTypes: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getHealthInsurancesWithNoTenant(): string[] {
|
getHealthInsurancesWithNoTenant(): string[] {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ import { Module } from '@nestjs/common';
|
||||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||||
import { CalculationService } from './calculation.service';
|
import { CalculationService } from './calculation.service';
|
||||||
import { PersonnelCostCalculationService } from './personnel-cost-calculation.service';
|
import { PersonnelCostCalculationService } from './personnel-cost-calculation.service';
|
||||||
|
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule, FeatBusinessObjectsModule],
|
||||||
providers: [CalculationService, PersonnelCostCalculationService],
|
providers: [CalculationService, PersonnelCostCalculationService],
|
||||||
exports: [CalculationService],
|
exports: [CalculationService],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import {
|
||||||
set,
|
set,
|
||||||
subDays,
|
subDays,
|
||||||
subHours,
|
subHours,
|
||||||
subMonths,
|
subWeeks
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
import { PrismaService } from 'src/core/database/prisma.service';
|
||||||
import { generateIntervalForTimeRange } from 'src/utils';
|
import { generateIntervalForTimeRange } from 'src/utils';
|
||||||
|
|
@ -310,7 +310,7 @@ export class KpiService {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
createdAt: {
|
createdAt: {
|
||||||
gte: subMonths(new Date(), 3),
|
gte: subWeeks(new Date(), 2),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { ApprovalState } from '@prisma/client';
|
|
||||||
import { TicketFilter } from './ticket-filter';
|
|
||||||
|
|
||||||
export class TicketFilterArgs implements TicketFilter {
|
|
||||||
errors: string[];
|
|
||||||
approvalStates: ApprovalState[];
|
|
||||||
documentInfo: ('approval' | 'transport_document')[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApprovalInfoArgs {
|
|
||||||
isNeeded?: boolean;
|
|
||||||
whoRequested?: string;
|
|
||||||
answeredFrom?: string;
|
|
||||||
location?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocumentInfoArgs {
|
|
||||||
approval: ApprovalInfoArgs;
|
|
||||||
tdLocation?: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import { ApprovalState, Prisma } from '@prisma/client';
|
|
||||||
|
|
||||||
export interface TicketFilter {
|
|
||||||
errors?: string[];
|
|
||||||
approvalStates?: ApprovalState[];
|
|
||||||
documentInfo?: ('approval' | 'transport_document')[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPrismaQueryFromTicketFilters(
|
|
||||||
filter: TicketFilter,
|
|
||||||
): Prisma.TicketWhereInput[] {
|
|
||||||
const conditions: Prisma.TicketWhereInput[] = [];
|
|
||||||
|
|
||||||
Object.entries(filter ?? {}).forEach(([property, args]) => {
|
|
||||||
switch (property) {
|
|
||||||
case 'errors':
|
|
||||||
if (args.length > 0)
|
|
||||||
conditions.push({
|
|
||||||
errors: {
|
|
||||||
hasSome: [...args],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
case 'approvalStates':
|
|
||||||
if (args.length > 0)
|
|
||||||
conditions.push({
|
|
||||||
approvalState: {
|
|
||||||
in: [...args],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
case 'documentInfo':
|
|
||||||
if (args.includes('approval')) {
|
|
||||||
conditions.push({
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
documentInfo: {
|
|
||||||
path: ['approval', 'location'],
|
|
||||||
not: 'company',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
documentInfo: {
|
|
||||||
path: ['approval', 'location'],
|
|
||||||
equals: Prisma.JsonNull,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (args.includes('td_company')) {
|
|
||||||
conditions.push({
|
|
||||||
documentInfo: {
|
|
||||||
path: ['tdLocation'],
|
|
||||||
equals: 'company',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (args.includes('td_customer')) {
|
|
||||||
conditions.push({
|
|
||||||
documentInfo: {
|
|
||||||
path: ['tdLocation'],
|
|
||||||
equals: 'customer',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return conditions;
|
|
||||||
}
|
|
||||||
|
|
@ -6,35 +6,41 @@ import {
|
||||||
Tour,
|
Tour,
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { Interval } from 'date-fns';
|
import { Interval } from 'date-fns';
|
||||||
import { DocumentInfoArgs } from './ticket-arg.types';
|
import { PatientService } from '../feat-business-objects/entities/patient/patient.service';
|
||||||
|
import { TicketService } from '../feat-business-objects/entities/ticket/ticket.service';
|
||||||
|
import { UpdateDocumentInfoDto } from '../feat-business-objects/entities/ticket/tickets.dto';
|
||||||
|
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
|
||||||
import {
|
import {
|
||||||
ValidationResult,
|
ValidationResult,
|
||||||
validatePatient,
|
validatePatient,
|
||||||
validatePatientDataOnTours,
|
validatePatientDataOnTours,
|
||||||
validateTour,
|
validateTour,
|
||||||
} from './ticket-validation.functions';
|
} from './ticket-validation.functions';
|
||||||
import { TicketRepository } from './ticket.repository';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TicketLifecycleService {
|
export class TicketLifecycleService {
|
||||||
private readonly logger = new Logger(TicketLifecycleService.name);
|
private readonly logger = new Logger(TicketLifecycleService.name);
|
||||||
|
|
||||||
constructor(private readonly repository: TicketRepository) {}
|
constructor(
|
||||||
|
private readonly patientService: PatientService,
|
||||||
|
private readonly ticketService: TicketService,
|
||||||
|
private readonly tourService: TourService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async createTickets(interval: Interval): Promise<void> {
|
async createTickets(interval: Interval): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const tours = await this.repository.findTours(interval);
|
const tours = await this.tourService.findInInterval(interval);
|
||||||
|
|
||||||
for (const tour of tours) {
|
for (const tour of tours) {
|
||||||
if (tour.ticketId) {
|
if (tour.ticketId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticketId = await this.repository.searchTicketIDForTour(tour);
|
const ticketId = await this.searchTicketIDForTour(tour);
|
||||||
if (ticketId) {
|
if (ticketId) {
|
||||||
await this.repository.addToursToTicket(ticketId, [tour]);
|
await this.ticketService.addToursToTicket(ticketId, [tour]);
|
||||||
} else {
|
} else {
|
||||||
await this.repository.createTicket([tour]);
|
await this.ticketService.createTicket([tour]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,9 +53,9 @@ export class TicketLifecycleService {
|
||||||
|
|
||||||
async validateDocumentInfoUpdate(
|
async validateDocumentInfoUpdate(
|
||||||
ticketId: string,
|
ticketId: string,
|
||||||
documentInfo: DocumentInfoArgs,
|
documentInfo: UpdateDocumentInfoDto,
|
||||||
) {
|
) {
|
||||||
const ticket = await this.repository.findById(ticketId);
|
const ticket = await this.ticketService.findById(ticketId);
|
||||||
|
|
||||||
switch (ticket.currentState) {
|
switch (ticket.currentState) {
|
||||||
case TicketValidationState.USER_INTERACTION_REQUIRED:
|
case TicketValidationState.USER_INTERACTION_REQUIRED:
|
||||||
|
|
@ -68,14 +74,14 @@ export class TicketLifecycleService {
|
||||||
stateUpdate = TicketValidationState.TD_UNCERTAIN;
|
stateUpdate = TicketValidationState.TD_UNCERTAIN;
|
||||||
else stateUpdate = TicketValidationState.DOCUMENTS_MISSING;
|
else stateUpdate = TicketValidationState.DOCUMENTS_MISSING;
|
||||||
|
|
||||||
return this.repository.updateTicket(ticketId, {
|
return this.ticketService.updateTicket(ticketId, {
|
||||||
documentInfo,
|
documentInfo,
|
||||||
approvalState: approvalStateUpdate,
|
approvalState: approvalStateUpdate,
|
||||||
state: stateUpdate,
|
state: stateUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
case TicketValidationState.TD_UNCERTAIN:
|
case TicketValidationState.TD_UNCERTAIN:
|
||||||
return this.repository.updateTicket(ticketId, {
|
return this.ticketService.updateTicket(ticketId, {
|
||||||
documentInfo,
|
documentInfo,
|
||||||
state:
|
state:
|
||||||
ticket.approvalState === ApprovalState.FREE &&
|
ticket.approvalState === ApprovalState.FREE &&
|
||||||
|
|
@ -84,7 +90,7 @@ export class TicketLifecycleService {
|
||||||
: TicketValidationState.DOCUMENTS_MISSING,
|
: TicketValidationState.DOCUMENTS_MISSING,
|
||||||
});
|
});
|
||||||
case TicketValidationState.DOCUMENTS_MISSING:
|
case TicketValidationState.DOCUMENTS_MISSING:
|
||||||
return this.repository.updateTicket(ticketId, {
|
return this.ticketService.updateTicket(ticketId, {
|
||||||
documentInfo,
|
documentInfo,
|
||||||
...((ticket.approvalState === ApprovalState.FREE ||
|
...((ticket.approvalState === ApprovalState.FREE ||
|
||||||
documentInfo.approval.location === 'company') &&
|
documentInfo.approval.location === 'company') &&
|
||||||
|
|
@ -95,8 +101,21 @@ export class TicketLifecycleService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async searchTicketIDForTour(tour: Tour): Promise<string | undefined> {
|
||||||
|
const tours = await Promise.all([
|
||||||
|
this.tourService.findFirstRecurring(tour),
|
||||||
|
this.tourService.findOnSameDayWithOppositeDirection(tour),
|
||||||
|
]);
|
||||||
|
const firstFoundTour = tours.filter((tour) => !!tour)[0];
|
||||||
|
if (firstFoundTour) {
|
||||||
|
const ticket = await this.ticketService.findIdByTourId(firstFoundTour.id);
|
||||||
|
return ticket?.id;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private async validateCreatedTickets(): Promise<void> {
|
private async validateCreatedTickets(): Promise<void> {
|
||||||
const tickets = await this.repository.findAllCreated();
|
const tickets = await this.ticketService.findAllCreated();
|
||||||
this.logger.debug(`Found ${tickets.length} with state CREATED`);
|
this.logger.debug(`Found ${tickets.length} with state CREATED`);
|
||||||
|
|
||||||
for (const ticket of tickets) {
|
for (const ticket of tickets) {
|
||||||
|
|
@ -109,7 +128,7 @@ export class TicketLifecycleService {
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
const { result: approvalResult } = await this.isApprovalNeeded(ticket);
|
const { result: approvalResult } = await this.isApprovalNeeded(ticket);
|
||||||
if (approvalResult) {
|
if (approvalResult) {
|
||||||
await this.repository.updateTicket(ticket.id, {
|
await this.ticketService.updateTicket(ticket.id, {
|
||||||
errors,
|
errors,
|
||||||
notes,
|
notes,
|
||||||
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
||||||
|
|
@ -126,14 +145,14 @@ export class TicketLifecycleService {
|
||||||
ordinanceType.indexOf('TSW') > -1,
|
ordinanceType.indexOf('TSW') > -1,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await this.repository.updateTicket(ticket.id, {
|
await this.ticketService.updateTicket(ticket.id, {
|
||||||
errors,
|
errors,
|
||||||
notes,
|
notes,
|
||||||
state: TicketValidationState.TD_UNCERTAIN,
|
state: TicketValidationState.TD_UNCERTAIN,
|
||||||
approvalState: ApprovalState.FREE,
|
approvalState: ApprovalState.FREE,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await this.repository.updateTicket(ticket.id, {
|
await this.ticketService.updateTicket(ticket.id, {
|
||||||
errors,
|
errors,
|
||||||
notes,
|
notes,
|
||||||
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
||||||
|
|
@ -142,15 +161,19 @@ export class TicketLifecycleService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.repository.setActionablesOnTicket(ticket.id, errors, notes);
|
await this.ticketService.setActionablesOnTicket(
|
||||||
|
ticket.id,
|
||||||
|
errors,
|
||||||
|
notes,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async archiveClosedTickets() {
|
private async archiveClosedTickets() {
|
||||||
const closedTickets = await this.repository.findAllClosed();
|
const closedTickets = await this.ticketService.findAllClosed();
|
||||||
for (const closedTicket of closedTickets) {
|
for (const closedTicket of closedTickets) {
|
||||||
await this.repository.setStateOfTicket(
|
await this.ticketService.setStateOfTicket(
|
||||||
closedTicket.id,
|
closedTicket.id,
|
||||||
TicketValidationState.ARCHIVED,
|
TicketValidationState.ARCHIVED,
|
||||||
);
|
);
|
||||||
|
|
@ -183,7 +206,7 @@ export class TicketLifecycleService {
|
||||||
if (!patientId && ticket.tours.at(0)?.type === 'Spontan') {
|
if (!patientId && ticket.tours.at(0)?.type === 'Spontan') {
|
||||||
patientResult = validatePatientDataOnTours(ticket.tours);
|
patientResult = validatePatientDataOnTours(ticket.tours);
|
||||||
} else {
|
} else {
|
||||||
const patient = await this.repository.findPatientById(patientId);
|
const patient = await this.patientService.findById(patientId);
|
||||||
if (!patient) {
|
if (!patient) {
|
||||||
patientResult = validatePatientDataOnTours(ticket.tours);
|
patientResult = validatePatientDataOnTours(ticket.tours);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -254,7 +277,7 @@ export class TicketLifecycleService {
|
||||||
['BTW', 'TSW'].includes(tour.ordinanceType) &&
|
['BTW', 'TSW'].includes(tour.ordinanceType) &&
|
||||||
tour.category !== 'Dialyse'
|
tour.category !== 'Dialyse'
|
||||||
) {
|
) {
|
||||||
const patient = await this.repository.findPatientById(tour.patientId);
|
const patient = await this.patientService.findById(tour.patientId);
|
||||||
if (
|
if (
|
||||||
['3', '4', '5'].includes(patient.careDegree) &&
|
['3', '4', '5'].includes(patient.careDegree) &&
|
||||||
(['aG', 'Bi', 'H'].includes(patient.disabilityMark1) ||
|
(['aG', 'Bi', 'H'].includes(patient.disabilityMark1) ||
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { PrismaModule } from 'src/core/database/prisma.module';
|
import { PrismaModule } from 'src/core/database/prisma.module';
|
||||||
|
import { FeatBusinessObjectsModule } from '../feat-business-objects/feat-business-objects.module';
|
||||||
import { TicketLifecycleService } from './ticket-lifecycle.service';
|
import { TicketLifecycleService } from './ticket-lifecycle.service';
|
||||||
import { TicketRepository } from './ticket.repository';
|
|
||||||
import { TicketService } from './tickets.service';
|
import { TicketService } from './tickets.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule, FeatBusinessObjectsModule],
|
||||||
providers: [TicketRepository, TicketService, TicketLifecycleService],
|
providers: [TicketService, TicketLifecycleService],
|
||||||
exports: [TicketService],
|
exports: [TicketService, TicketLifecycleService],
|
||||||
})
|
})
|
||||||
export class TicketSystemModule {}
|
export class FeatTicketSystemModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,504 +0,0 @@
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { Patient, Prisma, TicketValidationState, Tour } from '@prisma/client';
|
|
||||||
import { Interval, endOfDay, startOfDay } from 'date-fns';
|
|
||||||
import { ExpressionBuilder, Kysely } from 'kysely';
|
|
||||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
|
||||||
|
|
||||||
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
|
|
||||||
import { PrismaService } from 'src/core/database/prisma.service';
|
|
||||||
import { ApprovalState, DB } from 'src/core/database/types';
|
|
||||||
import { DocumentInfoArgs, TicketFilterArgs } from './ticket-arg.types';
|
|
||||||
import { createPrismaQueryFromTicketFilters } from './ticket-filter';
|
|
||||||
|
|
||||||
function withTours(eb: ExpressionBuilder<DB, 'Ticket'>) {
|
|
||||||
return jsonArrayFrom(
|
|
||||||
eb
|
|
||||||
.selectFrom('Tour')
|
|
||||||
.selectAll('Tour')
|
|
||||||
.whereRef('Tour.ticketId', '=', 'Ticket.id')
|
|
||||||
.orderBy('Tour.operationId'),
|
|
||||||
).as('tours');
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TicketRepository {
|
|
||||||
private logger = new Logger(TicketRepository.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
@Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async createTicket(tours: Tour[]) {
|
|
||||||
this.logger.log(`Creating Ticket for ${tours.length} tours`);
|
|
||||||
try {
|
|
||||||
const ticket = await this.db
|
|
||||||
.insertInto('Ticket')
|
|
||||||
.values({
|
|
||||||
currentState: TicketValidationState.CREATED,
|
|
||||||
})
|
|
||||||
.returningAll()
|
|
||||||
.executeTakeFirst();
|
|
||||||
|
|
||||||
await this.db
|
|
||||||
.insertInto('TicketStateHistory')
|
|
||||||
.values({
|
|
||||||
state: TicketValidationState.CREATED,
|
|
||||||
ticketId: ticket.id,
|
|
||||||
})
|
|
||||||
.executeTakeFirst();
|
|
||||||
|
|
||||||
await this.db
|
|
||||||
.updateTable('Tour')
|
|
||||||
.set({
|
|
||||||
ticketId: ticket.id,
|
|
||||||
})
|
|
||||||
.where(
|
|
||||||
'id',
|
|
||||||
'in',
|
|
||||||
tours.map(({ id }) => id),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return ticket;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async addToursToTicket(ticketId: string, tours: Tour[]) {
|
|
||||||
await this.db
|
|
||||||
.updateTable('Tour')
|
|
||||||
.set({
|
|
||||||
ticketId: ticketId,
|
|
||||||
})
|
|
||||||
.where(
|
|
||||||
'id',
|
|
||||||
'in',
|
|
||||||
tours.map(({ id }) => id),
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return this.db
|
|
||||||
.selectFrom('Ticket')
|
|
||||||
.where('id', '=', ticketId)
|
|
||||||
.selectAll()
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
setActionablesOnTicket(ticketId: string, errors: string[], notes: string[]) {
|
|
||||||
return this.db
|
|
||||||
.updateTable('Ticket')
|
|
||||||
.set({ errors, notes })
|
|
||||||
.where('id', '=', ticketId)
|
|
||||||
.returningAll()
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
async setStateOfTicket(ticketId: string, state: TicketValidationState) {
|
|
||||||
await this.db
|
|
||||||
.updateTable('Ticket')
|
|
||||||
.set({
|
|
||||||
currentState: state,
|
|
||||||
})
|
|
||||||
.where('Ticket.id', '=', ticketId)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return this.db
|
|
||||||
.insertInto('TicketStateHistory')
|
|
||||||
.values({ state, ticketId })
|
|
||||||
.returningAll()
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateTicket(
|
|
||||||
ticketId: string,
|
|
||||||
data: {
|
|
||||||
state?: TicketValidationState;
|
|
||||||
errors?: string[];
|
|
||||||
notes?: string[];
|
|
||||||
isUrgent?: boolean;
|
|
||||||
urgency?: Date;
|
|
||||||
approvalState?: ApprovalState;
|
|
||||||
documentInfo?: DocumentInfoArgs;
|
|
||||||
approvalPath?: string;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const { state, ...ticketData } = data;
|
|
||||||
|
|
||||||
await this.db
|
|
||||||
.updateTable('Ticket')
|
|
||||||
.set({ ...ticketData })
|
|
||||||
.where('id', '=', ticketId)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
if (state) {
|
|
||||||
await this.setStateOfTicket(ticketId, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.db
|
|
||||||
.selectFrom('Ticket')
|
|
||||||
.where('id', '=', ticketId)
|
|
||||||
.selectAll('Ticket')
|
|
||||||
.select((eb) => withTours(eb))
|
|
||||||
.executeTakeFirst();
|
|
||||||
}
|
|
||||||
|
|
||||||
findById(id: string) {
|
|
||||||
return this.prisma.ticket.findUnique({
|
|
||||||
where: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
tours: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findAll() {
|
|
||||||
return this.prisma.ticket.findMany({
|
|
||||||
where: {
|
|
||||||
state: {
|
|
||||||
none: {
|
|
||||||
state: TicketValidationState.ARCHIVED,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
tours: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findAllCreated() {
|
|
||||||
return this.prisma.ticket.findMany({
|
|
||||||
where: {
|
|
||||||
state: {
|
|
||||||
none: {
|
|
||||||
state: TicketValidationState.USER_INTERACTION_REQUIRED,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
tours: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findAllClosed() {
|
|
||||||
return this.prisma.ticket.findMany({
|
|
||||||
where: {
|
|
||||||
tours: {
|
|
||||||
every: {
|
|
||||||
check: {
|
|
||||||
not: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
search(query: string) {
|
|
||||||
return this.prisma.ticket.findMany({
|
|
||||||
where: {
|
|
||||||
state: {
|
|
||||||
none: {
|
|
||||||
state: TicketValidationState.ARCHIVED,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tours: {
|
|
||||||
some: {
|
|
||||||
OR: [
|
|
||||||
...query.split(' ').flatMap((word) => [
|
|
||||||
{
|
|
||||||
patientName: {
|
|
||||||
contains: word,
|
|
||||||
mode: Prisma.QueryMode.insensitive,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientSurname: {
|
|
||||||
contains: word,
|
|
||||||
mode: Prisma.QueryMode.insensitive,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
operationId: {
|
|
||||||
contains: word,
|
|
||||||
mode: Prisma.QueryMode.insensitive,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
take: 5,
|
|
||||||
include: {
|
|
||||||
tours: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async findByState(
|
|
||||||
state: TicketValidationState,
|
|
||||||
filter: TicketFilterArgs,
|
|
||||||
take?: number,
|
|
||||||
skip?: number,
|
|
||||||
) {
|
|
||||||
const query: Prisma.TicketFindManyArgs = {
|
|
||||||
...{
|
|
||||||
...(skip ? { skip, take: 10 } : {}),
|
|
||||||
...(take ? { take } : {}),
|
|
||||||
...(take && !skip ? { skip: 0 } : {}),
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
AND: createPrismaQueryFromTicketFilters(filter),
|
|
||||||
currentState: state,
|
|
||||||
tours: {
|
|
||||||
some: {
|
|
||||||
check: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
};
|
|
||||||
|
|
||||||
const [tickets, count] = await this.prisma.$transaction([
|
|
||||||
this.prisma.ticket.findMany({ ...query }),
|
|
||||||
this.prisma.ticket.count({ where: query.where }),
|
|
||||||
]);
|
|
||||||
return {
|
|
||||||
tickets,
|
|
||||||
total: count,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
findPatientById(patientId: string): Promise<Patient> {
|
|
||||||
return this.prisma.patient.findUnique({ where: { id: patientId } });
|
|
||||||
}
|
|
||||||
|
|
||||||
findStatesByTicketId(
|
|
||||||
ticketId: string,
|
|
||||||
): Promise<{ state: TicketValidationState }[]> {
|
|
||||||
return this.prisma.ticketStateHistory.findMany({
|
|
||||||
where: {
|
|
||||||
ticketId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
state: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findToursByTicketId(ticketId: string): Promise<Tour[]> {
|
|
||||||
return this.prisma.tour.findMany({
|
|
||||||
where: {
|
|
||||||
ticketId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async searchTicketIDForTour(tour: Tour): Promise<string | undefined> {
|
|
||||||
const tours = await Promise.all([
|
|
||||||
this.findFirstRecurringTour(tour),
|
|
||||||
this.findTourOnSameDayWithOppositeDirection(tour),
|
|
||||||
]);
|
|
||||||
const firstFoundTour = tours.filter((tour) => !!tour)[0];
|
|
||||||
if (firstFoundTour) {
|
|
||||||
const ticket = await this.prisma.ticket.findFirst({
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
tours: {
|
|
||||||
some: {
|
|
||||||
id: firstFoundTour.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return ticket?.id;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
findFirstRecurringTour(tour: Tour): Promise<Tour | null> {
|
|
||||||
const institutionStreet =
|
|
||||||
tour.direction === 'Hinfahrt' ? tour.targetStreet : tour.startStreet;
|
|
||||||
return this.prisma.tour.findFirst({
|
|
||||||
where: {
|
|
||||||
type: 'Serie',
|
|
||||||
category: tour.category,
|
|
||||||
ordinanceType: tour.ordinanceType,
|
|
||||||
patientId: tour.patientId,
|
|
||||||
patientName: tour.patientName,
|
|
||||||
patientSurname: tour.patientSurname,
|
|
||||||
patientStreet: tour.patientStreet,
|
|
||||||
patientCity: tour.patientCity,
|
|
||||||
patientZip: tour.patientZip,
|
|
||||||
OR: [
|
|
||||||
{ targetStreet: institutionStreet },
|
|
||||||
{ startStreet: institutionStreet },
|
|
||||||
],
|
|
||||||
rangeEndDate: {
|
|
||||||
gte: tour.startDate,
|
|
||||||
},
|
|
||||||
ticketId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
startDate: 'desc',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findTourOnSameDayWithOppositeDirection(tour: Tour): Promise<Tour | null> {
|
|
||||||
return this.prisma.tour.findFirst({
|
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gt: startOfDay(new Date(tour.startDate)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: endOfDay(new Date(tour.startDate)),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startStreet: tour.targetStreet,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
targetStreet: tour.startStreet,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
direction: tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findToursWithTicket(interval: Interval): Promise<Tour[]> {
|
|
||||||
return this.prisma.tour.findMany({
|
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
ticketId: {
|
|
||||||
not: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gte: new Date(interval.start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: new Date(interval.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTourFromTicket(tourId: string): Promise<Tour> {
|
|
||||||
return this.prisma.tour.update({
|
|
||||||
where: {
|
|
||||||
id: tourId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
Ticket: {
|
|
||||||
disconnect: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
findTours(interval: Interval): Promise<Tour[]> {
|
|
||||||
return this.prisma.tour.findMany({
|
|
||||||
where: {
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
gte: new Date(interval.start),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
startDate: {
|
|
||||||
lt: new Date(interval.end),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
carName: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Storno',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Löschen',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
carName: {
|
|
||||||
not: 'Abgabe',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
check: {
|
|
||||||
not: 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ticketId: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Pause
|
|
||||||
not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
patientId: {
|
|
||||||
// Feierabend
|
|
||||||
not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteEmptyTickets(): Promise<Prisma.BatchPayload> {
|
|
||||||
return this.prisma.ticket.deleteMany({
|
|
||||||
where: {
|
|
||||||
tours: {
|
|
||||||
none: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +1,15 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { Ticket, TicketValidationState, Tour } from '@prisma/client';
|
|
||||||
import { Interval } from 'date-fns';
|
import { Interval } from 'date-fns';
|
||||||
import { DocumentInfoArgs } from './ticket-arg.types';
|
|
||||||
import { TicketLifecycleService } from './ticket-lifecycle.service';
|
import { TicketLifecycleService } from './ticket-lifecycle.service';
|
||||||
import {
|
import {
|
||||||
TicketSystemEventTypes,
|
TicketSystemEventTypes,
|
||||||
TicketSystemValidatedEvent,
|
TicketSystemValidatedEvent,
|
||||||
} from './ticket-system.events';
|
} from './ticket-system.events';
|
||||||
import { TicketRepository } from './ticket.repository';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TicketService {
|
export class TicketService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly repository: TicketRepository,
|
|
||||||
private readonly lifecycle: TicketLifecycleService,
|
private readonly lifecycle: TicketLifecycleService,
|
||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
) {}
|
) {}
|
||||||
|
|
@ -26,30 +22,4 @@ export class TicketService {
|
||||||
new TicketSystemValidatedEvent(0),
|
new TicketSystemValidatedEvent(0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDocumentInfo(ticketId: string, documentInfo: DocumentInfoArgs) {
|
|
||||||
return this.lifecycle.validateDocumentInfoUpdate(ticketId, documentInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
findById(id: string): Promise<Ticket> {
|
|
||||||
return this.repository.findById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
findAll(): Promise<Ticket[]> {
|
|
||||||
return this.repository.findAll() as Promise<Ticket[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchTickets(query: string): Promise<Ticket[]> {
|
|
||||||
return this.repository.search(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
findToursByTicketId(ticketId: string): Promise<Tour[]> {
|
|
||||||
return this.repository.findToursByTicketId(ticketId);
|
|
||||||
}
|
|
||||||
|
|
||||||
findStatesByTicketId(ticketId: string): Promise<TicketValidationState[]> {
|
|
||||||
return this.repository
|
|
||||||
.findStatesByTicketId(ticketId)
|
|
||||||
.then((states) => states.map(({ state }) => state));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue