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) {