mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
feat(crm): Projektanfrage-Vorgangstyp – isProjectType + ProjectRequestDetails
Backend: - DealType.isProjectType Boolean-Flag (Admin-konfigurierbar in CRM Settings) - Neue 1:1-Tabelle project_request_details (ON DELETE CASCADE): notes, workload, startDate, duration, onsitePercent, rateRemote, rateOnsite - Migration 20260313_project_request - ProjectRequestDto mit Validierung (0-100% fuer Auslastung/Vorort-Anteil) - Deals-Service: nested create + upsert fuer projectRequest Frontend: - DealFormModal: Vorgangsart-Dropdown an Anfang verschoben; Projektanfrage-Sektion erscheint conditional bei isProjectType=true (Beschreibung, Auslastung/Start, Laufzeit/Vorort-Anteil, Stundensaetze) - CrmSettingsPage: DealTypesConfig mit Projektanfrage-Checkbox + Tabellenspalte - types.ts: ProjectRequestDetails, CreateProjectRequestPayload, Deal.projectRequest Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6bfce4af97
commit
4c739945f0
11 changed files with 494 additions and 46 deletions
23
Summarize.md
23
Summarize.md
|
|
@ -6,6 +6,29 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Aenderungen 2026-03-13 (9): Projektanfrage-Vorgangstyp (isProjectType + ProjectRequestDetails)
|
||||||
|
|
||||||
|
#### Backend (crm-service)
|
||||||
|
- `prisma/crm.schema.prisma` — `DealType` um `isProjectType Boolean @default(false)` erweitert; neues 1:1-Model `ProjectRequestDetails` (notes, workload, startDate, duration, onsitePercent, rateRemote, rateOnsite) mit `ON DELETE CASCADE` zu `Deal`; `Deal.projectRequest ProjectRequestDetails?` Relation hinzugefuegt
|
||||||
|
- `prisma/migrations/20260313_project_request/migration.sql` — `ALTER TABLE deal_types ADD COLUMN is_project_type`; `CREATE TABLE project_request_details` mit Unique-Index auf deal_id und FK mit CASCADE
|
||||||
|
- `src/deal-types/dto/create-deal-type.dto.ts` — `isProjectType?: boolean` (`@IsBoolean`) hinzugefuegt
|
||||||
|
- `src/deal-types/dto/update-deal-type.dto.ts` — `isProjectType?: boolean` hinzugefuegt
|
||||||
|
- `src/deals/dto/project-request.dto.ts` — Neues DTO: notes, workload (0-100%), startDate, duration, onsitePercent (0-100%), rateRemote, rateOnsite
|
||||||
|
- `src/deals/dto/create-deal.dto.ts` — `projectRequest?: ProjectRequestDto` mit `@ValidateNested()` + `@Type(()=>ProjectRequestDto)` hinzugefuegt
|
||||||
|
- `src/deals/deals.service.ts` — `create()`: Nested `projectRequest.create` + `include: {projectRequest:true}`; `findOne()`: `include: {projectRequest:true}`; `update()`: `projectRequest` aus Destructuring extrahiert, `upsert` vor Deal-Update, `include: {projectRequest:true}`
|
||||||
|
|
||||||
|
#### Frontend
|
||||||
|
- `crm/types.ts` — `DealType.isProjectType: boolean`; `CreateDealTypePayload.isProjectType?`; neues Interface `ProjectRequestDetails`; `CreateProjectRequestPayload`; `Deal.projectRequest?`; `Deal.dealType.isProjectType` ergaenzt; `CreateDealPayload.projectRequest?`
|
||||||
|
- `crm/deals/DealFormModal.tsx` — Vorgangsart-Dropdown an Anfang des Formulars; Projektanfrage-Sektion (grau hinterlegt, blauer Titel) erscheint conditional bei `isProjectType=true`: Beschreibung, Auslastung/Start-Zeile, Laufzeit/Vorort-Anteil-Zeile, Stundensatz Remote/Vorort-Zeile; Initialisierung aus `deal.projectRequest` beim Bearbeiten; Payload-Zusammenbau mit `projectRequest`-Objekt
|
||||||
|
- `crm/settings/CrmSettingsPage.tsx` — `DealTypesConfig`: `isProjectType` State + Spalte "Projektanfrage" in Tabelle (Checkbox im Edit-/Add-Modus, ✓/— im View-Modus)
|
||||||
|
|
||||||
|
#### Deployment-Hinweis (Schritt 9)
|
||||||
|
- Server: `prisma migrate deploy --schema prisma/crm.schema.prisma`
|
||||||
|
- Server: `prisma generate --schema prisma/crm.schema.prisma`
|
||||||
|
- Rebuild + Restart: crm-service + frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### Aenderungen 2026-03-13 (8): Vorgangsart (DealType) + Pipeline-Leerstate
|
### Aenderungen 2026-03-13 (8): Vorgangsart (DealType) + Pipeline-Leerstate
|
||||||
|
|
||||||
#### Backend (crm-service)
|
#### Backend (crm-service)
|
||||||
|
|
|
||||||
|
|
@ -448,13 +448,14 @@ enum ContractStatus {
|
||||||
// DealType - Vorgangsart (admin-konfigurierbar pro Tenant)
|
// DealType - Vorgangsart (admin-konfigurierbar pro Tenant)
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
model DealType {
|
model DealType {
|
||||||
id String @id @default(uuid()) @db.Uuid
|
id String @id @default(uuid()) @db.Uuid
|
||||||
tenantId String @map("tenant_id") @db.Uuid
|
tenantId String @map("tenant_id") @db.Uuid
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
color String @default("#6B7280") @db.VarChar(7)
|
color String @default("#6B7280") @db.VarChar(7)
|
||||||
sortOrder Int @default(0) @map("sort_order")
|
sortOrder Int @default(0) @map("sort_order")
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
isProjectType Boolean @default(false) @map("is_project_type")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
deals Deal[]
|
deals Deal[]
|
||||||
|
|
||||||
|
|
@ -550,13 +551,14 @@ model Deal {
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
// Relationen
|
// Relationen
|
||||||
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)
|
company Company? @relation(fields: [companyId], references: [id], onDelete: SetNull)
|
||||||
dealType DealType? @relation(fields: [dealTypeId], references: [id], onDelete: SetNull)
|
dealType DealType? @relation(fields: [dealTypeId], references: [id], onDelete: SetNull)
|
||||||
dealVouchers DealVoucher[]
|
dealVouchers DealVoucher[]
|
||||||
owners DealOwner[]
|
owners DealOwner[]
|
||||||
|
projectRequest ProjectRequestDetails?
|
||||||
|
|
||||||
@@index([tenantId])
|
@@index([tenantId])
|
||||||
@@index([tenantId, pipelineId])
|
@@index([tenantId, pipelineId])
|
||||||
|
|
@ -576,6 +578,28 @@ enum DealStatus {
|
||||||
@@schema("app_crm")
|
@@schema("app_crm")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// ProjectRequestDetails - Projektanfrage-Zusatzfelder (1:1 mit Deal)
|
||||||
|
// --------------------------------------------------------
|
||||||
|
model ProjectRequestDetails {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
dealId String @unique @map("deal_id") @db.Uuid
|
||||||
|
notes String? @db.Text
|
||||||
|
workload Decimal? @db.Decimal(5, 2)
|
||||||
|
startDate DateTime? @map("start_date")
|
||||||
|
duration String? @db.VarChar(200)
|
||||||
|
onsitePercent Decimal? @map("onsite_percent") @db.Decimal(5, 2)
|
||||||
|
rateRemote Decimal? @map("rate_remote") @db.Decimal(10, 2)
|
||||||
|
rateOnsite Decimal? @map("rate_onsite") @db.Decimal(10, 2)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
deal Deal @relation(fields: [dealId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@map("project_request_details")
|
||||||
|
@@schema("app_crm")
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Lexware Office Integration - Voucher Types
|
// Lexware Office Integration - Voucher Types
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
-- Migration: 20260313_project_request
|
||||||
|
-- Beschreibung: isProjectType auf deal_types + ProjectRequestDetails Tabelle
|
||||||
|
|
||||||
|
-- AlterTable: is_project_type auf deal_types
|
||||||
|
ALTER TABLE "app_crm"."deal_types" ADD COLUMN "is_project_type" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- CreateTable: project_request_details (1:1 mit deals, ON DELETE CASCADE)
|
||||||
|
CREATE TABLE "app_crm"."project_request_details" (
|
||||||
|
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||||
|
"deal_id" UUID NOT NULL,
|
||||||
|
"notes" TEXT,
|
||||||
|
"workload" DECIMAL(5,2),
|
||||||
|
"start_date" TIMESTAMP(3),
|
||||||
|
"duration" VARCHAR(200),
|
||||||
|
"onsite_percent" DECIMAL(5,2),
|
||||||
|
"rate_remote" DECIMAL(10,2),
|
||||||
|
"rate_onsite" DECIMAL(10,2),
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "project_request_details_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex (unique: jeder Deal max. 1 Projektanfrage)
|
||||||
|
CREATE UNIQUE INDEX "project_request_details_deal_id_key" ON "app_crm"."project_request_details"("deal_id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "app_crm"."project_request_details"
|
||||||
|
ADD CONSTRAINT "project_request_details_deal_id_fkey"
|
||||||
|
FOREIGN KEY ("deal_id")
|
||||||
|
REFERENCES "app_crm"."deals"("id")
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
IsString,
|
IsString,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsInt,
|
IsInt,
|
||||||
|
IsBoolean,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
Matches,
|
Matches,
|
||||||
Min,
|
Min,
|
||||||
|
|
@ -32,4 +33,12 @@ export class CreateDealTypeDto {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
default: false,
|
||||||
|
description: 'Spezialtyp: Projektanfrage (zeigt zusaetzliche Felder im Vorgangs-Formular)',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isProjectType?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
IsString,
|
IsString,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsInt,
|
IsInt,
|
||||||
|
IsBoolean,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
Matches,
|
Matches,
|
||||||
Min,
|
Min,
|
||||||
|
|
@ -29,4 +30,9 @@ export class UpdateDealTypeDto {
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Spezialtyp: Projektanfrage' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isProjectType?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,23 @@ export class DealsService {
|
||||||
owners: {
|
owners: {
|
||||||
create: { tenantId, userId, role: 'OWNER' },
|
create: { tenantId, userId, role: 'OWNER' },
|
||||||
},
|
},
|
||||||
|
...(dto.projectRequest
|
||||||
|
? {
|
||||||
|
projectRequest: {
|
||||||
|
create: {
|
||||||
|
notes: dto.projectRequest.notes,
|
||||||
|
workload: dto.projectRequest.workload,
|
||||||
|
startDate: dto.projectRequest.startDate
|
||||||
|
? new Date(dto.projectRequest.startDate)
|
||||||
|
: undefined,
|
||||||
|
duration: dto.projectRequest.duration,
|
||||||
|
onsitePercent: dto.projectRequest.onsitePercent,
|
||||||
|
rateRemote: dto.projectRequest.rateRemote,
|
||||||
|
rateOnsite: dto.projectRequest.rateOnsite,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
pipeline: { select: { id: true, name: true } },
|
pipeline: { select: { id: true, name: true } },
|
||||||
|
|
@ -98,6 +115,7 @@ export class DealsService {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
owners: true,
|
owners: true,
|
||||||
|
projectRequest: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -184,6 +202,7 @@ export class DealsService {
|
||||||
contact: true,
|
contact: true,
|
||||||
company: true,
|
company: true,
|
||||||
owners: true,
|
owners: true,
|
||||||
|
projectRequest: true,
|
||||||
dealVouchers: {
|
dealVouchers: {
|
||||||
include: {
|
include: {
|
||||||
voucher: {
|
voucher: {
|
||||||
|
|
@ -248,7 +267,7 @@ export class DealsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { lostReason, lostReasonText, ...restDto } = dto;
|
const { lostReason, lostReasonText, projectRequest, ...restDto } = dto;
|
||||||
|
|
||||||
const updateData: Prisma.DealUpdateInput = {
|
const updateData: Prisma.DealUpdateInput = {
|
||||||
...restDto,
|
...restDto,
|
||||||
|
|
@ -277,6 +296,36 @@ export class DealsService {
|
||||||
updateData.closedAt = new Date();
|
updateData.closedAt = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProjectRequest upsert (vor Deal-Update damit Include aktuelle Daten liefert)
|
||||||
|
if (projectRequest !== undefined && projectRequest !== null) {
|
||||||
|
await this.prisma.projectRequestDetails.upsert({
|
||||||
|
where: { dealId: id },
|
||||||
|
create: {
|
||||||
|
dealId: id,
|
||||||
|
notes: projectRequest.notes,
|
||||||
|
workload: projectRequest.workload,
|
||||||
|
startDate: projectRequest.startDate
|
||||||
|
? new Date(projectRequest.startDate)
|
||||||
|
: undefined,
|
||||||
|
duration: projectRequest.duration,
|
||||||
|
onsitePercent: projectRequest.onsitePercent,
|
||||||
|
rateRemote: projectRequest.rateRemote,
|
||||||
|
rateOnsite: projectRequest.rateOnsite,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
notes: projectRequest.notes,
|
||||||
|
workload: projectRequest.workload,
|
||||||
|
startDate: projectRequest.startDate
|
||||||
|
? new Date(projectRequest.startDate)
|
||||||
|
: undefined,
|
||||||
|
duration: projectRequest.duration,
|
||||||
|
onsitePercent: projectRequest.onsitePercent,
|
||||||
|
rateRemote: projectRequest.rateRemote,
|
||||||
|
rateOnsite: projectRequest.rateOnsite,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const updated = await this.prisma.deal.update({
|
const updated = await this.prisma.deal.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: updateData,
|
data: updateData,
|
||||||
|
|
@ -298,6 +347,7 @@ export class DealsService {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
owners: true,
|
owners: true,
|
||||||
|
projectRequest: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,11 @@ import {
|
||||||
IsEnum,
|
IsEnum,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
Min,
|
Min,
|
||||||
|
ValidateNested,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { ProjectRequestDto } from './project-request.dto';
|
||||||
|
|
||||||
export enum DealStatus {
|
export enum DealStatus {
|
||||||
OPEN = 'OPEN',
|
OPEN = 'OPEN',
|
||||||
|
|
@ -89,4 +92,13 @@ export class CreateDealDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
dealTypeId?: string;
|
dealTypeId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Projektanfrage-Details (nur bei Vorgangsart mit isProjectType=true)',
|
||||||
|
type: () => ProjectRequestDto,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => ProjectRequestDto)
|
||||||
|
projectRequest?: ProjectRequestDto;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
71
packages/crm-service/src/deals/dto/project-request.dto.ts
Normal file
71
packages/crm-service/src/deals/dto/project-request.dto.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsOptional,
|
||||||
|
IsNumber,
|
||||||
|
IsDateString,
|
||||||
|
MaxLength,
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ProjectRequestDto {
|
||||||
|
@ApiPropertyOptional({ description: 'Freitext / Projektbeschreibung' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
notes?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Auslastung in % (0–100)',
|
||||||
|
minimum: 0,
|
||||||
|
maximum: 100,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber({ maxDecimalPlaces: 2 })
|
||||||
|
@Min(0)
|
||||||
|
@Max(100)
|
||||||
|
workload?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ format: 'date-time', description: 'Projektstart' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
startDate?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Laufzeit (Freitext, z.B. "6 Monate")',
|
||||||
|
maxLength: 200,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(200)
|
||||||
|
duration?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Vorort-Anteil in % (0–100)',
|
||||||
|
minimum: 0,
|
||||||
|
maximum: 100,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber({ maxDecimalPlaces: 2 })
|
||||||
|
@Min(0)
|
||||||
|
@Max(100)
|
||||||
|
onsitePercent?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Stundensatz Remote (€/h)',
|
||||||
|
minimum: 0,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber({ maxDecimalPlaces: 2 })
|
||||||
|
@Min(0)
|
||||||
|
rateRemote?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Stundensatz Vorort (€/h)',
|
||||||
|
minimum: 0,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber({ maxDecimalPlaces: 2 })
|
||||||
|
@Min(0)
|
||||||
|
rateOnsite?: number;
|
||||||
|
}
|
||||||
|
|
@ -45,6 +45,23 @@ const rowStyle: React.CSSProperties = {
|
||||||
gap: '0.75rem',
|
gap: '0.75rem',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sectionStyle: React.CSSProperties = {
|
||||||
|
padding: '1rem',
|
||||||
|
background: 'var(--color-bg)',
|
||||||
|
border: '1px solid var(--color-border)',
|
||||||
|
borderRadius: 'var(--radius-sm)',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sectionTitleStyle: React.CSSProperties = {
|
||||||
|
fontSize: '0.8125rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--color-primary)',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
letterSpacing: '0.05em',
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
};
|
||||||
|
|
||||||
export function DealFormModal({
|
export function DealFormModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
|
|
@ -84,6 +101,15 @@ export function DealFormModal({
|
||||||
const [lostReasonText, setLostReasonText] = useState('');
|
const [lostReasonText, setLostReasonText] = useState('');
|
||||||
const [dealTypeId, setDealTypeId] = useState('');
|
const [dealTypeId, setDealTypeId] = useState('');
|
||||||
|
|
||||||
|
// Projektanfrage-Felder
|
||||||
|
const [prNotes, setPrNotes] = useState('');
|
||||||
|
const [prWorkload, setPrWorkload] = useState('');
|
||||||
|
const [prStartDate, setPrStartDate] = useState('');
|
||||||
|
const [prDuration, setPrDuration] = useState('');
|
||||||
|
const [prOnsitePercent, setPrOnsitePercent] = useState('');
|
||||||
|
const [prRateRemote, setPrRateRemote] = useState('');
|
||||||
|
const [prRateOnsite, setPrRateOnsite] = useState('');
|
||||||
|
|
||||||
// Kontakt-Suche
|
// Kontakt-Suche
|
||||||
const [contactSearch, setContactSearch] = useState('');
|
const [contactSearch, setContactSearch] = useState('');
|
||||||
const [contactResults, setContactResults] = useState<Contact[]>([]);
|
const [contactResults, setContactResults] = useState<Contact[]>([]);
|
||||||
|
|
@ -112,6 +138,10 @@ export function DealFormModal({
|
||||||
? [...selectedPipeline.stages].sort((a, b) => a.sortOrder - b.sortOrder)
|
? [...selectedPipeline.stages].sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
// Ausgewaehlte Vorgangsart → Projektanfrage-Flag
|
||||||
|
const selectedDealType = dealTypes.find((dt) => dt.id === dealTypeId) ?? null;
|
||||||
|
const isProjectDeal = selectedDealType?.isProjectType === true;
|
||||||
|
|
||||||
// Click-Outside für Kontakt- und Unternehmen-Dropdown
|
// Click-Outside für Kontakt- und Unternehmen-Dropdown
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClick(e: MouseEvent) {
|
function handleClick(e: MouseEvent) {
|
||||||
|
|
@ -199,6 +229,15 @@ export function DealFormModal({
|
||||||
setLostReason((deal.lostReason as LostReason) ?? '');
|
setLostReason((deal.lostReason as LostReason) ?? '');
|
||||||
setLostReasonText(deal.lostReasonText ?? '');
|
setLostReasonText(deal.lostReasonText ?? '');
|
||||||
setDealTypeId(deal.dealTypeId ?? '');
|
setDealTypeId(deal.dealTypeId ?? '');
|
||||||
|
// Projektanfrage initialisieren
|
||||||
|
const pr = deal.projectRequest;
|
||||||
|
setPrNotes(pr?.notes ?? '');
|
||||||
|
setPrWorkload(pr?.workload ? String(parseFloat(pr.workload)) : '');
|
||||||
|
setPrStartDate(pr?.startDate ? pr.startDate.slice(0, 10) : '');
|
||||||
|
setPrDuration(pr?.duration ?? '');
|
||||||
|
setPrOnsitePercent(pr?.onsitePercent ? String(parseFloat(pr.onsitePercent)) : '');
|
||||||
|
setPrRateRemote(pr?.rateRemote ? String(parseFloat(pr.rateRemote)) : '');
|
||||||
|
setPrRateOnsite(pr?.rateOnsite ? String(parseFloat(pr.rateOnsite)) : '');
|
||||||
if (deal.contact) {
|
if (deal.contact) {
|
||||||
const { id, firstName, lastName, companyName } = deal.contact;
|
const { id, firstName, lastName, companyName } = deal.contact;
|
||||||
const name =
|
const name =
|
||||||
|
|
@ -230,6 +269,13 @@ export function DealFormModal({
|
||||||
setLostReason('');
|
setLostReason('');
|
||||||
setLostReasonText('');
|
setLostReasonText('');
|
||||||
setDealTypeId('');
|
setDealTypeId('');
|
||||||
|
setPrNotes('');
|
||||||
|
setPrWorkload('');
|
||||||
|
setPrStartDate('');
|
||||||
|
setPrDuration('');
|
||||||
|
setPrOnsitePercent('');
|
||||||
|
setPrRateRemote('');
|
||||||
|
setPrRateOnsite('');
|
||||||
setSelectedContact(null);
|
setSelectedContact(null);
|
||||||
setContactSearch('');
|
setContactSearch('');
|
||||||
setSelectedCompany(null);
|
setSelectedCompany(null);
|
||||||
|
|
@ -270,6 +316,11 @@ export function DealFormModal({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Projektanfrage-Payload (nur wenn isProjectDeal und mind. ein Feld gesetzt)
|
||||||
|
const hasProjectData = isProjectDeal && (
|
||||||
|
prNotes || prWorkload || prStartDate || prDuration || prOnsitePercent || prRateRemote || prRateOnsite
|
||||||
|
);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
pipelineId,
|
pipelineId,
|
||||||
|
|
@ -284,6 +335,19 @@ export function DealFormModal({
|
||||||
...(status === 'LOST' && lostReason ? { lostReason } : {}),
|
...(status === 'LOST' && lostReason ? { lostReason } : {}),
|
||||||
...(status === 'LOST' && lostReasonText ? { lostReasonText } : {}),
|
...(status === 'LOST' && lostReasonText ? { lostReasonText } : {}),
|
||||||
...(dealTypeId ? { dealTypeId } : {}),
|
...(dealTypeId ? { dealTypeId } : {}),
|
||||||
|
...(hasProjectData
|
||||||
|
? {
|
||||||
|
projectRequest: {
|
||||||
|
...(prNotes ? { notes: prNotes } : {}),
|
||||||
|
...(prWorkload ? { workload: parseFloat(prWorkload) } : {}),
|
||||||
|
...(prStartDate ? { startDate: new Date(prStartDate).toISOString() } : {}),
|
||||||
|
...(prDuration ? { duration: prDuration } : {}),
|
||||||
|
...(prOnsitePercent ? { onsitePercent: parseFloat(prOnsitePercent) } : {}),
|
||||||
|
...(prRateRemote ? { rateRemote: parseFloat(prRateRemote) } : {}),
|
||||||
|
...(prRateOnsite ? { rateOnsite: parseFloat(prRateOnsite) } : {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveCustomFields = (entityId: string) => {
|
const saveCustomFields = (entityId: string) => {
|
||||||
|
|
@ -354,6 +418,130 @@ export function DealFormModal({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Vorgangsart – ganz oben, damit Projektanfrage-Felder rechtzeitig erscheinen */}
|
||||||
|
<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}
|
||||||
|
{dt.isProjectType ? ' 🗂' : ''}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/* Projektanfrage-Sektion (nur bei isProjectType) */}
|
||||||
|
{isProjectDeal && (
|
||||||
|
<div style={sectionStyle}>
|
||||||
|
<div style={sectionTitleStyle}>📋 Projektanfrage</div>
|
||||||
|
|
||||||
|
{/* Freitext */}
|
||||||
|
<div style={{ marginBottom: '0.75rem' }}>
|
||||||
|
<label style={labelStyle}>Beschreibung</label>
|
||||||
|
<textarea
|
||||||
|
style={{ ...inputStyle, minHeight: 72, resize: 'vertical' }}
|
||||||
|
value={prNotes}
|
||||||
|
onChange={(e) => setPrNotes(e.target.value)}
|
||||||
|
placeholder="Projektbeschreibung, Anforderungen..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Auslastung + Start */}
|
||||||
|
<div style={{ ...rowStyle, marginBottom: '0.75rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle}>Auslastung (%)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
style={inputStyle}
|
||||||
|
value={prWorkload}
|
||||||
|
onChange={(e) => setPrWorkload(e.target.value)}
|
||||||
|
placeholder="z.B. 80"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle}>Start</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
style={inputStyle}
|
||||||
|
value={prStartDate}
|
||||||
|
onChange={(e) => setPrStartDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Laufzeit + Vorort-Anteil */}
|
||||||
|
<div style={{ ...rowStyle, marginBottom: '0.75rem' }}>
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle}>Laufzeit</label>
|
||||||
|
<input
|
||||||
|
style={inputStyle}
|
||||||
|
value={prDuration}
|
||||||
|
onChange={(e) => setPrDuration(e.target.value)}
|
||||||
|
placeholder="z.B. 6 Monate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle}>Vorort-Anteil (%)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
style={inputStyle}
|
||||||
|
value={prOnsitePercent}
|
||||||
|
onChange={(e) => setPrOnsitePercent(e.target.value)}
|
||||||
|
placeholder="z.B. 20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stundensätze */}
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle}>Stundensatz Remote (€/h)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
style={inputStyle}
|
||||||
|
value={prRateRemote}
|
||||||
|
onChange={(e) => setPrRateRemote(e.target.value)}
|
||||||
|
placeholder="0.00"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={labelStyle}>Stundensatz Vorort (€/h)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0"
|
||||||
|
style={inputStyle}
|
||||||
|
value={prRateOnsite}
|
||||||
|
onChange={(e) => setPrRateOnsite(e.target.value)}
|
||||||
|
placeholder="0.00"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Titel */}
|
{/* Titel */}
|
||||||
<div style={{ marginBottom: '1rem' }}>
|
<div style={{ marginBottom: '1rem' }}>
|
||||||
<label style={labelStyle}>Titel *</label>
|
<label style={labelStyle}>Titel *</label>
|
||||||
|
|
@ -425,31 +613,6 @@ 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>
|
||||||
|
|
@ -594,7 +757,7 @@ export function DealFormModal({
|
||||||
right: 0,
|
right: 0,
|
||||||
background: 'var(--color-bg-card)',
|
background: 'var(--color-bg-card)',
|
||||||
border: '1px solid var(--color-border)',
|
border: '1px solid var(--color-border)',
|
||||||
borderRadius: 'var(--radius-sm)',
|
borderRadius: 'var(--shadow-md)',
|
||||||
boxShadow: 'var(--shadow-md)',
|
boxShadow: 'var(--shadow-md)',
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
maxHeight: 200,
|
maxHeight: 200,
|
||||||
|
|
|
||||||
|
|
@ -709,11 +709,13 @@ function DealTypesConfig() {
|
||||||
const [addMode, setAddMode] = useState(false);
|
const [addMode, setAddMode] = useState(false);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [color, setColor] = useState('#6B7280');
|
const [color, setColor] = useState('#6B7280');
|
||||||
|
const [isProjectType, setIsProjectType] = useState(false);
|
||||||
|
|
||||||
const startAdd = useCallback(() => {
|
const startAdd = useCallback(() => {
|
||||||
setEditId(null);
|
setEditId(null);
|
||||||
setName('');
|
setName('');
|
||||||
setColor('#6B7280');
|
setColor('#6B7280');
|
||||||
|
setIsProjectType(false);
|
||||||
setAddMode(true);
|
setAddMode(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -722,6 +724,7 @@ function DealTypesConfig() {
|
||||||
setEditId(item.id);
|
setEditId(item.id);
|
||||||
setName(item.name);
|
setName(item.name);
|
||||||
setColor(item.color);
|
setColor(item.color);
|
||||||
|
setIsProjectType(item.isProjectType ?? false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const cancel = useCallback(() => {
|
const cancel = useCallback(() => {
|
||||||
|
|
@ -729,22 +732,23 @@ function DealTypesConfig() {
|
||||||
setAddMode(false);
|
setAddMode(false);
|
||||||
setName('');
|
setName('');
|
||||||
setColor('#6B7280');
|
setColor('#6B7280');
|
||||||
|
setIsProjectType(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
if (!name.trim()) return;
|
if (!name.trim()) return;
|
||||||
if (addMode) {
|
if (addMode) {
|
||||||
createMut.mutate(
|
createMut.mutate(
|
||||||
{ name: name.trim(), color },
|
{ name: name.trim(), color, isProjectType },
|
||||||
{ onSuccess: cancel },
|
{ onSuccess: cancel },
|
||||||
);
|
);
|
||||||
} else if (editId) {
|
} else if (editId) {
|
||||||
updateMut.mutate(
|
updateMut.mutate(
|
||||||
{ id: editId, data: { name: name.trim(), color } },
|
{ id: editId, data: { name: name.trim(), color, isProjectType } },
|
||||||
{ onSuccess: cancel },
|
{ onSuccess: cancel },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [addMode, editId, name, color, createMut, updateMut, cancel]);
|
}, [addMode, editId, name, color, isProjectType, createMut, updateMut, cancel]);
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
|
|
@ -788,6 +792,7 @@ function DealTypesConfig() {
|
||||||
<th style={{ width: 40 }}>#</th>
|
<th style={{ width: 40 }}>#</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th style={{ width: 60 }}>Farbe</th>
|
<th style={{ width: 60 }}>Farbe</th>
|
||||||
|
<th style={{ width: 110, textAlign: 'center' }} title="Zeigt Projektanfrage-Felder im Vorgangs-Formular">Projektanfrage</th>
|
||||||
<th style={{ width: 100, textAlign: 'right' }}>Aktionen</th>
|
<th style={{ width: 100, textAlign: 'right' }}>Aktionen</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -819,6 +824,14 @@ function DealTypesConfig() {
|
||||||
onChange={(e) => setColor(e.target.value)}
|
onChange={(e) => setColor(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td style={{ textAlign: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isProjectType}
|
||||||
|
onChange={(e) => setIsProjectType(e.target.checked)}
|
||||||
|
title="Als Projektanfrage-Typ markieren"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className={styles.actionsCell}>
|
<div className={styles.actionsCell}>
|
||||||
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
|
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
|
||||||
|
|
@ -833,7 +846,7 @@ function DealTypesConfig() {
|
||||||
)}
|
)}
|
||||||
{items.length === 0 && !addMode && (
|
{items.length === 0 && !addMode && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4} className={styles.emptyRow}>
|
<td colSpan={5} className={styles.emptyRow}>
|
||||||
Noch keine Vorgangsarten definiert
|
Noch keine Vorgangsarten definiert
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -865,6 +878,14 @@ function DealTypesConfig() {
|
||||||
onChange={(e) => setColor(e.target.value)}
|
onChange={(e) => setColor(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td style={{ textAlign: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isProjectType}
|
||||||
|
onChange={(e) => setIsProjectType(e.target.checked)}
|
||||||
|
title="Als Projektanfrage-Typ markieren"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className={styles.actionsCell}>
|
<div className={styles.actionsCell}>
|
||||||
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
|
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
|
||||||
|
|
@ -906,6 +927,13 @@ function DealTypesConfig() {
|
||||||
title={item.color}
|
title={item.color}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
<td style={{ textAlign: 'center' }}>
|
||||||
|
{item.isProjectType ? (
|
||||||
|
<span title="Projektanfrage-Typ" style={{ color: 'var(--color-primary)', fontSize: '1rem' }}>✓</span>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem' }}>—</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className={styles.actionsCell}>
|
<div className={styles.actionsCell}>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@ export interface DealType {
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
|
isProjectType: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
_count?: { deals: number };
|
_count?: { deals: number };
|
||||||
|
|
@ -162,10 +163,37 @@ export interface CreateDealTypePayload {
|
||||||
name: string;
|
name: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
sortOrder?: number;
|
sortOrder?: number;
|
||||||
|
isProjectType?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateDealTypePayload = Partial<CreateDealTypePayload>;
|
export type UpdateDealTypePayload = Partial<CreateDealTypePayload>;
|
||||||
|
|
||||||
|
// --- ProjectRequestDetails ---
|
||||||
|
|
||||||
|
export interface ProjectRequestDetails {
|
||||||
|
id: string;
|
||||||
|
dealId: string;
|
||||||
|
notes: string | null;
|
||||||
|
workload: string | null; // Decimal kommt als String vom Backend
|
||||||
|
startDate: string | null;
|
||||||
|
duration: string | null;
|
||||||
|
onsitePercent: string | null; // Decimal als String
|
||||||
|
rateRemote: string | null; // Decimal als String
|
||||||
|
rateOnsite: string | null; // Decimal als String
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateProjectRequestPayload {
|
||||||
|
notes?: string;
|
||||||
|
workload?: number;
|
||||||
|
startDate?: string;
|
||||||
|
duration?: string;
|
||||||
|
onsitePercent?: number;
|
||||||
|
rateRemote?: number;
|
||||||
|
rateOnsite?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CompanyRelationship {
|
export interface CompanyRelationship {
|
||||||
id: string;
|
id: string;
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
|
|
@ -468,7 +496,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;
|
dealType?: { id: string; name: string; color: string; isProjectType: boolean } | null;
|
||||||
contact?: {
|
contact?: {
|
||||||
id: string;
|
id: string;
|
||||||
firstName: string | null;
|
firstName: string | null;
|
||||||
|
|
@ -477,6 +505,7 @@ export interface Deal {
|
||||||
} | null;
|
} | null;
|
||||||
company?: { id: string; name: string } | null;
|
company?: { id: string; name: string } | null;
|
||||||
dealVouchers?: DealVoucher[];
|
dealVouchers?: DealVoucher[];
|
||||||
|
projectRequest?: ProjectRequestDetails | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateDealPayload {
|
export interface CreateDealPayload {
|
||||||
|
|
@ -493,6 +522,7 @@ export interface CreateDealPayload {
|
||||||
lostReason?: LostReason;
|
lostReason?: LostReason;
|
||||||
lostReasonText?: string;
|
lostReasonText?: string;
|
||||||
dealTypeId?: string;
|
dealTypeId?: string;
|
||||||
|
projectRequest?: CreateProjectRequestPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateDealPayload = Partial<CreateDealPayload>;
|
export type UpdateDealPayload = Partial<CreateDealPayload>;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue