"use client"; import { Handle, Position, type Node, type NodeProps } from "@xyflow/react"; import { Building2, ChevronDown, Plus, UserRound } from "lucide-react"; import Link from "next/link"; import { memo } from "react"; import { Avatar } from "@/components/ui/Avatar"; import { NODE_DIMENSIONS } from "./graphLayout"; import type { ChartNode, ChartNodeKind } from "./types"; export type OrgChartNodeData = { chartNode: ChartNode; expanded: boolean; hasChildren: boolean; childCount: number; onToggle: (id: string) => void; }; export type OrgChartRFNode = Node; // One accent colour per node kind, carried by a left stripe. It is what makes // the four kinds separable at a glance when the chart is zoomed out far // enough that the text has stopped being legible. const KIND_ACCENT: Record = { person: "bg-brand-500", role: "bg-purple-text", group: "bg-ink-muted", vacancy: "bg-brand-200", }; const KIND_SHELL: Record = { person: "border-border bg-white", role: "border-border bg-white", group: "border-border-subtle bg-surface", vacancy: "border-dashed border-brand-200 bg-brand-50", }; // React Flow re-renders node components on every pan/zoom frame — memo is // required, not just tidy, to keep that smooth at a few hundred nodes. export const OrgChartNode = memo(function OrgChartNode({ id, data }: NodeProps) { const { chartNode, expanded, hasChildren, childCount, onToggle } = data; const { kind, label, sublabel, avatar, href, vacant, totalReports, absent, coveredBy, coveringFor } = chartNode; const isMatch = chartNode.matched ?? false; const { width, height } = NODE_DIMENSIONS[kind]; const content = ( <> {kind === "person" && avatar ? ( ) : kind === "vacancy" ? ( ) : kind === "role" ? ( ) : ( )} {label} {sublabel && ( {sublabel} )} {/* The stand-in replaces the report count rather than crowding in next to it: on a card this size the arrangement is the more important of the two, and it only appears while one is in force. */} {absent ? ( Abwesend{coveredBy ? ` · Vertretung: ${coveredBy}` : ""} ) : coveringFor ? ( Vertretung für {coveringFor} ) : ( totalReports !== undefined && totalReports > 0 && ( {childCount} direkt · {totalReports} gesamt ) )} ); return (
{/* Accent stripe, inset so it follows the card's rounded corner. */} {href ? ( {content} ) : (
{content}
)} {/* Overhangs the bottom edge, sitting on the connector to its children — the conventional org-chart affordance, and a 28px touch target. */} {hasChildren && ( )}
); });