diff --git a/README.md b/README.md index 0d302ce..6641a14 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,8 @@ Das Projekt basiert auf bewährten Web-Standards: * **Features:** * **Notizen:** Neue Funktion "Notizen" hinzugefügt inkl. Rich-Text-Editor. * **Haushaltsfreigabe:** Notizen können mit dem Haushalt geteilt werden. - * **Tags:** Artikel können nun mit Tags versehen werden, die im modernen Badge-Design angezeigt werden und nach denen in der Übersicht gefiltert werden kann. + * **Tags:** Artikel können nun mit Tags versehen werden, die im modernen Badge-Design angezeigt werden und nach denen in der Übersicht gefiltert werden kann. Im Hauptmenü gibt es nun den Punkt "Tags", um diese zentral umzubenennen oder zu löschen. * **Besitzer-Zuweisung:** Die Option "Für den gesamten Haushalt freigeben" wurde entfernt, da nun standardmäßig alle Artikel im Haushalt geteilt werden. Stattdessen kann nun jedem Artikel ein expliziter Besitzer (einzelnes Familienmitglied oder "Gemeinsam") zugewiesen werden. Die Artikelliste zeigt den Besitzer übersichtlich an. + * **Anzeigename:** In sämtlichen Bereichen der Anwendung (Listen, Dropdowns, Haushalt etc.) wird nun konsistent der Anzeigename (Display Name) statt des Login-Namens verwendet, sofern einer vergeben wurde. + * **Packlisten Editor:** In der Bearbeitungsansicht von Packlisten (Phase 1) werden im Lagerbestand nun ebenfalls die zugewiesenen Tags und Besitzer angezeigt. Zudem kann nach Tags gefiltert werden. * **Design-Update:** Die Notizen-Übersichtsseite wurde an das Design der ToDo-Listen angepasst (Split-View, List-Groups, weiße Notizhintergründe). Der Texteditor bietet nun deutsche Tooltips. Beim Speichern einer Notiz wird nun besser zurück in die Übersicht navigiert. Das Layout der Metainformationen sowie der Buttons wurde weiter optimiert. Die Notizen werden in der Liste alphabetisch sortiert. Der Editor wurde aktualisiert: Er besitzt nun eine feste Höhe mit internem Scrollbereich, sodass die Formatierungsleiste immer sichtbar bleibt. Zudem wurde eine Tabellen-Funktion hinzugefügt. diff --git a/src/add_article.php b/src/add_article.php index 7959f33..1766e91 100644 --- a/src/add_article.php +++ b/src/add_article.php @@ -46,7 +46,7 @@ $placeholders = implode(',', array_fill(0, count($household_member_ids), '?')); $types = str_repeat('i', count($household_member_ids)); // Lade alle Haushaltsmitglieder für das Besitzer-Dropdown -$stmt_all_members = $conn->prepare("SELECT id, username FROM users WHERE id IN ($placeholders) ORDER BY username ASC"); +$stmt_all_members = $conn->prepare("SELECT id, COALESCE(NULLIF(display_name, ''), username) AS username FROM users WHERE id IN ($placeholders) ORDER BY username ASC"); $stmt_all_members->bind_param($types, ...$household_member_ids); $stmt_all_members->execute(); $all_household_members = $stmt_all_members->get_result()->fetch_all(MYSQLI_ASSOC); diff --git a/src/articles.php b/src/articles.php index 67c4aa6..3520485 100644 --- a/src/articles.php +++ b/src/articles.php @@ -82,7 +82,7 @@ $placeholders = implode(',', array_fill(0, count($household_member_ids), '?')); $types = str_repeat('i', count($household_member_ids)); $sql = "SELECT a.id, a.name, a.weight_grams, a.quantity_owned, a.product_url, a.consumable, a.image_url, a.user_id, a.parent_article_id, - u.username as creator_name, a.household_id, a.product_designation, a.owner_id, o.username as owner_name, + COALESCE(NULLIF(u.display_name, ''), u.username) as creator_name, a.household_id, a.product_designation, a.owner_id, COALESCE(NULLIF(o.display_name, ''), o.username) as owner_name, c.id AS category_id, c.name AS category_name, c.color AS category_color, m.id AS manufacturer_id, m.name AS manufacturer_name, a.storage_location_id, l2.parent_id AS loc_parent_id, l2.name AS location_level2_name, l1.name AS location_level1_name diff --git a/src/edit_article.php b/src/edit_article.php index 21611f8..63af08f 100644 --- a/src/edit_article.php +++ b/src/edit_article.php @@ -147,7 +147,7 @@ if ($can_edit) { $types = str_repeat('i', count($household_member_ids)); // Lade alle Haushaltsmitglieder für das Besitzer-Dropdown - $stmt_all_members = $conn->prepare("SELECT id, username FROM users WHERE id IN ($placeholders) ORDER BY username ASC"); + $stmt_all_members = $conn->prepare("SELECT id, COALESCE(NULLIF(display_name, ''), username) AS username FROM users WHERE id IN ($placeholders) ORDER BY username ASC"); $stmt_all_members->bind_param($types, ...$household_member_ids); $stmt_all_members->execute(); $all_household_members = $stmt_all_members->get_result()->fetch_all(MYSQLI_ASSOC); diff --git a/src/header.php b/src/header.php index 0c6921c..5a1a5e0 100644 --- a/src/header.php +++ b/src/header.php @@ -73,7 +73,8 @@ if (isset($_SESSION['user_id'])) { - + + diff --git a/src/manage_packing_list_items.php b/src/manage_packing_list_items.php index a43aacf..18af655 100644 --- a/src/manage_packing_list_items.php +++ b/src/manage_packing_list_items.php @@ -69,25 +69,46 @@ if ($current_user_household_id) { $placeholders = implode(',', array_fill(0, count($household_member_ids), '?')); $types = str_repeat('i', count($household_member_ids)); -$sql_all_articles = "SELECT a.id, a.name, a.weight_grams, a.quantity_owned, a.product_designation, a.consumable, a.parent_article_id, a.image_url, c.name as category_name, m.name as manufacturer_name, sl.name as storage_location_name +$sql_all_articles = "SELECT a.id, a.name, a.weight_grams, a.quantity_owned, a.product_designation, a.consumable, a.parent_article_id, a.image_url, a.owner_id, c.name as category_name, m.name as manufacturer_name, sl.name as storage_location_name, COALESCE(NULLIF(o.display_name, ''), o.username) as owner_name FROM articles a LEFT JOIN categories c ON a.category_id = c.id LEFT JOIN manufacturers m ON a.manufacturer_id = m.id LEFT JOIN storage_locations sl ON a.storage_location_id = sl.id - WHERE a.user_id IN ($placeholders)"; -$all_params = $household_member_ids; -$all_types = $types; -if ($current_user_household_id) { - $sql_all_articles .= " OR a.household_id = ?"; - $all_params[] = $current_user_household_id; - $all_types .= 'i'; -} -$stmt_all_articles = $conn->prepare($sql_all_articles); + LEFT JOIN users o ON a.owner_id = o.id + WHERE (a.user_id IN ($placeholders) $stmt_all_articles->bind_param($all_types, ...$all_params); $stmt_all_articles->execute(); $all_articles_raw = $stmt_all_articles->get_result()->fetch_all(MYSQLI_ASSOC); $stmt_all_articles->close(); +// Lade Tags für all_articles +$tags_by_article = []; +$article_ids = array_column($all_articles_raw, 'id'); +if (!empty($article_ids)) { + $id_placeholders = implode(',', array_fill(0, count($article_ids), '?')); + $stmt_tags = $conn->prepare("SELECT at.article_id, t.name, t.color FROM tags t JOIN article_tags at ON t.id = at.tag_id WHERE at.article_id IN ($id_placeholders) ORDER BY t.name ASC"); + if ($stmt_tags) { + $types_tags = str_repeat('i', count($article_ids)); + $stmt_tags->bind_param($types_tags, ...$article_ids); + $stmt_tags->execute(); + $res_tags = $stmt_tags->get_result(); + while($row = $res_tags->fetch_assoc()) { + $tags_by_article[$row['article_id']][] = ['name' => $row['name'], 'color' => $row['color']]; + } + $stmt_tags->close(); + } +} +foreach ($all_articles_raw as &$article) { + $article['tags'] = $tags_by_article[$article['id']] ?? []; +} +unset($article); + +$stmt_all_tags = $conn->prepare("SELECT id, name FROM tags WHERE household_id = ? ORDER BY name ASC"); +$stmt_all_tags->bind_param("i", $current_user_household_id); +$stmt_all_tags->execute(); +$all_filter_tags = $stmt_all_tags->get_result()->fetch_all(MYSQLI_ASSOC); +$stmt_all_tags->close(); + $sql_items = "SELECT pli.id, pli.article_id, pli.quantity, pli.parent_packing_list_item_id, pli.carrier_user_id, pli.backpack_id, pli.backpack_compartment_id, COALESCE( a.name, @@ -171,7 +192,8 @@ $conn->close(); border: 1px solid #eee; border-radius: 8px; padding: 6px; text-align: center; background: #fff; display: flex; flex-direction: column; transition: transform 0.1s, box-shadow 0.1s; - height: 205px; + height: 235px; + position: relative; } .lager-card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.05); } .lager-img-wrapper { @@ -263,6 +285,7 @@ $conn->close();
+
@@ -416,6 +439,7 @@ $conn->close(); document.addEventListener('DOMContentLoaded', () => { const tsOptions = { create: false, sortField: { field: "text", direction: "asc" }, allowEmptyOption: true, onChange: function() { this.input.dispatchEvent(new Event('change')); } }; new TomSelect('#filter-category', tsOptions); + if (document.getElementById('filter-tag')) new TomSelect('#filter-tag', tsOptions); new TomSelect('#filter-manufacturer', tsOptions); new TomSelect('#group-by', tsOptions); new TomSelect('#sort-by', { create: false, allowEmptyOption: false, onChange: function() { this.input.dispatchEvent(new Event('change')); } }); @@ -462,6 +486,7 @@ $conn->close(); ['input', 'change'].forEach(evt => { document.getElementById('filter-text').addEventListener(evt, renderLager); document.getElementById('filter-category').addEventListener(evt, renderLager); + if (document.getElementById('filter-tag')) document.getElementById('filter-tag').addEventListener(evt, renderLager); document.getElementById('filter-manufacturer').addEventListener(evt, renderLager); document.getElementById('group-by').addEventListener(evt, renderLager); document.getElementById('sort-by').addEventListener(evt, renderLager); @@ -515,6 +540,7 @@ $conn->close(); function renderLager() { const filterText = document.getElementById('filter-text').value.toLowerCase(); const filterCategory = document.getElementById('filter-category').value; + const filterTag = document.getElementById('filter-tag') ? document.getElementById('filter-tag').value : ''; const filterManufacturer = document.getElementById('filter-manufacturer').value; const groupBy = document.getElementById('group-by').value; const sortBy = document.getElementById('sort-by').value; @@ -549,9 +575,10 @@ $conn->close(); const matchesText = article.name.toLowerCase().includes(filterText) || (article.manufacturer_name && article.manufacturer_name.toLowerCase().includes(filterText)) || (article.product_designation && article.product_designation.toLowerCase().includes(filterText)); const matchesCategory = !filterCategory || article.category_name === filterCategory; + const matchesTag = !filterTag || (article.tags && article.tags.some(t => t.name === filterTag)); const matchesManufacturer = !filterManufacturer || article.manufacturer_name === filterManufacturer; - if (matchesText && matchesCategory && matchesManufacturer) { + if (matchesText && matchesCategory && matchesTag && matchesManufacturer) { const aid = String(article.id); const qtyTable = tableQuantities[aid] || 0; const qtyBackpack = backpackQuantities[aid] || 0; @@ -564,16 +591,30 @@ $conn->close(); const imgUrl = article.image_url ? article.image_url : 'assets/images/keinbild.png'; const plusBtnClass = qtyTable > 0 ? 'btn-success' : 'btn-outline-primary'; + const ownerBadge = article.owner_id ? ` ${article.owner_name}` : ``; + let tagsHtml = ''; + if (article.tags && article.tags.length > 0) { + tagsHtml = '
'; + article.tags.forEach(t => { + tagsHtml += `${t.name}`; + }); + tagsHtml += '
'; + } + const metaText = [article.manufacturer_name, article.product_designation].filter(Boolean).join(' - '); const cardHtml = `
+
+ ${ownerBadge} +
${article.name}
${metaText}
${article.weight_grams}g + ${tagsHtml}
diff --git a/src/tags.php b/src/tags.php new file mode 100644 index 0000000..ceed114 --- /dev/null +++ b/src/tags.php @@ -0,0 +1,159 @@ +prepare("SELECT household_id FROM users WHERE id = ?"); +$stmt_household->bind_param("i", $current_user_id); +$stmt_household->execute(); +$household_id = $stmt_household->get_result()->fetch_assoc()['household_id']; +$stmt_household->close(); + +if (!$household_id) { + echo '
Tags können nur in einem Haushalt verwaltet werden. Bitte gründe oder trete einem Haushalt bei.
'; + require_once 'footer.php'; + exit; +} + +if ($_SERVER["REQUEST_METHOD"] == "POST") { + if (isset($_POST['edit_tag'])) { + $tag_id = intval($_POST['tag_id']); + $tag_name = trim($_POST['tag_name']); + + if (!empty($tag_name) && $tag_id > 0) { + $stmt = $conn->prepare("UPDATE tags SET name = ? WHERE id = ? AND household_id = ?"); + $stmt->bind_param("sii", $tag_name, $tag_id, $household_id); + if ($stmt->execute()) { + $log_message = htmlspecialchars($_SESSION['username']) . " hat den Tag '" . htmlspecialchars($tag_name) . "' bearbeitet."; + log_household_action($conn, $household_id, $current_user_id, $log_message); + $message = ''; + } else { + $message = ''; + } + $stmt->close(); + } + } +} +elseif (isset($_GET['action']) && $_GET['action'] == 'delete' && isset($_GET['id'])) { + $tag_id = intval($_GET['id']); + + $stmt_check = $conn->prepare("SELECT name FROM tags WHERE id = ? AND household_id = ?"); + $stmt_check->bind_param("ii", $tag_id, $household_id); + $stmt_check->execute(); + $res_check = $stmt_check->get_result(); + + if ($res_check->num_rows > 0) { + $tag_name = $res_check->fetch_assoc()['name']; + $stmt_delete = $conn->prepare("DELETE FROM tags WHERE id = ?"); + $stmt_delete->bind_param("i", $tag_id); + if ($stmt_delete->execute()) { + $log_message = htmlspecialchars($_SESSION['username']) . " hat den Tag '" . htmlspecialchars($tag_name) . "' gelöscht."; + log_household_action($conn, $household_id, $current_user_id, $log_message); + $message = ''; + } else { + $message = ''; + } + $stmt_delete->close(); + } else { + $message = ''; + } + $stmt_check->close(); +} + +$tags_query = $conn->prepare("SELECT id, name FROM tags WHERE household_id = ? ORDER BY name ASC"); +$tags_query->bind_param("i", $household_id); +$tags_query->execute(); +$tags_list = $tags_query->get_result()->fetch_all(MYSQLI_ASSOC); +$tags_query->close(); +$conn->close(); +?> + +
+
+

Tags im Haushalt

+
+
+ + +
Bestehende Tags
+ +
Keine Tags gefunden. Tags können direkt beim Erstellen oder Bearbeiten von Artikeln hinzugefügt werden.
+ +
+ +
+
+ +
+
+ + + + +
+
+ +
+ +
+
+ + + + + +