Quelltext des aktuellen Standes
<?php $this->layout('layouts/main', [
'page_title' => $page_title,
'current_route' => $current_route,
'full_width' => true,
'extra_css' => ['/assets/css/person-search.css', '/assets/css/company-search.css'],
'extra_js' => ['/assets/js/person-search.js', '/assets/js/company-search.js'],
]); ?>
<?php $isEdit = ($asset !== null); $prefill = $prefill ?? []; ?>
<?php
// Der Vorschlag, eine unbekannte Firma anzulegen, setzt das Adressbuch und
// das Recht voraus, es zu pflegen.
$kannFirmaAnlegen = !empty($contacts_enabled) && $this->hasCapability('manage_contacts');
?>
<style>
a { text-decoration:none; }
.inv-wrap { padding:0; }
/* Form layout */
.inv-form-card { background:var(--color-bg); border:1px solid var(--gray-200); border-radius:0.25rem; padding:1.75rem 2rem; }
.inv-form-section { margin-bottom:1.75rem; }
.inv-form-section-title { font-size:0.78rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:var(--gray-500); margin-bottom:0.85rem; padding-bottom:0.5rem; border-bottom:1px solid var(--gray-100); }
.inv-form-grid { display:grid; grid-template-columns:1fr 1fr 1fr; gap:1rem; }
.inv-form-grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
/* Formulargruppen: zentrale .form-group/.form-control; Abstände liefern die Grid-Gaps */
.inv-form-section .form-group { margin-bottom:0; display:flex; flex-direction:column; gap:0.35rem; }
.inv-form-actions { display:flex; gap:0.75rem; flex-wrap:wrap; align-items:center; margin-top:1.5rem; padding-top:1.25rem; border-top:1px solid var(--gray-100); }
/* Barcode-Scanner-Button */
.scan-btn { flex-shrink:0; width:2.4rem; height:2.4rem; border:1.5px 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:1.05rem; 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)} }
/* Standort-Dropdown: gemeinsam genutzte Innen-Elemente (Pfeil, Suche, Optionsliste).
Das eigentliche Auswahlfeld nutzt die zentrale .ss-*-Komponente aus app.js/app.css. */
.ss-arrow { flex-shrink:0; font-size:0.65rem; color:var(--gray-400); transition:transform 0.15s; }
.ss-search {
padding:0.5rem 0.75rem; border:none; border-bottom:1px solid var(--gray-200);
font-size:0.85rem; width:100%; box-sizing:border-box; outline:none;
background:var(--gray-50);
}
.ss-options { max-height:200px; overflow-y:auto; }
.ss-option { padding:0.5rem 0.75rem; font-size:0.85rem; cursor:pointer; color:var(--gray-700); }
.ss-option:hover, .ss-option.active { background:var(--color-primary-pale); color:var(--color-primary-strong); }
.ss-empty { padding:0.5rem 0.75rem; font-size:0.83rem; color:var(--gray-400); }
/* Location dropdown */
.loc-wrap { position:relative; }
.loc-display {
display:flex; align-items:center; justify-content:space-between; gap:0.4rem;
padding:0.55rem 0.75rem; border:1.5px solid var(--gray-300); border-radius:0.25rem;
font-size:0.88rem; background:var(--color-bg); cursor:pointer; user-select:none;
transition:border-color 0.15s, box-shadow 0.15s;
}
.loc-display:focus, .loc-display.open { border-color:var(--color-primary); box-shadow:0 0 0 3px rgba(var(--color-primary-rgb), 0.15); outline:none; }
.loc-display-text { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.loc-display-text.placeholder { color:var(--gray-400); }
.loc-clear { flex-shrink:0; font-size:0.85rem; color:var(--gray-400); line-height:1; cursor:pointer; padding:0 0.1rem; }
.loc-clear:hover { color:var(--red-600); }
.loc-dropdown {
display:none; position:absolute; top:calc(100% + 3px); left:0; right:0; z-index:200;
background:var(--color-bg); border:1.5px solid var(--gray-300); border-radius:0.25rem;
box-shadow:0 8px 24px rgba(0,0,0,0.1); overflow:hidden;
}
.loc-dropdown.open { display:block; }
@media(max-width:900px) {
.inv-hero { padding:1rem 1.25rem 1.5rem; }
.inv-form-grid { grid-template-columns:1fr 1fr; }
}
@media(max-width:560px) {
.inv-form-grid, .inv-form-grid-2 { grid-template-columns:1fr; }
}
</style>
<div class="inv-wrap">
<!-- Content Header -->
<div class="content-header">
<div>
<h1><?= $isEdit ? 'Asset bearbeiten' : 'Neues Asset anlegen' ?></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>
<?php if ($isEdit): ?>
<li class="breadcrumb-item"><a href="/inventory/<?= $asset['id'] ?>"><?= $this->e($asset['inventory_number']) ?></a></li>
<?php endif; ?>
<li class="breadcrumb-item active"><?= $isEdit ? 'Bearbeiten' : 'Neues Asset' ?></li>
</ol>
</div>
</div>
<div class="page-body">
<div class="inv-form-card">
<form method="POST" action="<?= $isEdit ? '/inventory/' . $asset['id'] : '/inventory' ?>">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<?php if ($isEdit): ?>
<input type="hidden" name="_method" value="PUT">
<!-- Optimistic Locking: erkennt parallele Änderungen anderer Nutzer -->
<input type="hidden" name="loaded_updated_at" value="<?= $this->e($asset['updated_at'] ?? '') ?>">
<?php endif; ?>
<!-- Identifikation -->
<div class="inv-form-section">
<div class="inv-form-section-title"><?= $this->te('Identifikation') ?></div>
<div class="form-group" style="margin-bottom:1rem;">
<label for="name"><?= $this->te('Name / Bezeichnung') ?></label>
<input class="form-control" type="text" id="name" name="name"
value="<?= $this->e($asset['name'] ?? ($prefill['name'] ?? '')) ?>"
placeholder="<?= $this->te('Leer lassen → wird auf Inventarnummer gesetzt') ?>"
maxlength="255">
<span class="form-hint"><?= $this->te('Freitext-Name für dieses Gerät, z.B. „AP Erdgeschoss Süd".') ?></span>
</div>
<div class="inv-form-grid-2">
<div class="form-group">
<label for="inventory_number"><?= $this->te('Inventarnummer') ?></label>
<input class="form-control" type="text" id="inventory_number" name="inventory_number"
value="<?= $isEdit ? $this->e($asset['inventory_number']) : '' ?>"
placeholder="<?= $this->te('Leer lassen für automatische Vergabe') ?>"
maxlength="100">
<?php if (!$isEdit): ?>
<span class="form-hint"><?= $this->te('Wird automatisch generiert wenn leer gelassen.') ?></span>
<?php endif; ?>
</div>
<div class="form-group">
<label for="serial_number"><?= $this->te('Seriennummer') ?></label>
<div style="display:flex;gap:0.4rem;align-items:stretch;">
<input class="form-control" type="text" id="serial_number" name="serial_number"
value="<?= $this->e($asset['serial_number'] ?? ($prefill['serial_number'] ?? '')) ?>"
placeholder="<?= $this->te('z.B. 3GH7T8X') ?>" maxlength="255">
<button type="button" class="scan-btn" onclick="openBarcodeScanner('serial_number')" title="<?= $this->te('Seriennummer per Kamera scannen') ?>">📷</button>
</div>
</div>
</div>
</div>
<!-- Klassifikation -->
<div class="inv-form-section">
<div class="inv-form-section-title"><?= $this->te('Klassifikation') ?></div>
<div class="inv-form-grid">
<div class="form-group">
<label for="category_id"><?= $this->te('Kategorie *') ?></label>
<select class="form-control" id="category_id" name="category_id" required onchange="filterTypes()">
<option value=""><?= $this->te('— Kategorie wählen —') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>"
data-useful-life="<?= (int)($cat['default_useful_life_months'] ?? 0) ?>"
<?= ((int)($asset['category_id'] ?? $preselect_category) === (int)$cat['id']) ? 'selected' : '' ?>>
<?= $this->e($cat['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="status"><?= $this->te('Status') ?></label>
<select class="form-control" id="status" name="status">
<option value="active" <?= ($asset['status'] ?? 'active') === 'active' ? 'selected' : '' ?>><?= $this->te('Aktiv') ?></option>
<option value="storage" <?= ($asset['status'] ?? '') === 'storage' ? 'selected' : '' ?>><?= $this->te('Lager') ?></option>
<option value="decommissioned" <?= ($asset['status'] ?? '') === 'decommissioned' ? 'selected' : '' ?>><?= $this->te('Ausgemustert') ?></option>
</select>
</div>
<div class="form-group">
<label for="manufacturer_id"><?= $this->te('Hersteller') ?></label>
<select class="form-control" id="manufacturer_id" name="manufacturer_id" onchange="filterTypes()">
<option value=""><?= $this->te('— Hersteller wählen —') ?></option>
<?php foreach ($manufacturers as $m): ?>
<option value="<?= $m['id'] ?>"
<?= (int)($asset['manufacturer_id'] ?? 0) === (int)$m['id'] ? 'selected' : '' ?>>
<?= $this->e($m['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label for="type_id"><?= $this->te('Modell / Typ') ?></label>
<select class="form-control" id="type_id" name="type_id">
<option value=""><?= $this->te('— Modell wählen —') ?></option>
<?php foreach ($types as $t): ?>
<option value="<?= $t['id'] ?>"
data-manufacturer="<?= $t['manufacturer_id'] ?>"
data-category="<?= $t['category_id'] ?: '' ?>"
<?= (int)($asset['type_id'] ?? 0) === (int)$t['id'] ? 'selected' : '' ?>>
<?= $this->e($t['manufacturer_name'] . ' – ' . $t['name']) ?>
</option>
<?php endforeach; ?>
</select>
<span class="form-hint"><?= $this->te('Wird nach Hersteller & Kategorie gefiltert.') ?></span>
</div>
</div>
</div>
<!-- Zuweisung & Standort -->
<div class="inv-form-section">
<div class="inv-form-section-title"><?= $this->te('Zuweisung & Standort') ?></div>
<div class="inv-form-grid-2">
<?php
// Führt das MDM die Zuweisung, wäre eine Eingabe hier beim
// nächsten Abgleich wieder überschrieben – das Feld bleibt
// dann lesbar, aber unveränderlich. Meldet das MDM keine
// Person, lässt der Abgleich die Zuweisung stehen und sie
// ist weiterhin von Hand pflegbar.
$mdmQuellen = ['intune' => 'Intune', 'relution' => 'Relution'];
$mdmQuelle = trim((string)($asset['mdm_source'] ?? ''));
// Beim Rückschreiben bleibt das Feld bedienbar: Die Änderung
// geht dann zuerst nach Intune und von dort beim nächsten
// Abgleich wieder zurück.
$mdmRueck = !empty($mdm_writeback);
$mdmFuehrt = $mdmQuelle !== '' && !empty($asset['mdm_person_id']) && !$mdmRueck;
$mdmName = $mdmQuellen[$mdmQuelle] ?? $mdmQuelle;
?>
<div class="form-group">
<label for="assigned_to"><?= $this->te('Zugewiesen an') ?></label>
<?php if ($mdmFuehrt): ?>
<input type="text" id="assigned_to" class="form-control" readonly
value="<?= $this->e(($asset['assigned_person_name'] ?? '') !== '' ? $asset['assigned_person_name'] : ($asset['assigned_to'] ?? '')) ?>"
aria-describedby="assigned-mdm-hint">
<p class="form-hint" id="assigned-mdm-hint">
<i class="fas fa-lock" aria-hidden="true"></i>
<?= $this->te('Wird von {mdm} geführt. Eine Änderung hier würde der nächste Abgleich überschreiben – die Zuweisung ändert sich in {mdm}.', ['mdm' => $mdmName]) ?>
</p>
<?php else: ?>
<input type="hidden" name="assigned_person_id" id="assigned_person_id"
value="<?= (int)($asset['assigned_person_id'] ?? 0) ?: '' ?>">
<div class="psx-wrap">
<input type="text" id="assigned_to" name="assigned_to"
class="form-control"
value="<?= $this->e($asset['assigned_to'] ?? '') ?>"
placeholder="<?= $this->te('Name oder Abteilung — oder Verzeichnis durchsuchen') ?>"
maxlength="255"
autocomplete="off"
data-person-search="/api/persons/search"
data-ps-min="1"
data-ps-set="name"
data-ps-fill-id="assigned_person_id"
data-ps-badge="ps-badge-assigned">
<span class="psx-selected" id="ps-badge-assigned" style="display:none;">
👤 <?= $this->te('Aus Verzeichnis') ?>
<button type="button" class="psx-selected-clear" aria-label="<?= $this->te('Verknüpfung aufheben') ?>">×</button>
</span>
</div>
<?php if ($mdmRueck): ?>
<p class="form-hint">
<i class="fas fa-exchange-alt" aria-hidden="true"></i>
<?= $this->te('Die Änderung wird als primärer Benutzer nach {mdm} geschrieben. Nur Personen aus dem Entra-Verzeichnis sind möglich; gelingt das Schreiben nicht, bleibt die bisherige Zuweisung stehen.', ['mdm' => $mdmName]) ?>
</p>
<?php elseif ($mdmQuelle !== ''): ?>
<p class="form-hint">
<?= $this->te('Von {mdm} verwaltet, dort ist aber keine Person hinterlegt – eine Zuweisung hier bleibt erhalten.', ['mdm' => $mdmName]) ?>
</p>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="form-group">
<label><?= $this->te('Standort') ?></label>
<input type="hidden" id="location_id" name="location_id" value="<?= (int)($asset['location_id'] ?? 0) ?: '' ?>">
<?php
// Find the pre-selected location's full path
$selectedLocPath = '';
if (!empty($asset['location_id'])) {
foreach ($locations as $loc) {
if ((int)$loc['id'] === (int)$asset['location_id']) {
$selectedLocPath = $loc['full_path'] ?? $loc['name'];
break;
}
}
}
?>
<div class="loc-wrap">
<!-- Display button (like searchable select) -->
<div class="loc-display" id="loc-display" tabindex="0">
<span class="loc-display-text <?= $selectedLocPath ? '' : 'placeholder' ?>" id="loc-display-text">
<?= $selectedLocPath ? $this->e($selectedLocPath) : '— Standort wählen —' ?>
</span>
<span class="loc-clear" id="loc-clear" title="<?= $this->te('Standort entfernen') ?>" style="<?= $selectedLocPath ? '' : 'display:none' ?>">×</span>
<span class="ss-arrow">▼</span>
</div>
<div class="loc-dropdown" id="loc-dropdown">
<input type="text" class="ss-search" id="loc-search" placeholder="<?= $this->te('Suchen…') ?>" autocomplete="off">
<div class="ss-options" id="loc-options">
<?php foreach ($locations as $loc): ?>
<div class="ss-option" data-id="<?= $loc['id'] ?>" data-path="<?= $this->e($loc['full_path']) ?>">
<?= $this->e($loc['full_path']) ?>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Kaufdaten -->
<div class="inv-form-section">
<div class="inv-form-section-title"><?= $this->te('Kaufdaten') ?></div>
<div class="inv-form-grid-2">
<div class="form-group">
<label for="purchase_date"><?= $this->te('Kaufdatum') ?></label>
<input class="form-control" type="date" id="purchase_date" name="purchase_date"
value="<?= $this->e($asset['purchase_date'] ?? '') ?>">
</div>
<div class="form-group">
<label for="warranty_until"><?= $this->te('Garantie bis') ?></label>
<input class="form-control" type="date" id="warranty_until" name="warranty_until"
value="<?= $this->e($asset['warranty_until'] ?? '') ?>">
</div>
</div>
<?php
/** @var ?\OpenNIT\Assets\Valuation $valuation */
$val = $valuation ?? null;
?>
<?php if ($val && $val->aktiv()): ?>
<div class="inv-form-grid-2" style="margin-top:1rem;">
<?php if ($val->brauchtAnschaffungswert()): ?>
<div class="form-group">
<label for="purchase_price"><?= $this->te('Anschaffungswert ({cur})', ['cur' => $this->currency()]) ?></label>
<input class="form-control" type="text" inputmode="decimal" id="purchase_price" name="purchase_price"
value="<?= $this->e(isset($asset['purchase_price']) && $asset['purchase_price'] !== null ? number_format((float)$asset['purchase_price'], 2, ',', '') : '') ?>"
placeholder="<?= $this->te('z. B. 1499,00') ?>" maxlength="20">
<span class="form-hint"><?= $this->te(
'Grundlage für {zweck}. Ohne Betrag bleiben die Werte leer.',
['zweck' => $this->t($val->abschreibung && $val->restwert
? 'Abschreibung und Restwert'
: ($val->abschreibung ? 'die Abschreibung' : 'den Restwert'))]
) ?></span>
</div>
<?php endif; ?>
<div class="form-group">
<label for="useful_life_months"><?= $this->te('Nutzungsdauer (Monate)') ?></label>
<input class="form-control" type="number" id="useful_life_months" name="useful_life_months"
value="<?= $this->e(isset($asset['useful_life_months']) && $asset['useful_life_months'] !== null ? (string)(int)$asset['useful_life_months'] : '') ?>"
min="1" max="<?= (int)($max_monate ?? 600) ?>" step="1" placeholder="z. B. 36">
<span class="form-hint"><?= $this->te('Läuft ab dem Kaufdatum. Die Kategorie kann den Wert vorbelegen.') ?></span>
</div>
</div>
<?php endif; ?>
</div>
<!-- Lieferant -->
<div class="inv-form-section">
<div class="inv-form-section-title"><?= $this->te('Lieferant') ?></div>
<div class="form-group" style="max-width:500px;">
<?php if ($contacts_enabled): ?>
<label for="supplier_company_id"><?= $this->te('Lieferant') ?></label>
<select class="form-control" id="supplier_company_id" name="supplier_company_id" <?= $kannFirmaAnlegen ? 'data-create-company' : '' ?>>
<option value=""><?= $this->te('— Kein Lieferant —') ?></option>
<?php foreach ($companies as $co): ?>
<option value="<?= $co['id'] ?>"
<?= (int)($asset['supplier_company_id'] ?? 0) === (int)$co['id'] ? 'selected' : '' ?>>
<?= $this->e($co['name']) ?>
</option>
<?php endforeach; ?>
</select>
<span class="form-hint"><?= $this->te('Firma aus dem Kontaktbuch wählen.') ?></span>
<?php else: ?>
<label for="supplier_name"><?= $this->te('Lieferant') ?></label>
<input class="form-control" type="text" id="supplier_name" name="supplier_name"
value="<?= $this->e($asset['supplier_name'] ?? '') ?>"
placeholder="<?= $this->te('z.B. Mustermann GmbH') ?>" maxlength="255">
<?php endif; ?>
</div>
</div>
<!-- Zusatzfelder (dynamisch geladen) -->
<div id="custom-fields-section" style="display:none;">
<div class="inv-form-section">
<div class="inv-form-section-title"><?= $this->te('Weitere Informationen') ?></div>
<div id="custom-fields-body"></div>
</div>
</div>
<!-- Notizen -->
<div class="inv-form-section">
<div class="inv-form-section-title"><?= $this->te('Notizen') ?></div>
<div class="form-group">
<label for="notes"><?= $this->te('Notizen / Anmerkungen') ?></label>
<textarea class="form-control" id="notes" name="notes" rows="4" placeholder="<?= $this->te('Interne Notizen…') ?>"><?= $this->e($asset['notes'] ?? '') ?></textarea>
</div>
</div>
<!-- Leihgerät -->
<div class="inv-form-section">
<div class="inv-form-section-title"><?= $this->te('Leihgerät') ?></div>
<div class="form-group" style="flex-direction:row;align-items:center;gap:0.75rem;">
<input type="checkbox" id="is_loan_device" name="is_loan_device" value="1"
style="width:18px;height:18px;cursor:pointer;accent-color:var(--color-primary);"
<?= !empty($asset['is_loan_device']) ? 'checked' : '' ?>>
<label for="is_loan_device" style="margin:0;font-weight:500;cursor:pointer;">
<?= $this->te('Als Leihgerät markieren') ?>
</label>
</div>
<p style="font-size:0.8rem;color:var(--gray-500);margin:0.25rem 0 0;">
<?= $this->t('Aktiviert: Das Gerät steht im <strong>Leihgeräte-Modul</strong> direkt zum Verleihen zur Verfügung.') ?>
</p>
</div>
<div class="inv-form-actions">
<button type="submit" class="btn btn-primary">
<?= $isEdit ? '💾 Speichern' : '+ Asset anlegen' ?>
</button>
<a href="<?= $isEdit ? '/inventory/' . $asset['id'] : '/inventory' ?>" class="btn btn-default"><?= $this->te('Abbrechen') ?></a>
</div>
</form>
</div>
</div>
</div>
<script>
// ---- Typ-Filter nach Hersteller + Kategorie ----
// Die Modell-/Typ-Auswahl zeigt nur Einträge, die zum gewählten Hersteller und
// zur Kategorie passen. Nicht passende Optionen werden deaktiviert; die zentrale
// durchsuchbare Auswahl (app.js) blendet deaktivierte Optionen aus.
function filterTypes() {
var mfrId = document.getElementById('manufacturer_id').value;
var catId = document.getElementById('category_id').value;
var sel = document.getElementById('type_id');
Array.from(sel.options).forEach(function(opt) {
if (!opt.value) { opt.disabled = false; return; }
var mfrMatch = !mfrId || opt.dataset.manufacturer === mfrId;
var catMatch = !catId || !opt.dataset.category || opt.dataset.category === catId;
opt.disabled = !(mfrMatch && catMatch);
if (opt.disabled && sel.value === opt.value) sel.value = '';
});
if (window.ssSync) window.ssSync(sel);
}
// ---- Standort-Dropdown mit Suche ----
(function() {
var hidden = document.getElementById('location_id');
var display = document.getElementById('loc-display');
var displayTxt= document.getElementById('loc-display-text');
var clearBtn = document.getElementById('loc-clear');
var dropdown = document.getElementById('loc-dropdown');
var searchInp = document.getElementById('loc-search');
var optList = document.getElementById('loc-options');
if (!display) return;
function openDrop() {
display.classList.add('open');
dropdown.classList.add('open');
searchInp.value = '';
filterLocs('');
searchInp.focus();
}
function closeDrop() {
display.classList.remove('open');
dropdown.classList.remove('open');
}
function pick(id, path) {
hidden.value = id;
displayTxt.textContent = path;
displayTxt.classList.remove('placeholder');
clearBtn.style.display = '';
closeDrop();
}
function clearSelection() {
hidden.value = '';
displayTxt.textContent = '— Standort wählen —';
displayTxt.classList.add('placeholder');
clearBtn.style.display = 'none';
}
function filterLocs(q) {
var any = false;
Array.from(optList.querySelectorAll('.ss-option')).forEach(function(div) {
var match = !q || div.dataset.path.toLowerCase().includes(q.toLowerCase());
div.style.display = match ? '' : 'none';
if (match) any = true;
});
var emptyEl = optList.querySelector('.ss-empty');
if (!any && q) {
if (!emptyEl) { emptyEl = document.createElement('div'); emptyEl.className = 'ss-empty'; emptyEl.textContent = 'Keine Ergebnisse'; optList.appendChild(emptyEl); }
emptyEl.style.display = '';
} else if (emptyEl) { emptyEl.style.display = 'none'; }
}
// Click on options
optList.addEventListener('click', function(e) {
var opt = e.target.closest('.ss-option');
if (opt) pick(opt.dataset.id, opt.dataset.path);
});
display.addEventListener('click', function(e) {
if (e.target === clearBtn || clearBtn.contains(e.target)) { e.stopPropagation(); clearSelection(); return; }
dropdown.classList.contains('open') ? closeDrop() : openDrop();
});
display.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openDrop(); }
if (e.key === 'Escape') closeDrop();
});
searchInp.addEventListener('input', function() { filterLocs(searchInp.value); });
document.addEventListener('click', function(e) {
if (!display.closest('.loc-wrap').contains(e.target)) closeDrop();
});
})();
// Init filter on load
filterTypes();
</script>
<script>
// ---- Benutzerdefinierte Felder ----
var CF_SAVED = <?= json_encode($custom_field_values ?? [], JSON_FORCE_OBJECT) ?>;
function loadCustomFields() {
var catId = document.getElementById('category_id').value;
var typeId = document.getElementById('type_id').value;
var section = document.getElementById('custom-fields-section');
var body = document.getElementById('custom-fields-body');
if (!catId && !typeId) {
section.style.display = 'none';
body.innerHTML = '';
return;
}
var url = '/api/inventory/custom-fields?';
if (catId) url += 'category_id=' + catId + '&';
if (typeId) url += 'type_id=' + typeId;
fetch(url, {credentials: 'same-origin'})
.then(function(r) { return r.json(); })
.then(function(data) {
var fields = data.fields || [];
if (!fields.length) {
section.style.display = 'none';
body.innerHTML = '';
return;
}
var html = '<div class="inv-form-grid">';
fields.forEach(function(f) {
// Gespeicherten Wert nehmen; fällt der weg, Standard-Wert des Feldes
var savedVal = CF_SAVED[f.id] !== undefined ? CF_SAVED[f.id]
: (f.default_value || '');
var req = f.is_required ? ' required' : '';
html += '<div class="form-group">';
html += '<label>' + escHtml(f.field_label) + (f.is_required ? ' <span class="text-danger">*</span>' : '') + '</label>';
if (f.field_type === 'dropdown' && f.dropdown_options && f.dropdown_options.length) {
html += '<select class="form-control" name="custom_fields[' + f.id + ']"' + req + '>';
html += '<option value="">— wählen —</option>';
f.dropdown_options.forEach(function(opt) {
var sel = (savedVal === opt) ? ' selected' : '';
html += '<option value="' + escHtml(opt) + '"' + sel + '>' + escHtml(opt) + '</option>';
});
html += '</select>';
} else {
html += '<input class="form-control" type="text" name="custom_fields[' + f.id + ']" value="' + escHtml(savedVal) + '" placeholder="' + escHtml(f.field_label) + '"' + req + '>';
}
html += '</div>';
});
html += '</div>';
body.innerHTML = html;
section.style.display = '';
})
.catch(function() {
section.style.display = 'none';
});
}
function escHtml(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
// Nutzungsdauer aus der Kategorie vorbelegen. Ein bereits eingetragener Wert
// bleibt stehen – die Vorgabe füllt nur eine Lücke.
(function () {
var kat = document.getElementById('category_id');
var dauer = document.getElementById('useful_life_months');
if (!kat || !dauer) { return; }
function vorbelegen() {
if (dauer.value.trim() !== '') { return; }
var opt = kat.options[kat.selectedIndex];
var vorgabe = opt ? parseInt(opt.getAttribute('data-useful-life') || '0', 10) : 0;
if (vorgabe > 0) { dauer.value = String(vorgabe); }
}
kat.addEventListener('change', vorbelegen);
vorbelegen();
})();
// Felder neu laden wenn Kategorie oder Typ sich ändert
document.getElementById('category_id').addEventListener('change', loadCustomFields);
document.getElementById('type_id').addEventListener('change', loadCustomFields);
// Beim Laden sofort aufrufen (Edit-Modus)
loadCustomFields();
</script>
<!-- Barcode-Scanner-Modal -->
<div id="barcode-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('📷 Barcode scannen') ?></span>
<button type="button" onclick="closeBarcodeScanner()" 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="barcode-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="barcode-status" style="font-size:0.82rem;color:var(--gray-500);flex:1;"><?= $this->te('Kamera wird gestartet…') ?></span>
<button type="button" onclick="closeBarcodeScanner()" class="btn btn-sm btn-default"><?= $this->te('Abbrechen') ?></button>
</div>
</div>
</div>
<script>
(function () {
var _reader = null, _target = 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('barcode-status').textContent = '⚠ Scanner-Bibliothek konnte nicht geladen werden.';
};
document.head.appendChild(s);
}
window.openBarcodeScanner = function (target) {
if (typeof target === 'string') target = document.getElementById(target);
_target = target;
if (!window.isSecureContext) {
alert('Barcode-Scanner benötigt HTTPS.\nBitte über eine gesicherte Verbindung aufrufen.');
return;
}
var modal = document.getElementById('barcode-modal');
var status = document.getElementById('barcode-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('barcode-video');
_reader.decodeFromConstraints(
{ video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } } },
video,
function (result, err, controls) {
_controls = controls;
if (result) {
if (_target) {
_target.value = result.getText();
_target.focus();
_target.dispatchEvent(new Event('input', { bubbles: true }));
_target.dispatchEvent(new Event('change', { bubbles: true }));
}
window.closeBarcodeScanner();
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 in den Rahmen halten…';
}
}
).catch(function (err) {
status.textContent = err.name === 'NotAllowedError'
? '⚠ Kamerazugriff verweigert – bitte in den Browser-Einstellungen erlauben.'
: '⚠ ' + (err.message || err.name);
});
});
};
window.closeBarcodeScanner = function () {
if (_controls) { try { _controls.stop(); } catch (_) {} _controls = null; }
if (_reader) { try { _reader.reset(); } catch (_) {} _reader = null; }
var v = document.getElementById('barcode-video');
if (v) { v.srcObject = null; v.pause(); }
document.getElementById('barcode-modal').style.display = 'none';
_target = null;
};
document.getElementById('barcode-modal').addEventListener('click', function (e) {
if (e.target === this) window.closeBarcodeScanner();
});
}());
</script>
<?php if ($kannFirmaAnlegen): ?>
<?= $this->render('partials/_company_quick_create') ?>
<?php endif; ?>