mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-24 23:56:40 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
3d8f568c9a
commit
56a9ed9647
14 changed files with 614 additions and 10 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
|||
119
packages/crm-service/src/companies/companies.controller.ts
Normal file
119
packages/crm-service/src/companies/companies.controller.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
12
packages/crm-service/src/companies/companies.module.ts
Normal file
12
packages/crm-service/src/companies/companies.module.ts
Normal file
|
|
@ -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 {}
|
||||
145
packages/crm-service/src/companies/companies.service.ts
Normal file
145
packages/crm-service/src/companies/companies.service.ts
Normal file
|
|
@ -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 } });
|
||||
}
|
||||
}
|
||||
87
packages/crm-service/src/companies/dto/create-company.dto.ts
Normal file
87
packages/crm-service/src/companies/dto/create-company.dto.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
88
packages/crm-service/src/companies/dto/update-company.dto.ts
Normal file
88
packages/crm-service/src/companies/dto/update-company.dto.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue