Refactor: Flat directory structure, removed Docker files from git, updated DB connection path

This commit is contained in:
Gemini Agent
2025-12-05 02:44:11 +00:00
parent eab7de42a4
commit ae9ab01421
224 changed files with 785 additions and 575 deletions

View File

@@ -54,7 +54,7 @@ function get_available_backpacks_for_user($conn, $target_user_id, $household_id)
}
$bps = [];
$sql_bp = "SELECT id, name, user_id FROM backpacks WHERE user_id = ?";
$sql_bp = "SELECT id, name, user_id, image_url FROM backpacks WHERE user_id = ?";
if ($household_id) {
$sql_bp .= " OR household_id = ?";
$stmt_bp = $conn->prepare($sql_bp);

View File

@@ -95,6 +95,12 @@ while ($row = $result->fetch_assoc()) {
<?php endif; ?>
</div>
<?php if(!empty($bp['image_url'])): ?>
<div class="mb-3 text-center" style="height: 150px; overflow: hidden; background-color: #f8f9fa; border-radius: 5px; display: flex; align-items: center; justify-content: center;">
<img src="<?php echo htmlspecialchars($bp['image_url']); ?>" alt="Rucksackbild" style="max-width: 100%; max-height: 100%; object-fit: contain;">
</div>
<?php endif; ?>
<p class="card-text text-muted small mb-2">
<?php echo htmlspecialchars($bp['manufacturer'] . ' ' . $bp['model']); ?>
</p>

View File

@@ -4,7 +4,7 @@
// NEU: Lade Zugangsdaten aus einer sicheren Konfigurationsdatei.
// Diese Datei sollte außerhalb deines öffentlichen Web-Verzeichnisses liegen!
// z.B. in /var/www/config/packliste.ini
$config_path = __DIR__ . '/../config.ini'; // Annahme: die Datei liegt ein Verzeichnis höher
$config_path = __DIR__ . '/config.ini'; // Pfad für flache Struktur angepasst
if (!file_exists($config_path)) {
die("Kritischer Fehler: Die Konfigurationsdatei wurde nicht gefunden. Bitte erstellen Sie die 'config.ini'.");

394
edit_backpack.php Executable file
View File

@@ -0,0 +1,394 @@
<?php
// Check for POST size limit exceeded immediately
if ($_SERVER['REQUEST_METHOD'] == 'POST' && empty($_POST) && empty($_FILES) && $_SERVER['CONTENT_LENGTH'] > 0) {
$max_size = ini_get('post_max_size');
die("Fehler: Die hochgeladene Datei überschreitet das Server-Limit von $max_size. Bitte wählen Sie ein kleineres Bild.");
}
// edit_backpack.php - Erstellen und Bearbeiten von Rucksäcken und Fächern
$page_title = "Rucksack bearbeiten";
require_once 'db_connect.php';
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
$user_id = $_SESSION['user_id'];
$backpack_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$backpack = null;
$compartments = [];
$message = '';
$image_url = '';
// Check Household
$household_id = null;
$stmt_hh = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_hh->bind_param("i", $user_id);
$stmt_hh->execute();
$res_hh = $stmt_hh->get_result();
if ($row = $res_hh->fetch_assoc()) {
$household_id = $row['household_id'];
}
// Image Upload Config
$upload_dir = 'uploads/images/';
if (!is_dir($upload_dir)) { @mkdir($upload_dir, 0777, true); }
function save_image_from_url($url, $upload_dir) {
$context_options = ["http" => ["header" => "User-Agent: Mozilla/5.0\r\n", "timeout" => 10]];
$context = stream_context_create($context_options);
$image_data = @file_get_contents($url, false, $context);
if ($image_data === false) return [false, "Bild-Download fehlgeschlagen."];
$image_info = @getimagesizefromstring($image_data);
if ($image_info === false) return [false, "Ungültiges Bild."];
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($image_info['mime'], $allowed)) return [false, "Format nicht unterstützt."];
$ext = image_type_to_extension($image_info[2], false);
$name = uniqid('bp_url_', true) . '.' . $ext;
if (file_put_contents($upload_dir . $name, $image_data)) return [true, $upload_dir . $name];
return [false, "Speicherfehler."];
}
function save_image_from_base64($base64_string, $upload_dir) {
if (preg_match('/^data:image\/(\w+);base64,/', $base64_string, $type)) {
$data = substr($base64_string, strpos($base64_string, ',') + 1);
$type = strtolower($type[1]);
if (!in_array($type, ['jpg', 'jpeg', 'png', 'gif'])) return [false, "Format nicht unterstützt."];
$data = base64_decode($data);
if ($data === false) return [false, "Dekodierfehler."];
$name = uniqid('bp_paste_', true) . '.' . $type;
if (file_put_contents($upload_dir . $name, $data)) return [true, $upload_dir . $name];
}
return [false, "Ungültiges Base64."];
}
// Load existing data
if ($backpack_id > 0) {
$stmt = $conn->prepare("SELECT * FROM backpacks WHERE id = ? AND user_id = ?");
$stmt->bind_param("ii", $backpack_id, $user_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$backpack = $result->fetch_assoc();
$image_url = $backpack['image_url'];
// Load Compartments
$stmt_c = $conn->prepare("SELECT * FROM backpack_compartments WHERE backpack_id = ? ORDER BY sort_order ASC");
$stmt_c->bind_param("i", $backpack_id);
$stmt_c->execute();
$res_c = $stmt_c->get_result();
while ($row = $res_c->fetch_assoc()) {
$compartments[] = $row;
}
} else {
$message = '<div class="alert alert-danger">Rucksack nicht gefunden oder keine Berechtigung.</div>';
$backpack_id = 0; // Reset to create mode
}
}
// Handle Form Submission BEFORE loading header
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$name = trim($_POST['name']);
$manufacturer = trim($_POST['manufacturer']);
$model = trim($_POST['model']);
$weight = intval($_POST['weight_grams']);
$volume = intval($_POST['volume_liters']);
$share_household = isset($_POST['share_household']) ? 1 : 0;
// Image Handling
$image_url_for_db = $image_url; // Keep existing by default
$pasted_image = $_POST['pasted_image_data'] ?? '';
$url_image = trim($_POST['image_url_input'] ?? '');
if (!empty($pasted_image)) {
list($ok, $res) = save_image_from_base64($pasted_image, $upload_dir);
if ($ok) {
$image_url_for_db = $res;
} else {
$message .= '<div class="alert alert-warning">Fehler beim Speichern des eingefügten Bildes: ' . htmlspecialchars($res) . '</div>';
}
} elseif (isset($_FILES['image_file']) && $_FILES['image_file']['error'] != UPLOAD_ERR_NO_FILE) {
// User attempted to upload a file
if ($_FILES['image_file']['error'] == UPLOAD_ERR_OK) {
$ext = strtolower(pathinfo($_FILES['image_file']['name'], PATHINFO_EXTENSION));
$allowed_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (in_array($ext, $allowed_exts)) {
$name_file = uniqid('bp_img_', true) . '.' . $ext;
if (move_uploaded_file($_FILES['image_file']['tmp_name'], $upload_dir . $name_file)) {
$image_url_for_db = $upload_dir . $name_file;
} else {
$message .= '<div class="alert alert-danger">Fehler beim Verschieben der Datei. Schreibrechte prüfen.</div>';
}
} else {
$message .= '<div class="alert alert-warning">Ungültiges Dateiformat. Erlaubt: JPG, PNG, GIF, WEBP.</div>';
}
} else {
// Handle upload errors
$err_code = $_FILES['image_file']['error'];
$err_msg = 'Unbekannter Fehler';
switch ($err_code) {
case UPLOAD_ERR_INI_SIZE: $err_msg = 'Datei ist zu groß (php.ini limit).'; break;
case UPLOAD_ERR_FORM_SIZE: $err_msg = 'Datei ist zu groß (HTML form limit).'; break;
case UPLOAD_ERR_PARTIAL: $err_msg = 'Datei wurde nur teilweise hochgeladen.'; break;
case UPLOAD_ERR_NO_TMP_DIR: $err_msg = 'Kein temporärer Ordner gefunden.'; break;
case UPLOAD_ERR_CANT_WRITE: $err_msg = 'Fehler beim Schreiben auf die Festplatte.'; break;
}
$message .= '<div class="alert alert-danger">Upload-Fehler: ' . $err_msg . '</div>';
}
} elseif (!empty($url_image)) {
list($ok, $res) = save_image_from_url($url_image, $upload_dir);
if ($ok) {
$image_url_for_db = $res;
} else {
$message .= '<div class="alert alert-warning">Fehler beim Laden von URL: ' . htmlspecialchars($res) . '</div>';
}
}
$final_household_id = ($share_household && $household_id) ? $household_id : NULL;
if ($backpack_id > 0) {
// Update
$stmt = $conn->prepare("UPDATE backpacks SET name=?, manufacturer=?, model=?, weight_grams=?, volume_liters=?, household_id=?, image_url=? WHERE id=? AND user_id=?");
$stmt->bind_param("sssiissii", $name, $manufacturer, $model, $weight, $volume, $final_household_id, $image_url_for_db, $backpack_id, $user_id);
$stmt->execute();
} else {
// Insert
$stmt = $conn->prepare("INSERT INTO backpacks (user_id, household_id, name, manufacturer, model, weight_grams, volume_liters, image_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("iisssiis", $user_id, $final_household_id, $name, $manufacturer, $model, $weight, $volume, $image_url_for_db);
$stmt->execute();
$backpack_id = $stmt->insert_id;
}
// Handle Compartments
if (isset($_POST['compartment_names'])) {
$comp_names = $_POST['compartment_names'];
$comp_ids = $_POST['compartment_ids'] ?? [];
// Get existing IDs to know what to delete
$existing_ids = [];
if($backpack_id > 0){
$stmt_check = $conn->prepare("SELECT id FROM backpack_compartments WHERE backpack_id = ?");
$stmt_check->bind_param("i", $backpack_id);
$stmt_check->execute();
$res_check = $stmt_check->get_result();
while($row = $res_check->fetch_assoc()) $existing_ids[] = $row['id'];
}
$kept_ids = [];
for ($i = 0; $i < count($comp_names); $i++) {
$c_name = trim($comp_names[$i]);
$c_id = intval($comp_ids[$i] ?? 0);
if (empty($c_name)) continue;
if ($c_id > 0 && in_array($c_id, $existing_ids)) {
// Update
$stmt_up = $conn->prepare("UPDATE backpack_compartments SET name = ?, sort_order = ? WHERE id = ?");
$stmt_up->bind_param("sii", $c_name, $i, $c_id);
$stmt_up->execute();
$kept_ids[] = $c_id;
} else {
// Insert
$stmt_in = $conn->prepare("INSERT INTO backpack_compartments (backpack_id, name, sort_order) VALUES (?, ?, ?)");
$stmt_in->bind_param("isi", $backpack_id, $c_name, $i);
$stmt_in->execute();
}
}
// Delete removed
foreach ($existing_ids as $ex_id) {
if (!in_array($ex_id, $kept_ids)) {
$conn->query("DELETE FROM backpack_compartments WHERE id = $ex_id");
}
}
}
header("Location: backpacks.php");
exit;
}
require_once 'header.php';
?>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h3 class="h5 mb-0"><?php echo $backpack_id > 0 ? 'Rucksack bearbeiten' : 'Neuen Rucksack anlegen'; ?></h3>
</div>
<div class="card-body">
<?php echo $message; ?>
<form method="post" id="backpackForm" enctype="multipart/form-data">
<input type="hidden" name="pasted_image_data" id="pasted_image_data">
<div class="row g-3 mb-4">
<div class="col-md-8">
<div class="row g-3">
<div class="col-md-12">
<label class="form-label">Bezeichnung (Spitzname)</label>
<input type="text" name="name" class="form-control" required value="<?php echo htmlspecialchars($backpack['name'] ?? ''); ?>" placeholder="z.B. Mein großer Trekker">
</div>
<div class="col-md-6">
<label class="form-label">Hersteller</label>
<input type="text" name="manufacturer" class="form-control" value="<?php echo htmlspecialchars($backpack['manufacturer'] ?? ''); ?>">
</div>
<div class="col-md-6">
<label class="form-label">Modell</label>
<input type="text" name="model" class="form-control" value="<?php echo htmlspecialchars($backpack['model'] ?? ''); ?>">
</div>
<div class="col-md-6">
<label class="form-label">Leergewicht (g)</label>
<input type="number" name="weight_grams" class="form-control" value="<?php echo htmlspecialchars($backpack['weight_grams'] ?? 0); ?>">
</div>
<div class="col-md-6">
<label class="form-label">Volumen (Liter)</label>
<input type="number" name="volume_liters" class="form-control" value="<?php echo htmlspecialchars($backpack['volume_liters'] ?? 0); ?>">
</div>
<div class="col-md-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="share_household" name="share_household" <?php echo (!empty($backpack['household_id']) || $backpack_id == 0) ? 'checked' : ''; ?>>
<label class="form-check-label" for="share_household">Mit Haushalt teilen</label>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<label class="form-label">Bild</label>
<div class="card bg-light mb-2">
<div class="card-body text-center p-2">
<img id="imagePreview" src="<?php echo !empty($image_url) ? htmlspecialchars($image_url) : 'keinbild.png'; ?>" class="img-fluid" style="max-height:150px; object-fit:contain;">
</div>
</div>
<input type="file" name="image_file" id="image_file" class="form-control form-control-sm mb-2" accept="image/*">
<input type="text" name="image_url_input" id="image_url_input" class="form-control form-control-sm mb-2" placeholder="oder Bild-URL">
<div id="pasteArea" class="paste-area small py-2" style="border: 2px dashed #ccc; text-align:center; cursor:pointer;">Bild einfügen (Strg+V)</div>
</div>
</div>
<h5 class="border-bottom pb-2 mb-3">Fächeraufteilung</h5>
<div class="alert alert-light small">
Definiere hier die Bereiche deines Rucksacks (z.B. Deckelfach, Bodenfach). Diese erscheinen später in der Packliste als Container.
</div>
<div id="compartments-container">
<?php if (empty($compartments)): ?>
<!-- Default Compartments for new backpack -->
<div class="input-group mb-2 compartment-row">
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" value="Hauptfach" placeholder="Fachname">
<input type="hidden" name="compartment_ids[]" value="0">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
</div>
<div class="input-group mb-2 compartment-row">
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" value="Deckelfach" placeholder="Fachname">
<input type="hidden" name="compartment_ids[]" value="0">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
</div>
<?php else: ?>
<?php foreach ($compartments as $comp):
// Ensure proper escaping for HTML attributes
$comp_name_escaped = htmlspecialchars($comp['name']);
$comp_id_escaped = htmlspecialchars($comp['id']);
?>
<div class="input-group mb-2 compartment-row">
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" value="<?php echo $comp_name_escaped; ?>">
<input type="hidden" name="compartment_ids[]" value="<?php echo $comp_id_escaped; ?>">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<button type="button" class="btn btn-sm btn-outline-primary mb-4" id="add-compartment"><i class="fas fa-plus"></i> Fach hinzufügen</button>
<div class="d-flex justify-content-between border-top pt-3">
<a href="backpacks.php" class="btn btn-secondary">Abbrechen</a>
<button type="submit" class="btn btn-primary"><i class="fas fa-save me-2"></i>Speichern</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.14.0/Sortable.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('compartments-container');
// Sortable for Compartments
new Sortable(container, {
handle: '.input-group-text',
animation: 150
});
document.getElementById('add-compartment').addEventListener('click', function() {
const div = document.createElement('div');
div.className = 'input-group mb-2 compartment-row';
div.innerHTML = `
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" placeholder="Fachname">
<input type="hidden" name="compartment_ids[]" value="0">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
`;
container.appendChild(div);
});
container.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-comp')) {
e.target.closest('.compartment-row').remove();
}
});
// Image Handling Logic (copied from add_article.php pattern)
const imageFileInput = document.getElementById('image_file');
const imagePreview = document.getElementById('imagePreview');
const pasteArea = document.getElementById('pasteArea');
const pastedImageDataInput = document.getElementById('pasted_image_data');
if(imageFileInput) {
imageFileInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) { imagePreview.src = e.target.result; }
reader.readAsDataURL(this.files[0]);
}
});
}
if (pasteArea) {
pasteArea.addEventListener('paste', function(e) {
e.preventDefault();
const items = (e.clipboardData || window.clipboardData).items;
for (const item of items) {
if (item.type.indexOf('image') === 0) {
const blob = item.getAsFile();
const reader = new FileReader();
reader.onload = function(event) {
imagePreview.src = event.target.result;
pastedImageDataInput.value = event.target.result;
pasteArea.innerText = "Bild eingefügt!";
pasteArea.style.borderColor = "green";
};
reader.readAsDataURL(blob);
}
}
});
// Add focus to paste area on click so it can receive paste events
pasteArea.setAttribute('tabindex', '0');
pasteArea.addEventListener('click', () => pasteArea.focus());
}
});
</script>
<?php require_once 'footer.php'; ?>

View File

@@ -186,31 +186,56 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
<p class="small text-muted">Wähle hier, wer welchen Rucksack trägt. Bereits vergebene Rucksäcke werden ausgeblendet.</p>
<?php
// Calculate used backpacks (except for the user's own current assignment)
// But we iterate users. So for each user loop, we need to know what OTHERS have assigned.
// Logic: Get all values from $current_assignments.
// Calculate used backpacks
$all_assigned_backpack_ids = array_values($current_assignments);
// Prepare data for JS
$user_backpacks_json = [];
foreach ($available_users as $user):
$user_backpacks = get_available_backpacks_for_user($conn, $user['id'], $packing_list['household_id']);
$my_current_bp_id = $current_assignments[$user['id']] ?? 0;
// Store for JS
$user_backpacks_json[$user['id']] = [
'current_id' => $my_current_bp_id,
'backpacks' => $user_backpacks
];
// Find current backpack details for display
$current_bp_details = null;
foreach ($user_backpacks as $bp) {
if ($bp['id'] == $my_current_bp_id) {
$current_bp_details = $bp;
break;
}
}
?>
<div class="mb-3">
<label class="form-label fw-bold"><?php echo htmlspecialchars($user['username']); ?></label>
<select class="form-select form-select-sm" name="backpacks[<?php echo $user['id']; ?>]">
<option value="0">-- Kein Rucksack --</option>
<?php foreach ($user_backpacks as $bp):
// FILTER: If backpack is used by someone else, skip it.
// "Used by someone else" means: ID is in $all_assigned_backpack_ids AND ID != $my_current_bp_id
if (in_array($bp['id'], $all_assigned_backpack_ids) && $bp['id'] != $my_current_bp_id) {
continue;
}
?>
<option value="<?php echo $bp['id']; ?>" <?php echo ($my_current_bp_id == $bp['id']) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($bp['name']); ?>
</option>
<?php endforeach; ?>
</select>
<div class="mb-4 border-bottom pb-3">
<label class="form-label fw-bold mb-2"><?php echo htmlspecialchars($user['username']); ?></label>
<input type="hidden" name="backpacks[<?php echo $user['id']; ?>]" id="input_bp_<?php echo $user['id']; ?>" value="<?php echo $my_current_bp_id; ?>">
<div class="d-flex align-items-center">
<!-- Selected Backpack Display -->
<div id="display_bp_<?php echo $user['id']; ?>" class="flex-grow-1 p-2 border rounded bg-white d-flex align-items-center" style="min-height: 60px;">
<?php if ($current_bp_details): ?>
<!-- Image if available -->
<?php if(!empty($current_bp_details['image_url'])): ?>
<img src="<?php echo htmlspecialchars($current_bp_details['image_url']); ?>" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;" class="me-2">
<?php else: ?>
<i class="fas fa-hiking fa-2x text-muted me-2"></i>
<?php endif; ?>
<div>
<div class="fw-bold small"><?php echo htmlspecialchars($current_bp_details['name']); ?></div>
</div>
<?php else: ?>
<span class="text-muted small fst-italic">Kein Rucksack zugewiesen</span>
<?php endif; ?>
</div>
<button type="button" class="btn btn-sm btn-outline-primary ms-2" onclick="openBackpackModal(<?php echo $user['id']; ?>, '<?php echo htmlspecialchars($user['username']); ?>')">
<i class="fas fa-exchange-alt"></i>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
@@ -229,4 +254,116 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
</div>
</div>
<?php require_once 'footer.php'; ?>
<!-- Generic Modal -->
<div class="modal fade" id="genericBpModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="genericBpModalTitle">Rucksack wählen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body bg-light" id="genericBpModalBody">
<!-- Content loaded via JS -->
</div>
</div>
</div>
</div>
<script>
const userBackpacksData = <?php echo json_encode($user_backpacks_json); ?>;
const allAssignedIds = <?php echo json_encode($all_assigned_backpack_ids); ?>;
function openBackpackModal(userId, username) {
const data = userBackpacksData[userId];
const currentBpId = data.current_id;
const titleEl = document.getElementById('genericBpModalTitle');
const bodyEl = document.getElementById('genericBpModalBody');
titleEl.textContent = 'Rucksack für ' + username + ' wählen';
let html = '<div class="row g-3">';
// Option: None
html += `
<div class="col-md-4 col-6">
<div class="card h-100 text-center p-3 bp-select-card ${currentBpId == 0 ? 'border-primary bg-white' : ''}" onclick="selectBackpack(${userId}, 0)">
<div class="card-body">
<i class="fas fa-times-circle fa-2x text-muted mb-2"></i>
<div class="small fw-bold">Kein Rucksack</div>
</div>
</div>
</div>
`;
data.backpacks.forEach(bp => {
// Filter logic
const isAssigned = allAssignedIds.some(id => String(id) === String(bp.id));
if (isAssigned && String(bp.id) !== String(currentBpId)) return;
const imgHtml = bp.image_url ? `<img src="${bp.image_url}" class="card-img-top" style="height: 100px; object-fit: contain; padding: 10px;">` : `<div class="text-center py-4"><i class="fas fa-hiking fa-3x text-muted"></i></div>`;
const activeClass = (String(bp.id) === String(currentBpId)) ? 'border-primary bg-white' : '';
html += `
<div class="col-md-4 col-6">
<div class="card h-100 bp-select-card ${activeClass}" onclick='selectBackpack(${userId}, ${bp.id})'>
${imgHtml}
<div class="card-body p-2 text-center">
<div class="small fw-bold text-truncate">${bp.name}</div>
</div>
</div>
</div>
`;
});
html += '</div>';
bodyEl.innerHTML = html;
new bootstrap.Modal(document.getElementById('genericBpModal')).show();
}
function selectBackpack(userId, bpId) {
// Update Hidden Input
document.getElementById('input_bp_' + userId).value = bpId;
// Find BP Data for display
let bpName = 'Kein Rucksack zugewiesen';
let bpImage = null;
if (bpId > 0) {
const data = userBackpacksData[userId];
const bp = data.backpacks.find(b => String(b.id) === String(bpId));
if (bp) {
bpName = bp.name;
bpImage = bp.image_url;
}
}
// Update Display
const displayDiv = document.getElementById('display_bp_' + userId);
let html = '';
if (bpId == 0) {
html = '<span class="text-muted small fst-italic">Kein Rucksack zugewiesen</span>';
} else {
if (bpImage) {
html += '<img src="' + bpImage + '" style="width: 40px; height: 40px; object-fit: cover; border-radius: 4px;" class="me-2">';
} else {
html += '<i class="fas fa-hiking fa-2x text-muted me-2"></i>';
}
html += '<div><div class="fw-bold small">' + bpName + '</div></div>';
}
displayDiv.innerHTML = html;
// Close Modal
const modalEl = document.getElementById('genericBpModal');
const modal = bootstrap.Modal.getInstance(modalEl);
modal.hide();
}
</script>
<style>
.bp-select-card { cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; border: 1px solid rgba(0,0,0,0.1); }
.bp-select-card:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); border-color: var(--color-primary); }
</style>
<?php require_once 'footer.php'; ?>
</replace>

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

@@ -236,7 +236,7 @@ function render_print_table_rows($items, $level = 0) {
<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>
<button onclick="window.print();" class="btn btn-outline-light btn-sm ms-2"><i class="fas fa-print me-2"></i>Drucken</button>
<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>
@@ -336,26 +336,6 @@ function render_print_table_rows($items, $level = 0) {
</div>
</div>
<div class="print-view">
<!-- Fallback Print View inside Details Page (usually hidden by CSS unless printing page directly) -->
<div class="print-header">
<h1><?php echo htmlspecialchars($packing_list['name']); ?></h1>
</div>
<table class="print-table" style="width:100%; border-collapse:collapse;">
<thead><tr style="border-bottom:2px solid black;"><th>Artikel</th><th>Anzahl</th><th style="text-align:right;">Gewicht</th></tr></thead>
<tbody>
<?php
// Simple flat print fallback
if(isset($items_by_parent[0])) {
foreach($items_by_parent[0] as $root) {
echo '<tr><td>' . htmlspecialchars($root['article_name']) . '</td><td>' . $root['quantity'] . '</td><td style="text-align:right;">' . $root['weight_grams'] . '</td></tr>';
}
}
?>
</tbody>
</table>
</div>
<div id="image-preview-tooltip"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {

225
print_packing_list.php Normal file
View File

@@ -0,0 +1,225 @@
<?php
// print_packing_list.php - Eigene Seite für die Druckansicht einer Packliste
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;
$packing_list = null;
$items_by_parent = [];
$message = '';
$total_weight_grams = 0;
$weight_by_category = [];
$total_consumable_weight = 0;
if ($packing_list_id > 0) {
$stmt = $conn->prepare("SELECT id, name, description, user_id FROM packing_lists WHERE id = ?");
if ($stmt) {
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows == 1) {
$packing_list = $result->fetch_assoc();
// Permission check logic could be expanded here
} else {
$message = 'Packliste nicht gefunden.';
}
$stmt->close();
}
} else {
$message = 'Keine ID.';
}
if ($packing_list) {
// Artikel abrufen - LEFT JOIN für Container
// WICHTIG: LEFT JOIN users für Trägernamen
$sql = "SELECT
pli.id AS packing_list_item_id, pli.article_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, 'Unbekannt') AS name,
a.weight_grams, c.name AS category_name, a.consumable, a.image_url,
u.username AS carrier_name
FROM packing_list_items pli
LEFT JOIN articles a ON pli.article_id = a.id
LEFT JOIN categories c ON a.category_id = c.id
LEFT JOIN backpacks bp ON pli.backpack_id = bp.id
LEFT JOIN backpack_compartments bpc ON pli.backpack_compartment_id = bpc.id
LEFT JOIN users u ON pli.carrier_user_id = u.id
WHERE pli.packing_list_id = ?
ORDER BY pli.order_index ASC";
$stmt = $conn->prepare($sql);
if ($stmt) {
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result();
$raw_items = [];
while ($row = $result->fetch_assoc()) {
// Fix Name for Backpacks if needed
if ($row['backpack_id'] && empty($row['name'])) $row['name'] = "Rucksack";
$raw_items[] = $row;
// Stats
$w = ($row['weight_grams'] ?? 0) * $row['quantity'];
$total_weight_grams += $w;
if ($row['consumable']) $total_consumable_weight += $w;
$cat = $row['category_name'] ?: 'Sonstiges';
if (!isset($weight_by_category[$cat])) $weight_by_category[$cat] = 0;
$weight_by_category[$cat] += $w;
}
$stmt->close();
// Build Tree
$items_by_id = [];
foreach ($raw_items as $item) {
$item['children'] = [];
$items_by_id[$item['packing_list_item_id']] = $item;
}
foreach ($items_by_id as $id => &$item) {
$pid = $item['parent_packing_list_item_id'];
if ($pid && isset($items_by_id[$pid])) {
$items_by_id[$pid]['children'][] = &$item;
}
}
unset($item);
foreach ($items_by_id as $item) {
if (!$item['parent_packing_list_item_id']) {
$items_by_parent[] = $item; // Root items
}
}
}
}
$conn->close();
$total_weight_without_consumables = $total_weight_grams - $total_consumable_weight;
// Recursive Render Function for Table Rows
function render_rows_recursive($items, $level = 0) {
foreach ($items as $item) {
$is_container = !empty($item['backpack_id']) || !empty($item['backpack_compartment_id']);
$indent = $level * 20; // px
$row_style = "";
$name_style = "";
if (!empty($item['backpack_id'])) {
$row_style = "font-weight:bold; background-color:#f0f0f0; border-top: 2px solid #000;";
$name_style = "text-transform:uppercase;";
} elseif (!empty($item['backpack_compartment_id'])) {
$row_style = "font-weight:bold; background-color:#fafafa; border-bottom:1px solid #ccc;";
$name_style = "font-style:italic;";
}
echo '<tr style="' . $row_style . '">';
// Checkbox
echo '<td class="print-checkbox-cell"><div class="print-checkbox" style="' . ($is_container ? 'visibility:hidden;' : '') . '"></div></td>';
// Name
echo '<td style="padding-left: ' . $indent . 'px;">';
if ($level > 0 && !$is_container) echo '<span style="color:#ccc; margin-right:5px;">↳</span>';
echo '<span style="' . $name_style . '">' . htmlspecialchars($item['name']) . '</span>';
echo '</td>';
// Qty
echo '<td class="text-center">' . ($is_container ? '' : $item['quantity']) . '</td>';
// Weight
echo '<td class="text-end">' . ($item['weight_grams'] > 0 ? number_format($item['weight_grams'], 0, ',', '.') . ' g' : '') . '</td>';
echo '</tr>';
if (!empty($item['children'])) {
render_rows_recursive($item['children'], $level + 1);
}
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title><?php echo htmlspecialchars($packing_list['name'] ?? 'Druckansicht'); ?></title>
<!-- Updated Version Force -->
<style>
@media print {
@page { size: A4; margin: 1.5cm; }
body { font-family: sans-serif; font-size: 10pt; -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
.no-print { display: none; }
}
body { font-family: sans-serif; color: #000; max-width: 900px; margin: 0 auto; padding: 20px; }
h1 { text-align: center; margin-bottom: 5px; }
p.desc { text-align: center; color: #666; margin-bottom: 30px; }
.carrier-section { margin-bottom: 30px; page-break-inside: avoid; }
.carrier-title { font-size: 14pt; border-bottom: 2px solid #000; margin-bottom: 10px; padding-bottom: 5px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 10px; }
th { text-align: left; border-bottom: 1px solid #000; padding: 5px; font-size: 9pt; }
td { padding: 4px 5px; border-bottom: 1px solid #eee; vertical-align: top; }
.text-center { text-align: center; }
.text-end { text-align: right; }
.print-checkbox { width: 12px; height: 12px; border: 1px solid #000; display: inline-block; }
.print-checkbox-cell { width: 20px; }
.stats { margin-top: 40px; border-top: 2px solid #000; padding-top: 10px; display: flex; flex-wrap: wrap; gap: 20px; }
.stat-item { width: 45%; margin-bottom: 5px; border-bottom: 1px dotted #ccc; display: flex; justify-content: space-between; }
</style>
</head>
<body>
<?php if ($packing_list): ?>
<h1><?php echo htmlspecialchars($packing_list['name']); ?></h1>
<p class="desc"><?php echo htmlspecialchars($packing_list['description'] ?? ''); ?></p>
<?php
if (empty($items_by_parent)) {
echo '<p style="text-align:center;">Keine Artikel.</p>';
} else {
// Group by Carrier
$items_by_carrier = [];
foreach ($items_by_parent as $root) {
$c_name = $root['carrier_name'] ?: 'Sonstiges';
$items_by_carrier[$c_name][] = $root;
}
foreach ($items_by_carrier as $carrier => $roots) {
echo '<div class="carrier-section">';
echo '<div class="carrier-title">' . htmlspecialchars($carrier) . '</div>';
echo '<table>';
echo '<thead><tr><th style="width:20px;"></th><th>Artikel / Container</th><th class="text-center" style="width:50px;">Anz.</th><th class="text-end" style="width:80px;">Gewicht</th></tr></thead>';
echo '<tbody>';
render_rows_recursive($roots);
echo '</tbody>';
echo '</table>';
echo '</div>';
}
}
?>
<div class="stats">
<div class="stat-item"><strong>Gesamtgewicht:</strong> <span><?php echo number_format($total_weight_grams, 0, ',', '.'); ?> g</span></div>
<div class="stat-item"><strong>Basisgewicht (ohne Verbrauch):</strong> <span><?php echo number_format($total_weight_without_consumables, 0, ',', '.'); ?> g</span></div>
<?php arsort($weight_by_category); foreach($weight_by_category as $cat => $w): ?>
<div class="stat-item"><span><?php echo htmlspecialchars($cat); ?>:</span> <span><?php echo number_format($w, 0, ',', '.'); ?> g</span></div>
<?php endforeach; ?>
</div>
<script>window.print();</script>
<?php else: ?>
<p style="color:red; text-align:center;"><?php echo htmlspecialchars($message); ?></p>
<?php endif; ?>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,249 +0,0 @@
<?php
// edit_backpack.php - Erstellen und Bearbeiten von Rucksäcken und Fächern
$page_title = "Rucksack bearbeiten";
require_once 'db_connect.php';
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
$user_id = $_SESSION['user_id'];
$backpack_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$backpack = null;
$compartments = [];
$message = '';
// Check Household
$household_id = null;
$stmt_hh = $conn->prepare("SELECT household_id FROM users WHERE id = ?");
$stmt_hh->bind_param("i", $user_id);
$stmt_hh->execute();
$res_hh = $stmt_hh->get_result();
if ($row = $res_hh->fetch_assoc()) {
$household_id = $row['household_id'];
}
// Handle Form Submission BEFORE loading header
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$name = trim($_POST['name']);
$manufacturer = trim($_POST['manufacturer']);
$model = trim($_POST['model']);
$weight = intval($_POST['weight_grams']);
$volume = intval($_POST['volume_liters']);
$share_household = isset($_POST['share_household']) ? 1 : 0;
$final_household_id = ($share_household && $household_id) ? $household_id : NULL;
if ($backpack_id > 0) {
// Update
$stmt = $conn->prepare("UPDATE backpacks SET name=?, manufacturer=?, model=?, weight_grams=?, volume_liters=?, household_id=? WHERE id=? AND user_id=?");
$stmt->bind_param("sssiisii", $name, $manufacturer, $model, $weight, $volume, $final_household_id, $backpack_id, $user_id);
$stmt->execute();
} else {
// Insert
$stmt = $conn->prepare("INSERT INTO backpacks (user_id, household_id, name, manufacturer, model, weight_grams, volume_liters) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("iisssii", $user_id, $final_household_id, $name, $manufacturer, $model, $weight, $volume);
$stmt->execute();
$backpack_id = $stmt->insert_id;
}
// Handle Compartments
if (isset($_POST['compartment_names'])) {
$comp_names = $_POST['compartment_names'];
$comp_ids = $_POST['compartment_ids'] ?? [];
// Get existing IDs to know what to delete
// We need to re-fetch existing just for this logic if not loaded yet, or just assume based on IDs
// Since we haven't loaded existing yet in this flow, let's just operate on IDs.
// First load existing IDs for safety to check ownership indirectly via backpack_id
$existing_ids = [];
$stmt_check = $conn->prepare("SELECT id FROM backpack_compartments WHERE backpack_id = ?");
$stmt_check->bind_param("i", $backpack_id);
$stmt_check->execute();
$res_check = $stmt_check->get_result();
while($row = $res_check->fetch_assoc()) $existing_ids[] = $row['id'];
$kept_ids = [];
for ($i = 0; $i < count($comp_names); $i++) {
$c_name = trim($comp_names[$i]);
$c_id = intval($comp_ids[$i] ?? 0);
if (empty($c_name)) continue;
if ($c_id > 0 && in_array($c_id, $existing_ids)) {
// Update
$stmt_up = $conn->prepare("UPDATE backpack_compartments SET name = ?, sort_order = ? WHERE id = ?");
$stmt_up->bind_param("sii", $c_name, $i, $c_id);
$stmt_up->execute();
$kept_ids[] = $c_id;
} else {
// Insert
$stmt_in = $conn->prepare("INSERT INTO backpack_compartments (backpack_id, name, sort_order) VALUES (?, ?, ?)");
$stmt_in->bind_param("isi", $backpack_id, $c_name, $i);
$stmt_in->execute();
}
}
// Delete removed
foreach ($existing_ids as $ex_id) {
if (!in_array($ex_id, $kept_ids)) {
$conn->query("DELETE FROM backpack_compartments WHERE id = $ex_id");
}
}
}
header("Location: backpacks.php");
exit;
}
require_once 'header.php';
// Load existing data (Moved after header inclusion is fine for display logic, but data loading could be before too)
if ($backpack_id > 0) {
$stmt = $conn->prepare("SELECT * FROM backpacks WHERE id = ? AND user_id = ?");
$stmt->bind_param("ii", $backpack_id, $user_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$backpack = $result->fetch_assoc();
// Load Compartments
$stmt_c = $conn->prepare("SELECT * FROM backpack_compartments WHERE backpack_id = ? ORDER BY sort_order ASC");
$stmt_c->bind_param("i", $backpack_id);
$stmt_c->execute();
$res_c = $stmt_c->get_result();
while ($row = $res_c->fetch_assoc()) {
$compartments[] = $row;
}
} else {
$message = '<div class="alert alert-danger">Rucksack nicht gefunden oder keine Berechtigung.</div>';
$backpack_id = 0; // Reset to create mode
}
}
?>
?>
<div class="container mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h3 class="h5 mb-0"><?php echo $backpack_id > 0 ? 'Rucksack bearbeiten' : 'Neuen Rucksack anlegen'; ?></h3>
</div>
<div class="card-body">
<?php echo $message; ?>
<form method="post" id="backpackForm">
<div class="row g-3 mb-4">
<div class="col-md-12">
<label class="form-label">Bezeichnung (Spitzname)</label>
<input type="text" name="name" class="form-control" required value="<?php echo htmlspecialchars($backpack['name'] ?? ''); ?>" placeholder="z.B. Mein großer Trekker">
</div>
<div class="col-md-6">
<label class="form-label">Hersteller</label>
<input type="text" name="manufacturer" class="form-control" value="<?php echo htmlspecialchars($backpack['manufacturer'] ?? ''); ?>">
</div>
<div class="col-md-6">
<label class="form-label">Modell</label>
<input type="text" name="model" class="form-control" value="<?php echo htmlspecialchars($backpack['model'] ?? ''); ?>">
</div>
<div class="col-md-6">
<label class="form-label">Leergewicht (g)</label>
<input type="number" name="weight_grams" class="form-control" value="<?php echo htmlspecialchars($backpack['weight_grams'] ?? 0); ?>">
</div>
<div class="col-md-6">
<label class="form-label">Volumen (Liter)</label>
<input type="number" name="volume_liters" class="form-control" value="<?php echo htmlspecialchars($backpack['volume_liters'] ?? 0); ?>">
</div>
<div class="col-md-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="share_household" name="share_household" <?php echo (!empty($backpack['household_id']) || $backpack_id == 0) ? 'checked' : ''; ?>>
<label class="form-check-label" for="share_household">Mit Haushalt teilen</label>
</div>
</div>
</div>
<h5 class="border-bottom pb-2 mb-3">Fächeraufteilung</h5>
<div class="alert alert-light small">
Definiere hier die Bereiche deines Rucksacks (z.B. Deckelfach, Bodenfach). Diese erscheinen später in der Packliste als Container.
</div>
<div id="compartments-container">
<?php if (empty($compartments)): ?>
<!-- Default Compartments for new backpack -->
<div class="input-group mb-2 compartment-row">
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" value="Hauptfach" placeholder="Fachname">
<input type="hidden" name="compartment_ids[]" value="0">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
</div>
<div class="input-group mb-2 compartment-row">
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" value="Deckelfach" placeholder="Fachname">
<input type="hidden" name="compartment_ids[]" value="0">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
</div>
<?php else: ?>
<?php foreach ($compartments as $comp): ?>
<div class="input-group mb-2 compartment-row">
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" value="<?php echo htmlspecialchars($comp['name']); ?>">
<input type="hidden" name="compartment_ids[]" value="<?php echo $comp['id']; ?>">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<button type="button" class="btn btn-sm btn-outline-primary mb-4" id="add-compartment"><i class="fas fa-plus"></i> Fach hinzufügen</button>
<div class="d-flex justify-content-between border-top pt-3">
<a href="backpacks.php" class="btn btn-secondary">Abbrechen</a>
<button type="submit" class="btn btn-primary"><i class="fas fa-save me-2"></i>Speichern</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.14.0/Sortable.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('compartments-container');
// Sortable for Compartments
new Sortable(container, {
handle: '.input-group-text',
animation: 150
});
document.getElementById('add-compartment').addEventListener('click', function() {
const div = document.createElement('div');
div.className = 'input-group mb-2 compartment-row';
div.innerHTML = `
<span class="input-group-text"><i class="fas fa-grip-lines text-muted"></i></span>
<input type="text" name="compartment_names[]" class="form-control" placeholder="Fachname">
<input type="hidden" name="compartment_ids[]" value="0">
<button type="button" class="btn btn-outline-danger btn-remove-comp"><i class="fas fa-times"></i></button>
`;
container.appendChild(div);
});
container.addEventListener('click', function(e) {
if (e.target.closest('.btn-remove-comp')) {
e.target.closest('.compartment-row').remove();
}
});
});
</script>
<?php require_once 'footer.php'; ?>

View File

@@ -1,283 +0,0 @@
<?php
// print_packing_list.php - Eigene Seite für die Druckansicht einer Packliste
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Prüfen, ob der Benutzer angemeldet ist
if (!isset($_SESSION['user_id'])) {
// Wenn nicht angemeldet, zur Login-Seite umleiten oder Fehlermeldung anzeigen
// Für die Druckansicht leiten wir einfach weiter, da ein unautorisierter Druck keinen Sinn macht.
header("Location: login.php");
exit;
}
require_once 'db_connect.php'; // Datenbankverbindung
$current_user_id = $_SESSION['user_id'];
$packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
$packing_list = null;
$packing_list_items = [];
$message = '';
$total_weight_grams = 0;
$weight_by_category = [];
$total_consumable_weight = 0;
if ($packing_list_id > 0) {
// Packliste abrufen
$stmt = $conn->prepare("SELECT id, name, description, user_id FROM packing_lists WHERE id = ?");
if ($stmt) {
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows == 1) {
$packing_list = $result->fetch_assoc();
// Nur eigene Packlisten oder globale Packlisten dürfen gedruckt werden
if ($packing_list['user_id'] != $current_user_id) {
$message = 'Sie sind nicht berechtigt, diese Packliste zu drucken.';
$packing_list = null;
}
} else {
$message = 'Packliste nicht gefunden.';
}
$stmt->close();
} else {
$message = 'Datenbankfehler beim Abrufen der Packliste.';
}
} else {
$message = 'Keine Packlisten-ID zum Drucken angegeben.';
}
if ($packing_list) {
// Artikel der Packliste abrufen - LEFT JOIN für Container Support
$stmt = $conn->prepare("SELECT pli.id AS packing_list_item_id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, pli.backpack_id, pli.backpack_compartment_id, COALESCE(a.name, pli.name) AS name, a.weight_grams, c.name AS category_name, a.consumable, a.image_url FROM packing_list_items pli LEFT JOIN articles a ON pli.article_id = a.id LEFT JOIN categories c ON a.category_id = c.id WHERE pli.packing_list_id = ? ORDER BY pli.parent_packing_list_item_id ASC, pli.order_index ASC");
if ($stmt) {
$stmt->bind_param("i", $packing_list_id);
$stmt->execute();
$result = $stmt->get_result();
$raw_packing_list_items = [];
while ($row = $result->fetch_assoc()) {
$raw_packing_list_items[] = $row;
$item_weight = $row['weight_grams'] ?? 0;
$item_total_weight = $item_weight * $row['quantity'];
$total_weight_grams += $item_total_weight;
$category_name = $row['category_name'] ?: 'Unkategorisiert';
if (!isset($weight_by_category[$category_name])) {
$weight_by_category[$category_name] = 0;
}
$weight_by_category[$category_name] += $item_total_weight;
if ($row['consumable']) {
$total_consumable_weight += $item_total_weight;
}
}
$stmt->close();
// Items in hierarchische Struktur bringen
$items_by_id = [];
foreach ($raw_packing_list_items as $item) {
$item['children'] = [];
$items_by_id[$item['packing_list_item_id']] = $item;
}
foreach ($items_by_id as $item_id => &$item) {
if ($item['parent_packing_list_item_id'] !== null && isset($items_by_id[$item['parent_packing_list_item_id']])) {
$items_by_id[$item['parent_packing_list_item_id']]['children'][] = &$items_by_id[$item_id];
}
}
unset($item); // Wichtig, um Referenzfehler zu vermeiden!
foreach ($items_by_id as $item) {
if ($item['parent_packing_list_item_id'] === null) {
$packing_list_items[] = $item;
}
}
} else {
$message = 'Datenbankfehler beim Abrufen der Packlistenartikel.';
}
}
$conn->close();
$total_weight_without_consumables = $total_weight_grams - $total_consumable_weight;
// Rekursive Funktion zur Darstellung der Artikel (für Druckansicht angepasst)
function renderPrintablePackingListItemsRecursive($items, $level = 0) {
$html = '<ul class="print-list-level level-' . $level . '">';
foreach ($items as $item) {
$item_weight = $item['weight_grams'] ?? 0;
$item_total_weight_current = $item_weight * $item['quantity'];
// Container Styling
$is_container = !empty($item['backpack_id']) || !empty($item['backpack_compartment_id']);
$container_style = "";
if (!empty($item['backpack_id'])) {
$container_style = "font-weight: bold; font-size: 1.1em; margin-top: 10px; border-bottom: 2px solid #ccc;";
} elseif (!empty($item['backpack_compartment_id'])) {
$container_style = "font-weight: bold; margin-top: 5px; color: #555;";
}
$details_string = '';
if (!$is_container) {
$details_string = '(' . number_format($item_weight, 0, ',', '.') . 'g' . ($item['category_name'] ? ', ' . htmlspecialchars($item['category_name']) : '') . ($item['consumable'] ? ', Verbrauchbar' : '') . ')';
}
$indent_style = ($level > 0) ? 'padding-left: ' . ($level * 1.5) . 'em;' : '';
$html .= '
<li class="print-list-item" style="' . $indent_style . ' ' . $container_style . '">
<span class="print-checkbox" style="' . ($is_container ? 'visibility:hidden;' : '') . '"></span>
<span class="print-quantity">' . ($is_container ? '' : htmlspecialchars($item['quantity']) . 'x') . '</span>
<span class="print-name">' . htmlspecialchars($item['name']) . '</span>
<span class="print-details">' . $details_string . '</span>
<span class="print-total-weight">' . ($is_container ? '' : number_format($item_total_weight_current, 0, ',', '.') . 'g') . '</span>
</li>';
if (!empty($item['children'])) {
$html .= renderPrintablePackingListItemsRecursive($item['children'], $level + 1);
}
}
$html .= '</ul>';
return $html;
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Packliste <?php echo htmlspecialchars($packing_list['name'] ?? 'Druckansicht'); ?></title>
<!-- Spezifisches Stylesheet für die Druckansicht einbinden -->
<link rel="stylesheet" href="print.css" media="print">
<!-- Optional: Ein minimales Stylesheet für die Bildschirmanzeige dieser Seite, falls nötig -->
<style>
/* Grundlegende Styles für die Bildschirmanzeige dieser Druckseite */
body {
font-family: Arial, sans-serif;
margin: 20px;
color: #333;
}
.container-print {
max-width: 800px;
margin: 0 auto;
padding: 20px;
border: 1px solid #ccc; /* Minimaler Rahmen für die Bildschirmanzeige */
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1, h2, h3 {
text-align: center;
color: #2c3e50;
margin-bottom: 10px;
}
p.description {
text-align: center;
color: #777;
margin-bottom: 20px;
}
.section-title {
font-size: 1.2em;
font-weight: bold;
margin-top: 20px;
margin-bottom: 10px;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
.print-list-level {
list-style: none;
padding: 0;
margin: 0;
}
.print-list-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 5px 0;
border-bottom: 1px dashed #eee;
font-size: 0.9em;
}
.print-list-item:last-child {
border-bottom: none;
}
.print-checkbox {
width: 15px;
height: 15px;
border: 1px solid #000;
flex-shrink: 0;
margin-right: 8px;
margin-top: 3px; /* Align with text baseline */
}
.print-quantity {
font-weight: bold;
flex-shrink: 0;
margin-right: 8px;
}
.print-name {
flex-grow: 1;
margin-right: 8px;
word-break: break-word; /* Allow long names to wrap */
}
.print-details {
color: #555;
flex-shrink: 0;
margin-right: 8px;
white-space: nowrap; /* Keep details on one line for print-preview */
}
.print-total-weight {
font-weight: bold;
flex-shrink: 0;
text-align: right;
}
.statistics-list {
list-style: none;
padding: 0;
}
.statistics-list li {
display: flex;
justify-content: space-between;
padding: 3px 0;
border-bottom: 1px solid #eee;
}
.statistics-list li:last-child {
border-bottom: none;
}
</style>
</head>
<body>
<div class="container-print">
<?php if ($packing_list): ?>
<h1><?php echo htmlspecialchars($packing_list['name']); ?></h1>
<p class="description"><?php echo htmlspecialchars($packing_list['description'] ?: 'Keine Beschreibung'); ?></p>
<h2 class="section-title">Enthaltene Artikel</h2>
<?php if (empty($packing_list_items)): ?>
<p>Diese Packliste enthält noch keine Artikel.</p>
<?php else: ?>
<?php echo renderPrintablePackingListItemsRecursive($packing_list_items); ?>
<?php endif; ?>
<h2 class="section-title">Statistiken</h2>
<ul class="statistics-list">
<li>
<strong>Gesamtgewicht:</strong>
<span><?php echo number_format($total_weight_grams, 0, ',', '.') . 'g'; ?></span>
</li>
<li>
<strong>Basisgewicht:</strong>
<span><?php echo number_format($total_weight_without_consumables, 0, ',', '.') . 'g'; ?></span>
</li>
<?php
arsort($weight_by_category); // Sortiere Kategorien nach Gewicht absteigend
foreach ($weight_by_category as $category_name => $weight) : ?>
<li>
<span>Gewicht <?php echo htmlspecialchars($category_name); ?>:</span>
<span><?php echo number_format($weight, 0, ',', '.') . 'g'; ?></span>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<p style="text-align: center; color: red;"><?php echo htmlspecialchars($message); ?></p>
<?php endif; ?>
</div>
<script>
// Startet den Druckdialog, sobald die Seite geladen ist
// setTimeout(function() { window.print(); }, 100); // Kleine Verzögerung zur Sicherheit
</script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 580 KiB

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 768 KiB

After

Width:  |  Height:  |  Size: 768 KiB

View File

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

View File

Before

Width:  |  Height:  |  Size: 668 KiB

After

Width:  |  Height:  |  Size: 668 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

Before

Width:  |  Height:  |  Size: 862 KiB

After

Width:  |  Height:  |  Size: 862 KiB

View File

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 422 KiB

View File

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 422 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 196 KiB

View File

Before

Width:  |  Height:  |  Size: 218 KiB

After

Width:  |  Height:  |  Size: 218 KiB

View File

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 211 KiB

View File

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View File

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

View File

Before

Width:  |  Height:  |  Size: 203 KiB

After

Width:  |  Height:  |  Size: 203 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Some files were not shown because too many files have changed in this diff Show More