i18n(frontend): translate all German UI strings to English

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>
This commit is contained in:
2026-03-08 20:07:01 +01:00
parent 915abe9d74
commit 206672a858
7 changed files with 122 additions and 122 deletions
@@ -39,15 +39,15 @@ interface Template {
// ---------------------------------------------------------------------------
const ALL_FIELD_DEFS: { key: string; defaultLabel: string }[] = [
{ key: 'ebene1', defaultLabel: 'Ebene 1' },
{ key: 'ebene2', defaultLabel: 'Ebene 2' },
{ key: 'baureihe', defaultLabel: 'Baureihe' },
{ key: 'ebene1', defaultLabel: 'Level 1' },
{ key: 'ebene2', defaultLabel: 'Level 2' },
{ key: 'baureihe', defaultLabel: 'Series' },
{ key: 'pim_id', defaultLabel: 'PIM-ID' },
{ key: 'produkt_baureihe', defaultLabel: 'Produkt / Baureihe' },
{ key: 'gewaehltes_produkt', defaultLabel: 'Gewähltes Produkt' },
{ key: 'name_cad_modell', defaultLabel: 'Name CAD-Modell' },
{ key: 'gewuenschte_bildnummer',defaultLabel: 'Gewünschte Bildnummer' },
{ key: 'lagertyp', defaultLabel: 'Lagertyp' },
{ key: 'produkt_baureihe', defaultLabel: 'Product / Series' },
{ key: 'gewaehltes_produkt', defaultLabel: 'Selected Product' },
{ key: 'name_cad_modell', defaultLabel: 'CAD Model Name' },
{ key: 'gewuenschte_bildnummer',defaultLabel: 'Desired Image No.' },
{ key: 'lagertyp', defaultLabel: 'Bearing Type' },
{ key: 'medias_rendering', defaultLabel: 'Medias Rendering' },
]
@@ -8,15 +8,15 @@ interface Props {
}
const STANDARD_FIELDS: { key: keyof ParsedRow; label: string; width: number; mono?: boolean }[] = [
{ key: 'ebene1', label: 'Ebene 1', width: 140 },
{ key: 'ebene2', label: 'Ebene 2', width: 120 },
{ key: 'baureihe', label: 'Baureihe', width: 160 },
{ 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: 'Produkt-Baureihe', width: 150 },
{ key: 'gewaehltes_produkt', label: 'Gewähltes Produkt', width: 150 },
{ 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: 'Bildnummer', width: 170, mono: true },
{ key: 'lagertyp', label: 'Lagertyp', width: 100 },
{ 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) {
+5 -5
View File
@@ -950,18 +950,18 @@ export default function AdminPage() {
<div>
<h2 className="font-semibold text-content">Dashboard Widget-Konfiguration</h2>
<p className="text-xs text-content-muted mt-0.5">
Legt das Standard-Widget-Layout für alle Nutzer dieses Tenants fest. Nutzer können ihr eigenes Layout individuell anpassen.
Sets the default widget layout for all users of this tenant. Users can customize their own layout individually.
</p>
</div>
</div>
<div className="p-4 flex items-center gap-4">
<div className="flex-1">
<p className="text-sm text-content-secondary">
Tenant-Standard:{' '}
Tenant default:{' '}
<span className="font-medium text-content">
{tenantDefaultWidgets && tenantDefaultWidgets.length > 0
? `${tenantDefaultWidgets.length} Widget${tenantDefaultWidgets.length !== 1 ? 's' : ''} konfiguriert`
: 'Noch kein Standard festgelegt (Systemvorgabe aktiv)'}
? `${tenantDefaultWidgets.length} Widget${tenantDefaultWidgets.length !== 1 ? 's' : ''} configured`
: 'No default set yet (system default active)'}
</span>
</p>
</div>
@@ -970,7 +970,7 @@ export default function AdminPage() {
className="btn-secondary text-sm flex items-center gap-2"
>
<LayoutDashboard size={14} />
Tenant-Standard-Dashboard bearbeiten
Edit Tenant Default Dashboard
</button>
</div>
</div>
+24 -24
View File
@@ -584,7 +584,7 @@ export default function OrderDetailPage() {
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-content-muted pointer-events-none" />
<input
type="text"
placeholder="Search name, Baureihe, Ebene…"
placeholder="Search name, series, level…"
value={filters.search}
onChange={(e) => updateFilter('search', e.target.value)}
className="w-full pl-8 pr-3 py-1.5 text-sm border border-border-default rounded-md focus:outline-none focus:ring-2 focus:ring-accent"
@@ -955,10 +955,10 @@ function OrderItemsTable({
Img
</th>
<SortableTh label="CAD Model" sortKey="name_cad_modell" current={sortKey} dir={sortDir} onSort={onSort} className="min-w-[160px]" />
<SortableTh label="Baureihe" sortKey="baureihe" current={sortKey} dir={sortDir} onSort={onSort} className="min-w-[110px]" />
<th className="py-2.5 px-3 text-left text-xs font-semibold text-content-secondary uppercase tracking-wide">Ebene 1</th>
<th className="py-2.5 px-3 text-left text-xs font-semibold text-content-secondary uppercase tracking-wide">Ebene 2</th>
<th className="py-2.5 px-3 text-left text-xs font-semibold text-content-secondary uppercase tracking-wide">Lagertyp</th>
<SortableTh label="Series" sortKey="baureihe" current={sortKey} dir={sortDir} onSort={onSort} className="min-w-[110px]" />
<th className="py-2.5 px-3 text-left text-xs font-semibold text-content-secondary uppercase tracking-wide">Level 1</th>
<th className="py-2.5 px-3 text-left text-xs font-semibold text-content-secondary uppercase tracking-wide">Level 2</th>
<th className="py-2.5 px-3 text-left text-xs font-semibold text-content-secondary uppercase tracking-wide">Bearing Type</th>
<th className="py-2.5 px-3 text-center text-xs font-semibold text-content-secondary uppercase tracking-wide">Rendering</th>
<th className="py-2.5 px-3 text-center text-xs font-semibold text-content-secondary uppercase tracking-wide">Parts</th>
<th className="py-2.5 px-3 text-center text-xs font-semibold text-content-secondary uppercase tracking-wide">STEP</th>
@@ -1076,22 +1076,22 @@ function ItemTableRow({
<p className="truncate" title={item.name_cad_modell ?? undefined}>{item.name_cad_modell || '—'}</p>
</td>
{/* Baureihe */}
{/* Series */}
<td className="py-2.5 px-3 text-content-secondary text-xs max-w-[130px]">
<p className="truncate">{item.baureihe || '—'}</p>
</td>
{/* Ebene 1 */}
{/* Level 1 */}
<td className="py-2.5 px-3 text-content-secondary text-xs max-w-[120px]">
<p className="truncate">{item.ebene1 || '—'}</p>
</td>
{/* Ebene 2 */}
{/* Level 2 */}
<td className="py-2.5 px-3 text-content-secondary text-xs max-w-[120px]">
<p className="truncate">{item.ebene2 || '—'}</p>
</td>
{/* Lagertyp */}
{/* Bearing Type */}
<td className="py-2.5 px-3 text-content-secondary text-xs max-w-[110px]">
<p className="truncate">{item.lagertyp || '—'}</p>
</td>
@@ -1175,12 +1175,12 @@ function ItemTableRow({
</div>
<div className="flex-1 grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
<Field label="Ebene 1" value={item.ebene1} />
<Field label="Level 1" value={item.ebene1} />
<Field label="PIM-ID" value={item.pim_id} />
<Field label="Produkt / Baureihe" value={item.produkt_baureihe} />
<Field label="Gewähltes Produkt" value={item.gewaehltes_produkt} />
<Field label="Bildnummer" value={item.gewuenschte_bildnummer} />
<Field label="Lagertyp" value={item.lagertyp} />
<Field label="Product / Series" value={item.produkt_baureihe} />
<Field label="Selected Product" value={item.gewaehltes_produkt} />
<Field label="Image No." value={item.gewuenschte_bildnummer} />
<Field label="Bearing Type" value={item.lagertyp} />
<Field label="Medias Rendering" value={item.medias_rendering?.toString() ?? '—'} />
</div>
</div>
@@ -1350,15 +1350,15 @@ function GalleryCard({ item, isDraft, onClick }: { item: any; isDraft: boolean;
// ── Source Spreadsheet ────────────────────────────────────────────────────────
const STD_COLS: { key: keyof OrderItem; label: string; editable?: boolean }[] = [
{ key: 'ebene1', label: 'Ebene 1', editable: true },
{ key: 'ebene2', label: 'Ebene 2', editable: true },
{ key: 'baureihe', label: 'Baureihe', editable: true },
{ key: 'ebene1', label: 'Level 1', editable: true },
{ key: 'ebene2', label: 'Level 2', editable: true },
{ key: 'baureihe', label: 'Series', editable: true },
{ key: 'pim_id', label: 'PIM-ID', editable: true },
{ key: 'produkt_baureihe', label: 'Produkt/Baureihe',editable: true },
{ key: 'gewaehltes_produkt', label: 'Gew. Produkt', editable: true },
{ key: 'produkt_baureihe', label: 'Product/Series', editable: true },
{ key: 'gewaehltes_produkt', label: 'Sel. Product', editable: true },
{ key: 'name_cad_modell', label: 'CAD-Modell', editable: true },
{ key: 'gewuenschte_bildnummer', label: 'Bildnummer', editable: true },
{ key: 'lagertyp', label: 'Lagertyp', editable: true },
{ key: 'gewuenschte_bildnummer', label: 'Image No.', editable: true },
{ key: 'lagertyp', label: 'Bearing Type', editable: true },
]
function SourceSpreadsheet({
@@ -1606,9 +1606,9 @@ function GalleryModal({
{/* Fields */}
<div className="px-5 py-3 grid grid-cols-2 gap-x-6 gap-y-1 text-sm border-t border-border-light">
<Field label="Baureihe" value={item.baureihe} />
<Field label="Ebene 2" value={item.ebene2} />
<Field label="Lagertyp" value={item.lagertyp} />
<Field label="Series" value={item.baureihe} />
<Field label="Level 2" value={item.ebene2} />
<Field label="Bearing Type" value={item.lagertyp} />
<Field label="Rendering" value={item.medias_rendering?.toString() ?? '—'} />
<Field label="Components" value={String(item.components?.length ?? 0)} />
<Field label="PIM-ID" value={item.pim_id} />
+37 -37
View File
@@ -66,34 +66,34 @@ export default function TenantsPage() {
const createMut = useMutation({
mutationFn: (data: TenantCreate) => createTenant(data),
onSuccess: () => {
toast.success('Tenant erstellt')
toast.success('Tenant created')
qc.invalidateQueries({ queryKey: ['tenants'] })
setShowCreate(false)
setCreateForm({ name: '', slug: '', is_active: true })
setSlugEdited(false)
},
onError: (e: any) => toast.error(e.response?.data?.detail || 'Fehler beim Erstellen'),
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to create'),
})
const updateMut = useMutation({
mutationFn: ({ id, data }: { id: string; data: TenantUpdate }) => updateTenant(id, data),
onSuccess: () => {
toast.success('Tenant aktualisiert')
toast.success('Tenant updated')
qc.invalidateQueries({ queryKey: ['tenants'] })
setEditingTenant(null)
},
onError: (e: any) => toast.error(e.response?.data?.detail || 'Fehler beim Aktualisieren'),
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to update'),
})
const deleteMut = useMutation({
mutationFn: (id: string) => deleteTenant(id),
onSuccess: (_data, id) => {
toast.success('Tenant gelöscht')
toast.success('Tenant deleted')
qc.invalidateQueries({ queryKey: ['tenants'] })
if (activeTenantId === id) setActiveTenantId(null)
setDeletingId(null)
},
onError: (e: any) => toast.error(e.response?.data?.detail || 'Fehler beim Löschen'),
onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to delete'),
})
// Auto-generate slug from name unless manually edited
@@ -123,7 +123,7 @@ export default function TenantsPage() {
<Building2 size={24} className="text-accent" />
<div>
<h1 className="text-xl font-bold text-content">Tenants</h1>
<p className="text-sm text-content-muted">Mandanten verwalten und Kontext wählen</p>
<p className="text-sm text-content-muted">Manage tenants and select context</p>
</div>
</div>
<button
@@ -131,14 +131,14 @@ export default function TenantsPage() {
className="flex items-center gap-2 px-4 py-2 bg-accent text-accent-text rounded-md text-sm font-medium hover:bg-accent-hover transition-colors"
>
<Plus size={16} />
Neuer Tenant
New Tenant
</button>
</div>
{/* Tenant Selector */}
<div className="mb-6 p-4 bg-surface border border-border-default rounded-lg">
<p className="text-xs font-semibold text-content-muted uppercase tracking-wider mb-2">
Admin Cross-Tenant-Ansicht
Admin Cross-Tenant View
</p>
<div className="relative inline-block">
<button
@@ -147,7 +147,7 @@ export default function TenantsPage() {
>
<Building2 size={14} className="text-content-muted shrink-0" />
<span className="flex-1 text-left truncate">
{activeTenant ? activeTenant.name : 'Alle Tenants / Admin-Ansicht'}
{activeTenant ? activeTenant.name : 'All Tenants / Admin View'}
</span>
<ChevronDown size={14} className="text-content-muted shrink-0" />
</button>
@@ -159,7 +159,7 @@ export default function TenantsPage() {
>
{activeTenantId === null && <Check size={14} className="text-accent shrink-0" />}
{activeTenantId !== null && <span className="w-[14px] shrink-0" />}
<span>Alle Tenants / Admin-Ansicht</span>
<span>All Tenants / Admin View</span>
</button>
{tenants.map((t) => (
<button
@@ -171,7 +171,7 @@ export default function TenantsPage() {
{activeTenantId !== t.id && <span className="w-[14px] shrink-0" />}
<span className="flex-1 truncate">{t.name}</span>
{!t.is_active && (
<span className="text-xs px-1.5 py-0.5 rounded bg-surface-muted text-content-muted">inaktiv</span>
<span className="text-xs px-1.5 py-0.5 rounded bg-surface-muted text-content-muted">inactive</span>
)}
</button>
))}
@@ -180,7 +180,7 @@ export default function TenantsPage() {
</div>
{activeTenant && (
<p className="mt-2 text-xs text-content-muted">
API-Requests werden mit Header <code className="font-mono bg-surface-alt px-1 rounded">X-Tenant-ID: {activeTenant.id}</code> gesendet.
API requests are sent with header <code className="font-mono bg-surface-alt px-1 rounded">X-Tenant-ID: {activeTenant.id}</code>.
</p>
)}
</div>
@@ -194,22 +194,22 @@ export default function TenantsPage() {
<th className="px-4 py-3 text-left font-semibold text-content-secondary">Slug</th>
<th className="px-4 py-3 text-left font-semibold text-content-secondary">Status</th>
<th className="px-4 py-3 text-left font-semibold text-content-secondary">
<span className="flex items-center gap-1"><Users size={13} /> Nutzer</span>
<span className="flex items-center gap-1"><Users size={13} /> Users</span>
</th>
<th className="px-4 py-3 text-left font-semibold text-content-secondary">Erstellt</th>
<th className="px-4 py-3 text-left font-semibold text-content-secondary">Created</th>
<th className="px-4 py-3" />
</tr>
</thead>
<tbody>
{isLoading && (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-content-muted">Lade Tenants</td>
<td colSpan={6} className="px-4 py-8 text-center text-content-muted">Loading tenants</td>
</tr>
)}
{!isLoading && tenants.length === 0 && (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-content-muted">
Noch keine Tenants vorhanden.
No tenants yet.
</td>
</tr>
)}
@@ -218,7 +218,7 @@ export default function TenantsPage() {
<td className="px-4 py-3 font-medium text-content">
<div className="flex items-center gap-2">
{activeTenantId === tenant.id && (
<span className="w-1.5 h-1.5 rounded-full bg-accent shrink-0" title="Aktiver Kontext" />
<span className="w-1.5 h-1.5 rounded-full bg-accent shrink-0" title="Active context" />
)}
{tenant.name}
</div>
@@ -227,11 +227,11 @@ export default function TenantsPage() {
<td className="px-4 py-3">
{tenant.is_active ? (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-status-success-bg text-status-success-text">
aktiv
active
</span>
) : (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-surface-muted text-content-muted">
inaktiv
inactive
</span>
)}
</td>
@@ -244,14 +244,14 @@ export default function TenantsPage() {
<button
onClick={() => openEdit(tenant)}
className="p-1.5 rounded hover:bg-surface-alt text-content-muted hover:text-content transition-colors"
title="Bearbeiten"
title="Edit"
>
<Pencil size={15} />
</button>
<button
onClick={() => setDeletingId(tenant.id)}
className="p-1.5 rounded hover:bg-status-error-bg text-content-muted hover:text-status-error-text transition-colors"
title="Löschen"
title="Delete"
>
<Trash2 size={15} />
</button>
@@ -268,7 +268,7 @@ export default function TenantsPage() {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-surface rounded-xl shadow-xl w-full max-w-md mx-4 p-6">
<div className="flex items-center justify-between mb-5">
<h2 className="text-lg font-semibold text-content">Neuer Tenant</h2>
<h2 className="text-lg font-semibold text-content">New Tenant</h2>
<button
onClick={() => setShowCreate(false)}
className="p-1.5 rounded hover:bg-surface-alt text-content-muted transition-colors"
@@ -284,7 +284,7 @@ export default function TenantsPage() {
type="text"
value={createForm.name}
onChange={(e) => handleCreateNameChange(e.target.value)}
placeholder="z.B. Schaeffler GmbH"
placeholder="e.g. Schaeffler GmbH"
className="w-full px-3 py-2 rounded-md border border-border-default bg-surface text-content text-sm focus:outline-none focus:ring-2 focus:ring-accent/50"
/>
</div>
@@ -292,13 +292,13 @@ export default function TenantsPage() {
<div>
<label className="block text-sm font-medium text-content-secondary mb-1">
Slug *
<span className="text-xs font-normal text-content-muted ml-1">(URL-Kennung, automatisch generiert)</span>
<span className="text-xs font-normal text-content-muted ml-1">(URL identifier, auto-generated)</span>
</label>
<input
type="text"
value={createForm.slug}
onChange={(e) => handleCreateSlugChange(e.target.value)}
placeholder="z.B. schaeffler-gmbh"
placeholder="e.g. schaeffler-gmbh"
className="w-full px-3 py-2 rounded-md border border-border-default bg-surface text-content text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
/>
</div>
@@ -313,7 +313,7 @@ export default function TenantsPage() {
/>
<div className="w-9 h-5 bg-surface-muted rounded-full peer peer-checked:bg-accent transition-colors after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-4" />
</label>
<span className="text-sm text-content-secondary">Aktiv</span>
<span className="text-sm text-content-secondary">Active</span>
</div>
</div>
@@ -322,14 +322,14 @@ export default function TenantsPage() {
onClick={() => setShowCreate(false)}
className="px-4 py-2 text-sm rounded-md border border-border-default text-content-secondary hover:bg-surface-alt transition-colors"
>
Abbrechen
Cancel
</button>
<button
onClick={() => createMut.mutate(createForm)}
disabled={!createForm.name.trim() || !createForm.slug.trim() || createMut.isPending}
className="px-4 py-2 text-sm rounded-md bg-accent text-accent-text font-medium hover:bg-accent-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{createMut.isPending ? 'Erstelle…' : 'Erstellen'}
{createMut.isPending ? 'Creating…' : 'Create'}
</button>
</div>
</div>
@@ -341,7 +341,7 @@ export default function TenantsPage() {
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-surface rounded-xl shadow-xl w-full max-w-md mx-4 p-6">
<div className="flex items-center justify-between mb-5">
<h2 className="text-lg font-semibold text-content">Tenant bearbeiten</h2>
<h2 className="text-lg font-semibold text-content">Edit Tenant</h2>
<button
onClick={() => setEditingTenant(null)}
className="p-1.5 rounded hover:bg-surface-alt text-content-muted transition-colors"
@@ -381,7 +381,7 @@ export default function TenantsPage() {
/>
<div className="w-9 h-5 bg-surface-muted rounded-full peer peer-checked:bg-accent transition-colors after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-4" />
</label>
<span className="text-sm text-content-secondary">Aktiv</span>
<span className="text-sm text-content-secondary">Active</span>
</div>
</div>
@@ -390,14 +390,14 @@ export default function TenantsPage() {
onClick={() => setEditingTenant(null)}
className="px-4 py-2 text-sm rounded-md border border-border-default text-content-secondary hover:bg-surface-alt transition-colors"
>
Abbrechen
Cancel
</button>
<button
onClick={() => updateMut.mutate({ id: editingTenant.id, data: editForm })}
disabled={updateMut.isPending}
className="px-4 py-2 text-sm rounded-md bg-accent text-accent-text font-medium hover:bg-accent-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{updateMut.isPending ? 'Speichere…' : 'Speichern'}
{updateMut.isPending ? 'Saving…' : 'Save'}
</button>
</div>
</div>
@@ -413,9 +413,9 @@ export default function TenantsPage() {
<Trash2 size={16} className="text-status-error-text" />
</div>
<div>
<h2 className="text-base font-semibold text-content">Tenant löschen?</h2>
<h2 className="text-base font-semibold text-content">Delete tenant?</h2>
<p className="text-sm text-content-muted mt-1">
Diese Aktion kann nicht rückgängig gemacht werden.
This action cannot be undone.
</p>
</div>
</div>
@@ -424,14 +424,14 @@ export default function TenantsPage() {
onClick={() => setDeletingId(null)}
className="px-4 py-2 text-sm rounded-md border border-border-default text-content-secondary hover:bg-surface-alt transition-colors"
>
Abbrechen
Cancel
</button>
<button
onClick={() => deleteMut.mutate(deletingId)}
disabled={deleteMut.isPending}
className="px-4 py-2 text-sm rounded-md bg-status-error-bg text-status-error-text font-medium hover:opacity-80 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
>
{deleteMut.isPending ? 'Lösche…' : 'Löschen'}
{deleteMut.isPending ? 'Deleting…' : 'Delete'}
</button>
</div>
</div>
+9 -9
View File
@@ -299,7 +299,7 @@ export default function UploadPage() {
<p className="text-xs text-content-secondary mb-3 leading-relaxed">
No products have been created yet. This is a <strong>preview</strong> of what will happen when you finalize the order.
Each unique <strong>Produkt (Baureihe)</strong> in the Excel becomes one product in the library.
Each unique <strong>Product (Series)</strong> in the Excel becomes one product in the library.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
@@ -321,14 +321,14 @@ export default function UploadPage() {
icon={<Ban size={18} className="text-amber-600" />}
value={previewResult.no_pim_id_count}
label="rows skipped"
description="No PIM-ID or Baureihe found. Cannot be matched to a product."
description="No PIM-ID or Product Series found. Cannot be matched to a product."
color="bg-status-warning-bg border-border-default text-status-warning-text"
/>
<StatCard
icon={<Copy size={18} className="text-orange-600" />}
value={previewResult.duplicate_count}
label="duplicate Baureihe"
description="Same Produkt-Baureihe appears multiple times. Pre-unchecked — only first occurrence imported."
label="duplicate Series"
description="Same Product Series appears multiple times. Pre-unchecked — only first occurrence imported."
color="bg-status-warning-bg border-border-default text-status-warning-text"
/>
</div>
@@ -341,10 +341,10 @@ export default function UploadPage() {
<span className="text-status-warning-text font-bold text-sm shrink-0"></span>
<div>
<p className="text-sm font-semibold text-status-warning-text">
{previewResult.duplicate_count} duplicate Produkt-Baureihe row{previewResult.duplicate_count !== 1 ? 's' : ''} detected
{previewResult.duplicate_count} duplicate Product Series row{previewResult.duplicate_count !== 1 ? 's' : ''} detected
</p>
<p className="text-xs text-status-warning-text mt-0.5">
Each product is unique only the <strong>first occurrence</strong> of a Baureihe will be imported.
Each product is unique only the <strong>first occurrence</strong> of a Product Series will be imported.
Duplicate rows are pre-unchecked (shown in amber). You can manually re-check them to overwrite the first.
</p>
</div>
@@ -378,7 +378,7 @@ export default function UploadPage() {
/>
</th>
<th className="px-4 py-2 font-medium text-content-secondary">PIM-ID</th>
<th className="px-4 py-2 font-medium text-content-secondary">Baureihe</th>
<th className="px-4 py-2 font-medium text-content-secondary">Series</th>
<th className="px-4 py-2 font-medium text-content-secondary"
title="Gew\u00e4hltes Produkt \u2014 the specific material/coating variant from the Excel"
>Gew. Produkt</th>
@@ -424,14 +424,14 @@ export default function UploadPage() {
{!hasId ? (
<span
className="text-xs px-2 py-0.5 rounded-full bg-surface-muted text-content-muted"
title="No PIM-ID or Baureihe — this row will be skipped"
title="No PIM-ID or Product Series — this row will be skipped"
>
skipped
</span>
) : row.is_duplicate ? (
<span
className="text-xs px-2 py-0.5 rounded-full bg-status-warning-bg text-status-warning-text font-medium"
title={`Duplicate Produkt-Baureihe — first occurrence is row ${row.duplicate_of_row}. Uncheck to exclude.`}
title={`Duplicate Product Series — first occurrence is row ${row.duplicate_of_row}. Uncheck to exclude.`}
>
Duplicate of row {row.duplicate_of_row}
</span>
+32 -32
View File
@@ -81,7 +81,7 @@ function InputNode({ selected }: { selected?: boolean }) {
label="STEP Input"
icon={<FileUp size={14} />}
color="green"
description="STEP-Datei Eingang"
description="STEP file input"
selected={selected}
hasTarget={false}
/>
@@ -91,7 +91,7 @@ function InputNode({ selected }: { selected?: boolean }) {
function ConvertNode({ selected }: { selected?: boolean }) {
return (
<BaseNode
label="STL Konvertierung"
label="STL Conversion"
icon={<RefreshCw size={14} />}
color="blue"
description="STEP → STL (cadquery)"
@@ -144,7 +144,7 @@ function OutputNode({ data, selected }: { data: { label?: string }; selected?: b
label={data.label ?? 'Output'}
icon={<Download size={14} />}
color="gray"
description="Ergebnis-Datei"
description="Output file"
selected={selected}
hasSource={false}
/>
@@ -168,7 +168,7 @@ function workflowToGraph(config: WorkflowConfig): { nodes: Node[]; edges: Edge[]
if (config.type === 'still') {
const nodes: Node[] = [
{ id: 'input', type: 'inputNode', position: { x: 0, y: Y }, data: { label: 'STEP Input' } },
{ id: 'convert', type: 'convertNode', position: { x: 220, y: Y }, data: { label: 'STL Konvertierung' } },
{ id: 'convert', type: 'convertNode', position: { x: 220, y: Y }, data: { label: 'STL Conversion' } },
{ id: 'render', type: 'renderNode', position: { x: 440, y: Y }, data: { label: 'Still Render', params: config.params } },
{ id: 'output', type: 'outputNode', position: { x: 660, y: Y }, data: { label: 'PNG Output' } },
]
@@ -238,7 +238,7 @@ function ConfigSidepanel({
}) {
return (
<div className="w-72 border-l border-border-default bg-surface p-4 space-y-5 overflow-y-auto">
<h3 className="font-semibold text-content">Node-Konfiguration</h3>
<h3 className="font-semibold text-content">Node Configuration</h3>
{/* Render Engine */}
<div>
@@ -282,7 +282,7 @@ function ConfigSidepanel({
{/* Resolution */}
<div>
<label className="text-sm text-content-secondary mb-2 block">Auflösung</label>
<label className="text-sm text-content-secondary mb-2 block">Resolution</label>
<div className="flex gap-2">
{([[1024, 1024], [2048, 2048], [4096, 4096]] as [number, number][]).map(([w]) => (
<button
@@ -325,7 +325,7 @@ function ConfigSidepanel({
{/* Duration */}
<div>
<label className="text-sm text-content-secondary mb-2 block">
Dauer (s): <span className="font-semibold text-content">{params.duration_s ?? 5}</span>
Duration (s): <span className="font-semibold text-content">{params.duration_s ?? 5}</span>
</label>
<input
type="range"
@@ -367,7 +367,7 @@ function NewWorkflowModal({ onClose, onCreate, isLoading }: NewWorkflowModalProp
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50">
<div className="bg-surface rounded-xl shadow-xl w-full max-w-md p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-content">Neuer Workflow</h2>
<h2 className="text-lg font-semibold text-content">New Workflow</h2>
<button onClick={onClose} className="text-content-muted hover:text-content">
<X size={20} />
</button>
@@ -378,7 +378,7 @@ function NewWorkflowModal({ onClose, onCreate, isLoading }: NewWorkflowModalProp
<label className="block text-sm text-content-secondary mb-1">Name</label>
<input
className="w-full border border-border-default rounded-lg px-3 py-2 text-sm bg-surface text-content focus:outline-none focus:ring-2 focus:ring-accent"
placeholder="z.B. Still Render Standard"
placeholder="e.g. Still Render Standard"
value={name}
onChange={e => setName(e.target.value)}
autoFocus
@@ -386,7 +386,7 @@ function NewWorkflowModal({ onClose, onCreate, isLoading }: NewWorkflowModalProp
</div>
<div>
<label className="block text-sm text-content-secondary mb-1">Typ</label>
<label className="block text-sm text-content-secondary mb-1">Type</label>
<div className="grid grid-cols-2 gap-2">
{([
{ value: 'still', label: 'Still', desc: 'Single PNG image' },
@@ -417,14 +417,14 @@ function NewWorkflowModal({ onClose, onCreate, isLoading }: NewWorkflowModalProp
onClick={onClose}
className="px-4 py-2 text-sm rounded-lg border border-border-default text-content-secondary hover:bg-surface-hover"
>
Abbrechen
Cancel
</button>
<button
disabled={!name.trim() || isLoading}
onClick={() => onCreate(name.trim(), type)}
className="px-4 py-2 text-sm rounded-lg bg-accent text-white hover:bg-accent-hover disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? 'Erstelle…' : 'Erstellen'}
{isLoading ? 'Creating…' : 'Create'}
</button>
</div>
</div>
@@ -529,7 +529,7 @@ function FlowCanvas({ workflow, onSave, isSaving }: FlowCanvasProps) {
<div className="flex flex-col flex-1 min-h-0">
{/* Canvas Toolbar */}
<div className="flex items-center gap-2 px-4 py-2 border-b border-border-default bg-surface">
<span className="text-sm font-medium text-content-secondary mr-2">Nodes:</span>
<span className="text-sm font-medium text-content-secondary mr-2">Nodes</span>
{NODE_PALETTE.map(item => (
<div
key={item.type}
@@ -551,7 +551,7 @@ function FlowCanvas({ workflow, onSave, isSaving }: FlowCanvasProps) {
className="flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-lg bg-accent text-white hover:bg-accent-hover disabled:opacity-50"
>
<Save size={14} />
{isSaving ? 'Speichere…' : 'Speichern'}
{isSaving ? 'Saving…' : 'Save'}
</button>
</div>
</div>
@@ -605,9 +605,9 @@ export default function WorkflowEditor() {
queryClient.invalidateQueries({ queryKey: ['workflows'] })
setSelectedId(wf.id)
setShowNewModal(false)
toast.success('Workflow erstellt')
toast.success('Workflow created')
},
onError: () => toast.error('Fehler beim Erstellen'),
onError: () => toast.error('Failed to create workflow'),
})
const updateMutation = useMutation({
@@ -615,9 +615,9 @@ export default function WorkflowEditor() {
updateWorkflow(id, { config }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['workflows'] })
toast.success('Workflow gespeichert')
toast.success('Workflow saved')
},
onError: () => toast.error('Fehler beim Speichern'),
onError: () => toast.error('Failed to save workflow'),
})
const deleteMutation = useMutation({
@@ -625,9 +625,9 @@ export default function WorkflowEditor() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['workflows'] })
setSelectedId(null)
toast.success('Workflow gelöscht')
toast.success('Workflow deleted')
},
onError: () => toast.error('Fehler beim Löschen'),
onError: () => toast.error('Failed to delete workflow'),
})
const handleCreate = (name: string, type: WorkflowConfig['type']) => {
@@ -675,7 +675,7 @@ export default function WorkflowEditor() {
<button
onClick={() => setShowNewModal(true)}
className="p-1 rounded hover:bg-surface-hover text-content-muted hover:text-content"
title="Neuer Workflow"
title="New Workflow"
>
<Plus size={16} />
</button>
@@ -683,17 +683,17 @@ export default function WorkflowEditor() {
<div className="flex-1 overflow-y-auto p-2 space-y-1">
{isLoading && (
<p className="text-xs text-content-muted px-2 py-4 text-center">Lade</p>
<p className="text-xs text-content-muted px-2 py-4 text-center">Loading</p>
)}
{!isLoading && workflows.length === 0 && (
<p className="text-xs text-content-muted px-2 py-4 text-center">
Noch keine Workflows.
No workflows yet.
<br />
<button
onClick={() => setShowNewModal(true)}
className="mt-1 text-accent hover:underline"
>
+ Neu erstellen
+ Create new
</button>
</p>
)}
@@ -712,12 +712,12 @@ export default function WorkflowEditor() {
<button
onClick={e => {
e.stopPropagation()
if (confirm(`Workflow "${wf.name}" löschen?`)) {
if (confirm(`Delete workflow "${wf.name}"?`)) {
deleteMutation.mutate(wf.id)
}
}}
className="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-red-100 hover:text-red-600 text-content-muted flex-shrink-0"
title="Löschen"
title="Delete"
>
<Trash2 size={12} />
</button>
@@ -730,7 +730,7 @@ export default function WorkflowEditor() {
{typeLabel[wf.config.type]}
</span>
{!wf.is_active && (
<span className="ml-1 text-xs text-content-muted">(inaktiv)</span>
<span className="ml-1 text-xs text-content-muted">(inactive)</span>
)}
</button>
))}
@@ -742,7 +742,7 @@ export default function WorkflowEditor() {
{/* Header */}
<div className="px-6 py-4 border-b border-border-default bg-surface flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold text-content">Workflow-Editor</h1>
<h1 className="text-xl font-semibold text-content">Workflow Editor</h1>
{selectedWorkflow && (
<p className="text-sm text-content-muted mt-0.5">{selectedWorkflow.name}</p>
)}
@@ -752,7 +752,7 @@ export default function WorkflowEditor() {
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-accent text-white text-sm font-medium hover:bg-accent-hover transition-colors"
>
<Plus size={16} />
Neuer Workflow
New Workflow
</button>
</div>
@@ -768,16 +768,16 @@ export default function WorkflowEditor() {
<div className="flex-1 flex items-center justify-center text-center">
<div>
<GitBranch size={48} className="mx-auto text-content-muted mb-4" />
<p className="text-content-secondary font-medium">Kein Workflow ausgewählt</p>
<p className="text-content-secondary font-medium">No workflow selected</p>
<p className="text-sm text-content-muted mt-1">
Wähle einen Workflow aus der Liste oder erstelle einen neuen.
Select a workflow from the list or create a new one.
</p>
<button
onClick={() => setShowNewModal(true)}
className="mt-4 flex items-center gap-2 px-4 py-2 mx-auto rounded-lg bg-accent text-white text-sm font-medium hover:bg-accent-hover"
>
<Plus size={16} />
Workflow erstellen
Create workflow
</button>
</div>
</div>