feat: add Woerterbuch category + column add/delete in grid editor
- New document category "Woerterbuch" (frontend type + backend validation) - Column delete: hover column header → red "x" button (with confirmation) - Column add: hover column header → "+" button inserts after that column - Both operations support undo/redo, update cell IDs and summary - Available in both GridEditor and StepGridReview (Kombi last step) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -212,6 +212,131 @@ export function useGridEditor(sessionId: string | null) {
|
||||
[grid, pushUndo],
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Column management
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
const deleteColumn = useCallback(
|
||||
(zoneIndex: number, colIndex: number) => {
|
||||
if (!grid) return
|
||||
const zone = grid.zones.find((z) => z.zone_index === zoneIndex)
|
||||
if (!zone || zone.columns.length <= 1) return // keep at least 1 column
|
||||
pushUndo(grid.zones)
|
||||
|
||||
setGrid((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
zones: prev.zones.map((z) => {
|
||||
if (z.zone_index !== zoneIndex) return z
|
||||
const newColumns = z.columns
|
||||
.filter((c) => c.index !== colIndex)
|
||||
.map((c, i) => ({ ...c, index: i, label: `column_${i + 1}` }))
|
||||
const newCells = z.cells
|
||||
.filter((c) => c.col_index !== colIndex)
|
||||
.map((c) => {
|
||||
const newCI = c.col_index > colIndex ? c.col_index - 1 : c.col_index
|
||||
return {
|
||||
...c,
|
||||
col_index: newCI,
|
||||
cell_id: `Z${zoneIndex}_R${String(c.row_index).padStart(2, '0')}_C${newCI}`,
|
||||
}
|
||||
})
|
||||
return { ...z, columns: newColumns, cells: newCells }
|
||||
}),
|
||||
summary: {
|
||||
...prev.summary,
|
||||
total_columns: prev.summary.total_columns - 1,
|
||||
total_cells: prev.zones.reduce(
|
||||
(sum, z) =>
|
||||
sum +
|
||||
(z.zone_index === zoneIndex
|
||||
? z.cells.filter((c) => c.col_index !== colIndex).length
|
||||
: z.cells.length),
|
||||
0,
|
||||
),
|
||||
},
|
||||
}
|
||||
})
|
||||
setDirty(true)
|
||||
},
|
||||
[grid, pushUndo],
|
||||
)
|
||||
|
||||
const addColumn = useCallback(
|
||||
(zoneIndex: number, afterColIndex: number) => {
|
||||
if (!grid) return
|
||||
const zone = grid.zones.find((z) => z.zone_index === zoneIndex)
|
||||
if (!zone) return
|
||||
pushUndo(grid.zones)
|
||||
|
||||
const newColIndex = afterColIndex + 1
|
||||
|
||||
setGrid((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
zones: prev.zones.map((z) => {
|
||||
if (z.zone_index !== zoneIndex) return z
|
||||
// Shift existing columns
|
||||
const shiftedCols = z.columns.map((c) =>
|
||||
c.index > afterColIndex ? { ...c, index: c.index + 1, label: `column_${c.index + 2}` } : c,
|
||||
)
|
||||
// Insert new column
|
||||
const refCol = z.columns.find((c) => c.index === afterColIndex) || z.columns[z.columns.length - 1]
|
||||
const newCol = {
|
||||
index: newColIndex,
|
||||
label: `column_${newColIndex + 1}`,
|
||||
x_min_px: refCol.x_max_px,
|
||||
x_max_px: refCol.x_max_px + (refCol.x_max_px - refCol.x_min_px),
|
||||
x_min_pct: refCol.x_max_pct,
|
||||
x_max_pct: Math.min(100, refCol.x_max_pct + (refCol.x_max_pct - refCol.x_min_pct)),
|
||||
bold: false,
|
||||
}
|
||||
const allCols = [...shiftedCols, newCol].sort((a, b) => a.index - b.index)
|
||||
|
||||
// Shift existing cells and create empty cells for new column
|
||||
const shiftedCells = z.cells.map((c) => {
|
||||
if (c.col_index > afterColIndex) {
|
||||
const newCI = c.col_index + 1
|
||||
return {
|
||||
...c,
|
||||
col_index: newCI,
|
||||
cell_id: `Z${zoneIndex}_R${String(c.row_index).padStart(2, '0')}_C${newCI}`,
|
||||
}
|
||||
}
|
||||
return c
|
||||
})
|
||||
// Create empty cells for each row
|
||||
const newCells = z.rows.map((row) => ({
|
||||
cell_id: `Z${zoneIndex}_R${String(row.index).padStart(2, '0')}_C${newColIndex}`,
|
||||
zone_index: zoneIndex,
|
||||
row_index: row.index,
|
||||
col_index: newColIndex,
|
||||
col_type: `column_${newColIndex + 1}`,
|
||||
text: '',
|
||||
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 { ...z, columns: allCols, cells: [...shiftedCells, ...newCells] }
|
||||
}),
|
||||
summary: {
|
||||
...prev.summary,
|
||||
total_columns: prev.summary.total_columns + 1,
|
||||
total_cells: prev.summary.total_cells + (zone?.rows.length ?? 0),
|
||||
},
|
||||
}
|
||||
})
|
||||
setDirty(true)
|
||||
},
|
||||
[grid, pushUndo],
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Undo / Redo
|
||||
// ------------------------------------------------------------------
|
||||
@@ -284,5 +409,7 @@ export function useGridEditor(sessionId: string | null) {
|
||||
canUndo,
|
||||
canRedo,
|
||||
getAdjacentCell,
|
||||
deleteColumn,
|
||||
addColumn,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user