import { useState } from 'react'; import type { ProcessNode } from '../../types/process'; import SubprocessCard from './SubprocessCard'; import './ProcessGroupList.css'; interface GroupCoords { lat?: string | number | null; lon?: string | number | null; hours?: string; box_scale?: string | number; } interface Props { processes: ProcessNode[]; groups: number[][]; groupNames: string[]; groupCoordinates: Record; onProcessesChange: (p: ProcessNode[]) => void; onGroupsChange: (g: number[][]) => void; onGroupNamesChange: (n: string[]) => void; onGroupCoordinatesChange: (c: Record) => void; onPlaceRequest: (target: string) => void; placementTarget: string | null; subprocessMapExpanded: Record; onSubprocessMapToggle: (gIdx: number) => void; expandedGroups?: Set; onExpandedGroupsChange?: (set: Set) => void; } export default function ProcessGroupList({ processes, groups, groupNames, groupCoordinates, onProcessesChange, onGroupsChange, onGroupNamesChange, onGroupCoordinatesChange, onPlaceRequest, placementTarget, subprocessMapExpanded, onSubprocessMapToggle, expandedGroups = new Set(), onExpandedGroupsChange, }: Props) { const [infoExpanded, setInfoExpanded] = useState>(new Set()); const [confirmDelete, setConfirmDelete] = useState(null); const toggleGroup = (idx: number) => { // If we're clicking an already expanded group, collapse it if (expandedGroups.has(idx)) { if (subprocessMapExpanded[idx]) { onSubprocessMapToggle(idx); } onExpandedGroupsChange?.(new Set()); // Collapse all } else { // Otherwise, ONLY expand the new one onExpandedGroupsChange?.(new Set([idx])); } }; const toggleInfo = (idx: number) => { setInfoExpanded((prev) => { const next = new Set(prev); if (next.has(idx)) next.delete(idx); else next.add(idx); return next; }); }; const updateGroupName = (gIdx: number, name: string) => { const names = [...groupNames]; names[gIdx] = name; onGroupNamesChange(names); }; const updateGroupCoord = ( gIdx: number, field: string, value: string | number ) => { const coords = { ...groupCoordinates }; coords[gIdx] = { ...(coords[gIdx] || {}), [field]: value }; onGroupCoordinatesChange(coords); }; const deleteGroup = (gIdx: number) => { const newGroups = groups.filter((_, i) => i !== gIdx); const newNames = groupNames.filter((_, i) => i !== gIdx); const newCoords = { ...groupCoordinates }; delete newCoords[gIdx]; // Re-index coordinates const reindexed: Record = {}; Object.keys(newCoords).forEach((k) => { const oldIdx = parseInt(k); const newIdx = oldIdx > gIdx ? oldIdx - 1 : oldIdx; reindexed[newIdx] = newCoords[oldIdx]; }); onGroupsChange(newGroups); onGroupNamesChange(newNames); onGroupCoordinatesChange(reindexed); setConfirmDelete(null); }; const addSubprocess = (gIdx: number) => { const newProcesses = [...processes]; const newSub: ProcessNode = { name: `Subprocess ${newProcesses.length + 1}`, lat: '', lon: '', box_scale: 1.0, next: '', hours: '', extra_info: { notes: '' }, streams: [], children: [], }; const newIdx = newProcesses.length; newProcesses.push(newSub); const newGroups = [...groups]; newGroups[gIdx] = [...newGroups[gIdx], newIdx]; onProcessesChange(newProcesses); onGroupsChange(newGroups); }; const updateSubprocess = (subIdx: number, updated: ProcessNode) => { const newProcesses = [...processes]; newProcesses[subIdx] = updated; onProcessesChange(newProcesses); }; const deleteSubprocess = (gIdx: number, subIdx: number) => { const newGroups = [...groups]; newGroups[gIdx] = newGroups[gIdx].filter((i) => i !== subIdx); // Don't remove from processes array to avoid re-indexing issues // Just remove from group if (newGroups[gIdx].length === 0) { // Remove empty group deleteGroup(gIdx); } else { onGroupsChange(newGroups); } }; return (
{groups.map((subIdxs, gIdx) => { const expanded = expandedGroups.has(gIdx); const showInfo = infoExpanded.has(gIdx); const gCoords = groupCoordinates[gIdx] || {}; const isPlacing = placementTarget === `group_${gIdx}`; return (
{/* Group header */}
updateGroupName(gIdx, e.target.value)} />
updateGroupCoord(gIdx, 'box_scale', parseFloat(e.target.value)) } title={`Box size: ${gCoords.box_scale ?? 1.5}`} />
{subIdxs.length} sub {confirmDelete === gIdx ? ( ) : ( )}
{/* Expanded content */} {expanded && (
{/* Info section */}
toggleInfo(gIdx)} > Information
{showInfo && (
updateGroupCoord(gIdx, 'lat', e.target.value) } />
updateGroupCoord(gIdx, 'lon', e.target.value) } />
updateGroupCoord(gIdx, 'hours', e.target.value) } />
)}
{/* Subprocesses */}
Subprocesses ({subIdxs.length})
{subIdxs.map((si) => { const sub = processes[si]; if (!sub) return null; return ( processes[idx]?.name).filter(Boolean)} onUpdate={(updated) => updateSubprocess(si, updated)} onDelete={() => deleteSubprocess(gIdx, si)} onPlaceRequest={onPlaceRequest} placementTarget={placementTarget} /> ); })}
)}
); })} {groups.length === 0 && (

No processes yet. Click Add Process to begin.

)}
); }