Compare commits

..

2 Commits

Author SHA1 Message Date
3dcbfecbe4 feat: add info field to articles for shop display
Added optional info text field to articles that can be managed in the backend and is displayed in the shop only when filled. The info appears in both card view and detail modal with an informative style.

- Added info column to items table with migration
- Updated backend edit form with textarea for info text
- Modified API to include info field in bestand response
- Enhanced shop frontend to display info badge when available

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 15:12:12 +01:00
470044c9c9 fix: resolve merge conflicts in app.py
Fixed remaining merge conflict markers in INSERT and SELECT statements for orders table to include email field.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-10 13:19:49 +01:00
3 changed files with 36 additions and 19 deletions

View File

@@ -605,6 +605,9 @@ function render(items) {
// Sale-Badge anzeigen
const saleBadge = item.sale ? '<div class="sale-badge">SALE 🔥</div>' : '';
// Info-Text (nur wenn vorhanden)
const infoText = item.info ? `<div class="card-info" style="margin-top: 8px; padding: 8px; background: rgba(243, 213, 42, 0.1); border-left: 3px solid var(--accent); border-radius: 4px; font-size: 12px; line-height: 1.4; color: var(--text);"> ${item.info}</div>` : '';
return `
<div class="card-tile">
${saleBadge}
@@ -615,6 +618,7 @@ function render(items) {
<div class="card-title">${item.artikel}</div>
<div class="card-price">${priceText}</div>
<div class="card-sub">${item.rows.length} Größen</div>
${infoText}
${stockBadge}
</div>
<div class="card-actions">
@@ -692,7 +696,9 @@ document.addEventListener("click", (e) => {
media.innerHTML = img ? `<img src="${img}" alt="${artikel}">` : "";
document.getElementById("detailTitle").textContent = artikel;
document.getElementById("detailPrice").textContent = detailBtn.dataset.preis || "Preis auf Anfrage";
document.getElementById("detailSizes").textContent = `${item.rows.length} Größen`;
const sizesText = `${item.rows.length} Größen`;
const infoText = item.info ? ` · ${item.info}` : '';
document.getElementById("detailSizes").textContent = sizesText + infoText;
const list = item.rows.map(r => {
const stock = Number(r.gezaehlt) || 0;
let stockInfo = '';

View File

@@ -167,6 +167,7 @@ def init_db() -> None:
ensure_price_column(db)
ensure_image_column(db)
ensure_sale_column(db)
ensure_info_column(db)
ensure_orders_columns(db)
ensure_payment_columns(db)
ensure_indexes(db)
@@ -201,6 +202,16 @@ def ensure_sale_column(db: sqlite3.Connection) -> None:
logger.info("Sale-Spalte für items erstellt")
def ensure_info_column(db: sqlite3.Connection) -> None:
"""Fügt info-Spalte für zusätzliche Artikelinformationen hinzu."""
cols = db.execute("PRAGMA table_info(items)").fetchall()
if any(c["name"] == "info" for c in cols):
return
db.execute("ALTER TABLE items ADD COLUMN info TEXT")
db.commit()
logger.info("Info-Spalte für items erstellt")
def ensure_orders_columns(db: sqlite3.Connection) -> None:
"""Sorgt für alle nachträglich eingeführten OrdersSpalten."""
cols = db.execute("PRAGMA table_info(orders)").fetchall()
@@ -475,6 +486,7 @@ def new_item():
if uploaded:
bild_url = uploaded
sale = 1 if request.form.get("sale") else 0
info = (request.form.get("info") or "").strip() or None
soll = int(request.form.get("soll") or 0)
gezaehlt = int(request.form.get("gezaehlt") or 0)
verkaeufe = int(request.form.get("verkaeufe") or 0)
@@ -483,12 +495,12 @@ def new_item():
db = get_db()
db.execute(
"""
INSERT INTO items (artikel, groesse, preis, bild_url, sale, soll, gezaehlt, verkaeufe, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO items (artikel, groesse, preis, bild_url, sale, info, soll, gezaehlt, verkaeufe, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(artikel, groesse, preis, bild_url, sale, soll, gezaehlt, verkaeufe, now_iso(), now_iso()),
(artikel, groesse, preis, bild_url, sale, info, soll, gezaehlt, verkaeufe, now_iso(), now_iso()),
)
db.execute("UPDATE items SET preis = ?, bild_url = ?, sale = ? WHERE artikel = ?", (preis, bild_url, sale, artikel))
db.execute("UPDATE items SET preis = ?, bild_url = ?, sale = ?, info = ? WHERE artikel = ?", (preis, bild_url, sale, info, artikel))
db.commit()
return redirect(url_for("bp.index"))
@@ -513,6 +525,7 @@ def edit_item(item_id: int):
if uploaded:
bild_url = uploaded
sale = 1 if request.form.get("sale") else 0
info = (request.form.get("info") or "").strip() or None
soll = int(request.form.get("soll") or 0)
gezaehlt = int(request.form.get("gezaehlt") or 0)
verkaeufe = int(request.form.get("verkaeufe") or 0)
@@ -520,12 +533,12 @@ def edit_item(item_id: int):
db.execute(
"""
UPDATE items
SET artikel = ?, groesse = ?, preis = ?, bild_url = ?, sale = ?, soll = ?, gezaehlt = ?, verkaeufe = ?, updated_at = ?
SET artikel = ?, groesse = ?, preis = ?, bild_url = ?, sale = ?, info = ?, soll = ?, gezaehlt = ?, verkaeufe = ?, updated_at = ?
WHERE id = ?
""",
(artikel, groesse, preis, bild_url, sale, soll, gezaehlt, verkaeufe, now_iso(), item_id),
(artikel, groesse, preis, bild_url, sale, info, soll, gezaehlt, verkaeufe, now_iso(), item_id),
)
db.execute("UPDATE items SET preis = ?, bild_url = ?, sale = ? WHERE artikel = ?", (preis, bild_url, sale, artikel))
db.execute("UPDATE items SET preis = ?, bild_url = ?, sale = ?, info = ? WHERE artikel = ?", (preis, bild_url, sale, info, artikel))
db.commit()
return redirect(url_for("bp.index"))
@@ -719,7 +732,7 @@ def build_bestand() -> list[dict]:
"""Aggregiert DBZeilen in die Struktur der LiveBestand Ansicht."""
rows = get_db().execute(
"""
SELECT artikel, groesse, preis, bild_url, sale, soll, gezaehlt, verkaeufe
SELECT artikel, groesse, preis, bild_url, sale, info, soll, gezaehlt, verkaeufe
FROM items
ORDER BY artikel, groesse
"""
@@ -732,7 +745,7 @@ def build_bestand() -> list[dict]:
continue
item = data.setdefault(
artikel,
{"artikel": artikel, "preis": 0, "bild_url": "", "sale": 0, "rows": [], "totals": {"soll": 0, "gezaehlt": 0, "abweichung": 0, "fehlbestand": 0, "verkaeufe": 0}},
{"artikel": artikel, "preis": 0, "bild_url": "", "sale": 0, "info": None, "rows": [], "totals": {"soll": 0, "gezaehlt": 0, "abweichung": 0, "fehlbestand": 0, "verkaeufe": 0}},
)
if not item["preis"]:
item["preis"] = float(r["preis"] or 0)
@@ -741,6 +754,9 @@ def build_bestand() -> list[dict]:
# Sale-Status (1 wenn mindestens eine Größe sale=1 hat)
if r["sale"]:
item["sale"] = 1
# Info-Text (nur wenn vorhanden)
if r["info"] and not item["info"]:
item["info"] = (r["info"] or "").strip()
soll = int(r["soll"] or 0)
gezaehlt = int(r["gezaehlt"] or 0)
verkaeufe = int(r["verkaeufe"] or 0)
@@ -886,13 +902,8 @@ def order():
db = get_db()
cursor = db.execute(
"""
<<<<<<< HEAD
INSERT INTO orders (name, handy, email, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at, payment_method, payment_status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, NULL, 0, NULL, NULL, ?, 'unpaid')
=======
INSERT INTO orders (name, handy, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at, payment_method, payment_status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, NULL, 0, NULL, NULL, ?, 'unpaid')
>>>>>>> origin/main
""",
(
data.get("name"),
@@ -1031,11 +1042,7 @@ def orders():
"""Bestellliste in der Verwaltung."""
rows = get_db().execute(
"""
<<<<<<< HEAD
SELECT id, name, handy, email, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at, payment_method, payment_status, paid_at, paid_by
=======
SELECT id, name, handy, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at, payment_method, payment_status, paid_at, paid_by
>>>>>>> origin/main
FROM orders
ORDER BY id DESC
LIMIT 500

View File

@@ -29,6 +29,10 @@
<input type="checkbox" name="sale" value="1" {% if item and item.sale %}checked{% endif %} style="width: auto; height: 18px;" />
<span style="color: var(--text);">Sale / Abverkauf 🔥</span>
</label>
<label style="grid-column: 1 / -1;">
Info-Text für Shop (optional)
<textarea name="info" rows="3" placeholder="z.B. Lieferzeit 2-3 Wochen, limitierte Auflage, etc.">{{ item.info if item and item.info else '' }}</textarea>
</label>
<label>
Soll
<input type="number" name="soll" min="0" value="{{ item.soll if item else 0 }}" />