import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Box, Typography } from '@mui/material' import { useNavigate } from 'react-router' import type { AgentDirectoryEntry, AgentLevel } from '../../domain/agentDirectory' import { AGENT_LEVEL_ORDER } from '../../domain/agentDirectory' import { agentDirectory } from '../../mock-data/agentDirectory' import { AgentRing } from './AgentRing' import { AgentPreviewPopover } from './AgentPreviewPopover' import { RING_SPECS, RING_REFERENCE_PX } from './agentRingConfig' import { ROUTES } from '../../lib/constants' import { BADGE_COLORS, DS_TEXT } from '../../lib/ds' interface Props { /** Bis zu welcher Stufe die Ringe sichtbar sind — kumulativ. */ level: AgentLevel } /** Misst die Containerbreite, damit Portraits und Schrift mitskalieren. */ function useContainerWidth(): [React.RefObject, number] { const ref = useRef(null) const [width, setWidth] = useState(RING_REFERENCE_PX) useEffect(() => { const node = ref.current if (!node) return // Ohne ResizeObserver bleibt die Referenzbreite stehen: die Darstellung ist // dann nicht mitskaliert, aber vollständig bedienbar. Ein harter Absturz // wegen einer fehlenden Browser-Schnittstelle wäre die schlechtere Antwort. if (typeof ResizeObserver === 'undefined') { setWidth(node.getBoundingClientRect().width || RING_REFERENCE_PX) return } const observer = new ResizeObserver(([entry]) => { setWidth(entry.contentRect.width) }) observer.observe(node) return () => observer.disconnect() }, []) return [ref, width] } /** * Kreisförmige Darstellung der digitalen Belegschaft. * * Aufbau nach der Referenzgrafik: Kernteam innen und gold gerahmt, danach die * Zentralen Dienste, aussen die Spezialisten der Geschäftsbereiche. Welche Ringe * sichtbar sind, bestimmt die gewählte Stufe — kumulativ, wie im Konzept. * * Nicht sichtbare Ringe werden ausgeblendet statt entfernt: so bleibt die * Bewegung beim Stufenwechsel ruhig und der Kreis springt nicht in der Grösse. */ export function TeamOverviewRing({ level }: Props) { const navigate = useNavigate() const [ref, width] = useContainerWidth() const [preview, setPreview] = useState(null) const [anchorEl, setAnchorEl] = useState(null) const scale = width / RING_REFERENCE_PX const visibleUpTo = AGENT_LEVEL_ORDER.indexOf(level) const agentsByLevel = useMemo(() => { const map = new Map() for (const spec of RING_SPECS) { map.set(spec.level, agentDirectory.filter(a => a.level === spec.level)) } return map }, []) const handleSelect = useCallback( (agent: AgentDirectoryEntry, element: HTMLElement | null) => { if (agent.isCore) { navigate(`${ROUTES.SUPPLY.TEAM_PERSONNEL}/${agent.id}`) return } setPreview(agent) setAnchorEl(element) }, [navigate], ) // Der Ringknoten meldet nur den Agenten; das auslösende Element holen wir uns // aus dem Fokus, damit die Vorschau am angeklickten Portrait hängt. const onNodeSelect = useCallback( (agent: AgentDirectoryEntry) => { const active = document.activeElement handleSelect(agent, active instanceof HTMLElement ? active : null) }, [handleSelect], ) const closePreview = useCallback(() => { setPreview(null) setAnchorEl(null) }, []) return ( <> {RING_SPECS.map((spec, index) => ( ))} {/* Nabe */} PROPERTY{' '} ON {agentDirectory.length} digitale Mitarbeitende ) }