mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 04: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 {
|
model Contact {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
|
@ -29,8 +76,10 @@ model Contact {
|
||||||
firstName String? @map("first_name") @db.VarChar(100)
|
firstName String? @map("first_name") @db.VarChar(100)
|
||||||
lastName String? @map("last_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)
|
companyName String? @map("company_name") @db.VarChar(200)
|
||||||
|
position String? @db.VarChar(200)
|
||||||
|
|
||||||
// Kontaktdaten
|
// Kontaktdaten
|
||||||
email String? @db.VarChar(255)
|
email String? @db.VarChar(255)
|
||||||
|
|
@ -59,11 +108,13 @@ model Contact {
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
// Relationen
|
// Relationen
|
||||||
|
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
|
||||||
activities Activity[]
|
activities Activity[]
|
||||||
deals Deal[]
|
deals Deal[]
|
||||||
|
|
||||||
@@index([tenantId])
|
@@index([tenantId])
|
||||||
@@index([tenantId, email])
|
@@index([tenantId, email])
|
||||||
|
@@index([tenantId, companyId])
|
||||||
@@index([tenantId, companyName])
|
@@index([tenantId, companyName])
|
||||||
@@index([tenantId, lastName, firstName])
|
@@index([tenantId, lastName, firstName])
|
||||||
@@index([tenantId, isActive])
|
@@index([tenantId, isActive])
|
||||||
|
|
@ -181,6 +232,7 @@ model Deal {
|
||||||
pipelineId String @map("pipeline_id") @db.Uuid
|
pipelineId String @map("pipeline_id") @db.Uuid
|
||||||
stageId String @map("stage_id") @db.Uuid
|
stageId String @map("stage_id") @db.Uuid
|
||||||
contactId String? @map("contact_id") @db.Uuid
|
contactId String? @map("contact_id") @db.Uuid
|
||||||
|
companyId String? @map("company_id") @db.Uuid
|
||||||
title String @db.VarChar(500)
|
title String @db.VarChar(500)
|
||||||
value Decimal? @db.Decimal(15, 2)
|
value Decimal? @db.Decimal(15, 2)
|
||||||
currency String @default("EUR") @db.VarChar(3)
|
currency String @default("EUR") @db.VarChar(3)
|
||||||
|
|
@ -201,11 +253,13 @@ model Deal {
|
||||||
pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
|
pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
|
||||||
stage PipelineStage @relation(fields: [stageId], references: [id])
|
stage PipelineStage @relation(fields: [stageId], references: [id])
|
||||||
contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull)
|
contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull)
|
||||||
|
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
@@index([tenantId])
|
@@index([tenantId])
|
||||||
@@index([tenantId, pipelineId])
|
@@index([tenantId, pipelineId])
|
||||||
@@index([tenantId, stageId])
|
@@index([tenantId, stageId])
|
||||||
@@index([tenantId, contactId])
|
@@index([tenantId, contactId])
|
||||||
|
@@index([tenantId, companyId])
|
||||||
@@index([tenantId, status])
|
@@index([tenantId, status])
|
||||||
@@map("deals")
|
@@map("deals")
|
||||||
@@schema("app_crm")
|
@@schema("app_crm")
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { ContactsModule } from './contacts/contacts.module';
|
||||||
import { ActivitiesModule } from './activities/activities.module';
|
import { ActivitiesModule } from './activities/activities.module';
|
||||||
import { PipelinesModule } from './pipelines/pipelines.module';
|
import { PipelinesModule } from './pipelines/pipelines.module';
|
||||||
import { DealsModule } from './deals/deals.module';
|
import { DealsModule } from './deals/deals.module';
|
||||||
|
import { CompaniesModule } from './companies/companies.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -27,6 +28,7 @@ import { DealsModule } from './deals/deals.module';
|
||||||
ActivitiesModule,
|
ActivitiesModule,
|
||||||
PipelinesModule,
|
PipelinesModule,
|
||||||
DealsModule,
|
DealsModule,
|
||||||
|
CompaniesModule,
|
||||||
],
|
],
|
||||||
providers: [
|
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,
|
type: dto.type,
|
||||||
firstName: dto.firstName,
|
firstName: dto.firstName,
|
||||||
lastName: dto.lastName,
|
lastName: dto.lastName,
|
||||||
|
companyId: dto.companyId,
|
||||||
companyName: dto.companyName,
|
companyName: dto.companyName,
|
||||||
|
position: dto.position,
|
||||||
email: dto.email,
|
email: dto.email,
|
||||||
phone: dto.phone,
|
phone: dto.phone,
|
||||||
mobile: dto.mobile,
|
mobile: dto.mobile,
|
||||||
|
|
@ -71,6 +73,9 @@ export class ContactsService {
|
||||||
skip: (page - 1) * pageSize,
|
skip: (page - 1) * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
orderBy: { [sortField]: query.order ?? 'desc' },
|
orderBy: { [sortField]: query.order ?? 'desc' },
|
||||||
|
include: {
|
||||||
|
company: { select: { id: true, name: true, industry: true } },
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
this.prisma.contact.count({ where }),
|
this.prisma.contact.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
@ -81,7 +86,10 @@ export class ContactsService {
|
||||||
async findOne(tenantId: string, id: string) {
|
async findOne(tenantId: string, id: string) {
|
||||||
const contact = await this.prisma.contact.findFirst({
|
const contact = await this.prisma.contact.findFirst({
|
||||||
where: { id, tenantId },
|
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) {
|
if (!contact) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
IsEnum,
|
IsEnum,
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsArray,
|
IsArray,
|
||||||
|
IsUUID,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
IsEmail,
|
IsEmail,
|
||||||
IsUrl,
|
IsUrl,
|
||||||
|
|
@ -33,12 +34,23 @@ export class CreateContactDto {
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'UUID des zugeordneten Unternehmens' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
companyId?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ maxLength: 200 })
|
@ApiPropertyOptional({ maxLength: 200 })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(200)
|
@MaxLength(200)
|
||||||
companyName?: string;
|
companyName?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ maxLength: 200, description: 'Position/Rolle im Unternehmen' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(200)
|
||||||
|
position?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ maxLength: 255 })
|
@ApiPropertyOptional({ maxLength: 255 })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import {
|
||||||
singleResponse,
|
singleResponse,
|
||||||
} from '../common/dto/pagination.dto';
|
} from '../common/dto/pagination.dto';
|
||||||
|
|
||||||
@ApiTags('Deals')
|
@ApiTags('Vorgaenge (Deals)')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
@UseGuards(TenantGuard)
|
@UseGuards(TenantGuard)
|
||||||
@Controller('deals')
|
@Controller('deals')
|
||||||
|
|
@ -38,7 +38,7 @@ export class DealsController {
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
@ApiOperation({ summary: 'Deal erstellen' })
|
@ApiOperation({ summary: 'Vorgang erstellen' })
|
||||||
async create(
|
async create(
|
||||||
@CurrentUser() user: JwtPayload,
|
@CurrentUser() user: JwtPayload,
|
||||||
@Body() dto: CreateDealDto,
|
@Body() dto: CreateDealDto,
|
||||||
|
|
@ -52,7 +52,7 @@ export class DealsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Deals auflisten (paginiert, filterbar)' })
|
@ApiOperation({ summary: 'Vorgaenge auflisten (paginiert, filterbar)' })
|
||||||
async findAll(
|
async findAll(
|
||||||
@CurrentUser() user: JwtPayload,
|
@CurrentUser() user: JwtPayload,
|
||||||
@Query() query: QueryDealsDto,
|
@Query() query: QueryDealsDto,
|
||||||
|
|
@ -67,7 +67,7 @@ export class DealsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@ApiOperation({ summary: 'Deal-Details abrufen' })
|
@ApiOperation({ summary: 'Vorgangsdetails abrufen' })
|
||||||
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
||||||
async findOne(
|
async findOne(
|
||||||
@CurrentUser() user: JwtPayload,
|
@CurrentUser() user: JwtPayload,
|
||||||
|
|
@ -78,7 +78,7 @@ export class DealsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@ApiOperation({ summary: 'Deal aktualisieren' })
|
@ApiOperation({ summary: 'Vorgang aktualisieren' })
|
||||||
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
||||||
async update(
|
async update(
|
||||||
@CurrentUser() user: JwtPayload,
|
@CurrentUser() user: JwtPayload,
|
||||||
|
|
@ -95,7 +95,7 @@ export class DealsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@ApiOperation({ summary: 'Deal loeschen' })
|
@ApiOperation({ summary: 'Vorgang loeschen' })
|
||||||
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
||||||
async remove(
|
async remove(
|
||||||
@CurrentUser() user: JwtPayload,
|
@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({
|
return this.prisma.deal.create({
|
||||||
data: {
|
data: {
|
||||||
tenantId,
|
tenantId,
|
||||||
pipelineId: dto.pipelineId,
|
pipelineId: dto.pipelineId,
|
||||||
stageId: dto.stageId,
|
stageId: dto.stageId,
|
||||||
contactId: dto.contactId,
|
contactId: dto.contactId,
|
||||||
|
companyId: dto.companyId,
|
||||||
title: dto.title,
|
title: dto.title,
|
||||||
value: dto.value,
|
value: dto.value,
|
||||||
currency: dto.currency ?? 'EUR',
|
currency: dto.currency ?? 'EUR',
|
||||||
|
|
@ -62,6 +73,12 @@ export class DealsService {
|
||||||
companyName: true,
|
companyName: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
company: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -81,6 +98,9 @@ export class DealsService {
|
||||||
if (query.contactId) {
|
if (query.contactId) {
|
||||||
where.contactId = query.contactId;
|
where.contactId = query.contactId;
|
||||||
}
|
}
|
||||||
|
if (query.companyId) {
|
||||||
|
where.companyId = query.companyId;
|
||||||
|
}
|
||||||
if (query.status) {
|
if (query.status) {
|
||||||
where.status = query.status;
|
where.status = query.status;
|
||||||
}
|
}
|
||||||
|
|
@ -116,6 +136,12 @@ export class DealsService {
|
||||||
companyName: true,
|
companyName: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
company: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.prisma.deal.count({ where }),
|
this.prisma.deal.count({ where }),
|
||||||
|
|
@ -131,11 +157,12 @@ export class DealsService {
|
||||||
pipeline: { include: { stages: { orderBy: { sortOrder: 'asc' } } } },
|
pipeline: { include: { stages: { orderBy: { sortOrder: 'asc' } } } },
|
||||||
stage: true,
|
stage: true,
|
||||||
contact: true,
|
contact: true,
|
||||||
|
company: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!deal) {
|
if (!deal) {
|
||||||
throw new NotFoundException('Deal nicht gefunden');
|
throw new NotFoundException('Vorgang nicht gefunden');
|
||||||
}
|
}
|
||||||
|
|
||||||
return deal;
|
return deal;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ export class CreateDealDto {
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
contactId?: string;
|
contactId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ format: 'uuid', description: 'Zugeordnetes Unternehmen' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
companyId?: string;
|
||||||
|
|
||||||
@ApiProperty({ maxLength: 500 })
|
@ApiProperty({ maxLength: 500 })
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(500)
|
@MaxLength(500)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ export class QueryDealsDto extends PaginationDto {
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
contactId?: string;
|
contactId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ format: 'uuid', description: 'Filter nach Unternehmen' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
companyId?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ enum: DealStatus })
|
@ApiPropertyOptional({ enum: DealStatus })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsEnum(DealStatus)
|
@IsEnum(DealStatus)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue