Quelltext des aktuellen Standes
<?php $this->layout('layouts/main', [
'page_title' => 'Lagerbestand',
'current_route' => 'stock',
'full_width' => true,
]); ?>
<div class="sk-wrap">
<!-- Content Header -->
<div class="content-header">
<div>
<h1><?= $this->te('Lagerbestand') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('Lagerbestand') ?></li>
</ol>
</div>
<?php if ($can_manage): ?>
<div class="content-header-actions">
<button type="button" class="btn btn-primary" onclick="skOpenArticle()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="16" height="16"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<?= $this->te('Artikel anlegen') ?>
</button>
</div>
<?php endif; ?>
</div>
<div class="page-body">
<!-- KPI Small Boxes -->
<div class="kpi-row">
<div class="kpi-col">
<div class="small-box bg-info">
<div class="inner">
<h3><?= (int)($totals['total_items'] ?? 0) ?></h3>
<p><?= $this->te('Artikel') ?></p>
</div>
<div class="icon"><i class="fas fa-boxes"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box bg-primary">
<div class="inner">
<h3><?= (int)($totals['total_quantity'] ?? 0) ?></h3>
<p><?= $this->te('Stück gesamt') ?></p>
</div>
<div class="icon"><i class="fas fa-cubes"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box <?= (int)($totals['low_stock'] ?? 0) > 0 ? 'bg-warning' : 'bg-secondary' ?>">
<div class="inner">
<h3><?= (int)($totals['low_stock'] ?? 0) ?></h3>
<p><?= $this->te('Niedriger Bestand') ?></p>
</div>
<div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box <?= (int)($totals['out_of_stock'] ?? 0) > 0 ? 'bg-danger' : 'bg-secondary' ?>">
<div class="inner">
<h3><?= (int)($totals['out_of_stock'] ?? 0) ?></h3>
<p><?= $this->te('Nicht verfügbar') ?></p>
</div>
<div class="icon"><i class="fas fa-times-circle"></i></div>
</div>
</div>
</div>
<!-- Filter & Kategorien -->
<div class="card card-outline card-primary">
<div class="filter-card-body">
<!-- Suchfeld -->
<form action="/stock" method="GET" id="skFilterForm" class="sk-search">
<?php if ($selected_category): ?>
<input type="hidden" name="category" value="<?= $selected_category ?>">
<?php endif; ?>
<input type="search" name="search" class="form-control form-control-sm"
placeholder="<?= $this->te('Artikel suchen…') ?>" value="<?= $this->e($search) ?>" autocomplete="off">
<button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-search"></i> <?= $this->te('Suchen') ?></button>
<?php if ($search || $selected_category): ?>
<a href="/stock" class="btn btn-default btn-sm" title="<?= $this->te('Filter zurücksetzen') ?>"><i class="fas fa-times"></i></a>
<?php endif; ?>
</form>
<!-- Kategorie-Chips -->
<?php if (!empty($categories)): ?>
<div class="sk-cat-chips">
<a href="/stock<?= $search ? '?search='.urlencode($search) : '' ?>"
class="sk-cat-chip <?= !$selected_category ? 'active' : '' ?>">
<span class="sk-cat-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>
</span>
<span class="sk-cat-name"><?= $this->te('Alle') ?></span>
<span class="sk-cat-count"><?= (int)($totals['total_items'] ?? 0) ?></span>
</a>
<?php foreach ($categories as $cat): ?>
<a href="/stock?category=<?= $cat['id'] ?><?= $search ? '&search='.urlencode($search) : '' ?>"
class="sk-cat-chip <?= $selected_category === (int)$cat['id'] ? 'active' : '' ?>">
<span class="sk-cat-icon"><?= $this->e($cat['icon']) ?></span>
<span class="sk-cat-name"><?= $this->e($cat['name']) ?></span>
<span class="sk-cat-count"><?= (int)($cat['item_count'] ?? 0) ?></span>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Tabellenbereich -->
<?php if (empty($items)): ?>
<div class="empty-state">
<span class="empty-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" width="44" height="44"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg></span>
<h2><?= ($search || $selected_category) ? 'Keine Artikel gefunden' : 'Noch keine Artikel' ?></h2>
<p><?= ($search || $selected_category) ? 'Passen Sie die Filterkriterien an.' : 'Legen Sie den ersten Lagerartikel an.' ?></p>
<?php if ($can_manage && !$search && !$selected_category): ?>
<button type="button" class="btn btn-primary" style="margin-top:1rem" onclick="skOpenArticle()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="16" height="16"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<?= $this->te('Artikel anlegen') ?>
</button>
<?php endif; ?>
</div>
<?php else: ?>
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th scope="col" style="width:130px"><?= $this->te('Bestand') ?></th>
<th scope="col"><?= $this->te('Artikel') ?></th>
<th scope="col" style="width:160px"><?= $this->te('Kategorie') ?></th>
<th scope="col" style="width:150px"><?= $this->te('Modell') ?></th>
<th scope="col" style="width:160px"><?= $this->te('Lagerort') ?></th>
<?php if ($can_manage): ?><th scope="col" class="text-right" style="width:210px"><?= $this->te('Aktionen') ?></th><?php else: ?><th scope="col" class="text-right" style="width:70px"><?= $this->te('Verlauf') ?></th><?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($items as $item):
$qty = (int)$item['quantity'];
$minQty = (int)$item['min_quantity'];
if ($qty === 0) { $qtyClass = 'sk-qty-empty'; $pillVar = 'danger'; $qtyLabel = 'Nicht verfügbar'; }
elseif ($minQty > 0 && $qty <= $minQty) { $qtyClass = 'sk-qty-warn'; $pillVar = 'warning'; $qtyLabel = 'Niedriger Bestand'; }
else { $qtyClass = 'sk-qty-ok'; $pillVar = 'success'; $qtyLabel = 'Verfügbar'; }
?>
<tr>
<td>
<div class="sk-stock">
<span class="sk-qty-num <?= $qtyClass ?>"><?= $qty ?></span>
<span class="status-pill status-pill--<?= $pillVar ?>"><?= $qtyLabel ?></span>
</div>
</td>
<td>
<a href="/stock/<?= $item['id'] ?>/movements" class="sk-link"><?= $this->e($item['name']) ?></a>
<?php if ($item['description']): ?>
<div class="sk-desc"><?= $this->e(mb_strimwidth($item['description'], 0, 80, '…')) ?></div>
<?php endif; ?>
</td>
<td class="sk-muted">
<?php if (!empty($item['category_name'])): ?>
<span class="sk-cat-tag"><span class="sk-cat-tag-icon"><?= $this->e($item['category_icon']) ?></span><?= $this->e($item['category_name']) ?></span>
<?php else: ?><span class="sk-muted">–</span><?php endif; ?>
</td>
<td class="sk-muted"><code class="sk-mono"><?= $this->e($item['model'] ?? '–') ?></code></td>
<td class="sk-muted"><?= $this->e($item['location'] ?? '–') ?></td>
<?php if ($can_manage): ?>
<td class="text-right">
<div class="sk-actions">
<button class="btn btn-outline-success btn-xs"
onclick="openModal('in',<?= $item['id'] ?>,<?= htmlspecialchars(json_encode($item['name'])) ?>)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" width="12" height="12"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<?= $this->te('Ein') ?>
</button>
<button class="btn btn-outline-danger btn-xs" <?= $qty === 0 ? 'disabled' : '' ?>
onclick="openModal('out',<?= $item['id'] ?>,<?= htmlspecialchars(json_encode($item['name'])) ?>,<?= $qty ?>)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" width="12" height="12"><line x1="5" y1="12" x2="19" y2="12"/></svg>
<?= $this->te('Aus') ?>
</button>
<a href="/stock/<?= $item['id'] ?>/movements" class="btn btn-default btn-xs" title="<?= $this->te('Verlauf') ?>">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13"><polyline points="12 8 12 12 14 14"/><path d="M3.05 11a9 9 0 1 1 .5 4"/></svg>
</a>
<button type="button" class="btn btn-default btn-xs" title="<?= $this->te('Bearbeiten') ?>"
onclick="skOpenArticle(this)"
data-id="<?= (int)$item['id'] ?>"
data-category="<?= (int)($item['category_id'] ?? 0) ?>"
data-name="<?= $this->e($item['name'] ?? '') ?>"
data-model="<?= $this->e($item['model'] ?? '') ?>"
data-desc="<?= $this->e($item['description'] ?? '') ?>"
data-min="<?= (int)($item['min_quantity'] ?? 0) ?>"
data-location="<?= $this->e($item['location'] ?? '') ?>">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
<form method="POST" action="/stock/<?= $item['id'] ?>/delete" style="display:inline"
onsubmit="return confirm('Artikel „<?= $this->e(addslashes($item['name'])) ?>“ wirklich löschen?\nAlle Buchungen werden ebenfalls gelöscht.')">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<button type="submit" class="btn btn-outline-danger btn-xs" title="<?= $this->te('Löschen') ?>">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
</form>
</div>
</td>
<?php else: ?>
<td class="text-right">
<a href="/stock/<?= $item['id'] ?>/movements" class="btn btn-default btn-xs" title="<?= $this->te('Verlauf') ?>">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13"><polyline points="12 8 12 12 14 14"/><path d="M3.05 11a9 9 0 1 1 .5 4"/></svg>
</a>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="sk-footer">
<span class="sk-count"><?= count($items) ?> Artikel</span>
<?php if ($search || $selected_category): ?>
<a href="/stock" class="sk-reset"><?= $this->te('Filter zurücksetzen') ?></a>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php include __DIR__ . '/_modals.php'; ?>
<style>
/* ====================================================
Lagerbestand · Prefix: sk-
==================================================== */
.sk-wrap { display: flex; flex-direction: column; }
/* ── Suchfeld (Filterkarte) ───────────────────────── */
.sk-search { flex: 1 1 100%; display: flex; gap: 0.5rem; align-items: center; margin: 0.5rem 0; }
.sk-search .form-control { flex: 1; min-width: 0; }
/* ── Kategorie-Chips ──────────────────────────────── */
.sk-cat-chips {
flex: 1 1 100%;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
align-items: center;
margin: 0 0 0.5rem;
padding-top: 0.875rem;
border-top: 1px solid var(--color-gray-100);
}
.sk-cat-chip {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.3rem 0.7rem 0.3rem 0.35rem;
border-radius: 0.55rem;
font-size: 0.8rem;
font-weight: 500;
color: var(--gray-700);
background: var(--color-bg);
border: 1px solid var(--gray-200);
text-decoration: none;
white-space: nowrap;
transition: border-color .12s, background .12s, color .12s, box-shadow .12s, transform .12s;
}
.sk-cat-chip:hover {
border-color: var(--slate-300);
background: var(--slate-50);
color: var(--gray-800);
text-decoration: none;
transform: translateY(-1px);
box-shadow: 0 3px 8px rgba(15,23,42,0.07);
}
.sk-cat-chip.active {
background: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-on-accent);
box-shadow: 0 2px 8px rgba(var(--color-primary-rgb), 0.28);
}
.sk-cat-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.6rem;
height: 1.6rem;
border-radius: 0.4rem;
background: var(--gray-100);
font-size: 0.92rem;
line-height: 1;
color: var(--gray-500);
flex-shrink: 0;
}
.sk-cat-chip.active .sk-cat-icon { background: rgba(255,255,255,0.22); color: var(--color-on-accent); }
.sk-cat-name { line-height: 1; }
.sk-cat-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.3rem;
height: 1.3rem;
padding: 0 0.4rem;
background: var(--gray-100);
color: var(--gray-500);
border-radius: 0.6rem;
font-size: 0.72rem;
font-weight: 700;
line-height: 1;
}
.sk-cat-chip.active .sk-cat-count { background: rgba(255,255,255,0.25); color: var(--color-on-accent); }
@media (max-width: 480px) {
.sk-search { flex-wrap: wrap; }
.sk-search .form-control { flex: 1 1 100%; }
}
/* ── Zellen ───────────────────────────────────────── */
.sk-link { color: var(--gray-900); font-weight: 500; text-decoration: none; }
.sk-link:hover { color: var(--color-primary); text-decoration: underline; }
.sk-desc { font-size: .78rem; color: var(--gray-400); margin-top: .15rem; }
.sk-mono { font-size: .82rem; font-variant-numeric: tabular-nums; color: var(--gray-600); }
.sk-muted { color: var(--gray-500); }
.sk-cat-tag {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.78rem;
color: var(--gray-700);
}
.sk-cat-tag-icon { font-size: 0.95em; line-height: 1; }
/* ── Bestand (Menge + Status-Pill) ────────────────── */
.sk-stock { display: inline-flex; flex-direction: column; align-items: flex-start; gap: .3rem; }
.sk-qty-num { font-size: 1.15rem; font-weight: 800; line-height: 1; font-variant-numeric: tabular-nums; }
.sk-qty-ok { color: var(--emerald-600); }
.sk-qty-warn { color: var(--amber-600); }
.sk-qty-empty { color: var(--red-600); }
/* ── Aktionen (zentrale .btn-Komponenten) ─────────── */
.sk-actions { display: inline-flex; gap: 0.3rem; align-items: center; justify-content: flex-end; flex-wrap: nowrap; }
.sk-actions form { margin: 0; }
.sk-actions .btn { white-space: nowrap; }
/* ── Footer ───────────────────────────────────────── */
.sk-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 0.875rem;
padding: 0 0.25rem;
}
.sk-count { font-size: 0.8rem; color: var(--gray-400); }
.sk-reset { font-size: 0.8rem; color: var(--gray-500); }
</style>