feat: add payment tracking for orders
- Add payment method selection (PayPal/Bar) to order form - Store payment_method and payment_status in database - Add payment status badges in admin orders view - Add "mark as paid" functionality for admins - PayPal account configurable via PAYPAL_ACCOUNT env variable - Frontend loads PayPal account dynamically from /wawi/config endpoint - Update email notifications to include payment method Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ Für Bestellungen per Mail zusätzlich:
|
|||||||
Optional:
|
Optional:
|
||||||
- `APP_API_KEY` – Schutz fuer `/wawi/api/bestand` und `/wawi/order`
|
- `APP_API_KEY` – Schutz fuer `/wawi/api/bestand` und `/wawi/order`
|
||||||
- `COOKIE_SECURE` – `0` fuer http lokal, `1` fuer https
|
- `COOKIE_SECURE` – `0` fuer http lokal, `1` fuer https
|
||||||
|
- `PAYPAL_ACCOUNT` – PayPal-E-Mail für Bestellformular (z.B. `paypal@beispiel.de`)
|
||||||
|
|
||||||
## Benutzerverwaltung
|
## Benutzerverwaltung
|
||||||
Beim ersten Start wird **ein Admin** aus ENV erzeugt:
|
Beim ersten Start wird **ein Admin** aus ENV erzeugt:
|
||||||
@@ -59,10 +60,12 @@ python import_from_html.py /pfad/zu/hellas_bestand.html --truncate
|
|||||||
## Live‑Shop‑Ansicht (index.html)
|
## Live‑Shop‑Ansicht (index.html)
|
||||||
`index.html` lädt den Bestand aus der WaWi‑App:
|
`index.html` lädt den Bestand aus der WaWi‑App:
|
||||||
- API‑Proxy: `/wawi/proxy/bestand`
|
- API‑Proxy: `/wawi/proxy/bestand`
|
||||||
|
- Konfiguration: `/wawi/config` (lädt PayPal-Konto aus ENV)
|
||||||
|
|
||||||
Wenn du diese Datei auf einem Webserver auslieferst, stelle sicher, dass die WaWi‑App unter `/wawi` erreichbar ist.
|
Wenn du diese Datei auf einem Webserver auslieferst, stelle sicher, dass die WaWi‑App unter `/wawi` erreichbar ist.
|
||||||
Wenn du Bestellungen direkt aus `index.html` abschickst, muss der `X-Order-Key` bzw. `?key=` dem `APP_API_KEY` entsprechen.
|
Wenn du Bestellungen direkt aus `index.html` abschickst, muss der `X-Order-Key` bzw. `?key=` dem `APP_API_KEY` entsprechen.
|
||||||
Der Key wird in `index.html` im Script‑Block gesetzt: `const ORDER_KEY = ""` (neben der Formularlogik). Trage dort deinen Key ein.
|
Der Key wird in `index.html` im Script‑Block gesetzt: `const ORDER_KEY = ""`. Trage dort deinen Key ein (oder leer lassen, wenn kein API-Key erforderlich).
|
||||||
|
Das PayPal-Konto wird automatisch aus der ENV-Variable `PAYPAL_ACCOUNT` geladen.
|
||||||
|
|
||||||
## Umgebungsvariablen (ENV)
|
## Umgebungsvariablen (ENV)
|
||||||
**Pflicht/Empfohlen für Produktion**
|
**Pflicht/Empfohlen für Produktion**
|
||||||
@@ -80,6 +83,7 @@ Der Key wird in `index.html` im Script‑Block gesetzt: `const ORDER_KEY = ""` (
|
|||||||
**Optional**
|
**Optional**
|
||||||
- `APP_API_KEY` – gemeinsamer API‑Key für `/wawi/api/bestand` **und** `/wawi/order`
|
- `APP_API_KEY` – gemeinsamer API‑Key für `/wawi/api/bestand` **und** `/wawi/order`
|
||||||
- `COOKIE_SECURE` – `1` (default) setzt Secure‑Cookie, `0` deaktiviert für http
|
- `COOKIE_SECURE` – `1` (default) setzt Secure‑Cookie, `0` deaktiviert für http
|
||||||
|
- `PAYPAL_ACCOUNT` – PayPal-E-Mail-Adresse für Bestellungen (wird im Bestellformular angezeigt)
|
||||||
|
|
||||||
## Deployment (systemd + Gunicorn)
|
## Deployment (systemd + Gunicorn)
|
||||||
1) App nach `/var/www/hellas/wawi` kopieren
|
1) App nach `/var/www/hellas/wawi` kopieren
|
||||||
@@ -112,6 +116,7 @@ Environment="SMTP_PASS=dein_pass"
|
|||||||
Environment="SMTP_FROM=bestand@example.com"
|
Environment="SMTP_FROM=bestand@example.com"
|
||||||
Environment="ORDER_TO=admin@example.com, zweite@example.com"
|
Environment="ORDER_TO=admin@example.com, zweite@example.com"
|
||||||
Environment="APP_API_KEY=api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
Environment="APP_API_KEY=api_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
Environment="PAYPAL_ACCOUNT=dein-paypal@beispiel.de"
|
||||||
ExecStart=/var/www/hellas/wawi/.venv/bin/gunicorn -w 3 -b 127.0.0.1:8000 app:app
|
ExecStart=/var/www/hellas/wawi/.venv/bin/gunicorn -w 3 -b 127.0.0.1:8000 app:app
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|
||||||
|
|||||||
30
index.html
30
index.html
@@ -428,6 +428,22 @@
|
|||||||
<label style="grid-column: 1 / -1;">Notiz
|
<label style="grid-column: 1 / -1;">Notiz
|
||||||
<textarea id="fNotiz" name="notiz" rows="3"></textarea>
|
<textarea id="fNotiz" name="notiz" rows="3"></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
<div style="grid-column: 1 / -1; padding: 12px; background: #f0f9ff; border: 1px solid #bfdbfe; border-radius: 4px; margin-top: 8px;">
|
||||||
|
<strong>Zahlungsart wählen:</strong>
|
||||||
|
<div style="margin-top: 8px;">
|
||||||
|
<label style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||||
|
<input type="radio" name="payment_method" value="paypal" checked style="margin-right: 8px;" />
|
||||||
|
<span><strong>PayPal (Familie & Freunde)</strong></span>
|
||||||
|
</label>
|
||||||
|
<div id="paypalInfo" style="margin-left: 28px; margin-bottom: 12px; font-size: 0.95em; color: #1e40af;">
|
||||||
|
Bitte sende den Betrag über PayPal an: <strong id="paypalAccount">–</strong>
|
||||||
|
</div>
|
||||||
|
<label style="display: flex; align-items: center;">
|
||||||
|
<input type="radio" name="payment_method" value="bar" style="margin-right: 8px;" />
|
||||||
|
<span><strong>Bar bei Abholung</strong></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="button" class="btn" id="orderCancel">Abbrechen</button>
|
<button type="button" class="btn" id="orderCancel">Abbrechen</button>
|
||||||
@@ -568,6 +584,20 @@ const form = document.getElementById("orderForm");
|
|||||||
const setField = (id, v) => document.getElementById(id).value = v || "";
|
const setField = (id, v) => document.getElementById(id).value = v || "";
|
||||||
const ORDER_KEY = "";
|
const ORDER_KEY = "";
|
||||||
|
|
||||||
|
// PayPal-Konto vom Backend laden
|
||||||
|
fetch("/wawi/config")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(config => {
|
||||||
|
if (config.paypal_account) {
|
||||||
|
document.getElementById("paypalAccount").textContent = config.paypal_account;
|
||||||
|
} else {
|
||||||
|
document.getElementById("paypalAccount").textContent = "Nicht konfiguriert";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
document.getElementById("paypalAccount").textContent = "Fehler beim Laden";
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
const imgBtn = e.target.closest(".thumb-btn");
|
const imgBtn = e.target.closest(".thumb-btn");
|
||||||
if (imgBtn) {
|
if (imgBtn) {
|
||||||
|
|||||||
57
wawi/app.py
57
wawi/app.py
@@ -167,6 +167,7 @@ def init_db() -> None:
|
|||||||
ensure_price_column(db)
|
ensure_price_column(db)
|
||||||
ensure_image_column(db)
|
ensure_image_column(db)
|
||||||
ensure_orders_columns(db)
|
ensure_orders_columns(db)
|
||||||
|
ensure_payment_columns(db)
|
||||||
ensure_indexes(db)
|
ensure_indexes(db)
|
||||||
ensure_admin_user(db)
|
ensure_admin_user(db)
|
||||||
|
|
||||||
@@ -208,6 +209,22 @@ def ensure_orders_columns(db: sqlite3.Connection) -> None:
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_payment_columns(db: sqlite3.Connection) -> None:
|
||||||
|
"""Fügt Zahlungs-Spalten nachträglich zur orders-Tabelle hinzu."""
|
||||||
|
cols = db.execute("PRAGMA table_info(orders)").fetchall()
|
||||||
|
names = {c["name"] for c in cols}
|
||||||
|
if "payment_method" not in names:
|
||||||
|
db.execute("ALTER TABLE orders ADD COLUMN payment_method TEXT DEFAULT 'bar'")
|
||||||
|
if "payment_status" not in names:
|
||||||
|
db.execute("ALTER TABLE orders ADD COLUMN payment_status TEXT DEFAULT 'unpaid'")
|
||||||
|
if "paid_at" not in names:
|
||||||
|
db.execute("ALTER TABLE orders ADD COLUMN paid_at TEXT")
|
||||||
|
if "paid_by" not in names:
|
||||||
|
db.execute("ALTER TABLE orders ADD COLUMN paid_by TEXT")
|
||||||
|
db.commit()
|
||||||
|
logger.info("Zahlungs-Spalten für orders überprüft/erstellt")
|
||||||
|
|
||||||
|
|
||||||
def ensure_indexes(db: sqlite3.Connection) -> None:
|
def ensure_indexes(db: sqlite3.Connection) -> None:
|
||||||
"""Erstellt Indizes für bessere Query-Performance (idempotent)."""
|
"""Erstellt Indizes für bessere Query-Performance (idempotent)."""
|
||||||
# Index für items.artikel (häufig gesucht/gefiltert)
|
# Index für items.artikel (häufig gesucht/gefiltert)
|
||||||
@@ -648,6 +665,14 @@ def proxy_bestand():
|
|||||||
return jsonify(build_bestand())
|
return jsonify(build_bestand())
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/config", methods=["GET"])
|
||||||
|
def config():
|
||||||
|
"""Öffentliche Konfiguration für Frontend (z. B. PayPal-Konto)."""
|
||||||
|
return jsonify({
|
||||||
|
"paypal_account": os.environ.get("PAYPAL_ACCOUNT", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def build_bestand() -> list[dict]:
|
def build_bestand() -> list[dict]:
|
||||||
"""Aggregiert DB‑Zeilen in die Struktur der Live‑Bestand Ansicht."""
|
"""Aggregiert DB‑Zeilen in die Struktur der Live‑Bestand Ansicht."""
|
||||||
rows = get_db().execute(
|
rows = get_db().execute(
|
||||||
@@ -724,11 +749,15 @@ def order():
|
|||||||
if int(data.get("menge") or 0) <= 0:
|
if int(data.get("menge") or 0) <= 0:
|
||||||
return jsonify({"error": "Menge muss größer als 0 sein."}), 400
|
return jsonify({"error": "Menge muss größer als 0 sein."}), 400
|
||||||
|
|
||||||
|
payment_method = (data.get("payment_method") or "bar").strip().lower()
|
||||||
|
if payment_method not in ("paypal", "bar"):
|
||||||
|
payment_method = "bar"
|
||||||
|
|
||||||
db = get_db()
|
db = get_db()
|
||||||
db.execute(
|
db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO orders (name, handy, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at)
|
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)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, NULL, 0, NULL, NULL, ?, 'unpaid')
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
data.get("name"),
|
data.get("name"),
|
||||||
@@ -739,6 +768,7 @@ def order():
|
|||||||
int(data.get("menge") or 0),
|
int(data.get("menge") or 0),
|
||||||
data.get("notiz"),
|
data.get("notiz"),
|
||||||
now_iso(),
|
now_iso(),
|
||||||
|
payment_method,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -759,6 +789,7 @@ def order():
|
|||||||
msg["From"] = smtp_from
|
msg["From"] = smtp_from
|
||||||
recipients = [a.strip() for a in to_addr.split(",") if a.strip()]
|
recipients = [a.strip() for a in to_addr.split(",") if a.strip()]
|
||||||
msg["To"] = ", ".join(recipients)
|
msg["To"] = ", ".join(recipients)
|
||||||
|
payment_label = "PayPal (Familie & Freunde)" if payment_method == "paypal" else "Bar/Abholung"
|
||||||
body = (
|
body = (
|
||||||
"Neue Bestellung:\n"
|
"Neue Bestellung:\n"
|
||||||
f"Name: {data.get('name')}\n"
|
f"Name: {data.get('name')}\n"
|
||||||
@@ -767,6 +798,7 @@ def order():
|
|||||||
f"Artikel: {data.get('artikel')}\n"
|
f"Artikel: {data.get('artikel')}\n"
|
||||||
f"Größe: {data.get('groesse')}\n"
|
f"Größe: {data.get('groesse')}\n"
|
||||||
f"Menge: {data.get('menge')}\n"
|
f"Menge: {data.get('menge')}\n"
|
||||||
|
f"Zahlungsart: {payment_label}\n"
|
||||||
f"Notiz: {data.get('notiz') or '-'}\n"
|
f"Notiz: {data.get('notiz') or '-'}\n"
|
||||||
"WaWi: https://hellas.welker.me/wawi\n"
|
"WaWi: https://hellas.welker.me/wawi\n"
|
||||||
)
|
)
|
||||||
@@ -799,7 +831,7 @@ def orders():
|
|||||||
"""Bestellliste in der Verwaltung."""
|
"""Bestellliste in der Verwaltung."""
|
||||||
rows = get_db().execute(
|
rows = get_db().execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, name, handy, mannschaft, artikel, groesse, menge, notiz, created_at, done, completed_by, completed_at, canceled, canceled_by, canceled_at
|
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
|
||||||
FROM orders
|
FROM orders
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
@@ -861,6 +893,25 @@ def complete_order(order_id: int):
|
|||||||
return redirect(url_for("bp.orders"))
|
return redirect(url_for("bp.orders"))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/orders/mark_paid/<int:order_id>", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def mark_paid(order_id: int):
|
||||||
|
"""Bestellung als bezahlt markieren."""
|
||||||
|
user = session.get("user") or "unknown"
|
||||||
|
db = get_db()
|
||||||
|
db.execute(
|
||||||
|
"""
|
||||||
|
UPDATE orders
|
||||||
|
SET payment_status = 'paid', paid_by = ?, paid_at = ?
|
||||||
|
WHERE id = ? AND payment_status != 'paid'
|
||||||
|
""",
|
||||||
|
(user, now_iso(), order_id),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
logger.info(f"Bestellung #{order_id} als bezahlt markiert von {user}")
|
||||||
|
return redirect(url_for("bp.orders"))
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/orders/cancel/<int:order_id>", methods=["POST"])
|
@bp.route("/orders/cancel/<int:order_id>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def cancel_order(order_id: int):
|
def cancel_order(order_id: int):
|
||||||
|
|||||||
19
wawi/static/style.css
Normal file → Executable file
19
wawi/static/style.css
Normal file → Executable file
@@ -198,6 +198,25 @@ input[type="text"], input[type="number"] {
|
|||||||
}
|
}
|
||||||
.to-top:hover { transform: translateY(-2px); }
|
.to-top:hover { transform: translateY(-2px); }
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.badge.success {
|
||||||
|
background: rgba(123, 213, 141, 0.2);
|
||||||
|
color: var(--ok);
|
||||||
|
border: 1px solid rgba(123, 213, 141, 0.4);
|
||||||
|
}
|
||||||
|
.badge.warning {
|
||||||
|
background: rgba(243, 213, 42, 0.2);
|
||||||
|
color: var(--accent);
|
||||||
|
border: 1px solid rgba(243, 213, 42, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
.actions { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
.btn.small { padding: 6px 8px; }
|
.btn.small { padding: 6px 8px; }
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
<th>Artikel</th>
|
<th>Artikel</th>
|
||||||
<th>Größe</th>
|
<th>Größe</th>
|
||||||
<th>Menge</th>
|
<th>Menge</th>
|
||||||
|
<th>Zahlungsart</th>
|
||||||
|
<th>Zahlung</th>
|
||||||
<th>Notiz</th>
|
<th>Notiz</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th class="actions">Aktion</th>
|
<th class="actions">Aktion</th>
|
||||||
@@ -35,6 +37,16 @@
|
|||||||
<td>{{ o.artikel }}</td>
|
<td>{{ o.artikel }}</td>
|
||||||
<td>{{ o.groesse }}</td>
|
<td>{{ o.groesse }}</td>
|
||||||
<td>{{ o.menge }}</td>
|
<td>{{ o.menge }}</td>
|
||||||
|
<td>
|
||||||
|
{% if o.payment_method == "paypal" %}PayPal
|
||||||
|
{% else %}Bar
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if o.payment_status == "paid" %}<span class="badge success">Bezahlt</span>
|
||||||
|
{% else %}<span class="badge warning">Unbezahlt</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>{{ o.notiz or "–" }}</td>
|
<td>{{ o.notiz or "–" }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if o.canceled %}Storniert
|
{% if o.canceled %}Storniert
|
||||||
@@ -48,6 +60,12 @@
|
|||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn small" type="submit">Erledigt</button>
|
<button class="btn small" type="submit">Erledigt</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% if o.payment_status != "paid" %}
|
||||||
|
<form method="post" action="{{ url_for('bp.mark_paid', order_id=o.id) }}" onsubmit="return confirm('Als bezahlt markieren?');">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
<button class="btn small success" type="submit">Bezahlt</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
<form method="post" action="{{ url_for('bp.cancel_order', order_id=o.id) }}" onsubmit="return confirm('Bestellung wirklich stornieren?');">
|
<form method="post" action="{{ url_for('bp.cancel_order', order_id=o.id) }}" onsubmit="return confirm('Bestellung wirklich stornieren?');">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button class="btn small danger" type="submit">Stornieren</button>
|
<button class="btn small danger" type="submit">Stornieren</button>
|
||||||
@@ -58,12 +76,14 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="order-history">
|
<tr class="order-history">
|
||||||
<td colspan="10">
|
<td colspan="12">
|
||||||
<details class="inline-details">
|
<details class="inline-details">
|
||||||
<summary>Historie</summary>
|
<summary>Historie</summary>
|
||||||
<div class="history-grid">
|
<div class="history-grid">
|
||||||
<div><strong>Abgeschlossen von:</strong> {{ o.completed_by or "–" }}</div>
|
<div><strong>Abgeschlossen von:</strong> {{ o.completed_by or "–" }}</div>
|
||||||
<div><strong>Abgeschlossen am:</strong> {{ o.completed_at or "–" }}</div>
|
<div><strong>Abgeschlossen am:</strong> {{ o.completed_at or "–" }}</div>
|
||||||
|
<div><strong>Bezahlt markiert von:</strong> {{ o.paid_by or "–" }}</div>
|
||||||
|
<div><strong>Bezahlt markiert am:</strong> {{ o.paid_at or "–" }}</div>
|
||||||
<div><strong>Storniert von:</strong> {{ o.canceled_by or "–" }}</div>
|
<div><strong>Storniert von:</strong> {{ o.canceled_by or "–" }}</div>
|
||||||
<div><strong>Storniert am:</strong> {{ o.canceled_at or "–" }}</div>
|
<div><strong>Storniert am:</strong> {{ o.canceled_at or "–" }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,7 +92,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="empty">Keine Bestellungen.</td>
|
<td colspan="12" class="empty">Keine Bestellungen.</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
Reference in New Issue
Block a user