import React, { useState, useMemo, useRef, useEffect } from 'react'; import { Customer, Project, Expert } from '../types'; interface OrgChartProps { customers: Customer[]; projects: Project[]; experts: Expert[]; } type Mode = 'Customer' | 'Project'; export const OrgChart: React.FC = ({ customers, projects, experts }) => { const [mode, setMode] = useState('Customer'); const [selectedId, setSelectedId] = useState(null); // Navigation State const [offset, setOffset] = useState({ x: 0, y: 0 }); const [zoom, setZoom] = useState(1); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const canvasRef = useRef(null); const getRelatedData = useMemo(() => { if (!selectedId) return null; if (mode === 'Customer') { const customer = customers.find(c => c.id === selectedId); if (!customer) return null; const companyProjects = projects.filter(p => p.customer === customer.company || (p.notes && p.notes.includes(`KUNDE: ${customer.company}`)) ); const relatedExperts = experts.filter(e => companyProjects.some(p => p.expert && p.expert.includes(e.name) || (p.notes && p.notes.includes(`EXPERTE: ${e.name}`))) ); return { root: customer, contacts: customer.contacts || [], projects: companyProjects, experts: relatedExperts }; } else { const project = projects.find(p => p.id === selectedId); if (!project) return null; const customerMatch = customers.find(c => c.company === project.customer || (project.notes && project.notes.includes(`KUNDE: ${c.company}`)) ); // Multi-Expert Support for Projects const expertNames = (project.expert || '').split(', ').map(s => s.trim()).filter(Boolean); const relatedExperts = experts.filter(e => expertNames.includes(e.name) || (project.notes && project.notes.includes(`EXPERTE: ${e.name}`)) ); return { root: project, customer: customerMatch, experts: relatedExperts, contacts: customerMatch?.contacts || [] }; } }, [mode, selectedId, customers, projects, experts]); // Native Wheel Listener for preventDefault support (necessary for Zoom override) useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const onWheel = (e: WheelEvent) => { // Prevent page scrolling/zooming while interacting with the canvas e.preventDefault(); if (e.ctrlKey) { // ZOOM MODE const zoomSpeed = 0.001; setZoom(prevZoom => { const newZoom = prevZoom - e.deltaY * zoomSpeed; return Math.min(Math.max(newZoom, 0.2), 2.5); }); } else if (e.shiftKey) { // HORIZONTAL SCROLL setOffset(prev => ({ ...prev, x: prev.x - e.deltaY })); } else { // VERTICAL SCROLL (Default) setOffset(prev => ({ ...prev, y: prev.y - e.deltaY })); } }; canvas.addEventListener('wheel', onWheel, { passive: false }); return () => canvas.removeEventListener('wheel', onWheel); }, []); // Mouse Handlers (Panning via Click & Drag) const handleMouseDown = (e: React.MouseEvent) => { if (e.button !== 0) return; // Only left click setIsDragging(true); setDragStart({ x: e.clientX - offset.x, y: e.clientY - offset.y }); }; const handleMouseMove = (e: React.MouseEvent) => { if (!isDragging) return; setOffset({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y }); }; const handleMouseUp = () => setIsDragging(false); const resetView = () => { setOffset({ x: 0, y: 0 }); setZoom(1); }; const handleSelect = (id: string) => { setSelectedId(id); resetView(); }; const ConnectionLines = () => { const color = mode === 'Customer' ? '#22c55e' : '#3b82f6'; return (
); }; return (

Relations Engine

Das ICON Beziehungs-Netzwerk

{/* Sidebar */}

{mode === 'Customer' ? 'Daten: Kunden' : 'Daten: Projekte'}

{(mode === 'Customer' ? customers : projects).map((item: any) => (
handleSelect(item.id)} className={`p-4 cursor-pointer transition-all hover:bg-slate-50 dark:hover:bg-slate-800 flex items-center gap-4 ${selectedId === item.id ? 'bg-brand-50/50 dark:bg-brand-500/10' : ''}`} >
{(item.company || item.title).charAt(0)}

{item.company || item.title}

))}
{/* Graph Area */}
{selectedId && getRelatedData ? (
{(getRelatedData.root as any).company?.charAt(0) || (getRelatedData.root as any).title?.charAt(0)}

{(getRelatedData.root as any).company || (getRelatedData.root as any).title}

{mode === 'Customer' ? 'UNTERNEHMEN' : 'AKTIVES PROJEKT'}

{mode === 'Customer' ? 'Interne Kontakte' : 'Auftraggeber'}

{mode === 'Customer' ? (
{getRelatedData.contacts.length > 0 ? getRelatedData.contacts.map((c: any) => (
{c.name.charAt(0)}

{c.name}

{c.role}

)) :
Keine Daten
}
) : ( getRelatedData.customer && (
{getRelatedData.customer.company.charAt(0)}

{getRelatedData.customer.company}

Zugeordneter Kunde

) )}

{mode === 'Customer' ? 'Laufende Beauftragungen' : 'Expertise'}

{mode === 'Customer' ? (
{getRelatedData.projects.length > 0 ? getRelatedData.projects.map((p: any) => (

{p.title}

Eingesetzte Experten

{experts.filter(e => p.expert && p.expert.includes(e.name) || (p.notes && p.notes.includes(`EXPERTE: ${e.name}`))).map(e => (
{e.name.charAt(0)}

{e.name}

))}
)) :
Keine Projekte
}
) : (
{getRelatedData.experts && getRelatedData.experts.length > 0 ? getRelatedData.experts.map((expert: Expert) => (
{expert.avatar ? : {expert.name.charAt(0)}}

{expert.name}

{expert.role}

)) : (
Keine Experten zugewiesen
)}
)}
) : (

Bereit zur Analyse

Wählen Sie ein Asset aus der Liste links aus.
Rad: Scrollen | Strg + Rad: Zoom | Shift + Rad: Horiz. Scroll

)} {/* Canvas Controls */} {selectedId && (

DRAG / SCROLL

{Math.round(zoom * 100)}% ZOOM

)}
); };