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:
Thomas Reitz 2026-03-13 17:17:40 +01:00
parent 6bfce4af97
commit 4c739945f0
11 changed files with 494 additions and 46 deletions

View file

@ -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
#### Backend (crm-service)

View file

@ -453,6 +453,7 @@ model DealType {
name String @db.VarChar(100)
color String @default("#6B7280") @db.VarChar(7)
sortOrder Int @default(0) @map("sort_order")
isProjectType Boolean @default(false) @map("is_project_type")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@ -557,6 +558,7 @@ model Deal {
dealType DealType? @relation(fields: [dealTypeId], references: [id], onDelete: SetNull)
dealVouchers DealVoucher[]
owners DealOwner[]
projectRequest ProjectRequestDetails?
@@index([tenantId])
@@index([tenantId, pipelineId])
@ -576,6 +578,28 @@ enum DealStatus {
@@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
// --------------------------------------------------------

View file

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

View file

@ -2,6 +2,7 @@ import {
IsString,
IsOptional,
IsInt,
IsBoolean,
MaxLength,
Matches,
Min,
@ -32,4 +33,12 @@ export class CreateDealTypeDto {
@IsInt()
@Min(0)
sortOrder?: number;
@ApiPropertyOptional({
default: false,
description: 'Spezialtyp: Projektanfrage (zeigt zusaetzliche Felder im Vorgangs-Formular)',
})
@IsOptional()
@IsBoolean()
isProjectType?: boolean;
}

View file

@ -2,6 +2,7 @@ import {
IsString,
IsOptional,
IsInt,
IsBoolean,
MaxLength,
Matches,
Min,
@ -29,4 +30,9 @@ export class UpdateDealTypeDto {
@IsInt()
@Min(0)
sortOrder?: number;
@ApiPropertyOptional({ description: 'Spezialtyp: Projektanfrage' })
@IsOptional()
@IsBoolean()
isProjectType?: boolean;
}

View file

@ -79,6 +79,23 @@ export class DealsService {
owners: {
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: {
pipeline: { select: { id: true, name: true } },
@ -98,6 +115,7 @@ export class DealsService {
},
},
owners: true,
projectRequest: true,
},
});
@ -184,6 +202,7 @@ export class DealsService {
contact: true,
company: true,
owners: true,
projectRequest: true,
dealVouchers: {
include: {
voucher: {
@ -248,7 +267,7 @@ export class DealsService {
}
}
const { lostReason, lostReasonText, ...restDto } = dto;
const { lostReason, lostReasonText, projectRequest, ...restDto } = dto;
const updateData: Prisma.DealUpdateInput = {
...restDto,
@ -277,6 +296,36 @@ export class DealsService {
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({
where: { id },
data: updateData,
@ -298,6 +347,7 @@ export class DealsService {
},
},
owners: true,
projectRequest: true,
},
});

View file

@ -7,8 +7,11 @@ import {
IsEnum,
MaxLength,
Min,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ProjectRequestDto } from './project-request.dto';
export enum DealStatus {
OPEN = 'OPEN',
@ -89,4 +92,13 @@ export class CreateDealDto {
@IsOptional()
@IsUUID()
dealTypeId?: string;
@ApiPropertyOptional({
description: 'Projektanfrage-Details (nur bei Vorgangsart mit isProjectType=true)',
type: () => ProjectRequestDto,
})
@IsOptional()
@ValidateNested()
@Type(() => ProjectRequestDto)
projectRequest?: ProjectRequestDto;
}

View 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 % (0100)',
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 % (0100)',
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;
}

View file

@ -45,6 +45,23 @@ const rowStyle: React.CSSProperties = {
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({
isOpen,
onClose,
@ -84,6 +101,15 @@ export function DealFormModal({
const [lostReasonText, setLostReasonText] = 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
const [contactSearch, setContactSearch] = useState('');
const [contactResults, setContactResults] = useState<Contact[]>([]);
@ -112,6 +138,10 @@ export function DealFormModal({
? [...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
useEffect(() => {
function handleClick(e: MouseEvent) {
@ -199,6 +229,15 @@ export function DealFormModal({
setLostReason((deal.lostReason as LostReason) ?? '');
setLostReasonText(deal.lostReasonText ?? '');
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) {
const { id, firstName, lastName, companyName } = deal.contact;
const name =
@ -230,6 +269,13 @@ export function DealFormModal({
setLostReason('');
setLostReasonText('');
setDealTypeId('');
setPrNotes('');
setPrWorkload('');
setPrStartDate('');
setPrDuration('');
setPrOnsitePercent('');
setPrRateRemote('');
setPrRateOnsite('');
setSelectedContact(null);
setContactSearch('');
setSelectedCompany(null);
@ -270,6 +316,11 @@ export function DealFormModal({
return;
}
// Projektanfrage-Payload (nur wenn isProjectDeal und mind. ein Feld gesetzt)
const hasProjectData = isProjectDeal && (
prNotes || prWorkload || prStartDate || prDuration || prOnsitePercent || prRateRemote || prRateOnsite
);
const payload = {
title: title.trim(),
pipelineId,
@ -284,6 +335,19 @@ export function DealFormModal({
...(status === 'LOST' && lostReason ? { lostReason } : {}),
...(status === 'LOST' && lostReasonText ? { lostReasonText } : {}),
...(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) => {
@ -354,6 +418,130 @@ export function DealFormModal({
</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 */}
<div style={{ marginBottom: '1rem' }}>
<label style={labelStyle}>Titel *</label>
@ -425,31 +613,6 @@ export function DealFormModal({
</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 */}
<div style={{ marginBottom: '1rem', position: 'relative' }} ref={contactRef}>
<label style={labelStyle}>Kontakt</label>
@ -594,7 +757,7 @@ export function DealFormModal({
right: 0,
background: 'var(--color-bg-card)',
border: '1px solid var(--color-border)',
borderRadius: 'var(--radius-sm)',
borderRadius: 'var(--shadow-md)',
boxShadow: 'var(--shadow-md)',
zIndex: 10,
maxHeight: 200,

View file

@ -709,11 +709,13 @@ function DealTypesConfig() {
const [addMode, setAddMode] = useState(false);
const [name, setName] = useState('');
const [color, setColor] = useState('#6B7280');
const [isProjectType, setIsProjectType] = useState(false);
const startAdd = useCallback(() => {
setEditId(null);
setName('');
setColor('#6B7280');
setIsProjectType(false);
setAddMode(true);
}, []);
@ -722,6 +724,7 @@ function DealTypesConfig() {
setEditId(item.id);
setName(item.name);
setColor(item.color);
setIsProjectType(item.isProjectType ?? false);
}, []);
const cancel = useCallback(() => {
@ -729,22 +732,23 @@ function DealTypesConfig() {
setAddMode(false);
setName('');
setColor('#6B7280');
setIsProjectType(false);
}, []);
const handleSave = useCallback(() => {
if (!name.trim()) return;
if (addMode) {
createMut.mutate(
{ name: name.trim(), color },
{ name: name.trim(), color, isProjectType },
{ onSuccess: cancel },
);
} else if (editId) {
updateMut.mutate(
{ id: editId, data: { name: name.trim(), color } },
{ id: editId, data: { name: name.trim(), color, isProjectType } },
{ onSuccess: cancel },
);
}
}, [addMode, editId, name, color, createMut, updateMut, cancel]);
}, [addMode, editId, name, color, isProjectType, createMut, updateMut, cancel]);
const handleDelete = useCallback(
(id: string) => {
@ -788,6 +792,7 @@ function DealTypesConfig() {
<th style={{ width: 40 }}>#</th>
<th>Name</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>
</tr>
</thead>
@ -819,6 +824,14 @@ function DealTypesConfig() {
onChange={(e) => setColor(e.target.value)}
/>
</td>
<td style={{ textAlign: 'center' }}>
<input
type="checkbox"
checked={isProjectType}
onChange={(e) => setIsProjectType(e.target.checked)}
title="Als Projektanfrage-Typ markieren"
/>
</td>
<td>
<div className={styles.actionsCell}>
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
@ -833,7 +846,7 @@ function DealTypesConfig() {
)}
{items.length === 0 && !addMode && (
<tr>
<td colSpan={4} className={styles.emptyRow}>
<td colSpan={5} className={styles.emptyRow}>
Noch keine Vorgangsarten definiert
</td>
</tr>
@ -865,6 +878,14 @@ function DealTypesConfig() {
onChange={(e) => setColor(e.target.value)}
/>
</td>
<td style={{ textAlign: 'center' }}>
<input
type="checkbox"
checked={isProjectType}
onChange={(e) => setIsProjectType(e.target.checked)}
title="Als Projektanfrage-Typ markieren"
/>
</td>
<td>
<div className={styles.actionsCell}>
<button className={styles.saveBtn} onClick={handleSave} disabled={!name.trim() || isSaving}>
@ -906,6 +927,13 @@ function DealTypesConfig() {
title={item.color}
/>
</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>
<div className={styles.actionsCell}>
<button

View file

@ -153,6 +153,7 @@ export interface DealType {
name: string;
color: string;
sortOrder: number;
isProjectType: boolean;
createdAt: string;
updatedAt: string;
_count?: { deals: number };
@ -162,10 +163,37 @@ export interface CreateDealTypePayload {
name: string;
color?: string;
sortOrder?: number;
isProjectType?: boolean;
}
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 {
id: string;
tenantId: string;
@ -468,7 +496,7 @@ export interface Deal {
customFields?: CustomFieldValue[];
pipeline?: { id: string; name: string; stages?: PipelineStage[] };
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?: {
id: string;
firstName: string | null;
@ -477,6 +505,7 @@ export interface Deal {
} | null;
company?: { id: string; name: string } | null;
dealVouchers?: DealVoucher[];
projectRequest?: ProjectRequestDetails | null;
}
export interface CreateDealPayload {
@ -493,6 +522,7 @@ export interface CreateDealPayload {
lostReason?: LostReason;
lostReasonText?: string;
dealTypeId?: string;
projectRequest?: CreateProjectRequestPayload;
}
export type UpdateDealPayload = Partial<CreateDealPayload>;