mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 01:56:39 +02:00
feat(crm): Vorgangsart (DealType) konfigurierbares Dropdown + Pipeline-Leerstate
- crm-service: Neues DealType-Model (deal_types Tabelle) mit name, color, sortOrder und Relation zu Deal.dealTypeId; Migration 20260313_deal_type - crm-service: Vollstaendiger CRUD REST-Endpoint /crm/deal-types (TenantGuard) - crm-service: CreateDealDto um optionales dealTypeId erweitert - frontend: DealType Interface, API (dealTypesApi), Hooks (useDealTypes/...) - frontend: CrmSettingsPage > Weitere Einstellungen > DealTypesConfig mit Farbpicker - frontend: DealFormModal: Vorgangsart-Dropdown + Hinweis bei leerer Pipeline-Liste Deployment: prisma migrate deploy && prisma generate im crm-service ausfuehren. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f72ac6cb90
commit
6bfce4af97
16 changed files with 742 additions and 1 deletions
26
Summarize.md
26
Summarize.md
|
|
@ -6,6 +6,32 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Aenderungen 2026-03-13 (8): Vorgangsart (DealType) + Pipeline-Leerstate
|
||||||
|
|
||||||
|
#### Backend (crm-service)
|
||||||
|
- `prisma/crm.schema.prisma` — Neues Model `DealType` (id, tenantId, name, color, sortOrder) mit `@@unique([tenantId, name])`, `@@map("deal_types")`; Relation `deals Deal[]`; Deal-Model um optionales Feld `dealTypeId String? @map("deal_type_id") @db.Uuid` + Relation `dealType DealType? @relation(...)` erweitert
|
||||||
|
- `prisma/migrations/20260313_deal_type/migration.sql` — Migration: CREATE TABLE `app_crm.deal_types`, ALTER TABLE `app_crm.deals` ADD COLUMN `deal_type_id`, Unique/Index/FK-Constraints
|
||||||
|
- `src/deal-types/dto/create-deal-type.dto.ts` — DTO: name (required), color (optional, Hex-Validierung), sortOrder (optional)
|
||||||
|
- `src/deal-types/dto/update-deal-type.dto.ts` — Alle Felder optional
|
||||||
|
- `src/deal-types/deal-types.service.ts` — CRUD-Service (findAll/create/update/remove) analog IndustriesService, Duplikat-Check, Nutzungs-Guard beim Loeschen
|
||||||
|
- `src/deal-types/deal-types.controller.ts` — REST-Controller: GET/POST/PATCH/DELETE unter `/crm/deal-types`, TenantGuard
|
||||||
|
- `src/deal-types/deal-types.module.ts` — NestJS-Modul
|
||||||
|
- `src/app.module.ts` — DealTypesModule registriert
|
||||||
|
- `src/deals/dto/create-deal.dto.ts` — `dealTypeId?: string` (UUID, optional) hinzugefuegt
|
||||||
|
- `src/deals/deals.service.ts` — `dealTypeId` beim Create an Prisma weitergegeben
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- `crm/types.ts` — Neues Interface `DealType` (id, tenantId, name, color, sortOrder, _count?); `CreateDealTypePayload`, `UpdateDealTypePayload`; `Deal.dealTypeId: string|null` + `dealType?: {...}|null`; `CreateDealPayload.dealTypeId?: string`
|
||||||
|
- `crm/api.ts` — `dealTypesApi` (list/create/update/delete auf `/crm/deal-types`) importiert; Typ-Imports erweitert
|
||||||
|
- `crm/hooks.ts` — `crmKeys.dealTypes`, `useDealTypes`, `useCreateDealType`, `useUpdateDealType`, `useDeleteDealType`
|
||||||
|
- `crm/settings/CrmSettingsPage.tsx` — `DealTypesConfig`-Komponente (analog IndustriesConfig mit Farbpicker, Sortierung); als erster Block im Tab "Weitere Einstellungen" platziert
|
||||||
|
- `crm/deals/DealFormModal.tsx` — Vorgangsart-Dropdown (select mit DealType-Optionen, "— Keine —" als Default); Pipeline-Leerstate-Hinweis mit Link zu CRM-Einstellungen; `dealTypeId` State + Payload-Uebergabe
|
||||||
|
|
||||||
|
#### Deployment-Hinweis
|
||||||
|
Nach Deploy: `npx prisma migrate deploy && npx prisma generate` im crm-service ausfuehren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Aenderungen 2026-03-13 (7): Profil-Bereich nach oben rechts verschoben (Topbar)
|
### Aenderungen 2026-03-13 (7): Profil-Bereich nach oben rechts verschoben (Topbar)
|
||||||
|
|
||||||
#### Frontend
|
#### Frontend
|
||||||
|
|
|
||||||
|
|
@ -444,6 +444,26 @@ enum ContractStatus {
|
||||||
@@schema("app_crm")
|
@@schema("app_crm")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// DealType - Vorgangsart (admin-konfigurierbar pro Tenant)
|
||||||
|
// --------------------------------------------------------
|
||||||
|
model DealType {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
tenantId String @map("tenant_id") @db.Uuid
|
||||||
|
name String @db.VarChar(100)
|
||||||
|
color String @default("#6B7280") @db.VarChar(7)
|
||||||
|
sortOrder Int @default(0) @map("sort_order")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
deals Deal[]
|
||||||
|
|
||||||
|
@@unique([tenantId, name])
|
||||||
|
@@index([tenantId])
|
||||||
|
@@map("deal_types")
|
||||||
|
@@schema("app_crm")
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Pipeline - Sales-Pipelines (konfigurierbar pro Tenant)
|
// Pipeline - Sales-Pipelines (konfigurierbar pro Tenant)
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
@ -515,6 +535,9 @@ model Deal {
|
||||||
closedAt DateTime? @map("closed_at")
|
closedAt DateTime? @map("closed_at")
|
||||||
notes String? @db.Text
|
notes String? @db.Text
|
||||||
|
|
||||||
|
// Vorgangsart
|
||||||
|
dealTypeId String? @map("deal_type_id") @db.Uuid
|
||||||
|
|
||||||
// Phase 1: Lost-Reason
|
// Phase 1: Lost-Reason
|
||||||
lostReason LostReason? @map("lost_reason")
|
lostReason LostReason? @map("lost_reason")
|
||||||
lostReasonText String? @map("lost_reason_text") @db.Text
|
lostReasonText String? @map("lost_reason_text") @db.Text
|
||||||
|
|
@ -531,6 +554,7 @@ model Deal {
|
||||||
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)
|
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
|
||||||
|
dealType DealType? @relation(fields: [dealTypeId], references: [id], onDelete: SetNull)
|
||||||
dealVouchers DealVoucher[]
|
dealVouchers DealVoucher[]
|
||||||
owners DealOwner[]
|
owners DealOwner[]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
-- Migration: 20260313_deal_type
|
||||||
|
-- Beschreibung: Vorgangsart (DealType) Tabelle + deal_type_id auf deals
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "app_crm"."deal_types" (
|
||||||
|
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"tenant_id" UUID NOT NULL,
|
||||||
|
"name" VARCHAR(100) NOT NULL,
|
||||||
|
"color" VARCHAR(7) NOT NULL DEFAULT '#6B7280',
|
||||||
|
"sort_order" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "deal_types_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AlterTable: deal_type_id auf deals
|
||||||
|
ALTER TABLE "app_crm"."deals" ADD COLUMN "deal_type_id" UUID;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "deal_types_tenant_id_name_key" ON "app_crm"."deal_types"("tenant_id", "name");
|
||||||
|
CREATE INDEX "deal_types_tenant_id_idx" ON "app_crm"."deal_types"("tenant_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "app_crm"."deals" ADD CONSTRAINT "deals_deal_type_id_fkey" FOREIGN KEY ("deal_type_id") REFERENCES "app_crm"."deal_types"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
@ -26,6 +26,7 @@ import { ImportModule } from './import/import.module';
|
||||||
import { EnrichmentModule } from './enrichment/enrichment.module';
|
import { EnrichmentModule } from './enrichment/enrichment.module';
|
||||||
import { ContractsModule } from './contracts/contracts.module';
|
import { ContractsModule } from './contracts/contracts.module';
|
||||||
import { GraphModule } from './graph/graph.module';
|
import { GraphModule } from './graph/graph.module';
|
||||||
|
import { DealTypesModule } from './deal-types/deal-types.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -55,6 +56,7 @@ import { GraphModule } from './graph/graph.module';
|
||||||
EnrichmentModule,
|
EnrichmentModule,
|
||||||
ContractsModule,
|
ContractsModule,
|
||||||
GraphModule,
|
GraphModule,
|
||||||
|
DealTypesModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
89
packages/crm-service/src/deal-types/deal-types.controller.ts
Normal file
89
packages/crm-service/src/deal-types/deal-types.controller.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiParam,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { DealTypesService } from './deal-types.service';
|
||||||
|
import { CreateDealTypeDto } from './dto/create-deal-type.dto';
|
||||||
|
import { UpdateDealTypeDto } from './dto/update-deal-type.dto';
|
||||||
|
import { CurrentUser, JwtPayload } from '../common/decorators';
|
||||||
|
import { TenantGuard } from '../auth/guards/tenant.guard';
|
||||||
|
import { singleResponse } from '../common/dto/pagination.dto';
|
||||||
|
|
||||||
|
@ApiTags('DealTypes')
|
||||||
|
@ApiBearerAuth('access-token')
|
||||||
|
@UseGuards(TenantGuard)
|
||||||
|
@Controller('deal-types')
|
||||||
|
export class DealTypesController {
|
||||||
|
constructor(private readonly dealTypesService: DealTypesService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({ summary: 'Vorgangsart erstellen' })
|
||||||
|
async create(
|
||||||
|
@CurrentUser() user: JwtPayload,
|
||||||
|
@Body() dto: CreateDealTypeDto,
|
||||||
|
) {
|
||||||
|
const dealType = await this.dealTypesService.create(user.tenantId!, dto);
|
||||||
|
return singleResponse(dealType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'Alle Vorgangsarten auflisten' })
|
||||||
|
async findAll(@CurrentUser() user: JwtPayload) {
|
||||||
|
const dealTypes = await this.dealTypesService.findAll(user.tenantId!);
|
||||||
|
return { data: dealTypes };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Vorgangsart abrufen' })
|
||||||
|
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
||||||
|
async findOne(
|
||||||
|
@CurrentUser() user: JwtPayload,
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
) {
|
||||||
|
const dealType = await this.dealTypesService.findOne(user.tenantId!, id);
|
||||||
|
return singleResponse(dealType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'Vorgangsart aktualisieren' })
|
||||||
|
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
||||||
|
async update(
|
||||||
|
@CurrentUser() user: JwtPayload,
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Body() dto: UpdateDealTypeDto,
|
||||||
|
) {
|
||||||
|
const dealType = await this.dealTypesService.update(
|
||||||
|
user.tenantId!,
|
||||||
|
id,
|
||||||
|
dto,
|
||||||
|
);
|
||||||
|
return singleResponse(dealType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'Vorgangsart loeschen' })
|
||||||
|
@ApiParam({ name: 'id', type: 'string', format: 'uuid' })
|
||||||
|
async remove(
|
||||||
|
@CurrentUser() user: JwtPayload,
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
) {
|
||||||
|
const dealType = await this.dealTypesService.remove(user.tenantId!, id);
|
||||||
|
return singleResponse(dealType);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/crm-service/src/deal-types/deal-types.module.ts
Normal file
10
packages/crm-service/src/deal-types/deal-types.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DealTypesController } from './deal-types.controller';
|
||||||
|
import { DealTypesService } from './deal-types.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [DealTypesController],
|
||||||
|
providers: [DealTypesService],
|
||||||
|
exports: [DealTypesService],
|
||||||
|
})
|
||||||
|
export class DealTypesModule {}
|
||||||
93
packages/crm-service/src/deal-types/deal-types.service.ts
Normal file
93
packages/crm-service/src/deal-types/deal-types.service.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
ConflictException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { CrmPrismaService } from '../prisma/crm-prisma.service';
|
||||||
|
import { CreateDealTypeDto } from './dto/create-deal-type.dto';
|
||||||
|
import { UpdateDealTypeDto } from './dto/update-deal-type.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DealTypesService {
|
||||||
|
constructor(private readonly prisma: CrmPrismaService) {}
|
||||||
|
|
||||||
|
async create(tenantId: string, dto: CreateDealTypeDto) {
|
||||||
|
const existing = await this.prisma.dealType.findUnique({
|
||||||
|
where: { tenantId_name: { tenantId, name: dto.name } },
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Vorgangsart "${dto.name}" existiert bereits`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.dealType.create({
|
||||||
|
data: {
|
||||||
|
tenantId,
|
||||||
|
name: dto.name,
|
||||||
|
color: dto.color ?? '#6B7280',
|
||||||
|
sortOrder: dto.sortOrder ?? 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(tenantId: string) {
|
||||||
|
return this.prisma.dealType.findMany({
|
||||||
|
where: { tenantId },
|
||||||
|
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }],
|
||||||
|
include: {
|
||||||
|
_count: { select: { deals: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(tenantId: string, id: string) {
|
||||||
|
const dealType = await this.prisma.dealType.findFirst({
|
||||||
|
where: { id, tenantId },
|
||||||
|
include: {
|
||||||
|
_count: { select: { deals: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dealType) {
|
||||||
|
throw new NotFoundException('Vorgangsart nicht gefunden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return dealType;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(tenantId: string, id: string, dto: UpdateDealTypeDto) {
|
||||||
|
await this.findOne(tenantId, id);
|
||||||
|
|
||||||
|
if (dto.name) {
|
||||||
|
const existing = await this.prisma.dealType.findFirst({
|
||||||
|
where: { tenantId, name: dto.name, NOT: { id } },
|
||||||
|
});
|
||||||
|
if (existing) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Vorgangsart "${dto.name}" existiert bereits`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.dealType.update({
|
||||||
|
where: { id },
|
||||||
|
data: dto,
|
||||||
|
include: {
|
||||||
|
_count: { select: { deals: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(tenantId: string, id: string) {
|
||||||
|
const dealType = await this.findOne(tenantId, id);
|
||||||
|
|
||||||
|
if (dealType._count.deals > 0) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Vorgangsart kann nicht geloescht werden — ${dealType._count.deals} Vorgang/Vorgaenge zugeordnet`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.prisma.dealType.delete({ where: { id } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsOptional,
|
||||||
|
IsInt,
|
||||||
|
MaxLength,
|
||||||
|
Matches,
|
||||||
|
Min,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CreateDealTypeDto {
|
||||||
|
@ApiProperty({ maxLength: 100, description: 'Name der Vorgangsart' })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
maxLength: 7,
|
||||||
|
default: '#6B7280',
|
||||||
|
description: 'Hex-Farbcode (z.B. #3B82F6)',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(7)
|
||||||
|
@Matches(/^#[0-9A-Fa-f]{6}$/, {
|
||||||
|
message: 'color muss ein gueltiger Hex-Farbcode sein (z.B. #3B82F6)',
|
||||||
|
})
|
||||||
|
color?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: 0, description: 'Sortierreihenfolge' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsOptional,
|
||||||
|
IsInt,
|
||||||
|
MaxLength,
|
||||||
|
Matches,
|
||||||
|
Min,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class UpdateDealTypeDto {
|
||||||
|
@ApiPropertyOptional({ maxLength: 100 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(100)
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ maxLength: 7 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(7)
|
||||||
|
@Matches(/^#[0-9A-Fa-f]{6}$/, {
|
||||||
|
message: 'color muss ein gueltiger Hex-Farbcode sein (z.B. #3B82F6)',
|
||||||
|
})
|
||||||
|
color?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
@ -74,6 +74,7 @@ export class DealsService {
|
||||||
notes: dto.notes,
|
notes: dto.notes,
|
||||||
lostReason: dto.lostReason,
|
lostReason: dto.lostReason,
|
||||||
lostReasonText: dto.lostReasonText,
|
lostReasonText: dto.lostReasonText,
|
||||||
|
dealTypeId: dto.dealTypeId,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
owners: {
|
owners: {
|
||||||
create: { tenantId, userId, role: 'OWNER' },
|
create: { tenantId, userId, role: 'OWNER' },
|
||||||
|
|
|
||||||
|
|
@ -84,4 +84,9 @@ export class CreateDealDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
lostReasonText?: string;
|
lostReasonText?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ format: 'uuid', description: 'Vorgangsart (konfigurierbar)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
dealTypeId?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ import type {
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
CreateRelationshipTypePayload,
|
CreateRelationshipTypePayload,
|
||||||
UpdateRelationshipTypePayload,
|
UpdateRelationshipTypePayload,
|
||||||
|
DealType,
|
||||||
|
CreateDealTypePayload,
|
||||||
|
UpdateDealTypePayload,
|
||||||
CompanyRelationship,
|
CompanyRelationship,
|
||||||
CreateCompanyRelationshipPayload,
|
CreateCompanyRelationshipPayload,
|
||||||
TenantUser,
|
TenantUser,
|
||||||
|
|
@ -392,6 +395,32 @@ export const relationshipTypesApi = {
|
||||||
.then((r) => r.data),
|
.then((r) => r.data),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Deal Types ---
|
||||||
|
|
||||||
|
export const dealTypesApi = {
|
||||||
|
list: () =>
|
||||||
|
api
|
||||||
|
.get<{ success: boolean; data: DealType[]; meta: { timestamp: string } }>(
|
||||||
|
'/crm/deal-types',
|
||||||
|
)
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
create: (data: CreateDealTypePayload) =>
|
||||||
|
api
|
||||||
|
.post<SingleResponse<DealType>>('/crm/deal-types', data)
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
update: (id: string, data: UpdateDealTypePayload) =>
|
||||||
|
api
|
||||||
|
.patch<SingleResponse<DealType>>(`/crm/deal-types/${id}`, data)
|
||||||
|
.then((r) => r.data),
|
||||||
|
|
||||||
|
delete: (id: string) =>
|
||||||
|
api
|
||||||
|
.delete<SingleResponse<DealType>>(`/crm/deal-types/${id}`)
|
||||||
|
.then((r) => r.data),
|
||||||
|
};
|
||||||
|
|
||||||
// --- Company Relationships ---
|
// --- Company Relationships ---
|
||||||
|
|
||||||
export const companyRelationshipsApi = {
|
export const companyRelationshipsApi = {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { Modal } from '../../components/Modal';
|
import { Modal } from '../../components/Modal';
|
||||||
import { useCreateDeal, useUpdateDeal, usePipelines, useSetCustomFieldValues } from '../hooks';
|
import { useCreateDeal, useUpdateDeal, usePipelines, useDealTypes, useSetCustomFieldValues } from '../hooks';
|
||||||
import { contactsApi, companiesApi } from '../api';
|
import { contactsApi, companiesApi } from '../api';
|
||||||
import { CustomFieldsForm } from '../CustomFieldsForm';
|
import { CustomFieldsForm } from '../CustomFieldsForm';
|
||||||
import type { Deal, DealStatus, LostReason, Contact, Company, CustomFieldValue } from '../types';
|
import type { Deal, DealStatus, LostReason, Contact, Company, CustomFieldValue } from '../types';
|
||||||
|
|
@ -68,6 +68,8 @@ export function DealFormModal({
|
||||||
|
|
||||||
const { data: pipelinesData } = usePipelines();
|
const { data: pipelinesData } = usePipelines();
|
||||||
const pipelines = pipelinesData?.data ?? [];
|
const pipelines = pipelinesData?.data ?? [];
|
||||||
|
const { data: dealTypesData } = useDealTypes();
|
||||||
|
const dealTypes = dealTypesData?.data ?? [];
|
||||||
|
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
|
|
@ -80,6 +82,7 @@ export function DealFormModal({
|
||||||
const [notes, setNotes] = useState('');
|
const [notes, setNotes] = useState('');
|
||||||
const [lostReason, setLostReason] = useState<LostReason | ''>('');
|
const [lostReason, setLostReason] = useState<LostReason | ''>('');
|
||||||
const [lostReasonText, setLostReasonText] = useState('');
|
const [lostReasonText, setLostReasonText] = useState('');
|
||||||
|
const [dealTypeId, setDealTypeId] = useState('');
|
||||||
|
|
||||||
// Kontakt-Suche
|
// Kontakt-Suche
|
||||||
const [contactSearch, setContactSearch] = useState('');
|
const [contactSearch, setContactSearch] = useState('');
|
||||||
|
|
@ -195,6 +198,7 @@ export function DealFormModal({
|
||||||
setNotes(deal.notes ?? '');
|
setNotes(deal.notes ?? '');
|
||||||
setLostReason((deal.lostReason as LostReason) ?? '');
|
setLostReason((deal.lostReason as LostReason) ?? '');
|
||||||
setLostReasonText(deal.lostReasonText ?? '');
|
setLostReasonText(deal.lostReasonText ?? '');
|
||||||
|
setDealTypeId(deal.dealTypeId ?? '');
|
||||||
if (deal.contact) {
|
if (deal.contact) {
|
||||||
const { id, firstName, lastName, companyName } = deal.contact;
|
const { id, firstName, lastName, companyName } = deal.contact;
|
||||||
const name =
|
const name =
|
||||||
|
|
@ -225,6 +229,7 @@ export function DealFormModal({
|
||||||
setNotes('');
|
setNotes('');
|
||||||
setLostReason('');
|
setLostReason('');
|
||||||
setLostReasonText('');
|
setLostReasonText('');
|
||||||
|
setDealTypeId('');
|
||||||
setSelectedContact(null);
|
setSelectedContact(null);
|
||||||
setContactSearch('');
|
setContactSearch('');
|
||||||
setSelectedCompany(null);
|
setSelectedCompany(null);
|
||||||
|
|
@ -278,6 +283,7 @@ export function DealFormModal({
|
||||||
...(notes ? { notes } : {}),
|
...(notes ? { notes } : {}),
|
||||||
...(status === 'LOST' && lostReason ? { lostReason } : {}),
|
...(status === 'LOST' && lostReason ? { lostReason } : {}),
|
||||||
...(status === 'LOST' && lostReasonText ? { lostReasonText } : {}),
|
...(status === 'LOST' && lostReasonText ? { lostReasonText } : {}),
|
||||||
|
...(dealTypeId ? { dealTypeId } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveCustomFields = (entityId: string) => {
|
const saveCustomFields = (entityId: string) => {
|
||||||
|
|
@ -360,6 +366,27 @@ export function DealFormModal({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pipeline leer – Hinweis */}
|
||||||
|
{pipelines.length === 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '0.75rem',
|
||||||
|
background: 'var(--color-warning-bg, #fffbeb)',
|
||||||
|
border: '1px solid var(--color-warning-border, #fde68a)',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
color: 'var(--color-warning, #92400e)',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
⚠️ Keine Pipelines vorhanden. Bitte zuerst unter{' '}
|
||||||
|
<a href="/crm/settings" style={{ color: 'inherit', fontWeight: 600 }}>
|
||||||
|
CRM Einstellungen → Pipelines
|
||||||
|
</a>{' '}
|
||||||
|
eine Pipeline anlegen.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Pipeline + Stage */}
|
{/* Pipeline + Stage */}
|
||||||
<div style={{ ...rowStyle, marginBottom: '1rem' }}>
|
<div style={{ ...rowStyle, marginBottom: '1rem' }}>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -398,6 +425,31 @@ export function DealFormModal({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Vorgangsart */}
|
||||||
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
|
<label style={labelStyle}>Vorgangsart</label>
|
||||||
|
<select
|
||||||
|
value={dealTypeId}
|
||||||
|
onChange={(e) => setDealTypeId(e.target.value)}
|
||||||
|
style={{ ...inputStyle, cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<option value="">— Keine —</option>
|
||||||
|
{dealTypes.map((dt) => (
|
||||||
|
<option key={dt.id} value={dt.id}>
|
||||||
|
{dt.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{dealTypes.length === 0 && (
|
||||||
|
<p style={{ fontSize: '0.8125rem', color: 'var(--color-text-muted)', marginTop: '0.25rem' }}>
|
||||||
|
Keine Vorgangsarten konfiguriert.{' '}
|
||||||
|
<a href="/crm/settings" style={{ color: 'var(--color-primary)' }}>
|
||||||
|
In CRM Einstellungen anlegen
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Kontakt-Suche */}
|
{/* Kontakt-Suche */}
|
||||||
<div style={{ marginBottom: '1rem', position: 'relative' }} ref={contactRef}>
|
<div style={{ marginBottom: '1rem', position: 'relative' }} ref={contactRef}>
|
||||||
<label style={labelStyle}>Kontakt</label>
|
<label style={labelStyle}>Kontakt</label>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
integrationsApi,
|
integrationsApi,
|
||||||
graphApi,
|
graphApi,
|
||||||
office365Api,
|
office365Api,
|
||||||
|
dealTypesApi,
|
||||||
} from './api';
|
} from './api';
|
||||||
import type {
|
import type {
|
||||||
ContactsQueryParams,
|
ContactsQueryParams,
|
||||||
|
|
@ -51,6 +52,8 @@ import type {
|
||||||
UpdateAccountTypePayload,
|
UpdateAccountTypePayload,
|
||||||
CreateRelationshipTypePayload,
|
CreateRelationshipTypePayload,
|
||||||
UpdateRelationshipTypePayload,
|
UpdateRelationshipTypePayload,
|
||||||
|
CreateDealTypePayload,
|
||||||
|
UpdateDealTypePayload,
|
||||||
CreateCompanyRelationshipPayload,
|
CreateCompanyRelationshipPayload,
|
||||||
LexwareContactSearchParams,
|
LexwareContactSearchParams,
|
||||||
LexwareVouchersQueryParams,
|
LexwareVouchersQueryParams,
|
||||||
|
|
@ -117,6 +120,10 @@ export const crmKeys = {
|
||||||
all: ['crm', 'relationshipTypes'] as const,
|
all: ['crm', 'relationshipTypes'] as const,
|
||||||
list: () => ['crm', 'relationshipTypes', 'list'] as const,
|
list: () => ['crm', 'relationshipTypes', 'list'] as const,
|
||||||
},
|
},
|
||||||
|
dealTypes: {
|
||||||
|
all: ['crm', 'dealTypes'] as const,
|
||||||
|
list: () => ['crm', 'dealTypes', 'list'] as const,
|
||||||
|
},
|
||||||
companyRelationships: {
|
companyRelationships: {
|
||||||
all: ['crm', 'companyRelationships'] as const,
|
all: ['crm', 'companyRelationships'] as const,
|
||||||
list: (companyId: string) =>
|
list: (companyId: string) =>
|
||||||
|
|
@ -639,6 +646,49 @@ export function useDeleteRelationshipType() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Deal Types (Vorgangsart)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
export function useDealTypes() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: crmKeys.dealTypes.list(),
|
||||||
|
queryFn: () => dealTypesApi.list(),
|
||||||
|
staleTime: 10 * 60 * 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateDealType() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (data: CreateDealTypePayload) => dealTypesApi.create(data),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: crmKeys.dealTypes.all });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateDealType() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, data }: { id: string; data: UpdateDealTypePayload }) =>
|
||||||
|
dealTypesApi.update(id, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: crmKeys.dealTypes.all });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteDealType() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => dealTypesApi.delete(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
qc.invalidateQueries({ queryKey: crmKeys.dealTypes.all });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Company Relationships
|
// Company Relationships
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ import {
|
||||||
useCreateRelationshipType,
|
useCreateRelationshipType,
|
||||||
useUpdateRelationshipType,
|
useUpdateRelationshipType,
|
||||||
useDeleteRelationshipType,
|
useDeleteRelationshipType,
|
||||||
|
useDealTypes,
|
||||||
|
useCreateDealType,
|
||||||
|
useUpdateDealType,
|
||||||
|
useDeleteDealType,
|
||||||
useCustomFieldDefs,
|
useCustomFieldDefs,
|
||||||
useCreateCustomFieldDef,
|
useCreateCustomFieldDef,
|
||||||
useUpdateCustomFieldDef,
|
useUpdateCustomFieldDef,
|
||||||
|
|
@ -29,6 +33,7 @@ import type {
|
||||||
Industry,
|
Industry,
|
||||||
AccountType,
|
AccountType,
|
||||||
RelationshipType,
|
RelationshipType,
|
||||||
|
DealType,
|
||||||
CustomFieldDef,
|
CustomFieldDef,
|
||||||
CustomFieldEntityType,
|
CustomFieldEntityType,
|
||||||
CustomFieldType,
|
CustomFieldType,
|
||||||
|
|
@ -688,6 +693,246 @@ function RelationshipTypesConfig() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// DealTypesConfig (Vorgangsart)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
function DealTypesConfig() {
|
||||||
|
const { data } = useDealTypes();
|
||||||
|
const createMut = useCreateDealType();
|
||||||
|
const updateMut = useUpdateDealType();
|
||||||
|
const deleteMut = useDeleteDealType();
|
||||||
|
|
||||||
|
const items: DealType[] = data?.data ?? [];
|
||||||
|
|
||||||
|
const [editId, setEditId] = useState<string | null>(null);
|
||||||
|
const [addMode, setAddMode] = useState(false);
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [color, setColor] = useState('#6B7280');
|
||||||
|
|
||||||
|
const startAdd = useCallback(() => {
|
||||||
|
setEditId(null);
|
||||||
|
setName('');
|
||||||
|
setColor('#6B7280');
|
||||||
|
setAddMode(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startEdit = useCallback((item: DealType) => {
|
||||||
|
setAddMode(false);
|
||||||
|
setEditId(item.id);
|
||||||
|
setName(item.name);
|
||||||
|
setColor(item.color);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cancel = useCallback(() => {
|
||||||
|
setEditId(null);
|
||||||
|
setAddMode(false);
|
||||||
|
setName('');
|
||||||
|
setColor('#6B7280');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSave = useCallback(() => {
|
||||||
|
if (!name.trim()) return;
|
||||||
|
if (addMode) {
|
||||||
|
createMut.mutate(
|
||||||
|
{ name: name.trim(), color },
|
||||||
|
{ onSuccess: cancel },
|
||||||
|
);
|
||||||
|
} else if (editId) {
|
||||||
|
updateMut.mutate(
|
||||||
|
{ id: editId, data: { name: name.trim(), color } },
|
||||||
|
{ onSuccess: cancel },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [addMode, editId, name, color, createMut, updateMut, cancel]);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
if (window.confirm('Vorgangsart wirklich löschen?')) {
|
||||||
|
deleteMut.mutate(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[deleteMut],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSort = useCallback(
|
||||||
|
(item: DealType, direction: 'up' | 'down') => {
|
||||||
|
const newOrder =
|
||||||
|
direction === 'up'
|
||||||
|
? Math.max(0, item.sortOrder - 1)
|
||||||
|
: item.sortOrder + 1;
|
||||||
|
updateMut.mutate({ id: item.id, data: { sortOrder: newOrder } });
|
||||||
|
},
|
||||||
|
[updateMut],
|
||||||
|
);
|
||||||
|
|
||||||
|
const isSaving = createMut.isPending || updateMut.isPending;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.card}>
|
||||||
|
<div className={styles.configHeader}>
|
||||||
|
<div>
|
||||||
|
<h2 className={styles.cardTitle}>Vorgangsarten</h2>
|
||||||
|
<p className={styles.cardDesc} style={{ marginBottom: 0 }}>
|
||||||
|
Kategorien für Vorgänge (z.B. Neukunde, Nachkauf, Partneranfrage). Farben werden als Badge angezeigt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button className={styles.addBtn} onClick={startAdd} disabled={addMode}>
|
||||||
|
<PlusIcon /> Hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table className={styles.configTable}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: 40 }}>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th style={{ width: 60 }}>Farbe</th>
|
||||||
|
<th style={{ width: 100, textAlign: 'right' }}>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{addMode && (
|
||||||
|
<tr>
|
||||||
|
<td>—</td>
|
||||||
|
<td>
|
||||||
|
<div className={styles.inlineForm}>
|
||||||
|
<input
|
||||||
|
className={styles.inlineInput}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="Vorgangsart-Name"
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleSave();
|
||||||
|
if (e.key === 'Escape') cancel();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className={styles.colorInput}
|
||||||
|
value={color}
|
||||||
|
onChange={(e) => setColor(e.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className={styles.actionsCell}>
|
||||||
|
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button className={styles.cancelBtn} onClick={cancel}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{items.length === 0 && !addMode && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={4} className={styles.emptyRow}>
|
||||||
|
Noch keine Vorgangsarten definiert
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{items.map((item, idx) =>
|
||||||
|
editId === item.id ? (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>{idx + 1}</td>
|
||||||
|
<td>
|
||||||
|
<div className={styles.inlineForm}>
|
||||||
|
<input
|
||||||
|
className={styles.inlineInput}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') handleSave();
|
||||||
|
if (e.key === 'Escape') cancel();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className={styles.colorInput}
|
||||||
|
value={color}
|
||||||
|
onChange={(e) => setColor(e.target.value)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className={styles.actionsCell}>
|
||||||
|
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button className={styles.cancelBtn} onClick={cancel}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>
|
||||||
|
<div className={styles.sortBtns}>
|
||||||
|
<button
|
||||||
|
className={styles.sortBtn}
|
||||||
|
onClick={() => handleSort(item, 'up')}
|
||||||
|
disabled={idx === 0}
|
||||||
|
title="Nach oben"
|
||||||
|
>
|
||||||
|
<ArrowUpIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.sortBtn}
|
||||||
|
onClick={() => handleSort(item, 'down')}
|
||||||
|
disabled={idx === items.length - 1}
|
||||||
|
title="Nach unten"
|
||||||
|
>
|
||||||
|
<ArrowDownIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{item.name}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
className={styles.colorDot}
|
||||||
|
style={{ backgroundColor: item.color }}
|
||||||
|
title={item.color}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className={styles.actionsCell}>
|
||||||
|
<button
|
||||||
|
className={styles.iconBtn}
|
||||||
|
onClick={() => startEdit(item)}
|
||||||
|
title="Bearbeiten"
|
||||||
|
>
|
||||||
|
<PencilIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.iconBtn} ${styles.iconBtnDanger}`}
|
||||||
|
onClick={() => handleDelete(item.id)}
|
||||||
|
title="Löschen"
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// CustomFieldsConfig (Phase 2.1)
|
// CustomFieldsConfig (Phase 2.1)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -1640,6 +1885,7 @@ export function CrmSettingsPage() {
|
||||||
{/* Tab: Weitere Einstellungen */}
|
{/* Tab: Weitere Einstellungen */}
|
||||||
{activeTab === 'settings' && (
|
{activeTab === 'settings' && (
|
||||||
<>
|
<>
|
||||||
|
<DealTypesConfig />
|
||||||
<IndustriesConfig />
|
<IndustriesConfig />
|
||||||
<AccountTypesConfig />
|
<AccountTypesConfig />
|
||||||
<RelationshipTypesConfig />
|
<RelationshipTypesConfig />
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,25 @@ export interface RelationshipType {
|
||||||
_count?: { relationships: number };
|
_count?: { relationships: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DealType {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
sortOrder: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
_count?: { deals: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDealTypePayload {
|
||||||
|
name: string;
|
||||||
|
color?: string;
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateDealTypePayload = Partial<CreateDealTypePayload>;
|
||||||
|
|
||||||
export interface CompanyRelationship {
|
export interface CompanyRelationship {
|
||||||
id: string;
|
id: string;
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
|
|
@ -437,6 +456,7 @@ export interface Deal {
|
||||||
status: DealStatus;
|
status: DealStatus;
|
||||||
expectedCloseDate: string | null;
|
expectedCloseDate: string | null;
|
||||||
closedAt: string | null;
|
closedAt: string | null;
|
||||||
|
dealTypeId: string | null;
|
||||||
lostReason: LostReason | null;
|
lostReason: LostReason | null;
|
||||||
lostReasonText: string | null;
|
lostReasonText: string | null;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
|
|
@ -448,6 +468,7 @@ export interface Deal {
|
||||||
customFields?: CustomFieldValue[];
|
customFields?: CustomFieldValue[];
|
||||||
pipeline?: { id: string; name: string; stages?: PipelineStage[] };
|
pipeline?: { id: string; name: string; stages?: PipelineStage[] };
|
||||||
stage?: { id: string; name: string; color: string };
|
stage?: { id: string; name: string; color: string };
|
||||||
|
dealType?: { id: string; name: string; color: string } | null;
|
||||||
contact?: {
|
contact?: {
|
||||||
id: string;
|
id: string;
|
||||||
firstName: string | null;
|
firstName: string | null;
|
||||||
|
|
@ -471,6 +492,7 @@ export interface CreateDealPayload {
|
||||||
notes?: string;
|
notes?: string;
|
||||||
lostReason?: LostReason;
|
lostReason?: LostReason;
|
||||||
lostReasonText?: string;
|
lostReasonText?: string;
|
||||||
|
dealTypeId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateDealPayload = Partial<CreateDealPayload>;
|
export type UpdateDealPayload = Partial<CreateDealPayload>;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue