Feature: Rucksack-Link, Packlisten-Validierung und Detail-Statistiken
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 28s
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 28s
- backpacks.php: Button für Hersteller-Link hinzugefügt. - add_packing_list.php: Validierung gegen doppelte Rucksack-Zuweisung (JS & PHP). - packing_list_detail.php: Charts auf Grüntöne umgestellt, Modal für Träger-Statistiken hinzugefügt.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// add_packing_list.php - Neue Packliste hinzufügen
|
||||
// KORREKTUR: Die Verarbeitungslogik wurde an den Anfang der Datei verschoben, um die Weiterleitung zu reparieren.
|
||||
// VALIDIERUNG: Verhindert doppelte Rucksack-Zuweisung (Server & Client)
|
||||
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
@@ -12,7 +12,7 @@ if (!isset($_SESSION['user_id'])) {
|
||||
|
||||
require_once 'db_connect.php';
|
||||
require_once 'household_actions.php';
|
||||
require_once 'backpack_utils.php'; // Shared backpack functions
|
||||
require_once 'backpack_utils.php';
|
||||
|
||||
$current_user_id = $_SESSION['user_id'];
|
||||
$message = '';
|
||||
@@ -32,7 +32,6 @@ if ($household_id_for_user) {
|
||||
$stmt_u = $conn->prepare("SELECT id, username FROM users WHERE household_id = ?");
|
||||
$stmt_u->bind_param("i", $household_id_for_user);
|
||||
} else {
|
||||
// Just the current user
|
||||
$stmt_u = $conn->prepare("SELECT id, username FROM users WHERE id = ?");
|
||||
$stmt_u->bind_param("i", $current_user_id);
|
||||
}
|
||||
@@ -48,8 +47,28 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
$description = trim($_POST['description']);
|
||||
$household_id = isset($_POST['is_household_list']) && $household_id_for_user ? $household_id_for_user : NULL;
|
||||
|
||||
// Server-Side Validation for duplicate backpacks
|
||||
$selected_backpacks = [];
|
||||
$has_duplicate_backpacks = false;
|
||||
if (isset($_POST['participate']) && is_array($_POST['participate'])) {
|
||||
foreach ($_POST['participate'] as $uid => $val) {
|
||||
if ($val) {
|
||||
$bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0;
|
||||
if ($bid > 0) {
|
||||
if (in_array($bid, $selected_backpacks)) {
|
||||
$has_duplicate_backpacks = true;
|
||||
break;
|
||||
}
|
||||
$selected_backpacks[] = $bid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($name)) {
|
||||
$message = '<div class="alert alert-danger" role="alert">Der Packlistenname darf nicht leer sein.</div>';
|
||||
} elseif ($has_duplicate_backpacks) {
|
||||
$message = '<div class="alert alert-danger" role="alert">Fehler: Ein Rucksack kann nicht mehreren Personen zugewiesen werden. Bitte korrigieren Sie die Auswahl.</div>';
|
||||
} else {
|
||||
$stmt = $conn->prepare("INSERT INTO packing_lists (user_id, household_id, name, description) VALUES (?, ?, ?, ?)");
|
||||
if ($stmt === false) {
|
||||
@@ -59,19 +78,11 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
if ($stmt->execute()) {
|
||||
$new_list_id = $conn->insert_id;
|
||||
|
||||
// Handle Backpacks and Participation
|
||||
if (isset($_POST['participate']) && is_array($_POST['participate'])) {
|
||||
foreach ($_POST['participate'] as $uid => $val) {
|
||||
$uid = intval($uid);
|
||||
// Only add if checked (value is usually '1')
|
||||
if ($val) {
|
||||
$bid = isset($_POST['backpacks'][$uid]) ? intval($_POST['backpacks'][$uid]) : 0;
|
||||
|
||||
// Insert Carrier (even if no backpack is assigned, they are a carrier on the list)
|
||||
// But DB schema: packing_list_carriers links user to list. Backpack is optional.
|
||||
// If we want them on the list, we insert.
|
||||
|
||||
// Check if $bid is valid (>0)
|
||||
$bid_to_insert = ($bid > 0) ? $bid : NULL;
|
||||
|
||||
$stmt_in = $conn->prepare("INSERT INTO packing_list_carriers (packing_list_id, user_id, backpack_id) VALUES (?, ?, ?)");
|
||||
@@ -84,10 +95,6 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no array (e.g. single user form without checkboxes?), usually we force current user?
|
||||
// Or assume no one selected. Let's ensure Current User is added if not explicitly unchecked?
|
||||
// Actually, the UI below will default to checked for everyone.
|
||||
}
|
||||
|
||||
if ($household_id_for_user) {
|
||||
@@ -117,7 +124,7 @@ require_once 'header.php';
|
||||
<div class="card-body p-4">
|
||||
<?php if(!empty($message)) echo $message; ?>
|
||||
|
||||
<form action="add_packing_list.php" method="post">
|
||||
<form action="add_packing_list.php" method="post" id="createListForm">
|
||||
<div class="row">
|
||||
<div class="col-md-7">
|
||||
<div class="mb-3">
|
||||
@@ -142,17 +149,18 @@ require_once 'header.php';
|
||||
<div class="card bg-light border-0">
|
||||
<div class="card-body">
|
||||
<p class="small text-muted">Wähle aus, wer mitkommt und wer welchen Rucksack trägt.</p>
|
||||
<div id="backpack-warning" class="alert alert-warning d-none small p-2 mb-2">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i> Achtung: Ein Rucksack wurde mehrfach ausgewählt!
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$user_backpacks_json = [];
|
||||
$all_assigned_backpack_ids = []; // Empty for new list
|
||||
$all_assigned_backpack_ids = [];
|
||||
|
||||
foreach ($available_users as $user):
|
||||
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $household_id_for_user);
|
||||
// Pre-select: None (0)
|
||||
$current_bp_id = 0;
|
||||
|
||||
// Store for JS
|
||||
$user_backpacks_json[$user['id']] = [
|
||||
'current_id' => $current_bp_id,
|
||||
'backpacks' => $user_backpacks
|
||||
@@ -160,32 +168,103 @@ require_once 'header.php';
|
||||
?>
|
||||
<div class="mb-3 pb-3 border-bottom">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" name="participate[<?php echo $user['id']; ?>]" value="1" checked id="part_<?php echo $user['id']; ?>">
|
||||
<input class="form-check-input participation-check" type="checkbox" name="participate[<?php echo $user['id']; ?>]" value="1" checked id="part_<?php echo $user['id']; ?>">
|
||||
<label class="form-check-label fw-bold" for="part_<?php echo $user['id']; ?>">
|
||||
<?php echo htmlspecialchars($user['username']); ?>
|
||||
</label>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<!-- Pass class for JS validation -->
|
||||
<?php echo render_backpack_card_selector($user, $current_bp_id, $user_backpacks); ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary"><i class="fas fa-plus-circle me-2"></i>Packliste erstellen</button>
|
||||
<a href="packing_lists.php" class="btn btn-secondary"><i class="fas fa-arrow-left me-2"></i>Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn"><i class="fas fa-plus-circle me-2"></i>Packliste erstellen</button>
|
||||
<a href="packing_lists.php" class="btn btn-secondary"><i class="fas fa-arrow-left me-2"></i>Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Script for Validation -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('createListForm');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const warning = document.getElementById('backpack-warning');
|
||||
|
||||
// Helper function to check duplicates
|
||||
function validateBackpacks() {
|
||||
const selected = [];
|
||||
let hasDuplicate = false;
|
||||
|
||||
// Find all hidden inputs that store the backpack ID (usually named backpacks[UserID])
|
||||
// Since render_backpack_card_selector uses a hidden input with name="backpacks[ID]", we select those
|
||||
const inputs = document.querySelectorAll('input[name^="backpacks["]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
// Check if user participates
|
||||
// The input name is backpacks[123], we need to find checkbox participate[123]
|
||||
const userIdMatch = input.name.match(/backpacks\[(\d+)\]/);
|
||||
if (userIdMatch) {
|
||||
const userId = userIdMatch[1];
|
||||
const partCheck = document.getElementById('part_' + userId);
|
||||
|
||||
if (partCheck && partCheck.checked) {
|
||||
const val = parseInt(input.value);
|
||||
if (val > 0) {
|
||||
if (selected.includes(val)) {
|
||||
hasDuplicate = true;
|
||||
}
|
||||
selected.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hasDuplicate) {
|
||||
warning.classList.remove('d-none');
|
||||
submitBtn.disabled = true;
|
||||
} else {
|
||||
warning.classList.add('d-none');
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Observe changes.
|
||||
// The modal script updates the hidden input. We can listen to a custom event or use MutationObserver.
|
||||
// Or simply listen to clicks on the "Select" buttons in the modal, but the modal logic is separate.
|
||||
// However, the `render_backpack_modal_script` updates the UI and the hidden input.
|
||||
// We can attach a MutationObserver to the form to detect value changes or DOM changes in the backpack cards.
|
||||
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
validateBackpacks();
|
||||
});
|
||||
|
||||
observer.observe(document.querySelector('.card-body'), {
|
||||
subtree: true,
|
||||
attributes: true, // value change might not trigger attribute change in DOM for inputs, but innerHTML changes of cards do
|
||||
childList: true
|
||||
});
|
||||
|
||||
// Also listen to participation checkboxes
|
||||
document.querySelectorAll('.participation-check').forEach(chk => {
|
||||
chk.addEventListener('change', validateBackpacks);
|
||||
});
|
||||
|
||||
// Initial check
|
||||
validateBackpacks();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php echo render_backpack_modal_script($user_backpacks_json, $all_assigned_backpack_ids); ?>
|
||||
|
||||
<!-- Render Modal JS -->
|
||||
<?php echo render_backpack_modal_script($user_backpacks_json, $all_assigned_backpack_ids); ?>
|
||||
|
||||
<?php require_once 'footer.php'; ?>
|
||||
<?php require_once 'footer.php'; ?>
|
||||
@@ -33,7 +33,6 @@ if (isset($_POST['delete_backpack_id'])) {
|
||||
}
|
||||
|
||||
// Fetch Backpacks (Personal + Household)
|
||||
// Logic: Show my backpacks AND backpacks from my household (if I'm in one)
|
||||
$household_id = null;
|
||||
$stmt_hh = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
|
||||
$stmt_hh->bind_param("i", $user_id);
|
||||
@@ -44,6 +43,11 @@ if ($row = $res_hh->fetch_assoc()) {
|
||||
}
|
||||
|
||||
$backpacks = [];
|
||||
// AUTO-MIGRATION: Check if product_url column exists (just to be safe for display)
|
||||
// Ideally handled in edit_backpack but good to be safe.
|
||||
$check_col = $conn->query("SHOW COLUMNS FROM backpacks LIKE 'product_url'");
|
||||
$has_product_url = ($check_col && $check_col->num_rows > 0);
|
||||
|
||||
$sql = "SELECT b.*, u.username as owner_name
|
||||
FROM backpacks b
|
||||
JOIN users u ON b.user_id = u.id
|
||||
@@ -65,7 +69,6 @@ $result = $stmt->get_result();
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$backpacks[] = $row;
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="card">
|
||||
@@ -112,7 +115,6 @@ while ($row = $result->fetch_assoc()) {
|
||||
|
||||
<!-- Compartments Preview -->
|
||||
<?php
|
||||
// Fetch compartment count
|
||||
$stmt_c = $conn->prepare("SELECT COUNT(*) as cnt FROM backpack_compartments WHERE backpack_id = ?");
|
||||
$stmt_c->bind_param("i", $bp['id']);
|
||||
$stmt_c->execute();
|
||||
@@ -121,16 +123,24 @@ while ($row = $result->fetch_assoc()) {
|
||||
<p class="small text-muted"><i class="fas fa-layer-group me-1"></i> <?php echo $cnt; ?> Fächer definiert</p>
|
||||
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-top-0 d-flex justify-content-end gap-2">
|
||||
<?php if ($bp['user_id'] == $user_id): ?>
|
||||
<a href="edit_backpack.php?id=<?php echo $bp['id']; ?>" class="btn btn-sm btn-outline-primary"><i class="fas fa-edit"></i> Bearbeiten</a>
|
||||
<form method="post" onsubmit="return confirm('Rucksack wirklich löschen?');" class="d-inline">
|
||||
<input type="hidden" name="delete_backpack_id" value="<?php echo $bp['id']; ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<button class="btn btn-sm btn-outline-secondary" disabled>Nur Eigentümer kann bearbeiten</button>
|
||||
<?php endif; ?>
|
||||
<div class="card-footer bg-transparent border-top-0 d-flex justify-content-between align-items-center gap-2">
|
||||
<div>
|
||||
<?php if (!empty($bp['product_url'])): ?>
|
||||
<a href="<?php echo htmlspecialchars($bp['product_url']); ?>" target="_blank" class="btn btn-sm btn-outline-info" title="Herstellerseite öffnen"><i class="fas fa-external-link-alt"></i> Info</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-1">
|
||||
<?php if ($bp['user_id'] == $user_id): ?>
|
||||
<a href="edit_backpack.php?id=<?php echo $bp['id']; ?>" class="btn btn-sm btn-outline-primary"><i class="fas fa-edit"></i></a>
|
||||
<form method="post" onsubmit="return confirm('Rucksack wirklich löschen?');" class="d-inline">
|
||||
<input type="hidden" name="delete_backpack_id" value="<?php echo $bp['id']; ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"><i class="fas fa-trash"></i></button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<button class="btn btn-sm btn-outline-secondary" disabled><i class="fas fa-lock"></i></button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,4 +150,4 @@ while ($row = $result->fetch_assoc()) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once 'footer.php'; ?>
|
||||
<?php require_once 'footer.php'; ?>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
// packing_list_detail.php - Detailansicht einer Packliste
|
||||
// FINALE, STABILE VERSION: Mit modernem Tree-View, Toggle-Funktion und externem CSS.
|
||||
// FINALE, STABILE VERSION: Mit modernem Tree-View, Toggle-Funktion, Grünen Charts & Träger-Modal
|
||||
|
||||
if (session_status() == PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
@@ -21,6 +21,9 @@ $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.");
|
||||
}
|
||||
@@ -57,8 +60,6 @@ $stmt_list_owner->close();
|
||||
|
||||
$page_title = "Packliste: " . htmlspecialchars($packing_list['name']);
|
||||
|
||||
// Robust SQL: Fetches names from Backpacks/Compartments if article is missing
|
||||
// FIX: Include backpack own weight in weight_grams calculation
|
||||
$sql = "SELECT
|
||||
pli.id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id,
|
||||
pli.backpack_id, pli.backpack_compartment_id,
|
||||
@@ -67,7 +68,8 @@ $sql = "SELECT
|
||||
a.image_url, a.product_designation, a.consumable,
|
||||
c.name AS category_name,
|
||||
m.name AS manufacturer_name,
|
||||
u.username AS carrier_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
|
||||
@@ -87,9 +89,6 @@ $items_by_id = [];
|
||||
$items_by_parent = [];
|
||||
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
// Fix Names for Display: Removed redundant prefix logic
|
||||
// if ($row['backpack_id']) $row['article_name'] = "Rucksack: " . $row['article_name'];
|
||||
|
||||
$items_by_id[$row['id']] = $row;
|
||||
$parent_id = $row['parent_packing_list_item_id'] ?: 0;
|
||||
if (!isset($items_by_parent[$parent_id])) {
|
||||
@@ -102,6 +101,9 @@ while ($row = $result->fetch_assoc()) {
|
||||
$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;
|
||||
|
||||
@@ -115,13 +117,33 @@ while ($row = $result->fetch_assoc()) {
|
||||
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 for recursive counting
|
||||
// Helper functions (same as before)
|
||||
function get_recursive_quantity($parent_id, $items_by_parent) {
|
||||
$count = 0;
|
||||
if (isset($items_by_parent[$parent_id])) {
|
||||
@@ -133,41 +155,35 @@ function get_recursive_quantity($parent_id, $items_by_parent) {
|
||||
return $count;
|
||||
}
|
||||
|
||||
// Helper for recursive weight calculation
|
||||
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) {
|
||||
// Child weight
|
||||
$weight += ($child['quantity'] * $child['weight_grams']);
|
||||
// Plus its descendants
|
||||
$weight += get_recursive_weight($child['id'], $items_by_parent);
|
||||
}
|
||||
}
|
||||
return $weight;
|
||||
}
|
||||
|
||||
// Recursive Rendering
|
||||
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']);
|
||||
|
||||
// Visual Styles
|
||||
$bg_class = "";
|
||||
$text_class = "";
|
||||
$icon = "";
|
||||
|
||||
if ($is_backpack) {
|
||||
$bg_class = "table-success"; // Greenish for 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"; // Light gray for Compartment
|
||||
$bg_class = "table-light";
|
||||
$text_class = "fw-bold fst-italic text-muted";
|
||||
$icon = '<i class="fas fa-folder-open me-2 text-warning"></i>';
|
||||
} else {
|
||||
// Standard Item
|
||||
$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 . '">';
|
||||
}
|
||||
@@ -175,16 +191,12 @@ function render_item_row($item, $level, $items_by_parent) {
|
||||
$indent_px = $level * 25;
|
||||
$weight_display = $item['weight_grams'] > 0 ? number_format($item['weight_grams'], 0, ',', '.') . ' g' : '-';
|
||||
|
||||
// Calculate Total Weight display logic
|
||||
$total_weight_val = 0;
|
||||
if ($is_backpack || $is_compartment) {
|
||||
// For containers: Recursive weight of children
|
||||
// For backpacks specifically: Add own weight + children
|
||||
$children_weight = get_recursive_weight($item['id'], $items_by_parent);
|
||||
$own_weight = $item['quantity'] * $item['weight_grams']; // Usually 1 * empty_weight
|
||||
$own_weight = $item['quantity'] * $item['weight_grams'];
|
||||
$total_weight_val = $own_weight + $children_weight;
|
||||
} else {
|
||||
// Standard items
|
||||
$total_weight_val = $item['weight_grams'] * $item['quantity'];
|
||||
}
|
||||
|
||||
@@ -192,13 +204,10 @@ function render_item_row($item, $level, $items_by_parent) {
|
||||
|
||||
echo '<tr class="' . $bg_class . '" data-id="' . $item['id'] . '" data-parent-id="' . ($item['parent_packing_list_item_id'] ?: 0) . '">';
|
||||
|
||||
// Name Column with Indentation
|
||||
echo '<td>';
|
||||
echo '<div style="padding-left: ' . $indent_px . 'px; display: flex; align-items: center;">';
|
||||
|
||||
// Tree Toggle or Spacer
|
||||
if ($has_children) {
|
||||
// Explicitly style the button
|
||||
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>';
|
||||
@@ -209,26 +218,20 @@ function render_item_row($item, $level, $items_by_parent) {
|
||||
echo '</div>';
|
||||
echo '</td>';
|
||||
|
||||
// Other Columns
|
||||
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>';
|
||||
|
||||
// Quantity / Child Count Badge
|
||||
echo '<td class="text-center">';
|
||||
if ($is_backpack) {
|
||||
// Rucksack: Keine Summe anzeigen (User Wunsch)
|
||||
echo '';
|
||||
} elseif ($is_compartment) {
|
||||
// Fächer: Rekursive Summe aller enthaltenen Artikel
|
||||
$total_items = get_recursive_quantity($item['id'], $items_by_parent);
|
||||
if ($total_items > 0) {
|
||||
// Grün, aber gleiche Form wie Standard (kein rounded-pill)
|
||||
echo '<span class="badge bg-success text-white border border-success" title="Enthält ' . $total_items . ' Artikel">' . $total_items . '</span>';
|
||||
}
|
||||
} else {
|
||||
// Standard Artikel Menge
|
||||
echo '<span class="badge bg-white text-dark border">' . $item['quantity'] . 'x</span>';
|
||||
}
|
||||
echo '</td>';
|
||||
@@ -244,15 +247,6 @@ function render_item_row($item, $level, $items_by_parent) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function render_print_table_rows($items, $level = 0) {
|
||||
foreach($items as $item) {
|
||||
// Simulate structure for print function which might expect different array format?
|
||||
// Actually, this function is called below with items from $sorted_items_by_carrier which likely won't work
|
||||
// because we rewrote the main fetching logic to flattened array $items_by_parent.
|
||||
// We need to adapt the print view section below to use $items_by_parent logic.
|
||||
}
|
||||
}
|
||||
?>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
|
||||
@@ -294,7 +288,6 @@ function render_print_table_rows($items, $level = 0) {
|
||||
<tr><td colspan="8" class="text-center p-4 text-muted">Liste ist leer.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php
|
||||
// Group roots by carrier
|
||||
$items_by_carrier = [];
|
||||
foreach ($items_by_parent[0] as $root) {
|
||||
$c = $root['carrier_name'] ?: 'Sonstiges';
|
||||
@@ -303,7 +296,13 @@ function render_print_table_rows($items, $level = 0) {
|
||||
|
||||
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><?php echo htmlspecialchars($carrier); ?></td>
|
||||
<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);
|
||||
@@ -341,7 +340,6 @@ function render_print_table_rows($items, $level = 0) {
|
||||
<div class="col-6"><h6 class="text-center small">nach Träger</h6><div class="chart-container"><canvas id="carrierWeightChart"></canvas></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Re-added Category Weight Table -->
|
||||
<div class="stats-table-container mt-4">
|
||||
<h6>Gewicht nach Kategorie</h6>
|
||||
<div class="table-responsive">
|
||||
@@ -366,8 +364,29 @@ function render_print_table_rows($items, $level = 0) {
|
||||
</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
|
||||
const tooltip = document.getElementById('image-preview-tooltip');
|
||||
@@ -387,18 +406,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) });
|
||||
|
||||
// Chart logic
|
||||
// Chart logic (GREEN TONES)
|
||||
Chart.register(ChartDataLabels);
|
||||
const highContrastColors = ['#00429d', '#4771b2', '#73a2c6', '#a4d3d9', '#deeedb', '#ffc993', '#f48f65', '#d45039', '#930000'];
|
||||
// Green palette ranging from dark to light
|
||||
const greenColors = [
|
||||
'#004d00', '#006600', '#008000', '#009900', '#00b300',
|
||||
'#00cc00', '#00e600', '#1aff1a', '#4dff4d', '#80ff80', '#b3ffb3'
|
||||
];
|
||||
|
||||
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: highContrastColors, borderWidth: 2, hoverOffset: 15 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } } } });
|
||||
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: 2, hoverOffset: 15 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } } } });
|
||||
}
|
||||
|
||||
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: highContrastColors, borderWidth: 2, hoverOffset: 15 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } } } });
|
||||
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: 2, hoverOffset: 15 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { display: false } } } });
|
||||
}
|
||||
|
||||
// Collapsible Tree Logic
|
||||
@@ -433,6 +456,59 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user