Fix: Weiße Seite bei Direkteinstieg behoben (DB Auto-Migration in db_connect.php verschoben)
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 37s

This commit is contained in:
Gemini
2026-05-13 19:40:23 +00:00
parent c0088d5ce2
commit 8cd81846c9
3 changed files with 652 additions and 15 deletions

View File

@@ -39,4 +39,21 @@ $conn->set_charset("utf8mb4");
// Zeitzone setzen
date_default_timezone_set('Europe/Berlin');
// --- AUTO MIGRATIONS ---
// Ensure display_name exists
$check_display_name = $conn->query("SHOW COLUMNS FROM users LIKE 'display_name'");
if ($check_display_name && $check_display_name->num_rows == 0) {
$conn->query("ALTER TABLE users ADD COLUMN display_name VARCHAR(255) DEFAULT NULL");
}
// Ensure todo_lists and todo_items exist
$conn->query("CREATE TABLE IF NOT EXISTS todo_lists (id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, household_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
$conn->query("CREATE TABLE IF NOT EXISTS todo_items (id INT AUTO_INCREMENT PRIMARY KEY, todo_list_id INT NOT NULL, title VARCHAR(255) NOT NULL, is_completed TINYINT(1) DEFAULT 0, order_index INT DEFAULT 0, FOREIGN KEY (todo_list_id) REFERENCES todo_lists(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
$check_todo_list_id = $conn->query("SHOW COLUMNS FROM packing_lists LIKE 'todo_list_id'");
if ($check_todo_list_id && $check_todo_list_id->num_rows == 0) {
$conn->query("ALTER TABLE packing_lists ADD COLUMN todo_list_id INT DEFAULT NULL");
$conn->query("ALTER TABLE packing_lists ADD CONSTRAINT fk_packing_list_todo FOREIGN KEY (todo_list_id) REFERENCES todo_lists(id) ON DELETE SET NULL");
}
?>

View File

@@ -13,21 +13,6 @@ if (!isset($_SESSION['user_id'])) {
require_once 'db_connect.php';
// --- AUTO MIGRATIONS ---
$check_display_name = $conn->query("SHOW COLUMNS FROM users LIKE 'display_name'");
if ($check_display_name && $check_display_name->num_rows == 0) {
$conn->query("ALTER TABLE users ADD COLUMN display_name VARCHAR(255) DEFAULT NULL");
}
$conn->query("CREATE TABLE IF NOT EXISTS todo_lists (id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, household_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
$conn->query("CREATE TABLE IF NOT EXISTS todo_items (id INT AUTO_INCREMENT PRIMARY KEY, todo_list_id INT NOT NULL, title VARCHAR(255) NOT NULL, is_completed TINYINT(1) DEFAULT 0, order_index INT DEFAULT 0, FOREIGN KEY (todo_list_id) REFERENCES todo_lists(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
$check_todo_list_id = $conn->query("SHOW COLUMNS FROM packing_lists LIKE 'todo_list_id'");
if ($check_todo_list_id && $check_todo_list_id->num_rows == 0) {
$conn->query("ALTER TABLE packing_lists ADD COLUMN todo_list_id INT DEFAULT NULL");
$conn->query("ALTER TABLE packing_lists ADD CONSTRAINT fk_packing_list_todo FOREIGN KEY (todo_list_id) REFERENCES todo_lists(id) ON DELETE SET NULL");
}
require_once 'header.php';
$current_user_id = $_SESSION['user_id'];

View File

@@ -0,0 +1,635 @@
<?php
// packing_list_detail.php - Detailansicht einer Packliste
// FINALE, STABILE VERSION: Mit modernem Tree-View, Toggle-Funktion, Grünen Charts & Träger-Modal
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
require_once 'db_connect.php';
$current_user_id = $_SESSION['user_id'];
$packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
// Handle Todo Toggle
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['toggle_todo_item'])) {
$item_id = intval($_POST['item_id']);
$status = isset($_POST['status']) ? intval($_POST['status']) : 0;
$stmt = $conn->prepare("UPDATE todo_items SET is_completed = ? WHERE id = ?");
$stmt->bind_param("ii", $status, $item_id);
$stmt->execute();
$stmt->close();
// Redirect to avoid form resubmission
header("Location: packing_list_detail.php?id=" . $packing_list_id);
exit;
}
require_once 'header.php';
$packing_list = null;
$total_weight_grams = 0;
$total_consumable_weight = 0;
$weight_by_category = [];
$weight_by_carrier = [];
$weight_by_carrier_non_consumable = [];
// Array for Carrier Detailed Stats (Modal)
$carrier_stats_details = [];
if ($packing_list_id <= 0) {
die("Keine Packlisten-ID angegeben.");
}
// Erweiterte Berechtigungsprüfung für Haushalte
$stmt_current_user = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_current_user->bind_param("i", $current_user_id);
$stmt_current_user->execute();
$current_user_household_id = $stmt_current_user->get_result()->fetch_assoc()['household_id'];
$stmt_current_user->close();
$stmt_list_owner = $conn->prepare(
"SELECT pl.*, u.household_id as owner_household_id
FROM packing_lists pl
JOIN users u ON pl.user_id = u.id
WHERE pl.id = ?"
);
$stmt_list_owner->bind_param("i", $packing_list_id);
$stmt_list_owner->execute();
$result = $stmt_list_owner->get_result();
if ($result->num_rows > 0) {
$packing_list = $result->fetch_assoc();
$is_owner = ($packing_list['user_id'] == $current_user_id);
$is_household_list = !empty($packing_list['household_id']);
$is_in_same_household = ($is_household_list && $packing_list['household_id'] == $current_user_household_id);
if (!$is_owner && !$is_in_same_household) {
die("Packliste nicht gefunden oder Zugriff verweigert.");
}
} else {
die("Packliste nicht gefunden oder Zugriff verweigert.");
}
$stmt_list_owner->close();
$todo_items = [];
$todo_list_name = '';
if (!empty($packing_list['todo_list_id'])) {
$stmt_tl = $conn->prepare("SELECT name FROM todo_lists WHERE id = ?");
$stmt_tl->bind_param("i", $packing_list['todo_list_id']);
$stmt_tl->execute();
$res_tl = $stmt_tl->get_result();
if ($row = $res_tl->fetch_assoc()) $todo_list_name = $row['name'];
$stmt_tl->close();
$stmt_td = $conn->prepare("SELECT * FROM todo_items WHERE todo_list_id = ? ORDER BY is_completed ASC, id ASC");
$stmt_td->bind_param("i", $packing_list['todo_list_id']);
$stmt_td->execute();
$todo_items = $stmt_td->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_td->close();
}
// Check for items on the table
$stmt_table = $conn->prepare("SELECT COUNT(*) as table_count FROM packing_list_items WHERE packing_list_id = ? AND carrier_user_id IS NULL AND backpack_id IS NULL AND backpack_compartment_id IS NULL AND parent_packing_list_item_id IS NULL");
$stmt_table->bind_param("i", $packing_list_id);
$stmt_table->execute();
$table_items_count = $stmt_table->get_result()->fetch_assoc()['table_count'];
$stmt_table->close();
$page_title = "Packliste: " . htmlspecialchars($packing_list['name']);
// FIX: Join Categories also for Backpacks
$sql = "SELECT
pli.id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id,
pli.backpack_id, pli.backpack_compartment_id,
COALESCE(a.name, pli.name, bp.name, bpc.name, 'Unbekanntes Item') AS article_name,
COALESCE(a.weight_grams, bp.weight_grams, 0) as weight_grams,
a.image_url, a.product_designation, a.consumable,
COALESCE(c.name, c_bp.name, 'Sonstiges') AS category_name,
m.name AS manufacturer_name,
COALESCE(NULLIF(u.display_name, ''), u.username) AS carrier_name,
u.id AS carrier_id
FROM packing_list_items AS pli
LEFT JOIN articles AS a ON pli.article_id = a.id
LEFT JOIN backpacks AS bp ON pli.backpack_id = bp.id
LEFT JOIN backpack_compartments AS bpc ON pli.backpack_compartment_id = bpc.id
LEFT JOIN categories AS c ON a.category_id = c.id
LEFT JOIN categories AS c_bp ON bp.category_id = c_bp.id
LEFT JOIN manufacturers AS m ON a.manufacturer_id = m.id
LEFT JOIN users AS u ON pli.carrier_user_id = u.id
WHERE pli.packing_list_id = ?
AND NOT (pli.carrier_user_id IS NULL AND pli.backpack_id IS NULL AND pli.backpack_compartment_id IS NULL AND pli.parent_packing_list_item_id IS NULL)
ORDER BY pli.order_index ASC";
$stmt = $conn->prepare($sql);
if (!$stmt) {
die("SQL Fehler: " . $conn->error);
}
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result();
$items_by_id = [];
$items_by_parent = [];
while ($row = $result->fetch_assoc()) {
$items_by_id[$row['id']] = $row;
$parent_id = $row['parent_packing_list_item_id'] ?: 0;
if (!isset($items_by_parent[$parent_id])) {
$items_by_parent[$parent_id] = [];
}
$items_by_parent[$parent_id][] = $row;
// Stats
$item_weight = $row['quantity'] * $row['weight_grams'];
$total_weight_grams += $item_weight;
$carrier_name = $row['carrier_name'] ?: 'Sonstiges';
$carrier_id = $row['carrier_id'] ?: 0;
// Init stats arrays
if (!isset($weight_by_carrier[$carrier_name])) $weight_by_carrier[$carrier_name] = 0;
$weight_by_carrier[$carrier_name] += $item_weight;
$cat_name = $row['category_name'] ?: 'Sonstiges';
if (!isset($weight_by_category[$cat_name])) $weight_by_category[$cat_name] = 0;
$weight_by_category[$cat_name] += $item_weight;
if ($row['consumable']) {
$total_consumable_weight += $item_weight;
} else {
if (!isset($weight_by_carrier_non_consumable[$carrier_name])) $weight_by_carrier_non_consumable[$carrier_name] = 0;
$weight_by_carrier_non_consumable[$carrier_name] += $item_weight;
}
// Prepare Detailed Stats per Carrier for Modal
if (!isset($carrier_stats_details[$carrier_name])) {
$carrier_stats_details[$carrier_name] = [
'total_weight' => 0,
'base_weight' => 0,
'consumable_weight' => 0,
'categories' => []
];
}
$carrier_stats_details[$carrier_name]['total_weight'] += $item_weight;
if ($row['consumable']) {
$carrier_stats_details[$carrier_name]['consumable_weight'] += $item_weight;
} else {
$carrier_stats_details[$carrier_name]['base_weight'] += $item_weight;
}
if (!isset($carrier_stats_details[$carrier_name]['categories'][$cat_name])) {
$carrier_stats_details[$carrier_name]['categories'][$cat_name] = 0;
}
$carrier_stats_details[$carrier_name]['categories'][$cat_name] += $item_weight;
}
$stmt->close();
$conn->close();
$total_weight_without_consumables = $total_weight_grams - $total_consumable_weight;
// Helper functions (same as before)
function get_recursive_quantity($parent_id, $items_by_parent) {
$count = 0;
if (isset($items_by_parent[$parent_id])) {
foreach ($items_by_parent[$parent_id] as $child) {
$count += $child['quantity'];
$count += get_recursive_quantity($child['id'], $items_by_parent);
}
}
return $count;
}
function get_recursive_weight($parent_id, $items_by_parent) {
$weight = 0;
if (isset($items_by_parent[$parent_id])) {
foreach ($items_by_parent[$parent_id] as $child) {
$weight += ($child['quantity'] * $child['weight_grams']);
$weight += get_recursive_weight($child['id'], $items_by_parent);
}
}
return $weight;
}
function render_item_row($item, $level, $items_by_parent) {
$has_children = isset($items_by_parent[$item['id']]);
$is_backpack = !empty($item['backpack_id']);
$is_compartment = !empty($item['backpack_compartment_id']);
$bg_class = "";
$text_class = "";
$icon = "";
if ($is_backpack) {
$bg_class = "table-success";
$text_class = "fw-bold text-uppercase";
$icon = '<i class="fas fa-hiking me-2 text-success"></i>';
} elseif ($is_compartment) {
$bg_class = "table-light";
$text_class = "fw-bold fst-italic text-muted";
// Check if linked article (image present)
if (!empty($item['image_url'])) {
$img_src = htmlspecialchars($item['image_url']);
$icon = '<img src="' . $img_src . '" class="item-image me-2 article-image-trigger" data-preview-url="' . $img_src . '">';
} else {
$icon = '<i class="fas fa-folder-open me-2 text-warning"></i>';
}
} else {
$img_src = !empty($item['image_url']) ? htmlspecialchars($item['image_url']) : 'assets/images/keinbild.png';
$icon = '<img src="' . $img_src . '" class="item-image me-2 article-image-trigger" data-preview-url="' . $img_src . '">';
}
$indent_px = $level * 25;
$weight_display = $item['weight_grams'] > 0 ? number_format($item['weight_grams'], 0, ',', '.') . ' g' : '-';
$total_weight_val = 0;
if ($is_backpack || $is_compartment) {
$children_weight = get_recursive_weight($item['id'], $items_by_parent);
$own_weight = $item['quantity'] * $item['weight_grams'];
$total_weight_val = $own_weight + $children_weight;
} else {
$total_weight_val = $item['weight_grams'] * $item['quantity'];
}
$total_weight_display = ($total_weight_val > 0) ? number_format($total_weight_val, 0, ',', '.') . ' g' : '-';
echo '<tr class="' . $bg_class . '" data-id="' . $item['id'] . '" data-parent-id="' . ($item['parent_packing_list_item_id'] ?: 0) . '">';
echo '<td>';
echo '<div style="padding-left: ' . $indent_px . 'px; display: flex; align-items: center;">';
if ($has_children) {
echo '<button type="button" class="btn btn-sm p-0 me-2 border-0 bg-transparent text-primary toggle-tree-btn" data-target-id="' . $item['id'] . '" style="width:20px; cursor:pointer;"><i class="fas fa-chevron-down"></i></button>';
} else {
echo '<span style="width: 20px; display: inline-block; margin-right: 0.5rem;"></span>';
}
echo $icon;
echo '<span class="' . $text_class . '">' . htmlspecialchars($item['article_name']) . '</span>';
echo '</div>';
echo '</td>';
echo '<td class="text-center">' . ($item['consumable'] ? '<i class="fas fa-cookie-bite text-warning" title="Verbrauch"></i>' : '') . '</td>';
echo '<td>' . htmlspecialchars($item['manufacturer_name'] ?: '') . '</td>';
echo '<td>' . htmlspecialchars($item['product_designation'] ?: '') . '</td>';
echo '<td>' . htmlspecialchars($item['category_name'] ?: '') . '</td>';
echo '<td class="text-center">';
if ($is_backpack) {
echo '';
} elseif ($is_compartment) {
$total_items = get_recursive_quantity($item['id'], $items_by_parent);
if ($total_items > 0) {
echo '<span class="badge bg-success text-white border border-success" title="Enthält ' . $total_items . ' Artikel">' . $total_items . '</span>';
}
} else {
echo '<span class="badge bg-white text-dark border">' . $item['quantity'] . 'x</span>';
}
echo '</td>';
echo '<td class="text-end text-muted">' . $weight_display . '</td>';
echo '<td class="text-end fw-bold">' . $total_weight_display . '</td>';
echo '</tr>';
if ($has_children) {
foreach ($items_by_parent[$item['id']] as $child) {
render_item_row($child, $level + 1, $items_by_parent);
}
}
}
?>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
<div class="screen-view">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h1 class="h4 mb-0"><i class="fas fa-clipboard-list me-2"></i>Packliste: <?php echo htmlspecialchars($packing_list['name']); ?></h1>
<div class="print-hide">
<?php if ($packing_list['user_id'] == $current_user_id || (!empty($packing_list['household_id']) && $packing_list['household_id'] == $current_user_household_id)): ?>
<a href="manage_packing_list_items.php?id=<?php echo $packing_list_id; ?>" class="btn btn-outline-light btn-sm"><i class="fas fa-edit me-2"></i>Bearbeiten</a>
<?php endif; ?>
<a href="packing_lists.php" class="btn btn-outline-light btn-sm ms-2"><i class="fas fa-arrow-left me-2"></i>Zur Übersicht</a>
<a href="print_packing_list.php?id=<?php echo $packing_list_id; ?>" target="_blank" class="btn btn-outline-light btn-sm ms-2"><i class="fas fa-print me-2"></i>Drucken</a>
</div>
</div>
</div>
<?php if ($table_items_count > 0): ?>
<div class="alert alert-warning print-hide">
<i class="fas fa-exclamation-triangle me-2"></i><strong>Achtung:</strong> Auf dem Tisch liegen noch Artikel, die keinem Rucksack und keinem Träger zugewiesen sind! Diese fließen nicht in die Statistik oder den Druck ein.
<?php if ($packing_list['user_id'] == $current_user_id || (!empty($packing_list['household_id']) && $packing_list['household_id'] == $current_user_household_id)): ?>
<a href="manage_packing_list_items.php?id=<?php echo $packing_list_id; ?>&phase=2" class="alert-link ms-2">Jetzt zuweisen <i class="fas fa-arrow-right"></i></a>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="row">
<div class="col-lg-8 mb-4 mb-lg-0 d-flex flex-column">
<div class="card flex-grow-1">
<div class="card-header"><h5 class="mb-0"><i class="fas fa-box-open me-2"></i>Gepackte Artikel</h5></div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover table-sm mb-0 align-middle">
<thead class="table-light">
<tr>
<th>Artikel / Struktur</th>
<th class="text-center" style="width: 30px;"><i class="fas fa-cookie-bite"></i></th>
<th>Hersteller</th>
<th>Modell</th>
<th>Kategorie</th>
<th class="text-center">Anz.</th>
<th class="text-end">Gewicht</th>
<th class="text-end">Gesamt</th>
</tr>
</thead>
<tbody>
<?php if (empty($items_by_parent[0])): ?>
<tr><td colspan="8" class="text-center p-4 text-muted">Liste ist leer.</td></tr>
<?php else: ?>
<?php
$items_by_carrier = [];
foreach ($items_by_parent[0] as $root) {
$c = $root['carrier_name'] ?: 'Sonstiges';
$items_by_carrier[$c][] = $root;
}
foreach ($items_by_carrier as $carrier => $roots): ?>
<tr class="table-secondary border-bottom border-white">
<td colspan="8" class="fw-bold text-uppercase ps-3 py-2">
<i class="fas fa-user-circle me-2"></i>
<a href="#" class="text-dark text-decoration-none carrier-stats-link" data-carrier="<?php echo htmlspecialchars($carrier); ?>">
<?php echo htmlspecialchars($carrier); ?>
<small class="text-muted ms-2 font-weight-normal">(Klicken für Statistik)</small>
</a>
</td>
</tr>
<?php foreach ($roots as $root_item):
render_item_row($root_item, 0, $items_by_parent);
endforeach; ?>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<?php if (!empty($packing_list['todo_list_id'])): ?>
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center"><h5 class="mb-0"><i class="fas fa-list-check me-2"></i>ToDo: <?php echo htmlspecialchars($todo_list_name); ?></h5></div>
<div class="card-body p-0">
<ul class="list-group list-group-flush">
<?php foreach ($todo_items as $item): ?>
<li class="list-group-item d-flex justify-content-between align-items-center py-2 px-3">
<form method="post" style="display:inline; margin:0;" class="flex-grow-1">
<input type="hidden" name="item_id" value="<?php echo $item['id']; ?>">
<div class="form-check d-flex align-items-center">
<input class="form-check-input me-2" type="checkbox" onChange="this.form.submit()" name="status" value="<?php echo $item['is_completed'] ? '0' : '1'; ?>" <?php echo $item['is_completed'] ? 'checked' : ''; ?> style="width:1.3em; height:1.3em; cursor:pointer;">
<input type="hidden" name="toggle_todo_item" value="1">
<label class="form-check-label ms-2 <?php echo $item['is_completed'] ? 'text-decoration-line-through text-muted' : ''; ?>" style="cursor:pointer; width:100%; margin-top:2px;">
<?php echo htmlspecialchars($item['title']); ?>
</label>
</div>
</form>
</li>
<?php endforeach; ?>
<?php if (empty($todo_items)): ?>
<li class="list-group-item text-muted text-center py-3">Keine Einträge in dieser Liste.</li>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<div class="card h-100">
<div class="card-header card-header-stats"><h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>Statistiken</h5></div>
<div class="card-body">
<div class="stats-table-container mb-4">
<h6>Gewicht pro Träger</h6>
<ul class="list-group list-group-flush bg-transparent">
<?php foreach ($weight_by_carrier as $carrier => $weight): ?>
<li class="list-group-item px-1 py-2 bg-transparent">
<strong><?php echo htmlspecialchars($carrier); ?></strong>
<div class="d-flex justify-content-between align-items-center text-muted small mt-1">
Gesamt:<span class="badge bg-success rounded-pill"><?php echo number_format($weight, 0, ',', '.'); ?> g</span>
</div>
<div class="d-flex justify-content-between align-items-center text-muted small">
Basis (o. Verbr.):<span class="badge bg-secondary rounded-pill"><?php echo number_format($weight_by_carrier_non_consumable[$carrier] ?? 0, 0, ',', '.'); ?> g</span>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
<div class="row">
<div class="col-6"><h6 class="text-center small">nach Kategorie</h6><div class="chart-container"><canvas id="categoryWeightChart"></canvas></div></div>
<div class="col-6"><h6 class="text-center small">nach Träger</h6><div class="chart-container"><canvas id="carrierWeightChart"></canvas></div></div>
</div>
<div class="stats-table-container mt-4">
<h6>Gewicht nach Kategorie</h6>
<div class="table-responsive">
<table id="category-weight-table" class="table table-sm table-hover mb-0">
<tbody>
<?php
arsort($weight_by_category);
foreach($weight_by_category as $cat => $w):
?>
<tr>
<td><?php echo htmlspecialchars($cat); ?></td>
<td class="text-end"><?php echo number_format($w, 0, ',', '.'); ?> g</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal for Carrier Statistics -->
<div class="modal fade" id="carrierStatsModal" tabindex="-1" aria-labelledby="carrierStatsModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="carrierStatsModalLabel">Statistik für Träger</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<div class="modal-body">
<div id="carrier-stats-content">
<!-- Populated via JS -->
</div>
</div>
</div>
</div>
</div>
<div id="image-preview-tooltip"></div>
<script>
// Pass PHP Data to JS
const carrierStatsData = <?php echo json_encode($carrier_stats_details); ?>;
document.addEventListener('DOMContentLoaded', function() {
// Tooltip logic with EVENT DELEGATION
const tooltip = document.getElementById('image-preview-tooltip');
if (tooltip) {
document.body.addEventListener('mouseover', function(e) {
if (e.target.classList.contains('article-image-trigger')) {
const url = e.target.getAttribute('data-preview-url');
if (url && !url.endsWith('assets/images/keinbild.png')) {
tooltip.style.backgroundImage = `url('${url}')`;
tooltip.style.display = 'block';
}
}
});
document.body.addEventListener('mousemove', function(e) {
if (tooltip.style.display === 'block') {
tooltip.style.left = e.pageX + 15 + 'px';
tooltip.style.top = e.pageY + 15 + 'px';
}
});
document.body.addEventListener('mouseout', function(e) {
if (e.target.classList.contains('article-image-trigger')) {
tooltip.style.display = 'none';
}
});
}
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) });
// Chart logic (PURE GREEN TONES - High Contrast)
Chart.register(ChartDataLabels);
const greenColors = [
'#2e7d32', // Dark Forest Green
'#4caf50', // Standard Green
'#81c784', // Light Pastel Green
'#a5d6a7', // Very Light Green
'#1b5e20', // Deep Dark Green
'#66bb6a', // Medium Green
'#00e676', // Neon Green (Accent)
'#c8e6c9', // White-Green
'#00c853', // Vibrant Green
'#69f0ae', // Soft Neon
'#388e3c' // Solid Green
];
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
layout: {
padding: 20 // Add padding to prevent cutting off hover
},
plugins: {
legend: { display: false },
datalabels: { display: false }
}
};
const catCtx = document.getElementById('categoryWeightChart');
if (catCtx) {
new Chart(catCtx, { type: 'doughnut', data: { labels: <?php echo json_encode(array_keys($weight_by_category)); ?>, datasets: [{ data: <?php echo json_encode(array_values($weight_by_category)); ?>, backgroundColor: greenColors, borderWidth: 0, hoverOffset: 20 }] }, options: chartOptions });
}
const carrierCtx = document.getElementById('carrierWeightChart');
if (carrierCtx) {
new Chart(carrierCtx, { type: 'doughnut', data: { labels: <?php echo json_encode(array_keys($weight_by_carrier)); ?>, datasets: [{ data: <?php echo json_encode(array_values($weight_by_carrier)); ?>, backgroundColor: greenColors, borderWidth: 0, hoverOffset: 20 }] }, options: chartOptions });
}
// Collapsible Tree Logic
document.querySelectorAll('.toggle-tree-btn').forEach(btn => {
btn.addEventListener('click', function() {
const parentId = this.getAttribute('data-target-id');
const icon = this.querySelector('i');
const isExpanded = icon.classList.contains('fa-chevron-down');
icon.classList.toggle('fa-chevron-down');
icon.classList.toggle('fa-chevron-right');
toggleChildren(parentId, !isExpanded);
});
});
function toggleChildren(parentId, show) {
const directChildren = document.querySelectorAll(`tr[data-parent-id="${parentId}"]`);
directChildren.forEach(row => {
row.style.display = show ? '' : 'none';
const rowId = row.getAttribute('data-id');
if (!show) {
toggleChildren(rowId, false);
} else {
const btn = row.querySelector('.toggle-tree-btn');
if (btn) {
const isExpanded = btn.querySelector('i').classList.contains('fa-chevron-down');
if (isExpanded) {
toggleChildren(rowId, true);
}
}
}
});
}
// Carrier Stats Modal Logic
const statsModal = new bootstrap.Modal(document.getElementById('carrierStatsModal'));
document.querySelectorAll('.carrier-stats-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const carrierName = this.getAttribute('data-carrier');
const data = carrierStatsData[carrierName];
if (data) {
document.getElementById('carrierStatsModalLabel').innerText = 'Statistik für ' + carrierName;
// Format Numbers
const fmt = (n) => new Intl.NumberFormat('de-DE').format(n) + ' g';
let catRows = '';
// Sort categories by weight desc
const sortedCats = Object.entries(data.categories).sort((a, b) => b[1] - a[1]);
sortedCats.forEach(([cat, w]) => {
catRows += `<tr><td>${cat}</td><td class="text-end">${fmt(w)}</td></tr>`;
});
const content = `
<div class="row mb-3">
<div class="col-6">
<div class="card text-center bg-light h-100">
<div class="card-body py-2">
<small class="text-muted">Gesamtgewicht</small>
<h5 class="card-title mb-0 text-primary">${fmt(data.total_weight)}</h5>
</div>
</div>
</div>
<div class="col-6">
<div class="card text-center bg-light h-100">
<div class="card-body py-2">
<small class="text-muted">Basisgewicht (o. Verb.)</small>
<h5 class="card-title mb-0 text-success">${fmt(data.base_weight)}</h5>
</div>
</div>
</div>
</div>
<h6 class="border-bottom pb-2">Kategorien</h6>
<table class="table table-sm table-striped">
<tbody>${catRows}</tbody>
</table>
`;
document.getElementById('carrier-stats-content').innerHTML = content;
statsModal.show();
}
});
});
});
</script>
<?php require_once 'footer.php'; ?>