[split-required] Split 58 monoliths across Python, Go, TypeScript (Phases 1-3)
Phase 1 — Python (klausur-service): 5 monoliths → 36 files - dsfa_corpus_ingestion.py (1,828 LOC → 5 files) - cv_ocr_engines.py (2,102 LOC → 7 files) - cv_layout.py (3,653 LOC → 10 files) - vocab_worksheet_api.py (2,783 LOC → 8 files) - grid_build_core.py (1,958 LOC → 6 files) Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files - staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3) - policy_handlers.go (700 → 2), repository.go (684 → 2) - search.go (592 → 2), ai_extraction_handlers.go (554 → 2) - seed_data.go (591 → 2), grade_service.go (646 → 2) Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files - sdk/types.ts (2,108 → 16 domain files) - ai/rag/page.tsx (2,686 → 14 files) - 22 page.tsx files split into _components/ + _hooks/ - 11 component files split into sub-components - 10 SDK data catalogs added to loc-exceptions - Deleted dead backup index_original.ts (4,899 LOC) All original public APIs preserved via re-export facades. Zero new errors: Python imports verified, Go builds clean, TypeScript tsc --noEmit shows only pre-existing errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
322
admin-lehrer/components/grid-editor/useGridEditorActions.ts
Normal file
322
admin-lehrer/components/grid-editor/useGridEditorActions.ts
Normal file
@@ -0,0 +1,322 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { StructuredGrid, GridZone } from './types'
|
||||
|
||||
interface ActionDeps {
|
||||
grid: StructuredGrid | null
|
||||
setGrid: React.Dispatch<React.SetStateAction<StructuredGrid | null>>
|
||||
setDirty: React.Dispatch<React.SetStateAction<boolean>>
|
||||
pushUndo: (zones: GridZone[]) => void
|
||||
}
|
||||
|
||||
export function useGridEditorActions(deps: ActionDeps) {
|
||||
const { grid, setGrid, setDirty, pushUndo } = deps
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Multi-select state
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const [selectedCells, setSelectedCells] = useState<Set<string>>(new Set())
|
||||
|
||||
const toggleCellSelection = useCallback(
|
||||
(cellId: string) => {
|
||||
setSelectedCells((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(cellId)) next.delete(cellId)
|
||||
else next.add(cellId)
|
||||
return next
|
||||
})
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const clearCellSelection = useCallback(() => {
|
||||
setSelectedCells(new Set())
|
||||
}, [])
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Cell editing
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const updateCellText = useCallback(
|
||||
(cellId: string, newText: string) => {
|
||||
if (!grid) return
|
||||
pushUndo(grid.zones)
|
||||
|
||||
setGrid((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
zones: prev.zones.map((zone) => {
|
||||
// Check if cell exists
|
||||
const existing = zone.cells.find((c) => c.cell_id === cellId)
|
||||
if (existing) {
|
||||
return {
|
||||
...zone,
|
||||
cells: zone.cells.map((cell) =>
|
||||
cell.cell_id === cellId ? { ...cell, text: newText } : cell,
|
||||
),
|
||||
}
|
||||
}
|
||||
// Cell doesn't exist — create it if the cellId belongs to this zone
|
||||
// cellId format: Z{zone}_R{row}_C{col}
|
||||
const match = cellId.match(/^Z(\d+)_R(\d+)_C(\d+)$/)
|
||||
if (!match || parseInt(match[1]) !== zone.zone_index) return zone
|
||||
const rowIndex = parseInt(match[2])
|
||||
const colIndex = parseInt(match[3])
|
||||
const col = zone.columns.find((c) => c.index === colIndex)
|
||||
const newCell = {
|
||||
cell_id: cellId,
|
||||
zone_index: zone.zone_index,
|
||||
row_index: rowIndex,
|
||||
col_index: colIndex,
|
||||
col_type: col?.label ?? '',
|
||||
text: newText,
|
||||
confidence: 0,
|
||||
bbox_px: { x: 0, y: 0, w: 0, h: 0 },
|
||||
bbox_pct: { x: 0, y: 0, w: 0, h: 0 },
|
||||
word_boxes: [],
|
||||
ocr_engine: 'manual',
|
||||
is_bold: false,
|
||||
}
|
||||
return { ...zone, cells: [...zone.cells, newCell] }
|
||||
}),
|
||||
}
|
||||
})
|
||||
setDirty(true)
|
||||
},
|
||||
[grid, pushUndo, setGrid, setDirty],
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Cell color
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Set a manual color override on a cell.
|
||||
* - hex string (e.g. "#dc2626"): force text color
|
||||
* - null: force no color (clear bar)
|
||||
* - undefined: remove override, restore OCR-detected color
|
||||
*/
|
||||
const setCellColor = useCallback(
|
||||
(cellId: string, color: string | null | undefined) => {
|
||||
if (!grid) return
|
||||
pushUndo(grid.zones)
|
||||
setGrid((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
zones: prev.zones.map((zone) => ({
|
||||
...zone,
|
||||
cells: zone.cells.map((cell) => {
|
||||
if (cell.cell_id !== cellId) return cell
|
||||
if (color === undefined) {
|
||||
// Remove override entirely — restore OCR behavior
|
||||
const { color_override: _, ...rest } = cell
|
||||
return rest
|
||||
}
|
||||
return { ...cell, color_override: color }
|
||||
}),
|
||||
})),
|
||||
}
|
||||
})
|
||||
setDirty(true)
|
||||
},
|
||||
[grid, pushUndo, setGrid, setDirty],
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Column formatting
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const toggleColumnBold = useCallback(
|
||||
(zoneIndex: number, colIndex: number) => {
|
||||
if (!grid) return
|
||||
pushUndo(grid.zones)
|
||||
|
||||
setGrid((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
zones: prev.zones.map((zone) => {
|
||||
if (zone.zone_index !== zoneIndex) return zone
|
||||
const col = zone.columns.find((c) => c.index === colIndex)
|
||||
const newBold = col ? !col.bold : true
|
||||
return {
|
||||
...zone,
|
||||
columns: zone.columns.map((c) =>
|
||||
c.index === colIndex ? { ...c, bold: newBold } : c,
|
||||
),
|
||||
cells: zone.cells.map((cell) =>
|
||||
cell.col_index === colIndex
|
||||
? { ...cell, is_bold: newBold }
|
||||
: cell,
|
||||
),
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
setDirty(true)
|
||||
},
|
||||
[grid, pushUndo, setGrid, setDirty],
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Row formatting
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const toggleRowHeader = useCallback(
|
||||
(zoneIndex: number, rowIndex: number) => {
|
||||
if (!grid) return
|
||||
pushUndo(grid.zones)
|
||||
|
||||
// Cycle: normal -> header -> footer -> normal
|
||||
setGrid((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
zones: prev.zones.map((zone) => {
|
||||
if (zone.zone_index !== zoneIndex) return zone
|
||||
return {
|
||||
...zone,
|
||||
rows: zone.rows.map((r) => {
|
||||
if (r.index !== rowIndex) return r
|
||||
if (!r.is_header && !r.is_footer) {
|
||||
return { ...r, is_header: true, is_footer: false }
|
||||
} else if (r.is_header) {
|
||||
return { ...r, is_header: false, is_footer: true }
|
||||
} else {
|
||||
return { ...r, is_header: false, is_footer: false }
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
setDirty(true)
|
||||
},
|
||||
[grid, pushUndo, setGrid, setDirty],
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Column pattern auto-correction
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Detect dominant prefix+number patterns per column and complete
|
||||
* partial matches. E.g. if 3+ cells read "p.70", "p.71", etc.,
|
||||
* a cell reading ".65" is corrected to "p.65".
|
||||
* Returns the number of corrections made.
|
||||
*/
|
||||
const autoCorrectColumnPatterns = useCallback(() => {
|
||||
if (!grid) return 0
|
||||
pushUndo(grid.zones)
|
||||
|
||||
let totalFixed = 0
|
||||
const numberPattern = /^(.+?)(\d+)\s*$/
|
||||
|
||||
setGrid((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
zones: prev.zones.map((zone) => {
|
||||
// Group cells by column
|
||||
const cellsByCol = new Map<number, { cell: (typeof zone.cells)[0]; idx: number }[]>()
|
||||
zone.cells.forEach((cell, idx) => {
|
||||
const arr = cellsByCol.get(cell.col_index) || []
|
||||
arr.push({ cell, idx })
|
||||
cellsByCol.set(cell.col_index, arr)
|
||||
})
|
||||
|
||||
const newCells = [...zone.cells]
|
||||
|
||||
for (const [, colEntries] of cellsByCol) {
|
||||
// Count prefix occurrences
|
||||
const prefixCounts = new Map<string, number>()
|
||||
for (const { cell } of colEntries) {
|
||||
const m = cell.text.trim().match(numberPattern)
|
||||
if (m) {
|
||||
prefixCounts.set(m[1], (prefixCounts.get(m[1]) || 0) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Find dominant prefix (>= 3 occurrences)
|
||||
let dominantPrefix = ''
|
||||
let maxCount = 0
|
||||
for (const [prefix, count] of prefixCounts) {
|
||||
if (count >= 3 && count > maxCount) {
|
||||
dominantPrefix = prefix
|
||||
maxCount = count
|
||||
}
|
||||
}
|
||||
if (!dominantPrefix) continue
|
||||
|
||||
// Fix partial matches — entries that are just [.?\s*]NUMBER
|
||||
for (const { cell, idx } of colEntries) {
|
||||
const text = cell.text.trim()
|
||||
if (!text || text.startsWith(dominantPrefix)) continue
|
||||
|
||||
const numMatch = text.match(/^[.\s]*(\d+)\s*$/)
|
||||
if (numMatch) {
|
||||
newCells[idx] = { ...newCells[idx], text: `${dominantPrefix}${numMatch[1]}` }
|
||||
totalFixed++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ...zone, cells: newCells }
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
if (totalFixed > 0) setDirty(true)
|
||||
return totalFixed
|
||||
}, [grid, pushUndo, setGrid, setDirty])
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Multi-select bulk formatting
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/** Toggle bold on all selected cells (and their columns). */
|
||||
const toggleSelectedBold = useCallback(() => {
|
||||
if (!grid || selectedCells.size === 0) return
|
||||
pushUndo(grid.zones)
|
||||
|
||||
// Determine if we're turning bold on or off (majority rule)
|
||||
const cells = grid.zones.flatMap((z) => z.cells)
|
||||
const selectedArr = cells.filter((c) => selectedCells.has(c.cell_id))
|
||||
const boldCount = selectedArr.filter((c) => c.is_bold).length
|
||||
const newBold = boldCount < selectedArr.length / 2
|
||||
|
||||
setGrid((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
zones: prev.zones.map((zone) => ({
|
||||
...zone,
|
||||
cells: zone.cells.map((cell) =>
|
||||
selectedCells.has(cell.cell_id) ? { ...cell, is_bold: newBold } : cell,
|
||||
),
|
||||
})),
|
||||
}
|
||||
})
|
||||
setDirty(true)
|
||||
setSelectedCells(new Set())
|
||||
}, [grid, selectedCells, pushUndo, setGrid, setDirty])
|
||||
|
||||
return {
|
||||
// Cell editing
|
||||
updateCellText,
|
||||
setCellColor,
|
||||
// Column formatting
|
||||
toggleColumnBold,
|
||||
// Row formatting
|
||||
toggleRowHeader,
|
||||
// Pattern correction
|
||||
autoCorrectColumnPatterns,
|
||||
// Multi-select
|
||||
selectedCells,
|
||||
toggleCellSelection,
|
||||
clearCellSelection,
|
||||
toggleSelectedBold,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user