diff --git a/src/add_article.php b/src/add_article.php index f74714e..d76cc76 100644 --- a/src/add_article.php +++ b/src/add_article.php @@ -1,7 +1,6 @@ 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(); - - - -

Neuen Artikel erstellen

@@ -226,6 +218,7 @@ $conn->close();
" method="post" enctype="multipart/form-data"> +
@@ -272,6 +265,23 @@ $conn->close();
Bild & Produktseite
+ + + +
+ +
+ + + +
+
+ +
Hier klicken & einfügen
@@ -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); }); - + \ No newline at end of file diff --git a/src/edit_backpack.php b/src/edit_backpack.php index 3098ae5..d7553e6 100644 --- a/src/edit_backpack.php +++ b/src/edit_backpack.php @@ -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';
+
+ +
+ + +
+
> @@ -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 = ` - + div.innerHTML = + ` @@ -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); - } - }); - } }); - \ No newline at end of file + diff --git a/src/manage_packing_list_items.php b/src/manage_packing_list_items.php index 819ac34..69c56b1 100644 --- a/src/manage_packing_list_items.php +++ b/src/manage_packing_list_items.php @@ -118,8 +118,8 @@ $conn->close();
Verfügbare Artikel
-
-
+
+
@@ -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);