Files
Simple-Wishlist/include/listgenerator.php

343 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 === '') ? '&nbsp;' : 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();
}