feat: Produktpflege mit Bildupload + Beschreibung + Aktiv-Status

Backend (routes/artikel.js):
- 3 neue Spalten via Auto-Migration (bootstrap.js):
  - bild_url VARCHAR(500)
  - beschreibung TEXT
  - aktiv TINYINT(1) DEFAULT 1
- POST/PUT/DELETE Routes jetzt unter requireRole(admin)
- GET unterstuetzt ?all=1 fuer inaktive Produkte (Produktpflege-Ansicht)
- POST /artikel/:id/bild und DELETE /artikel/:id/bild fuer Bildverwaltung
- DELETE faengt FK-Constraint ab (Hinweis: stattdessen inaktiv setzen)

Frontend:
- Neue Komponente Produkte.jsx als Kartenraster
  - Bildupload, Beschreibung, Pfandbetrag, Aktiv-Toggle, Edit/Loeschen
  - Inaktive werden ausgegraut dargestellt
- TopNav: neuer Admin-Menupunkt Produkte (FaTags Icon)
- Dashboard.jsx: Route eingebaut
- client.js: api.getArtikel(true) fuer inaktive, uploadArtikelBild, deleteArtikelBild
This commit is contained in:
christian
2026-06-02 09:44:14 +00:00
parent 5f6d536401
commit a7f74eaf1c
6 changed files with 323 additions and 27 deletions

View File

@@ -1,15 +1,21 @@
import express from 'express'; import express from 'express';
import fs from 'fs';
import path from 'path';
import { body, validationResult } from 'express-validator'; import { body, validationResult } from 'express-validator';
import pool from '../config/database.js'; import pool from '../config/database.js';
import { authenticateToken, requireTenant } from '../middleware/auth.js'; import { authenticateToken, requireTenant, requireRole } from '../middleware/auth.js';
import { upload } from '../middleware/upload.js';
const router = express.Router(); const router = express.Router();
router.use(authenticateToken, requireTenant); router.use(authenticateToken, requireTenant);
// GET alle Artikel (auch Inaktive fuer Admin)
router.get('/', async (req, res) => { router.get('/', async (req, res) => {
try { try {
const showInactive = req.query.all === '1';
const where = showInactive ? 'tenant_id = ?' : 'tenant_id = ? AND aktiv = 1';
const [rows] = await pool.query( const [rows] = await pool.query(
'SELECT * FROM artikel WHERE tenant_id = ? ORDER BY bezeichnung ASC', `SELECT * FROM artikel WHERE ${where} ORDER BY bezeichnung ASC`,
[req.user.tenant_id] [req.user.tenant_id]
); );
res.json(rows); res.json(rows);
@@ -27,35 +33,49 @@ router.get('/:id', async (req, res) => {
} catch (e) { res.status(500).json({ error: 'Serverfehler' }); } } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
}); });
router.post('/', [ // POST: Anlegen (Admin only)
router.post('/', requireRole(['admin']), [
body('bezeichnung').notEmpty().trim(), body('bezeichnung').notEmpty().trim(),
body('pfand_betrag').isFloat({ min: 0 }) body('pfand_betrag').isFloat({ min: 0 }),
body('beschreibung').optional({ checkFalsy: true }).trim(),
], async (req, res) => { ], async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
try { try {
await pool.query( await pool.query(
'INSERT INTO artikel (id, tenant_id, bezeichnung, pfand_betrag) VALUES (UUID(), ?, ?, ?)', `INSERT INTO artikel (id, tenant_id, bezeichnung, pfand_betrag, beschreibung, aktiv)
[req.user.tenant_id, req.body.bezeichnung, req.body.pfand_betrag] VALUES (UUID(), ?, ?, ?, ?, 1)`,
[req.user.tenant_id, req.body.bezeichnung, req.body.pfand_betrag,
req.body.beschreibung || null]
); );
const [rows] = await pool.query( const [rows] = await pool.query(
'SELECT * FROM artikel WHERE tenant_id = ? ORDER BY erstellt_am DESC LIMIT 1', 'SELECT * FROM artikel WHERE tenant_id = ? ORDER BY erstellt_am DESC LIMIT 1',
[req.user.tenant_id] [req.user.tenant_id]
); );
res.status(201).json(rows[0]); res.status(201).json(rows[0]);
} catch (e) { res.status(500).json({ error: 'Serverfehler' }); } } catch (e) {
console.error('POST artikel:', e);
res.status(500).json({ error: 'Serverfehler' });
}
}); });
router.put('/:id', [ // PUT: Aendern (Admin only)
router.put('/:id', requireRole(['admin']), [
body('bezeichnung').notEmpty().trim(), body('bezeichnung').notEmpty().trim(),
body('pfand_betrag').isFloat({ min: 0 }) body('pfand_betrag').isFloat({ min: 0 }),
body('beschreibung').optional({ checkFalsy: true }).trim(),
body('aktiv').optional().isBoolean(),
], async (req, res) => { ], async (req, res) => {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() }); if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
try { try {
const aktivVal = req.body.aktiv === undefined ? 1 : (req.body.aktiv ? 1 : 0);
const [r] = await pool.query( const [r] = await pool.query(
'UPDATE artikel SET bezeichnung=?, pfand_betrag=? WHERE id=? AND tenant_id=?', `UPDATE artikel
[req.body.bezeichnung, req.body.pfand_betrag, req.params.id, req.user.tenant_id] SET bezeichnung=?, pfand_betrag=?, beschreibung=?, aktiv=?
WHERE id=? AND tenant_id=?`,
[req.body.bezeichnung, req.body.pfand_betrag, req.body.beschreibung || null,
aktivVal, req.params.id, req.user.tenant_id]
); );
if (r.affectedRows === 0) return res.status(404).json({ error: 'Artikel nicht gefunden' }); if (r.affectedRows === 0) return res.status(404).json({ error: 'Artikel nicht gefunden' });
const [rows] = await pool.query('SELECT * FROM artikel WHERE id=? AND tenant_id=?', const [rows] = await pool.query('SELECT * FROM artikel WHERE id=? AND tenant_id=?',
@@ -64,13 +84,72 @@ router.put('/:id', [
} catch (e) { res.status(500).json({ error: 'Serverfehler' }); } } catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
}); });
router.delete('/:id', async (req, res) => { // POST Produktbild (Admin only)
router.post('/:id/bild', requireRole(['admin']), upload.single('foto'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'Kein Bild hochgeladen' });
try { try {
const [[a]] = await pool.query('SELECT bild_url FROM artikel WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
if (!a) {
try { fs.unlinkSync(req.file.path); } catch {}
return res.status(404).json({ error: 'Artikel nicht gefunden' });
}
// Altes Bild loeschen (best effort)
if (a.bild_url) {
const oldPath = path.join(process.env.UPLOAD_DIR || '/app/uploads',
a.bild_url.replace(/^\/uploads\//, ''));
try { fs.unlinkSync(oldPath); } catch {}
}
const newUrl = `/uploads/${req.file.filename}`;
await pool.query('UPDATE artikel SET bild_url=? WHERE id=? AND tenant_id=?',
[newUrl, req.params.id, req.user.tenant_id]);
const [rows] = await pool.query('SELECT * FROM artikel WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
res.json(rows[0]);
} catch (e) {
console.error('POST artikel/bild:', e);
res.status(500).json({ error: 'Serverfehler' });
}
});
router.delete('/:id/bild', requireRole(['admin']), async (req, res) => {
try {
const [[a]] = await pool.query('SELECT bild_url FROM artikel WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
if (!a) return res.status(404).json({ error: 'Artikel nicht gefunden' });
if (a.bild_url) {
const p = path.join(process.env.UPLOAD_DIR || '/app/uploads',
a.bild_url.replace(/^\/uploads\//, ''));
try { fs.unlinkSync(p); } catch {}
}
await pool.query('UPDATE artikel SET bild_url=NULL WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
res.json({ message: 'Bild entfernt' });
} catch (e) { res.status(500).json({ error: 'Serverfehler' }); }
});
router.delete('/:id', requireRole(['admin']), async (req, res) => {
try {
// Bild-Datei mitloeschen
const [[a]] = await pool.query('SELECT bild_url FROM artikel WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]);
if (a?.bild_url) {
const p = path.join(process.env.UPLOAD_DIR || '/app/uploads',
a.bild_url.replace(/^\/uploads\//, ''));
try { fs.unlinkSync(p); } catch {}
}
const [r] = await pool.query('DELETE FROM artikel WHERE id=? AND tenant_id=?', const [r] = await pool.query('DELETE FROM artikel WHERE id=? AND tenant_id=?',
[req.params.id, req.user.tenant_id]); [req.params.id, req.user.tenant_id]);
if (r.affectedRows === 0) return res.status(404).json({ error: 'Artikel nicht gefunden' }); if (r.affectedRows === 0) return res.status(404).json({ error: 'Artikel nicht gefunden' });
res.json({ message: 'Artikel gelöscht' }); res.json({ message: 'Artikel gelöscht' });
} catch (e) { res.status(500).json({ error: 'Serverfehler' }); } } catch (e) {
if (e.code === 'ER_ROW_IS_REFERENCED' || e.code === 'ER_ROW_IS_REFERENCED_2') {
return res.status(400).json({
error: 'Artikel wird in Lieferungen/Rücknahmen verwendet — bitte stattdessen auf "Inaktiv" setzen.'
});
}
res.status(500).json({ error: 'Serverfehler' });
}
}); });
export default router; export default router;

View File

@@ -1,18 +1,33 @@
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import pool from '../config/database.js'; import pool from '../config/database.js';
const EXPIRY_COLS = ['notified_7d_at', 'notified_1d_at', 'notified_expired_at']; // Spaltenname -> SQL-Definition
const COLS_TO_ADD = {
tenants: {
notified_7d_at: 'TIMESTAMP NULL',
notified_1d_at: 'TIMESTAMP NULL',
notified_expired_at: 'TIMESTAMP NULL',
},
artikel: {
bild_url: 'VARCHAR(500) NULL',
beschreibung: 'TEXT NULL',
aktiv: 'TINYINT(1) NOT NULL DEFAULT 1',
},
};
async function ensureExpiryColumns() { async function ensureColumns() {
const [cols] = await pool.query( for (const [table, cols] of Object.entries(COLS_TO_ADD)) {
const [existing] = await pool.query(
`SELECT COLUMN_NAME FROM information_schema.columns `SELECT COLUMN_NAME FROM information_schema.columns
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tenants'` WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?`,
[table]
); );
const have = new Set(cols.map(c => c.COLUMN_NAME)); const have = new Set(existing.map(c => c.COLUMN_NAME));
for (const col of EXPIRY_COLS) { for (const [col, def] of Object.entries(cols)) {
if (!have.has(col)) { if (!have.has(col)) {
await pool.query(`ALTER TABLE tenants ADD COLUMN ${col} TIMESTAMP NULL`); await pool.query(`ALTER TABLE ${table} ADD COLUMN ${col} ${def}`);
console.log('[bootstrap] tenants ALTER ADD', col); console.log(`[bootstrap] ${table} ALTER ADD ${col}`);
}
} }
} }
} }
@@ -25,7 +40,7 @@ export async function bootstrapSuperAdmin() {
} }
// Migrations // Migrations
try { await ensureExpiryColumns(); } try { await ensureColumns(); }
catch (e) { console.error('[bootstrap] Migration-Fehler:', e.message); } catch (e) { console.error('[bootstrap] Migration-Fehler:', e.message); }
const email = process.env.SUPERADMIN_EMAIL; const email = process.env.SUPERADMIN_EMAIL;

View File

@@ -77,7 +77,21 @@ class ApiClient {
deleteKundenBild(id) { return this.request(`/kunden-bilder/bild/${id}`, { method: 'DELETE' }); } deleteKundenBild(id) { return this.request(`/kunden-bilder/bild/${id}`, { method: 'DELETE' }); }
// Artikel // Artikel
getArtikel() { return this.request('/artikel'); } getArtikel(includeInactive) {
return this.request('/artikel' + (includeInactive ? '?all=1' : ''));
}
async uploadArtikelBild(id, file) {
const fd = new FormData();
fd.append('foto', file);
const res = await fetch(`${API_BASE_URL}/artikel/${id}/bild`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${this.getToken()}` },
body: fd,
});
if (!res.ok) { const e = await res.json().catch(()=>({})); throw new Error(e.error || `HTTP ${res.status}`); }
return res.json();
}
deleteArtikelBild(id) { return this.request(`/artikel/${id}/bild`, { method: 'DELETE' }); }
createArtikel(a) { return this.request('/artikel', { method: 'POST', body: JSON.stringify(a) }); } createArtikel(a) { return this.request('/artikel', { method: 'POST', body: JSON.stringify(a) }); }
updateArtikel(id, a) { return this.request(`/artikel/${id}`, { method: 'PUT', body: JSON.stringify(a) }); } updateArtikel(id, a) { return this.request(`/artikel/${id}`, { method: 'PUT', body: JSON.stringify(a) }); }
deleteArtikel(id) { return this.request(`/artikel/${id}`, { method: 'DELETE' }); } deleteArtikel(id) { return this.request(`/artikel/${id}`, { method: 'DELETE' }); }

View File

@@ -6,6 +6,7 @@ import AdminDashboard from './AdminDashboard';
import KundenVerwaltung from '../KundenVerwaltung'; import KundenVerwaltung from '../KundenVerwaltung';
import MitarbeiterVerwaltung from '../MitarbeiterVerwaltung'; import MitarbeiterVerwaltung from '../MitarbeiterVerwaltung';
import Geraete from '../Geraete'; import Geraete from '../Geraete';
import Produkte from '../Produkte';
import TourPlanung from '../TourPlanung'; import TourPlanung from '../TourPlanung';
import Account from './Account'; import Account from './Account';
@@ -38,6 +39,7 @@ export default function Dashboard({ user, onLogout }) {
{activeView === 'account' && <Account user={user} />} {activeView === 'account' && <Account user={user} />}
{isAdmin && activeView === 'admin' && <AdminDashboard />} {isAdmin && activeView === 'admin' && <AdminDashboard />}
{isAdmin && activeView === 'kunden' && <KundenVerwaltung />} {isAdmin && activeView === 'kunden' && <KundenVerwaltung />}
{isAdmin && activeView === 'produkte' && <Produkte />}
{isAdmin && activeView === 'mitarbeiter' && <MitarbeiterVerwaltung user={user} />} {isAdmin && activeView === 'mitarbeiter' && <MitarbeiterVerwaltung user={user} />}
{isAdmin && activeView === 'geraete' && <Geraete />} {isAdmin && activeView === 'geraete' && <Geraete />}
</main> </main>

View File

@@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { import {
FaPlusCircle, FaTruck, FaUndo, FaUsers, FaAddressBook, FaPlusCircle, FaTruck, FaUndo, FaUsers, FaAddressBook,
FaChartBar, FaUserCircle, FaSignOutAlt, FaBoxes, FaMapMarkedAlt FaChartBar, FaUserCircle, FaSignOutAlt, FaBoxes, FaMapMarkedAlt, FaTags
} from 'react-icons/fa'; } from 'react-icons/fa';
const mainItems = [ const mainItems = [
@@ -14,6 +14,7 @@ const mainItems = [
const adminItems = [ const adminItems = [
{ icon: FaChartBar, label: 'Auswertung', view: 'admin' }, { icon: FaChartBar, label: 'Auswertung', view: 'admin' },
{ icon: FaAddressBook, label: 'Kunden', view: 'kunden' }, { icon: FaAddressBook, label: 'Kunden', view: 'kunden' },
{ icon: FaTags, label: 'Produkte', view: 'produkte' },
{ icon: FaBoxes, label: 'Geräte', view: 'geraete' }, { icon: FaBoxes, label: 'Geräte', view: 'geraete' },
{ icon: FaUsers, label: 'Mitarbeiter', view: 'mitarbeiter' }, { icon: FaUsers, label: 'Mitarbeiter', view: 'mitarbeiter' },
]; ];

185
src/components/Produkte.jsx Normal file
View File

@@ -0,0 +1,185 @@
import { useEffect, useState } from 'react';
import { FaPlus, FaEdit, FaTrash, FaTimes, FaCamera, FaCheck, FaBan } from 'react-icons/fa';
import { api } from '../api/client';
const API_BASE = import.meta.env.VITE_API_URL || '/api';
const empty = { bezeichnung: '', pfand_betrag: '', beschreibung: '', aktiv: true };
export default function Produkte() {
const [list, setList] = useState([]);
const [form, setForm] = useState(empty);
const [editId, setEditId] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [bildBusy, setBildBusy] = useState(null); // id des Artikels der gerade Bild hochlaedt
const load = async () => {
setLoading(true);
try { setList(await api.getArtikel(true)); setError(''); }
catch (e) { setError(e.message); }
finally { setLoading(false); }
};
useEffect(() => { load(); }, []);
const startEdit = (a) => {
setEditId(a.id);
setForm({
bezeichnung: a.bezeichnung || '',
pfand_betrag: a.pfand_betrag ?? '',
beschreibung: a.beschreibung || '',
aktiv: !!a.aktiv,
});
};
const cancel = () => { setEditId(null); setForm(empty); };
const save = async () => {
if (!form.bezeichnung) return setError('Bezeichnung erforderlich');
if (form.pfand_betrag === '' || isNaN(parseFloat(form.pfand_betrag)))
return setError('Pfandbetrag erforderlich');
try {
const payload = {
bezeichnung: form.bezeichnung,
pfand_betrag: parseFloat(form.pfand_betrag),
beschreibung: form.beschreibung || null,
aktiv: form.aktiv,
};
if (editId) await api.updateArtikel(editId, payload);
else await api.createArtikel(payload);
cancel(); load();
} catch (e) { setError(e.message); }
};
const del = async (id) => {
if (!confirm('Produkt wirklich löschen? (Falls bereits in Vorgängen verwendet, lieber auf Inaktiv setzen.)')) return;
try { await api.deleteArtikel(id); load(); }
catch (e) { setError(e.message); }
};
const uploadBild = async (id, file) => {
if (!file) return;
setBildBusy(id);
try { await api.uploadArtikelBild(id, file); load(); }
catch (e) { setError(e.message); } finally { setBildBusy(null); }
};
const deleteBild = async (id) => {
if (!confirm('Bild entfernen?')) return;
try { await api.deleteArtikelBild(id); load(); }
catch (e) { setError(e.message); }
};
const toggleAktiv = async (a) => {
try {
await api.updateArtikel(a.id, {
bezeichnung: a.bezeichnung,
pfand_betrag: a.pfand_betrag,
beschreibung: a.beschreibung,
aktiv: !a.aktiv,
});
load();
} catch (e) { setError(e.message); }
};
const bildUrl = (a) =>
a.bild_url ? (a.bild_url.startsWith('http') ? a.bild_url : `${API_BASE.replace('/api', '')}${a.bild_url}`) : null;
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-slate-900">Produkte</h2>
<div className="text-sm text-slate-500">{list.length} Produkte</div>
</div>
{error && <div className="rounded-md bg-rose-50 px-3 py-2 text-sm text-rose-700 ring-1 ring-rose-200">{error}</div>}
<div className="card">
<h3 className="font-medium text-slate-800 mb-3">{editId ? 'Produkt bearbeiten' : 'Neues Produkt'}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="sm:col-span-2">
<label className="label">Bezeichnung *</label>
<input className="input" value={form.bezeichnung}
onChange={e => setForm(f => ({ ...f, bezeichnung: e.target.value }))} />
</div>
<div>
<label className="label">Pfandbetrag () *</label>
<input type="number" step="0.01" min="0" className="input" value={form.pfand_betrag}
onChange={e => setForm(f => ({ ...f, pfand_betrag: e.target.value }))} />
</div>
<div className="flex items-end">
<label className="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" checked={form.aktiv}
onChange={e => setForm(f => ({ ...f, aktiv: e.target.checked }))} />
Aktiv (im Vorgang-Formular auswählbar)
</label>
</div>
<div className="sm:col-span-2">
<label className="label">Beschreibung (optional)</label>
<textarea rows={2} className="input"
placeholder="z. B. Maße, Material, Hinweise"
value={form.beschreibung}
onChange={e => setForm(f => ({ ...f, beschreibung: e.target.value }))} />
</div>
</div>
<div className="flex gap-2 mt-4">
<button onClick={save} className="btn-primary"><FaPlus /> {editId ? 'Speichern' : 'Anlegen'}</button>
{editId && <button onClick={cancel} className="btn-secondary"><FaTimes /> Abbrechen</button>}
</div>
</div>
{loading ? <div className="text-center py-8 text-slate-500">Lade</div> : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{list.map(a => {
const url = bildUrl(a);
return (
<div key={a.id} className={`card flex flex-col gap-3 ${a.aktiv ? '' : 'opacity-60'}`}>
<div className="relative">
{url ? (
<img src={url} alt={a.bezeichnung}
className="w-full aspect-video object-cover rounded-lg ring-1 ring-slate-200 bg-slate-50" />
) : (
<div className="w-full aspect-video grid place-items-center rounded-lg bg-slate-50 ring-1 ring-slate-200 text-slate-300 text-3xl">
<FaCamera />
</div>
)}
{url && (
<button onClick={() => deleteBild(a.id)} title="Bild entfernen"
className="absolute top-2 right-2 h-7 w-7 grid place-items-center bg-white/95 rounded-full text-rose-600 shadow-soft">
<FaTimes size={12} />
</button>
)}
</div>
<div className="flex-1">
<div className="flex items-start justify-between gap-2">
<div className="font-semibold text-slate-900">{a.bezeichnung}</div>
{!a.aktiv && <span className="badge-slate">inaktiv</span>}
</div>
<div className="text-brand-900 font-bold text-lg">
{Number(a.pfand_betrag).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' })}
</div>
{a.beschreibung && <p className="text-xs text-slate-500 mt-1 line-clamp-2">{a.beschreibung}</p>}
</div>
<div className="flex items-center gap-2 -mx-1 pt-2 border-t border-slate-100">
<input id={`up-${a.id}`} type="file" accept="image/*" className="sr-only"
onChange={e => uploadBild(a.id, e.target.files?.[0])} disabled={bildBusy === a.id} />
<label htmlFor={`up-${a.id}`} className="btn-secondary flex-1 cursor-pointer text-xs py-2">
<FaCamera /> {bildBusy === a.id ? 'Lade…' : (url ? 'Bild ändern' : 'Bild')}
</label>
<button onClick={() => toggleAktiv(a)} title={a.aktiv ? 'Deaktivieren' : 'Aktivieren'}
className="btn-ghost px-2">
{a.aktiv ? <FaBan className="text-amber-600" /> : <FaCheck className="text-emerald-600" />}
</button>
<button onClick={() => startEdit(a)} className="btn-ghost px-2"><FaEdit /></button>
<button onClick={() => del(a.id)} className="btn-ghost px-2 text-rose-600"><FaTrash /></button>
</div>
</div>
);
})}
{list.length === 0 && (
<div className="col-span-full card text-center text-slate-400 py-12">Noch keine Produkte angelegt.</div>
)}
</div>
)}
</div>
);
}