Feature: ToDo Lists, Display Names and Sync logic bugfix
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 15s

- 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.
This commit is contained in:
Gemini Agent
2026-05-13 06:36:18 +00:00
parent ca00ca3469
commit 480c4d4cd9
8 changed files with 439 additions and 49 deletions

39
migrate.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
require_once 'src/db_connect.php';
$queries = [
"ALTER TABLE users ADD COLUMN display_name VARCHAR(255) DEFAULT NULL",
"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
)",
"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
)",
"ALTER TABLE packing_lists ADD COLUMN todo_list_id INT DEFAULT NULL",
"ALTER TABLE packing_lists ADD CONSTRAINT fk_packing_list_todo FOREIGN KEY (todo_list_id) REFERENCES todo_lists(id) ON DELETE SET NULL"
];
foreach ($queries as $q) {
echo "Running: $q\n";
if (!$conn->query($q)) {
echo "Error: " . $conn->error . "\n";
} else {
echo "Success.\n";
}
}
$conn->close();
?>

View File

@@ -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']);

View File

@@ -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'])) {
<li class="nav-item"><a class="nav-link" href="backpacks.php"><i class="fas fa-hiking fa-fw"></i>Rucksäcke</a></li>
<li class="nav-item"><a class="nav-link" href="packing_lists.php"><i class="fas fa-clipboard-list fa-fw"></i>Packlisten</a></li>
<li class="nav-item"><a class="nav-link" href="storage_locations.php"><i class="fas fa-archive fa-fw"></i>Lagerorte</a></li>
<li class="nav-item"><a class="nav-link" href="categories.php"><i class="fas fa-tags fa-fw"></i>Kategorien</a></li>
<li class="nav-item"><a class="nav-link" href="todo_lists.php"><i class="fas fa-list-check fa-fw"></i>ToDo-Listen</a></li>
<li class="nav-item"><a class="nav-link" href="household.php"><i class="fas fa-users-cog fa-fw"></i>Haushalt</a></li>
<li class="nav-item"><a class="nav-link" href="manufacturers.php"><i class="fas fa-industry fa-fw"></i>Hersteller</a></li>
<li class="nav-item"><a class="nav-link" href="household.php"><i class="fas fa-users-cog fa-fw"></i>Haushalt</a></li>
<li class="nav-item"><a class="nav-link" href="user_profile.php"><i class="fas fa-user-cog fa-fw"></i>Profil</a></li>
<li class="nav-item"><a class="nav-link" href="help.php"><i class="fas fa-question-circle fa-fw"></i>Hilfe</a></li>
</ul>
<div class="username-display">
<div>Hallo, <strong><?php echo htmlspecialchars($current_username); ?></strong></div>
<div>Hallo, <strong><?php echo htmlspecialchars($display_name); ?></strong></div>
<div class="mt-3">
<a class="btn btn-sm btn-outline-light w-100" href="logout.php"><i class="fas fa-sign-out-alt me-2"></i>Abmelden</a>
</div>

View File

@@ -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'];

View File

@@ -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 = '<option value="table">Zurück auf den Tisch</option>';
// 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();
}
}

View File

@@ -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) {
</div>
</div>
<div class="col-lg-4">
<?php if (!empty($packing_list['todo_list_id'])): ?>
<div class="card mb-4 border-primary">
<div class="card-header bg-primary text-white"><h5 class="mb-0"><i class="fas fa-list-check me-2"></i>ToDo: <?php echo htmlspecialchars($todo_list_name); ?></h5></div>
<div class="card-body p-0">
<ul class="list-group list-group-flush">
<?php foreach ($todo_items as $item): ?>
<li class="list-group-item d-flex justify-content-between align-items-center py-2 px-3">
<form method="post" style="display:inline; margin:0;" class="flex-grow-1">
<input type="hidden" name="item_id" value="<?php echo $item['id']; ?>">
<div class="form-check d-flex align-items-center">
<input class="form-check-input me-2" type="checkbox" onChange="this.form.submit()" name="status" value="<?php echo $item['is_completed'] ? '0' : '1'; ?>" <?php echo $item['is_completed'] ? 'checked' : ''; ?> style="width:1.3em; height:1.3em; cursor:pointer;">
<input type="hidden" name="toggle_todo_item" value="1">
<label class="form-check-label ms-2 <?php echo $item['is_completed'] ? 'text-decoration-line-through text-muted' : ''; ?>" style="cursor:pointer; width:100%; margin-top:2px;">
<?php echo htmlspecialchars($item['title']); ?>
</label>
</div>
</form>
</li>
<?php endforeach; ?>
<?php if (empty($todo_items)): ?>
<li class="list-group-item text-muted text-center py-3">Keine Einträge in dieser Liste.</li>
<?php endif; ?>
</ul>
</div>
</div>
<?php endif; ?>
<div class="card h-100">
<div class="card-header card-header-stats"><h5 class="mb-0"><i class="fas fa-chart-bar me-2"></i>Statistiken</h5></div>
<div class="card-body">

183
src/todo_lists.php Normal file
View File

@@ -0,0 +1,183 @@
<?php
// todo_lists.php - Verwaltung von ToDo-Listen
$page_title = "ToDo-Listen";
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}
require_once 'db_connect.php';
require_once 'header.php';
$current_user_id = $_SESSION['user_id'];
$message = '';
// Load household info
$stmt_household = $conn->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);
?>
<div class="container-fluid p-0">
<h1 class="h3 mb-4 text-gray-800"><i class="fas fa-list-check me-2"></i>ToDo-Listen</h1>
<?php if ($message) echo "<div class='alert alert-info'>" . htmlspecialchars($message) . "</div>"; ?>
<div class="row">
<!-- List Selection -->
<div class="col-md-4 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h6 class="m-0 font-weight-bold">Meine Listen</h6>
</div>
<div class="list-group list-group-flush">
<?php foreach ($todo_lists as $list): ?>
<a href="?list_id=<?php echo $list['id']; ?>" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center <?php echo $active_list_id == $list['id'] ? 'active' : ''; ?>">
<?php echo htmlspecialchars($list['name']); ?>
<form method="post" style="display:inline;" onsubmit="return confirm('Liste wirklich löschen?');">
<input type="hidden" name="list_id" value="<?php echo $list['id']; ?>">
<button type="submit" name="delete_list" class="btn btn-sm btn-link text-<?php echo $active_list_id == $list['id'] ? 'white' : 'danger'; ?> p-0"><i class="fas fa-trash"></i></button>
</form>
</a>
<?php endforeach; ?>
<?php if (empty($todo_lists)): ?>
<div class="list-group-item text-muted">Keine Listen vorhanden.</div>
<?php endif; ?>
</div>
<div class="card-body bg-light">
<form method="post">
<div class="input-group">
<input type="text" name="list_name" class="form-control" placeholder="Neue Liste erstellen..." required>
<button type="submit" name="create_list" class="btn btn-primary"><i class="fas fa-plus"></i></button>
</div>
</form>
</div>
</div>
</div>
<!-- List Items -->
<div class="col-md-8 mb-4">
<?php if ($active_list_id > 0): ?>
<?php
$stmt = $conn->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'];
?>
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary"><?php echo htmlspecialchars($active_list_name); ?></h6>
</div>
<div class="card-body">
<ul class="list-group list-group-flush mb-3">
<?php foreach ($items as $item): ?>
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
<form method="post" style="display:inline; margin:0;" class="flex-grow-1">
<input type="hidden" name="item_id" value="<?php echo $item['id']; ?>">
<input type="hidden" name="list_id" value="<?php echo $active_list_id; ?>">
<div class="form-check d-flex align-items-center">
<input class="form-check-input me-2" type="checkbox" onChange="this.form.submit()" name="status" value="<?php echo $item['is_completed'] ? '0' : '1'; ?>" <?php echo $item['is_completed'] ? 'checked' : ''; ?> style="width:1.5em; height:1.5em; cursor:pointer;">
<input type="hidden" name="toggle_item" value="1">
<label class="form-check-label ms-2 <?php echo $item['is_completed'] ? 'text-decoration-line-through text-muted' : ''; ?>" style="cursor:pointer; font-size:1.1em; width:100%; margin-top:3px;">
<?php echo htmlspecialchars($item['title']); ?>
</label>
</div>
</form>
<form method="post" style="display:inline;">
<input type="hidden" name="item_id" value="<?php echo $item['id']; ?>">
<input type="hidden" name="list_id" value="<?php echo $active_list_id; ?>">
<button type="submit" name="delete_item" class="btn btn-sm btn-outline-danger"><i class="fas fa-times"></i></button>
</form>
</li>
<?php endforeach; ?>
<?php if (empty($items)): ?>
<li class="list-group-item text-muted px-0">Noch keine Punkte auf dieser Liste.</li>
<?php endif; ?>
</ul>
<form method="post">
<input type="hidden" name="list_id" value="<?php echo $active_list_id; ?>">
<div class="input-group">
<input type="text" name="item_title" class="form-control" placeholder="Neuer Punkt..." required>
<button type="submit" name="add_item" class="btn btn-success"><i class="fas fa-plus me-2"></i>Hinzufügen</button>
</div>
</form>
</div>
</div>
<?php else: ?>
<div class="alert alert-secondary">Wähle links eine Liste aus oder erstelle eine neue.</div>
<?php endif; ?>
</div>
</div>
</div>
<?php
require_once 'footer.php';
if (isset($conn) && $conn instanceof mysqli) $conn->close();
?>

View File

@@ -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';
</div>
<div class="card-body p-4">
<form action="user_profile.php" method="post">
<div class="row mb-4">
<div class="col-md-8">
<h6 class="fw-bold">Anzeigename</h6>
<p class="text-muted small mb-0">Wie möchtest du im System (z.B. in Packlisten) für andere genannt werden?</p>
</div>
<div class="col-md-4">
<input type="text" class="form-control" name="display_name" value="<?php echo htmlspecialchars($current_display_name); ?>" placeholder="Max Mustermann">
</div>
</div>
<hr class="my-4">
<div class="row mb-4">
<div class="col-md-8">
<h6 class="fw-bold">Tabellenansicht</h6>