OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

18 KB
<?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>