mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 00:16:41 +02:00
feat(crm): Dediziertes Projektanfrage-Formular + Button in Vorgänge-Liste
- ProjectRequestFormModal: Eigenständiges Formular mit Projektdetails oben (Beschreibung, Auslastung/Start, Laufzeit/Vorort-Anteil, Stundensätze) und Standard-Vorgangsdaten darunter (Titel, Pipeline/Stage, Kontakt, etc.) Auto-Select bei genau einem isProjectType-Typ; Warnung wenn nicht konfiguriert - DealsPage: Neuer outlined Button "Neue Projektanfrage" neben "Neuer Vorgang" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4c739945f0
commit
b197660ac8
3 changed files with 655 additions and 0 deletions
|
|
@ -6,6 +6,14 @@
|
|||
|
||||
---
|
||||
|
||||
### Aenderungen 2026-03-13 (10): Dediziertes Projektanfrage-Formular + Button in Vorgänge-Liste
|
||||
|
||||
#### Frontend
|
||||
- `crm/deals/ProjectRequestFormModal.tsx` — Neues dediziertes Modal "Neue Projektanfrage": Projektdetails-Sektion oben (Beschreibung, Auslastung/Start, Laufzeit/Vorort-Anteil, Stundensätze), darunter Vorgangsdaten (Titel, Pipeline/Stage, Kontakt, Unternehmen, Volumen/Abschluss, Notizen); Auto-Select bei genau einem isProjectType-Typ; Warnung wenn kein Typ konfiguriert; Submit-Button disabled bei fehlendem Typ
|
||||
- `crm/deals/DealsPage.tsx` — Zweiter Button "Neue Projektanfrage" (outlined, primary) neben "Neuer Vorgang"; `isProjectRequestOpen` State; `ProjectRequestFormModal` eingebunden
|
||||
|
||||
---
|
||||
|
||||
### Aenderungen 2026-03-13 (9): Projektanfrage-Vorgangstyp (isProjectType + ProjectRequestDetails)
|
||||
|
||||
#### Backend (crm-service)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||
import { Modal } from '../../components/Modal';
|
||||
import { useDeals, useDeleteDeal, usePipelines } from '../hooks';
|
||||
import { DealFormModal } from './DealFormModal';
|
||||
import { ProjectRequestFormModal } from './ProjectRequestFormModal';
|
||||
import type { Deal, DealStatus, DealsQueryParams } from '../types';
|
||||
import styles from './DealsPage.module.css';
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ export function DealsPage() {
|
|||
const [pipelineFilter, setPipelineFilter] = useState('');
|
||||
const [stageFilter, setStageFilter] = useState('');
|
||||
const [isCreateOpen, setCreateOpen] = useState(false);
|
||||
const [isProjectRequestOpen, setProjectRequestOpen] = useState(false);
|
||||
const [editingDeal, setEditingDeal] = useState<Deal | null>(null);
|
||||
const [deletingDeal, setDeletingDeal] = useState<Deal | null>(null);
|
||||
|
||||
|
|
@ -118,6 +120,21 @@ export function DealsPage() {
|
|||
>
|
||||
{pagination?.total ?? 0} Vorgänge gesamt
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setProjectRequestOpen(true)}
|
||||
style={{
|
||||
padding: '0.5rem 1rem',
|
||||
background: 'transparent',
|
||||
color: 'var(--color-primary)',
|
||||
border: '1px solid var(--color-primary)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
+ Neue Projektanfrage
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCreateOpen(true)}
|
||||
style={{
|
||||
|
|
@ -415,6 +432,13 @@ export function DealsPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal: Neue Projektanfrage */}
|
||||
<ProjectRequestFormModal
|
||||
isOpen={isProjectRequestOpen}
|
||||
onClose={() => setProjectRequestOpen(false)}
|
||||
onSuccess={() => { setProjectRequestOpen(false); }}
|
||||
/>
|
||||
|
||||
{/* Modal: Neuen Vorgang anlegen */}
|
||||
<DealFormModal
|
||||
isOpen={isCreateOpen}
|
||||
|
|
|
|||
623
packages/frontend/src/crm/deals/ProjectRequestFormModal.tsx
Normal file
623
packages/frontend/src/crm/deals/ProjectRequestFormModal.tsx
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
/**
|
||||
* ProjectRequestFormModal
|
||||
* Dediziertes Formular für Projektanfragen – separates Modal neben "Neuer Vorgang".
|
||||
* Zeigt projektspezifische Felder prominent am Anfang, gefolgt von Standard-Vorgangs-Feldern.
|
||||
*/
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Modal } from '../../components/Modal';
|
||||
import { useCreateDeal, usePipelines, useDealTypes } from '../hooks';
|
||||
import { contactsApi, companiesApi } from '../api';
|
||||
import type { Contact, Company } from '../types';
|
||||
|
||||
interface ProjectRequestFormModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Styles (inline, konsistent mit DealFormModal)
|
||||
// --------------------------------------------------------
|
||||
const labelStyle: React.CSSProperties = {
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
color: 'var(--color-text)',
|
||||
marginBottom: '0.25rem',
|
||||
display: 'block',
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '0.625rem 0.75rem',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
fontSize: '0.9375rem',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
background: 'var(--color-bg-card)',
|
||||
color: 'var(--color-text)',
|
||||
};
|
||||
|
||||
const rowStyle: React.CSSProperties = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '0.75rem',
|
||||
};
|
||||
|
||||
const dividerStyle: React.CSSProperties = {
|
||||
borderTop: '1px solid var(--color-border)',
|
||||
margin: '1.25rem 0',
|
||||
};
|
||||
|
||||
const sectionLabelStyle: React.CSSProperties = {
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.06em',
|
||||
color: 'var(--color-text-muted)',
|
||||
marginBottom: '0.875rem',
|
||||
};
|
||||
|
||||
// --------------------------------------------------------
|
||||
export function ProjectRequestFormModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: ProjectRequestFormModalProps) {
|
||||
const createMutation = useCreateDeal();
|
||||
const { data: pipelinesData } = usePipelines();
|
||||
const pipelines = pipelinesData?.data ?? [];
|
||||
const { data: dealTypesData } = useDealTypes();
|
||||
const dealTypes = dealTypesData?.data ?? [];
|
||||
|
||||
// Nur isProjectType-Typen
|
||||
const projectDealTypes = dealTypes.filter((dt) => dt.isProjectType);
|
||||
|
||||
// ---- 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('');
|
||||
|
||||
// ---- Standard-Vorgang-Felder ----
|
||||
const [title, setTitle] = useState('');
|
||||
const [dealTypeId, setDealTypeId] = useState('');
|
||||
const [pipelineId, setPipelineId] = useState('');
|
||||
const [stageId, setStageId] = useState('');
|
||||
const [value, setValue] = useState('');
|
||||
const [currency, setCurrency] = useState('EUR');
|
||||
const [expectedCloseDate, setExpectedCloseDate] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Kontakt-Suche
|
||||
const [contactSearch, setContactSearch] = useState('');
|
||||
const [contactResults, setContactResults] = useState<Contact[]>([]);
|
||||
const [selectedContact, setSelectedContact] = useState<{ id: string; name: string } | null>(null);
|
||||
const [showContactDropdown, setShowContactDropdown] = useState(false);
|
||||
const contactRef = useRef<HTMLDivElement>(null);
|
||||
const contactTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
// Unternehmen-Suche
|
||||
const [companySearch, setCompanySearch] = useState('');
|
||||
const [companyResults, setCompanyResults] = useState<Company[]>([]);
|
||||
const [selectedCompany, setSelectedCompany] = useState<{ id: string; name: string } | null>(null);
|
||||
const [showCompanyDropdown, setShowCompanyDropdown] = useState(false);
|
||||
const companyRef = useRef<HTMLDivElement>(null);
|
||||
const companyTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
// Stages der gewählten Pipeline
|
||||
const selectedPipeline = pipelines.find((p) => p.id === pipelineId);
|
||||
const stages = selectedPipeline?.stages
|
||||
? [...selectedPipeline.stages].sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
: [];
|
||||
|
||||
// ---- Reset beim Öffnen ----
|
||||
const reset = useCallback(() => {
|
||||
setPrNotes('');
|
||||
setPrWorkload('');
|
||||
setPrStartDate('');
|
||||
setPrDuration('');
|
||||
setPrOnsitePercent('');
|
||||
setPrRateRemote('');
|
||||
setPrRateOnsite('');
|
||||
setTitle('');
|
||||
setValue('');
|
||||
setCurrency('EUR');
|
||||
setExpectedCloseDate('');
|
||||
setNotes('');
|
||||
setError('');
|
||||
setSelectedContact(null);
|
||||
setContactSearch('');
|
||||
setSelectedCompany(null);
|
||||
setCompanySearch('');
|
||||
setContactResults([]);
|
||||
setCompanyResults([]);
|
||||
setShowContactDropdown(false);
|
||||
setShowCompanyDropdown(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
reset();
|
||||
// DealType: auto-select falls genau einer vorhanden
|
||||
setDealTypeId(projectDealTypes.length === 1 ? projectDealTypes[0].id : '');
|
||||
// Pipeline: Standard-Pipeline vorauswählen
|
||||
const defaultPl = pipelines.find((p) => p.isDefault) ?? pipelines[0];
|
||||
setPipelineId(defaultPl?.id ?? '');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen]);
|
||||
|
||||
// Wenn Pipeline wechselt → erste Stage
|
||||
useEffect(() => {
|
||||
if (stages.length > 0) setStageId(stages[0].id);
|
||||
else setStageId('');
|
||||
}, [pipelineId, stages.length]); // eslint-disable-line
|
||||
|
||||
// Click-outside
|
||||
useEffect(() => {
|
||||
function h(e: MouseEvent) {
|
||||
if (contactRef.current && !contactRef.current.contains(e.target as Node))
|
||||
setShowContactDropdown(false);
|
||||
if (companyRef.current && !companyRef.current.contains(e.target as Node))
|
||||
setShowCompanyDropdown(false);
|
||||
}
|
||||
document.addEventListener('mousedown', h);
|
||||
return () => document.removeEventListener('mousedown', h);
|
||||
}, []);
|
||||
|
||||
// Kontakt suchen (debounced)
|
||||
useEffect(() => {
|
||||
if (contactTimeout.current) clearTimeout(contactTimeout.current);
|
||||
if (!contactSearch || contactSearch.length < 2) { setContactResults([]); return; }
|
||||
contactTimeout.current = setTimeout(async () => {
|
||||
try {
|
||||
const res = await contactsApi.list({ search: contactSearch, pageSize: 8 });
|
||||
setContactResults(res.data);
|
||||
setShowContactDropdown(true);
|
||||
} catch { setContactResults([]); }
|
||||
}, 300);
|
||||
return () => { if (contactTimeout.current) clearTimeout(contactTimeout.current); };
|
||||
}, [contactSearch]);
|
||||
|
||||
// Unternehmen suchen (debounced)
|
||||
useEffect(() => {
|
||||
if (companyTimeout.current) clearTimeout(companyTimeout.current);
|
||||
if (!companySearch || companySearch.length < 2) { setCompanyResults([]); return; }
|
||||
companyTimeout.current = setTimeout(async () => {
|
||||
try {
|
||||
const res = await companiesApi.list({ search: companySearch, pageSize: 8 });
|
||||
setCompanyResults(res.data);
|
||||
setShowCompanyDropdown(true);
|
||||
} catch { setCompanyResults([]); }
|
||||
}, 300);
|
||||
return () => { if (companyTimeout.current) clearTimeout(companyTimeout.current); };
|
||||
}, [companySearch]);
|
||||
|
||||
// ---- Submit ----
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (!dealTypeId) {
|
||||
setError('Bitte eine Vorgangsart (Projektanfrage-Typ) auswählen.');
|
||||
return;
|
||||
}
|
||||
if (!title.trim()) {
|
||||
setError('Titel ist ein Pflichtfeld.');
|
||||
return;
|
||||
}
|
||||
if (!pipelineId) {
|
||||
setError('Bitte eine Pipeline auswählen.');
|
||||
return;
|
||||
}
|
||||
if (!stageId) {
|
||||
setError('Bitte eine Stage auswählen.');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasProjectData =
|
||||
prNotes || prWorkload || prStartDate || prDuration ||
|
||||
prOnsitePercent || prRateRemote || prRateOnsite;
|
||||
|
||||
const payload = {
|
||||
title: title.trim(),
|
||||
pipelineId,
|
||||
stageId,
|
||||
dealTypeId,
|
||||
status: 'OPEN' as const,
|
||||
currency,
|
||||
...(selectedContact ? { contactId: selectedContact.id } : {}),
|
||||
...(selectedCompany ? { companyId: selectedCompany.id } : {}),
|
||||
...(value ? { value: parseFloat(value) } : {}),
|
||||
...(expectedCloseDate ? { expectedCloseDate: new Date(expectedCloseDate).toISOString() } : {}),
|
||||
...(notes ? { notes } : {}),
|
||||
...(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) } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
createMutation.mutate(payload, {
|
||||
onSuccess: () => {
|
||||
reset();
|
||||
onSuccess();
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg =
|
||||
(err as { response?: { data?: { error?: { message?: string } } } })
|
||||
?.response?.data?.error?.message ?? 'Fehler beim Anlegen';
|
||||
setError(msg);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// ---- UI-Hilfskomponenten ----
|
||||
const SearchDropdown = ({
|
||||
results,
|
||||
show,
|
||||
onSelect,
|
||||
renderLabel,
|
||||
renderSub,
|
||||
}: {
|
||||
results: (Contact | Company)[];
|
||||
show: boolean;
|
||||
onSelect: (item: Contact | Company) => void;
|
||||
renderLabel: (item: Contact | Company) => string;
|
||||
renderSub?: (item: Contact | Company) => string | undefined;
|
||||
}) => {
|
||||
if (!show || results.length === 0) return null;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
background: 'var(--color-bg-card)',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
boxShadow: 'var(--shadow-md)',
|
||||
zIndex: 20,
|
||||
maxHeight: 200,
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{results.map((item) => {
|
||||
const label = renderLabel(item);
|
||||
const sub = renderSub?.(item);
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => onSelect(item)}
|
||||
style={{ padding: '0.5rem 0.75rem', cursor: 'pointer', fontSize: '0.875rem', borderBottom: '1px solid var(--color-border)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLDivElement).style.background = 'var(--color-bg)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLDivElement).style.background = 'transparent')}
|
||||
>
|
||||
{label}
|
||||
{sub && <span style={{ color: 'var(--color-text-muted)', marginLeft: '0.5rem', fontSize: '0.8125rem' }}>{sub}</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ---- Render ----
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title="Neue Projektanfrage"
|
||||
maxWidth="620px"
|
||||
>
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* Fehler-Banner */}
|
||||
{error && (
|
||||
<div style={{ padding: '0.75rem', background: '#fef2f2', border: '1px solid #fecaca', borderRadius: 'var(--radius-sm)', color: 'var(--color-error)', fontSize: '0.875rem', marginBottom: '1rem' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Kein Projektanfrage-Typ konfiguriert – Warnung */}
|
||||
{projectDealTypes.length === 0 && (
|
||||
<div style={{ padding: '0.875rem 1rem', 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: '1.25rem' }}>
|
||||
⚠️ Noch kein Projektanfrage-Typ konfiguriert. Bitte zuerst unter{' '}
|
||||
<a href="/crm/settings" style={{ color: 'inherit', fontWeight: 600 }}>
|
||||
CRM Einstellungen → Vorgangsarten
|
||||
</a>{' '}
|
||||
eine Vorgangsart anlegen und "Projektanfrage" aktivieren.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Vorgangsart-Auswahl (nur wenn mehrere Projektanfrage-Typen) */}
|
||||
{projectDealTypes.length > 1 && (
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<label style={labelStyle}>Vorgangsart *</label>
|
||||
<select
|
||||
value={dealTypeId}
|
||||
onChange={(e) => setDealTypeId(e.target.value)}
|
||||
style={{ ...inputStyle, cursor: 'pointer' }}
|
||||
>
|
||||
<option value="">— Typ wählen —</option>
|
||||
{projectDealTypes.map((dt) => (
|
||||
<option key={dt.id} value={dt.id}>{dt.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ================================================ */}
|
||||
{/* PROJEKTANFRAGE-FELDER */}
|
||||
{/* ================================================ */}
|
||||
<div style={sectionLabelStyle}>📋 Projektdetails</div>
|
||||
|
||||
{/* Beschreibung */}
|
||||
<div style={{ marginBottom: '0.875rem' }}>
|
||||
<label style={labelStyle}>Beschreibung</label>
|
||||
<textarea
|
||||
style={{ ...inputStyle, minHeight: 88, resize: 'vertical' }}
|
||||
value={prNotes}
|
||||
onChange={(e) => setPrNotes(e.target.value)}
|
||||
placeholder="Projektbeschreibung, Anforderungen, Rahmenbedingungen..."
|
||||
autoFocus={projectDealTypes.length > 0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Auslastung + Start */}
|
||||
<div style={{ ...rowStyle, marginBottom: '0.875rem' }}>
|
||||
<div>
|
||||
<label style={labelStyle}>Auslastung (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="5"
|
||||
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.875rem' }}>
|
||||
<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="5"
|
||||
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, marginBottom: '0.875rem' }}>
|
||||
<div>
|
||||
<label style={labelStyle}>Stundensatz Remote (€/h)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
style={inputStyle}
|
||||
value={prRateRemote}
|
||||
onChange={(e) => setPrRateRemote(e.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>Stundensatz Vorort (€/h)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="1"
|
||||
min="0"
|
||||
style={inputStyle}
|
||||
value={prRateOnsite}
|
||||
onChange={(e) => setPrRateOnsite(e.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={dividerStyle} />
|
||||
|
||||
{/* ================================================ */}
|
||||
{/* STANDARD-VORGANG-FELDER */}
|
||||
{/* ================================================ */}
|
||||
<div style={sectionLabelStyle}>📁 Vorgangsdaten</div>
|
||||
|
||||
{/* Titel */}
|
||||
<div style={{ marginBottom: '0.875rem' }}>
|
||||
<label style={labelStyle}>Titel *</label>
|
||||
<input
|
||||
style={inputStyle}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="z.B. Projektanfrage – Kundenname / Thema"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Pipeline + Stage */}
|
||||
{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: '0.875rem' }}>
|
||||
⚠️ Keine Pipelines konfiguriert.{' '}
|
||||
<a href="/crm/settings" style={{ color: 'inherit', fontWeight: 600 }}>CRM Einstellungen → Pipelines</a>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ ...rowStyle, marginBottom: '0.875rem' }}>
|
||||
<div>
|
||||
<label style={labelStyle}>Pipeline *</label>
|
||||
<select value={pipelineId} onChange={(e) => setPipelineId(e.target.value)} style={{ ...inputStyle, cursor: 'pointer' }}>
|
||||
<option value="">Pipeline wählen</option>
|
||||
{pipelines.filter((p) => p.isActive).map((p) => (
|
||||
<option key={p.id} value={p.id}>{p.name}{p.isDefault ? ' (Standard)' : ''}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>Stage *</label>
|
||||
<select value={stageId} onChange={(e) => setStageId(e.target.value)} style={{ ...inputStyle, cursor: 'pointer' }} disabled={stages.length === 0}>
|
||||
<option value="">Stage wählen</option>
|
||||
{stages.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Kontakt-Suche */}
|
||||
<div style={{ marginBottom: '0.875rem', position: 'relative' }} ref={contactRef}>
|
||||
<label style={labelStyle}>Kontakt</label>
|
||||
<input
|
||||
style={inputStyle}
|
||||
value={contactSearch}
|
||||
onChange={(e) => { setContactSearch(e.target.value); if (selectedContact) setSelectedContact(null); }}
|
||||
onFocus={() => { if (contactResults.length > 0) setShowContactDropdown(true); }}
|
||||
placeholder="Kontakt suchen..."
|
||||
/>
|
||||
{selectedContact && (
|
||||
<button type="button" onClick={() => { setSelectedContact(null); setContactSearch(''); }}
|
||||
style={{ position: 'absolute', right: 8, top: 30, background: 'none', border: 'none', color: 'var(--color-text-muted)', cursor: 'pointer', fontSize: '1rem' }}>×</button>
|
||||
)}
|
||||
<SearchDropdown
|
||||
results={contactResults}
|
||||
show={showContactDropdown}
|
||||
onSelect={(item) => {
|
||||
const c = item as Contact;
|
||||
const name = c.type === 'ORGANIZATION' ? c.companyName : [c.firstName, c.lastName].filter(Boolean).join(' ');
|
||||
setSelectedContact({ id: c.id, name: name ?? '' });
|
||||
setContactSearch(name ?? '');
|
||||
setShowContactDropdown(false);
|
||||
}}
|
||||
renderLabel={(item) => {
|
||||
const c = item as Contact;
|
||||
return (c.type === 'ORGANIZATION' ? c.companyName : [c.firstName, c.lastName].filter(Boolean).join(' ')) ?? '';
|
||||
}}
|
||||
renderSub={(item) => (item as Contact).email ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Unternehmen-Suche */}
|
||||
<div style={{ marginBottom: '0.875rem', position: 'relative' }} ref={companyRef}>
|
||||
<label style={labelStyle}>Unternehmen</label>
|
||||
<input
|
||||
style={inputStyle}
|
||||
value={companySearch}
|
||||
onChange={(e) => { setCompanySearch(e.target.value); if (selectedCompany) setSelectedCompany(null); }}
|
||||
onFocus={() => { if (companyResults.length > 0) setShowCompanyDropdown(true); }}
|
||||
placeholder="Unternehmen suchen..."
|
||||
/>
|
||||
{selectedCompany && (
|
||||
<button type="button" onClick={() => { setSelectedCompany(null); setCompanySearch(''); }}
|
||||
style={{ position: 'absolute', right: 8, top: 30, background: 'none', border: 'none', color: 'var(--color-text-muted)', cursor: 'pointer', fontSize: '1rem' }}>×</button>
|
||||
)}
|
||||
<SearchDropdown
|
||||
results={companyResults}
|
||||
show={showCompanyDropdown}
|
||||
onSelect={(item) => {
|
||||
const comp = item as Company;
|
||||
setSelectedCompany({ id: comp.id, name: comp.name });
|
||||
setCompanySearch(comp.name);
|
||||
setShowCompanyDropdown(false);
|
||||
}}
|
||||
renderLabel={(item) => (item as Company).name}
|
||||
renderSub={(item) => (item as Company).industry ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Wert + Erwarteter Abschluss */}
|
||||
<div style={{ ...rowStyle, marginBottom: '0.875rem' }}>
|
||||
<div>
|
||||
<label style={labelStyle}>Volumen (€)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="500"
|
||||
min="0"
|
||||
style={inputStyle}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>Erw. Abschluss</label>
|
||||
<input
|
||||
type="date"
|
||||
style={inputStyle}
|
||||
value={expectedCloseDate}
|
||||
onChange={(e) => setExpectedCloseDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interne Notizen */}
|
||||
<div style={{ marginBottom: '1.25rem' }}>
|
||||
<label style={labelStyle}>Interne Notizen</label>
|
||||
<textarea
|
||||
style={{ ...inputStyle, minHeight: 52, resize: 'vertical' }}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Interne Bemerkungen zum Vorgang..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={createMutation.isPending}
|
||||
style={{ padding: '0.5rem 1rem', background: 'transparent', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', fontSize: '0.875rem', cursor: 'pointer', color: 'var(--color-text-secondary)' }}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending || projectDealTypes.length === 0}
|
||||
style={{ padding: '0.5rem 1.25rem', background: 'var(--color-primary)', color: 'white', border: 'none', borderRadius: 'var(--radius-sm)', fontSize: '0.875rem', fontWeight: 600, cursor: createMutation.isPending ? 'wait' : 'pointer', opacity: (createMutation.isPending || projectDealTypes.length === 0) ? 0.6 : 1 }}
|
||||
>
|
||||
{createMutation.isPending ? 'Anlegen...' : 'Projektanfrage anlegen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue