From c9e2c4a44c5c3540875278196ec5f2746877c634 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Tue, 10 Mar 2026 19:22:33 +0100 Subject: [PATCH] feat(crm): add PATCH endpoint for pipeline stages + HTTPS router - New PATCH /pipelines/:id/stages/:stageId endpoint to update stage name, color, and sortOrder - Added HTTPS (websecure) Traefik router for CRM service - Both requested by frontend developer via INSIGHT-CRM.md Co-Authored-By: Claude Opus 4.6 --- docker-compose.crm.yml | 7 +++++ .../src/pipelines/dto/update-stage.dto.ts | 29 +++++++++++++++++++ .../src/pipelines/pipelines.controller.ts | 20 +++++++++++++ .../src/pipelines/pipelines.service.ts | 27 +++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 packages/crm-service/src/pipelines/dto/update-stage.dto.ts diff --git a/docker-compose.crm.yml b/docker-compose.crm.yml index fa4afa4..28210f2 100644 --- a/docker-compose.crm.yml +++ b/docker-compose.crm.yml @@ -39,6 +39,13 @@ services: - "traefik.http.routers.crm.entrypoints=web" - "traefik.http.services.crm.loadbalancer.server.port=3100" - "traefik.http.routers.crm.middlewares=cors-api@file,security-headers@file" + # HTTPS Router + - "traefik.http.routers.crm-secure.rule=Host(`172.20.10.59`) && PathPrefix(`/api/v1/crm`)" + - "traefik.http.routers.crm-secure.priority=100" + - "traefik.http.routers.crm-secure.entrypoints=websecure" + - "traefik.http.routers.crm-secure.tls=true" + - "traefik.http.routers.crm-secure.service=crm" + - "traefik.http.routers.crm-secure.middlewares=cors-api@file,security-headers@file" healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3100/health"] interval: 30s diff --git a/packages/crm-service/src/pipelines/dto/update-stage.dto.ts b/packages/crm-service/src/pipelines/dto/update-stage.dto.ts new file mode 100644 index 0000000..775a0de --- /dev/null +++ b/packages/crm-service/src/pipelines/dto/update-stage.dto.ts @@ -0,0 +1,29 @@ +import { + IsString, + IsOptional, + IsInt, + Min, + MaxLength, + Matches, +} from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class UpdateStageDto { + @ApiPropertyOptional({ maxLength: 200 }) + @IsOptional() + @IsString() + @MaxLength(200) + name?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsInt() + @Min(0) + sortOrder?: number; + + @ApiPropertyOptional({ description: 'Hex-Farbcode, z.B. #3B82F6' }) + @IsOptional() + @IsString() + @Matches(/^#[0-9A-Fa-f]{6}$/) + color?: string; +} diff --git a/packages/crm-service/src/pipelines/pipelines.controller.ts b/packages/crm-service/src/pipelines/pipelines.controller.ts index 1ea158c..41d1a34 100644 --- a/packages/crm-service/src/pipelines/pipelines.controller.ts +++ b/packages/crm-service/src/pipelines/pipelines.controller.ts @@ -20,6 +20,7 @@ import { import { PipelinesService } from './pipelines.service'; import { CreatePipelineDto, CreatePipelineStageDto } from './dto/create-pipeline.dto'; import { UpdatePipelineDto } from './dto/update-pipeline.dto'; +import { UpdateStageDto } from './dto/update-stage.dto'; import { CurrentUser, JwtPayload } from '../common/decorators'; import { TenantGuard } from '../auth/guards/tenant.guard'; import { singleResponse } from '../common/dto/pagination.dto'; @@ -116,6 +117,25 @@ export class PipelinesController { return singleResponse(stage); } + @Patch(':id/stages/:stageId') + @ApiOperation({ summary: 'Stufe aktualisieren (Name, Farbe, Reihenfolge)' }) + @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) + @ApiParam({ name: 'stageId', type: 'string', format: 'uuid' }) + async updateStage( + @CurrentUser() user: JwtPayload, + @Param('id', ParseUUIDPipe) pipelineId: string, + @Param('stageId', ParseUUIDPipe) stageId: string, + @Body() dto: UpdateStageDto, + ) { + const stage = await this.pipelinesService.updateStage( + user.tenantId!, + pipelineId, + stageId, + dto, + ); + return singleResponse(stage); + } + @Delete(':id/stages/:stageId') @ApiOperation({ summary: 'Stufe aus Pipeline entfernen' }) @ApiParam({ name: 'id', type: 'string', format: 'uuid' }) diff --git a/packages/crm-service/src/pipelines/pipelines.service.ts b/packages/crm-service/src/pipelines/pipelines.service.ts index a09a552..cbe3fb0 100644 --- a/packages/crm-service/src/pipelines/pipelines.service.ts +++ b/packages/crm-service/src/pipelines/pipelines.service.ts @@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { CrmPrismaService } from '../prisma/crm-prisma.service'; import { CreatePipelineDto } from './dto/create-pipeline.dto'; import { UpdatePipelineDto } from './dto/update-pipeline.dto'; +import { UpdateStageDto } from './dto/update-stage.dto'; @Injectable() export class PipelinesService { @@ -98,6 +99,32 @@ export class PipelinesService { }); } + async updateStage( + tenantId: string, + pipelineId: string, + stageId: string, + dto: UpdateStageDto, + ) { + await this.findOne(tenantId, pipelineId); + + const stage = await this.prisma.pipelineStage.findFirst({ + where: { id: stageId, pipelineId }, + }); + + if (!stage) { + throw new NotFoundException('Pipeline-Stufe nicht gefunden'); + } + + return this.prisma.pipelineStage.update({ + where: { id: stageId }, + data: { + ...(dto.name !== undefined && { name: dto.name }), + ...(dto.sortOrder !== undefined && { sortOrder: dto.sortOrder }), + ...(dto.color !== undefined && { color: dto.color }), + }, + }); + } + async removeStage(tenantId: string, pipelineId: string, stageId: string) { await this.findOne(tenantId, pipelineId);