commit working state

Too many changes to split them up. This commit contains roughly two months worth of work.

This commit introduces the first aggregate and rewrites the ticket system to follow DDD patterns.
It adds Spartan NG as the new component library. TanStack Query to replace GraphQL. Oh and Prisma is
almost over as well.
This commit is contained in:
Marcel Arndt
2026-01-19 12:21:48 +01:00
parent 62e663d053
commit ce676f20a4
334 changed files with 64333 additions and 52554 deletions
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@avicenna/shared-dtos",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"watch": "tsc --watch"
},
"devDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@nestjs/swagger": "^11.2.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"date-fns": "^4.1.0"
}
}
@@ -0,0 +1,18 @@
export interface OptimizationTourDto {
id: string;
operationId: string;
ordinanceType: string;
startDate: string;
optimizationInMinutes: number;
departedForPickup: string | null;
arrivedAtPickup: string | null;
departedPickup: string | null;
arrivedAtTarget: string | null;
completed: string | null;
}
export interface OptimizationCarDto {
name: string;
tours: OptimizationTourDto[];
}
@@ -0,0 +1,11 @@
export interface AccountingListDto {
tourId: string;
operationId: string;
ordinanceType: string;
startDate: Date;
revenue: number;
isTicketOpen: boolean;
hasRevenueDeviation: boolean;
hasOptimizationPotential: boolean;
}
@@ -0,0 +1,231 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { CostTypeVariant } from '@prisma/client';
import { Type } from 'class-transformer';
export class TourDto {
@ApiProperty()
id!: string;
@ApiProperty()
operationId!: string;
@ApiProperty()
done!: boolean;
@ApiProperty()
direction!: string;
@ApiProperty({ nullable: true })
carName!: string | null;
@ApiProperty({ nullable: true })
driverId!: string | null;
@ApiProperty({ nullable: true })
driverName!: string | null;
@ApiProperty({ nullable: true })
codriverId!: string | null;
@ApiProperty({ nullable: true })
codriverName!: string | null;
@ApiProperty({ nullable: true })
patientId!: string | null;
@ApiProperty({ nullable: true })
patientName!: string | null;
@ApiProperty({ nullable: true })
patientSurname!: string | null;
@ApiProperty({ nullable: true })
patientStreet!: string | null;
@ApiProperty({ nullable: true })
patientZip!: string | null;
@ApiProperty({ nullable: true })
patientCity!: string | null;
@ApiProperty({ nullable: true })
healthInsurance!: string | null;
@ApiProperty({ nullable: true })
healthInsuranceNumber!: string | null;
@ApiProperty({ nullable: true })
category!: string | null;
@ApiProperty({ nullable: true })
transportType!: string | null;
@ApiProperty({ nullable: true })
ordinanceType!: string | null;
@ApiProperty({ nullable: true })
startInstitution!: string | null;
@ApiProperty({ nullable: true })
startStreet!: string | null;
@ApiProperty({ nullable: true })
startZip!: string | null;
@ApiProperty({ nullable: true })
startCity!: string | null;
@ApiProperty({ nullable: true })
targetInstitution!: string | null;
@ApiProperty({ nullable: true })
targetStreet!: string | null;
@ApiProperty({ nullable: true })
targetZip!: string | null;
@ApiProperty({ nullable: true })
targetCity!: string | null;
@ApiProperty({ type: Date })
@Type(() => Date)
startDate!: Date;
@ApiProperty({ nullable: true })
startBegin!: string | null;
@ApiProperty({ nullable: true })
startEnd!: string | null;
@ApiProperty({ nullable: true })
target!: string | null;
@ApiProperty({ nullable: true })
targetBegin!: string | null;
@ApiProperty({ nullable: true })
empty!: string | null;
@ApiProperty()
check!: number;
@ApiProperty({ nullable: true })
occupiedKm!: number | null;
@ApiProperty({ nullable: true })
totalKm!: number | null;
@ApiProperty({ nullable: true })
type!: string | null;
@ApiProperty({ nullable: true, type: Date })
@Type(() => Date)
rangeEndDate!: Date | null;
@ApiProperty({ nullable: true })
revenueDispoLive!: number | null;
@ApiProperty({ nullable: true })
revenue!: number | null;
@ApiProperty({ nullable: true })
consumptionCosts!: number | null;
@ApiProperty()
revenueDeviation!: number;
@ApiProperty({ nullable: true, type: Date })
@Type(() => Date)
billDate!: Date | null;
@ApiProperty({ nullable: true })
billNumber!: string | null;
@ApiProperty({ nullable: true, type: Date })
@Type(() => Date)
createdAt!: Date | null;
@ApiProperty({ nullable: true, type: Date })
@Type(() => Date)
updatedAt!: Date | null;
@ApiProperty({ nullable: true, type: Date })
@Type(() => Date)
deletedAt!: Date | null;
@ApiProperty({ nullable: true })
hasInfection!: boolean | null;
@ApiProperty({ nullable: true })
infectionName!: string | null;
@ApiProperty()
optimizationInMinutes!: number;
@ApiProperty({ nullable: true })
ticketId!: string | null;
// Optional!: Wenn das Ticket eingebettet wird (war vorher ein ResolveField)
// Du musst hier ggf. dein TicketDto importieren oder 'any' nutzen.
@ApiPropertyOptional()
ticket?: any;
}
export class PaginatedTourDto {
@ApiProperty({ type: [TourDto] })
@Type(() => TourDto)
nodes!: TourDto[];
@ApiProperty()
prevCursor!: string;
@ApiProperty()
endCursor!: string;
@ApiProperty()
totalCount!: number;
@ApiProperty()
hasNextPage!: boolean;
}
// --- COST BREAKDOWN ---
export class ServicePositionDto {
@ApiProperty({ enum: CostTypeVariant })
variant!: CostTypeVariant;
@ApiProperty()
description!: string;
@ApiProperty()
value!: number;
}
export class TourCostBreakdownDto {
@ApiProperty()
tariffDescription!: string;
@ApiProperty()
tariffValidityPeriod!: string;
@ApiProperty({ type: [ServicePositionDto] })
@Type(() => ServicePositionDto)
servicePositions!: ServicePositionDto[];
}
// --- STATE META ---
export class TourStateMetaDto {
@ApiProperty()
count!: number;
@ApiPropertyOptional()
revenueSum?: number;
@ApiPropertyOptional()
positiveRevenueDeviation?: number;
@ApiPropertyOptional()
negativeRevenueDeviation?: number;
}
@@ -0,0 +1,3 @@
export * from './accounting-car.dto';
export * from './accounting-list.dto';
export * from './accounting.dto';
@@ -0,0 +1,5 @@
export interface AddressDto {
street: string;
city: string;
zip: string;
}
@@ -0,0 +1,5 @@
export * from './address.dto';
export * from './interval.dto';
export * from './kpi.dto';
export * from './paginated-result.dto';
export * from './validation-error';
@@ -0,0 +1,12 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
export class IntervalDto {
@ApiProperty()
@Type(() => Date)
start!: Date;
@ApiProperty()
@Type(() => Date)
end!: Date;
}
@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';
export class MetricDto {
@ApiProperty()
label!: string;
@ApiProperty()
value!: number;
}
export class KpiInfoDto {
@ApiProperty()
main!: MetricDto;
@ApiProperty()
secondary!: MetricDto;
@ApiProperty()
additional!: MetricDto[];
}
@@ -0,0 +1,9 @@
export interface PaginatedResult<T> {
data: T[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
};
}
@@ -0,0 +1,5 @@
export interface ValidationError {
field: string;
message: string;
source: { type: 'TOUR' | 'PATIENT'; id: string };
}
@@ -0,0 +1,4 @@
export * from './accounting';
export * from './common';
export * from './manager';
export * from './tickets';
@@ -0,0 +1 @@
export * from './manager.dto';
@@ -0,0 +1,79 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IntervalDto } from '../common';
export class KPIMetadataDto {
@ApiProperty()
tourAmount!: number;
@ApiProperty()
availableTime!: number;
@ApiProperty()
baseValue!: number;
@ApiProperty()
breakEven!: number;
}
export class KPIDto {
@ApiProperty()
value!: number;
@ApiProperty({ type: KPIMetadataDto })
@Type(() => KPIMetadataDto)
meta!: KPIMetadataDto;
}
export class KPIByCarDto {
@ApiProperty()
carName!: string;
@ApiProperty({ type: KPIDto })
@Type(() => KPIDto)
kpi!: KPIDto;
}
export class TimePerTourKPISummaryDto {
@ApiProperty({ type: IntervalDto })
@Type(() => IntervalDto)
interval!: IntervalDto;
@ApiProperty({ type: KPIDto })
@Type(() => KPIDto)
kpi!: KPIDto;
@ApiProperty({ type: [KPIByCarDto] })
@Type(() => KPIByCarDto)
cars!: KPIByCarDto[];
}
// --- Metrics ---
export class TimePerTourMetricCategoryAmountDto {
@ApiProperty()
category!: string;
@ApiProperty()
amount!: number;
}
export class TimePerTourMetricDto {
@ApiProperty({ type: IntervalDto })
@Type(() => IntervalDto)
interval!: IntervalDto;
@ApiProperty() availableTime!: number;
@ApiProperty() tourAmount!: number;
@ApiProperty() breakEven!: number;
@ApiProperty() revenue!: number;
@ApiProperty() averageRevenue!: number;
@ApiProperty() personnelCost!: number;
@ApiProperty() consumptionCost!: number;
@ApiProperty() forecast!: number;
@ApiProperty() forecastRevenue!: number;
@ApiProperty({ type: [TimePerTourMetricCategoryAmountDto] })
@Type(() => TimePerTourMetricCategoryAmountDto)
categoryAmounts!: TimePerTourMetricCategoryAmountDto[];
}
@@ -0,0 +1,4 @@
export * from './ticket-detail.dto';
export * from './ticket-list.dto';
export * from './ticket-queries.dto';
export * from './ticket.enum';
@@ -0,0 +1,101 @@
import { ValidationError } from '../common';
import { AddressDto } from '../common/address.dto';
import {
ApprovalRequestStatus,
ApprovalRequirement,
TicketStage,
} from './ticket.enum';
export interface TicketTourSummaryDto {
id: string;
operationId: string;
date: string;
startAddress: AddressDto;
targetAddress: AddressDto;
}
export interface TicketApprovalViewDto {
requirement: ApprovalRequirement;
status: ApprovalRequestStatus;
statusLabel: string;
actions: {
override: {
isPossible: boolean;
label: string;
confirmationMessage?: string;
};
registerExternalRequest: {
isPossible: boolean;
label: string;
}
sendRequest: {
isPossible: boolean;
label: string;
description: string;
};
sendReminder: {
isPossible: boolean;
label: string;
description: string;
};
approve: {
isPossible: boolean;
};
};
}
export interface TicketTransportDocumentView {
isDocumentPresent: boolean;
label: string;
digitalDownloadLink?: string;
actions: {
upload: {
isPossible: boolean;
};
receivePhysical: {
isPossible: boolean;
label: string;
};
};
}
export interface TicketDetailDto {
id: string;
stage: TicketStage;
general: {
ordinanceType: string;
transportType: string;
category: string;
hasInfection: boolean;
infectionName: string | null;
isSeries: boolean;
seriesEndDate: Date;
};
validationErrors: ValidationError[];
patient: {
id: string;
name: string;
healthInsurance: string;
address: AddressDto;
};
approval: TicketApprovalViewDto;
transportDocument: TicketTransportDocumentView;
tours: TicketTourSummaryDto[];
createdAt: Date;
updatedAt: Date;
}
@@ -0,0 +1,16 @@
export interface TicketListItemDto {
id: string;
ordinanceType: string;
patientName: string;
stage: string;
nextStart: Date;
tourCount: number;
errorCount: number;
approvalRequirement: string;
approvalStatus: string;
isOverdue: boolean;
createdAt: Date;
updatedAt: Date;
}
@@ -0,0 +1,22 @@
import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsOptional, IsString, Max, Min, ValidationArguments } from 'class-validator';
import { TicketStage } from './ticket.enum';
export class GetBoardQueryDto {
@IsString()
@IsEnum(TicketStage, {'message': (args: ValidationArguments) => `${args.value} is no part of ${args.object}`})
stage!: string;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(50)
limit: number = 10;
}
@@ -0,0 +1,26 @@
export enum TicketStage {
DRAFT = 'DRAFT',
DATA_INCOMPLETE = 'DATA_INCOMPLETE',
ASSESSMENT_PENDING = 'ASSESSMENT',
APPROVAL_IN_PROGRESS = 'APPROVAL',
DOCUMENTS_PENDING = 'DOCS_PENDING',
READY = 'READY',
// Normaler Durchlauf
ARCHIVED = 'ARCHIVED',
// Ticket wurde vorzeitig geschlossen
CLOSED = 'CLOSED'
}
export enum ApprovalRequirement {
UNKOWN = 'UNKNOWN',
NOT_REQUIRED = 'NOT_REQUIRED',
REQUIRED = 'REQUIRED',
}
export enum ApprovalRequestStatus {
NOT_STARTED = 'NOT_STARTED',
PRE_INQUIRY_SENT = 'PRE_INQUIRY_SENT',
FULL_REQUEST_SENT = 'FULL_REQUEST_SENT',
EXTERNAL_REQUEST = 'EXTERNAL_REQUEST',
GRANTED = 'GRANTED',
}
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"moduleResolution": "node",
"outDir": "./dist",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}