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); }
/* Filter – einheitliche Höhe (Standard wie in allen Modulen) */
.filter-card-body .inv-filter-input,
.filter-card-body .inv-filter-btn,
.filter-card-body .inv-view-toggle { height:2.375rem; box-sizing:border-box; }
.inv-filter-input { padding:0 0.75rem; border:1px solid var(--gray-300); border-radius:0.25rem; font-size:0.83rem; background:var(--color-bg); outline:none; }
.inv-filter-input:focus { border-color:var(--color-primary); box-shadow:0 0 0 2px rgba(var(--color-primary-rgb), 0.15); }
/* View Toggle */
.inv-view-toggle { display:inline-flex; border:1px solid var(--gray-200); border-radius:0.25rem; overflow:hidden; }
.inv-view-btn { padding:0.4rem 0.65rem; background:var(--color-bg); border:none; cursor:pointer; color:var(--gray-500); line-height:1; transition:background 0.15s,color 0.15s; }
.inv-view-btn:hover { background:var(--gray-100); color:var(--gray-700); }
.inv-view-btn.active { background:var(--color-primary); color:var(--color-on-accent); }
/* Card Grid */
.inv-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(260px,1fr)); gap:1.25rem; }
.inv-card {
background:var(--color-bg); border:1px solid var(--gray-200); border-radius:0.25rem;
overflow:hidden; transition:box-shadow 0.15s; text-decoration:none;
color:inherit; display:flex; flex-direction:column;
}
.inv-card:hover { box-shadow:0 4px 20px rgba(0,0,0,0.1); }
.inv-card-img { width:100%; height:160px; background:var(--gray-100); display:flex; align-items:center; justify-content:center; overflow:hidden; }
.inv-card-img img { width:100%; height:100%; object-fit:cover; }
.inv-card-img-placeholder { color:var(--gray-300); }
.inv-card-body { padding:1rem; flex:1; display:flex; flex-direction:column; gap:0.3rem; }
.inv-card-invno { font-size:0.95rem; font-weight:700; color:var(--gray-900); }
.inv-card-name { font-size:0.82rem; color:var(--gray-700); }
.inv-card-model { font-size:0.78rem; color:var(--gray-500); }
.inv-card-person{ font-size:0.75rem; color:var(--gray-400); }
.inv-card-footer { padding:0.65rem 1rem; border-top:1px solid var(--gray-100); display:flex; align-items:center; justify-content:space-between; }
/* Table: Sortier-Header (modul-spezifisch) */
.inv-th-sort { display:inline-flex; align-items:center; gap:0.25rem; color:inherit; text-decoration:none; cursor:pointer; }
.inv-th-sort:hover { color:var(--gray-700); }
.inv-th-sort.active { color:var(--color-primary); }
.inv-sort-icon { font-size:0.7rem; opacity:0.4; }
.inv-th-sort.active .inv-sort-icon { opacity:1; }
.inv-invno { font-weight:700; font-size:0.85rem; color:var(--slate-900); }
.inv-invno:hover { color:var(--color-primary); }
.loc-path { font-size:0.72rem; color:var(--gray-400); }
.loc-path-sep { margin:0 0.2rem; }
.loc-leaf { color:var(--gray-700); font-weight:600; }
/* Pagination */
.inv-pager-wrap { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:0.75rem; padding:1.1rem 0 0; }
.inv-pager-info { font-size:0.8rem; color:var(--gray-500); white-space:nowrap; }
.inv-pager-perpage { display:flex; align-items:center; gap:0.4rem; font-size:0.8rem; color:var(--gray-500); }
.inv-pager-perpage select { padding:0.25rem 0.5rem; border:1px solid var(--gray-300); border-radius:0.25rem; font-size:0.8rem; background:var(--color-bg); cursor:pointer; }
.inv-pagination { display:flex; gap:0.25rem; align-items:center; flex-wrap:wrap; }
.inv-page-btn { display:inline-flex; align-items:center; justify-content:center; min-width:2rem; height:2rem; padding:0 0.5rem; border:1px solid var(--gray-200); border-radius:0.25rem; background:var(--color-bg); font-size:0.82rem; color:var(--gray-700); text-decoration:none; transition:background 0.12s,color 0.12s; }
.inv-page-btn:hover:not(.disabled) { background:var(--gray-100); border-color:var(--gray-300); }
.inv-page-btn.active { background:var(--color-primary); color:var(--color-on-accent); border-color:var(--color-primary); font-weight:700; cursor:default; }
.inv-page-btn.disabled { color:var(--gray-300); cursor:default; pointer-events:none; }
.inv-page-dots { display:inline-flex; align-items:center; justify-content:center; min-width:2rem; height:2rem; font-size:0.82rem; color:var(--gray-400); }
/* Scan-Button */
.scan-btn { flex-shrink:0; width:2.1rem; height:2.1rem; border:1px solid var(--gray-300); border-radius:0.25rem;
background:var(--gray-50); color:var(--gray-500); cursor:pointer; display:inline-flex; align-items:center; justify-content:center;
transition:background 0.15s,border-color 0.15s,color 0.15s; font-size:1rem; padding:0; }
.scan-btn:hover { background:var(--color-primary-pale); border-color:var(--color-primary); color:var(--color-primary); }
@keyframes bc-scan { 0%{top:0} 100%{top:calc(100% - 2px)} }
.inv-search-wrap { position:relative; flex:1; min-width:200px; }
/* Filterleiste: feste zwei Zeilen */
.inv-filter-row { display:flex; gap:0.5rem; flex-wrap:wrap; align-items:center; }
.inv-filter-row + .inv-filter-row { margin-top:0.55rem; }
.ss-filter--grow { flex:1 1 340px; min-width:260px; max-width:640px; }
.inv-saved-views { display:flex; gap:0.4rem; align-items:center; flex-wrap:wrap; flex:1 1 auto; }
@media(max-width:700px){
.inv-filter-row { flex-direction:column !important; align-items:stretch !important; flex-wrap:nowrap !important; gap:0.55rem !important; }
.inv-filter-row .inv-search-wrap { flex:none !important; min-width:0 !important; width:100% !important; box-sizing:border-box; }
.inv-filter-row .ss-wrap,
.inv-filter-row .inv-filter-btn { width:100% !important; box-sizing:border-box; max-width:none !important; }
/* Gestapelt behalten die Filter ihre natürliche Höhe – die Breiten-basis
(z. B. 340px von .ss-filter--grow) wird hier neutralisiert. */
.inv-filter-row .ss-wrap { flex:0 0 auto !important; }
.inv-saved-views { width:100%; }
}
/* Checkbox column */
.cb-th { width:2rem; padding:0.45rem 0.5rem !important; }
.cb-td { width:2rem; padding:0.45rem 0.5rem !important; text-align:center; }
.inv-cb { width:1rem; height:1rem; cursor:pointer; accent-color:var(--color-primary); }
/* Bulk action bar */
#bulk-bar {
display:none; position:fixed; bottom:0; left:0; right:0; z-index:9000;
background:var(--slate-800); color:var(--slate-100); padding:0.7rem 1.5rem;
align-items:center; gap:1rem; flex-wrap:wrap;
box-shadow:0 -4px 24px rgba(0,0,0,0.25);
}
#bulk-bar.visible { display:flex; }
#bulk-bar-count { font-size:0.88rem; font-weight:700; margin-right:0.25rem; }
.bulk-btn { display:inline-flex; align-items:center; gap:0.35rem; padding:0.45rem 1rem;
border-radius:0.25rem; font-size:0.82rem; font-weight:600; cursor:pointer; border:none; }
.bulk-btn-print { background:var(--color-primary); color:var(--color-on-accent); }
.bulk-btn-print:hover { background:var(--color-primary-dark); }
.bulk-btn-edit { background:var(--slate-600); color:var(--color-on-accent); }
.bulk-btn-edit:hover { background:var(--slate-500); }
.bulk-btn-delete { background:var(--red-600); color:var(--color-on-accent); }
.bulk-btn-delete:hover { background:var(--red-700); }
.bulk-btn-clear { background:transparent; color:var(--slate-400); border:1px solid var(--slate-600); }
.bulk-btn-clear:hover { background:var(--slate-700); color:var(--slate-100); }
/* Filter-Dropdowns nutzen die zentrale durchsuchbare Komponente (app.js/app.css).
Die Breiten-Zellen (.ss-filter) bleiben; der zentrale .ss-wrap füllt sie. */
.ss-filter .ss-wrap { width:100%; }
.ss-filter { width:170px; }
.ss-filter--wide { width:240px; }
@media(max-width:768px){
.inv-grid { grid-template-columns:1fr; }
#inv-list-filter > .ss-wrap { width:100% !important; }
}
</style>
<?php if ($can_manage): ?>
<div id="bulk-bar">
<span id="bulk-bar-count"><?= $this->te('0 ausgewählt') ?></span>
<?php if (in_array($label_print_method ?? 'both', ['windows_print', 'both'])): ?>
<button type="button" class="bulk-btn bulk-btn-print" onclick="bulkPrintLabels()"><?= $this->te('🏷 Etiketten drucken') ?></button>
<?php endif; ?>
<button type="button" class="bulk-btn bulk-btn-edit" onclick="openBulkEdit()"><?= $this->te('✎ Bearbeiten') ?></button>
<button type="button" class="bulk-btn bulk-btn-delete" onclick="bulkDeleteSelected()"><?= $this->te('🗑 Löschen') ?></button>
<button type="button" class="bulk-btn bulk-btn-clear" onclick="clearSelection()"><?= $this->te('✕ Auswahl aufheben') ?></button>
</div>
<!-- Massenbearbeitung: nur ausgefüllte Felder werden geändert -->
<div class="modal-overlay" id="bulk-edit-modal" style="display:none;">
<div class="modal-card" style="max-width:480px;">
<div class="modal-header">
<span>✎ <span id="bulk-edit-count">0</span> <?= $this->te('Gerät(e) bearbeiten') ?></span>
<button type="button" class="modal-close" onclick="closeBulkEdit()" aria-label="<?= $this->te('Schließen') ?>">×</button>
</div>
<div class="modal-body">
<p style="font-size:0.84rem;color:var(--gray-500);margin:0 0 1rem;">
<?= $this->te('Nur die Felder, die Sie ausfüllen, werden geändert. Leer gelassene Felder bleiben an jedem Gerät so, wie sie sind.') ?>
</p>
<div class="form-group">
<label for="bulk-status"><?= $this->te('Status') ?></label>
<select class="form-control form-control-sm" id="bulk-status">
<option value=""><?= $this->te('— nicht ändern —') ?></option>
<option value="active"><?= $this->te('Aktiv') ?></option>
<option value="storage"><?= $this->te('Lager') ?></option>
<option value="decommissioned"><?= $this->te('Ausgemustert') ?></option>
</select>
</div>
<div class="form-group">
<label for="bulk-location"><?= $this->te('Standort') ?></label>
<select class="form-control form-control-sm" id="bulk-location">
<option value=""><?= $this->te('— nicht ändern —') ?></option>
<?php foreach (($locations ?? []) as $loc): ?>
<option value="<?= (int)$loc['id'] ?>"><?= $this->e($loc['path'] ?? $loc['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="bulk-assigned"><?= $this->te('Zugewiesen an') ?></label>
<input type="text" class="form-control form-control-sm" id="bulk-assigned"
placeholder="<?= $this->te('Name oder Abteilung') ?>" maxlength="255">
<span class="form-hint">
<?= $this->te('Nur wirksam, wenn das Häkchen darunter gesetzt ist – so lässt sich die Zuweisung auch bewusst leeren. Geräte, deren Zuweisung ein MDM führt, bleiben unberührt.') ?>
</span>
<label style="display:inline-flex;align-items:center;gap:0.4rem;margin-top:0.5rem;font-size:0.84rem;">
<input type="checkbox" id="bulk-assigned-set"> <?= $this->te('Zuweisung ändern') ?>
</label>
</div>
<div id="bulk-edit-error" class="alert alert-danger" style="display:none;margin-top:0.75rem;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" onclick="closeBulkEdit()"><?= $this->te('Abbrechen') ?></button>
<button type="button" class="btn btn-primary" id="bulk-edit-submit" onclick="submitBulkEdit()"><?= $this->te('Ändern') ?></button>
</div>
</div>
</div>
<?php endif; ?>
<div class="inv-wrap">
<!-- Content Header -->
<div class="content-header">
<div>
<h1>
<?php if ($current_category): ?>
<?= $current_category['icon'] ?> <?= $this->e($current_category['name']) ?>
<?php else: ?>
<?= $this->te('Alle Assets') ?>
<?php endif; ?>
</h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item"><a href="/inventory"><?= $this->te('Inventar') ?></a></li>
<li class="breadcrumb-item active"><?= $current_category ? $this->e($current_category['name']) : $this->te('Alle Assets') ?></li>
</ol>
</div>
<div class="content-header-actions">
<div class="inv-view-toggle" title="<?= $this->te('Ansicht wechseln') ?>">
<button type="button" class="inv-view-btn <?= $view_mode === 'cards' ? 'active' : '' ?>" onclick="setViewMode('cards')" title="<?= $this->te('Kacheln') ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="3" width="8" height="8" rx="1"/><rect x="13" y="3" width="8" height="8" rx="1"/><rect x="3" y="13" width="8" height="8" rx="1"/><rect x="13" y="13" width="8" height="8" rx="1"/></svg>
</button>
<button type="button" class="inv-view-btn <?= $view_mode === 'table' ? 'active' : '' ?>" onclick="setViewMode('table')" title="<?= $this->te('Liste') ?>">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
</div>
<a href="/inventory" class="btn btn-default"><?= $this->te('← Übersicht') ?></a>
<?php if ($can_manage): ?>
<a href="/inventory/bulk-create<?= $category_id ? '?category=' . $category_id : '' ?>" class="btn btn-default"><?= $this->te('⊞ Bulk anlegen') ?></a>
<a href="/inventory/create<?= $category_id ? '?category=' . $category_id : '' ?>" class="btn btn-primary"><?= $this->te('+ Neues Asset') ?></a>
<?php endif; ?>
</div>
</div>
<div class="page-body">
<!-- Filter + View Toggle -->
<div class="card card-outline card-primary mb-3">
<div class="filter-card-body">
<form id="inv-list-filter" method="GET" action="/inventory/list">
<?php if ($category_id): ?>
<input type="hidden" name="category" value="<?= $category_id ?>">
<?php endif; ?>
<!-- Zeile 1: Suche + Filter -->
<div class="inv-filter-row">
<div class="inv-search-wrap">
<input type="search" id="search-q" name="q" value="<?= $this->e($q) ?>" placeholder="<?= $this->te('Person, Inv.Nr., Seriennr., Modell …') ?>" class="inv-filter-input" style="width:100%;padding-left:2rem;padding-right:2.4rem;">
<button type="button" class="inv-scan-btn" onclick="openSearchScanner()" title="<?= $this->te('Barcode oder QR-Code scannen') ?>">📷</button>
</div>
<div class="ss-filter"><select name="status" id="f-status" class="form-control form-control-sm">
<option value=""><?= $this->te('Alle Status') ?></option>
<option value="active" <?= $status_filter === 'active' ? 'selected' : '' ?>><?= $this->te('Aktiv') ?></option>
<option value="storage" <?= $status_filter === 'storage' ? 'selected' : '' ?>><?= $this->te('Lager') ?></option>
<option value="decommissioned" <?= $status_filter === 'decommissioned' ? 'selected' : '' ?>><?= $this->te('Ausgemustert') ?></option>
</select>
</div>
<?php if (!$category_id): ?>
<div class="ss-filter"><select name="category" id="f-category" class="form-control form-control-sm">
<option value=""><?= $this->te('Alle Kategorien') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= (string)$cat['id'] === (string)$category_id ? 'selected' : '' ?>>
<?= $this->e($cat['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="ss-filter"><select name="manufacturer" id="f-manufacturer" class="form-control form-control-sm">
<option value=""><?= $this->te('Alle Hersteller') ?></option>
<?php foreach (($manufacturers ?? []) as $m): ?>
<option value="<?= $m['id'] ?>" <?= (string)$m['id'] === (string)($manufacturer_filter ?? '') ? 'selected' : '' ?>><?= $this->e($m['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="ss-filter"><select name="type" id="f-type" class="form-control form-control-sm">
<option value=""><?= $this->te('Alle Modelle') ?></option>
<?php foreach (($types ?? []) as $t): ?>
<option value="<?= $t['id'] ?>" <?= (string)$t['id'] === (string)($type_filter ?? '') ? 'selected' : '' ?>><?= $this->e($t['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="ss-filter"><select name="warranty" id="f-warranty" class="form-control form-control-sm">
<option value=""><?= $this->te('Garantie: alle') ?></option>
<option value="expiring" <?= ($warranty_filter ?? '') === 'expiring' ? 'selected' : '' ?>><?= $this->te('läuft bald ab (90 T.)') ?></option>
<option value="expired" <?= ($warranty_filter ?? '') === 'expired' ? 'selected' : '' ?>><?= $this->te('abgelaufen') ?></option>
</select>
</div>
<div class="ss-filter"><select name="assignment" id="f-assignment" class="form-control form-control-sm">
<option value=""><?= $this->te('Zuweisung: alle') ?></option>
<option value="assigned" <?= ($assignment_filter ?? '') === 'assigned' ? 'selected' : '' ?>><?= $this->te('zugewiesen') ?></option>
<option value="unassigned" <?= ($assignment_filter ?? '') === 'unassigned' ? 'selected' : '' ?>><?= $this->te('nicht zugewiesen') ?></option>
</select>
</div>
<?php
/** @var ?\OpenNIT\Assets\Valuation $valuation */
$val = $valuation ?? null;
?>
<?php if ($val && $val->nutzungsdauer): ?>
<div class="ss-filter"><select name="lifecycle" id="f-lifecycle" class="form-control form-control-sm">
<option value=""><?= $this->te('Nutzungsdauer: alle') ?></option>
<option value="expiring" <?= ($lifecycle_filter ?? '') === 'expiring' ? 'selected' : '' ?>><?= $this->te('läuft aus (6 Mon.)') ?></option>
<option value="expired" <?= ($lifecycle_filter ?? '') === 'expired' ? 'selected' : '' ?>><?= $this->te('abgelaufen') ?></option>
<option value="missing" <?= ($lifecycle_filter ?? '') === 'missing' ? 'selected' : '' ?>><?= $this->te('ohne Angabe') ?></option>
</select>
</div>
<?php endif; ?>
<button type="submit" class="btn btn-primary inv-filter-btn"><?= $this->te('Suchen') ?></button>
<?php
$hasActiveFilter = $q !== '' || $status_filter !== ''
|| !empty($manufacturer_filter) || !empty($type_filter) || !empty($location_filter)
|| !empty($warranty_filter) || !empty($assignment_filter)
|| !empty($lifecycle_filter);
?>
<?php if ($hasActiveFilter): ?>
<?php $resetUrl = '/inventory/list' . ($category_id ? '?category=' . $category_id : ''); ?>
<a href="<?= $resetUrl ?>" style="font-size:0.82rem;color:var(--gray-500);"><?= $this->te('✕ Zurücksetzen') ?></a>
<?php endif; ?>
<span style="margin-left:auto;font-size:0.82rem;color:var(--gray-500);">
<?= $this->te($total === 1 ? '{anzahl} Asset' : '{anzahl} Assets', ['anzahl' => $total]) ?>
<?php if (($book_value_total ?? null) !== null): ?>
<?= $this->te('· Restwert') ?> <strong style="color:var(--gray-700);"><?= $this->money((float)$book_value_total, 2) ?></strong>
<?php endif; ?>
</span>
</div>
<!-- Zeile 2: Standort (breit) + gespeicherte Ansichten -->
<div class="inv-filter-row inv-filter-row2">
<div class="ss-filter--grow"><select name="location" id="f-location" class="form-control form-control-sm">
<option value=""><?= $this->te('Alle Standorte') ?></option>
<?php foreach (($locations ?? []) as $l): ?>
<option value="<?= $l['id'] ?>" <?= (string)$l['id'] === (string)($location_filter ?? '') ? 'selected' : '' ?>><?= $this->e($l['full_path'] ?? $l['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="inv-saved-views">
<span style="font-size:0.78rem;color:var(--gray-500);font-weight:600;"><?= $this->te('Ansichten:') ?></span>
<?php foreach (($saved_views ?? []) as $v): ?>
<span style="display:inline-flex;align-items:center;gap:0.25rem;background:var(--blue-50);border:1px solid var(--blue-200);border-radius:9999px;padding:0.1rem 0.2rem 0.1rem 0.6rem;font-size:0.78rem;">
<a href="/inventory/list?<?= $this->e($v['query'] ?? '') ?>" style="color:var(--blue-700);text-decoration:none;font-weight:600;"><?= $this->e($v['name'] ?? '') ?></a>
<button type="button" onclick="invDeleteSavedView(<?= htmlspecialchars(json_encode($v['name'] ?? ''), ENT_QUOTES) ?>)" title="<?= $this->te('Ansicht löschen') ?>"
style="border:none;background:none;color:var(--gray-400);cursor:pointer;font-size:0.85rem;line-height:1;padding:0 0.15rem;">✕</button>
</span>
<?php endforeach; ?>
<button type="button" onclick="invSaveCurrentView()" style="border:1px dashed var(--gray-300);background:var(--color-bg);color:var(--gray-600);border-radius:9999px;padding:0.1rem 0.7rem;font-size:0.78rem;cursor:pointer;"><?= $this->te('+ Aktuelle Ansicht speichern') ?></button>
<a href="/inventory/export<?= !empty($_GET) ? '?' . http_build_query(array_diff_key($_GET, ['page' => '', 'per_page' => ''])) : '' ?>"
style="margin-left:auto;border:1px solid var(--gray-300);background:var(--color-bg);color:var(--gray-600);border-radius:0.25rem;padding:0.15rem 0.7rem;font-size:0.78rem;text-decoration:none;"><?= $this->te('⬇ CSV-Export') ?></a>
</div>
</div>
</form>
</div>
</div>
<script>
var INV_CSRF = <?= json_encode($this->csrf()) ?>;
// Der Server begründet eine Ablehnung im Feld `error`; nur nach einem
// bestätigten Schreibvorgang neu laden, damit ein Fehlschlag sichtbar
// wird statt in einem Neuladen ohne Wirkung unterzugehen.
function invPostSavedView(url, fd, fallback) {
return fetch(url, {
method: 'POST',
body: fd,
credentials: 'same-origin',
headers: { 'Accept': 'application/json' }
})
.then(function (r) { return r.json().catch(function () { return {}; }); })
.then(function (data) {
if (data && data.ok) { location.reload(); return; }
alert((data && data.error) || fallback);
})
.catch(function () { alert(fallback); });
}
function invSaveCurrentView() {
var name = prompt('Name für diese Ansicht:');
if (!name || !name.trim()) return;
var fd = new FormData();
fd.append('_csrf', INV_CSRF);
fd.append('name', name);
fd.append('query', window.location.search.replace(/^\?/, ''));
invPostSavedView('/inventory/saved-views', fd, 'Speichern fehlgeschlagen.');
}
function invDeleteSavedView(name) {
if (!confirm('Ansicht „' + name + '" löschen?')) return;
var fd = new FormData();
fd.append('_csrf', INV_CSRF);
fd.append('name', name);
invPostSavedView('/inventory/saved-views/delete', fd, 'Löschen fehlgeschlagen.');
}
</script>
<?php
$statusMap = [
'active' => ['Aktiv', 'status-pill--success'],
'storage' => ['Lager', 'status-pill--warning'],
'decommissioned' => ['Ausgemustert', 'status-pill--neutral'],
];
?>
<?php if (empty($assets)): ?>
<div class="empty-state">
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M3 9h18"/></svg>
<p style="font-weight:600;margin:1rem 0 0.5rem;"><?= $this->te('Keine Assets gefunden') ?></p>
<?php if ($can_manage): ?>
<a href="/inventory/create<?= $category_id ? '?category=' . $category_id : '' ?>" style="color:var(--color-primary);font-weight:600;font-size:0.85rem;"><?= $this->te('Erstes Asset anlegen →') ?></a>
<?php endif; ?>
</div>
<?php elseif ($view_mode === 'cards'): ?>
<div class="inv-grid">
<?php foreach ($assets as $a):
[$sLabel, $sCls] = $statusMap[$a['status']] ?? ['Unbekannt', 'status-pill--neutral'];
?>
<a href="/inventory/<?= $a['id'] ?>" class="inv-card">
<div class="inv-card-img">
<?php if (!empty($a['type_image'])): ?>
<img src="/uploads/asset-types/<?= $this->e($a['type_image']) ?>" alt="<?= $this->e($a['type_name'] ?? '') ?>">
<?php else: ?>
<svg class="inv-card-img-placeholder" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M3 9h18"/></svg>
<?php endif; ?>
</div>
<div class="inv-card-body">
<div class="inv-card-invno"><?= $this->e($a['inventory_number']) ?></div>
<?php if (!empty($a['name'])): ?>
<div class="inv-card-name"><?= $this->e($a['name']) ?></div>
<?php endif; ?>
<?php if ($a['manufacturer_name'] || $a['type_name']): ?>
<div class="inv-card-model"><?= $this->e(implode(' · ', array_filter([$a['manufacturer_name'], $a['type_name']]))) ?></div>
<?php endif; ?>
<?php if (!empty($a['assigned_to'])): ?>
<div class="inv-card-person">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle;margin-right:2px;"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/></svg>
<?= $this->e($a['assigned_to']) ?>
</div>
<?php endif; ?>
<?php $vz = $a['valuation'] ?? null; ?>
<?php if ($vz && $val && $val->restwert && $vz['restwert'] !== null): ?>
<div class="inv-card-model"><?= $this->t('Restwert') ?> <?= $this->money((float)$vz['restwert'], 2) ?></div>
<?php endif; ?>
<?php if ($vz && $val && $val->nutzungsdauer && $vz['status'] === 'abgelaufen'): ?>
<div class="inv-card-model" style="color:var(--red-600);"><?= $this->te('Nutzungsdauer abgelaufen') ?></div>
<?php elseif ($vz && $val && $val->nutzungsdauer && $vz['status'] === 'laeuft_aus'): ?>
<div class="inv-card-model" style="color:var(--amber-600);"><?= $this->te('Nutzungsdauer läuft aus') ?></div>
<?php endif; ?>
</div>
<div class="inv-card-footer">
<span class="status-pill <?= $sCls ?>"><?= $sLabel ?></span>
<?php if (!empty($a['location_path'])): ?>
<span class="loc-path">
<?php foreach ($a['location_path'] as $i => $part): ?>
<?php if ($i > 0): ?><span class="loc-path-sep">›</span><?php endif; ?>
<?php if ($i === count($a['location_path']) - 1): ?>
<span class="loc-leaf" style="color:var(--gray-700);"><?= $this->e($part) ?></span>
<?php else: ?>
<?= $this->e($part) ?>
<?php endif; ?>
<?php endforeach; ?>
</span>
<?php elseif (!empty($a['location_name'])): ?>
<span style="font-size:0.72rem;color:var(--gray-700);font-weight:600;"><?= $this->e($a['location_name']) ?></span>
<?php endif; ?>
</div>
</a>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="table-wrapper">
<table class="data-table">
<?php
// Sort helper: generates URL toggling direction for the given column
$sortUrl = function(string $col) use ($sort_by, $sort_dir): string {
$dir = ($sort_by === $col && $sort_dir === 'ASC') ? 'DESC' : 'ASC';
return '?' . http_build_query(array_merge($_GET, ['sort_by' => $col, 'sort_dir' => $dir, 'page' => 1]));
};
$sortIcon = function(string $col) use ($sort_by, $sort_dir): string {
if ($sort_by !== $col) return '<span class="inv-sort-icon">↕</span>';
return '<span class="inv-sort-icon">' . ($sort_dir === 'ASC' ? '↑' : '↓') . '</span>';
};
?>
<thead>
<tr>
<?php if ($can_manage): ?><th scope="col" class="cb-th"><input type="checkbox" class="inv-cb" id="cb-all" title="<?= $this->te('Alle auswählen') ?>" onchange="toggleAll(this)"></th><?php endif; ?>
<th scope="col"><a href="<?= $sortUrl('name') ?>" class="inv-th-sort <?= $sort_by === 'name' ? 'active' : '' ?>">Name <?= $sortIcon('name') ?></a></th>
<th scope="col"><a href="<?= $sortUrl('inventory_number') ?>" class="inv-th-sort <?= $sort_by === 'inventory_number' ? 'active' : '' ?>"><?= $this->te('Inventarnummer') ?> <?= $sortIcon('inventory_number') ?></a></th>
<th scope="col"><a href="<?= $sortUrl('category') ?>" class="inv-th-sort <?= $sort_by === 'category' ? 'active' : '' ?>"><?= $this->te('Kategorie') ?> <?= $sortIcon('category') ?></a></th>
<th scope="col"><a href="<?= $sortUrl('manufacturer') ?>" class="inv-th-sort <?= $sort_by === 'manufacturer' ? 'active' : '' ?>"><?= $this->te('Hersteller / Modell') ?> <?= $sortIcon('manufacturer') ?></a></th>
<th scope="col"><a href="<?= $sortUrl('location') ?>" class="inv-th-sort <?= $sort_by === 'location' ? 'active' : '' ?>"><?= $this->te('Standort') ?> <?= $sortIcon('location') ?></a></th>
<th scope="col"><a href="<?= $sortUrl('assigned_to') ?>" class="inv-th-sort <?= $sort_by === 'assigned_to' ? 'active' : '' ?>"><?= $this->te('Zugewiesen') ?> <?= $sortIcon('assigned_to') ?></a></th>
<?php if ($val && $val->nutzungsdauer): ?><th scope="col"><?= $this->te('Nutzungsende') ?></th><?php endif; ?>
<?php if ($val && $val->restwert): ?><th scope="col" style="text-align:right;"><?= $this->te('Restwert') ?></th><?php endif; ?>
<th scope="col"><a href="<?= $sortUrl('status') ?>" class="inv-th-sort <?= $sort_by === 'status' ? 'active' : '' ?>">Status <?= $sortIcon('status') ?></a></th>
<th scope="col" style="width:60px;"></th>
</tr>
</thead>
<tbody>
<?php foreach ($assets as $a):
[$sLabel, $sCls] = $statusMap[$a['status']] ?? ['Unbekannt', 'status-pill--neutral'];
?>
<tr data-id="<?= (int)$a['id'] ?>" data-inv="<?= $this->e($a['inventory_number']) ?>" class="asset-row">
<?php if ($can_manage): ?><td class="cb-td"><input type="checkbox" class="inv-cb row-cb" onchange="updateBulkBar()"></td><?php endif; ?>
<td>
<?php if (!empty($a['name'])): ?>
<a href="/inventory/<?= $a['id'] ?>" style="font-weight:600;"><?= $this->e($a['name']) ?></a>
<?php else: ?>
<span style="color:var(--gray-300);">—</span>
<?php endif; ?>
</td>
<td>
<a href="/inventory/<?= $a['id'] ?>" class="inv-invno"><?= $this->e($a['inventory_number']) ?></a>
<?php if ($a['serial_number']): ?>
<small style="display:block;font-size:0.72rem;color:var(--gray-400);"><?= $this->te('SN: {nummer}', ['nummer' => $a['serial_number']]) ?></small>
<?php endif; ?>
</td>
<td><?= $this->e($a['category_icon'] ?? '') ?> <?= $this->e($a['category_name']) ?></td>
<td>
<?= $a['manufacturer_name'] ? $this->e($a['manufacturer_name']) : '<span style="color:var(--gray-300);">—</span>' ?>
<?php if ($a['type_name']): ?>
<small style="display:block;font-size:0.78rem;color:var(--gray-500);"><?= $this->e($a['type_name']) ?></small>
<?php endif; ?>
</td>
<td>
<?php if (!empty($a['location_path'])): ?>
<span class="loc-path">
<?php foreach ($a['location_path'] as $i => $part): ?>
<?php if ($i > 0): ?><span class="loc-path-sep">›</span><?php endif; ?>
<?php if ($i === count($a['location_path']) - 1): ?>
<span class="loc-leaf"><?= $this->e($part) ?></span>
<?php else: ?>
<?= $this->e($part) ?>
<?php endif; ?>
<?php endforeach; ?>
</span>
<?php elseif (!empty($a['location_name'])): ?>
<span class="loc-leaf" style="font-size:0.82rem;"><?= $this->e($a['location_name']) ?></span>
<?php else: ?>
<span style="color:var(--gray-300);">—</span>
<?php endif; ?>
</td>
<td style="font-size:0.82rem;"><?= ($a['assigned_to'] ?? '') !== '' ? $this->e($a['assigned_to']) : '—' ?></td>
<?php $vz = $a['valuation'] ?? null; ?>
<?php if ($val && $val->nutzungsdauer): ?>
<td style="font-size:0.82rem;">
<?php if ($vz && $vz['ende'] !== null): ?>
<?php
$vzFarbe = match ($vz['status']) {
'abgelaufen' => 'var(--red-600)',
'laeuft_aus' => 'var(--amber-600)',
default => 'var(--gray-700)',
};
?>
<span style="color:<?= $vzFarbe ?>;"><?= date('d.m.Y', strtotime($vz['ende'])) ?></span>
<?php else: ?>
<span style="color:var(--gray-300);">—</span>
<?php endif; ?>
</td>
<?php endif; ?>
<?php if ($val && $val->restwert): ?>
<td style="font-size:0.82rem;text-align:right;white-space:nowrap;">
<?= $vz && $vz['restwert'] !== null
? $this->money((float)$vz['restwert'], 2)
: '<span style="color:var(--gray-300);">—</span>' ?>
</td>
<?php endif; ?>
<td>
<span class="status-pill <?= $sCls ?>"><?= $sLabel ?></span>
</td>
<td><a href="/inventory/<?= $a['id'] ?>" style="font-size:0.78rem;color:var(--color-primary);font-weight:600;"><?= $this->te('Details →') ?></a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<!-- Pagination -->
<?php
$totalPages = max(1, (int)ceil($total / $per_page));
$from = min($total, ($page - 1) * $per_page + 1);
$to = min($total, $page * $per_page);
$baseParams = array_diff_key($_GET, ['page' => '', 'per_page' => '']);
// Page range with ellipsis: always show first, last, current ±2
$window = [];
for ($i = max(1, $page - 2); $i <= min($totalPages, $page + 2); $i++) {
$window[] = $i;
}
$showPages = array_unique(array_merge([1], $window, [$totalPages]));
sort($showPages);
?>
<div class="inv-pager-wrap">
<!-- Left: entry count + per-page -->
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap;">
<span class="inv-pager-info">
<?php if ($total === 0): ?><?= $this->te('Keine Einträge') ?>
<?php else: ?><?= $this->t('Zeige {von}–{bis} von {gesamt} Einträgen', ['von' => $from, 'bis' => $to, 'gesamt' => $total]) ?>
<?php endif; ?>
</span>
<span class="inv-pager-perpage">
<?= $this->te('Pro Seite:') ?>
<select onchange="location.href='?<?= http_build_query(array_merge($baseParams, ['page' => 1])) ?>&per_page='+this.value">
<?php foreach ([25, 50, 100, 200] as $opt): ?>
<option value="<?= $opt ?>" <?= $opt === $per_page ? 'selected' : '' ?>><?= $opt ?></option>
<?php endforeach; ?>
</select>
</span>
</div>
<!-- Right: page buttons -->
<?php if ($totalPages > 1): ?>
<div class="inv-pagination">
<!-- First + Prev -->
<?php $pUrl = fn($p) => '?' . http_build_query(array_merge($baseParams, ['page' => $p, 'per_page' => $per_page])); ?>
<a href="<?= $page > 1 ? $pUrl(1) : '#' ?>" class="inv-page-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="<?= $this->te('Erste Seite') ?>">«</a>
<a href="<?= $page > 1 ? $pUrl($page - 1) : '#' ?>" class="inv-page-btn <?= $page <= 1 ? 'disabled' : '' ?>" title="<?= $this->te('Vorherige Seite') ?>">‹</a>
<?php $prev = null; foreach ($showPages as $p): ?>
<?php if ($prev !== null && $p - $prev > 1): ?>
<span class="inv-page-dots">…</span>
<?php endif; ?>
<a href="<?= $pUrl($p) ?>" class="inv-page-btn <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
<?php $prev = $p; endforeach; ?>
<!-- Next + Last -->
<a href="<?= $page < $totalPages ? $pUrl($page + 1) : '#' ?>" class="inv-page-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="<?= $this->te('Nächste Seite') ?>">›</a>
<a href="<?= $page < $totalPages ? $pUrl($totalPages) : '#' ?>" class="inv-page-btn <?= $page >= $totalPages ? 'disabled' : '' ?>" title="<?= $this->te('Letzte Seite') ?>">»</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
<script>
// Filter-Dropdowns nutzen die zentrale durchsuchbare Komponente (app.js).
function setViewMode(mode) {
var fd = new FormData();
fd.append('mode', mode);
fd.append('_csrf', document.querySelector('meta[name="csrf-token"]')?.content || '<?= $this->csrf() ?>');
fetch('/inventory/save-view-mode', { method: 'POST', body: fd })
.then(function() { location.reload(); });
}
<?php if ($can_manage): ?>
var BULK_LOGO_URL = <?= json_encode($label_logo_url ?? '') ?>;
var BULK_CSRF = '<?= $this->csrf() ?>';
function getCheckedRows() {
return Array.from(document.querySelectorAll('.row-cb:checked'))
.map(function(cb) { return cb.closest('tr.asset-row'); })
.filter(Boolean);
}
function updateBulkBar() {
var rows = getCheckedRows();
var bar = document.getElementById('bulk-bar');
var counter = document.getElementById('bulk-bar-count');
var all = document.getElementById('cb-all');
var total = document.querySelectorAll('.row-cb').length;
if (rows.length > 0) {
bar.classList.add('visible');
counter.textContent = rows.length + ' ausgewählt';
} else {
bar.classList.remove('visible');
}
if (all) {
all.indeterminate = rows.length > 0 && rows.length < total;
all.checked = rows.length === total && total > 0;
}
}
function toggleAll(cb) {
document.querySelectorAll('.row-cb').forEach(function(el) { el.checked = cb.checked; });
updateBulkBar();
}
function clearSelection() {
document.querySelectorAll('.row-cb, #cb-all').forEach(function(el) {
el.checked = false; el.indeterminate = false;
});
updateBulkBar();
}
function bulkPrintLabels() {
var rows = getCheckedRows();
var logoUrl = BULK_LOGO_URL;
var origin = window.location.origin;
if (!rows.length) return;
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
var labels = rows.map(function(tr) {
return { id: parseInt(tr.dataset.id, 10), inv: tr.dataset.inv };
});
<?php $g = $label_geo ?? ['w'=>54,'h'=>25,'qr'=>20,'pad'=>1.5,'gap'=>1.5,'gapInner'=>1,'logoH'=>9,'logoW'=>30,'font'=>8]; ?>
var css = [
'@page { size: <?= $g['w'] ?>mm <?= $g['h'] ?>mm; margin: 0; }',
'body { margin:0; padding:0; background:#fff; }',
'.lbl { width:<?= $g['w'] ?>mm; height:<?= $g['h'] ?>mm; display:flex; flex-direction:row; align-items:center;',
' padding:<?= $g['pad'] ?>mm; box-sizing:border-box; gap:<?= $g['gap'] ?>mm;',
' page-break-after:always; break-after:page; }',
'.lbl:last-child { page-break-after:avoid; break-after:avoid; }',
'.lbl-qr { flex:0 0 <?= $g['qr'] ?>mm; display:flex; align-items:center; justify-content:center; }',
'.lbl-qr canvas { width:<?= $g['qr'] ?>mm!important; height:<?= $g['qr'] ?>mm!important; }',
'.lbl-right { flex:1; display:flex; flex-direction:column; align-items:center;',
' justify-content:center; gap:<?= $g['gapInner'] ?>mm; overflow:hidden; }',
'.lbl-right img { max-height:<?= $g['logoH'] ?>mm; max-width:<?= $g['logoW'] ?>mm; object-fit:contain; }',
'.lbl-right span { font-family:Arial,sans-serif; font-size:<?= $g['font'] ?>pt; font-weight:bold;',
' text-align:center; word-break:break-all; color:#000; }'
].join('\n');
var body = '';
labels.forEach(function(lbl, i) {
body += '<div class="lbl">';
body += '<div class="lbl-qr"><canvas id="qr-' + i + '"></canvas></div>';
body += '<div class="lbl-right">';
if (logoUrl) body += '<img src="' + esc(logoUrl) + '" alt="">';
body += '<span>' + esc(lbl.inv) + '</span>';
body += '</div></div>';
});
var win = window.open('', '_blank', 'width=300,height=200');
if (!win) { alert('Bitte Popup-Fenster für diese Seite erlauben.'); return; }
win.document.write('<!DOCTYPE html><html><head><meta charset="utf-8"><style>' + css + '</style></head><body>' + body + '</body></html>');
win.document.close();
win.focus();
var script = win.document.createElement('script');
script.src = '/assets/js/qrcode.min.js';
script.onload = function () {
var pending = labels.length;
labels.forEach(function(lbl, i) {
var canvas = win.document.getElementById('qr-' + i);
win.QRCode.toCanvas(canvas, origin + '/inventory/' + lbl.id,
{ width: 76, margin: 0, color: { dark: '#000000', light: '#ffffff' } },
function () { if (--pending === 0) { setTimeout(function() { win.print(); win.onafterprint = function() { win.close(); }; }, 100); } }
);
});
};
script.onerror = function () {
setTimeout(function() { win.print(); win.onafterprint = function() { win.close(); }; }, 250);
};
win.document.head.appendChild(script);
}
// ── Massenbearbeitung ────────────────────────────────────────────────
function openBulkEdit() {
var rows = getCheckedRows();
if (!rows.length) return;
document.getElementById('bulk-edit-count').textContent = rows.length;
document.getElementById('bulk-edit-error').style.display = 'none';
document.getElementById('bulk-edit-modal').style.display = 'flex';
}
function closeBulkEdit() {
document.getElementById('bulk-edit-modal').style.display = 'none';
}
function submitBulkEdit() {
var rows = getCheckedRows();
if (!rows.length) { closeBulkEdit(); return; }
var status = document.getElementById('bulk-status').value;
var location = document.getElementById('bulk-location').value;
var setZuw = document.getElementById('bulk-assigned-set').checked;
var zuweisung = document.getElementById('bulk-assigned').value;
var fehler = document.getElementById('bulk-edit-error');
if (!status && !location && !setZuw) {
fehler.textContent = 'Bitte mindestens ein Feld zum Ändern auswählen.';
fehler.style.display = 'block';
return;
}
var knopf = document.getElementById('bulk-edit-submit');
knopf.disabled = true;
var fd = new FormData();
fd.append('_csrf', BULK_CSRF);
rows.forEach(function (tr) { fd.append('ids[]', parseInt(tr.dataset.id, 10)); });
if (status) fd.append('status', status);
if (location) fd.append('location_id', location);
if (setZuw) fd.append('assigned_to', zuweisung);
fetch('/inventory/bulk-update', { method: 'POST', body: fd })
.then(function (r) { return r.json(); })
.then(function (data) {
knopf.disabled = false;
if (!data.ok) {
fehler.textContent = data.error || 'Änderung fehlgeschlagen.';
fehler.style.display = 'block';
return;
}
closeBulkEdit();
// Die Liste zeigt die geänderten Werte erst nach dem Neuladen.
window.location.reload();
})
.catch(function () {
knopf.disabled = false;
fehler.textContent = 'Netzwerkfehler. Bitte Seite neu laden.';
fehler.style.display = 'block';
});
}
function bulkDeleteSelected() {
var rows = getCheckedRows();
if (!rows.length) return;
var invNrs = rows.map(function(tr) { return tr.dataset.inv; }).join(', ');
var msg = rows.length === 1
? 'Asset "' + invNrs + '" wirklich löschen?'
: rows.length + ' Assets wirklich löschen?\n\n' + invNrs;
if (!confirm(msg)) return;
var ids = rows.map(function(tr) { return parseInt(tr.dataset.id, 10); });
var fd = new FormData();
fd.append('_csrf', BULK_CSRF);
ids.forEach(function(id) { fd.append('ids[]', id); });
fetch('/inventory/bulk-delete', { method: 'POST', body: fd })
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.ok) {
rows.forEach(function(tr) { tr.remove(); });
clearSelection();
// Update displayed count
var info = document.querySelector('.inv-pager-info');
if (info) info.textContent = data.deleted + ' Asset(s) gelöscht.';
} else {
alert(data.error || 'Fehler beim Löschen.');
}
})
.catch(function() { alert('Netzwerkfehler. Bitte Seite neu laden.'); });
}
<?php endif; ?>
// ── Barcode-/QR-Scanner für Suche ────────────────────────────────────
(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('scan-search-status').textContent = '⚠ Scanner-Bibliothek konnte nicht geladen werden.';
};
document.head.appendChild(s);
}
window.openSearchScanner = function () {
if (!window.isSecureContext) {
alert('Barcode-Scanner benötigt HTTPS.');
return;
}
var modal = document.getElementById('scan-search-modal');
var status = document.getElementById('scan-search-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('scan-search-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();
closeSearchScanner();
// QR-Code vom Inventar-Etikett → direkt zur Asset-Seite
var invMatch = text.match(/\/inventory\/(\d+)\s*$/);
if (invMatch) {
window.location.href = '/inventory/' + invMatch[1];
return;
}
// Sonst: Suchfeld befüllen + Formular absenden
var inp = document.getElementById('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);
});
});
};
function closeSearchScanner() {
if (_controls) { try { _controls.stop(); } catch (_) {} _controls = null; }
if (_reader) { try { _reader.reset(); } catch (_) {} _reader = null; }
var v = document.getElementById('scan-search-video');
if (v) { v.srcObject = null; v.pause(); }
document.getElementById('scan-search-modal').style.display = 'none';
}
window.closeSearchScanner = closeSearchScanner;
document.addEventListener('DOMContentLoaded', function () {
var modal = document.getElementById('scan-search-modal');
if (modal) modal.addEventListener('click', function (e) { if (e.target === this) closeSearchScanner(); });
});
}());
</script>
<!-- Barcode-/QR-Scanner-Modal (Suche) -->
<div id="scan-search-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="closeSearchScanner()" 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="scan-search-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="scan-search-status" style="font-size:0.82rem;color:var(--gray-500);flex:1;"><?= $this->te('Kamera wird gestartet…') ?></span>
<button type="button" onclick="closeSearchScanner()" 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>