13 von 16

Admin · Kapitel 13 von 16

PHP · MySQL · HTML/CSS – Schritt für Schritt

Kapitel 12

statistiken.php – Admin-Bereich

Diese Seite ist nur für Admins zugänglich und macht drei Dinge gleichzeitig: sie zeigt Statistiken, sie erlaubt Produkte anzulegen, und sie erlaubt bestehende Produkte freizugeben, zu sperren oder zu löschen. Der Reihe nach.

Der komplette Code

statistiken.php
<?php
require_once('db.php');
session_start();

// admin-check
if (!isset($_SESSION['user_id']) || ($_SESSION['rolle'] ?? '') !== 'admin') {
    header('Location: index.php');
    exit;
}

$fehler = '';
$erfolg = '';

// ===== POST-HANDLER =====
if ($_SERVER['REQUEST_METHOD'] === 'POST') {

    // FREIGABE WECHSELN
    if (isset($_POST['toggle_id'])) {
        $id = (int)$_POST['toggle_id'];
        $stmt = $conn->prepare("UPDATE products SET freigeben = 1 - freigeben WHERE id = ?");
        $stmt->execute([$id]);
    }

    // LÖSCHEN
    elseif (isset($_POST['delete_id'])) {
        $id = (int)$_POST['delete_id'];
        try {
            $stmt = $conn->prepare("DELETE FROM products WHERE id = ?");
            $stmt->execute([$id]);
        } catch (PDOException $e) {
            // fk-constraint: produkt schon in order_items
            $fehler = 'Produkt kann nicht gelöscht werden (bereits bestellt). Sperre es stattdessen.';
        }
    }

    // HINZUFÜGEN
    elseif (isset($_POST['add_name'])) {
        $name        = trim($_POST['add_name'] ?? '');
        $artikelcode = trim($_POST['add_artikelcode'] ?? '');
        $price       = (float)str_replace(',', '.', $_POST['add_price'] ?? '0');
        $category_id = (int)($_POST['add_category'] ?? 0);
        $description = trim($_POST['add_description'] ?? '');
        $picture     = trim($_POST['add_picture'] ?? '');

        // validierung
        if ($name === '' || $artikelcode === '') {
            $fehler = 'Name und Artikelcode sind pflicht.';
        } elseif ($price <= 0) {
            $fehler = 'Preis muss größer als 0 sein.';
        } elseif ($category_id <= 0) {
            $fehler = 'Kategorie auswählen.';
        } else {
            // artikelcode-unique-check (zertifizierung-anforderung!)
            $stmt = $conn->prepare("SELECT id FROM products WHERE artikelcode = ?");
            $stmt->execute([$artikelcode]);
            if ($stmt->fetch()) {
                $fehler = 'Artikelcode existiert bereits.';
            } else {
                $stmt = $conn->prepare(
                    "INSERT INTO products 
                     (name, artikelcode, price, category_id, description, picture, freigeben) 
                     VALUES (?, ?, ?, ?, ?, ?, 0)"
                );
                $stmt->execute([$name, $artikelcode, $price, $category_id, $description, $picture]);
                $erfolg = 'Produkt angelegt (noch gesperrt, muss freigegeben werden).';
            }
        }
    }
}

// ===== DATEN LADEN =====
$anzahl_user         = $conn->query("SELECT COUNT(*) FROM users")->fetchColumn();
$anzahl_produkte     = $conn->query("SELECT COUNT(*) FROM products")->fetchColumn();
$anzahl_bestellungen = $conn->query("SELECT COUNT(*) FROM orders")->fetchColumn();
$umsatz              = $conn->query("SELECT COALESCE(SUM(menge * preis), 0) FROM order_items")->fetchColumn();

$kategorien    = $conn->query("SELECT id, name FROM categories ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
$alle_produkte = $conn->query(
    "SELECT p.id, p.name, p.artikelcode, p.price, p.freigeben, c.name AS kategorie_name
     FROM products p
     LEFT JOIN categories c ON c.id = p.category_id
     ORDER BY p.id DESC"
)->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>Statistiken & Admin</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
<?php include('navbar.php'); ?>

<h1>Statistiken</h1>

<p>Registrierte User: <strong><?= (int)$anzahl_user ?></strong></p>
<p>Produkte: <strong><?= (int)$anzahl_produkte ?></strong></p>
<p>Bestellungen: <strong><?= (int)$anzahl_bestellungen ?></strong></p>
<p>Umsatz: <strong><?= number_format($umsatz, 2, ',', '.') ?> €</strong></p>

<hr>
<h1>Produktverwaltung</h1>

<?php if ($fehler !== ''): ?>
    <p style="color:red;"><?= htmlspecialchars($fehler, ENT_QUOTES, 'UTF-8') ?></p>
<?php endif; ?>
<?php if ($erfolg !== ''): ?>
    <p style="color:green;"><?= htmlspecialchars($erfolg, ENT_QUOTES, 'UTF-8') ?></p>
<?php endif; ?>

<h2>Neues Produkt anlegen</h2>
<form action="statistiken.php" method="POST">
    <label>Name:<br><input type="text" name="add_name" required></label><br><br>
    <label>Artikelcode:<br><input type="text" name="add_artikelcode" required></label><br><br>
    <label>Preis (€):<br><input type="number" name="add_price" step="0.01" min="0.01" required></label><br><br>
    <label>Kategorie:<br>
        <select name="add_category" required>
            <option value="">-- bitte wählen --</option>
            <?php foreach ($kategorien as $kat): ?>
                <option value="<?= (int)$kat['id'] ?>">
                    <?= htmlspecialchars($kat['name'], ENT_QUOTES, 'UTF-8') ?>
                </option>
            <?php endforeach; ?>
        </select>
    </label><br><br>
    <label>Beschreibung:<br><textarea name="add_description" rows="3" cols="40"></textarea></label><br><br>
    <label>Bildpfad (z.B. bilder/xyz.jpg):<br><input type="text" name="add_picture"></label><br><br>
    <button type="submit">Produkt anlegen</button>
</form>

<h2>Alle Produkte</h2>
<table border="1" cellpadding="6">
    <tr>
        <th>ID</th><th>Artikelcode</th><th>Name</th><th>Kategorie</th>
        <th>Preis</th><th>Status</th><th>Aktionen</th>
    </tr>
    <?php foreach ($alle_produkte as $p): ?>
        <tr>
            <td><?= (int)$p['id'] ?></td>
            <td><?= htmlspecialchars($p['artikelcode'], ENT_QUOTES, 'UTF-8') ?></td>
            <td><?= htmlspecialchars($p['name'], ENT_QUOTES, 'UTF-8') ?></td>
            <td><?= htmlspecialchars($p['kategorie_name'] ?? '-', ENT_QUOTES, 'UTF-8') ?></td>
            <td><?= number_format($p['price'], 2, ',', '.') ?> €</td>
            <td>
                <?= $p['freigeben'] ? '<span style="color:green;">freigegeben</span>'
                                    : '<span style="color:red;">gesperrt</span>' ?>
            </td>
            <td>
                <form action="statistiken.php" method="POST" style="display:inline;">
                    <input type="hidden" name="toggle_id" value="<?= (int)$p['id'] ?>">
                    <button type="submit">
                        <?= $p['freigeben'] ? 'Sperren' : 'Freigeben' ?>
                    </button>
                </form>
                <form action="statistiken.php" method="POST" style="display:inline;"
                      onsubmit="return confirm('Wirklich löschen?');">
                    <input type="hidden" name="delete_id" value="<?= (int)$p['id'] ?>">
                    <button type="submit">Löschen</button>
                </form>
            </td>
        </tr>
    <?php endforeach; ?>
</table>

</body>
</html>

Wichtige Konzepte in diesem File

1. Admin-Check gleich am Anfang

if (!isset($_SESSION['user_id']) || ($_SESSION['rolle'] ?? '') !== 'admin') {
    header('Location: index.php');
    exit;
}

Wer nicht eingeloggt ist ODER nicht die Rolle "admin" hat → sofort raus, noch bevor irgendwas anderes passiert. Kein Rendering, kein DB-Zugriff für Unbefugte. Das ist die eigentliche Sicherheit – das Verstecken in der navbar ist nur Deko.

2. Ein File, drei Aktionen – unterschieden durch hidden inputs

Es gibt keinen ?action=delete-Parameter. Stattdessen unterscheiden die Formulare durch welches hidden field mitgeschickt wird:

  • toggle_id → Freigabe wechseln
  • delete_id → löschen
  • add_name → neues Produkt anlegen

Das elseif-Konstrukt stellt sicher, dass immer nur eine Aktion greift.

3. Toggle-Trick: freigeben = 1 - freigeben

UPDATE products SET freigeben = 1 - freigeben WHERE id = ?

Aus 1 wird 0 (1 - 1 = 0), aus 0 wird 1 (1 - 0 = 1). Ein einziges Statement für beide Richtungen, kein IF drumherum. Klein aber elegant.

4. Artikelcode-Unique-Check

Doppelt abgesichert: die DB hat UNIQUE auf der Spalte, und PHP prüft davor mit SELECT. Die DB würde einen Fehler werfen – aber der Check davor liefert eine schöne Fehlermeldung statt Exception. Das ist die ausdrückliche Anforderung aus der zertifizierung-Angabe: "es darf nicht möglich sein, dass mehrere Produkte denselben Artikelcode haben".

5. Löschen wirft FK-Fehler wenn Produkt schon bestellt

Wichtig zu verstehen: ein Produkt, das in order_items steckt, kann nicht gelöscht werden – der Foreign Key verhindert es. Genau deshalb steht der DELETE in einem try/catch. Fällt die Query auf die Nase, fängt der Catch das ab und schlägt vor: Produkt sperren statt löschen (freigeben = 0 setzen). Das ist auch in echten Shops so – historische Bestellungen sollen ihre Produkte behalten, sonst wird die Rechnung unlesbar.

6. Zwei separate forms statt einem mit mehreren buttons

Jede Aktion (Toggle, Löschen) ist ein eigenes <form>, damit das onsubmit="return confirm(...)" beim Löschen nur beim Löschen greift und nicht auch beim Toggeln nachfragt.

7. Neue Produkte starten immer als "gesperrt"

Der INSERT setzt hart freigeben = 0. Der Admin muss aktiv freigeben – so kann kein halbfertiger Datensatz versehentlich im Shop landen.

Was diese Seite laut zertifizierung-Angabe noch nicht kann: "Statistik über die 5 meistbestellten, 5 seltensten und den Bestellverlauf über 4 Wochen". Die vier Zahlen oben sind ein guter Anfang, aber die drei Ranking-Queries fehlen noch. Snippets dazu im nächsten Kapitel (SQL-Cheatsheet).

16 Kapitel · ein Projekt

Der Codeinander Lernpfad

DEEN in Vorbereitung

Grundlagen

01GrundlagenIntro & Setup 02GrundlagenDatenbank-Schema 03Grundlagendb.php – Verbindung

Auth

04Authregister.php 05Authlogin.php 06Authlogout.php

Shop

07Shopnavbar.php 08Shopindex.php – Liste 09Shopproductdetail.php

Warenkorb

10Warenkorbcart_add.php 11Warenkorbwarenkorb.php 12Warenkorbcart_remove.php

Admin

13Adminstatistiken.php + Admin 14Admincheckout.php + Rechnung

Referenz

15ReferenzSQL-Cheatsheet 16Referenzzertifizierung-Checkliste