From 56a9ed9647324b0c2cae7593dd31fbe296b154de Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Tue, 10 Mar 2026 19:30:34 +0100 Subject: [PATCH] feat(crm): add Company entity + rename Deals to Vorgaenge - New Company model with full CRUD under /api/v1/crm/companies - Contact now has companyId relation + position field - Deal now has companyId relation - Company detail includes contacts (top 20) and deals (top 10) - All endpoints include company in responses - Swagger tags renamed: Deals -> Vorgaenge (Deals) - Error messages use "Vorgang" instead of "Deal" Co-Authored-By: Claude Opus 4.6 --- packages/crm-service/prisma/crm.schema.prisma | 58 ++++++- packages/crm-service/src/app.module.ts | 2 + .../src/companies/companies.controller.ts | 119 ++++++++++++++ .../src/companies/companies.module.ts | 12 ++ .../src/companies/companies.service.ts | 145 ++++++++++++++++++ .../src/companies/dto/create-company.dto.ts | 87 +++++++++++ .../src/companies/dto/query-companies.dto.ts | 40 +++++ .../src/companies/dto/update-company.dto.ts | 88 +++++++++++ .../src/contacts/contacts.service.ts | 10 +- .../src/contacts/dto/create-contact.dto.ts | 12 ++ .../crm-service/src/deals/deals.controller.ts | 12 +- .../crm-service/src/deals/deals.service.ts | 29 +++- .../src/deals/dto/create-deal.dto.ts | 5 + .../src/deals/dto/query-deals.dto.ts | 5 + 14 files changed, 614 insertions(+), 10 deletions(-) create mode 100644 packages/crm-service/src/companies/companies.controller.ts create mode 100644 packages/crm-service/src/companies/companies.module.ts create mode 100644 packages/crm-service/src/companies/companies.service.ts create mode 100644 packages/crm-service/src/companies/dto/create-company.dto.ts create mode 100644 packages/crm-service/src/companies/dto/query-companies.dto.ts create mode 100644 packages/crm-service/src/companies/dto/update-company.dto.ts diff --git a/packages/crm-service/prisma/crm.schema.prisma b/packages/crm-service/prisma/crm.schema.prisma index d734fb6..eae01f3 100644 --- a/packages/crm-service/prisma/crm.schema.prisma +++ b/packages/crm-service/prisma/crm.schema.prisma @@ -18,7 +18,54 @@ datasource db { } // -------------------------------------------------------- -// Contact - CRM-Kontakte (Personen & Organisationen) +// Company - Unternehmen (uebergeordnete Entity) +// -------------------------------------------------------- +model Company { + id String @id @default(uuid()) @db.Uuid + tenantId String @map("tenant_id") @db.Uuid + + name String @db.VarChar(200) + industry String? @db.VarChar(100) + + // Kontaktdaten + email String? @db.VarChar(255) + phone String? @db.VarChar(50) + website String? @db.VarChar(500) + + // Adresse + street String? @db.VarChar(200) + zip String? @db.VarChar(20) + city String? @db.VarChar(100) + state String? @db.VarChar(100) + country String? @default("DE") @db.VarChar(2) + + // Zusaetzlich + notes String? @db.Text + tags String[] @default([]) + + isActive Boolean @default(true) @map("is_active") + + // Audit-Trail + createdBy String @map("created_by") @db.Uuid + updatedBy String? @map("updated_by") @db.Uuid + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + // Relationen + contacts Contact[] + deals Deal[] + + @@index([tenantId]) + @@index([tenantId, name]) + @@index([tenantId, industry]) + @@index([tenantId, isActive]) + @@map("companies") + @@schema("app_crm") +} + +// -------------------------------------------------------- +// Contact - CRM-Kontakte (Personen) // -------------------------------------------------------- model Contact { id String @id @default(uuid()) @db.Uuid @@ -29,8 +76,10 @@ model Contact { firstName String? @map("first_name") @db.VarChar(100) lastName String? @map("last_name") @db.VarChar(100) - // Organisation + // Unternehmenszuordnung + companyId String? @map("company_id") @db.Uuid companyName String? @map("company_name") @db.VarChar(200) + position String? @db.VarChar(200) // Kontaktdaten email String? @db.VarChar(255) @@ -59,11 +108,13 @@ model Contact { updatedAt DateTime @updatedAt @map("updated_at") // Relationen + company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) activities Activity[] deals Deal[] @@index([tenantId]) @@index([tenantId, email]) + @@index([tenantId, companyId]) @@index([tenantId, companyName]) @@index([tenantId, lastName, firstName]) @@index([tenantId, isActive]) @@ -181,6 +232,7 @@ model Deal { pipelineId String @map("pipeline_id") @db.Uuid stageId String @map("stage_id") @db.Uuid contactId String? @map("contact_id") @db.Uuid + companyId String? @map("company_id") @db.Uuid title String @db.VarChar(500) value Decimal? @db.Decimal(15, 2) currency String @default("EUR") @db.VarChar(3) @@ -201,11 +253,13 @@ model Deal { pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade) stage PipelineStage @relation(fields: [stageId], references: [id]) contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull) + company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull) @@index([tenantId]) @@index([tenantId, pipelineId]) @@index([tenantId, stageId]) @@index([tenantId, contactId]) + @@index([tenantId, companyId]) @@index([tenantId, status]) @@map("deals") @@schema("app_crm") diff --git a/packages/crm-service/src/app.module.ts b/packages/crm-service/src/app.module.ts index 104f5d6..b57d756 100644 --- a/packages/crm-service/src/app.module.ts +++ b/packages/crm-service/src/app.module.ts @@ -12,6 +12,7 @@ import { ContactsModule } from './contacts/contacts.module'; import { ActivitiesModule } from './activities/activities.module'; import { PipelinesModule } from './pipelines/pipelines.module'; import { DealsModule } from './deals/deals.module'; +import { CompaniesModule } from './companies/companies.module'; @Module({ imports: [ @@ -27,6 +28,7 @@ import { DealsModule } from './deals/deals.module'; ActivitiesModule, PipelinesModule, DealsModule, + CompaniesModule, ], providers: [ { diff --git a/packages/crm-service/src/companies/companies.controller.ts b/packages/crm-service/src/companies/companies.controller.ts new file mode 100644 index 0000000..6d92de0 --- /dev/null +++ b/packages/crm-service/src/companies/companies.controller.ts @@ -0,0 +1,119 @@ +import { + Controller, + Get, + Post, + Patch, + Delete, + Body, + Param, + Query, + ParseUUIDPipe, + HttpCode, + HttpStatus, + UseGuards, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiBearerAuth, + ApiParam, + ApiQuery, +} from '@nestjs/swagger'; +import { CompaniesService } from './companies.service'; +import { CreateCompanyDto } from './dto/create-company.dto'; +import { UpdateCompanyDto } from './dto/update-company.dto'; +import { QueryCompaniesDto } from './dto/query-companies.dto'; +import { CurrentUser, JwtPayload } from '../common/decorators'; +import { TenantGuard } from '../auth/guards/tenant.guard'; +import { + paginatedResponse, + singleResponse, +} from '../common/dto/pagination.dto'; + +@ApiTags('Companies') +@ApiBearerAuth('access-token') +@UseGuards(TenantGuard) +@Controller('companies') +export class CompaniesController { + constructor(private readonly companiesService: CompaniesService) {} + + @Post() + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: 'Unternehmen erstellen' }) + async create( + @CurrentUser() user: JwtPayload, + @Body() dto: CreateCompanyDto, + ) { + const company = await this.companiesService.create( + user.tenantId!, + user.sub, + dto, + ); + return singleResponse(company); + } + + @Get() + @ApiOperation({ summary: 'Unternehmen auflisten (paginiert, suchbar)' }) + @ApiQuery({ name: 'page', required: false }) + @ApiQuery({ name: 'pageSize', required: false }) + @ApiQuery({ name: 'search', required: false }) + @ApiQuery({ name: 'industry', required: false }) + @ApiQuery({ name: 'sort', required: false }) + @ApiQuery({ name: 'order', required: false, enum: ['asc', 'desc'] }) + async findAll( + @CurrentUser() user: JwtPayload, + @Query() query: QueryCompaniesDto, + ) { + const result = await this.companiesService.findAll( + user.tenantId!, + query, + ); + return paginatedResponse( + result.data, + result.total, + result.page, + result.pageSize, + ); + } + + @Get(':id') + @ApiOperation({ + summary: 'Unternehmensdetails (inkl. Kontakte und Deals)', + }) + @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) + async findOne( + @CurrentUser() user: JwtPayload, + @Param('id', ParseUUIDPipe) id: string, + ) { + const company = await this.companiesService.findOne(user.tenantId!, id); + return singleResponse(company); + } + + @Patch(':id') + @ApiOperation({ summary: 'Unternehmen aktualisieren' }) + @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) + async update( + @CurrentUser() user: JwtPayload, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateCompanyDto, + ) { + const company = await this.companiesService.update( + user.tenantId!, + id, + user.sub, + dto, + ); + return singleResponse(company); + } + + @Delete(':id') + @ApiOperation({ summary: 'Unternehmen loeschen' }) + @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) + async remove( + @CurrentUser() user: JwtPayload, + @Param('id', ParseUUIDPipe) id: string, + ) { + const company = await this.companiesService.remove(user.tenantId!, id); + return singleResponse(company); + } +} diff --git a/packages/crm-service/src/companies/companies.module.ts b/packages/crm-service/src/companies/companies.module.ts new file mode 100644 index 0000000..aef5e48 --- /dev/null +++ b/packages/crm-service/src/companies/companies.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { CompaniesController } from './companies.controller'; +import { CompaniesService } from './companies.service'; +import { CrmPrismaModule } from '../prisma/crm-prisma.module'; + +@Module({ + imports: [CrmPrismaModule], + controllers: [CompaniesController], + providers: [CompaniesService], + exports: [CompaniesService], +}) +export class CompaniesModule {} diff --git a/packages/crm-service/src/companies/companies.service.ts b/packages/crm-service/src/companies/companies.service.ts new file mode 100644 index 0000000..faddc68 --- /dev/null +++ b/packages/crm-service/src/companies/companies.service.ts @@ -0,0 +1,145 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { CrmPrismaService } from '../prisma/crm-prisma.service'; +import { CreateCompanyDto } from './dto/create-company.dto'; +import { UpdateCompanyDto } from './dto/update-company.dto'; +import { QueryCompaniesDto } from './dto/query-companies.dto'; +import { Prisma } from '.prisma/crm-client'; + +@Injectable() +export class CompaniesService { + constructor(private readonly prisma: CrmPrismaService) {} + + async create(tenantId: string, userId: string, dto: CreateCompanyDto) { + return this.prisma.company.create({ + data: { + tenantId, + createdBy: userId, + name: dto.name, + industry: dto.industry, + email: dto.email, + phone: dto.phone, + website: dto.website, + street: dto.street, + zip: dto.zip, + city: dto.city, + state: dto.state, + country: dto.country, + notes: dto.notes, + tags: dto.tags ?? [], + isActive: dto.isActive ?? true, + }, + include: { + _count: { select: { contacts: true, deals: true } }, + }, + }); + } + + async findAll(tenantId: string, query: QueryCompaniesDto) { + const page = query.page ?? 1; + const pageSize = query.pageSize ?? 25; + + const where: Prisma.CompanyWhereInput = { tenantId }; + + if (query.industry) { + where.industry = { contains: query.industry, mode: 'insensitive' }; + } + + if (query.search) { + where.OR = [ + { name: { contains: query.search, mode: 'insensitive' } }, + { industry: { contains: query.search, mode: 'insensitive' } }, + { email: { contains: query.search, mode: 'insensitive' } }, + { city: { contains: query.search, mode: 'insensitive' } }, + ]; + } + + const allowedSortFields = [ + 'createdAt', + 'updatedAt', + 'name', + 'industry', + 'city', + ]; + const sortField = allowedSortFields.includes(query.sort ?? '') + ? (query.sort as string) + : 'createdAt'; + + const [data, total] = await Promise.all([ + this.prisma.company.findMany({ + where, + skip: (page - 1) * pageSize, + take: pageSize, + orderBy: { [sortField]: query.order ?? 'desc' }, + include: { + _count: { select: { contacts: true, deals: true } }, + }, + }), + this.prisma.company.count({ where }), + ]); + + return { data, total, page, pageSize }; + } + + async findOne(tenantId: string, id: string) { + const company = await this.prisma.company.findFirst({ + where: { id, tenantId }, + include: { + contacts: { + where: { isActive: true }, + orderBy: { createdAt: 'desc' }, + take: 20, + select: { + id: true, + firstName: true, + lastName: true, + email: true, + phone: true, + position: true, + isActive: true, + }, + }, + deals: { + orderBy: { createdAt: 'desc' }, + take: 10, + include: { + pipeline: { select: { id: true, name: true } }, + stage: { select: { id: true, name: true, color: true } }, + }, + }, + _count: { select: { contacts: true, deals: true } }, + }, + }); + + if (!company) { + throw new NotFoundException('Unternehmen nicht gefunden'); + } + + return company; + } + + async update( + tenantId: string, + id: string, + userId: string, + dto: UpdateCompanyDto, + ) { + await this.findOne(tenantId, id); + + return this.prisma.company.update({ + where: { id }, + data: { + ...dto, + updatedBy: userId, + }, + include: { + _count: { select: { contacts: true, deals: true } }, + }, + }); + } + + async remove(tenantId: string, id: string) { + await this.findOne(tenantId, id); + + return this.prisma.company.delete({ where: { id } }); + } +} diff --git a/packages/crm-service/src/companies/dto/create-company.dto.ts b/packages/crm-service/src/companies/dto/create-company.dto.ts new file mode 100644 index 0000000..84e8299 --- /dev/null +++ b/packages/crm-service/src/companies/dto/create-company.dto.ts @@ -0,0 +1,87 @@ +import { + IsString, + IsOptional, + IsBoolean, + IsEmail, + IsUrl, + IsArray, + MaxLength, +} from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +export class CreateCompanyDto { + @ApiProperty({ maxLength: 200 }) + @IsString() + @MaxLength(200) + name!: string; + + @ApiPropertyOptional({ maxLength: 100 }) + @IsOptional() + @IsString() + @MaxLength(100) + industry?: string; + + @ApiPropertyOptional({ maxLength: 255 }) + @IsOptional() + @IsEmail() + @MaxLength(255) + email?: string; + + @ApiPropertyOptional({ maxLength: 50 }) + @IsOptional() + @IsString() + @MaxLength(50) + phone?: string; + + @ApiPropertyOptional({ maxLength: 500 }) + @IsOptional() + @IsUrl() + @MaxLength(500) + website?: string; + + @ApiPropertyOptional({ maxLength: 200 }) + @IsOptional() + @IsString() + @MaxLength(200) + street?: string; + + @ApiPropertyOptional({ maxLength: 20 }) + @IsOptional() + @IsString() + @MaxLength(20) + zip?: string; + + @ApiPropertyOptional({ maxLength: 100 }) + @IsOptional() + @IsString() + @MaxLength(100) + city?: string; + + @ApiPropertyOptional({ maxLength: 100 }) + @IsOptional() + @IsString() + @MaxLength(100) + state?: string; + + @ApiPropertyOptional({ maxLength: 2, default: 'DE' }) + @IsOptional() + @IsString() + @MaxLength(2) + country?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + notes?: string; + + @ApiPropertyOptional({ type: [String] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + tags?: string[]; + + @ApiPropertyOptional({ default: true }) + @IsOptional() + @IsBoolean() + isActive?: boolean; +} diff --git a/packages/crm-service/src/companies/dto/query-companies.dto.ts b/packages/crm-service/src/companies/dto/query-companies.dto.ts new file mode 100644 index 0000000..8c6e153 --- /dev/null +++ b/packages/crm-service/src/companies/dto/query-companies.dto.ts @@ -0,0 +1,40 @@ +import { IsOptional, IsString, IsInt, Min, Max, IsIn } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class QueryCompaniesDto { + @ApiPropertyOptional({ default: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number; + + @ApiPropertyOptional({ default: 25, maximum: 100 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + pageSize?: number; + + @ApiPropertyOptional({ description: 'Suche in Name, Branche, E-Mail' }) + @IsOptional() + @IsString() + search?: string; + + @ApiPropertyOptional({ description: 'Filter nach Branche' }) + @IsOptional() + @IsString() + industry?: string; + + @ApiPropertyOptional({ default: 'createdAt' }) + @IsOptional() + @IsString() + sort?: string; + + @ApiPropertyOptional({ default: 'desc', enum: ['asc', 'desc'] }) + @IsOptional() + @IsIn(['asc', 'desc']) + order?: 'asc' | 'desc'; +} diff --git a/packages/crm-service/src/companies/dto/update-company.dto.ts b/packages/crm-service/src/companies/dto/update-company.dto.ts new file mode 100644 index 0000000..ebc1e09 --- /dev/null +++ b/packages/crm-service/src/companies/dto/update-company.dto.ts @@ -0,0 +1,88 @@ +import { + IsString, + IsOptional, + IsBoolean, + IsEmail, + IsUrl, + IsArray, + MaxLength, +} from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class UpdateCompanyDto { + @ApiPropertyOptional({ maxLength: 200 }) + @IsOptional() + @IsString() + @MaxLength(200) + name?: string; + + @ApiPropertyOptional({ maxLength: 100 }) + @IsOptional() + @IsString() + @MaxLength(100) + industry?: string; + + @ApiPropertyOptional({ maxLength: 255 }) + @IsOptional() + @IsEmail() + @MaxLength(255) + email?: string; + + @ApiPropertyOptional({ maxLength: 50 }) + @IsOptional() + @IsString() + @MaxLength(50) + phone?: string; + + @ApiPropertyOptional({ maxLength: 500 }) + @IsOptional() + @IsUrl() + @MaxLength(500) + website?: string; + + @ApiPropertyOptional({ maxLength: 200 }) + @IsOptional() + @IsString() + @MaxLength(200) + street?: string; + + @ApiPropertyOptional({ maxLength: 20 }) + @IsOptional() + @IsString() + @MaxLength(20) + zip?: string; + + @ApiPropertyOptional({ maxLength: 100 }) + @IsOptional() + @IsString() + @MaxLength(100) + city?: string; + + @ApiPropertyOptional({ maxLength: 100 }) + @IsOptional() + @IsString() + @MaxLength(100) + state?: string; + + @ApiPropertyOptional({ maxLength: 2 }) + @IsOptional() + @IsString() + @MaxLength(2) + country?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + notes?: string; + + @ApiPropertyOptional({ type: [String] }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + tags?: string[]; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + isActive?: boolean; +} diff --git a/packages/crm-service/src/contacts/contacts.service.ts b/packages/crm-service/src/contacts/contacts.service.ts index afb1a7d..e45e5d9 100644 --- a/packages/crm-service/src/contacts/contacts.service.ts +++ b/packages/crm-service/src/contacts/contacts.service.ts @@ -17,7 +17,9 @@ export class ContactsService { type: dto.type, firstName: dto.firstName, lastName: dto.lastName, + companyId: dto.companyId, companyName: dto.companyName, + position: dto.position, email: dto.email, phone: dto.phone, mobile: dto.mobile, @@ -71,6 +73,9 @@ export class ContactsService { skip: (page - 1) * pageSize, take: pageSize, orderBy: { [sortField]: query.order ?? 'desc' }, + include: { + company: { select: { id: true, name: true, industry: true } }, + }, }), this.prisma.contact.count({ where }), ]); @@ -81,7 +86,10 @@ export class ContactsService { async findOne(tenantId: string, id: string) { const contact = await this.prisma.contact.findFirst({ where: { id, tenantId }, - include: { activities: { orderBy: { createdAt: 'desc' }, take: 10 } }, + include: { + company: { select: { id: true, name: true, industry: true, city: true, website: true } }, + activities: { orderBy: { createdAt: 'desc' }, take: 10 }, + }, }); if (!contact) { diff --git a/packages/crm-service/src/contacts/dto/create-contact.dto.ts b/packages/crm-service/src/contacts/dto/create-contact.dto.ts index a2a0d31..9e42108 100644 --- a/packages/crm-service/src/contacts/dto/create-contact.dto.ts +++ b/packages/crm-service/src/contacts/dto/create-contact.dto.ts @@ -4,6 +4,7 @@ import { IsEnum, IsBoolean, IsArray, + IsUUID, MaxLength, IsEmail, IsUrl, @@ -33,12 +34,23 @@ export class CreateContactDto { @MaxLength(100) lastName?: string; + @ApiPropertyOptional({ description: 'UUID des zugeordneten Unternehmens' }) + @IsOptional() + @IsUUID() + companyId?: string; + @ApiPropertyOptional({ maxLength: 200 }) @IsOptional() @IsString() @MaxLength(200) companyName?: string; + @ApiPropertyOptional({ maxLength: 200, description: 'Position/Rolle im Unternehmen' }) + @IsOptional() + @IsString() + @MaxLength(200) + position?: string; + @ApiPropertyOptional({ maxLength: 255 }) @IsOptional() @IsEmail() diff --git a/packages/crm-service/src/deals/deals.controller.ts b/packages/crm-service/src/deals/deals.controller.ts index 3223d80..91954aa 100644 --- a/packages/crm-service/src/deals/deals.controller.ts +++ b/packages/crm-service/src/deals/deals.controller.ts @@ -29,7 +29,7 @@ import { singleResponse, } from '../common/dto/pagination.dto'; -@ApiTags('Deals') +@ApiTags('Vorgaenge (Deals)') @ApiBearerAuth('access-token') @UseGuards(TenantGuard) @Controller('deals') @@ -38,7 +38,7 @@ export class DealsController { @Post() @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: 'Deal erstellen' }) + @ApiOperation({ summary: 'Vorgang erstellen' }) async create( @CurrentUser() user: JwtPayload, @Body() dto: CreateDealDto, @@ -52,7 +52,7 @@ export class DealsController { } @Get() - @ApiOperation({ summary: 'Deals auflisten (paginiert, filterbar)' }) + @ApiOperation({ summary: 'Vorgaenge auflisten (paginiert, filterbar)' }) async findAll( @CurrentUser() user: JwtPayload, @Query() query: QueryDealsDto, @@ -67,7 +67,7 @@ export class DealsController { } @Get(':id') - @ApiOperation({ summary: 'Deal-Details abrufen' }) + @ApiOperation({ summary: 'Vorgangsdetails abrufen' }) @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) async findOne( @CurrentUser() user: JwtPayload, @@ -78,7 +78,7 @@ export class DealsController { } @Patch(':id') - @ApiOperation({ summary: 'Deal aktualisieren' }) + @ApiOperation({ summary: 'Vorgang aktualisieren' }) @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) async update( @CurrentUser() user: JwtPayload, @@ -95,7 +95,7 @@ export class DealsController { } @Delete(':id') - @ApiOperation({ summary: 'Deal loeschen' }) + @ApiOperation({ summary: 'Vorgang loeschen' }) @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) async remove( @CurrentUser() user: JwtPayload, diff --git a/packages/crm-service/src/deals/deals.service.ts b/packages/crm-service/src/deals/deals.service.ts index 411fba0..77647b4 100644 --- a/packages/crm-service/src/deals/deals.service.ts +++ b/packages/crm-service/src/deals/deals.service.ts @@ -35,12 +35,23 @@ export class DealsService { } } + // Unternehmen validieren (optional) + if (dto.companyId) { + const company = await this.prisma.company.findFirst({ + where: { id: dto.companyId, tenantId }, + }); + if (!company) { + throw new NotFoundException('Unternehmen nicht gefunden'); + } + } + return this.prisma.deal.create({ data: { tenantId, pipelineId: dto.pipelineId, stageId: dto.stageId, contactId: dto.contactId, + companyId: dto.companyId, title: dto.title, value: dto.value, currency: dto.currency ?? 'EUR', @@ -62,6 +73,12 @@ export class DealsService { companyName: true, }, }, + company: { + select: { + id: true, + name: true, + }, + }, }, }); } @@ -81,6 +98,9 @@ export class DealsService { if (query.contactId) { where.contactId = query.contactId; } + if (query.companyId) { + where.companyId = query.companyId; + } if (query.status) { where.status = query.status; } @@ -116,6 +136,12 @@ export class DealsService { companyName: true, }, }, + company: { + select: { + id: true, + name: true, + }, + }, }, }), this.prisma.deal.count({ where }), @@ -131,11 +157,12 @@ export class DealsService { pipeline: { include: { stages: { orderBy: { sortOrder: 'asc' } } } }, stage: true, contact: true, + company: true, }, }); if (!deal) { - throw new NotFoundException('Deal nicht gefunden'); + throw new NotFoundException('Vorgang nicht gefunden'); } return deal; diff --git a/packages/crm-service/src/deals/dto/create-deal.dto.ts b/packages/crm-service/src/deals/dto/create-deal.dto.ts index b83255c..ccfe1c1 100644 --- a/packages/crm-service/src/deals/dto/create-deal.dto.ts +++ b/packages/crm-service/src/deals/dto/create-deal.dto.ts @@ -30,6 +30,11 @@ export class CreateDealDto { @IsUUID() contactId?: string; + @ApiPropertyOptional({ format: 'uuid', description: 'Zugeordnetes Unternehmen' }) + @IsOptional() + @IsUUID() + companyId?: string; + @ApiProperty({ maxLength: 500 }) @IsString() @MaxLength(500) diff --git a/packages/crm-service/src/deals/dto/query-deals.dto.ts b/packages/crm-service/src/deals/dto/query-deals.dto.ts index 6474190..4ffe270 100644 --- a/packages/crm-service/src/deals/dto/query-deals.dto.ts +++ b/packages/crm-service/src/deals/dto/query-deals.dto.ts @@ -19,6 +19,11 @@ export class QueryDealsDto extends PaginationDto { @IsUUID() contactId?: string; + @ApiPropertyOptional({ format: 'uuid', description: 'Filter nach Unternehmen' }) + @IsOptional() + @IsUUID() + companyId?: string; + @ApiPropertyOptional({ enum: DealStatus }) @IsOptional() @IsEnum(DealStatus)