feat: Artikel können jetzt mit Tags versehen werden
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 33s
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 33s
This commit is contained in:
@@ -230,7 +230,8 @@ Das Projekt basiert auf bewährten Web-Standards:
|
||||
* Die störende JavaScript `confirm()`-Meldung beim Löschen von Artikeln in der Übersicht (`articles.php`) wurde durch ein einheitliches, modernes Bootstrap-Modal ersetzt.
|
||||
|
||||
### 19.06.2026
|
||||
* **Features:**
|
||||
* **Notizen:** Neue Funktion "Notizen" hinzugefügt inkl. Rich-Text-Editor.
|
||||
* **Haushaltsfreigabe:** Notizen können mit dem Haushalt geteilt werden.
|
||||
* **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.
|
||||
* **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.
|
||||
|
||||
@@ -58,6 +58,15 @@ $stmt_man_load->execute();
|
||||
$manufacturers = $stmt_man_load->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
$stmt_man_load->close();
|
||||
|
||||
// Lade Tags
|
||||
$stmt_tags_load = $conn->prepare("SELECT name FROM tags WHERE household_id = ? ORDER BY name ASC");
|
||||
$stmt_tags_load->bind_param("i", $household_id_for_user);
|
||||
$stmt_tags_load->execute();
|
||||
$existing_tags = [];
|
||||
$result_tags = $stmt_tags_load->get_result();
|
||||
while($row = $result_tags->fetch_assoc()) { $existing_tags[] = $row['name']; }
|
||||
$stmt_tags_load->close();
|
||||
|
||||
// Lade Lagerorte
|
||||
$stmt_loc_load = $conn->prepare("SELECT id, name, parent_id FROM storage_locations WHERE user_id IN ($placeholders) ORDER BY parent_id, name");
|
||||
$stmt_loc_load->bind_param($types, ...$household_member_ids);
|
||||
@@ -188,6 +197,40 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
|
||||
if ($stmt_insert_article) {
|
||||
$stmt_insert_article->bind_param("iisiiisssisii", $current_user_id, $is_household_item, $name, $weight_grams, $quantity_owned, $category_id, $consumable, $image_url_for_db, $product_url_for_db, $manufacturer_id, $product_designation_for_db, $storage_location_id, $parent_article_id);
|
||||
if ($stmt_insert_article->execute()) {
|
||||
$new_article_id = $conn->insert_id;
|
||||
|
||||
// Tags verarbeiten
|
||||
$tags_json = $_POST['tags'] ?? '';
|
||||
if (!empty($tags_json)) {
|
||||
$tags_arr = json_decode($tags_json, true);
|
||||
if (is_array($tags_arr)) {
|
||||
foreach ($tags_arr as $t) {
|
||||
$tag_name = trim($t['value']);
|
||||
if (empty($tag_name)) continue;
|
||||
|
||||
$stmt_check_tag = $conn->prepare("SELECT id FROM tags WHERE household_id = ? AND name = ?");
|
||||
$stmt_check_tag->bind_param("is", $household_id_for_user, $tag_name);
|
||||
$stmt_check_tag->execute();
|
||||
$res = $stmt_check_tag->get_result();
|
||||
if ($res->num_rows > 0) {
|
||||
$tag_id = $res->fetch_assoc()['id'];
|
||||
} else {
|
||||
$stmt_add_tag = $conn->prepare("INSERT INTO tags (household_id, name) VALUES (?, ?)");
|
||||
$stmt_add_tag->bind_param("is", $household_id_for_user, $tag_name);
|
||||
$stmt_add_tag->execute();
|
||||
$tag_id = $conn->insert_id;
|
||||
$stmt_add_tag->close();
|
||||
}
|
||||
$stmt_check_tag->close();
|
||||
|
||||
$stmt_link_tag = $conn->prepare("INSERT IGNORE INTO article_tags (article_id, tag_id) VALUES (?, ?)");
|
||||
$stmt_link_tag->bind_param("ii", $new_article_id, $tag_id);
|
||||
$stmt_link_tag->execute();
|
||||
$stmt_link_tag->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($household_id_for_user) {
|
||||
$log_message = htmlspecialchars($_SESSION['username']) . " hat den Artikel '" . htmlspecialchars($name) . "' hinzugefügt.";
|
||||
log_household_action($conn, $household_id_for_user, $current_user_id, $log_message);
|
||||
@@ -209,6 +252,29 @@ $conn->close();
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
|
||||
|
||||
<!-- Tagify CSS/JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@yaireo/tagify"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@yaireo/tagify/dist/tagify.css" rel="stylesheet" type="text/css" />
|
||||
<style>
|
||||
.tagify {
|
||||
--tag-bg: #e9ecef;
|
||||
--tag-hover: #dde0e3;
|
||||
--tag-text-color: #495057;
|
||||
--tag-border-radius: 50px;
|
||||
border-color: #dee2e6;
|
||||
--tags-border-color: #dee2e6;
|
||||
--tags-hover-border-color: #b3d4fc;
|
||||
--tags-focus-border-color: #86b7fe;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.tagify__tag > div {
|
||||
border-radius: 50px;
|
||||
}
|
||||
.tagify__tag__removeBtn {
|
||||
border-radius: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h2 class="h4 mb-0"><i class="fas fa-plus-circle me-2"></i>Neuen Artikel erstellen</h2>
|
||||
@@ -251,6 +317,7 @@ $conn->close();
|
||||
<div class="col-md-6 mb-3"><label for="quantity_owned" class="form-label">Anzahl im Besitz</label><input type="number" class="form-control" id="quantity_owned" name="quantity_owned" value="<?php echo htmlspecialchars($quantity_owned); ?>" min="1"></div>
|
||||
<div class="col-12 mb-3"><label for="category_id" class="form-label">Kategorie</label><select class="form-select" id="category_id" name="category_id"><option value="">-- Keine --</option><option value="new">-- Neue hinzufügen --</option><?php foreach ($categories as $cat): ?><option value="<?php echo htmlspecialchars($cat['id']); ?>"><?php echo htmlspecialchars($cat['name']); ?></option><?php endforeach; ?></select><div id="new_category_container" class="mt-2" style="display: none;"><input type="text" class="form-control" name="new_category_name" placeholder="Name der neuen Kategorie"></div></div>
|
||||
<div class="col-12 mb-3"><label for="storage_location_id" class="form-label">Lagerort</label><select class="form-select" id="storage_location_id" name="storage_location_id"><option value="">-- Kein fester Ort --</option><?php foreach ($storage_locations_structured as $loc1_id => $loc1_data): ?><optgroup label="<?php echo htmlspecialchars($loc1_data['name']); ?>"><?php foreach ($loc1_data['children'] as $loc2): ?><option value="<?php echo $loc2['id']; ?>"><?php echo htmlspecialchars($loc2['name']); ?></option><?php endforeach; ?></optgroup><?php endforeach; ?></select></div>
|
||||
<div class="col-12 mb-3"><label for="tags" class="form-label">Tags</label><input name="tags" class="form-control" placeholder="Tags eingeben oder auswählen..." value="<?php echo htmlspecialchars($_POST['tags'] ?? ''); ?>"></div>
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<div class="form-check form-switch mb-2"><input class="form-check-input" type="checkbox" role="switch" id="consumable" name="consumable" value="1"><label class="form-check-label" for="consumable">Verbrauchsartikel (unbegrenzte Anzahl)</label></div>
|
||||
@@ -464,6 +531,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
if(document.getElementById('parent_article_id')) new TomSelect('#parent_article_id', tsOptionsBase);
|
||||
if(document.getElementById('storage_location_id')) new TomSelect('#storage_location_id', tsOptionsBase);
|
||||
|
||||
// Initialize Tagify
|
||||
const tagInput = document.querySelector('input[name="tags"]');
|
||||
if(tagInput) {
|
||||
new Tagify(tagInput, {
|
||||
whitelist: <?php echo json_encode($existing_tags); ?>,
|
||||
dropdown: {
|
||||
maxItems: 20,
|
||||
classname: "tags-look",
|
||||
enabled: 0,
|
||||
closeOnSelect: false
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php require_once 'footer.php'; ?>
|
||||
@@ -109,12 +109,40 @@ if ($stmt === false) {
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
// Lade Tags für die geladenen Artikel
|
||||
$tags_by_article = [];
|
||||
$article_ids = array_column($articles, '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 ($articles as &$article) {
|
||||
$article['tags'] = $tags_by_article[$article['id']] ?? [];
|
||||
}
|
||||
unset($article);
|
||||
|
||||
$stmt_cat_load = $conn->prepare("SELECT id, name FROM categories WHERE user_id IN ($placeholders) ORDER BY name ASC");
|
||||
$stmt_cat_load->bind_param($types, ...$household_member_ids);
|
||||
$stmt_cat_load->execute();
|
||||
$categories_for_filter = $stmt_cat_load->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
$stmt_cat_load->close();
|
||||
|
||||
$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();
|
||||
|
||||
$stmt_man_load = $conn->prepare("SELECT id, name FROM manufacturers WHERE user_id IN ($placeholders) ORDER BY name ASC");
|
||||
$stmt_man_load->bind_param($types, ...$household_member_ids);
|
||||
$stmt_man_load->execute();
|
||||
@@ -172,6 +200,28 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['bulk_move_articles']))
|
||||
$articles[] = $row;
|
||||
}
|
||||
$stmt->close();
|
||||
|
||||
// Fetch tags for reloaded articles
|
||||
$tags_by_article = [];
|
||||
$article_ids = array_column($articles, '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 ($articles as &$article) {
|
||||
$article['tags'] = $tags_by_article[$article['id']] ?? [];
|
||||
}
|
||||
unset($article);
|
||||
} else {
|
||||
$message = '<div class="alert alert-danger" role="alert">Fehler beim Verschieben: ' . $stmt_update->error . '</div>';
|
||||
}
|
||||
@@ -196,10 +246,11 @@ $conn->close();
|
||||
|
||||
<div class="filter-controls p-3 border-bottom bg-light">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-md-3"><input type="text" id="filter-text" class="form-control form-control-sm" placeholder="Suchen..."></div>
|
||||
<div class="col-md-2"><input type="text" id="filter-text" class="form-control form-control-sm" placeholder="Suchen..."></div>
|
||||
<div class="col-md-2"><select id="filter-category" class="form-select form-select-sm"><option value="">Alle Kategorien</option><?php foreach($categories_for_filter as $cat) echo '<option value="'.$cat['id'].'">'.htmlspecialchars($cat['name']).'</option>'; ?></select></div>
|
||||
<div class="col-md-2"><select id="filter-tag" class="form-select form-select-sm"><option value="">Alle Tags</option><?php foreach($all_filter_tags as $t) echo '<option value="'.htmlspecialchars($t['name']).'">'.htmlspecialchars($t['name']).'</option>'; ?></select></div>
|
||||
<div class="col-md-2"><select id="filter-manufacturer" class="form-select form-select-sm"><option value="">Alle Hersteller</option><?php foreach($manufacturers_for_filter as $man) echo '<option value="'.$man['id'].'">'.htmlspecialchars($man['name']).'</option>'; ?></select></div>
|
||||
<div class="col-md-3"><select id="filter-location" class="form-select form-select-sm"><option value="">Alle Lagerorte</option><?php echo $location_options_html; ?></select></div>
|
||||
<div class="col-md-2"><select id="filter-location" class="form-select form-select-sm"><option value="">Alle Lagerorte</option><?php echo $location_options_html; ?></select></div>
|
||||
<div class="col-md-2 text-end">
|
||||
<div class="btn-group btn-group-sm me-2" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary active" id="btn-view-list" title="Listenansicht"><i class="fas fa-list"></i></button>
|
||||
@@ -435,20 +486,22 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
function renderTable() {
|
||||
const textValue = filterText.value.toLowerCase();
|
||||
const categoryValue = filterCategory.value;
|
||||
const tagValue = document.getElementById('filter-tag') ? document.getElementById('filter-tag').value : '';
|
||||
const manufacturerValue = filterManufacturer.value;
|
||||
const locationValue = filterLocation ? filterLocation.value : '';
|
||||
const isSearching = textValue.length > 0 || categoryValue || manufacturerValue || locationValue;
|
||||
const isSearching = textValue.length > 0 || categoryValue || tagValue || manufacturerValue || locationValue;
|
||||
|
||||
function articleMatchesFilter(article) {
|
||||
const matchesText = article.name.toLowerCase().includes(textValue) ||
|
||||
(article.manufacturer_name && article.manufacturer_name.toLowerCase().includes(textValue)) ||
|
||||
(article.product_designation && article.product_designation.toLowerCase().includes(textValue));
|
||||
const matchesCategory = !categoryValue || article.category_id == categoryValue;
|
||||
const matchesTag = !tagValue || (article.tags && article.tags.some(t => t.name === tagValue));
|
||||
const matchesManufacturer = !manufacturerValue || article.manufacturer_id == manufacturerValue;
|
||||
const locId = article.storage_location_id;
|
||||
const locParentId = article.loc_parent_id;
|
||||
const matchesLocation = !locationValue || locId == locationValue || locParentId == locationValue;
|
||||
return matchesText && matchesCategory && matchesManufacturer && matchesLocation;
|
||||
return matchesText && matchesCategory && matchesTag && matchesManufacturer && matchesLocation;
|
||||
}
|
||||
|
||||
const filteredList = articlesHierarchical.filter(article => {
|
||||
@@ -588,6 +641,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
const metaTextDisplay = metaTextRaw || ' ';
|
||||
const catName = article.category_name || 'Ohne Kat.';
|
||||
const catColor = article.category_color || '#e2e8f0';
|
||||
|
||||
let tagsHtml = '';
|
||||
if (article.tags && article.tags.length > 0) {
|
||||
tagsHtml = '<div class="mt-1 d-flex flex-wrap justify-content-center gap-1" style="max-height: 38px; overflow: hidden;" title="' + article.tags.map(t=>t.name).join(', ') + '">';
|
||||
article.tags.forEach(t => {
|
||||
tagsHtml += `<span class="badge rounded-pill bg-light text-dark border border-secondary" style="font-size: 0.65em; font-weight: 500;">${t.name}</span>`;
|
||||
});
|
||||
tagsHtml += '</div>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="lager-card article-card" title="${article.name}${metaTextRaw ? ' (' + metaTextRaw + ')' : ''}">
|
||||
@@ -600,8 +662,9 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
<div class="lager-title">${article.name}</div>
|
||||
<div class="lager-meta">${metaTextDisplay}</div>
|
||||
<div class="text-muted d-block mb-1" style="font-size:0.75em;">${new Intl.NumberFormat('de-DE').format(article.weight_grams)} g | ${quantityBadge}</div>
|
||||
<div class="lager-meta"><span class="badge border w-100 text-truncate p-1" style="background-color: ${catColor}; color: #fff; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; font-weight: 600;">${catName}</span></div>
|
||||
<div class="lager-controls d-flex justify-content-center gap-1 mt-auto">
|
||||
<div class="lager-meta mb-1"><span class="badge border w-100 text-truncate p-1" style="background-color: ${catColor}; color: #fff; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000; font-weight: 600;">${catName}</span></div>
|
||||
${tagsHtml}
|
||||
<div class="lager-controls d-flex justify-content-center gap-1 mt-auto pt-1">
|
||||
${productLink}
|
||||
${actionButtons}
|
||||
</div>
|
||||
@@ -631,7 +694,17 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
const indentStyle = level > 0 ? `padding-left: ${1.5 * level}rem;` : '';
|
||||
const treePrefix = level > 0 ? `<span class="tree-line" style="left: ${0.5 * level}rem;"></span>` : '';
|
||||
const nameCellContent = `<div class="tree-view-item level-${level}" style="position:relative;">${treePrefix}<span class="item-name-text">${article.name}</span></div>`;
|
||||
|
||||
let tagsHtml = '';
|
||||
if (article.tags && article.tags.length > 0) {
|
||||
tagsHtml = '<div class="mt-1 d-flex flex-wrap gap-1">';
|
||||
article.tags.forEach(t => {
|
||||
tagsHtml += `<span class="badge rounded-pill bg-light text-dark border border-secondary" style="font-size: 0.7em; font-weight: 500;">${t.name}</span>`;
|
||||
});
|
||||
tagsHtml += '</div>';
|
||||
}
|
||||
|
||||
const nameCellContent = `<div class="tree-view-item level-${level}" style="position:relative;">${treePrefix}<div><span class="item-name-text">${article.name}</span>${tagsHtml}</div></div>`;
|
||||
const checkboxHtml = (isOwner || isHouseholdArticle) ? `<input type="checkbox" class="form-check-input bulk-select-checkbox" value="${article.id}">` : '';
|
||||
|
||||
html += `<tr>
|
||||
@@ -774,6 +847,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
filterText.addEventListener('input', renderTable);
|
||||
filterCategory.addEventListener('change', renderTable);
|
||||
if(document.getElementById('filter-tag')) document.getElementById('filter-tag').addEventListener('change', renderTable);
|
||||
filterManufacturer.addEventListener('change', renderTable);
|
||||
if(filterLocation) filterLocation.addEventListener('change', renderTable);
|
||||
|
||||
|
||||
@@ -82,4 +82,23 @@ $conn->query("CREATE TABLE IF NOT EXISTS notes (
|
||||
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");
|
||||
|
||||
// Ensure tags table exists
|
||||
$conn->query("CREATE TABLE IF NOT EXISTS tags (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
household_id INT DEFAULT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
color VARCHAR(20) DEFAULT 'bg-secondary',
|
||||
FOREIGN KEY (household_id) REFERENCES households(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY unique_tag (household_id, name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
|
||||
// Ensure article_tags table exists
|
||||
$conn->query("CREATE TABLE IF NOT EXISTS article_tags (
|
||||
article_id INT NOT NULL,
|
||||
tag_id INT NOT NULL,
|
||||
PRIMARY KEY (article_id, tag_id),
|
||||
FOREIGN KEY (article_id) REFERENCES articles(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci");
|
||||
?>
|
||||
|
||||
@@ -170,6 +170,26 @@ if ($can_edit) {
|
||||
$stmt_parent_articles->execute();
|
||||
$parent_articles = $stmt_parent_articles->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
$stmt_parent_articles->close();
|
||||
|
||||
// Lade Tags für Haushalt
|
||||
$stmt_tags_load = $conn->prepare("SELECT name FROM tags WHERE household_id = ? ORDER BY name ASC");
|
||||
$stmt_tags_load->bind_param("i", $household_id_for_user);
|
||||
$stmt_tags_load->execute();
|
||||
$existing_tags = [];
|
||||
$result_tags = $stmt_tags_load->get_result();
|
||||
while($row = $result_tags->fetch_assoc()) { $existing_tags[] = $row['name']; }
|
||||
$stmt_tags_load->close();
|
||||
|
||||
// Lade aktuelle Tags des Artikels
|
||||
$stmt_current_tags = $conn->prepare("SELECT t.name FROM tags t JOIN article_tags at ON t.id = at.tag_id WHERE at.article_id = ?");
|
||||
$stmt_current_tags->bind_param("i", $article_id);
|
||||
$stmt_current_tags->execute();
|
||||
$current_article_tags = [];
|
||||
$result_ct = $stmt_current_tags->get_result();
|
||||
while($row = $result_ct->fetch_assoc()) { $current_article_tags[] = $row['name']; }
|
||||
$stmt_current_tags->close();
|
||||
|
||||
$current_tags_value = implode(',', $current_article_tags);
|
||||
}
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
|
||||
@@ -244,6 +264,44 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && $can_edit) {
|
||||
if ($stmt_update) {
|
||||
$stmt_update->bind_param("siiisssisiiii", $name, $weight_grams, $quantity_owned, $category_id, $consumable, $image_url_for_db, $product_url_for_db, $manufacturer_id, $product_designation_for_db, $is_household_item, $storage_location_id, $parent_article_id, $article_id);
|
||||
if ($stmt_update->execute()) {
|
||||
// Alte Tags löschen
|
||||
$stmt_del_tags = $conn->prepare("DELETE FROM article_tags WHERE article_id = ?");
|
||||
$stmt_del_tags->bind_param("i", $article_id);
|
||||
$stmt_del_tags->execute();
|
||||
$stmt_del_tags->close();
|
||||
|
||||
// Neue Tags speichern
|
||||
$tags_json = $_POST['tags'] ?? '';
|
||||
if (!empty($tags_json)) {
|
||||
$tags_arr = json_decode($tags_json, true);
|
||||
if (is_array($tags_arr)) {
|
||||
foreach ($tags_arr as $t) {
|
||||
$tag_name = trim($t['value']);
|
||||
if (empty($tag_name)) continue;
|
||||
|
||||
$stmt_check_tag = $conn->prepare("SELECT id FROM tags WHERE household_id = ? AND name = ?");
|
||||
$stmt_check_tag->bind_param("is", $household_id_for_user, $tag_name);
|
||||
$stmt_check_tag->execute();
|
||||
$res = $stmt_check_tag->get_result();
|
||||
if ($res->num_rows > 0) {
|
||||
$tag_id = $res->fetch_assoc()['id'];
|
||||
} else {
|
||||
$stmt_add_tag = $conn->prepare("INSERT INTO tags (household_id, name) VALUES (?, ?)");
|
||||
$stmt_add_tag->bind_param("is", $household_id_for_user, $tag_name);
|
||||
$stmt_add_tag->execute();
|
||||
$tag_id = $conn->insert_id;
|
||||
$stmt_add_tag->close();
|
||||
}
|
||||
$stmt_check_tag->close();
|
||||
|
||||
$stmt_link_tag = $conn->prepare("INSERT IGNORE INTO article_tags (article_id, tag_id) VALUES (?, ?)");
|
||||
$stmt_link_tag->bind_param("ii", $article_id, $tag_id);
|
||||
$stmt_link_tag->execute();
|
||||
$stmt_link_tag->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($household_id_for_user) {
|
||||
$log_message = htmlspecialchars($_SESSION['username']) . " hat den Artikel '" . htmlspecialchars($name) . "' bearbeitet.";
|
||||
log_household_action($conn, $household_id_for_user, $current_user_id, $log_message);
|
||||
@@ -264,9 +322,28 @@ $conn->close();
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
|
||||
|
||||
<!-- Tom Select CSS/JS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/css/tom-select.bootstrap5.min.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js"></script>
|
||||
<!-- Tagify CSS/JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@yaireo/tagify"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/@yaireo/tagify/dist/tagify.css" rel="stylesheet" type="text/css" />
|
||||
<style>
|
||||
.tagify {
|
||||
--tag-bg: #e9ecef;
|
||||
--tag-hover: #dde0e3;
|
||||
--tag-text-color: #495057;
|
||||
--tag-border-radius: 50px;
|
||||
border-color: #dee2e6;
|
||||
--tags-border-color: #dee2e6;
|
||||
--tags-hover-border-color: #b3d4fc;
|
||||
--tags-focus-border-color: #86b7fe;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.tagify__tag > div {
|
||||
border-radius: 50px;
|
||||
}
|
||||
.tagify__tag__removeBtn {
|
||||
border-radius: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
@@ -310,6 +387,7 @@ $conn->close();
|
||||
<div class="col-md-6 mb-3"><label for="quantity_owned" class="form-label">Anzahl im Besitz</label><input type="number" class="form-control" id="quantity_owned" name="quantity_owned" value="<?php echo htmlspecialchars($quantity_owned); ?>" min="1" <?php if($consumable) echo 'disabled'; ?>></div>
|
||||
<div class="col-12 mb-3"><label for="category_id" class="form-label">Kategorie</label><select class="form-select" id="category_id" name="category_id"><option value="">-- Keine Kategorie --</option><option value="new">-- Neue Kategorie hinzufügen --</option><?php foreach ($categories as $cat): ?><option value="<?php echo htmlspecialchars($cat['id']); ?>" <?php if ($category_id == $cat['id']) echo 'selected'; ?>><?php echo htmlspecialchars($cat['name']); ?></option><?php endforeach; ?></select><div id="new_category_container" class="mt-2" style="display: none;"><input type="text" class="form-control" name="new_category_name" placeholder="Name der neuen Kategorie"></div></div>
|
||||
<div class="col-12 mb-3"><label for="storage_location_id" class="form-label">Lagerort</label><select class="form-select" id="storage_location_id" name="storage_location_id"><option value="">-- Kein fester Ort --</option><?php foreach ($storage_locations_structured as $loc1_id => $loc1_data): ?><optgroup label="<?php echo htmlspecialchars($loc1_data['name']); ?>"><?php foreach ($loc1_data['children'] as $loc2): ?><option value="<?php echo $loc2['id']; ?>" <?php if ($storage_location_id == $loc2['id']) echo 'selected'; ?>><?php echo htmlspecialchars($loc2['name']); ?></option><?php endforeach; ?></optgroup><?php endforeach; ?></select></div>
|
||||
<div class="col-12 mb-3"><label for="tags" class="form-label">Tags</label><input name="tags" class="form-control" placeholder="Tags eingeben oder auswählen..." value="<?php echo htmlspecialchars(isset($_POST['tags']) ? $_POST['tags'] : $current_tags_value); ?>"></div>
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<div class="form-check form-switch mb-2"><input class="form-check-input" type="checkbox" role="switch" id="consumable" name="consumable" value="1" <?php echo $consumable ? 'checked' : ''; ?>><label class="form-check-label" for="consumable">Verbrauchsartikel (unbegrenzte Anzahl)</label></div>
|
||||
@@ -445,6 +523,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if(document.getElementById('category_id')) new TomSelect('#category_id', tsOptions);
|
||||
if(document.getElementById('parent_article_id')) new TomSelect('#parent_article_id', tsOptions);
|
||||
if(document.getElementById('storage_location_id')) new TomSelect('#storage_location_id', tsOptions);
|
||||
|
||||
// Initialize Tagify
|
||||
const tagInput = document.querySelector('input[name="tags"]');
|
||||
if(tagInput) {
|
||||
new Tagify(tagInput, {
|
||||
whitelist: <?php echo json_encode($existing_tags); ?>,
|
||||
dropdown: {
|
||||
maxItems: 20,
|
||||
classname: "tags-look",
|
||||
enabled: 0,
|
||||
closeOnSelect: false
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php require_once 'footer.php'; ?>
|
||||
|
||||
Reference in New Issue
Block a user