Quelltext des aktuellen Standes
<?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') ?>">📷</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('📷 Gerät scannen') ?></span>
<button type="button" onclick="closeIndexScanner()" class="modal-close" aria-label="<?= $this->te('Schließen') ?>">✕</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 · <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>