From 480c4d4cd930e4e785909821f7ccbbb922ac3c32 Mon Sep 17 00:00:00 2001 From: Gemini Agent Date: Wed, 13 May 2026 06:36:18 +0000 Subject: [PATCH] Feature: ToDo Lists, Display Names and Sync logic bugfix - Created ToDo list functionality (CRUD) with household support. - Linked ToDo lists to packing lists and displayed them on the packing list overview page. - Added 'display_name' to users table, allowing separation of login name and display name. - Fixed the irregular '+' click bug in Phase 1 by completely decoupling the local UI state updates from the backend sync latency, relying purely on DOM syncing. --- migrate.php | 39 +++++++ src/api_packing_list_handler.php | 4 +- src/header.php | 19 +++- src/index.php | 8 ++ src/manage_packing_list_items.php | 152 +++++++++++++++++-------- src/packing_list_detail.php | 57 ++++++++++ src/todo_lists.php | 183 ++++++++++++++++++++++++++++++ src/user_profile.php | 26 +++++ 8 files changed, 439 insertions(+), 49 deletions(-) create mode 100644 migrate.php create mode 100644 src/todo_lists.php diff --git a/migrate.php b/migrate.php new file mode 100644 index 0000000..1f899fa --- /dev/null +++ b/migrate.php @@ -0,0 +1,39 @@ +query($q)) { + echo "Error: " . $conn->error . "\n"; + } else { + echo "Success.\n"; + } +} +$conn->close(); +?> \ No newline at end of file diff --git a/src/api_packing_list_handler.php b/src/api_packing_list_handler.php index 282c2e7..38510ba 100644 --- a/src/api_packing_list_handler.php +++ b/src/api_packing_list_handler.php @@ -148,7 +148,9 @@ try { // Update Compartment Names $conn->query("UPDATE packing_list_items pli JOIN backpack_compartments bc ON pli.backpack_compartment_id = bc.id SET pli.name = bc.name WHERE pli.packing_list_id = $packing_list_id AND pli.backpack_compartment_id IS NOT NULL"); - break; + $conn->commit(); + echo json_encode(['success' => true, 'id_map' => $id_map, 'items' => get_all_items($conn, $packing_list_id)]); + exit; case 'delete_item': $item_id = intval($data['item_id']); diff --git a/src/header.php b/src/header.php index 926474f..e0dd96c 100644 --- a/src/header.php +++ b/src/header.php @@ -6,6 +6,20 @@ if (session_status() == PHP_SESSION_NONE) { } $current_username = isset($_SESSION['username']) ? $_SESSION['username'] : 'Gast'; +$display_name = $current_username; + +if (isset($_SESSION['user_id'])) { + $stmt_dn = $conn->prepare("SELECT display_name FROM users WHERE id = ?"); + $stmt_dn->bind_param("i", $_SESSION['user_id']); + $stmt_dn->execute(); + $res_dn = $stmt_dn->get_result(); + if ($row_dn = $res_dn->fetch_assoc()) { + if (!empty($row_dn['display_name'])) { + $display_name = $row_dn['display_name']; + } + } + $stmt_dn->close(); +} $pending_invitation = null; if (isset($_SESSION['user_id'])) { @@ -57,14 +71,15 @@ if (isset($_SESSION['user_id'])) { - + +
-
Hallo,
+
Hallo,
diff --git a/src/index.php b/src/index.php index ebffccf..5660013 100644 --- a/src/index.php +++ b/src/index.php @@ -12,6 +12,14 @@ if (!isset($_SESSION['user_id'])) { } require_once 'db_connect.php'; + +// --- AUTO MIGRATIONS --- +$conn->query("ALTER TABLE users ADD COLUMN display_name VARCHAR(255) DEFAULT NULL"); +$conn->query("CREATE TABLE IF NOT EXISTS todo_lists (id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, household_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"); +$conn->query("CREATE TABLE IF NOT EXISTS todo_items (id INT AUTO_INCREMENT PRIMARY KEY, todo_list_id INT NOT NULL, title VARCHAR(255) NOT NULL, is_completed TINYINT(1) DEFAULT 0, order_index INT DEFAULT 0, FOREIGN KEY (todo_list_id) REFERENCES todo_lists(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"); +$conn->query("ALTER TABLE packing_lists ADD COLUMN todo_list_id INT DEFAULT NULL"); +$conn->query("ALTER TABLE packing_lists ADD CONSTRAINT fk_packing_list_todo FOREIGN KEY (todo_list_id) REFERENCES todo_lists(id) ON DELETE SET NULL"); + require_once 'header.php'; $current_user_id = $_SESSION['user_id']; diff --git a/src/manage_packing_list_items.php b/src/manage_packing_list_items.php index b902aff..70ec097 100644 --- a/src/manage_packing_list_items.php +++ b/src/manage_packing_list_items.php @@ -99,7 +99,7 @@ $packed_items_raw = $stmt_items->get_result()->fetch_all(MYSQLI_ASSOC); $stmt_items->close(); $carriers_data = []; -$stmt_carriers = $conn->prepare("SELECT u.id, u.username FROM users u JOIN packing_list_carriers plc ON u.id = plc.user_id WHERE plc.packing_list_id = ? ORDER BY u.username"); +$stmt_carriers = $conn->prepare("SELECT u.id, COALESCE(u.display_name, u.username) AS username FROM users u JOIN packing_list_carriers plc ON u.id = plc.user_id WHERE plc.packing_list_id = ? ORDER BY username"); $stmt_carriers->bind_param("i", $packing_list_id); $stmt_carriers->execute(); $carriers_result = $stmt_carriers->get_result(); @@ -586,15 +586,69 @@ $conn->close(); } function adjustTable(articleId, delta, includeChildren = false) { - sendApiRequest({ action: 'adjust_table_quantity', article_id: articleId, delta: delta, include_children: includeChildren }) - .then(newItems => { - if(Array.isArray(newItems)) packedItems = newItems; - // Restore scroll positions - const panes = document.querySelectorAll('.pane-content'); - const scrollMap = Array.from(panes).map(p => p.scrollTop); - fullRender(); - Array.from(panes).forEach((p, i) => { if (scrollMap[i] !== undefined) p.scrollTop = scrollMap[i]; }); - }); + let existingIndex = packedItems.findIndex(item => String(item.article_id) === String(articleId) && item.carrier_user_id == null && !item.backpack_id && !item.backpack_compartment_id && !item.parent_packing_list_item_id); + + if (delta > 0) { + if (existingIndex > -1) { + packedItems[existingIndex].quantity = parseInt(packedItems[existingIndex].quantity, 10) + delta; + } else { + const article = allArticles.find(a => String(a.id) === String(articleId)); + if (article) { + const newItemId = 'new_' + Date.now() + Math.floor(Math.random() * 1000); + packedItems.push({ + id: newItemId, + article_id: article.id, + quantity: delta, + name: article.name, + weight_grams: article.weight_grams, + product_designation: article.product_designation, + consumable: article.consumable, + image_url: article.image_url, + manufacturer_name: article.manufacturer_name, + backpack_id: null, + backpack_compartment_id: null, + carrier_user_id: null, + parent_packing_list_item_id: null + }); + + if (includeChildren) { + const children = allArticles.filter(a => String(a.parent_article_id) === String(articleId)); + children.forEach((child, idx) => { + packedItems.push({ + id: 'new_' + Date.now() + idx + 1000, + article_id: child.id, + quantity: delta, + name: child.name, + weight_grams: child.weight_grams, + product_designation: child.product_designation, + consumable: child.consumable, + image_url: child.image_url, + manufacturer_name: child.manufacturer_name, + backpack_id: null, + backpack_compartment_id: null, + carrier_user_id: null, + parent_packing_list_item_id: newItemId + }); + }); + } + } + } + } else if (delta < 0) { + if (existingIndex > -1) { + if (packedItems[existingIndex].quantity > 1) { + packedItems[existingIndex].quantity = parseInt(packedItems[existingIndex].quantity, 10) - 1; + } else { + packedItems.splice(existingIndex, 1); + } + } + } + + const panes = document.querySelectorAll('.pane-content'); + const scrollMap = Array.from(panes).map(p => p.scrollTop); + fullRender(); + Array.from(panes).forEach((p, i) => { if (scrollMap[i] !== undefined) p.scrollTop = scrollMap[i]; }); + + syncListState(); } function handleSortableEnd(evt) { @@ -812,12 +866,23 @@ $conn->close(); traverse(rootList, carrierId, null); }); - sendApiRequest(payload).then(newItems => { - if(Array.isArray(newItems)) packedItems = newItems; - const panes = document.querySelectorAll('.pane-content'); - const scrollMap = Array.from(panes).map(p => p.scrollTop); - fullRender(); - Array.from(panes).forEach((p, i) => { if (scrollMap[i] !== undefined) p.scrollTop = scrollMap[i]; }); + sendApiRequest(payload).then(res => { + if (res) { + if (res.items && Array.isArray(res.items)) { + packedItems = res.items; + } + if (res.id_map) { + Object.keys(res.id_map).forEach(oldId => { + const els = document.querySelectorAll(`.packed-item-container[data-item-id="${oldId}"]`); + els.forEach(el => el.dataset.itemId = res.id_map[oldId]); + }); + } + // Re-render only the Lager grid silently so +/- buttons get updated without disrupting the right panes + const panes = document.querySelectorAll('.pane-content'); + const scrollMap = Array.from(panes).map(p => p.scrollTop); + renderLager(); + Array.from(panes).forEach((p, i) => { if (scrollMap[i] !== undefined) p.scrollTop = scrollMap[i]; }); + } }).catch(err => { console.error("Sync failed:", err); alert("Fehler beim Speichern! Bitte Seite neu laden."); @@ -844,19 +909,15 @@ $conn->close(); const selectEl = document.getElementById('move-destination-select'); selectEl.innerHTML = ''; - // Populate select with all containers const containers = packedItems.filter(item => item.backpack_id || item.backpack_compartment_id); - // First add backpacks containers.filter(c => c.backpack_id).forEach(bp => { const opt = document.createElement('option'); opt.value = bp.id; opt.textContent = `👜 Rucksack: ${bp.name}`; - // Get the carrier Name const carrier = carriers.find(c => String(c.id) === String(bp.carrier_user_id)); if (carrier) opt.textContent += ` (${carrier.username})`; selectEl.appendChild(opt); - // Add its compartments containers.filter(c => c.parent_packing_list_item_id === bp.id).forEach(comp => { const optComp = document.createElement('option'); optComp.value = comp.id; @@ -872,9 +933,19 @@ $conn->close(); window.moveItemModalInstance.hide(); if (destId === 'table') { - document.getElementById('table-container').appendChild(itemToMoveEl); + const articleId = itemToMoveEl.dataset.articleId; + const qtyInput = itemToMoveEl.querySelector('.quantity-input'); + const qty = parseInt(qtyInput ? qtyInput.value : 1, 10); + itemToMoveEl.remove(); + const tableContainer = document.getElementById('table-container'); + const existingEl = Array.from(tableContainer.children).find(el => el.classList.contains('packed-item-container') && String(el.dataset.articleId) === String(articleId)); + if (existingEl) { + const exInput = existingEl.querySelector('.quantity-input'); + if (exInput) exInput.value = parseInt(exInput.value, 10) + qty; + } else { + tableContainer.appendChild(itemToMoveEl); + } } else { - // Find the container element const destEl = document.querySelector(`.packed-item-container[data-item-id="${destId}"] > .nested-sortable`); if (destEl) { destEl.appendChild(itemToMoveEl); @@ -895,10 +966,7 @@ $conn->close(); if (isTable) { if (confirm('Diesen Artikel wirklich aus der Liste entfernen?')) { itemEl.remove(); - sendApiRequest({ action: 'delete_item', item_id: itemId }).then(newItems => { - if(Array.isArray(newItems)) packedItems = newItems; - fullRender(); - }); + syncListState(); } } else { itemToRemoveId = itemId; @@ -908,10 +976,7 @@ $conn->close(); document.getElementById('btn-remove-delete').onclick = () => { window.removeItemModalInstance.hide(); itemToRemoveEl.remove(); - sendApiRequest({ action: 'delete_item', item_id: itemToRemoveId }).then(newItems => { - if(Array.isArray(newItems)) packedItems = newItems; - fullRender(); - }); + syncListState(); }; document.getElementById('btn-remove-totable').onclick = () => { window.removeItemModalInstance.hide(); @@ -919,12 +984,16 @@ $conn->close(); const qty = parseInt(qtyInput ? qtyInput.value : 1, 10); const articleId = itemToRemoveEl.dataset.articleId; itemToRemoveEl.remove(); - sendApiRequest({ action: 'adjust_table_quantity', article_id: articleId, delta: qty }) - .then(() => sendApiRequest({ action: 'delete_item', item_id: itemToRemoveId })) - .then(newItems => { - if(Array.isArray(newItems)) packedItems = newItems; - fullRender(); - }); + + const tableContainer = document.getElementById('table-container'); + const existingEl = Array.from(tableContainer.children).find(el => el.classList.contains('packed-item-container') && String(el.dataset.articleId) === String(articleId)); + if (existingEl) { + const exInput = existingEl.querySelector('.quantity-input'); + if (exInput) exInput.value = parseInt(exInput.value, 10) + qty; + } else { + tableContainer.appendChild(itemToRemoveEl); + } + syncListState(); }; } window.removeItemModalInstance.show(); @@ -935,16 +1004,7 @@ $conn->close(); function handleQuantityChange(e) { const input = e.target; if (input.classList.contains('quantity-input')) { - const itemEl = input.closest('.packed-item-container'); - const itemId = itemEl.dataset.itemId; - const newQuantity = input.value; - clearTimeout(input.dataset.timeout); - input.dataset.timeout = setTimeout(() => { - sendApiRequest({ action: 'update_quantity', item_id: itemId, quantity: newQuantity }).then(newItems => { - if(Array.isArray(newItems)) packedItems = newItems; - fullRender(); - }); - }, 500); + syncListState(); } } diff --git a/src/packing_list_detail.php b/src/packing_list_detail.php index e26dfaf..a1371ad 100644 --- a/src/packing_list_detail.php +++ b/src/packing_list_detail.php @@ -15,6 +15,19 @@ require_once 'header.php'; $current_user_id = $_SESSION['user_id']; $packing_list_id = isset($_GET['id']) ? intval($_GET['id']) : 0; $packing_list = null; + +// Handle Todo Toggle +if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['toggle_todo_item'])) { + $item_id = intval($_POST['item_id']); + $status = intval($_POST['status']); + $stmt = $conn->prepare("UPDATE todo_items SET is_completed = ? WHERE id = ?"); + $stmt->bind_param("ii", $status, $item_id); + $stmt->execute(); + $stmt->close(); + // Redirect to avoid form resubmission + header("Location: packing_list_detail.php?id=" . $packing_list_id); + exit; +} $total_weight_grams = 0; $total_consumable_weight = 0; $weight_by_category = []; @@ -58,6 +71,23 @@ if ($result->num_rows > 0) { } $stmt_list_owner->close(); +$todo_items = []; +$todo_list_name = ''; +if (!empty($packing_list['todo_list_id'])) { + $stmt_tl = $conn->prepare("SELECT name FROM todo_lists WHERE id = ?"); + $stmt_tl->bind_param("i", $packing_list['todo_list_id']); + $stmt_tl->execute(); + $res_tl = $stmt_tl->get_result(); + if ($row = $res_tl->fetch_assoc()) $todo_list_name = $row['name']; + $stmt_tl->close(); + + $stmt_td = $conn->prepare("SELECT * FROM todo_items WHERE todo_list_id = ? ORDER BY is_completed ASC, id ASC"); + $stmt_td->bind_param("i", $packing_list['todo_list_id']); + $stmt_td->execute(); + $todo_items = $stmt_td->get_result()->fetch_all(MYSQLI_ASSOC); + $stmt_td->close(); +} + $page_title = "Packliste: " . htmlspecialchars($packing_list['name']); // FIX: Join Categories also for Backpacks @@ -324,6 +354,33 @@ function render_item_row($item, $level, $items_by_parent) {
+ +
+
ToDo:
+
+
    + +
  • +
    + +
    + style="width:1.3em; height:1.3em; cursor:pointer;"> + + +
    +
    +
  • + + +
  • Keine Einträge in dieser Liste.
  • + +
+
+
+ +
Statistiken
diff --git a/src/todo_lists.php b/src/todo_lists.php new file mode 100644 index 0000000..df6a206 --- /dev/null +++ b/src/todo_lists.php @@ -0,0 +1,183 @@ +prepare("SELECT household_id FROM users WHERE id = ?"); +$stmt_household->bind_param("i", $current_user_id); +$stmt_household->execute(); +$current_user_household_id = $stmt_household->get_result()->fetch_assoc()['household_id']; +$stmt_household->close(); + +// Actions +if ($_SERVER["REQUEST_METHOD"] == "POST") { + if (isset($_POST['create_list'])) { + $name = trim($_POST['list_name']); + if (!empty($name)) { + $stmt = $conn->prepare("INSERT INTO todo_lists (user_id, household_id, name) VALUES (?, ?, ?)"); + $stmt->bind_param("iis", $current_user_id, $current_user_household_id, $name); + if ($stmt->execute()) { + $message = "ToDo-Liste erstellt."; + } else { + $message = "Fehler beim Erstellen."; + } + $stmt->close(); + } + } elseif (isset($_POST['delete_list'])) { + $list_id = intval($_POST['list_id']); + $stmt = $conn->prepare("DELETE FROM todo_lists WHERE id = ? AND (user_id = ? OR household_id = ?)"); + $stmt->bind_param("iii", $list_id, $current_user_id, $current_user_household_id); + $stmt->execute(); + $stmt->close(); + } elseif (isset($_POST['add_item'])) { + $list_id = intval($_POST['list_id']); + $title = trim($_POST['item_title']); + if (!empty($title)) { + $stmt = $conn->prepare("INSERT INTO todo_items (todo_list_id, title) VALUES (?, ?)"); + $stmt->bind_param("is", $list_id, $title); + $stmt->execute(); + $stmt->close(); + } + } elseif (isset($_POST['delete_item'])) { + $item_id = intval($_POST['item_id']); + $stmt = $conn->prepare("DELETE FROM todo_items WHERE id = ?"); + $stmt->bind_param("i", $item_id); + $stmt->execute(); + $stmt->close(); + } elseif (isset($_POST['toggle_item'])) { + $item_id = intval($_POST['item_id']); + $status = intval($_POST['status']); + $stmt = $conn->prepare("UPDATE todo_items SET is_completed = ? WHERE id = ?"); + $stmt->bind_param("ii", $status, $item_id); + $stmt->execute(); + $stmt->close(); + } +} + +// Fetch all lists +$sql_lists = "SELECT * FROM todo_lists WHERE user_id = ? OR (household_id IS NOT NULL AND household_id = ?) ORDER BY created_at DESC"; +$stmt = $conn->prepare($sql_lists); +$stmt->bind_param("ii", $current_user_id, $current_user_household_id); +$stmt->execute(); +$lists_res = $stmt->get_result(); +$todo_lists = $lists_res->fetch_all(MYSQLI_ASSOC); +$stmt->close(); + +$active_list_id = isset($_GET['list_id']) ? intval($_GET['list_id']) : (!empty($todo_lists) ? $todo_lists[0]['id'] : 0); + +?> +
+

ToDo-Listen

+ + " . htmlspecialchars($message) . "
"; ?> + +
+ +
+
+
+
Meine Listen
+
+
+ + + +
+ + +
+
+ + +
Keine Listen vorhanden.
+ +
+
+
+
+ + +
+
+
+
+
+ + +
+ 0): ?> + prepare("SELECT * FROM todo_items WHERE todo_list_id = ? ORDER BY is_completed ASC, id ASC"); + $stmt->bind_param("i", $active_list_id); + $stmt->execute(); + $items = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); + $stmt->close(); + + $active_list_name = ''; + foreach ($todo_lists as $l) if ($l['id'] == $active_list_id) $active_list_name = $l['name']; + ?> +
+
+
+
+
+
    + +
  • +
    + + +
    + style="width:1.5em; height:1.5em; cursor:pointer;"> + + +
    +
    +
    + + + +
    +
  • + + +
  • Noch keine Punkte auf dieser Liste.
  • + +
+ +
+ +
+ + +
+
+
+
+ +
Wähle links eine Liste aus oder erstelle eine neue.
+ +
+
+
+ +close(); +?> \ No newline at end of file diff --git a/src/user_profile.php b/src/user_profile.php index 1ce2724..b240229 100644 --- a/src/user_profile.php +++ b/src/user_profile.php @@ -29,11 +29,25 @@ while ($row = $result_settings->fetch_assoc()) { $stmt_load_settings->close(); $items_per_page = $user_settings['items_per_page'] ?? 10; +// Lade display_name +$stmt_load_user = $conn->prepare("SELECT display_name FROM users WHERE id = ?"); +$stmt_load_user->bind_param("i", $current_user_id); +$stmt_load_user->execute(); +$current_display_name = $stmt_load_user->get_result()->fetch_assoc()['display_name'] ?? ''; +$stmt_load_user->close(); + // FORMULARVERARBEITUNG if ($_SERVER["REQUEST_METHOD"] == "POST" && !isset($_POST['action'])) { if (isset($_POST['save_settings'])) { $new_items_per_page = intval($_POST['items_per_page']); $new_collapse_default = isset($_POST['articles_collapse_default']) ? '1' : '0'; + $new_display_name = trim($_POST['display_name']); + + $stmt_update_name = $conn->prepare("UPDATE users SET display_name = ? WHERE id = ?"); + $stmt_update_name->bind_param("si", $new_display_name, $current_user_id); + $stmt_update_name->execute(); + $stmt_update_name->close(); + $current_display_name = $new_display_name; if (in_array($new_items_per_page, [10, 25, 50, 100])) { $sql_save = "INSERT INTO user_settings (user_id, setting_key, setting_value) VALUES (?, 'items_per_page', ?), (?, 'articles_collapse_default', ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)"; @@ -179,6 +193,18 @@ require_once 'header.php';
+
+
+
Anzeigename
+

Wie möchtest du im System (z.B. in Packlisten) für andere genannt werden?

+
+
+ +
+
+ +
+
Tabellenansicht