Compare commits

...

2 Commits

Author SHA1 Message Date
Gemini Agent
47e51ce60f Merge branch 'master' of https://git.klenzel.net/admin/packliste
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 12s
2025-12-06 16:24:16 +00:00
Gemini Agent
0392c69019 Feature: Verbesserungen an Artikel-Bildern, Rucksäcken und Filtern
- add_article.php: Auswahl bereits hochgeladener Bilder hinzugefügt.
- edit_backpack.php: Link zur Herstellerseite (neue Spalte product_url) und Fix für 'Neuer Hersteller' hinzugefügt. Auto-Migration für DB-Spalte integriert.
- manage_packing_list_items.php: Filteroptionen um 'Alle' erweitert.
2025-12-06 16:20:15 +00:00
3 changed files with 176 additions and 112 deletions

View File

@@ -1,7 +1,6 @@
<?php
// add_article.php - Formular zum Hinzufügen eines neuen Artikels
// FINALE, VOLLSTÄNDIGE VERSION mit allen Funktionen (Komponenten, Bestand, etc.)
// KORREKTUR: SSL-Verifizierung und @-Fehlerunterdrückung korrigiert.
// FINALE, VOLLSTÄNDIGE VERSION mit Bilder-Auswahl
$page_title = "Neuen Artikel hinzufügen";
@@ -45,25 +44,28 @@ if ($household_id_for_user) {
$placeholders = implode(',', array_fill(0, count($household_member_ids), '?'));
$types = str_repeat('i', count($household_member_ids));
// Lade Kategorien, Hersteller, Lagerorte und potenzielle Eltern-Artikel
// Lade Kategorien
$stmt_cat_load = $conn->prepare("SELECT id, name FROM categories WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_cat_load->bind_param($types, ...$household_member_ids);
$stmt_cat_load->execute();
$categories = $stmt_cat_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_cat_load->close();
// Lade Hersteller
$stmt_man_load = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id IN ($placeholders) ORDER BY name ASC");
$stmt_man_load->bind_param($types, ...$household_member_ids);
$stmt_man_load->execute();
$manufacturers = $stmt_man_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_man_load->close();
// Lade Lagerorte
$stmt_loc_load = $conn->prepare("SELECT id, name, parent_id FROM storage_locations WHERE user_id IN ($placeholders) ORDER BY parent_id, name");
$stmt_loc_load->bind_param($types, ...$household_member_ids);
$stmt_loc_load->execute();
$all_locations = $stmt_loc_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_loc_load->close();
// Lade Eltern-Artikel
$all_parent_params = array_merge($household_member_ids, [$household_id_for_user]);
$all_parent_types = $types . 'i';
$stmt_parent_articles = $conn->prepare("SELECT a.id, a.name, m.name as manufacturer_name, a.product_designation FROM articles a LEFT JOIN manufacturers m ON a.manufacturer_id = m.id WHERE (a.user_id IN ($placeholders) OR a.household_id = ?) AND a.parent_article_id IS NULL ORDER BY a.name ASC");
@@ -72,6 +74,14 @@ $stmt_parent_articles->execute();
$parent_articles = $stmt_parent_articles->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_parent_articles->close();
// Lade letzte Bilder (für Auswahl)
$stmt_imgs = $conn->prepare("SELECT DISTINCT image_url FROM articles WHERE user_id IN ($placeholders) AND image_url IS NOT NULL AND image_url != '' AND image_url != '0' ORDER BY created_at DESC LIMIT 24");
$stmt_imgs->bind_param($types, ...$household_member_ids);
$stmt_imgs->execute();
$recent_images = $stmt_imgs->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_imgs->close();
// Strukturierung Lagerorte
$storage_locations_structured = [];
foreach ($all_locations as $loc) { if ($loc['parent_id'] === NULL) { if (!isset($storage_locations_structured[$loc['id']])) { $storage_locations_structured[$loc['id']] = ['name' => $loc['name'], 'children' => []]; } } }
foreach ($all_locations as $loc) { if ($loc['parent_id'] !== NULL && isset($storage_locations_structured[$loc['parent_id']])) { $storage_locations_structured[$loc['parent_id']]['children'][] = $loc; } }
@@ -80,50 +90,31 @@ $upload_dir = 'uploads/images/';
if (!is_dir($upload_dir)) { @mkdir($upload_dir, 0777, true); }
function save_image_from_url($url, $upload_dir) {
// KORREKTUR: Unsichere SSL-Optionen entfernt, @-Operator entfernt
$context_options = ["http" => ["header" => "User-Agent: Mozilla/5.0\r\n", "timeout" => 10]];
$context = stream_context_create($context_options);
// Fehlerbehandlung für file_get_contents
$image_data = file_get_contents($url, false, $context);
if ($image_data === false) {
return [false, "Bild konnte von der URL nicht heruntergeladen werden. Überprüfen Sie den Link und die Erreichbarkeit."];
}
// Fehlerbehandlung für getimagesizefromstring
$image_info = getimagesizefromstring($image_data);
if ($image_info === false) {
return [false, "Die angegebene URL führt nicht zu einem gültigen Bild."];
}
$allowed_mime_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($image_info['mime'], $allowed_mime_types)) {
return [false, "Nicht unterstützter Bildtyp: " . htmlspecialchars($image_info['mime'])];
}
$extension = image_type_to_extension($image_info[2], false);
$unique_file_name = uniqid('img_url_', true) . '.' . $extension;
$destination = $upload_dir . $unique_file_name;
if (file_put_contents($destination, $image_data)) {
return [true, $destination];
}
return [false, "Bild konnte nicht auf dem Server gespeichert werden."];
$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('img_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, "Nicht unterstützter Bildtyp aus der Zwischenablage."]; }
if (!in_array($type, ['jpg', 'jpeg', 'png', 'gif'])) return [false, "Format nicht unterstützt."];
$data = base64_decode($data);
if ($data === false) { return [false, "Base64-Dekodierung fehlgeschlagen."]; }
} else { return [false, "Ungültiger Base64-String."]; }
$unique_file_name = uniqid('img_paste_', true) . '.' . $type;
$destination = $upload_dir . $unique_file_name;
if (file_put_contents($destination, $data)) { return [true, $destination]; }
return [false, "Bild aus der Zwischenablage konnte nicht gespeichert werden."];
if ($data === false) return [false, "Dekodierfehler."];
$name = uniqid('img_paste_', true) . '.' . $type;
if (file_put_contents($upload_dir . $name, $data)) return [true, $upload_dir . $name];
}
return [false, "Ungültiges Base64."];
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
@@ -138,6 +129,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
$product_url = trim($_POST['product_url']);
$product_designation = trim($_POST['product_designation']);
$pasted_image_data = $_POST['pasted_image_data'] ?? '';
$selected_existing_image = trim($_POST['selected_existing_image'] ?? '');
if (isset($_POST['manufacturer_id']) && $_POST['manufacturer_id'] === 'new') {
$new_manufacturer_name = trim($_POST['new_manufacturer_name']);
@@ -173,6 +165,10 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
$image_url_for_db = $destination;
} else { $image_error = "Fehler beim Verschieben der hochgeladenen Datei."; }
}
elseif (!empty($selected_existing_image)) {
// User chose an existing image
$image_url_for_db = $selected_existing_image;
}
elseif (!empty($image_url_from_input)) {
list($success, $result) = save_image_from_url($image_url_from_input, $upload_dir);
if ($success) { $image_url_for_db = $result; } else { $image_error = $result; }
@@ -213,10 +209,6 @@ $conn->close();
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
<!-- Tom Select CSS/JS -->
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0"><i class="fas fa-plus-circle me-2"></i>Neuen Artikel erstellen</h2>
@@ -226,6 +218,7 @@ $conn->close();
<?php if(!empty($message)) echo $message; ?>
<form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post" enctype="multipart/form-data">
<input type="hidden" name="pasted_image_data" id="pasted_image_data">
<input type="hidden" name="selected_existing_image" id="selected_existing_image">
<div class="row g-4">
<div class="col-lg-7 d-flex flex-column">
<div class="card section-card mb-4">
@@ -272,6 +265,23 @@ $conn->close();
<div class="card-header"><h5 class="mb-0"><i class="fas fa-image me-2"></i>Bild & Produktseite</h5></div>
<div class="card-body d-flex flex-column">
<div class="mb-3"><label for="product_url" class="form-label">Produktseite (URL)</label><input type="url" class="form-control" id="product_url" name="product_url" value="<?php echo htmlspecialchars($product_url); ?>" placeholder="https://..."></div>
<!-- New: Existing Images Selection -->
<?php if(!empty($recent_images)): ?>
<div class="mb-3">
<label class="form-label">Aus vorhandenen Bildern wählen:</label>
<div class="d-flex flex-wrap gap-2 p-2 border rounded bg-light" style="max-height: 150px; overflow-y: auto;">
<?php foreach($recent_images as $img): ?>
<img src="<?php echo htmlspecialchars($img['image_url']); ?>"
class="existing-image-option rounded border"
style="width: 50px; height: 50px; object-fit: cover; cursor: pointer;"
data-url="<?php echo htmlspecialchars($img['image_url']); ?>"
title="Klicken zum Auswählen">
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="mb-3"><label for="image_file" class="form-label">1. Bild hochladen</label><input class="form-control" type="file" id="image_file" name="image_file" accept="image/*"></div>
<div class="mb-3"><label for="image_url" class="form-label">2. ODER Bild-URL angeben</label><input type="url" class="form-control" id="image_url" name="image_url" placeholder="https://..."></div>
<div class="mb-3"><label class="form-label">3. ODER Bild einfügen (Strg+V)</label><div id="pasteArea" class="paste-area"><i class="fas fa-paste"></i> Hier klicken & einfügen</div></div>
@@ -293,28 +303,44 @@ document.addEventListener('DOMContentLoaded', function() {
const select = document.getElementById(selectId);
const container = document.getElementById(containerId);
if (!select || !container) return;
const input = container.querySelector('input');
function toggle() {
// Must check TomSelect wrapper input if handled, but the value propagates
const isNew = select.value === 'new';
container.style.display = isNew ? 'block' : 'none';
const input = container.querySelector('input');
if (input) input.required = isNew;
if (!isNew && input) { input.value = ''; }
}
// Native change (for backup)
select.addEventListener('change', toggle);
// Initial
toggle();
// Return the toggle function so TomSelect can call it
return toggle;
}
setupAddNewOption('manufacturer_id', 'new_manufacturer_container');
setupAddNewOption('category_id', 'new_category_container');
const toggleMan = setupAddNewOption('manufacturer_id', 'new_manufacturer_container');
const toggleCat = setupAddNewOption('category_id', 'new_category_container');
const imageFileInput = document.getElementById('image_file');
const imageUrlInput = document.getElementById('image_url');
const imagePreview = document.getElementById('imagePreview');
const pasteArea = document.getElementById('pasteArea');
const pastedImageDataInput = document.getElementById('pasted_image_data');
const selectedExistingInput = document.getElementById('selected_existing_image');
const existingImages = document.querySelectorAll('.existing-image-option');
function clearOtherImageInputs(source) {
if (source !== 'file') imageFileInput.value = '';
if (source !== 'url') imageUrlInput.value = '';
if (source !== 'paste') pastedImageDataInput.value = '';
if (source !== 'existing') selectedExistingInput.value = '';
// Reset styles
existingImages.forEach(img => img.classList.remove('border-primary', 'border-3'));
}
if(imageFileInput) {
imageFileInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
@@ -325,6 +351,18 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
}
// Existing Image Click Handler
existingImages.forEach(img => {
img.addEventListener('click', function() {
const url = this.getAttribute('data-url');
imagePreview.src = url;
clearOtherImageInputs('existing');
selectedExistingInput.value = url;
this.classList.add('border-primary', 'border-3');
});
});
if (pasteArea) {
pasteArea.addEventListener('paste', function(e) {
e.preventDefault();
@@ -345,7 +383,11 @@ document.addEventListener('DOMContentLoaded', function() {
pasteArea.addEventListener('dragover', (e) => { e.preventDefault(); pasteArea.classList.add('hover'); });
pasteArea.addEventListener('dragleave', () => pasteArea.classList.remove('hover'));
pasteArea.addEventListener('drop', (e) => { e.preventDefault(); pasteArea.classList.remove('hover'); });
// Click to focus
pasteArea.setAttribute('tabindex', '0');
pasteArea.addEventListener('click', () => pasteArea.focus());
}
const consumableCheckbox = document.getElementById('consumable');
const quantityOwnedInput = document.getElementById('quantity_owned');
consumableCheckbox.addEventListener('change', function() {
@@ -358,20 +400,25 @@ document.addEventListener('DOMContentLoaded', function() {
});
// Initialize Tom Select for searchable dropdowns
const tsOptions = {
const tsOptionsBase = {
create: false,
sortField: { field: "text", direction: "asc" },
onChange: function(value) {
// Force trigger change event for the "New" option toggle logic
const event = new Event('change');
this.input.dispatchEvent(event);
}
sortField: { field: "text", direction: "asc" }
};
if(document.getElementById('manufacturer_id')) new TomSelect('#manufacturer_id', tsOptions);
if(document.getElementById('category_id')) new TomSelect('#category_id', tsOptions);
if(document.getElementById('parent_article_id')) new TomSelect('#parent_article_id', tsOptions);
if(document.getElementById('storage_location_id')) new TomSelect('#storage_location_id', tsOptions);
if(document.getElementById('manufacturer_id')) {
new TomSelect('#manufacturer_id', {
...tsOptionsBase,
onChange: function(value) { if(toggleMan) toggleMan(); }
});
}
if(document.getElementById('category_id')) {
new TomSelect('#category_id', {
...tsOptionsBase,
onChange: function(value) { if(toggleCat) toggleCat(); }
});
}
if(document.getElementById('parent_article_id')) new TomSelect('#parent_article_id', tsOptionsBase);
if(document.getElementById('storage_location_id')) new TomSelect('#storage_location_id', tsOptionsBase);
});
</script>
<?php require_once 'footer.php'; ?>
<?php require_once 'footer.php'; ?>

View File

@@ -23,6 +23,13 @@ $backpack = null;
$compartments = [];
$message = '';
$image_url = '';
$product_url = '';
// AUTO-MIGRATION: Check if product_url column exists, if not add it
$check_col = $conn->query("SHOW COLUMNS FROM backpacks LIKE 'product_url'");
if ($check_col && $check_col->num_rows == 0) {
$conn->query("ALTER TABLE backpacks ADD COLUMN product_url VARCHAR(255) DEFAULT NULL AFTER image_url");
}
// Check Household
$household_id = null;
@@ -75,6 +82,7 @@ if ($backpack_id > 0) {
if ($result->num_rows > 0) {
$backpack = $result->fetch_assoc();
$image_url = $backpack['image_url'];
$product_url = $backpack['product_url'] ?? '';
// Load Compartments
$stmt_c = $conn->prepare("SELECT * FROM backpack_compartments WHERE backpack_id = ? ORDER BY sort_order ASC");
@@ -107,11 +115,21 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if ($_POST['manufacturer_select'] === 'new') {
$new_man = trim($_POST['new_manufacturer_name']);
if (!empty($new_man)) {
// Optional: Save to manufacturers table for future use
$stmt_new_man = $conn->prepare("INSERT INTO manufacturers (name, user_id) VALUES (?, ?)");
$stmt_new_man->bind_param("si", $new_man, $user_id);
$stmt_new_man->execute();
$manufacturer = $new_man;
// Check if exists first
$stmt_check_man = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id = ? AND name = ?");
$stmt_check_man->bind_param("is", $user_id, $new_man);
$stmt_check_man->execute();
$res_check = $stmt_check_man->get_result();
if ($res_check->num_rows > 0) {
$manufacturer = $res_check->fetch_assoc()['name'];
} else {
// Save to manufacturers table
$stmt_new_man = $conn->prepare("INSERT INTO manufacturers (name, user_id) VALUES (?, ?)");
$stmt_new_man->bind_param("si", $new_man, $user_id);
$stmt_new_man->execute();
$manufacturer = $new_man;
}
}
} else {
// Look up name from ID
@@ -129,6 +147,7 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$weight = intval($_POST['weight_grams']);
$volume = intval($_POST['volume_liters']);
$share_household = isset($_POST['share_household']) ? 1 : 0;
$product_url_input = trim($_POST['product_url'] ?? '');
// Image Handling
$image_url_for_db = $image_url; // Keep existing by default
@@ -183,13 +202,13 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
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 = $conn->prepare("UPDATE backpacks SET name=?, manufacturer=?, model=?, weight_grams=?, volume_liters=?, household_id=?, image_url=?, product_url=? WHERE id=? AND user_id=?");
$stmt->bind_param("sssiisssii", $name, $manufacturer, $model, $weight, $volume, $final_household_id, $image_url_for_db, $product_url_input, $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 = $conn->prepare("INSERT INTO backpacks (user_id, household_id, name, manufacturer, model, weight_grams, volume_liters, image_url, product_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("iisssiiss", $user_id, $final_household_id, $name, $manufacturer, $model, $weight, $volume, $image_url_for_db, $product_url_input);
$stmt->execute();
$backpack_id = $stmt->insert_id;
}
@@ -243,9 +262,6 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
exit;
}
// Handle Form Submission BEFORE loading header
// ... (existing logic) ...
require_once 'header.php';
?>
@@ -302,6 +318,13 @@ require_once 'header.php';
<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">
<label class="form-label">Hersteller-Link (Produktseite)</label>
<div class="input-group">
<span class="input-group-text"><i class="fas fa-link"></i></span>
<input type="url" name="product_url" class="form-control" value="<?php echo htmlspecialchars($product_url); ?>" placeholder="https://...">
</div>
</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' : ''; ?>>
@@ -378,14 +401,35 @@ document.addEventListener('DOMContentLoaded', function() {
const manSelect = document.getElementById('manufacturer_select');
const manContainer = document.getElementById('new_manufacturer_container');
function toggleManContainer(val) {
if (val === 'new') {
manContainer.style.display = 'block';
const input = manContainer.querySelector('input');
if(input) input.required = true;
} else {
manContainer.style.display = 'none';
const input = manContainer.querySelector('input');
if(input) input.required = false;
}
}
if (manSelect) {
// Initial check
toggleManContainer(manSelect.value);
// Vanilla Listener (Backup)
manSelect.addEventListener('change', function() {
if (this.value === 'new') {
manContainer.style.display = 'block';
manContainer.querySelector('input').required = true;
} else {
manContainer.style.display = 'none';
manContainer.querySelector('input').required = false;
toggleManContainer(this.value);
});
}
// Tom Select Init
if(manSelect) {
new TomSelect(manSelect, {
create: false,
sortField: { field: "text", direction: "asc" },
onChange: function(value) {
toggleManContainer(value);
}
});
}
@@ -401,8 +445,8 @@ document.addEventListener('DOMContentLoaded', function() {
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>
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>
@@ -416,7 +460,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// Image Handling Logic (copied from add_article.php pattern)
// Image Handling Logic
const imageFileInput = document.getElementById('image_file');
const imagePreview = document.getElementById('imagePreview');
const pasteArea = document.getElementById('pasteArea');
@@ -433,7 +477,6 @@ document.addEventListener('DOMContentLoaded', function() {
}
if (pasteArea) {
// ... existing paste logic ...
pasteArea.addEventListener('paste', function(e) {
e.preventDefault();
const items = (e.clipboardData || window.clipboardData).items;
@@ -451,24 +494,10 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
});
// Add focus to paste area on click so it can receive paste events
pasteArea.setAttribute('tabindex', '0');
pasteArea.addEventListener('click', () => pasteArea.focus());
}
// Tom Select Init
const manSelect = document.getElementById('manufacturer_select');
if(manSelect) {
new TomSelect(manSelect, {
create: false,
sortField: { field: "text", direction: "asc" },
onChange: function(value) {
const event = new Event('change');
manSelect.dispatchEvent(event);
}
});
}
});
</script>
<?php require_once 'footer.php'; ?>
<?php require_once 'footer.php'; ?>

View File

@@ -118,8 +118,8 @@ $conn->close();
<h5 class="mb-0">Verfügbare Artikel</h5>
<div class="row g-2 mt-2">
<div class="col-12"><input type="text" id="filter-text" class="form-control form-control-sm" placeholder="Artikel, Hersteller, Modell suchen..."></div>
<div class="col-md-6"><select id="filter-category" class="form-select form-select-sm"><option value="">Alle Kategorien</option><?php foreach($categories as $c) echo '<option>'.htmlspecialchars($c).'</option>'; ?></select></div>
<div class="col-md-6"><select id="filter-manufacturer" class="form-select form-select-sm"><option value="">Alle Hersteller</option><?php foreach($manufacturers as $m) echo '<option>'.htmlspecialchars($m).'</option>'; ?></select></div>
<div class="col-md-6"><select id="filter-category" class="form-select form-select-sm"><option value="">-- Alle Kategorien --</option><?php foreach($categories as $c) echo '<option>'.htmlspecialchars($c).'</option>'; ?></select></div>
<div class="col-md-6"><select id="filter-manufacturer" class="form-select form-select-sm"><option value="">-- Alle Hersteller --</option><?php foreach($manufacturers as $m) echo '<option>'.htmlspecialchars($m).'</option>'; ?></select></div>
</div>
</div>
<div class="pane-content" id="available-items-list"></div>
@@ -220,11 +220,13 @@ $conn->close();
let childrenModal = null;
document.addEventListener('DOMContentLoaded', () => {
// Initialize Tom Select for filters
// Initialize Tom Select for filters with allowEmptyOption
const tsOptions = {
create: false,
sortField: { field: "text", direction: "asc" },
allowEmptyOption: true,
onChange: function(value) {
// Propagate change
this.input.dispatchEvent(new Event('change'));
}
};
@@ -492,19 +494,6 @@ $conn->close();
const pliId = child.dataset.itemId;
const articleId = child.dataset.articleId;
// Check for container data
let bpId = null;
let bpCompId = null;
if (child.querySelector('.backpack-root-item')) {
// Assuming we store this in dataset or infer from something?
// Actually, the sync relies on existing items usually. But for structure,
// we need to persist these IDs if they exist on the element.
// Since createPackedItemDOM doesn't attach them to dataset, let's fix that or rely on existing data logic?
// Better: attach to dataset in createPackedItemDOM
}
// Correction: The frontend re-sends the whole tree. If we don't send bpId, it might be lost if backend just upserts.
// However, backend 'sync_list' usually recreates or updates.
// Let's add data attributes to the DOM first.
payload.list.push({
pli_id: pliId,
@@ -530,9 +519,8 @@ $conn->close();
// Save scroll positions
const scrollPos = {
avail: document.getElementById('available-items-list').scrollTop,
carrier: document.getElementById('carriers-container').scrollTop // Note: carriers-container itself might not scroll, but the panes do
carrier: document.getElementById('carriers-container').scrollTop
};
// The actual scrolling element is .pane-content
const panes = document.querySelectorAll('.pane-content');
const scrollMap = Array.from(panes).map(p => p.scrollTop);