diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e1647bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.env +.env.* + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# .env.example soll mit ins Repo - sie enthaelt nur Platzhalter +!server/.env.example diff --git a/Draw/Ansicht Experten.png b/Draw/Ansicht Experten.png new file mode 100644 index 0000000..4059088 Binary files /dev/null and b/Draw/Ansicht Experten.png differ diff --git a/Draw/Ansicht für Kunden.jpg b/Draw/Ansicht für Kunden.jpg new file mode 100644 index 0000000..da095b0 Binary files /dev/null and b/Draw/Ansicht für Kunden.jpg differ diff --git a/Draw/App Zeichnung.pdf b/Draw/App Zeichnung.pdf new file mode 100644 index 0000000..bf4f7b6 Binary files /dev/null and b/Draw/App Zeichnung.pdf differ diff --git a/Draw/App Zeichnung.pptx b/Draw/App Zeichnung.pptx new file mode 100644 index 0000000..dc86642 Binary files /dev/null and b/Draw/App Zeichnung.pptx differ diff --git a/Draw/Ausschreibungen.png b/Draw/Ausschreibungen.png new file mode 100644 index 0000000..c75f7a1 Binary files /dev/null and b/Draw/Ausschreibungen.png differ diff --git a/Draw/Noitzen.txt b/Draw/Noitzen.txt new file mode 100644 index 0000000..e180844 --- /dev/null +++ b/Draw/Noitzen.txt @@ -0,0 +1,14 @@ +18.02.2026 - Kai + +Anforderungen an die Vertriebssoftware + +- Kontakte +> Wer, wo, Position, Mail, Rufnummer, Linkedin +- Kanban Board (Drag & Drop) + + +Random: +- Reminder Funktion wenn ein Projekt ausläuft (2 Monate vorher) +- Bots die Recherche bzgl. Stellenausschreibungen / Projekte fahren und auf der anderen Seite nach Personen suchen können. +- Automatisierter CV (ggf. via Bots) > Kontaktdaten auf Kai ändern +- Automatisierte freundliche Absage mich Chance für das nächste Projekt \ No newline at end of file diff --git a/Featureliste.txt b/Featureliste.txt new file mode 100644 index 0000000..867ea98 --- /dev/null +++ b/Featureliste.txt @@ -0,0 +1,106 @@ +14.09.2026 + +Was muss geändert werden: + +- Verfügbarkeitsstatus entfernen aus den Kontakten +- Können wir eine Liste automatisiert erstellen, der letzte Arbeitgeber (Branche & Unternehmen) + > Würde in den "UNternehmen" erscheinen und dort im Stammbaum der "Verlasenen" anzeigen + > "Unternehmenskarte" > Eine Interaktive Karte mit allen bekannten Unternhemen + - Historie der Externen + - Unsere Kunden + - etc. + > Ähnlich zur Netzwerkansicht im perinco CRM +- Wenn man auf den Hintergrund klickt soll sich das Fenster vorne schließen +- Pro Berater eine Historie (Log was haben wir mit ihm bereits gemacht, zB vorgestellt bei Kunde XY) + > Bei Kunden entsprechend ebenfalls darstellen, dort muss es eine Historie geben was für oder bei dem Kunden gleaufen ist (z.B. Bertaer XY wurde beworben + Datum etc. / Vertrag wurde geschlossen am XY von XY) +- Ansicht der Kunden so aufbauen wie die Expertenansicht > heißt dann Unternehmen +- NEU: Mit @ Zeichen Unternehmen oder Kontakte bennen können in allen Texten. Jedes mal wenn ein @ verwendet wurde, muss dies in der Historie ebenfalls auftauchen. +- Wenn Personen oder Unternehmen via @ hinzugefügt wurden, sollen diese anklickbar sein und jeweils das entsprechende Profil geöffnet werden. +- Das gesamte CRM muss durchsuchbar sein (ähnlich Google) bei dem ich nach Inhalten Personen Verträgen etc. suchen kann. + > Die vorhandene Sucher ausbauen/ersetzen +- Kunden wird in die neue Kategorie Unternehmen einbezogen und via "Umschalter" kann man dann zwischen unseren Kunden (aktiv/alumni) und allen Unternehmen umschalten. Also da wo ein Haken bei "aktiver Kunde" im Unternehmen hinterlegt ist. + > Am besten im Stammbaum Ansicht darstellen + - Ein Stammbaum Element "Verlassen", hier dann alle Ex Personen eintragen. Diese Info sollte auch in die Historie der jeweiligen Personen hinterlegt werden. +- Für Unternehmen und Kotnakte (ex Experten=: + - Arbeiten diese mit Freelancern oder nicht? + - Hatten wir schon einmal ein Interview oder nicht und falls ja wann mit wem? + > Historie jedoch je Kontakt oder Unternhemen in dessen Karte durchsuchbar machen. +- Live! Ansicht ist derzeit nicht bearbeitbar! + - Live Anischt wie im Perinco CRM. +- NEU: Lead Pipeline +- Für einen Lead Attribute zwischen "persönlicher Kontkat" oder "Noch kein Kontakt gehabt" +- NEU: Anlage von Expeten automatisiert (Upload CV Word/PDF) + > Auslesen und eintragen in CRM (inkl. CV anhängen) + > Die Option für einen Multiupload soll bestehen +- NEU: Reiter Historie > Hier sollen die letzte gesamten Tätigkeiten chronologisch aufgelistet werden: Timestamp|Aktivität (z.B. 14.09.'26 09:35| Hans Peter neu angelegt). +- NEU: LESENDER Zugriff auf perinco CRM für Kontakte! + - Mit Schalter einschalten oder Ausschalten und mit grünem Rahmen versehen. + - ACHTUNG: Auch im perinco CRM anpassen! +- Bei den Unterhmen ein Feld "Gruppe" das z.B. bei Der Finanzinformatik die Finanzinformatik zusammenfasst und unterhalb der Gruppe gibt es dann die einzelnen Firmen z.B. FI-Hamburg FI-Frankfurt etc. + - Falls nicht vorhandne ignorieren +- "Branche" der Unternehmen bei Unternehmen in Stammdaten hinterlegen und bearbeiten lassen. +- Mitarbeiterzahl für die Unternehmen hinzufügen +- Automatische Bildziehung bei Linkein (wie im perinco CRM) + - Ggf. auch aus Word bzw. PDF beim Import inkl. resize auf 400x400px (ca.) +- WICHTIG!! "Experten" muss "Kontakte" heißen da hier auch die Ansprechpartne der Kunden rein müssen inkl. Lead-Quelle! +- Für die Unternehmen ein 5-Sterne Rating (dies gibt Info darüber wie interessant der Kunde für uns ist) + - Filter dazu in die Netzwerkübersicht +- KANBAN zu den Leads erstellen + - Checkliste mit Historie innerhalb der einzelnen dort liegenden Kacheln als ToDo Liste behandeln + - Status: Für Später (Zurückgestellte), Neue Lead, Erstkontakt, 1. Gespräch, Followup geplant, Followup Gespräch, Closed, Absage + - Archiviert Unter-Reiter mit einer art Excelliste inkl. aller vergangenen Closed Leads oder Absagen. + - Inkl. Option zurückzuschieben + - In den Kacheln brauchen wir: + - Daten des Unternehmens, Daten des Ansprechpartners (Kontakt) + - Historie + - Next Steps / ToDos (Checkliste) + - WICHTIG: Reminderfunktion! Mit Timer Einstellung in Tagen (z.B. 2 Tage dann würde im CRM ein Pop-up aufgegen "Erinnerung: Kunde XY anrufen!") + + Autreminder: Wenn ein Lead älter als einen Monat ist, Pop-up: "Achtung, Lead seit einem Monat nicht kontaktiert! Soll dieser archiviert werden?". Falls nicht archiviert, Läuft Autoreminder von vorne (wieder ein Monat) +- Automatisch in der Historie (egal in welcher) Welcher User hat was bearbeitet +- Scraper für Stellenausschreibungen (ähnlich der Scanner). + - Kai liefert Quellen & + +- Morning Post für Kai > Anhand der letzten 30 Unternehmen die in der Historie zu letzt benannt wurden recherchieren und als eigene Zeitung jeden Morgen bereitstellen. +- Themes für Kai > Duknukem3d / Half-Life mit Crowbar + + + + + + + + + + + + + + + +ALT: + +Needed Features: + +Experten: +X CV mit hochladen bzw. Link hinterlegen für Onedrive +X Wenn man auf einen Kontakt klickt, soll die Karte im Bearbeitungsmodus geöffnet werden. +X 2. Ansicht - Listenansicht und einen Umsschalter um zwischen den Kacheln und Listen zu switchen. +X Tag Feld so bearbeiten, dass mit Komma getrennte Wörter bei "Hinzufügen" als einzelen Tags angelegt werden (Das Auslesetool soll später aus den CVs dann einen Komma getrennten Tag Text ausgeben um ihn hier einzufügen). +X Farbe ändern, Interview Bubble im Bild soll auch blau sein bei Interview und bei gebucht blau. + +Ausschreibungen: +X einen WYSIWYG Editor mit KI Verknüpfung zu OpenAI erstellen. + + +Kunden +X Überall wo Firma stehet in "Kunde" ändern +X Feld für eine Website hinzufügen +X Das Feld Hauptansprechparnter soll eine Auswahl aus den unten angelegten Ansprechpartnern sein. +X In der Übersicht eines Kunden, soll die Mailadresse der Ansprechpartner klickbar sein, damit sich direkt Outlook öffnet. +X Warum hat John und Jaja ein Bild und wo kann ich das hitnerlegen? + +Next Steps: +- Automatisiert PDF/Word Umwandlung in ICON - CVs +- Automatisierte CV Analyse inkl. Eintagung in Datenbank > PDF / Word auslesen. + > Drei Ordner (Eingang|Screening|Auswahl) in denen CVs abgelegt werden und von der KI analysiert + > Eine Liste soll anzeigen welche CVs zur Sichtung stehen, hier soll man mit ja oder nein die CVs zur KI übergeben können, welche dann diese in die Datenbank übernimmt. diff --git a/deploy/deploy.ps1 b/deploy/deploy.ps1 new file mode 100644 index 0000000..a64d73e --- /dev/null +++ b/deploy/deploy.ps1 @@ -0,0 +1,60 @@ +# ICON CRM - Deployment auf icrm.icon-experts.de (31.70.144.60) +# +# Gebaut wird lokal, hochgeladen werden nur dist/ und die Backend-Quellen. +# +# Aufruf aus dem Projektverzeichnis: +# $env:ICON_CRM_SSH_PASSWORD = '...' +# powershell -File deploy/deploy.ps1 +# powershell -File deploy/deploy.ps1 -SkipBuild +# powershell -File deploy/deploy.ps1 -Deps # bei neuen Backend-Paketen + +param( + [string]$ServerHost = '31.70.144.60', + [int]$Port = 22, + [string]$User = 'root', + [string]$Password = $env:ICON_CRM_SSH_PASSWORD, + [string]$RemotePath = '/var/www/icon_CRM', + [switch]$SkipBuild, + [switch]$Deps +) + +$ErrorActionPreference = 'Stop' + +if (-not $Password) { + throw 'Kein Passwort. Setze $env:ICON_CRM_SSH_PASSWORD oder uebergib -Password.' +} + +$plink = 'C:\Program Files\PuTTY\plink.exe' +$pscp = 'C:\Program Files\PuTTY\pscp.exe' +foreach ($tool in @($plink, $pscp)) { + if (-not (Test-Path $tool)) { throw "Nicht gefunden: $tool" } +} + +$projectRoot = Split-Path -Parent $PSScriptRoot +Set-Location $projectRoot + +if (-not $SkipBuild) { + Write-Host '==> Frontend bauen' -ForegroundColor Yellow + npm run build + if ($LASTEXITCODE -ne 0) { throw 'Build fehlgeschlagen - es wird nichts hochgeladen.' } +} + +$target = "$User@$ServerHost" + +Write-Host '==> dist/ hochladen' -ForegroundColor Yellow +& $pscp -r -batch -P $Port -pw $Password 'dist' "${target}:$RemotePath/" + +Write-Host '==> Backend hochladen' -ForegroundColor Yellow +& $pscp -r -batch -P $Port -pw $Password 'server\src' "${target}:$RemotePath/server/" +& $pscp -batch -P $Port -pw $Password 'server\package.json' "${target}:$RemotePath/server/" + +$remoteCommand = "systemctl restart icon-crm-backend; sleep 3; systemctl is-active icon-crm-backend; curl -s http://127.0.0.1:5007/api/health" +if ($Deps) { + $remoteCommand = "cd $RemotePath/server && npm install --omit=dev --no-audit --no-fund; $remoteCommand" +} + +Write-Host '==> Backend neu starten und pruefen' -ForegroundColor Yellow +& $plink -ssh -batch -P $Port -pw $Password $target $remoteCommand + +Write-Host '' +Write-Host 'Fertig.' -ForegroundColor Green diff --git a/deploy/icon-crm-backend.service b/deploy/icon-crm-backend.service new file mode 100644 index 0000000..8550552 --- /dev/null +++ b/deploy/icon-crm-backend.service @@ -0,0 +1,19 @@ +[Unit] +Description=ICON CRM Backend (Express + MariaDB) +After=network.target mariadb.service +Wants=mariadb.service + +[Service] +Type=simple +User=root +WorkingDirectory=/var/www/icon_CRM/server +EnvironmentFile=/var/www/icon_CRM/server/.env +ExecStart=/usr/bin/node src/server.js +Restart=on-failure +RestartSec=5 +# Der Server hat 8 GB. Die Grenze ist kein Sparzwang, sondern ein Sicherheitsnetz: +# ein Leck im Backend soll nicht Apache und MariaDB mitreissen. +MemoryMax=1024M + +[Install] +WantedBy=multi-user.target diff --git a/deploy/icrm.icon-experts.de.conf b/deploy/icrm.icon-experts.de.conf new file mode 100644 index 0000000..2b63fe4 --- /dev/null +++ b/deploy/icrm.icon-experts.de.conf @@ -0,0 +1,43 @@ +# ICON CRM - Apache Vhost +# Ablage: /etc/apache2/sites-available/icrm.icon-experts.de.conf +# Aktivieren: a2ensite icrm.icon-experts.de && systemctl reload apache2 +# +# Reihenfolge beim Aufsetzen: +# 1. DNS A-Record icrm.icon-experts.de -> 31.70.144.60 +# 2. Diese Datei aktivieren (HTTP-Teil), Apache neu laden +# 3. certbot --apache -d icrm.icon-experts.de (legt den :443-Block selbst an) + + + ServerName icrm.icon-experts.de + DocumentRoot /var/www/icon_CRM/dist + + + Options -Indexes +FollowSymLinks + AllowOverride None + Require all granted + # Single Page Application: alle unbekannten Pfade auf index.html + FallbackResource /index.html + + + # Die mit Hash benannten Build-Dateien aendern sich nie - die darf der + # Browser dauerhaft behalten. index.html dagegen nie, sonst sieht ein + # Benutzer nach dem Rollout weiter die alte Anwendung. + + ExpiresActive On + + ExpiresDefault "access plus 1 year" + + + + Header set Cache-Control "no-cache, must-revalidate" + + + # Die API laeuft lokal auf 127.0.0.1:5007 und ist von aussen + # ausschliesslich hierueber erreichbar. + ProxyPreserveHost On + ProxyPass /api http://127.0.0.1:5007/api + ProxyPassReverse /api http://127.0.0.1:5007/api + + ErrorLog ${APACHE_LOG_DIR}/icon_crm_error.log + CustomLog ${APACHE_LOG_DIR}/icon_crm_access.log combined + diff --git a/index.html b/index.html new file mode 100644 index 0000000..6e968f3 --- /dev/null +++ b/index.html @@ -0,0 +1,50 @@ + + + + + + + ICON CRM + + + + + + + + + + +
+ + + diff --git a/legacy/App.tsx b/legacy/App.tsx new file mode 100644 index 0000000..3f26f08 --- /dev/null +++ b/legacy/App.tsx @@ -0,0 +1,149 @@ + +import React, { useState, useEffect } from 'react'; +import { Sidebar } from './components/Sidebar'; +import { Header } from './components/Header'; +import { Dashboard } from './components/Dashboard'; +import { KanbanBoard } from './components/KanbanBoard'; +import { Tenders } from './components/Tenders'; +import { Customers } from './components/Customers'; +import { Experts } from './components/Experts'; +import { Projects } from './components/Projects'; +import { OrgChart } from './components/OrgChart'; +import { Settings } from './components/Settings'; +import { ViewState, Tender, Expert, Customer, Project } from './types'; +import { supabase, TABLES } from './lib/supabase'; + +const App: React.FC = () => { + const [currentView, setCurrentView] = useState('Dashboard'); + const [isDarkMode, setIsDarkMode] = useState(true); + const [isSidebarOpen, setIsSidebarOpen] = useState(true); + + const [tenders, setTenders] = useState([]); + const [customers, setCustomers] = useState([]); + const [experts, setExperts] = useState([]); + const [projects, setProjects] = useState([]); + const [isInitialLoading, setIsInitialLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); + + const fetchData = async (showRefreshIndicator = false) => { + if (showRefreshIndicator) setIsRefreshing(true); + try { + const { data: customersData } = await supabase + .from(TABLES.CUSTOMERS) + .select('*, icon_contacts(*)'); + + const formattedCustomers = (customersData || []).map(c => ({ + ...c, + contacts: c.icon_contacts || [] + })); + setCustomers(formattedCustomers); + + const { data: expertsData } = await supabase.from(TABLES.EXPERTS).select('*'); + setExperts(expertsData || []); + + const { data: projectsData } = await supabase.from(TABLES.PROJECTS).select('*'); + setProjects(projectsData || []); + + const { data: tendersData } = await supabase + .from(TABLES.TENDERS) + .select(` + *, + selectedCustomer:icon_customers(*), + selectedExperts:icon_tender_experts(icon_experts(*)) + `) + .neq('status', 'Archiviert'); + + if (tendersData) { + const formattedTenders: Tender[] = tendersData.map(t => ({ + id: t.id, + title: t.title, + source: t.source, + link: t.link, + details: t.details, + notes: t.notes, + status: t.status, + kanbanStatus: t.kanban_status, + selectedCustomer: t.selectedCustomer, + selectedExperts: t.selectedExperts?.map((se: any) => se.icon_experts) || [], + applicantsCount: t.selectedExperts?.length || 0 + })); + setTenders(formattedTenders); + } + } catch (error) { + console.error('Error fetching data:', error); + } finally { + setIsInitialLoading(false); + setIsRefreshing(false); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + useEffect(() => { + if (isDarkMode) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }, [isDarkMode]); + + const toggleDarkMode = () => setIsDarkMode(!isDarkMode); + + if (isInitialLoading) { + return ( +
+
+
+

ICON DATABASE CONNECTING...

+
+
+ ); + } + + const renderContent = () => { + switch (currentView) { + case 'Dashboard': return ; + case 'Live!': return fetchData(true)} />; + case 'Ausschreibungen': return fetchData(true)} />; + case 'Kunden': return fetchData(true)} />; + case 'Experten': return fetchData(true)} />; + case 'Laufende Projekte': return fetchData(true)} />; + case 'Organigramm': return ; + case 'Einstellungen': return ; + default: return ; + } + }; + + return ( +
+ setIsSidebarOpen(!isSidebarOpen)} + /> + +
+
setIsSidebarOpen(!isSidebarOpen)} + /> +
+ {isRefreshing && ( +
+
+ Synchronisiere... +
+ )} + {renderContent()} +
+
+
+ ); +}; + +export default App; diff --git a/legacy/components/Customers.tsx b/legacy/components/Customers.tsx new file mode 100644 index 0000000..95573d3 --- /dev/null +++ b/legacy/components/Customers.tsx @@ -0,0 +1,448 @@ + +import React, { useState, useEffect } from 'react'; +import { Customer, Contact } from '../types'; +import { supabase, TABLES } from '../lib/supabase'; + +interface CustomersProps { + customers: Customer[]; + refreshData: () => Promise; +} + +export const Customers: React.FC = ({ customers, refreshData }) => { + const [selectedCustomerId, setSelectedCustomerId] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [editingCustomer, setEditingCustomer] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [customerToDeleteId, setCustomerToDeleteId] = useState(null); + + const [customerForm, setCustomerForm] = useState({ + company: '', + name: '', + logo: '', + notes: '' + }); + + const [contactsForm, setContactsForm] = useState[]>([]); + + useEffect(() => { + if (!selectedCustomerId && customers.length > 0) { + setSelectedCustomerId(customers[0].id); + } + }, [customers, selectedCustomerId]); + + const activeCustomer = customers.find(c => c.id === selectedCustomerId) || customers[0]; + + const handleOpenCreate = () => { + setEditingCustomer(null); + setCustomerForm({ company: '', name: '', logo: '', notes: '' }); + setContactsForm([{ name: '', role: '', email: '', phone: '', avatar: '' }]); + setIsModalOpen(true); + }; + + const handleOpenEdit = (customer: Customer) => { + setEditingCustomer(customer); + setCustomerForm({ + company: customer.company || '', + name: customer.name || '', + logo: customer.logo || '', + notes: customer.notes || '' + }); + setContactsForm(customer.contacts && customer.contacts.length > 0 + ? customer.contacts + : [{ name: '', role: '', email: '', phone: '', avatar: '' }] + ); + setIsModalOpen(true); + }; + + const handleLogoUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => setCustomerForm(prev => ({ ...prev, logo: reader.result as string })); + reader.readAsDataURL(file); + } + }; + + const addContactRow = () => { + setContactsForm([...contactsForm, { name: '', role: '', email: '', phone: '', avatar: '' }]); + }; + + const removeContactRow = (index: number) => { + setContactsForm(contactsForm.filter((_, i) => i !== index)); + }; + + const updateContactField = (index: number, field: keyof Contact, value: string) => { + const newContacts = [...contactsForm]; + newContacts[index] = { ...newContacts[index], [field]: value }; + setContactsForm(newContacts); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!customerForm.company.trim()) { + alert("Bitte geben Sie einen Firmennamen an."); + return; + } + + setIsSaving(true); + + try { + let finalCustomerId = editingCustomer?.id; + + if (editingCustomer) { + const { error } = await supabase.from(TABLES.CUSTOMERS).update(customerForm).eq('id', editingCustomer.id); + if (error) throw error; + } else { + const { data, error } = await supabase.from(TABLES.CUSTOMERS).insert([customerForm]).select(); + if (error) throw error; + finalCustomerId = data[0].id; + } + + if (finalCustomerId) { + await supabase.from(TABLES.CONTACTS).delete().eq('customer_id', finalCustomerId); + + const contactsToInsert = contactsForm + .filter(c => c.name && c.name.trim() !== '') + .map(c => { + const { id, ...cleanContact } = c as any; + return { + ...cleanContact, + customer_id: finalCustomerId + }; + }); + + if (contactsToInsert.length > 0) { + const { error: cError } = await supabase.from(TABLES.CONTACTS).insert(contactsToInsert); + if (cError) throw cError; + } + } + + setIsModalOpen(false); + await refreshData(); + if (!editingCustomer && finalCustomerId) setSelectedCustomerId(finalCustomerId); + } catch (error: any) { + console.error('Save error:', error); + alert('Fehler beim Speichern: ' + (error.message || 'Unbekannter Datenbankfehler')); + } finally { + setIsSaving(false); + } + }; + + const confirmDeleteCustomer = async () => { + if (!customerToDeleteId) return; + try { + await supabase.from(TABLES.CUSTOMERS).delete().eq('id', customerToDeleteId); + if (selectedCustomerId === customerToDeleteId) setSelectedCustomerId(null); + await refreshData(); + } catch (error) { + console.error('Delete error:', error); + } finally { + setCustomerToDeleteId(null); + } + }; + + return ( +
+ {/* Sidebar: Firmenliste */} +
+ + +
+
+

Kundenstamm

+
+
+ {customers.map(c => ( +
setSelectedCustomerId(c.id)} + className={`p-5 cursor-pointer transition-all flex items-center justify-between group ${selectedCustomerId === c.id ? 'bg-brand-50/50 dark:bg-brand-500/10' : 'hover:bg-slate-50 dark:hover:bg-slate-800'}`} + > +
+
+ {c.logo ? ( + {c.company} + ) : ( + {c.company.charAt(0)} + )} +
+
+

{c.company}

+

{c.contacts?.length || 0} Personen

+
+
+ +
+ ))} +
+
+
+ +
+ {activeCustomer ? ( +
+
+
+
+
+ {activeCustomer.logo ? ( + {activeCustomer.company} + ) : ( + activeCustomer.company.charAt(0) + )} +
+
+

{activeCustomer.company}

+
+ PREMIUM KUNDE +
+
+
+ +
+ +
+

Unternehmensprofil & Insights

+
+ {activeCustomer.notes || 'Keine Firmennotizen hinterlegt.'} +
+
+
+ +
+
+ +

Ansprechpartner ({activeCustomer.contacts?.length || 0})

+
+ +
+ {activeCustomer.contacts?.map(contact => ( +
+
+ {contact.avatar ? ( + {contact.name} + ) : ( +
{contact.name.charAt(0)}
+ )} +
+

{contact.name}

+

{contact.role || 'Mitarbeiter'}

+ +
+
+ + {contact.email || 'N/A'} +
+
+ + {contact.phone || 'N/A'} +
+
+
+ ))} +
+
+
+ ) : ( +
+
+ +
+

Bereit zur Auswahl

+

Wählen Sie ein Unternehmen aus der Liste links aus.

+
+ )} +
+ + {isModalOpen && ( +
+
+
+

+ {editingCustomer ? 'Firma aktualisieren' : 'Neues Unternehmen anlegen'} +

+ +
+ +
+
+ {/* Logo & Basic Info */} +
+
+
+ {customerForm.logo ? ( + Preview + ) : ( +
+ +
+ )} + +
+

Firmenlogo (optional)

+
+ +
+
+ + setCustomerForm({...customerForm, company: e.target.value})} + className="w-full px-6 py-4 bg-slate-100 dark:bg-slate-800 border-2 border-transparent focus:border-brand-500 rounded-2xl outline-none font-bold text-slate-900 dark:text-white transition-all shadow-inner" + placeholder="Z.B. Acme Corp" + /> +
+
+ + setCustomerForm({...customerForm, name: e.target.value})} + className="w-full px-6 py-4 bg-slate-100 dark:bg-slate-800 border-2 border-transparent focus:border-brand-500 rounded-2xl outline-none font-bold text-slate-900 dark:text-white transition-all shadow-inner" + placeholder="Z.B. Max Mustermann" + /> +
+
+
+ +
+ +