'use client'
/**
* SpreadsheetView — Fortune Sheet integration for unified grid display.
*
* Converts unified grid data into Fortune Sheet format and renders
* a full-featured Excel-like spreadsheet editor.
*/
import { useMemo } from 'react'
import dynamic from 'next/dynamic'
// Lazy-load Fortune Sheet (uses canvas, SSR-incompatible)
const Workbook = dynamic(
() => import('@fortune-sheet/react').then((m) => m.Workbook),
{ ssr: false, loading: () =>
Spreadsheet wird geladen...
},
)
// Import Fortune Sheet CSS
import '@fortune-sheet/react/dist/index.css'
import type { GridZone, GridEditorCell } from '@/components/grid-editor/types'
interface SpreadsheetViewProps {
unifiedGrid: any // unified_grid_result from backend
height?: number
}
/**
* Convert unified grid data to Fortune Sheet format.
*/
function unifiedGridToSheet(grid: any) {
const zone: GridZone | undefined = grid?.zones?.[0]
if (!zone) return null
const numRows = zone.rows.length
const numCols = zone.columns.length
// Build celldata array
const celldata: any[] = []
const merges: Record = {}
// Build cell lookup
const cellMap = new Map()
for (const cell of zone.cells) {
cellMap.set(`${cell.row_index}_${cell.col_index}`, cell)
}
for (const cell of zone.cells) {
const r = cell.row_index
const c = cell.col_index
const text = cell.text ?? ''
const isBox = cell.source_zone_type === 'box'
const boxHex = cell.box_region?.bg_hex
// Cell value
const v: any = {
v: text,
m: text,
}
// Bold
if (cell.is_bold) {
v.bl = 1
}
// Text color from word_boxes or color_override
const color = cell.color_override
?? cell.word_boxes?.find((wb: any) => wb.color_name && wb.color_name !== 'black')?.color
if (color) {
v.fc = color
}
// Box background tint
if (isBox && boxHex) {
v.bg = `${boxHex}15` // very light tint
}
// Multi-line: enable text wrap
if (text.includes('\n')) {
v.tb = '2' // text wrap
}
celldata.push({ r, c, v })
// Colspan → merge
const colspan = cell.colspan || 0
if (colspan > 1 || cell.col_type === 'spanning_header') {
const cs = colspan || numCols
merges[`${r}_${c}`] = { r, c, rs: 1, cs }
}
}
// Column widths from zone columns
const columnlen: Record = {}
for (const col of zone.columns) {
const w = (col.x_max_px ?? 0) - (col.x_min_px ?? 0)
columnlen[String(col.index)] = Math.max(60, Math.round(w * 0.4)) // scale down for screen
}
// Row heights
const dominantH = grid.dominant_row_h || 30
const rowlen: Record = {}
for (const row of zone.rows) {
// Count max lines in this row's cells
const rowCells = zone.cells.filter((c: any) => c.row_index === row.index)
const maxLines = Math.max(1, ...rowCells.map((c: any) => (c.text ?? '').split('\n').length))
rowlen[String(row.index)] = Math.max(22, Math.round(dominantH * 0.6 * maxLines))
}
// Box region borders
const borderInfo: any[] = []
// Collect box cells and draw borders around box regions
const boxCells = zone.cells.filter((c: any) => c.source_zone_type === 'box' && c.box_region?.border)
if (boxCells.length > 0) {
const boxHex = boxCells[0].box_region?.bg_hex || '#2563eb'
const boxRows = [...new Set(boxCells.map((c: any) => c.row_index))].sort((a: number, b: number) => a - b)
const boxCols = [...new Set(boxCells.map((c: any) => c.col_index))].sort((a: number, b: number) => a - b)
if (boxRows.length > 0 && boxCols.length > 0) {
borderInfo.push({
rangeType: 'range',
borderType: 'border-all',
color: boxHex,
style: 1,
range: [{
row: [boxRows[0], boxRows[boxRows.length - 1]],
column: [boxCols[0], boxCols[boxCols.length - 1]],
}],
})
}
}
return {
name: 'Seite',
id: 'unified',
celldata,
row: numRows,
column: numCols,
config: {
merge: Object.keys(merges).length > 0 ? merges : undefined,
columnlen,
rowlen,
borderInfo: borderInfo.length > 0 ? borderInfo : undefined,
},
}
}
export function SpreadsheetView({ unifiedGrid, height = 600 }: SpreadsheetViewProps) {
const sheet = useMemo(() => unifiedGridToSheet(unifiedGrid), [unifiedGrid])
if (!sheet) {
return Keine Daten für Spreadsheet.
}
return (
)
}