206672a858
Replace German labels, button text, toast messages, table headers, tooltips, and placeholder strings across 7 files: - WorkflowEditor: buttons, toasts, node labels - Tenants: buttons, toasts, dialog text, table headers - Admin: widget layout description - OrderDetail: column headers (Baureihe→Series, Ebene→Level, Lagertyp→Bearing Type) - ExcelSpreadsheet: column label definitions - Upload: series/duplicate warning strings - TemplateEditor: ALL_FIELD_DEFS default labels API field names (baureihe, ebene1, produkt_baureihe etc.) unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
169 lines
6.9 KiB
TypeScript
169 lines
6.9 KiB
TypeScript
import React from 'react'
|
|
import { ParsedRow, ParsedComponent, ParsedExcelResponse } from '../../api/uploads'
|
|
|
|
interface Props {
|
|
parsed: ParsedExcelResponse
|
|
rows: ParsedRow[]
|
|
onChange: (rows: ParsedRow[]) => void
|
|
}
|
|
|
|
const STANDARD_FIELDS: { key: keyof ParsedRow; label: string; width: number; mono?: boolean }[] = [
|
|
{ key: 'ebene1', label: 'Level 1', width: 140 },
|
|
{ key: 'ebene2', label: 'Level 2', width: 120 },
|
|
{ key: 'baureihe', label: 'Series', width: 160 },
|
|
{ key: 'pim_id', label: 'PIM-ID', width: 110 },
|
|
{ key: 'produkt_baureihe', label: 'Product Series', width: 150 },
|
|
{ key: 'gewaehltes_produkt', label: 'Selected Product', width: 150 },
|
|
{ key: 'name_cad_modell', label: 'CAD-Modell', width: 190, mono: true },
|
|
{ key: 'gewuenschte_bildnummer', label: 'Image No.', width: 170, mono: true },
|
|
{ key: 'lagertyp', label: 'Bearing Type', width: 100 },
|
|
]
|
|
|
|
export default function ExcelSpreadsheet({ parsed, rows, onChange }: Props) {
|
|
const maxComps = Math.max(0, ...rows.map((r) => r.components.length))
|
|
|
|
function updateField(ri: number, field: keyof ParsedRow, value: string | boolean | null) {
|
|
const next = rows.map((r, i) => (i === ri ? { ...r, [field]: value } : r))
|
|
onChange(next)
|
|
}
|
|
|
|
function updateComp(ri: number, ci: number, field: keyof ParsedComponent, value: string) {
|
|
const next = rows.map((r, i) => {
|
|
if (i !== ri) return r
|
|
const comps = r.components.map((c, j) =>
|
|
j === ci ? { ...c, [field]: value || null } : c,
|
|
)
|
|
// If the row doesn't have this component slot yet, pad it
|
|
while (comps.length <= ci) {
|
|
comps.push({ part_name: null, material: null, component_type: null, column_index: 11 + comps.length * 2 })
|
|
}
|
|
comps[ci] = { ...comps[ci], [field]: value || null }
|
|
return { ...r, components: comps }
|
|
})
|
|
onChange(next)
|
|
}
|
|
|
|
const cell =
|
|
'w-full px-2 py-1 text-xs bg-transparent border-0 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-accent rounded focus:bg-surface'
|
|
const th =
|
|
'px-2 py-2 text-left text-xs font-semibold text-content-secondary whitespace-nowrap bg-surface-alt border-b border-r border-border-default sticky top-0 z-10'
|
|
const td = 'border-b border-r border-border-light p-0'
|
|
|
|
return (
|
|
<div className="card overflow-hidden">
|
|
<div className="p-4 border-b border-border-default flex items-center justify-between">
|
|
<div>
|
|
<h2 className="font-semibold text-content">
|
|
{parsed.template_name || parsed.category_key} — {rows.length} rows
|
|
</h2>
|
|
<p className="text-xs text-content-muted mt-0.5">Click any cell to edit before creating the order</p>
|
|
</div>
|
|
<span className="badge badge-blue">{maxComps} component columns</span>
|
|
</div>
|
|
|
|
<div className="overflow-auto" style={{ maxHeight: '65vh' }}>
|
|
<table className="text-sm border-collapse" style={{ minWidth: 'max-content' }}>
|
|
<thead>
|
|
{/* Group header row */}
|
|
<tr>
|
|
<th className={`${th} text-center`} colSpan={1}>#</th>
|
|
<th className={`${th} text-center bg-status-info-bg`} colSpan={STANDARD_FIELDS.length}>
|
|
Standard Fields
|
|
</th>
|
|
<th className={`${th} text-center`}>Rendering</th>
|
|
{Array.from({ length: maxComps }, (_, i) => (
|
|
<th key={i} className={`${th} text-center bg-status-warning-bg`} colSpan={2}>
|
|
Component {i + 1}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
|
|
{/* Field name row */}
|
|
<tr>
|
|
<th className={`${th} text-content-muted`}>#</th>
|
|
{STANDARD_FIELDS.map((f) => (
|
|
<th key={f.key} className={`${th} bg-status-info-bg`} style={{ minWidth: f.width }}>
|
|
{f.label}
|
|
</th>
|
|
))}
|
|
<th className={`${th} text-center`} style={{ minWidth: 72 }}>
|
|
Rendering
|
|
</th>
|
|
{Array.from({ length: maxComps }, (_, i) => (
|
|
<React.Fragment key={i}>
|
|
<th className={`${th} bg-status-warning-bg`} style={{ minWidth: 180 }}>
|
|
Part Name
|
|
</th>
|
|
<th className={`${th} bg-status-warning-bg`} style={{ minWidth: 110 }}>
|
|
Material
|
|
</th>
|
|
</React.Fragment>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
{rows.map((row, ri) => (
|
|
<tr key={row.row_index} className={ri % 2 === 0 ? 'bg-surface' : 'bg-surface-alt/50'}>
|
|
{/* Row number */}
|
|
<td className={`${td} px-2 py-1.5 text-xs text-content-muted font-mono text-right`}>
|
|
{row.row_index}
|
|
</td>
|
|
|
|
{/* Standard text fields */}
|
|
{STANDARD_FIELDS.map((f) => (
|
|
<td key={f.key} className={td}>
|
|
<input
|
|
type="text"
|
|
value={(row[f.key] as string | null) ?? ''}
|
|
onChange={(e) => updateField(ri, f.key, e.target.value || null)}
|
|
className={`${cell} ${f.mono ? 'font-mono' : ''}`}
|
|
/>
|
|
</td>
|
|
))}
|
|
|
|
{/* Rendering checkbox */}
|
|
<td className={`${td} text-center`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={row.medias_rendering ?? false}
|
|
onChange={(e) => updateField(ri, 'medias_rendering', e.target.checked)}
|
|
className="w-3.5 h-3.5"
|
|
/>
|
|
</td>
|
|
|
|
{/* Component pairs */}
|
|
{Array.from({ length: maxComps }, (_, ci) => {
|
|
const comp = row.components[ci]
|
|
return (
|
|
<React.Fragment key={ci}>
|
|
<td className={td}>
|
|
<input
|
|
type="text"
|
|
value={comp?.part_name ?? ''}
|
|
onChange={(e) => updateComp(ri, ci, 'part_name', e.target.value)}
|
|
className={`${cell} font-mono`}
|
|
placeholder="—"
|
|
/>
|
|
</td>
|
|
<td className={td}>
|
|
<input
|
|
type="text"
|
|
value={comp?.material ?? ''}
|
|
onChange={(e) => updateComp(ri, ci, 'material', e.target.value)}
|
|
className={cell}
|
|
placeholder="—"
|
|
/>
|
|
</td>
|
|
</React.Fragment>
|
|
)
|
|
})}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|