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 }[] = [ const ALL_FIELD_DEFS: { key: string; defaultLabel: string }[] = [
{ key: 'ebene1', defaultLabel: 'Ebene 1' }, { key: 'ebene1', defaultLabel: 'Level 1' },
{ key: 'ebene2', defaultLabel: 'Ebene 2' }, { key: 'ebene2', defaultLabel: 'Level 2' },
{ key: 'baureihe', defaultLabel: 'Baureihe' }, { key: 'baureihe', defaultLabel: 'Series' },
{ key: 'pim_id', defaultLabel: 'PIM-ID' }, { key: 'pim_id', defaultLabel: 'PIM-ID' },
{ key: 'produkt_baureihe', defaultLabel: 'Produkt / Baureihe' }, { key: 'produkt_baureihe', defaultLabel: 'Product / Series' },
{ key: 'gewaehltes_produkt', defaultLabel: 'Gewähltes Produkt' }, { key: 'gewaehltes_produkt', defaultLabel: 'Selected Product' },
{ key: 'name_cad_modell', defaultLabel: 'Name CAD-Modell' }, { key: 'name_cad_modell', defaultLabel: 'CAD Model Name' },
{ key: 'gewuenschte_bildnummer',defaultLabel: 'Gewünschte Bildnummer' }, { key: 'gewuenschte_bildnummer',defaultLabel: 'Desired Image No.' },
{ key: 'lagertyp', defaultLabel: 'Lagertyp' }, { key: 'lagertyp', defaultLabel: 'Bearing Type' },
{ key: 'medias_rendering', defaultLabel: 'Medias Rendering' }, { key: 'medias_rendering', defaultLabel: 'Medias Rendering' },
] ]
@@ -8,15 +8,15 @@ interface Props {
} }
const STANDARD_FIELDS: { key: keyof ParsedRow; label: string; width: number; mono?: boolean }[] = [ const STANDARD_FIELDS: { key: keyof ParsedRow; label: string; width: number; mono?: boolean }[] = [
{ key: 'ebene1', label: 'Ebene 1', width: 140 }, { key: 'ebene1', label: 'Level 1', width: 140 },
{ key: 'ebene2', label: 'Ebene 2', width: 120 }, { key: 'ebene2', label: 'Level 2', width: 120 },
{ key: 'baureihe', label: 'Baureihe', width: 160 }, { key: 'baureihe', label: 'Series', width: 160 },
{ key: 'pim_id', label: 'PIM-ID', width: 110 }, { key: 'pim_id', label: 'PIM-ID', width: 110 },
{ key: 'produkt_baureihe', label: 'Produkt-Baureihe', width: 150 }, { key: 'produkt_baureihe', label: 'Product Series', width: 150 },
{ key: 'gewaehltes_produkt', label: 'Gewähltes Produkt', width: 150 }, { key: 'gewaehltes_produkt', label: 'Selected Product', width: 150 },
{ key: 'name_cad_modell', label: 'CAD-Modell', width: 190, mono: true }, { key: 'name_cad_modell', label: 'CAD-Modell', width: 190, mono: true },
{ key: 'gewuenschte_bildnummer', label: 'Bildnummer', width: 170, mono: true }, { key: 'gewuenschte_bildnummer', label: 'Image No.', width: 170, mono: true },
{ key: 'lagertyp', label: 'Lagertyp', width: 100 }, { key: 'lagertyp', label: 'Bearing Type', width: 100 },
] ]
export default function ExcelSpreadsheet({ parsed, rows, onChange }: Props) { export default function ExcelSpreadsheet({ parsed, rows, onChange }: Props) {
+5 -5
View File
@@ -950,18 +950,18 @@ export default function AdminPage() {
<div> <div>
<h2 className="font-semibold text-content">Dashboard Widget-Konfiguration</h2> <h2 className="font-semibold text-content">Dashboard Widget-Konfiguration</h2>
<p className="text-xs text-content-muted mt-0.5"> <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> </p>
</div> </div>
</div> </div>
<div className="p-4 flex items-center gap-4"> <div className="p-4 flex items-center gap-4">
<div className="flex-1"> <div className="flex-1">
<p className="text-sm text-content-secondary"> <p className="text-sm text-content-secondary">
Tenant-Standard:{' '} Tenant default:{' '}
<span className="font-medium text-content"> <span className="font-medium text-content">
{tenantDefaultWidgets && tenantDefaultWidgets.length > 0 {tenantDefaultWidgets && tenantDefaultWidgets.length > 0
? `${tenantDefaultWidgets.length} Widget${tenantDefaultWidgets.length !== 1 ? 's' : ''} konfiguriert` ? `${tenantDefaultWidgets.length} Widget${tenantDefaultWidgets.length !== 1 ? 's' : ''} configured`
: 'Noch kein Standard festgelegt (Systemvorgabe aktiv)'} : 'No default set yet (system default active)'}
</span> </span>
</p> </p>
</div> </div>
@@ -970,7 +970,7 @@ export default function AdminPage() {
className="btn-secondary text-sm flex items-center gap-2" className="btn-secondary text-sm flex items-center gap-2"
> >
<LayoutDashboard size={14} /> <LayoutDashboard size={14} />
Tenant-Standard-Dashboard bearbeiten Edit Tenant Default Dashboard
</button> </button>
</div> </div>
</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" /> <Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-content-muted pointer-events-none" />
<input <input
type="text" type="text"
placeholder="Search name, Baureihe, Ebene…" placeholder="Search name, series, level…"
value={filters.search} value={filters.search}
onChange={(e) => updateFilter('search', e.target.value)} 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" 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 Img
</th> </th>
<SortableTh label="CAD Model" sortKey="name_cad_modell" current={sortKey} dir={sortDir} onSort={onSort} className="min-w-[160px]" /> <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]" /> <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">Ebene 1</th> <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">Ebene 2</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">Lagertyp</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">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">Parts</th>
<th className="py-2.5 px-3 text-center text-xs font-semibold text-content-secondary uppercase tracking-wide">STEP</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> <p className="truncate" title={item.name_cad_modell ?? undefined}>{item.name_cad_modell || '—'}</p>
</td> </td>
{/* Baureihe */} {/* Series */}
<td className="py-2.5 px-3 text-content-secondary text-xs max-w-[130px]"> <td className="py-2.5 px-3 text-content-secondary text-xs max-w-[130px]">
<p className="truncate">{item.baureihe || '—'}</p> <p className="truncate">{item.baureihe || '—'}</p>
</td> </td>
{/* Ebene 1 */} {/* Level 1 */}
<td className="py-2.5 px-3 text-content-secondary text-xs max-w-[120px]"> <td className="py-2.5 px-3 text-content-secondary text-xs max-w-[120px]">
<p className="truncate">{item.ebene1 || '—'}</p> <p className="truncate">{item.ebene1 || '—'}</p>
</td> </td>
{/* Ebene 2 */} {/* Level 2 */}
<td className="py-2.5 px-3 text-content-secondary text-xs max-w-[120px]"> <td className="py-2.5 px-3 text-content-secondary text-xs max-w-[120px]">
<p className="truncate">{item.ebene2 || '—'}</p> <p className="truncate">{item.ebene2 || '—'}</p>
</td> </td>
{/* Lagertyp */} {/* Bearing Type */}
<td className="py-2.5 px-3 text-content-secondary text-xs max-w-[110px]"> <td className="py-2.5 px-3 text-content-secondary text-xs max-w-[110px]">
<p className="truncate">{item.lagertyp || '—'}</p> <p className="truncate">{item.lagertyp || '—'}</p>
</td> </td>
@@ -1175,12 +1175,12 @@ function ItemTableRow({
</div> </div>
<div className="flex-1 grid grid-cols-2 md:grid-cols-3 gap-4 text-sm"> <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="PIM-ID" value={item.pim_id} />
<Field label="Produkt / Baureihe" value={item.produkt_baureihe} /> <Field label="Product / Series" value={item.produkt_baureihe} />
<Field label="Gewähltes Produkt" value={item.gewaehltes_produkt} /> <Field label="Selected Product" value={item.gewaehltes_produkt} />
<Field label="Bildnummer" value={item.gewuenschte_bildnummer} /> <Field label="Image No." value={item.gewuenschte_bildnummer} />
<Field label="Lagertyp" value={item.lagertyp} /> <Field label="Bearing Type" value={item.lagertyp} />
<Field label="Medias Rendering" value={item.medias_rendering?.toString() ?? '—'} /> <Field label="Medias Rendering" value={item.medias_rendering?.toString() ?? '—'} />
</div> </div>
</div> </div>
@@ -1350,15 +1350,15 @@ function GalleryCard({ item, isDraft, onClick }: { item: any; isDraft: boolean;
// ── Source Spreadsheet ──────────────────────────────────────────────────────── // ── Source Spreadsheet ────────────────────────────────────────────────────────
const STD_COLS: { key: keyof OrderItem; label: string; editable?: boolean }[] = [ const STD_COLS: { key: keyof OrderItem; label: string; editable?: boolean }[] = [
{ key: 'ebene1', label: 'Ebene 1', editable: true }, { key: 'ebene1', label: 'Level 1', editable: true },
{ key: 'ebene2', label: 'Ebene 2', editable: true }, { key: 'ebene2', label: 'Level 2', editable: true },
{ key: 'baureihe', label: 'Baureihe', editable: true }, { key: 'baureihe', label: 'Series', editable: true },
{ key: 'pim_id', label: 'PIM-ID', editable: true }, { key: 'pim_id', label: 'PIM-ID', editable: true },
{ key: 'produkt_baureihe', label: 'Produkt/Baureihe',editable: true }, { key: 'produkt_baureihe', label: 'Product/Series', editable: true },
{ key: 'gewaehltes_produkt', label: 'Gew. Produkt', editable: true }, { key: 'gewaehltes_produkt', label: 'Sel. Product', editable: true },
{ key: 'name_cad_modell', label: 'CAD-Modell', editable: true }, { key: 'name_cad_modell', label: 'CAD-Modell', editable: true },
{ key: 'gewuenschte_bildnummer', label: 'Bildnummer', editable: true }, { key: 'gewuenschte_bildnummer', label: 'Image No.', editable: true },
{ key: 'lagertyp', label: 'Lagertyp', editable: true }, { key: 'lagertyp', label: 'Bearing Type', editable: true },
] ]
function SourceSpreadsheet({ function SourceSpreadsheet({
@@ -1606,9 +1606,9 @@ function GalleryModal({
{/* Fields */} {/* Fields */}
<div className="px-5 py-3 grid grid-cols-2 gap-x-6 gap-y-1 text-sm border-t border-border-light"> <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="Series" value={item.baureihe} />
<Field label="Ebene 2" value={item.ebene2} /> <Field label="Level 2" value={item.ebene2} />
<Field label="Lagertyp" value={item.lagertyp} /> <Field label="Bearing Type" value={item.lagertyp} />
<Field label="Rendering" value={item.medias_rendering?.toString() ?? '—'} /> <Field label="Rendering" value={item.medias_rendering?.toString() ?? '—'} />
<Field label="Components" value={String(item.components?.length ?? 0)} /> <Field label="Components" value={String(item.components?.length ?? 0)} />
<Field label="PIM-ID" value={item.pim_id} /> <Field label="PIM-ID" value={item.pim_id} />
+37 -37
View File
@@ -66,34 +66,34 @@ export default function TenantsPage() {
const createMut = useMutation({ const createMut = useMutation({
mutationFn: (data: TenantCreate) => createTenant(data), mutationFn: (data: TenantCreate) => createTenant(data),
onSuccess: () => { onSuccess: () => {
toast.success('Tenant erstellt') toast.success('Tenant created')
qc.invalidateQueries({ queryKey: ['tenants'] }) qc.invalidateQueries({ queryKey: ['tenants'] })
setShowCreate(false) setShowCreate(false)
setCreateForm({ name: '', slug: '', is_active: true }) setCreateForm({ name: '', slug: '', is_active: true })
setSlugEdited(false) 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({ const updateMut = useMutation({
mutationFn: ({ id, data }: { id: string; data: TenantUpdate }) => updateTenant(id, data), mutationFn: ({ id, data }: { id: string; data: TenantUpdate }) => updateTenant(id, data),
onSuccess: () => { onSuccess: () => {
toast.success('Tenant aktualisiert') toast.success('Tenant updated')
qc.invalidateQueries({ queryKey: ['tenants'] }) qc.invalidateQueries({ queryKey: ['tenants'] })
setEditingTenant(null) 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({ const deleteMut = useMutation({
mutationFn: (id: string) => deleteTenant(id), mutationFn: (id: string) => deleteTenant(id),
onSuccess: (_data, id) => { onSuccess: (_data, id) => {
toast.success('Tenant gelöscht') toast.success('Tenant deleted')
qc.invalidateQueries({ queryKey: ['tenants'] }) qc.invalidateQueries({ queryKey: ['tenants'] })
if (activeTenantId === id) setActiveTenantId(null) if (activeTenantId === id) setActiveTenantId(null)
setDeletingId(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 // Auto-generate slug from name unless manually edited
@@ -123,7 +123,7 @@ export default function TenantsPage() {
<Building2 size={24} className="text-accent" /> <Building2 size={24} className="text-accent" />
<div> <div>
<h1 className="text-xl font-bold text-content">Tenants</h1> <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>
</div> </div>
<button <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" 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} /> <Plus size={16} />
Neuer Tenant New Tenant
</button> </button>
</div> </div>
{/* Tenant Selector */} {/* Tenant Selector */}
<div className="mb-6 p-4 bg-surface border border-border-default rounded-lg"> <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"> <p className="text-xs font-semibold text-content-muted uppercase tracking-wider mb-2">
Admin Cross-Tenant-Ansicht Admin Cross-Tenant View
</p> </p>
<div className="relative inline-block"> <div className="relative inline-block">
<button <button
@@ -147,7 +147,7 @@ export default function TenantsPage() {
> >
<Building2 size={14} className="text-content-muted shrink-0" /> <Building2 size={14} className="text-content-muted shrink-0" />
<span className="flex-1 text-left truncate"> <span className="flex-1 text-left truncate">
{activeTenant ? activeTenant.name : 'Alle Tenants / Admin-Ansicht'} {activeTenant ? activeTenant.name : 'All Tenants / Admin View'}
</span> </span>
<ChevronDown size={14} className="text-content-muted shrink-0" /> <ChevronDown size={14} className="text-content-muted shrink-0" />
</button> </button>
@@ -159,7 +159,7 @@ export default function TenantsPage() {
> >
{activeTenantId === null && <Check size={14} className="text-accent shrink-0" />} {activeTenantId === null && <Check size={14} className="text-accent shrink-0" />}
{activeTenantId !== null && <span className="w-[14px] shrink-0" />} {activeTenantId !== null && <span className="w-[14px] shrink-0" />}
<span>Alle Tenants / Admin-Ansicht</span> <span>All Tenants / Admin View</span>
</button> </button>
{tenants.map((t) => ( {tenants.map((t) => (
<button <button
@@ -171,7 +171,7 @@ export default function TenantsPage() {
{activeTenantId !== t.id && <span className="w-[14px] shrink-0" />} {activeTenantId !== t.id && <span className="w-[14px] shrink-0" />}
<span className="flex-1 truncate">{t.name}</span> <span className="flex-1 truncate">{t.name}</span>
{!t.is_active && ( {!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> </button>
))} ))}
@@ -180,7 +180,7 @@ export default function TenantsPage() {
</div> </div>
{activeTenant && ( {activeTenant && (
<p className="mt-2 text-xs text-content-muted"> <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> </p>
)} )}
</div> </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">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">Status</th>
<th className="px-4 py-3 text-left font-semibold text-content-secondary"> <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>
<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" /> <th className="px-4 py-3" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{isLoading && ( {isLoading && (
<tr> <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> </tr>
)} )}
{!isLoading && tenants.length === 0 && ( {!isLoading && tenants.length === 0 && (
<tr> <tr>
<td colSpan={6} className="px-4 py-8 text-center text-content-muted"> <td colSpan={6} className="px-4 py-8 text-center text-content-muted">
Noch keine Tenants vorhanden. No tenants yet.
</td> </td>
</tr> </tr>
)} )}
@@ -218,7 +218,7 @@ export default function TenantsPage() {
<td className="px-4 py-3 font-medium text-content"> <td className="px-4 py-3 font-medium text-content">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{activeTenantId === tenant.id && ( {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} {tenant.name}
</div> </div>
@@ -227,11 +227,11 @@ export default function TenantsPage() {
<td className="px-4 py-3"> <td className="px-4 py-3">
{tenant.is_active ? ( {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"> <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>
) : ( ) : (
<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"> <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> </span>
)} )}
</td> </td>
@@ -244,14 +244,14 @@ export default function TenantsPage() {
<button <button
onClick={() => openEdit(tenant)} onClick={() => openEdit(tenant)}
className="p-1.5 rounded hover:bg-surface-alt text-content-muted hover:text-content transition-colors" className="p-1.5 rounded hover:bg-surface-alt text-content-muted hover:text-content transition-colors"
title="Bearbeiten" title="Edit"
> >
<Pencil size={15} /> <Pencil size={15} />
</button> </button>
<button <button
onClick={() => setDeletingId(tenant.id)} onClick={() => setDeletingId(tenant.id)}
className="p-1.5 rounded hover:bg-status-error-bg text-content-muted hover:text-status-error-text transition-colors" 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} /> <Trash2 size={15} />
</button> </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="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="bg-surface rounded-xl shadow-xl w-full max-w-md mx-4 p-6">
<div className="flex items-center justify-between mb-5"> <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 <button
onClick={() => setShowCreate(false)} onClick={() => setShowCreate(false)}
className="p-1.5 rounded hover:bg-surface-alt text-content-muted transition-colors" className="p-1.5 rounded hover:bg-surface-alt text-content-muted transition-colors"
@@ -284,7 +284,7 @@ export default function TenantsPage() {
type="text" type="text"
value={createForm.name} value={createForm.name}
onChange={(e) => handleCreateNameChange(e.target.value)} 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" 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> </div>
@@ -292,13 +292,13 @@ export default function TenantsPage() {
<div> <div>
<label className="block text-sm font-medium text-content-secondary mb-1"> <label className="block text-sm font-medium text-content-secondary mb-1">
Slug * 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> </label>
<input <input
type="text" type="text"
value={createForm.slug} value={createForm.slug}
onChange={(e) => handleCreateSlugChange(e.target.value)} 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" 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> </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" /> <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> </label>
<span className="text-sm text-content-secondary">Aktiv</span> <span className="text-sm text-content-secondary">Active</span>
</div> </div>
</div> </div>
@@ -322,14 +322,14 @@ export default function TenantsPage() {
onClick={() => setShowCreate(false)} 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" 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>
<button <button
onClick={() => createMut.mutate(createForm)} onClick={() => createMut.mutate(createForm)}
disabled={!createForm.name.trim() || !createForm.slug.trim() || createMut.isPending} 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" 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> </button>
</div> </div>
</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="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="bg-surface rounded-xl shadow-xl w-full max-w-md mx-4 p-6">
<div className="flex items-center justify-between mb-5"> <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 <button
onClick={() => setEditingTenant(null)} onClick={() => setEditingTenant(null)}
className="p-1.5 rounded hover:bg-surface-alt text-content-muted transition-colors" 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" /> <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> </label>
<span className="text-sm text-content-secondary">Aktiv</span> <span className="text-sm text-content-secondary">Active</span>
</div> </div>
</div> </div>
@@ -390,14 +390,14 @@ export default function TenantsPage() {
onClick={() => setEditingTenant(null)} 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" 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>
<button <button
onClick={() => updateMut.mutate({ id: editingTenant.id, data: editForm })} onClick={() => updateMut.mutate({ id: editingTenant.id, data: editForm })}
disabled={updateMut.isPending} 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" 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> </button>
</div> </div>
</div> </div>
@@ -413,9 +413,9 @@ export default function TenantsPage() {
<Trash2 size={16} className="text-status-error-text" /> <Trash2 size={16} className="text-status-error-text" />
</div> </div>
<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"> <p className="text-sm text-content-muted mt-1">
Diese Aktion kann nicht rückgängig gemacht werden. This action cannot be undone.
</p> </p>
</div> </div>
</div> </div>
@@ -424,14 +424,14 @@ export default function TenantsPage() {
onClick={() => setDeletingId(null)} 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" 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>
<button <button
onClick={() => deleteMut.mutate(deletingId)} onClick={() => deleteMut.mutate(deletingId)}
disabled={deleteMut.isPending} 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" 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> </button>
</div> </div>
</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"> <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. 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> </p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"> <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" />} icon={<Ban size={18} className="text-amber-600" />}
value={previewResult.no_pim_id_count} value={previewResult.no_pim_id_count}
label="rows skipped" 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" color="bg-status-warning-bg border-border-default text-status-warning-text"
/> />
<StatCard <StatCard
icon={<Copy size={18} className="text-orange-600" />} icon={<Copy size={18} className="text-orange-600" />}
value={previewResult.duplicate_count} value={previewResult.duplicate_count}
label="duplicate Baureihe" label="duplicate Series"
description="Same Produkt-Baureihe appears multiple times. Pre-unchecked — only first occurrence imported." 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" color="bg-status-warning-bg border-border-default text-status-warning-text"
/> />
</div> </div>
@@ -341,10 +341,10 @@ export default function UploadPage() {
<span className="text-status-warning-text font-bold text-sm shrink-0"></span> <span className="text-status-warning-text font-bold text-sm shrink-0"></span>
<div> <div>
<p className="text-sm font-semibold text-status-warning-text"> <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>
<p className="text-xs text-status-warning-text mt-0.5"> <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. Duplicate rows are pre-unchecked (shown in amber). You can manually re-check them to overwrite the first.
</p> </p>
</div> </div>
@@ -378,7 +378,7 @@ export default function UploadPage() {
/> />
</th> </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">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" <th className="px-4 py-2 font-medium text-content-secondary"
title="Gew\u00e4hltes Produkt \u2014 the specific material/coating variant from the Excel" title="Gew\u00e4hltes Produkt \u2014 the specific material/coating variant from the Excel"
>Gew. Produkt</th> >Gew. Produkt</th>
@@ -424,14 +424,14 @@ export default function UploadPage() {
{!hasId ? ( {!hasId ? (
<span <span
className="text-xs px-2 py-0.5 rounded-full bg-surface-muted text-content-muted" 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 skipped
</span> </span>
) : row.is_duplicate ? ( ) : row.is_duplicate ? (
<span <span
className="text-xs px-2 py-0.5 rounded-full bg-status-warning-bg text-status-warning-text font-medium" 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} Duplicate of row {row.duplicate_of_row}
</span> </span>
+32 -32
View File
@@ -81,7 +81,7 @@ function InputNode({ selected }: { selected?: boolean }) {
label="STEP Input" label="STEP Input"
icon={<FileUp size={14} />} icon={<FileUp size={14} />}
color="green" color="green"
description="STEP-Datei Eingang" description="STEP file input"
selected={selected} selected={selected}
hasTarget={false} hasTarget={false}
/> />
@@ -91,7 +91,7 @@ function InputNode({ selected }: { selected?: boolean }) {
function ConvertNode({ selected }: { selected?: boolean }) { function ConvertNode({ selected }: { selected?: boolean }) {
return ( return (
<BaseNode <BaseNode
label="STL Konvertierung" label="STL Conversion"
icon={<RefreshCw size={14} />} icon={<RefreshCw size={14} />}
color="blue" color="blue"
description="STEP → STL (cadquery)" description="STEP → STL (cadquery)"
@@ -144,7 +144,7 @@ function OutputNode({ data, selected }: { data: { label?: string }; selected?: b
label={data.label ?? 'Output'} label={data.label ?? 'Output'}
icon={<Download size={14} />} icon={<Download size={14} />}
color="gray" color="gray"
description="Ergebnis-Datei" description="Output file"
selected={selected} selected={selected}
hasSource={false} hasSource={false}
/> />
@@ -168,7 +168,7 @@ function workflowToGraph(config: WorkflowConfig): { nodes: Node[]; edges: Edge[]
if (config.type === 'still') { if (config.type === 'still') {
const nodes: Node[] = [ const nodes: Node[] = [
{ id: 'input', type: 'inputNode', position: { x: 0, y: Y }, data: { label: 'STEP Input' } }, { 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: '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' } }, { id: 'output', type: 'outputNode', position: { x: 660, y: Y }, data: { label: 'PNG Output' } },
] ]
@@ -238,7 +238,7 @@ function ConfigSidepanel({
}) { }) {
return ( return (
<div className="w-72 border-l border-border-default bg-surface p-4 space-y-5 overflow-y-auto"> <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 */} {/* Render Engine */}
<div> <div>
@@ -282,7 +282,7 @@ function ConfigSidepanel({
{/* Resolution */} {/* Resolution */}
<div> <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"> <div className="flex gap-2">
{([[1024, 1024], [2048, 2048], [4096, 4096]] as [number, number][]).map(([w]) => ( {([[1024, 1024], [2048, 2048], [4096, 4096]] as [number, number][]).map(([w]) => (
<button <button
@@ -325,7 +325,7 @@ function ConfigSidepanel({
{/* Duration */} {/* Duration */}
<div> <div>
<label className="text-sm text-content-secondary mb-2 block"> <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> </label>
<input <input
type="range" 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="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="bg-surface rounded-xl shadow-xl w-full max-w-md p-6">
<div className="flex items-center justify-between mb-4"> <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"> <button onClick={onClose} className="text-content-muted hover:text-content">
<X size={20} /> <X size={20} />
</button> </button>
@@ -378,7 +378,7 @@ function NewWorkflowModal({ onClose, onCreate, isLoading }: NewWorkflowModalProp
<label className="block text-sm text-content-secondary mb-1">Name</label> <label className="block text-sm text-content-secondary mb-1">Name</label>
<input <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" 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} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
autoFocus autoFocus
@@ -386,7 +386,7 @@ function NewWorkflowModal({ onClose, onCreate, isLoading }: NewWorkflowModalProp
</div> </div>
<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"> <div className="grid grid-cols-2 gap-2">
{([ {([
{ value: 'still', label: 'Still', desc: 'Single PNG image' }, { value: 'still', label: 'Still', desc: 'Single PNG image' },
@@ -417,14 +417,14 @@ function NewWorkflowModal({ onClose, onCreate, isLoading }: NewWorkflowModalProp
onClick={onClose} onClick={onClose}
className="px-4 py-2 text-sm rounded-lg border border-border-default text-content-secondary hover:bg-surface-hover" className="px-4 py-2 text-sm rounded-lg border border-border-default text-content-secondary hover:bg-surface-hover"
> >
Abbrechen Cancel
</button> </button>
<button <button
disabled={!name.trim() || isLoading} disabled={!name.trim() || isLoading}
onClick={() => onCreate(name.trim(), type)} 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" 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> </button>
</div> </div>
</div> </div>
@@ -529,7 +529,7 @@ function FlowCanvas({ workflow, onSave, isSaving }: FlowCanvasProps) {
<div className="flex flex-col flex-1 min-h-0"> <div className="flex flex-col flex-1 min-h-0">
{/* Canvas Toolbar */} {/* Canvas Toolbar */}
<div className="flex items-center gap-2 px-4 py-2 border-b border-border-default bg-surface"> <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 => ( {NODE_PALETTE.map(item => (
<div <div
key={item.type} 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" 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} /> <Save size={14} />
{isSaving ? 'Speichere…' : 'Speichern'} {isSaving ? 'Saving…' : 'Save'}
</button> </button>
</div> </div>
</div> </div>
@@ -605,9 +605,9 @@ export default function WorkflowEditor() {
queryClient.invalidateQueries({ queryKey: ['workflows'] }) queryClient.invalidateQueries({ queryKey: ['workflows'] })
setSelectedId(wf.id) setSelectedId(wf.id)
setShowNewModal(false) 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({ const updateMutation = useMutation({
@@ -615,9 +615,9 @@ export default function WorkflowEditor() {
updateWorkflow(id, { config }), updateWorkflow(id, { config }),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['workflows'] }) 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({ const deleteMutation = useMutation({
@@ -625,9 +625,9 @@ export default function WorkflowEditor() {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['workflows'] }) queryClient.invalidateQueries({ queryKey: ['workflows'] })
setSelectedId(null) 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']) => { const handleCreate = (name: string, type: WorkflowConfig['type']) => {
@@ -675,7 +675,7 @@ export default function WorkflowEditor() {
<button <button
onClick={() => setShowNewModal(true)} onClick={() => setShowNewModal(true)}
className="p-1 rounded hover:bg-surface-hover text-content-muted hover:text-content" className="p-1 rounded hover:bg-surface-hover text-content-muted hover:text-content"
title="Neuer Workflow" title="New Workflow"
> >
<Plus size={16} /> <Plus size={16} />
</button> </button>
@@ -683,17 +683,17 @@ export default function WorkflowEditor() {
<div className="flex-1 overflow-y-auto p-2 space-y-1"> <div className="flex-1 overflow-y-auto p-2 space-y-1">
{isLoading && ( {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 && ( {!isLoading && workflows.length === 0 && (
<p className="text-xs text-content-muted px-2 py-4 text-center"> <p className="text-xs text-content-muted px-2 py-4 text-center">
Noch keine Workflows. No workflows yet.
<br /> <br />
<button <button
onClick={() => setShowNewModal(true)} onClick={() => setShowNewModal(true)}
className="mt-1 text-accent hover:underline" className="mt-1 text-accent hover:underline"
> >
+ Neu erstellen + Create new
</button> </button>
</p> </p>
)} )}
@@ -712,12 +712,12 @@ export default function WorkflowEditor() {
<button <button
onClick={e => { onClick={e => {
e.stopPropagation() e.stopPropagation()
if (confirm(`Workflow "${wf.name}" löschen?`)) { if (confirm(`Delete workflow "${wf.name}"?`)) {
deleteMutation.mutate(wf.id) 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" 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} /> <Trash2 size={12} />
</button> </button>
@@ -730,7 +730,7 @@ export default function WorkflowEditor() {
{typeLabel[wf.config.type]} {typeLabel[wf.config.type]}
</span> </span>
{!wf.is_active && ( {!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> </button>
))} ))}
@@ -742,7 +742,7 @@ export default function WorkflowEditor() {
{/* Header */} {/* Header */}
<div className="px-6 py-4 border-b border-border-default bg-surface flex items-center justify-between"> <div className="px-6 py-4 border-b border-border-default bg-surface flex items-center justify-between">
<div> <div>
<h1 className="text-xl font-semibold text-content">Workflow-Editor</h1> <h1 className="text-xl font-semibold text-content">Workflow Editor</h1>
{selectedWorkflow && ( {selectedWorkflow && (
<p className="text-sm text-content-muted mt-0.5">{selectedWorkflow.name}</p> <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" 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} /> <Plus size={16} />
Neuer Workflow New Workflow
</button> </button>
</div> </div>
@@ -768,16 +768,16 @@ export default function WorkflowEditor() {
<div className="flex-1 flex items-center justify-center text-center"> <div className="flex-1 flex items-center justify-center text-center">
<div> <div>
<GitBranch size={48} className="mx-auto text-content-muted mb-4" /> <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"> <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> </p>
<button <button
onClick={() => setShowNewModal(true)} 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" 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} /> <Plus size={16} />
Workflow erstellen Create workflow
</button> </button>
</div> </div>
</div> </div>