Feature: ToDo Lists, Display Names and Sync logic bugfix
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 15s
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:
@@ -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']);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
183
src/todo_lists.php
Normal 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();
|
||||
?>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user