import { useId, useRef, useState, type DragEvent, type KeyboardEvent } from "react"; import { ArrowDown, ArrowUp, GripVertical, LockKeyhole, Plus, Trash2 } from "lucide-react"; import type { NavigationPreferences, PlatformNavItem, ProductAreaContribution } from "../types"; import ActionToolbar from "./ActionToolbar"; import Button from "./Button"; import IconButton from "./IconButton"; import ToggleSwitch from "./ToggleSwitch"; import { usePlatformLanguage } from "../i18n/LanguageContext"; import { navigationEditorTranslations } from "../i18n/navigationEditorTranslations"; import { inheritedNavigationLayout, materializeNavigationLayout, moveNavigationEntry, navigationEditorOrder, navigationId, type NavigationPreferenceScope } from "./navigationPreferenceLayout"; export default function NavigationPreferenceEditor({ items, productAreas = [], value, onChange, scope, disabled = false }: { items: PlatformNavItem[]; productAreas?: ProductAreaContribution[]; value: NavigationPreferences | null; onChange: (value: NavigationPreferences | null) => void; scope: NavigationPreferenceScope; disabled?: boolean; }) { const { translateText, language, t } = usePlatformLanguage(); function navigationText(name: string, values: Record = {}) { const fallback = navigationEditorTranslations[language]?.[name] ?? navigationEditorTranslations.en[name] ?? name; const template = t(`i18n:govoplan-core.navigation_editor_${name}`, fallback); return template.replace(/\{(\w+)\}/g, (match, field: string) => String(values[field] ?? match)); } const instructionsId = useId(); const inherited = inheritedNavigationLayout(items, scope, productAreas); const editable = materializeNavigationLayout(value, inherited); const layoutStatus = value === null ? "inherited" : (editable.separators?.length ?? 0) > 0 ? "grouped" : "flat"; const byId = new Map(items.map((item) => [navigationId(item), item])); const inheritedScope = scope === "system" ? "module" : scope === "tenant" ? "system" : "tenant"; const ancestorLocked = (id: string) => Boolean(byId.get(id)?.navigationLayers?.[inheritedScope]?.locked); const locked = (id: string) => ancestorLocked(id) || Boolean(editable.locked?.includes(id)); const separators = new Map((editable.separators ?? []).map((item) => [item.id, item])); const effective = { ...editable, hidden: editable.hidden.filter((id) => !locked(id)) }; const orderedIds = navigationEditorOrder(items, effective); const available = items.filter((item) => effective.hidden.includes(navigationId(item))); const [selectedModule, setSelectedModule] = useState(""); const [dragged, setDragged] = useState(null); const [dropTarget, setDropTarget] = useState(null); const [announcement, setAnnouncement] = useState(""); const pickup = useRef<{ id: string; original: NavigationPreferences | null } | null>(null); const addId = available.some((item) => navigationId(item) === selectedModule) ? selectedModule : navigationId(available[0] ?? { to: "", label: "" }); function labelFor(id: string) { return translateText(byId.get(id)?.label ?? separators.get(id)?.label ?? "") || translateText(navigationText("separator")); } function update(patch: Partial) { if (disabled) return; const next = { ...effective, order: orderedIds, ...patch, contract_version: "1" as const }; // Optional modules can be temporarily absent. Preserve their stored place // instead of destroying it when an unrelated visible entry is edited. const unavailable = new Set(editable.order.filter((id) => !byId.has(id) && !separators.has(id))); const order = [...next.order]; for (const id of editable.order) { if (!unavailable.has(id) || order.includes(id)) continue; const following = editable.order.slice(editable.order.indexOf(id) + 1).find((entry) => order.includes(entry)); order.splice(following ? order.indexOf(following) : order.length, 0, id); } onChange({ ...next, order }); } function move(id: string, offset: -1 | 1) { const target = orderedIds[orderedIds.indexOf(id) + offset]; if (!target || disabled) return; const next = moveNavigationEntry(orderedIds, id, target, offset === 1); update({ order: next }); setAnnouncement(translateText(navigationText("moved", { label: labelFor(id), position: next.indexOf(id) + 1, total: next.length }))); } function remove(id: string) { if (locked(id)) return; update({ order: orderedIds.filter((item) => item !== id), hidden: byId.has(id) ? [...new Set([...effective.hidden, id])] : effective.hidden, separators: (effective.separators ?? []).filter((item) => item.id !== id) }); } function drop(event: DragEvent, target: string) { event.preventDefault(); if (disabled || !dragged) return; const bounds = event.currentTarget.getBoundingClientRect(); const next = moveNavigationEntry(orderedIds, dragged, target, event.clientY > bounds.top + bounds.height / 2); if (next.some((id, index) => id !== orderedIds[index])) update({ order: next }); setAnnouncement(translateText(navigationText("moved", { label: labelFor(dragged), position: next.indexOf(dragged) + 1, total: next.length }))); setDragged(null); setDropTarget(null); } function keyboardDrag(event: KeyboardEvent, id: string) { if (disabled) return; if (event.key === " " || event.key === "Enter") { event.preventDefault(); if (pickup.current) { pickup.current = null; setDragged(null); setAnnouncement(translateText(navigationText("dropped"))); } else { pickup.current = { id, original: value }; setDragged(id); setAnnouncement(translateText(navigationText("picked_up"))); } } else if (pickup.current?.id === id && (event.key === "ArrowUp" || event.key === "ArrowDown")) { event.preventDefault(); move(id, event.key === "ArrowUp" ? -1 : 1); } else if (pickup.current && event.key === "Escape") { event.preventDefault(); onChange(pickup.current.original); pickup.current = null; setDragged(null); setAnnouncement(translateText(navigationText("cancelled"))); } } return (

{navigationText("help")}

{navigationText(`${layoutStatus}_status`)}

    {orderedIds.map((id, index) => { const item = byId.get(id); const separator = separators.get(id); const label = labelFor(id); return (
  1. { if (!disabled && dragged) { event.preventDefault(); event.dataTransfer.dropEffect = "move"; setDropTarget(id); } }} onDrop={(event) => drop(event, id)}>
    } className="navigation-preference-drag" disabled={disabled} draggable={!disabled} aria-describedby={instructionsId} aria-pressed={dragged === id} onKeyDown={(event) => keyboardDrag(event, id)} onDragStart={(event) => { setDragged(id); event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", id); }} onDragEnd={() => { setDragged(null); setDropTarget(null); }} /> } onClick={() => move(id, -1)} disabled={disabled || index === 0} /> } onClick={() => move(id, 1)} disabled={disabled || index === orderedIds.length - 1} />
    {separator ? : <>{label}{id}}
    {item && (scope === "system" || scope === "tenant") &&
  2. ); })}
{orderedIds.length === 0 &&

{navigationText("empty")}

}

{announcement}

); }