ref: add repository and service for all entities

This commit is contained in:
Marcel Arndt 2025-06-12 17:49:49 +02:00
parent 5eb775523f
commit 51ba4a1f83
67 changed files with 1982 additions and 2728 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
export enum TicketSystemEventTypes {
Validated = 'ticket-system.validated',
}
export class TicketSystemValidatedEvent {
constructor(public readonly count: number) {}
}

View File

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

View File

@ -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: [],
};
}

View File

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

View File

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

View File

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

View File

@ -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],
}) })

View File

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

View File

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

View File

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

View File

@ -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(
`Employee has no personnelNumber ${JSON.stringify({
id: employee.id, id: employee.id,
surname: employee.surname, surname: employee.surname,
name: employee.name, name: employee.name,
})}`); })}`,
);
return false; return false;
} }
return true; return true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export const ANOMALY_SLEEP_TIME_MS = 60 * 60 * 1000;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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')[]
}

View File

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

View File

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

View File

@ -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[] = [];

View File

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

View File

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

View File

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

View File

@ -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: {
description: {
contains: 'AOK',
},
ordinanceType, ordinanceType,
}, tourDate,
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: [
{
name: healthInsurance,
},
{
name: {
contains: healthInsurance,
},
},
],
tariffGroups: {
some: {
ordinanceType, 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: {
description: {
contains: 'AOK',
},
ordinanceType, ordinanceType,
}, tourDate,
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: {
tenants: {
some: {
id: tenant.id,
},
},
ordinanceType, ordinanceType,
}, tourDate,
OR: [ );
{
AND: [
{ validTo: null },
{
validFrom: {
lte: tourDate,
},
},
],
},
{
AND: [
{ validTo: { gte: tourDate } },
{ validFrom: { lte: tourDate } },
],
},
],
},
include: {
tariffGroup: true,
costTypes: true,
},
});
} }
getHealthInsurancesWithNoTenant(): string[] { getHealthInsurancesWithNoTenant(): string[] {

View File

@ -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],
}) })

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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