343 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			343 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | ||
| declare(strict_types=1);
 | ||
| 
 | ||
| // Konfiguration einbinden (stellt $servername, $username, $password, $db, $imagedir bereit)
 | ||
| require_once __DIR__ . '/../config/config.php';
 | ||
| 
 | ||
| /**
 | ||
|  * HTML-Escape Helper (fallback, falls global e() fehlt)
 | ||
|  */
 | ||
| if (!function_exists('e')) {
 | ||
|   function e(string $s): string
 | ||
|   {
 | ||
|     return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
 | ||
|   }
 | ||
| }
 | ||
| /** Attribut-Helper (für data-*, value, title, …) */
 | ||
| if (!function_exists('attr')) {
 | ||
|   function attr(string $s): string
 | ||
|   {
 | ||
|     return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| /** Pfad zum Platzhalterbild ermitteln */
 | ||
| /** Pfad zum Platzhalterbild ermitteln */
 | ||
| if (!function_exists('lg_default_image')) {
 | ||
|   function lg_default_image(): string
 | ||
|   {
 | ||
|     $dir = $_SERVER['DOCUMENT_ROOT'] . '/' . ($GLOBALS['placeholders_imagedir'] ?? 'img/placeholders');
 | ||
| 
 | ||
|     // Alle PNG/JPG Dateien aus dem Ordner holen
 | ||
|     $files = glob($dir . '/*.{png,jpg,jpeg,gif,webp}', GLOB_BRACE);
 | ||
| 
 | ||
|     if ($files && count($files) > 0) {
 | ||
|       // Einen zufälligen auswählen
 | ||
|       $randomFile = $files[array_rand($files)];
 | ||
| 
 | ||
|       // In relative URL umwandeln
 | ||
|       $relativePath = str_replace($_SERVER['DOCUMENT_ROOT'], '', $randomFile);
 | ||
|       return $relativePath;
 | ||
|     }
 | ||
| 
 | ||
|     // Fallback: statisches no-image
 | ||
|     return '/img/no-image-katie-1.png';
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| 
 | ||
| /**
 | ||
|  * DB-Connector (keine Fehlerdetails nach außen)
 | ||
|  */
 | ||
| function lg_db(): mysqli
 | ||
| {
 | ||
|   global $servername, $username, $password, $db;
 | ||
|   $conn = new mysqli($servername, $username, $password, $db);
 | ||
|   if ($conn->connect_error) {
 | ||
|     http_response_code(500);
 | ||
|     exit('Interner Fehler (DB)');
 | ||
|   }
 | ||
|   $conn->set_charset('utf8mb4');
 | ||
|   return $conn;
 | ||
| }
 | ||
| 
 | ||
| /**
 | ||
|  * Einzelne Karten-Kachel rendern
 | ||
|  */
 | ||
| function generateListItem(
 | ||
|   int $ListItemID,
 | ||
|   ?string $ItemImage,
 | ||
|   string $ItemTitle,
 | ||
|   ?string $ItemLink,
 | ||
|   int $ItemPriceCents,
 | ||
|   ?string $ItemComment,
 | ||
|   int $ItemReserved,
 | ||
|   ?string $ItemDate,
 | ||
|   int $ItemQty,
 | ||
| ): void {
 | ||
|   global $loggedin, $imagedir;
 | ||
| 
 | ||
|   // Preis formatieren (de_DE)
 | ||
|   $priceText = '';
 | ||
|   if ($ItemPriceCents > 0) {
 | ||
|     if (class_exists('NumberFormatter')) {
 | ||
|       $fmt = new NumberFormatter('de_DE', NumberFormatter::CURRENCY);
 | ||
|       $priceText = $fmt->formatCurrency($ItemPriceCents / 100, 'EUR');
 | ||
|     } else {
 | ||
|       $priceText = number_format($ItemPriceCents / 100, 2, ',', '.') . ' €';
 | ||
|     }
 | ||
|   }
 | ||
| 
 | ||
|   // Kommentar (2 Zeilen max., CSS macht den Clamp)
 | ||
|   $commentPlain = trim((string) $ItemComment);
 | ||
|   $commentHtml = ($commentPlain === '') ? ' ' : e($commentPlain);
 | ||
| 
 | ||
|   // Bild (lokal oder Placeholder)
 | ||
|   $srcPath = '';
 | ||
|   if (!empty($ItemImage)) {
 | ||
|     $local = rtrim($imagedir, '/') . '/' . $ItemImage;
 | ||
|     $srcPath = @is_file($local) ? $local : lg_default_image();
 | ||
|   } else {
 | ||
|     $srcPath = lg_default_image();
 | ||
|   }
 | ||
|   $imageTag = '<div class="ratio ratio-16x9"><img src="' . e($srcPath) . '" class="card-img-top object-fit-contain" alt="Bild zum Wunsch"></div>';
 | ||
| 
 | ||
|   // Titelzeile: Qty-Pill (nur wenn >1) + einzeiliger Titel
 | ||
|   $qtyPill = ($ItemQty > 1) ? '<span class="qty-pill me-2">×' . (int) $ItemQty . '</span>' : '';
 | ||
|   $titleHtml = '<div class="d-flex align-items-center card-title-row">'
 | ||
|     . $qtyPill
 | ||
|     . '<h5 class="card-title flex-grow-1 mb-0">' . e($ItemTitle) . '</h5>'
 | ||
|     . '</div>';
 | ||
| 
 | ||
|   // Status-Badge links
 | ||
|   $statusLeft = '';
 | ||
|   if ($ItemReserved >= $ItemQty) {
 | ||
|     $statusLeft = '<span class="badge bg-danger">alle reserviert</span>';
 | ||
|   } elseif ($ItemReserved > 0) {
 | ||
|     $statusLeft = '<span class="badge bg-warning text-dark">'
 | ||
|       . (int) $ItemReserved . ' / ' . (int) $ItemQty . ' reserviert</span>';
 | ||
|   }
 | ||
| 
 | ||
|   // Datum (lokal lesbar)
 | ||
|   $dateHtml = '';
 | ||
|   if (!empty($ItemDate)) {
 | ||
|     $ts = strtotime($ItemDate);
 | ||
|     $dateHtml = $ts ? date('d.m.Y', $ts) : e($ItemDate);
 | ||
|   }
 | ||
| 
 | ||
|   // User-Buttons (Reserve / Cancel / Vendor)
 | ||
|   // --- unten: Aktionsleiste (Icons, wachsen bei Hover) ---
 | ||
|   // (Reserve nur wenn möglich; Cancel immer; Vendor nur wenn Link)
 | ||
|   $userBtns = [];
 | ||
| 
 | ||
|   if ($ItemReserved < $ItemQty) {
 | ||
|     $userBtns[] = sprintf(
 | ||
|       '<button class="btn btn-primary btn-pill justify-content-end"
 | ||
|                aria-label="Reservieren"
 | ||
|                data-bs-toggle="modal" data-bs-target="#reservationModal"
 | ||
|                data-wishid="%d" data-reserved="0">
 | ||
|                <i class="fa-solid fa-lock"></i>
 | ||
|                <span>Reservieren</span>
 | ||
|                </button>',
 | ||
|       $ListItemID
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   if ($ItemReserved > 0) {
 | ||
|     $userBtns[] = sprintf(
 | ||
|       '<button class="btn btn-outline-primary btn-pill justify-content-center"
 | ||
|                aria-label="Reservierung aufheben"
 | ||
|                data-bs-toggle="modal" data-bs-target="#reservationModal"
 | ||
|                data-wishid="%d" data-reserved="1">
 | ||
|                <i class="fa-solid fa-unlock"></i>
 | ||
|                <span>Reservierung aufheben</span>
 | ||
|                </button>',
 | ||
|       $ListItemID
 | ||
|     );
 | ||
|   }
 | ||
| 
 | ||
|   if (!empty($ItemLink)) {
 | ||
|     $safeLink = e($ItemLink);
 | ||
|     $userBtns[] =
 | ||
|       '<a class="btn btn-outline-dark btn-pill justify-content-start"
 | ||
|                aria-label="Zum Anbieter"
 | ||
|                href="' . $safeLink . '" target="_blank" rel="noopener">
 | ||
|                <i class="fa-solid fa-up-right-from-square">
 | ||
|                </i><span>Zum Anbieter</span>
 | ||
|                </a>';
 | ||
|   }
 | ||
| 
 | ||
|   $userBtnRow = implode("\n", $userBtns);
 | ||
| 
 | ||
|   // ... in deinem Echo-Heredoc:
 | ||
|   // <div class="btn-group btn-group-pills mt-3" role="group" aria-label="Aktionen">
 | ||
|   //   {$userBtnRow}
 | ||
|   // </div>
 | ||
| 
 | ||
| 
 | ||
|   // Admin-Icons (nur eingeloggt)
 | ||
|   $adminFloating = '';
 | ||
|   if (!empty($loggedin)) {
 | ||
|     $adminFloating = '
 | ||
|     <div class="admin-actions btn-group btn-group-sm" role="group" aria-label="Admin-Action">
 | ||
|       <button class="btn btn-outline-secondary btn-icon" title="nach oben"
 | ||
|               data-bs-toggle="modal" data-bs-target="#pushprioModal" data-wishid="' . (int) $ListItemID . '">
 | ||
|         <i class="fa-solid fa-arrow-up"></i>
 | ||
|       </button>
 | ||
|       <button class="btn btn-outline-warning btn-icon" title="edit"
 | ||
|               data-bs-toggle="modal" data-bs-target="#itemModal"
 | ||
|               data-mode="edit"
 | ||
|               data-wishid="' . (int) $ListItemID . '"
 | ||
|               data-title="' . attr($ItemTitle) . '"
 | ||
|               data-description="' . attr($commentPlain) . '"
 | ||
|               data-price="' . attr(($ItemPriceCents > 0) ? number_format($ItemPriceCents / 100, 2, ',', '.') : '') . '"
 | ||
|               data-qty="' . (int) $ItemQty . '"
 | ||
|               data-link="' . attr((string) $ItemLink) . '">
 | ||
|         <i class="fa-solid fa-pen"></i>
 | ||
|       </button>
 | ||
|       <button class="btn btn-outline-danger btn-icon" title="löschen"
 | ||
|               data-bs-toggle="modal" data-bs-target="#deleteModal" data-wishid="' . (int) $ListItemID . '">
 | ||
|         <i class="fa-solid fa-trash"></i>
 | ||
|       </button>
 | ||
|     </div>';
 | ||
|   }
 | ||
| 
 | ||
|   // Preis-Badge rechts
 | ||
|   $priceHtml = $priceText !== '' ? '<span class="badge text-bg-light fs-6 fw-semibold">' . e($priceText) . '</span>' : '';
 | ||
| 
 | ||
|   echo <<<HTML
 | ||
|     <div class="col">
 | ||
|           <div class="card wl-card shadow-sm h-100 position-relative">
 | ||
|         {$adminFloating}
 | ||
|         {$imageTag}
 | ||
|         <div class="card-body d-flex flex-column">
 | ||
|           {$titleHtml}
 | ||
|           <p class="card-text">{$commentHtml}</p>
 | ||
| 
 | ||
|           <div class="d-flex justify-content-between align-items-center mt-1">
 | ||
|             <div>{$statusLeft}</div>
 | ||
|             <div class="status-right text-end">
 | ||
|               <div>{$priceHtml}</div>
 | ||
|               <div class="text-muted small">{$dateHtml}</div>
 | ||
|             </div>
 | ||
|           </div>
 | ||
| 
 | ||
|         <div class="btn-group btn-group-pills mt-3" role="group" aria-label="Aktionen">
 | ||
|           {$userBtnRow}
 | ||
|         </div>
 | ||
|         </div>
 | ||
|       </div>
 | ||
|     </div>
 | ||
| HTML;
 | ||
| }
 | ||
| 
 | ||
| 
 | ||
| /**
 | ||
|  * Haupt-Builder für die Listenansicht
 | ||
|  * $ListID: aktive Liste (int, intern)
 | ||
|  * $sortby: einer aus der Whitelist unten
 | ||
|  */
 | ||
| function wishlistMainBuilder(int $ListID, string $sortby = 'priority'): void
 | ||
| {
 | ||
|   global $loggedin;
 | ||
| 
 | ||
|   $conn = lg_db();
 | ||
| 
 | ||
|   // 1) Listen-Metadaten holen
 | ||
|   $stmt = $conn->prepare('SELECT title, description FROM lists WHERE ID = ?');
 | ||
|   $stmt->bind_param('i', $ListID);
 | ||
|   $stmt->execute();
 | ||
|   $res = $stmt->get_result();
 | ||
| 
 | ||
|   $listTitle = 'Unbekannte Liste';
 | ||
|   $listDesc = '';
 | ||
|   if ($res && $row = $res->fetch_assoc()) {
 | ||
|     $listTitle = e((string) $row['title']);
 | ||
|     $listDesc = e((string) $row['description']);
 | ||
|   }
 | ||
|   $stmt->close();
 | ||
| 
 | ||
|   // 2) ORDER BY Whitelist
 | ||
|   $orderWhitelist = [
 | ||
|     'priority' => 'priority DESC',
 | ||
|     'price_asc' => 'price ASC',
 | ||
|     'price_desc' => 'price DESC',
 | ||
|     'date_desc' => 'date DESC',
 | ||
|     'date_asc' => 'date ASC',
 | ||
|     'random' => 'RAND()',
 | ||
|   ];
 | ||
|   $orderSql = $orderWhitelist[$sortby] ?? $orderWhitelist['priority'];
 | ||
| 
 | ||
|   // 3) Wünsche laden
 | ||
|   $sql = "
 | ||
|   SELECT
 | ||
|     w.ID,
 | ||
|     w.image,
 | ||
|     w.title,
 | ||
|     w.link,
 | ||
|     w.price,
 | ||
|     w.description,
 | ||
|     w.date,
 | ||
|     w.qty,
 | ||
|     COALESCE(rc.reserved_count, 0) AS reserved_count
 | ||
|   FROM wishes w
 | ||
|   LEFT JOIN v_wish_reserved_counts rc
 | ||
|          ON rc.wish_id = w.ID
 | ||
|   WHERE w.wishlist = ?
 | ||
|   ORDER BY {$orderSql}";
 | ||
| 
 | ||
|   $stmt = $conn->prepare($sql);
 | ||
|   $stmt->bind_param('i', $ListID);
 | ||
|   $stmt->execute();
 | ||
|   $items = $stmt->get_result();
 | ||
| 
 | ||
|   // 4) Header-Text (vorher berechnen, kein Ternary im Heredoc)
 | ||
|   $loginMsg = $loggedin
 | ||
|     ? 'Eingeloggt: Du kannst Einträge bearbeiten, löschen und priorisieren.'
 | ||
|     : 'Tipp: Du kannst Einträge reservieren. Nur mit deinem Reservierungs-Passwort lässt sich die Reservierung wieder lösen.';
 | ||
| 
 | ||
|   echo <<<HTML
 | ||
| <section class="py-5 text-center container">
 | ||
|   <div class="row py-lg-4">
 | ||
|     <div class="col-lg-8 col-md-10 mx-auto">
 | ||
|       <h1 class="fw-light">{$listTitle}</h1>
 | ||
|       <p class="lead text-muted">{$listDesc}</p>
 | ||
|       <p class="text-muted">{$loginMsg}</p>
 | ||
|     </div>
 | ||
|   </div>
 | ||
| </section>
 | ||
| 
 | ||
| <div class="album py-3 bg-light">
 | ||
|   <div class="container">
 | ||
|     <div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3">
 | ||
| HTML;
 | ||
| 
 | ||
|   if ($items !== false && $items->num_rows > 0) {
 | ||
|     while ($row = $items->fetch_assoc()) {
 | ||
|       generateListItem(
 | ||
|         (int) $row['ID'],
 | ||
|         $row['image'] !== null ? (string) $row['image'] : null,
 | ||
|         (string) $row['title'],
 | ||
|         $row['link'] !== null ? (string) $row['link'] : null,
 | ||
|         (int) $row['price'],
 | ||
|         $row['description'] !== null ? (string) $row['description'] : null,
 | ||
|         (int) $row['reserved_count'],
 | ||
|         $row['date'] !== null ? (string) $row['date'] : null,
 | ||
|         isset($row['qty']) ? (int) $row['qty'] : 1
 | ||
|       );
 | ||
|     }
 | ||
| 
 | ||
|   } else {
 | ||
|     echo '<div class="col"><div class="alert alert-info w-100">Keine Einträge vorhanden.</div></div>';
 | ||
|   }
 | ||
| 
 | ||
|   echo <<<HTML
 | ||
|         </div>
 | ||
|       </div>
 | ||
|     </div>
 | ||
| HTML;
 | ||
| 
 | ||
|   $stmt->close();
 | ||
|   $conn->close();
 | ||
| }
 |