OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

15 KB
<?php $this->layout('layouts/main', ['page_title' => $page_title, 'current_route' => $current_route, 'full_width' => true]); ?>
<style>
a { text-decoration:none; }
.inv-wrap { padding:0; }

/* Scanner-Knopf im Suchfeld */
.inv-scan-btn {
    position:absolute; right:0.3rem; top:50%; transform:translateY(-50%);
    width:1.6rem; height:1.6rem; border:none; background:none; color:var(--gray-400);
    cursor:pointer; display:inline-flex; align-items:center; justify-content:center;
    font-size:0.95rem; padding:0; border-radius:0.2rem; transition:color 0.15s, background 0.15s;
}
.inv-scan-btn:hover { color:var(--color-primary); background:var(--blue-50); }

/* Edle Kategorie-Karten – ganze Karte = Liste, dezentes "+" = Neu */
.inv-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(232px, 1fr)); gap:0.85rem; }

.inv-tile {
    position:relative;
    min-width:0; /* Kachel folgt der Spaltenbreite und darf unter ihre Inhaltsbreite schrumpfen */
    background:var(--color-bg); border:1px solid var(--color-gray-100); border-radius:0.75rem;
    box-shadow:0 1px 2px rgba(16,24,40,0.04);
    transition:box-shadow 0.2s ease, transform 0.16s ease, border-color 0.2s ease;
}
.inv-tile:hover { box-shadow:0 10px 26px rgba(16,24,40,0.09); border-color:var(--color-gray-300); transform:translateY(-2px); }
.inv-tile-body { display:flex; align-items:center; gap:0.85rem; padding:0.95rem 1rem; text-decoration:none; color:inherit; }
.inv-tile.has-add .inv-tile-body { padding-right:3.3rem; }
.inv-tile-icon {
    width:46px; height:46px; flex-shrink:0; border-radius:0.65rem;
    display:flex; align-items:center; justify-content:center; font-size:1.4rem;
}
.inv-tile-info { flex:1; min-width:0; }
.inv-tile-name { font-size:0.93rem; font-weight:700; color:var(--slate-800); letter-spacing:-0.01em; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.inv-tile-meta { font-size:0.8rem; color:var(--slate-400); margin-top:0.25rem; }
.inv-tile-count { font-size:1rem; font-weight:800; color:var(--slate-700); }
.inv-tile-add {
    position:absolute; top:50%; right:0.75rem; transform:translateY(-50%);
    width:30px; height:30px; border-radius:0.55rem;
    display:flex; align-items:center; justify-content:center;
    background:var(--slate-100); color:var(--slate-500); font-size:1.25rem; line-height:1;
    text-decoration:none; transition:background 0.15s ease, color 0.15s ease;
}
.inv-tile-add:hover { background:var(--color-primary); color:var(--color-on-accent); }

@media(max-width:960px) {
    .inv-grid { grid-template-columns:repeat(auto-fill, minmax(190px, 1fr)); gap:0.8rem; }
}
@media(max-width:480px) {
    .inv-grid { grid-template-columns:1fr 1fr; gap:0.7rem; }
    .inv-tile-body { padding:0.8rem; }
    .inv-tile.has-add .inv-tile-body { padding-right:3rem; }
}
@keyframes bc-scan { 0%{top:0} 100%{top:calc(100% - 2px)} }
.filter-card-body { padding:0.1rem 1rem; }
#inv-index-search { flex:1; min-width:0; }
@media(max-width:600px) {
    .filter-card-body { padding:0.1rem 1rem; }
    #inv-index-search { flex-direction:column !important; align-items:stretch !important; width:100% !important; box-sizing:border-box !important; }
    #inv-index-search .inv-scan-group { flex:none !important; min-width:0 !important; width:100% !important; max-width:none !important; box-sizing:border-box; }
    #inv-index-search .btn { width:100%; }
}
</style>

<div class="inv-wrap">
    <!-- Content Header -->
    <div class="content-header">
        <div>
            <h1><?= $this->te('Inventar') ?></h1>
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
                <li class="breadcrumb-item active"><?= $this->te('Inventar') ?></li>
            </ol>
        </div>
        <div class="content-header-actions">
            <?php if ($can_manage): ?>
                <a href="/inventory/create" class="btn btn-primary"><?= $this->te('+ Neues Asset') ?></a>
            <?php endif; ?>
            <a href="/inventory/list" class="btn btn-default"><?= $this->te('Alle Assets') ?></a>
            <?php if ($can_manage): ?>
                <a href="/inventory/manufacturers" class="btn btn-default"><?= $this->te('Hersteller') ?></a>
                <a href="/inventory/types" class="btn btn-default"><?= $this->te('Modelle') ?></a>
            <?php endif; ?>
            <?php if ($can_admin): ?>
                <a href="/admin/assets/categories" class="btn btn-default"><?= $this->te('Kategorien') ?></a>
            <?php endif; ?>
        </div>
    </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><?= number_format($total_assets) ?></h3>
                    <p><?= $this->te('Assets gesamt') ?></p>
                </div>
                <div class="icon"><i class="fas fa-laptop"></i></div>
                <a href="/inventory/list" class="small-box-footer"><?= $this->te('Alle anzeigen') ?> <i class="fas fa-arrow-circle-right"></i></a>
            </div>
        </div>
        <div class="kpi-col">
            <a href="/inventory/list?status=storage" style="text-decoration:none;">
            <div class="small-box bg-warning">
                <div class="inner">
                    <h3><?= number_format($storage_count) ?></h3>
                    <p><?= $this->te('Im Lager') ?></p>
                </div>
                <div class="icon"><i class="fas fa-warehouse"></i></div>
                <span class="small-box-footer"><?= $this->te('Lager anzeigen') ?> <i class="fas fa-arrow-circle-right"></i></span>
            </div>
            </a>
        </div>
        <div class="kpi-col">
            <div class="small-box bg-secondary">
                <div class="inner">
                    <h3><?= count($categories) ?></h3>
                    <p><?= $this->te('Kategorien') ?></p>
                </div>
                <div class="icon"><i class="fas fa-tags"></i></div>
            </div>
        </div>
        <?php
        /** @var ?\OpenNIT\Assets\Valuation $valuation */
        $val = $valuation ?? null;
        ?>
        <?php if ($val && $val->nutzungsdauer): ?>
        <div class="kpi-col">
            <a href="/inventory/list?lifecycle=expired" style="text-decoration:none;">
            <div class="small-box bg-danger">
                <div class="inner">
                    <h3><?= number_format((int)($lifecycle_expired ?? 0)) ?></h3>
                    <p><?= $this->te('Nutzungsdauer abgelaufen') ?></p>
                </div>
                <div class="icon"><i class="fas fa-hourglass-end"></i></div>
                <span class="small-box-footer"><?= $this->te('Ersatzplanung') ?> <i class="fas fa-arrow-circle-right"></i></span>
            </div>
            </a>
        </div>
        <?php endif; ?>
        <?php if ($val && $val->restwert): ?>
        <div class="kpi-col">
            <div class="small-box bg-success">
                <div class="inner">
                    <h3><?= $this->money((float)($book_value_total ?? 0), 0) ?></h3>
                    <p><?= $this->te('Restwert gesamt') ?></p>
                </div>
                <div class="icon"><i class="fas fa-coins"></i></div>
            </div>
        </div>
        <?php endif; ?>
    </div>

    <!-- Globale Suche -->
    <div class="card card-outline card-primary mb-3">
        <div class="filter-card-body">
            <form id="inv-index-search" method="GET" action="/inventory/list" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;">
                <div class="inv-scan-group" style="position:relative;flex:1;min-width:200px;max-width:1800px;">
                                        <input type="search" id="index-search-q" name="q" placeholder="<?= $this->te('Suche nach Person, Inventarnr. oder Seriennr. …') ?>" class="form-control" style="padding-left:2rem;padding-right:2.4rem;width:100%;" autofocus>
                    <button type="button" class="inv-scan-btn" onclick="openIndexScanner()" title="<?= $this->te('Barcode oder QR-Code scannen') ?>">&#128247;</button>
                </div>
                <button type="submit" class="btn btn-primary"><?= $this->te('Suchen') ?></button>
            </form>
        </div>
    </div>

    <!-- Kacheln -->
        <?php if (empty($categories)): ?>
            <div class="empty-state">
                <span class="empty-icon">📦</span>
                <p><?= $this->te('Keine Kategorien vorhanden.') ?></p>
                <?php if ($can_admin): ?>
                    <a href="/admin/assets/categories" class="btn btn-primary" style="margin-top:1rem;"><?= $this->te('Kategorien verwalten') ?></a>
                <?php endif; ?>
            </div>
        <?php else: ?>
        <div class="inv-grid">
            <?php foreach ($categories as $cat): ?>
            <div class="inv-tile<?= $can_manage ? ' has-add' : '' ?>">
                <a href="/inventory/list?category=<?= $cat['id'] ?>" class="inv-tile-body">
                    <div class="inv-tile-icon" style="background:<?= $this->e($cat['color']) ?>1a;"><?= $cat['icon'] ?></div>
                    <div class="inv-tile-info">
                        <div class="inv-tile-name"><?= $this->e($cat['name']) ?></div>
                        <div class="inv-tile-meta"><span class="inv-tile-count"><?= (int)$cat['asset_count'] ?></span> <?= $this->te('Assets') ?></div>
                    </div>
                </a>
                <?php if ($can_manage): ?>
                    <a href="/inventory/create?category=<?= $cat['id'] ?>" class="inv-tile-add" title="<?= $this->te('Neu anlegen') ?>" aria-label="Neues Asset in <?= $this->e($cat['name']) ?> anlegen">+</a>
                <?php endif; ?>
            </div>
            <?php endforeach; ?>
        </div>
        <?php endif; ?>
    </div>
</div>

<!-- Barcode-/QR-Scanner-Modal -->
<div id="idx-scan-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:99999;align-items:center;justify-content:center;padding:1rem;">
    <div style="background:var(--color-bg);border-radius:0.5rem;overflow:hidden;width:100%;max-width:480px;box-shadow:0 8px 40px rgba(0,0,0,0.4);">
        <div style="display:flex;align-items:center;justify-content:space-between;padding:0.9rem 1.25rem;background:var(--gray-800);">
            <span style="color:var(--gray-100);font-weight:700;font-size:0.9rem;"><?= $this->te('&#128247; Gerät scannen') ?></span>
            <button type="button" onclick="closeIndexScanner()" class="modal-close" aria-label="<?= $this->te('Schließen') ?>">&#10005;</button>
        </div>
        <div style="position:relative;background:#000;aspect-ratio:16/9;max-height:300px;overflow:hidden;">
            <video id="idx-scan-video" autoplay playsinline muted style="width:100%;height:100%;object-fit:cover;display:block;"></video>
            <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none;">
                <div style="width:75%;height:38%;border:2px solid rgba(var(--color-primary-rgb), 0.9);border-radius:4px;box-shadow:0 0 0 9999px rgba(0,0,0,0.35);position:relative;overflow:hidden;">
                    <div style="position:absolute;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--color-primary),transparent);animation:bc-scan 1.8s linear infinite;"></div>
                </div>
            </div>
        </div>
        <div style="padding:0.75rem 1.25rem;background:var(--gray-50);display:flex;align-items:center;gap:0.75rem;">
            <span id="idx-scan-status" style="font-size:0.82rem;color:var(--gray-500);flex:1;"><?= $this->te('Kamera wird gestartet…') ?></span>
            <button type="button" onclick="closeIndexScanner()" class="btn btn-sm btn-default"><?= $this->te('Abbrechen') ?></button>
        </div>
        <div style="padding:0.5rem 1.25rem 0.85rem;background:var(--gray-50);border-top:1px solid var(--gray-100);">
            <p style="margin:0;font-size:0.75rem;color:var(--gray-400);line-height:1.5;">
                <?= $this->t('<strong style="color:var(--gray-500);">QR-Code</strong> vom Inventar-Etikett → öffnet Asset direkt &nbsp;·&nbsp; <strong style="color:var(--gray-500);">Barcode</strong> (Seriennummer) → Suche nach dem Gerät') ?>
            </p>
        </div>
    </div>
</div>
<script>
(function () {
    var _reader = null, _controls = null;

    function loadZXing(cb) {
        if (window.ZXingBrowser) { cb(); return; }
        var s = document.createElement('script');
        s.src = '/assets/js/zxing-browser.min.js';
        s.onload = cb;
        s.onerror = function () {
            document.getElementById('idx-scan-status').textContent = '⚠ Scanner-Bibliothek konnte nicht geladen werden.';
        };
        document.head.appendChild(s);
    }

    window.openIndexScanner = function () {
        if (!window.isSecureContext) { alert('Barcode-Scanner benötigt HTTPS.'); return; }
        var modal  = document.getElementById('idx-scan-modal');
        var status = document.getElementById('idx-scan-status');
        modal.style.display = 'flex';
        status.textContent  = 'Scanner wird geladen…';
        loadZXing(function () {
            status.textContent = 'Kamera wird gestartet…';
            _reader = new window.ZXingBrowser.BrowserMultiFormatReader();
            var video = document.getElementById('idx-scan-video');
            _reader.decodeFromConstraints(
                { video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } } },
                video,
                function (result, err, controls) {
                    _controls = controls;
                    if (result) {
                        var text = result.getText();
                        closeIndexScanner();
                        var invMatch = text.match(/\/inventory\/(\d+)\s*$/);
                        if (invMatch) { window.location.href = '/inventory/' + invMatch[1]; return; }
                        var inp = document.getElementById('index-search-q');
                        if (inp) { inp.value = text; inp.closest('form').submit(); }
                        return;
                    }
                    if (err && err.name !== 'NotFoundException') {
                        status.textContent = err.name === 'NotAllowedError'
                            ? '⚠ Kamerazugriff verweigert – bitte in den Browser-Einstellungen erlauben.'
                            : '⚠ ' + (err.message || err.name);
                    } else {
                        status.textContent = 'Barcode oder QR-Code in den Rahmen halten…';
                    }
                }
            ).catch(function (err) {
                status.textContent = err.name === 'NotAllowedError'
                    ? '⚠ Kamerazugriff verweigert – bitte in den Browser-Einstellungen erlauben.'
                    : '⚠ ' + (err.message || err.name);
            });
        });
    };

    window.closeIndexScanner = function () {
        if (_controls) { try { _controls.stop(); } catch (_) {} _controls = null; }
        if (_reader)   { try { _reader.reset(); }   catch (_) {} _reader   = null; }
        var v = document.getElementById('idx-scan-video');
        if (v) { v.srcObject = null; v.pause(); }
        document.getElementById('idx-scan-modal').style.display = 'none';
    };

    document.getElementById('idx-scan-modal').addEventListener('click', function (e) {
        if (e.target === this) window.closeIndexScanner();
    });
}());
</script>