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 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-10 19:22:33 +01:00
parent f62d032480
commit c9e2c4a44c
4 changed files with 83 additions and 0 deletions

View file

@ -39,6 +39,13 @@ services:
- "traefik.http.routers.crm.entrypoints=web" - "traefik.http.routers.crm.entrypoints=web"
- "traefik.http.services.crm.loadbalancer.server.port=3100" - "traefik.http.services.crm.loadbalancer.server.port=3100"
- "traefik.http.routers.crm.middlewares=cors-api@file,security-headers@file" - "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: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3100/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3100/health"]
interval: 30s interval: 30s

View file

@ -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;
}

View file

@ -20,6 +20,7 @@ import {
import { PipelinesService } from './pipelines.service'; import { PipelinesService } from './pipelines.service';
import { CreatePipelineDto, CreatePipelineStageDto } from './dto/create-pipeline.dto'; import { CreatePipelineDto, CreatePipelineStageDto } from './dto/create-pipeline.dto';
import { UpdatePipelineDto } from './dto/update-pipeline.dto'; import { UpdatePipelineDto } from './dto/update-pipeline.dto';
import { UpdateStageDto } from './dto/update-stage.dto';
import { CurrentUser, JwtPayload } from '../common/decorators'; import { CurrentUser, JwtPayload } from '../common/decorators';
import { TenantGuard } from '../auth/guards/tenant.guard'; import { TenantGuard } from '../auth/guards/tenant.guard';
import { singleResponse } from '../common/dto/pagination.dto'; import { singleResponse } from '../common/dto/pagination.dto';
@ -116,6 +117,25 @@ export class PipelinesController {
return singleResponse(stage); 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') @Delete(':id/stages/:stageId')
@ApiOperation({ summary: 'Stufe aus Pipeline entfernen' }) @ApiOperation({ summary: 'Stufe aus Pipeline entfernen' })
@ApiParam({ name: 'id', type: 'string', format: 'uuid' }) @ApiParam({ name: 'id', type: 'string', format: 'uuid' })

View file

@ -2,6 +2,7 @@ import { Injectable, NotFoundException } from '@nestjs/common';
import { CrmPrismaService } from '../prisma/crm-prisma.service'; import { CrmPrismaService } from '../prisma/crm-prisma.service';
import { CreatePipelineDto } from './dto/create-pipeline.dto'; import { CreatePipelineDto } from './dto/create-pipeline.dto';
import { UpdatePipelineDto } from './dto/update-pipeline.dto'; import { UpdatePipelineDto } from './dto/update-pipeline.dto';
import { UpdateStageDto } from './dto/update-stage.dto';
@Injectable() @Injectable()
export class PipelinesService { 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) { async removeStage(tenantId: string, pipelineId: string, stageId: string) {
await this.findOne(tenantId, pipelineId); await this.findOne(tenantId, pipelineId);