OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

list.php

57 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); }

/* 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('&#127991; Etiketten drucken') ?></button>
    <?php endif; ?>
    <button type="button" class="bulk-btn bulk-btn-edit" onclick="openBulkEdit()"><?= $this->te('&#9998; Bearbeiten') ?></button>
    <button type="button" class="bulk-btn bulk-btn-delete" onclick="bulkDeleteSelected()"><?= $this->te('&#128465; Löschen') ?></button>
    <button type="button" class="bulk-btn bulk-btn-clear" onclick="clearSelection()"><?= $this->te('&#10005; 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>&#9998; <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') ?>">&times;</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('&mdash; nicht ändern &mdash;') ?></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('&mdash; nicht ändern &mdash;') ?></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 &ndash; 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('&#8862; 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') ?>">&#128247;</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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }

    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('&#128247; Gerät scannen') ?></span>
            <button type="button" onclick="closeSearchScanner()" 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="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 &nbsp;·&nbsp; <strong style="color:var(--gray-500);">Barcode</strong> (Seriennummer) → Suche nach dem Gerät') ?>
            </p>
        </div>
    </div>
</div>