Files
packliste/articles.php
2025-12-03 23:23:12 +00:00

392 lines
19 KiB
PHP

<?php
// articles.php - Zeigt alle verfügbaren Artikel an
// FINALE VERSION: Gruppiert nach Kategorien, Sticky Header, Accordion
$page_title = "Artikel im Haushalt";
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 'household_actions.php';
require_once 'header.php';
$current_user_id = $_SESSION['user_id'];
$message = '';
// Lade Haushalts-ID und Mitglieder-IDs
$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();
$household_member_ids = [$current_user_id];
if ($current_user_household_id) {
$stmt_members = $conn->prepare("SELECT id FROM users WHERE household_id = ?");
$stmt_members->bind_param("i", $current_user_household_id);
$stmt_members->execute();
$result_members = $stmt_members->get_result();
while ($row = $result_members->fetch_assoc()) {
if (!in_array($row['id'], $household_member_ids)) {
$household_member_ids[] = $row['id'];
}
}
$stmt_members->close();
}
// Löschlogik
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['delete_article_id'])) {
$article_to_delete_id = intval($_POST['delete_article_id']);
$stmt_get_article = $conn->prepare("SELECT name, user_id, household_id FROM articles WHERE id = ?");
$stmt_get_article->bind_param("i", $article_to_delete_id);
$stmt_get_article->execute();
$article_to_delete = $stmt_get_article->get_result()->fetch_assoc();
$stmt_get_article->close();
$can_delete = false;
if ($article_to_delete) {
$is_owner = ($article_to_delete['user_id'] == $current_user_id);
$is_household_article = !empty($article_to_delete['household_id']) && $article_to_delete['household_id'] == $current_user_household_id;
if ($is_owner || $is_household_article) {
$can_delete = true;
}
}
if ($can_delete) {
$stmt_delete = $conn->prepare("DELETE FROM articles WHERE id = ?");
$stmt_delete->bind_param("i", $article_to_delete_id);
if ($stmt_delete->execute()) {
if ($current_user_household_id) {
$log_message = htmlspecialchars($_SESSION['username']) . " hat den Artikel '" . htmlspecialchars($article_to_delete['name']) . "' gelöscht.";
log_household_action($conn, $current_user_household_id, $current_user_id, $log_message);
}
$message = '<div class="alert alert-success" role="alert">Artikel erfolgreich gelöscht.</div>';
} else {
$message = '<div class="alert alert-danger" role="alert">Fehler beim Löschen des Artikels: ' . $stmt_delete->error . '</div>';
}
$stmt_delete->close();
} else {
$message = '<div class="alert alert-danger" role="alert">Sie sind nicht berechtigt, diesen Artikel zu löschen.</div>';
}
}
$articles = [];
$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,
c.id AS category_id, c.name AS category_name,
m.id AS manufacturer_id, m.name AS manufacturer_name,
l2.name AS location_level2_name, l1.name AS location_level1_name
FROM articles a
JOIN users u ON a.user_id = u.id
LEFT JOIN categories c ON a.category_id = c.id
LEFT JOIN manufacturers m ON a.manufacturer_id = m.id
LEFT JOIN storage_locations l2 ON a.storage_location_id = l2.id
LEFT JOIN storage_locations l1 ON l2.parent_id = l1.id
WHERE a.user_id IN ($placeholders) OR a.household_id = ?
ORDER BY c.name ASC, a.name ASC"; // Pre-sort by category helps, but JS does the grouping
$stmt = $conn->prepare($sql);
if ($stmt === false) {
$message .= '<div class="alert alert-danger" role="alert">SQL Prepare-Fehler (Artikel laden): ' . $conn->error . '</div>';
} else {
$all_params = array_merge($household_member_ids, [$current_user_household_id]);
$all_types = $types . 'i';
$stmt->bind_param($all_types, ...$all_params);
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
$articles[] = $row;
}
$stmt->close();
}
$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_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();
$manufacturers_for_filter = $stmt_man_load->get_result()->fetch_all(MYSQLI_ASSOC);
$stmt_man_load->close();
$conn->close();
?>
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0"><i class="fas fa-boxes me-2"></i>Artikel im Haushalt</h2>
<a href="add_article.php" class="btn btn-sm btn-outline-light"><i class="fas fa-plus me-2"></i>Neuen Artikel hinzufügen</a>
</div>
<div class="card-body p-0">
<?php echo $message; ?>
<div class="filter-controls p-3 border-bottom bg-light">
<div class="row g-2">
<div class="col-md-5"><input type="text" id="filter-text" class="form-control form-control-sm" placeholder="Suchen..."></div>
<div class="col-md-3"><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-4"><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>
</div>
<div class="table-responsive" style="max-height: 75vh;">
<table class="table table-hover mb-0">
<thead>
<tr>
<th style="width: 60px;">Bild</th>
<th style="width: 40px;"><i class="fas fa-link"></i></th>
<th>Name</th>
<th>Hersteller</th>
<th>Modell/Typ</th>
<th>Gewicht</th>
<th>Anzahl</th>
<th>Kategorie</th>
<th>Lagerort</th>
<th>Besitzer</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="articlesTableBody">
</tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" id="imageModal" tabindex="-1" aria-labelledby="imageModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="imageModalLabel">Bildansicht</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<img src="" id="fullImage" class="modal-image" alt="Vollbildansicht">
</div>
</div>
</div>
</div>
<div id="image-preview-tooltip" class="image-preview-tooltip"></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const articlesData = <?php echo json_encode($articles); ?>;
const currentUserId = <?php echo $current_user_id; ?>;
const currentUserHouseholdId = <?php echo json_encode($current_user_household_id); ?>;
const collapseDefault = <?php echo isset($_SESSION['articles_collapse_default']) && $_SESSION['articles_collapse_default'] == '1' ? 'true' : 'false'; ?>;
const articlesById = {};
// ... (Rest of data prep) ...
articlesData.forEach(article => {
articlesById[article.id] = { ...article, children: [] };
});
const articlesHierarchical = [];
Object.values(articlesById).forEach(article => {
if (article.parent_article_id && articlesById[article.parent_article_id]) {
articlesById[article.parent_article_id].children.push(article);
} else {
articlesHierarchical.push(article);
}
});
const tableBody = document.getElementById('articlesTableBody');
const filterText = document.getElementById('filter-text');
const filterCategory = document.getElementById('filter-category');
const filterManufacturer = document.getElementById('filter-manufacturer');
const collapsedCategories = new Set();
// Initialize collapsed state based on user setting
if (collapseDefault) {
// We need to know all category names to collapse them initially
// We can derive them from data or just handle it dynamically in renderTable,
// but for cleanest initial state, let's populate the Set.
const uniqueCategories = new Set(articlesData.map(a => a.category_name || 'Ohne Kategorie'));
uniqueCategories.forEach(c => collapsedCategories.add(c));
}
function renderTable() {
const textValue = filterText.value.toLowerCase();
const categoryValue = filterCategory.value;
const manufacturerValue = filterManufacturer.value;
const isSearching = textValue.length > 0 || categoryValue || manufacturerValue;
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 matchesManufacturer = !manufacturerValue || article.manufacturer_id == manufacturerValue;
return matchesText && matchesCategory && matchesManufacturer;
}
const filteredList = articlesHierarchical.filter(article => {
if (articleMatchesFilter(article)) return true;
if (article.children.length > 0) return article.children.some(child => articleMatchesFilter(child));
return false;
});
const groupedArticles = {};
filteredList.forEach(article => {
const catName = article.category_name || 'Ohne Kategorie';
if (!groupedArticles[catName]) groupedArticles[catName] = [];
groupedArticles[catName].push(article);
});
const sortedCategories = Object.keys(groupedArticles).sort((a, b) => {
if (a === 'Ohne Kategorie') return 1;
if (b === 'Ohne Kategorie') return -1;
return a.localeCompare(b);
});
let tableHTML = '';
if (sortedCategories.length === 0) {
tableHTML = '<tr><td colspan="12" class="text-center text-muted p-4">Keine Artikel gefunden.</td></tr>';
} else {
sortedCategories.forEach(catName => {
const items = groupedArticles[catName];
// If searching, expand all. If not, respect collapsed state.
const isCollapsed = collapsedCategories.has(catName) && !isSearching;
const count = items.reduce((acc, item) => acc + 1 + item.children.length, 0);
tableHTML += `<tr class="category-header ${isCollapsed ? 'collapsed' : ''}" data-category="${catName}">
<td colspan="12">
<i class="fas fa-chevron-down me-2"></i> ${catName}
<span class="badge bg-light text-dark rounded-pill ms-2">${count}</span>
</td>
</tr>`;
if (!isCollapsed) {
items.forEach(article => {
tableHTML += generateRowHTML(article);
});
}
});
}
tableBody.innerHTML = tableHTML;
initializeInteractivity();
}
function generateRowHTML(article, level = 0) {
let html = '';
const imagePath = article.image_url ? article.image_url : 'keinbild.png';
const productLink = article.product_url ? `<a href="${article.product_url}" target="_blank" class="btn btn-sm btn-outline-secondary" data-bs-toggle="tooltip" title="Produktseite öffnen"><i class="fas fa-external-link-alt"></i></a>` : '';
const creatorBadge = article.user_id != currentUserId ? `<span class="badge bg-info">${article.creator_name}</span>` : `<span class="badge bg-primary">Ich</span>`;
const householdBadge = article.household_id ? `<span class="badge bg-success" data-bs-toggle="tooltip" title="Für den Haushalt freigegeben"><i class="fas fa-users"></i></span>` : `<span class="badge bg-secondary" data-bs-toggle="tooltip" title="Privater Artikel"><i class="fas fa-user"></i></span>`;
const consumableIcon = article.consumable == 1 ? ` <i class="fas fa-cookie-bite text-warning" data-bs-toggle="tooltip" title="Verbrauchsartikel"></i>` : '';
const quantityBadge = article.consumable == 1 ? `<span class="badge bg-secondary rounded-pill" data-bs-toggle="tooltip" title="Verbrauchsartikel">&infin;</span>` : `<span class="badge bg-secondary rounded-pill">${article.quantity_owned}</span>`;
let actionButtons = '';
const isOwner = (article.user_id == currentUserId);
const isHouseholdArticle = (article.household_id && article.household_id == currentUserHouseholdId);
if (isOwner || isHouseholdArticle) {
actionButtons = `<div class="btn-group">
<a href="edit_article.php?id=${article.id}" class="btn btn-sm btn-outline-primary" data-bs-toggle="tooltip" title="Bearbeiten"><i class="fas fa-edit"></i></a>`;
actionButtons += `<form action="articles.php" method="post" class="d-inline" onsubmit="return confirm('Sind Sie sicher?');"><input type="hidden" name="delete_article_id" value="${article.id}"><button type="submit" class="btn btn-sm btn-outline-danger" data-bs-toggle="tooltip" title="Löschen"><i class="fas fa-trash"></i></button></form>`;
actionButtons += `</div>`;
}
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>`;
html += `<tr>
<td><img src="${imagePath}" alt="${article.name}" style="width: 40px; height: 40px; object-fit: cover; border-radius: 5px;" class="article-image-trigger" data-preview-url="${imagePath}"></td>
<td class="text-center">${productLink}</td>
<td>${nameCellContent}</td>
<td>${article.manufacturer_name || '---'}</td>
<td>${article.product_designation || '---'}</td>
<td>${new Intl.NumberFormat('de-DE').format(article.weight_grams)} g</td>
<td>${quantityBadge}</td>
<td>${article.category_name || '---'}</td>
<td>${article.location_level1_name ? `<small>${article.location_level1_name} <i class="fas fa-chevron-right fa-xs"></i> ${article.location_level2_name}</small>` : '---'}</td>
<td>${creatorBadge}</td>
<td>${householdBadge}${consumableIcon}</td>
<td>${actionButtons}</td>
</tr>`;
if (article.children.length > 0) {
article.children.forEach(child => {
html += generateRowHTML(child, level + 1);
});
}
return html;
}
const tooltip = document.getElementById('image-preview-tooltip');
const imageModal = new bootstrap.Modal(document.getElementById('imageModal'));
const fullImageElement = document.getElementById('fullImage');
const imageModalLabel = document.getElementById('imageModalLabel');
function initializeInteractivity() {
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
if (tooltip) {
document.querySelectorAll('.article-image-trigger').forEach(trigger => {
trigger.addEventListener('mouseover', function (e) {
const previewUrl = this.getAttribute('data-preview-url');
if(previewUrl && !previewUrl.endsWith('keinbild.png')) {
tooltip.style.backgroundImage = `url('${previewUrl}')`;
tooltip.style.display = 'block';
}
});
trigger.addEventListener('mousemove', function (e) {
tooltip.style.left = e.pageX + 15 + 'px';
tooltip.style.top = e.pageY + 15 + 'px';
});
trigger.addEventListener('mouseout', function (e) {
tooltip.style.display = 'none';
});
trigger.addEventListener('click', function(e) {
const fullImageUrl = this.getAttribute('data-preview-url');
const articleName = this.getAttribute('alt');
if(fullImageUrl && !fullImageUrl.endsWith('keinbild.png')) {
fullImageElement.src = fullImageUrl;
imageModalLabel.textContent = articleName;
imageModal.show();
}
});
});
}
document.querySelectorAll('.category-header').forEach(header => {
header.addEventListener('click', function() {
const catName = this.getAttribute('data-category');
if (collapsedCategories.has(catName)) {
collapsedCategories.delete(catName);
} else {
collapsedCategories.add(catName);
}
renderTable();
});
});
}
filterText.addEventListener('input', renderTable);
filterCategory.addEventListener('change', renderTable);
filterManufacturer.addEventListener('change', renderTable);
renderTable();
});
</script>
<?php require_once 'footer.php'; ?>