Files
breakpilot-lehrer/admin-lehrer/components/grid-editor/GridEditor.tsx
Benjamin Admin bc1804ad18 Fix vsplit side-by-side rendering: invalid TypeScript type annotation
Changed `typeof grid.zones[][]` to `GridZone[][]` which was causing
a silent build error, preventing the vsplit zone grouping logic from
being compiled into the production bundle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 17:09:52 +01:00

279 lines
8.6 KiB
TypeScript

'use client'
import { useCallback, useEffect, useState } from 'react'
import { useGridEditor } from './useGridEditor'
import type { GridZone } from './types'
import { GridToolbar } from './GridToolbar'
import { GridTable } from './GridTable'
import { GridImageOverlay } from './GridImageOverlay'
interface GridEditorProps {
sessionId: string | null
onNext?: () => void
}
export function GridEditor({ sessionId, onNext }: GridEditorProps) {
const {
grid,
loading,
saving,
error,
dirty,
selectedCell,
setSelectedCell,
buildGrid,
loadGrid,
saveGrid,
updateCellText,
toggleColumnBold,
toggleRowHeader,
undo,
redo,
canUndo,
canRedo,
getAdjacentCell,
} = useGridEditor(sessionId)
const [showOverlay, setShowOverlay] = useState(false)
// Load grid on mount
useEffect(() => {
if (sessionId) {
loadGrid()
}
}, [sessionId, loadGrid])
// Keyboard shortcuts
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) {
e.preventDefault()
undo()
} else if ((e.metaKey || e.ctrlKey) && e.key === 'z' && e.shiftKey) {
e.preventDefault()
redo()
} else if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault()
saveGrid()
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [undo, redo, saveGrid])
const handleNavigate = useCallback(
(cellId: string, direction: 'up' | 'down' | 'left' | 'right') => {
const target = getAdjacentCell(cellId, direction)
if (target) {
setSelectedCell(target)
// Focus the input
setTimeout(() => {
const el = document.getElementById(`cell-${target}`)
if (el) {
el.focus()
if (el instanceof HTMLInputElement) el.select()
}
}, 0)
}
},
[getAdjacentCell, setSelectedCell],
)
if (!sessionId) {
return (
<div className="text-center py-12 text-gray-400">
Keine Session ausgewaehlt.
</div>
)
}
if (loading) {
return (
<div className="flex items-center justify-center py-16">
<div className="flex items-center gap-3 text-gray-500 dark:text-gray-400">
<svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Grid wird aufgebaut...
</div>
</div>
)
}
if (error) {
return (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<p className="text-sm text-red-700 dark:text-red-300">
Fehler: {error}
</p>
<button
onClick={buildGrid}
className="mt-2 text-xs px-3 py-1.5 bg-red-600 text-white rounded hover:bg-red-700"
>
Erneut versuchen
</button>
</div>
)
}
if (!grid || !grid.zones.length) {
return (
<div className="text-center py-12">
<p className="text-gray-400 mb-4">Kein Grid vorhanden.</p>
<button
onClick={buildGrid}
className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 text-sm"
>
Grid aus OCR-Ergebnissen erstellen
</button>
</div>
)
}
return (
<div className="space-y-4">
{/* Summary bar */}
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
<span>{grid.summary.total_zones} Zone(n)</span>
<span>{grid.summary.total_columns} Spalten</span>
<span>{grid.summary.total_rows} Zeilen</span>
<span>{grid.summary.total_cells} Zellen</span>
{grid.boxes_detected > 0 && (
<span className="text-amber-600 dark:text-amber-400">
{grid.boxes_detected} Box(en) erkannt
</span>
)}
{grid.summary.color_stats && Object.entries(grid.summary.color_stats)
.filter(([name]) => name !== 'black')
.map(([name, count]) => (
<span key={name} className="inline-flex items-center gap-1">
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: {
red: '#dc2626', blue: '#2563eb', green: '#16a34a',
orange: '#ea580c', purple: '#9333ea', yellow: '#ca8a04',
}[name] || '#6b7280' }} />
<span>{count} {name}</span>
</span>
))
}
{(grid.summary.recovered_colored ?? 0) > 0 && (
<span className="text-purple-600 dark:text-purple-400">
+{grid.summary.recovered_colored} recovered
</span>
)}
<span className="text-gray-400">
{grid.duration_seconds.toFixed(1)}s
</span>
</div>
{/* Toolbar */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-2">
<GridToolbar
dirty={dirty}
saving={saving}
canUndo={canUndo}
canRedo={canRedo}
showOverlay={showOverlay}
onSave={saveGrid}
onUndo={undo}
onRedo={redo}
onRebuild={buildGrid}
onToggleOverlay={() => setShowOverlay(!showOverlay)}
/>
</div>
{/* Image overlay */}
{showOverlay && (
<GridImageOverlay sessionId={sessionId} grid={grid} />
)}
{/* Zone tables — group vsplit zones side by side */}
<div className="space-y-4">
{(() => {
// Group consecutive zones with same vsplit_group
const groups: GridZone[][] = []
for (const zone of grid.zones) {
const prev = groups[groups.length - 1]
if (
prev &&
zone.vsplit_group != null &&
prev[0].vsplit_group === zone.vsplit_group
) {
prev.push(zone)
} else {
groups.push([zone])
}
}
return groups.map((group) =>
group.length === 1 ? (
<div
key={group[0].zone_index}
className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"
>
<GridTable
zone={group[0]}
layoutMetrics={grid.layout_metrics}
selectedCell={selectedCell}
onSelectCell={setSelectedCell}
onCellTextChange={updateCellText}
onToggleColumnBold={toggleColumnBold}
onToggleRowHeader={toggleRowHeader}
onNavigate={handleNavigate}
/>
</div>
) : (
<div
key={`vsplit-${group[0].vsplit_group}`}
className="flex gap-2"
>
{group.map((zone) => (
<div
key={zone.zone_index}
className="flex-1 min-w-0 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"
>
<GridTable
zone={zone}
layoutMetrics={grid.layout_metrics}
selectedCell={selectedCell}
onSelectCell={setSelectedCell}
onCellTextChange={updateCellText}
onToggleColumnBold={toggleColumnBold}
onToggleRowHeader={toggleRowHeader}
onNavigate={handleNavigate}
/>
</div>
))}
</div>
),
)
})()}
</div>
{/* Tip */}
<div className="text-[11px] text-gray-400 dark:text-gray-500 flex items-center gap-4">
<span>Tab: naechste Zelle</span>
<span>Enter: Zeile runter</span>
<span>Spalte fett: Klick auf Spaltenkopf</span>
<span>Header: Klick auf Zeilennummer</span>
<span>Ctrl+Z/Y: Undo/Redo</span>
<span>Ctrl+S: Speichern</span>
</div>
{/* Next step button */}
{onNext && (
<div className="flex justify-end">
<button
onClick={async () => {
if (dirty) await saveGrid()
onNext()
}}
className="px-4 py-2 bg-teal-600 text-white text-sm rounded-lg hover:bg-teal-700 transition-colors"
>
Fertig
</button>
</div>
)}
</div>
)
}