From ec9f3ea36434fbb7306328fec335dd230007fc10 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Thu, 12 Mar 2026 20:20:30 +0100 Subject: [PATCH] =?UTF-8?q?feat(crm):=20UI/UX=20Redesign=20=E2=80=93=20Kon?= =?UTF-8?q?takt=20Eingabemaske=20&=20Detailansicht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neue Drawer-Komponente (right-side slide-in, sticky footer, Portal) - ContactFormModal: Modal → Drawer, Tab 1 Allgemein / Tab 2 Details & Adresse - Checkbox "Adresse vom Unternehmen übernehmen" (blendet Adressfelder aus) - Typ-Feld entfernt (Kontakte immer PERSON) - ContactDetailPage: Subtitle "Position @ Unternehmen" im Header - Kontaktdaten-Card: 2 Sub-Spalten, kein Firma-Duplikat, Mobil tel:-Link, LinkedIn-Icon - Neue Card "Notizen & Tags" (Badges + Notizen-Text + Custom Fields) - Aktivitäten-Spalte auf minmax(380px, 40%) verbreitert Co-Authored-By: Claude Sonnet 4.6 --- Summarize.md | 32 +- .../frontend/src/components/Drawer.module.css | 97 ++ packages/frontend/src/components/Drawer.tsx | 69 ++ .../crm/contacts/ContactDetailPage.module.css | 67 +- .../src/crm/contacts/ContactDetailPage.tsx | 490 ++++----- .../src/crm/contacts/ContactFormModal.tsx | 969 ++++++++++-------- 6 files changed, 1073 insertions(+), 651 deletions(-) create mode 100644 packages/frontend/src/components/Drawer.module.css create mode 100644 packages/frontend/src/components/Drawer.tsx diff --git a/Summarize.md b/Summarize.md index aaa908a..7463d12 100644 --- a/Summarize.md +++ b/Summarize.md @@ -1,11 +1,41 @@ # INSIGHT MVP - Aenderungsprotokoll -## Stand: 2026-03-08 +## Stand: 2026-03-12 ### Aktueller Sprint: Sprint 1 (Alpha) --- +### Aenderungen 2026-03-12: CRM UI/UX Redesign – Kontakt-Entitaet + +#### Drawer-Komponente (neu) +- `packages/frontend/src/components/Drawer.tsx` – Wiederverwendbares Right-Side-Drawer-Pattern mit Portal, ESC-Key, Backdrop-Click, optionalem Footer +- `packages/frontend/src/components/Drawer.module.css` – Animiertes Slide-In von rechts, sticky Header + Footer, scrollbarer Body + +#### ContactFormModal – Redesign (Modal → Drawer + Tabs) +- Gewechselt von `` (zentriert) zu `` (von rechts, 540px) +- Zwei-Tab-Struktur: + - Tab "Allgemein": Vorname, Nachname, Unternehmen (Autocomplete), Position, Abteilung, E-Mail, Telefon, Mobil, LinkedIn + - Tab "Details & Adresse": Geburtsdatum, Quelle, Status, Website, Adresse, Notizen, Tags, Custom Fields +- "Adresse vom Unternehmen uebernehmen" Checkbox: wird angezeigt wenn Unternehmen verlinkt ist; bei aktivierter Checkbox werden Adressfelder ausgeblendet und die Adresse wird aus dem verlinkten Unternehmen uebernommen +- "Typ"-Feld entfernt (Kontakte sind immer Personen; ORGANIZATION-Typ bleibt intern erhalten) +- Sticky Footer (Abbrechen / Anlegen|Speichern) via HTML5 `form` Attribut +- Adress-Duplikate bereinigt + +#### ContactDetailPage – Redesign +- **Header**: Subtitle "Position @ Unternehmen" unter dem Namen (z.B. "Geschaeftsfuehrerin @ team neusta SE"), Status-Dot inline +- **Kontaktdaten-Card**: Zwei Sub-Spalten (Kommunikation links, Kontext rechts), Adresse als volle Breite darunter + - Entfernt: Vorname/Nachname (im Header), Firma-Duplikat (nur noch "Unternehmen" als Link) + - Hinzugefuegt: Mobil als `tel:`-Link, LinkedIn mit Icon, Geburtsdatum, Quelle, Abteilung +- **Neue Card "Notizen & Tags"**: Tags als Badges, Notizen-Text, Custom Fields +- **Layout**: Rechte Spalte (Aktivitaeten) auf `minmax(380px, 40%)` verbreitert (min. 1/3 fuer kuenftigen O365-Feed) +- **CSS**: `.infoColumns` (2-spaltig), `.addressRow`, `.subtitle`, `.nameRow` neu + +#### TypeScript +- `npx tsc --noEmit` in packages/frontend: 0 Fehler + +--- + ### Aenderungen in dieser Session #### 1. Projektinitialisierung & Infrastruktur-Definition diff --git a/packages/frontend/src/components/Drawer.module.css b/packages/frontend/src/components/Drawer.module.css new file mode 100644 index 0000000..0edf262 --- /dev/null +++ b/packages/frontend/src/components/Drawer.module.css @@ -0,0 +1,97 @@ +/* ============================================================ + Drawer – Right-side slide-in panel + ============================================================ */ + +.overlay { + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(0, 0, 0, 0.4); + animation: fadeIn 0.2s ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.panel { + position: fixed; + top: 0; + right: 0; + height: 100vh; + max-width: 100vw; + background: var(--color-bg-card, #fff); + box-shadow: -4px 0 32px rgba(0, 0, 0, 0.18); + display: flex; + flex-direction: column; + animation: slideIn 0.25s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes slideIn { + from { transform: translateX(100%); } + to { transform: translateX(0); } +} + +/* ---- Header ---- */ +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.25rem 1.5rem; + border-bottom: 1px solid var(--color-border, #e5e7eb); + flex-shrink: 0; + background: var(--color-bg-card, #fff); +} + +.title { + font-size: 1.125rem; + font-weight: 600; + margin: 0; + color: var(--color-text); +} + +.closeButton { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + font-size: 1.5rem; + line-height: 1; + color: var(--color-text-muted, #9ca3af); + background: none; + border: none; + border-radius: var(--radius-sm, 4px); + cursor: pointer; + transition: all 0.15s; + flex-shrink: 0; +} + +.closeButton:hover { + background: var(--color-bg, #f3f4f6); + color: var(--color-text, #111827); +} + +/* ---- Body (scrollable) ---- */ +.body { + flex: 1; + overflow-y: auto; + padding: 1.5rem; + overscroll-behavior: contain; +} + +/* ---- Footer (sticky) ---- */ +.footer { + padding: 1rem 1.5rem; + border-top: 1px solid var(--color-border, #e5e7eb); + flex-shrink: 0; + background: var(--color-bg-card, #fff); +} + +/* ---- Responsive ---- */ +@media (max-width: 600px) { + .panel { + width: 100% !important; + } +} diff --git a/packages/frontend/src/components/Drawer.tsx b/packages/frontend/src/components/Drawer.tsx new file mode 100644 index 0000000..cb39966 --- /dev/null +++ b/packages/frontend/src/components/Drawer.tsx @@ -0,0 +1,69 @@ +import { useEffect, useCallback, type ReactNode } from 'react'; +import { createPortal } from 'react-dom'; +import styles from './Drawer.module.css'; + +interface DrawerProps { + isOpen: boolean; + onClose: () => void; + title: string; + children: ReactNode; + footer?: ReactNode; + width?: string; +} + +export function Drawer({ + isOpen, + onClose, + title, + children, + footer, + width = '520px', +}: DrawerProps) { + const handleEscape = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }, + [onClose], + ); + + useEffect(() => { + if (isOpen) { + document.addEventListener('keydown', handleEscape); + document.body.style.overflow = 'hidden'; + } + return () => { + document.removeEventListener('keydown', handleEscape); + document.body.style.overflow = ''; + }; + }, [isOpen, handleEscape]); + + if (!isOpen) return null; + + return createPortal( +
+
e.stopPropagation()} + role="dialog" + aria-modal="true" + aria-label={title} + > +
+

{title}

+ +
+
{children}
+ {footer &&
{footer}
} +
+
, + document.body, + ); +} diff --git a/packages/frontend/src/crm/contacts/ContactDetailPage.module.css b/packages/frontend/src/crm/contacts/ContactDetailPage.module.css index a7dc7a2..e09e0d9 100644 --- a/packages/frontend/src/crm/contacts/ContactDetailPage.module.css +++ b/packages/frontend/src/crm/contacts/ContactDetailPage.module.css @@ -2,6 +2,7 @@ ContactDetailPage – Layout & Komponenten ============================================================ */ +/* ---- Navigation ---- */ .backLink { display: inline-flex; align-items: center; @@ -17,9 +18,10 @@ color: var(--color-primary); } +/* ---- Header ---- */ .header { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; margin-bottom: 1.5rem; gap: 1rem; @@ -28,8 +30,15 @@ .headerLeft { display: flex; - align-items: center; + align-items: flex-start; gap: 0.75rem; + min-width: 0; +} + +.nameRow { + display: flex; + align-items: center; + gap: 0.625rem; } .name { @@ -39,19 +48,27 @@ color: var(--color-text); } +.subtitle { + font-size: 0.9375rem; + color: var(--color-text-muted); + margin: 0.2rem 0 0; +} + +/* ---- Main layout ---- */ .layout { display: grid; - grid-template-columns: 1fr 360px; + grid-template-columns: 1fr minmax(380px, 40%); gap: 1.5rem; align-items: start; } -@media (max-width: 900px) { +@media (max-width: 960px) { .layout { grid-template-columns: 1fr; } } +/* ---- Card ---- */ .card { background: var(--color-bg-card); border-radius: var(--radius-md); @@ -67,16 +84,34 @@ color: var(--color-text); } +/* ---- Two-column info layout inside a card ---- */ +.infoColumns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.25rem 2rem; + align-items: start; +} + +@media (max-width: 640px) { + .infoColumns { + grid-template-columns: 1fr; + gap: 0; + } +} + +/* ---- Info grid (label | value pairs) ---- */ .infoGrid { display: grid; - grid-template-columns: 110px 1fr; - gap: 0.5rem 1rem; + grid-template-columns: 100px 1fr; + gap: 0.5rem 0.75rem; font-size: 0.875rem; + align-content: start; } .infoLabel { color: var(--color-text-muted); font-weight: 500; + padding-top: 0.0625rem; /* optical alignment */ } .infoValue { @@ -84,7 +119,14 @@ word-break: break-word; } -/* Tags */ +/* ---- Address row (full-width below infoColumns) ---- */ +.addressRow { + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--color-border); +} + +/* ---- Tags ---- */ .tags { display: flex; flex-wrap: wrap; @@ -101,7 +143,7 @@ font-weight: 500; } -/* Timeline */ +/* ---- Timeline ---- */ .timeline { display: flex; flex-direction: column; @@ -155,16 +197,11 @@ white-space: pre-wrap; } -/* Notes */ -.notesSection { - margin-top: 1.5rem; - padding-top: 1rem; - border-top: 1px solid var(--color-border); -} - +/* ---- Notes ---- */ .notesText { font-size: 0.875rem; color: var(--color-text-secondary); white-space: pre-wrap; line-height: 1.5; + margin: 0; } diff --git a/packages/frontend/src/crm/contacts/ContactDetailPage.tsx b/packages/frontend/src/crm/contacts/ContactDetailPage.tsx index 1f1cf94..0bba98c 100644 --- a/packages/frontend/src/crm/contacts/ContactDetailPage.tsx +++ b/packages/frontend/src/crm/contacts/ContactDetailPage.tsx @@ -6,20 +6,10 @@ import { ActivityFormModal } from '../activities/ActivityFormModal'; import { Modal } from '../../components/Modal'; import { LexwareSection } from '../lexware/LexwareSection'; import { CustomFieldsDisplay } from '../CustomFieldsDisplay'; -import type { Contact, Activity, ActivityType, ContactType } from '../types'; +import type { Contact, Activity, ActivityType } from '../types'; import { CONTACT_SOURCE_LABELS, ENTITY_STATUS_LABELS } from '../types'; import styles from './ContactDetailPage.module.css'; -const TYPE_COLORS: Record = { - PERSON: { bg: '#dbeafe', color: '#1e40af' }, - ORGANIZATION: { bg: '#d1fae5', color: '#065f46' }, -}; - -const TYPE_LABELS: Record = { - PERSON: 'Person', - ORGANIZATION: 'Organisation', -}; - const ACTIVITY_TYPE_LABELS: Record = { NOTE: 'Notiz', CALL: 'Anruf', @@ -73,6 +63,8 @@ function activityIcon(type: ActivityType): React.ReactNode { ); + default: + return null; } } @@ -96,6 +88,21 @@ const currencyFormatter = new Intl.NumberFormat('de-DE', { currency: 'EUR', }); +// LinkedIn icon SVG +function LinkedInIcon() { + return ( + + + + ); +} + export function ContactDetailPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); @@ -107,7 +114,7 @@ export function ContactDetailPage() { const [isActivityOpen, setActivityOpen] = useState(false); const [isDeleteOpen, setDeleteOpen] = useState(false); - if (isLoading) return

Laden...

; + if (isLoading) return

Laden…

; if (error || !data) return (

@@ -119,9 +126,14 @@ export function ContactDetailPage() { const activities: Activity[] = contact.activities ?? []; const deals = dealsData?.data ?? []; + // Subtitle: "Position @ Unternehmen" + const subtitle = [contact.position, contact.company?.name] + .filter(Boolean) + .join(' @ '); + return (

- {/* Zurück */} + {/* Back link */} - {/* Header */} + {/* ── Header ── */}
-

{contactDisplayName(contact)}

- - {TYPE_LABELS[contact.type]} - - +
+
+

{contactDisplayName(contact)}

+ +
+ {subtitle && ( +

{subtitle}

+ )} +
-
+
- {/* 2-Spalten Layout */} + {/* ── Two-column layout ── */}
- {/* Links: Info + Deals */} + {/* ── Left column ── */}
- {/* Info Card */} + {/* Contact data card */}

Kontaktdaten

-
- {contact.type === 'PERSON' && contact.firstName && ( - <> - Vorname - {contact.firstName} - - )} - {contact.type === 'PERSON' && contact.lastName && ( - <> - Nachname - {contact.lastName} - - )} - {contact.companyName && ( - <> - Firma - - {contact.companyName} - - - )} - {contact.company && ( - <> - Unternehmen - - + {/* Left: communication */} +
+ {contact.email && ( + <> + E-Mail + + + {contact.email} + + + + )} + {contact.phone && ( + <> + Telefon + {contact.phone} + + )} + {contact.mobile && ( + <> + Mobil + + + {contact.mobile} + + + + )} + {contact.linkedinUrl && ( + <> + LinkedIn + + + + {contact.linkedinUrl.replace( + /^https?:\/\/(www\.)?linkedin\.com\/in\//, + '', + )} + + + + )} + {contact.website && ( + <> + Website + + + {contact.website} + + + + )} +
+ + {/* Right: context */} +
+ {contact.company && ( + <> + Unternehmen + + + {contact.company.name} + + {contact.company.industry && ( + + ({contact.company.industry}) + + )} + + + )} + {contact.position && ( + <> + Position + + {contact.position} + + + )} + {contact.department && ( + <> + Abteilung + + {contact.department} + + + )} + {contact.birthday && ( + <> + Geburtsdatum + + {new Date(contact.birthday).toLocaleDateString('de-DE')} + + + )} + {contact.source && ( + <> + Quelle + + {CONTACT_SOURCE_LABELS[contact.source] ?? contact.source} + + + )} + {contact.status && contact.status !== 'ACTIVE' && ( + <> + Status + - {contact.company.name} - - {contact.company.industry && ( - - ({contact.company.industry}) - - )} - - - )} - {contact.position && ( - <> - Position - {contact.position} - - )} - {contact.email && ( - <> - E-Mail - - - {contact.email} - - - - )} - {contact.phone && ( - <> - Telefon - {contact.phone} - - )} - {contact.mobile && ( - <> - Mobil - {contact.mobile} - - )} - {contact.website && ( - <> - Website - - - {contact.website} - - - - )} - {contact.linkedinUrl && ( - <> - LinkedIn - - - {contact.linkedinUrl.replace(/^https?:\/\/(www\.)?linkedin\.com\/in\//, '')} - - - - )} - {contact.department && ( - <> - Abteilung - {contact.department} - - )} - {contact.birthday && ( - <> - Geburtstag - - {new Date(contact.birthday).toLocaleDateString('de-DE')} - - - )} - {contact.source && ( - <> - Quelle - - {CONTACT_SOURCE_LABELS[contact.source] ?? contact.source} - - - )} - {contact.status && contact.status !== 'ACTIVE' && ( - <> - Status - - {ENTITY_STATUS_LABELS[contact.status] ?? contact.status} - - - )} - {(contact.street || contact.zip || contact.city) && ( - <> + {ENTITY_STATUS_LABELS[contact.status] ?? contact.status} + + + )} +
+
+ + {/* Address — full-width below sub-columns */} + {(contact.street || contact.zip || contact.city) && ( +
+
Adresse - {contact.street && <>{contact.street}
} + {contact.street && ( + <> + {contact.street} +
+ + )} {contact.zip} {contact.city} {contact.country && contact.country !== 'DE' && ( <>, {contact.country} )}
- - )} -
- - {/* Tags */} - {contact.tags && contact.tags.length > 0 && ( -
- - Tags - -
- {contact.tags.map((tag) => ( - - {tag} - - ))}
)} - - {/* Notizen */} - {contact.notes && ( -
- - Notizen - -

{contact.notes}

-
- )} - - {/* Custom Fields */} - {contact.customFields && contact.customFields.length > 0 && ( - - )}
- {/* Verknüpfte Vorgänge */} + {/* Notizen & Tags card */} + {((contact.tags && contact.tags.length > 0) || + contact.notes || + (contact.customFields && contact.customFields.length > 0)) && ( +
+

Notizen & Tags

+ + {contact.tags && contact.tags.length > 0 && ( +
+
+ {contact.tags.map((tag) => ( + + {tag} + + ))} +
+
+ )} + + {contact.notes && ( +

+ {contact.notes} +

+ )} + + {contact.customFields && contact.customFields.length > 0 && ( +
+ +
+ )} +
+ )} + + {/* Linked deals */} {deals.length > 0 && (

@@ -397,11 +432,7 @@ export function ContactDetailPage() {

- + @@ -508,7 +536,7 @@ export function ContactDetailPage() { - {/* Rechts: Aktivitäten-Timeline */} + {/* ── Right column: Activities ── */}
-
{act.subject}
+
+ {act.subject} +
{ACTIVITY_TYPE_LABELS[act.type]} ·{' '} {formatDate(act.createdAt)} @@ -573,7 +603,7 @@ export function ContactDetailPage() {
- {/* Modals */} + {/* ── Modals ── */} setEditOpen(false)} @@ -588,7 +618,7 @@ export function ContactDetailPage() { onSuccess={() => setActivityOpen(false)} /> - {/* Löschen-Modal */} + {/* Delete confirmation */} setDeleteOpen(false)} @@ -656,7 +686,7 @@ export function ContactDetailPage() { opacity: deleteMutation.isPending ? 0.7 : 1, }} > - {deleteMutation.isPending ? 'Löschen...' : 'Endgültig löschen'} + {deleteMutation.isPending ? 'Löschen…' : 'Endgültig löschen'}
diff --git a/packages/frontend/src/crm/contacts/ContactFormModal.tsx b/packages/frontend/src/crm/contacts/ContactFormModal.tsx index 3bbb2e5..7e37d8f 100644 --- a/packages/frontend/src/crm/contacts/ContactFormModal.tsx +++ b/packages/frontend/src/crm/contacts/ContactFormModal.tsx @@ -1,9 +1,16 @@ import { useState, useEffect, useRef, useCallback } from 'react'; -import { Modal } from '../../components/Modal'; +import { Drawer } from '../../components/Drawer'; import { useCreateContact, useUpdateContact, useSetCustomFieldValues } from '../hooks'; import { companiesApi } from '../api'; import { CustomFieldsForm } from '../CustomFieldsForm'; -import type { Contact, ContactType, ContactSource, EntityStatus, Company, CustomFieldValue } from '../types'; +import type { + Contact, + ContactType, + ContactSource, + EntityStatus, + Company, + CustomFieldValue, +} from '../types'; import { CONTACT_SOURCE_LABELS, ENTITY_STATUS_LABELS } from '../types'; interface ContactFormModalProps { @@ -13,6 +20,10 @@ interface ContactFormModalProps { onSuccess: () => void; } +type FormTab = 'general' | 'details'; + +const FORM_ID = 'contact-form-drawer'; + const labelStyle: React.CSSProperties = { fontSize: '0.875rem', fontWeight: 500, @@ -39,6 +50,18 @@ const rowStyle: React.CSSProperties = { gap: '0.75rem', }; +const sectionLabelStyle: React.CSSProperties = { + fontSize: '0.75rem', + fontWeight: 600, + textTransform: 'uppercase', + letterSpacing: '0.05em', + color: 'var(--color-text-muted)', + margin: '1.5rem 0 0.75rem', + paddingBottom: '0.5rem', + borderBottom: '1px solid var(--color-border)', + display: 'block', +}; + export function ContactFormModal({ isOpen, onClose, @@ -52,7 +75,11 @@ export function ContactFormModal({ const mutation = isEditMode ? updateMutation : createMutation; const [error, setError] = useState(''); - const customFieldValuesRef = useRef>({}); + const [activeTab, setActiveTab] = useState('general'); + + const customFieldValuesRef = useRef< + Record + >({}); const customFields: CustomFieldValue[] = contact?.customFields ?? []; const handleCustomFieldsChange = useCallback( (values: Record) => { @@ -60,6 +87,9 @@ export function ContactFormModal({ }, [], ); + + // type is preserved internally for existing ORGANIZATION contacts, + // but new contacts always default to PERSON (no UI selector) const [type, setType] = useState('PERSON'); const [firstName, setFirstName] = useState(''); const [lastName, setLastName] = useState(''); @@ -80,8 +110,10 @@ export function ContactFormModal({ const [source, setSource] = useState(''); const [department, setDepartment] = useState(''); const [status, setStatus] = useState('ACTIVE'); + // When a company is linked, the address can be inherited from it + const [useCompanyAddress, setUseCompanyAddress] = useState(false); - // Unternehmen-Suche + // Company autocomplete const [companySearch, setCompanySearch] = useState(''); const [companyResults, setCompanyResults] = useState([]); const [selectedCompany, setSelectedCompany] = useState<{ @@ -92,7 +124,7 @@ export function ContactFormModal({ const companyRef = useRef(null); const companySearchTimeout = useRef>(); - // Click-Outside für Unternehmen-Dropdown + // Click-outside for company dropdown useEffect(() => { function handleClick(e: MouseEvent) { if ( @@ -106,7 +138,7 @@ export function ContactFormModal({ return () => document.removeEventListener('mousedown', handleClick); }, []); - // Unternehmen suchen (debounced) + // Company search (debounced) useEffect(() => { if (companySearchTimeout.current) clearTimeout(companySearchTimeout.current); if (!companySearch || companySearch.length < 2) { @@ -130,9 +162,11 @@ export function ContactFormModal({ }; }, [companySearch]); + // Initialise form when drawer opens useEffect(() => { if (isOpen) { setError(''); + setActiveTab('general'); if (contact) { setType(contact.type); setFirstName(contact.firstName ?? ''); @@ -150,16 +184,24 @@ export function ContactFormModal({ setTagsInput((contact.tags ?? []).join(', ')); setPosition(contact.position ?? ''); setLinkedinUrl(contact.linkedinUrl ?? ''); - setBirthday(contact.birthday ? contact.birthday.split('T')[0] : ''); + setBirthday( + contact.birthday ? contact.birthday.split('T')[0] : '', + ); setSource((contact.source as ContactSource) ?? ''); setDepartment(contact.department ?? ''); setStatus(contact.status ?? 'ACTIVE'); if (contact.company) { - setSelectedCompany({ id: contact.company.id, name: contact.company.name }); + setSelectedCompany({ + id: contact.company.id, + name: contact.company.name, + }); setCompanySearch(contact.company.name); + // Default: use company address when contact has no own address + setUseCompanyAddress(!contact.street); } else { setSelectedCompany(null); setCompanySearch(''); + setUseCompanyAddress(false); } } else { setType('PERSON'); @@ -184,6 +226,7 @@ export function ContactFormModal({ setStatus('ACTIVE'); setSelectedCompany(null); setCompanySearch(''); + setUseCompanyAddress(true); // default: inherit from company once one is selected } setCompanyResults([]); setShowCompanyDropdown(false); @@ -199,6 +242,17 @@ export function ContactFormModal({ .map((t) => t.trim()) .filter(Boolean); + // Only include address when not inheriting from company + const addressPayload = + !selectedCompany || !useCompanyAddress + ? { + ...(street ? { street } : {}), + ...(zip ? { zip } : {}), + ...(city ? { city } : {}), + country, + } + : {}; + const payload = { type, ...(type === 'PERSON' ? { firstName, lastName } : { companyName }), @@ -211,10 +265,7 @@ export function ContactFormModal({ ...(source ? { source } : {}), ...(department ? { department } : {}), status, - ...(street ? { street } : {}), - ...(zip ? { zip } : {}), - ...(city ? { city } : {}), - country, + ...addressPayload, ...(notes ? { notes } : {}), ...(tags.length > 0 ? { tags } : {}), ...(selectedCompany ? { companyId: selectedCompany.id } : {}), @@ -228,7 +279,10 @@ export function ContactFormModal({ setCustomFieldValues.mutate({ entityId, data: { - values: entries.map(([fieldDefId, value]) => ({ fieldDefId, value })), + values: entries.map(([fieldDefId, value]) => ({ + fieldDefId, + value, + })), }, }); }; @@ -243,8 +297,11 @@ export function ContactFormModal({ }, onError: (err: unknown) => { const msg = - (err as { response?: { data?: { error?: { message?: string } } } }) - ?.response?.data?.error?.message ?? 'Fehler beim Speichern'; + ( + err as { + response?: { data?: { error?: { message?: string } } }; + } + )?.response?.data?.error?.message ?? 'Fehler beim Speichern'; setError(msg); }, }, @@ -257,412 +314,514 @@ export function ContactFormModal({ }, onError: (err: unknown) => { const msg = - (err as { response?: { data?: { error?: { message?: string } } } }) - ?.response?.data?.error?.message ?? 'Fehler beim Anlegen'; + ( + err as { + response?: { data?: { error?: { message?: string } } }; + } + )?.response?.data?.error?.message ?? 'Fehler beim Anlegen'; setError(msg); }, }); } }; + const tabBtnStyle = (tab: FormTab): React.CSSProperties => ({ + padding: '0.5rem 1.25rem', + background: 'none', + border: 'none', + borderBottom: `2px solid ${activeTab === tab ? 'var(--color-primary)' : 'transparent'}`, + cursor: 'pointer', + fontSize: '0.9375rem', + fontWeight: activeTab === tab ? 600 : 400, + color: + activeTab === tab + ? 'var(--color-primary)' + : 'var(--color-text-secondary)', + marginBottom: '-1px', + transition: 'color 0.15s, border-color 0.15s', + whiteSpace: 'nowrap', + }); + + const drawerFooter = ( +
+ + +
+ ); + return ( - -
- {error && ( -
- {error} -
- )} - - {/* Typ */} -
- - -
- - {/* Unternehmen-Suche */} -
- - { - setCompanySearch(e.target.value); - if (selectedCompany) setSelectedCompany(null); - }} - onFocus={() => { - if (companyResults.length > 0) setShowCompanyDropdown(true); - }} - placeholder="Unternehmen suchen..." - /> - {selectedCompany && ( - - )} - {showCompanyDropdown && companyResults.length > 0 && ( -
- {companyResults.map((comp) => ( -
{ - setSelectedCompany({ id: comp.id, name: comp.name }); - setCompanySearch(comp.name); - setShowCompanyDropdown(false); - }} - style={{ - padding: '0.5rem 0.75rem', - cursor: 'pointer', - fontSize: '0.875rem', - borderBottom: '1px solid var(--color-border)', - }} - onMouseEnter={(e) => - ((e.target as HTMLDivElement).style.background = - 'var(--color-bg)') - } - onMouseLeave={(e) => - ((e.target as HTMLDivElement).style.background = - 'transparent') - } - > - {comp.name} - {comp.industry && ( - - {comp.industry} - - )} -
- ))} -
- )} -
- - {/* Position + Abteilung (nur bei Person) */} - {type === 'PERSON' && ( -
-
- - setPosition(e.target.value)} - placeholder="z.B. Geschaeftsfuehrer" - /> -
-
- - setDepartment(e.target.value)} - placeholder="z.B. Einkauf, Vertrieb" - /> -
-
- )} - - {/* Name */} - {type === 'PERSON' ? ( -
-
- - setFirstName(e.target.value)} - placeholder="Max" - /> -
-
- - setLastName(e.target.value)} - placeholder="Mustermann" - /> -
-
- ) : ( -
- - setCompanyName(e.target.value)} - placeholder="Muster GmbH" - /> -
- )} - - {/* E-Mail */} -
- - setEmail(e.target.value)} - placeholder="mail@example.com" - /> -
- - {/* Telefon / Mobil */} -
-
- - setPhone(e.target.value)} - placeholder="+49 ..." - /> -
-
- - setMobile(e.target.value)} - placeholder="+49 ..." - /> -
-
- - {/* Website + LinkedIn */} -
-
- - setWebsite(e.target.value)} - placeholder="https://..." - /> -
-
- - setLinkedinUrl(e.target.value)} - placeholder="https://linkedin.com/in/..." - /> -
-
- - {/* Geburtsdatum + Quelle + Status */} -
- {type === 'PERSON' && ( -
- - setBirthday(e.target.value)} - /> -
- )} -
- - -
-
- - -
-
- - {/* Adresse */} -
- - setStreet(e.target.value)} - /> -
-
-
- - setZip(e.target.value)} - /> -
-
- - setCity(e.target.value)} - /> -
-
-
- - setCountry(e.target.value)} - placeholder="DE" - maxLength={2} - /> -
- - {/* Notizen */} -
- -
navigate(`/crm/deals/${deal.id}`)} > {deal.title}