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'],
'extra_js' => ['/assets/js/person-search.js'],
]);
$today = date('Y-m-d');
?>
<style>
a { text-decoration: none; }
a:hover { text-decoration: none; }
.ldi-wrap { padding: 0; }
.ldi-layout { display:grid; grid-template-columns:1fr 320px; gap:1.75rem; align-items:start; }
.ldi-main {}
.ldi-aside {}
.ldi-grid { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
.ldi-field label { display:block; font-size:0.82rem; font-weight:600; color:var(--gray-700); margin-bottom:0.35rem; }
.ldi-field input, .ldi-field textarea, .ldi-field select {
width:100%; padding:0.55rem 0.75rem; border:1px solid var(--gray-300); border-radius:0.25rem;
font-size:0.88rem; outline:none; font-family:inherit; background:var(--color-bg);
box-sizing:border-box;
}
.ldi-field input:focus, .ldi-field textarea:focus, .ldi-field select:focus {
border-color:var(--amber-600); box-shadow:0 0 0 2px rgba(217,119,6,0.15);
}
.ldi-field textarea { resize:vertical; min-height:120px; line-height:1.6; }
.ldi-field.required label::after { content:' *'; color:var(--red-500); }
/* Conflict banner */
.ldi-conflict { display:none; background:var(--red-100); border:1px solid var(--red-300); border-radius:0.25rem;
padding:0.75rem 1rem; font-size:0.85rem; color:var(--red-600); font-weight:600; margin-top:0.5rem;
align-items:center; gap:0.5rem; }
.ldi-conflict.visible { display:flex; }
.ldi-future-notice { display:none; background:var(--amber-100); border:1px solid var(--amber-200); border-radius:0.25rem;
padding:0.6rem 0.9rem; font-size:0.82rem; color:var(--amber-800); margin-top:0.5rem; }
.ldi-future-notice.visible { display:block; }
/* Wahl zwischen Reservierung und Sofortausgabe */
.ldi-handover { margin-top:0.75rem; }
.ldi-handover.hidden { display:none; }
.ldi-handover-label { display:block; font-size:0.82rem; font-weight:600; color:var(--gray-700); margin-bottom:0.35rem; }
.ldi-handover-opt { display:flex; align-items:flex-start; gap:0.6rem; padding:0.55rem 0.75rem; border-radius:0.25rem;
background:var(--gray-50); margin-bottom:0.4rem; cursor:pointer; transition:background 0.15s; }
.ldi-handover-opt:hover { background:var(--gray-100); }
.ldi-handover-opt input[type=radio] { width:16px; height:16px; margin-top:0.15rem; accent-color:var(--amber-600); flex-shrink:0; cursor:pointer; }
.ldi-handover-opt strong { display:block; font-size:0.85rem; font-weight:600; color:var(--gray-700); }
.ldi-handover-opt small { display:block; font-size:0.75rem; color:var(--gray-500); margin-top:0.1rem; }
/* Accessories checklist */
.ldi-acc-item { display:flex; align-items:center; gap:0.75rem; padding:0.6rem 0.75rem; border-radius:0.25rem; background:var(--gray-50); margin-bottom:0.4rem; cursor:pointer; transition:background 0.15s; }
.ldi-acc-item:hover { background:var(--gray-100); }
.ldi-acc-item input[type=checkbox] { width:16px; height:16px; accent-color:var(--amber-600); flex-shrink:0; cursor:pointer; }
.ldi-acc-name { font-size:0.88rem; font-weight:600; color:var(--gray-700); }
.ldi-acc-desc { font-size:0.75rem; color:var(--gray-400); }
/* Device selector – hidden, replaced by custom picker */
.ldi-device-select { display:none !important; }
/* Custom device picker – Optik an die zentrale durchsuchbare Auswahl (.ss-*)
angeglichen; behält aber die modul-eigenen Extras (Status-Badges, Gruppen,
„Bereits gewählt", Geräte-Vorschau). */
.ldi-devpicker { position:relative; }
.ldi-dp-trigger {
display:flex; align-items:center; justify-content:space-between; gap:0.5rem;
padding:0.4375rem 0.75rem; border:1px solid var(--color-border); border-radius:var(--radius);
background:var(--color-bg); cursor:pointer; user-select:none; font-family:inherit;
transition:border-color var(--transition-fast), box-shadow var(--transition-fast); min-height:2.375rem;
}
.ldi-dp-trigger:hover, .ldi-dp-trigger:focus,
.ldi-devpicker.open .ldi-dp-trigger {
border-color:var(--color-primary); box-shadow:0 0 0 3px rgba(var(--color-primary-rgb), 0.15); outline:none;
}
.ldi-dp-label { flex:1; min-width:0; font-size:var(--font-size-sm); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ldi-dp-placeholder { color:var(--color-text-muted); }
.ldi-dp-sel-name { font-weight:600; color:var(--color-text); }
.ldi-dp-sel-type { font-size:0.76rem; color:var(--color-text-muted); margin-left:0.35rem; }
.ldi-dp-chevron { color:var(--color-text-muted); flex-shrink:0; transition:transform var(--transition-fast); }
.ldi-devpicker.open .ldi-dp-chevron { transform:rotate(180deg); }
.ldi-dp-panel {
display:none; position:fixed; z-index:99999;
background:var(--color-bg); border:1px solid var(--color-border);
border-radius:var(--radius); box-shadow:var(--shadow-lg); overflow:hidden;
}
.ldi-dp-panel.dp-open { display:block; }
.ldi-dp-search {
display:block; width:calc(100% - 0.8rem); margin:0.4rem; padding:0.375rem 0.625rem;
border:1px solid var(--color-border); border-radius:var(--radius-sm);
font-size:var(--font-size-sm); font-family:inherit; outline:none; background:var(--color-bg); box-sizing:border-box;
}
.ldi-dp-search:focus { border-color:var(--color-primary); box-shadow:0 0 0 2px rgba(var(--color-primary-rgb), 0.15); }
.ldi-dp-list { max-height:260px; overflow-y:auto; padding:0.2rem 0; border-top:1px solid var(--color-border); }
.ldi-dp-group {
padding:0.3rem 0.875rem 0.2rem;
font-size:0.68rem; font-weight:700; text-transform:uppercase; letter-spacing:0.07em;
color:var(--color-text-muted); background:var(--color-bg); position:sticky; top:0;
}
.ldi-dp-option {
display:flex; align-items:center; gap:0.5rem;
padding:0.4rem 0.875rem; cursor:pointer;
transition:background var(--transition-fast);
}
.ldi-dp-option:hover:not(.disabled) { background:var(--color-bg-hover); }
.ldi-dp-option.disabled { opacity:0.4; cursor:not-allowed; }
.ldi-dp-opt-name { flex:1; min-width:0; font-size:var(--font-size-sm); font-weight:600; color:var(--color-text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ldi-dp-opt-type { font-size:0.72rem; color:var(--color-text-muted); flex-shrink:0; }
.ldi-dp-badge { display:inline-flex; padding:0.1rem 0.45rem; border-radius:9999px; font-size:0.68rem; font-weight:700; flex-shrink:0; }
.ldi-dp-badge.avail { background:var(--green-100); color:var(--green-700); }
.ldi-dp-badge.reserved { background:var(--amber-100); color:var(--amber-800); }
.ldi-dp-badge.lent { background:var(--red-100); color:var(--red-600); }
.ldi-dp-badge.other { background:var(--gray-100); color:var(--gray-500); }
.ldi-dp-empty { padding:1rem; text-align:center; font-size:0.83rem; color:var(--gray-400); }
/* Device preview card */
.ldi-device-preview { display:none; margin-top:1rem; }
.ldi-device-preview.visible { display:block; }
.ldi-dev-card {
display:flex; gap:1.25rem; align-items:center;
background:linear-gradient(135deg,var(--amber-50) 0%,var(--color-bg) 60%);
border:1.5px solid var(--amber-200); border-radius:0.25rem;
padding:1rem 1.25rem; position:relative; overflow:hidden;
}
.ldi-dev-card::before {
content:''; position:absolute; top:0; left:0; width:4px; height:100%;
background:linear-gradient(180deg,var(--amber-600),var(--amber-400));
}
.ldi-dev-img {
width:80px; height:80px; border-radius:0.25rem; flex-shrink:0;
background:var(--gray-100); display:flex; align-items:center; justify-content:center;
overflow:hidden; border:2px solid rgba(217,119,6,0.2);
}
.ldi-dev-img img { width:100%; height:100%; object-fit:cover; }
.ldi-dev-info { flex:1; min-width:0; }
.ldi-dev-title { font-size:1.05rem; font-weight:800; color:var(--gray-900); margin-bottom:0.3rem; }
.ldi-dev-chips { display:flex; gap:0.4rem; flex-wrap:wrap; margin-bottom:0.3rem; }
.ldi-dev-chip { display:inline-flex; align-items:center; padding:0.15rem 0.6rem; border-radius:9999px; font-size:0.72rem; font-weight:700; }
.ldi-dev-chip-type { background:var(--amber-100); color:var(--amber-800); }
.ldi-dev-chip-avail { background:var(--green-100); color:var(--green-700); }
.ldi-dev-chip-reserved { background:var(--amber-100); color:var(--amber-800); }
.ldi-dev-serial { font-size:0.75rem; color:var(--gray-400); font-family:monospace; }
.ldi-dev-desc { font-size:0.8rem; color:var(--gray-500); margin-top:0.35rem; line-height:1.5; }
/* Device slots */
.ldi-slot { padding:1.25rem; border-top:1px solid var(--gray-100); }
.ldi-slot:first-child { border-top:none; }
.ldi-slot-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:0.75rem; }
.ldi-slot-num { font-size:0.72rem; font-weight:700; text-transform:uppercase; letter-spacing:0.06em; color:var(--amber-600); }
.ldi-slot-remove { background:none; border:none; color:var(--gray-300); cursor:pointer; padding:0.3rem; border-radius:0.25rem; transition:color 0.15s, background 0.15s; }
.ldi-slot-remove:hover { color:var(--red-600); background:var(--red-100); }
.ldi-add-device { display:flex; align-items:center; gap:0.5rem; width:100%; padding:0.85rem 1.25rem;
font-size:0.83rem; font-weight:600; color:var(--amber-600); background:none; border:none; cursor:pointer;
border-top:1px solid var(--gray-100); transition:background 0.15s; }
.ldi-add-device:hover { background:var(--amber-50); }
.ldi-slot-acc { margin-top:0.75rem; padding-top:0.75rem; border-top:1px solid var(--gray-100); }
.ldi-slot-acc-title { font-size:0.72rem; font-weight:700; text-transform:uppercase; letter-spacing:0.06em; color:var(--gray-400); margin-bottom:0.5rem; }
.ldi-slot-conflict { display:none; background:var(--red-100); border:1px solid var(--red-300); border-radius:0.25rem;
padding:0.6rem 0.9rem; font-size:0.82rem; color:var(--red-600); font-weight:600; margin-top:0.5rem;
align-items:center; gap:0.4rem; }
.ldi-slot-conflict.visible { display:flex; }
/* Submit */
.ldi-submit-bar { padding:1.25rem 1.75rem; background:var(--gray-50); border-top:1px solid var(--gray-200); display:flex; gap:0.75rem; justify-content:flex-end; }
@media(max-width:960px) {
.ldi-layout { grid-template-columns:1fr; }
.ldi-grid { grid-template-columns:1fr; }
}
@media(max-width:640px) {
.ldi-slot { padding:1rem; }
.ldi-dev-card { flex-direction:column; }
.ldi-dev-img { width:60px; height:60px; }
.ldi-submit-bar { flex-direction:column; gap:0.5rem; }
.ldi-submit-bar .btn { width:100%; text-align:center; justify-content:center; }
}
@media(max-width:480px) {
.ldi-dp-panel { left:0 !important; right:0 !important; width:auto !important; }
.ldi-acc-item { font-size:0.8rem; }
}
</style>
<div class="ldi-wrap">
<!-- Content Header -->
<div class="content-header">
<div>
<h1><?= $this->te('Gerät ausgeben') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item"><a href="/loans"><?= $this->te('Leihgeräte') ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('Ausgeben') ?></li>
</ol>
</div>
</div>
<form method="POST" action="/loans/lendings" id="issueForm">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="page-body ldi-layout">
<div class="ldi-main">
<!-- Devices (multi-device support) -->
<?php
$statuses = ['available'=>'Verfügbar','lent'=>'Ausgeliehen','reserved'=>'Reserviert','maintenance'=>'Wartung','retired'=>'Ausgemustert'];
$optionsHtml = '<option value="">— Bitte Gerät auswählen —</option>';
foreach ($devices as $d) {
$dis = in_array($d['status'], ['maintenance','retired']) ? ' disabled' : '';
$typeStr = $d['type'] ? ' (' . htmlspecialchars($d['type'], ENT_QUOTES) . ')' : '';
$sel = ($device && (int)$d['id'] === (int)$device['id']) ? ' selected' : '';
$statusText = $statuses[$d['status']] ?? $d['status'];
if (in_array($d['status'], ['lent','reserved']) && !empty($d['occupied_until'])) {
$statusText = ($d['status'] === 'lent' ? 'Verliehen' : 'Reserviert') . ' bis ' . $d['occupied_until'];
}
$optionsHtml .= '<option value="' . $d['id'] . '" data-status="' . $d['status'] . '"' . $sel . $dis . '>'
. htmlspecialchars($d['title'], ENT_QUOTES) . $typeStr
. ' — ' . $statusText . '</option>';
}
?>
<div class="detail-card" id="devicesCard">
<div class="detail-card-header">
<span class="detail-card-title"><?= $this->te('Geräte auswählen') ?></span>
<span id="deviceCountBadge" style="display:none;font-size:0.72rem;font-weight:700;padding:0.15rem 0.65rem;border-radius:9999px;background:var(--amber-100);color:var(--amber-800);"></span>
</div>
<div id="deviceSlots">
<div class="ldi-slot" id="slot_0" data-slot="0">
<div class="ldi-slot-header">
<span class="ldi-slot-num"><?= $this->te('Gerät 1') ?></span>
<button type="button" class="ldi-slot-remove" onclick="removeSlot(this)" style="display:none;" title="<?= $this->te('Entfernen') ?>" aria-label="<?= $this->te('Gerät entfernen') ?>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<div class="ldi-field required">
<label><?= $this->te('Leihgerät') ?> <span style="color:var(--red-500)">*</span></label>
<div class="ldi-devpicker" id="picker_0"></div>
<select name="device_id[]" class="ldi-device-select slot-select" onchange="onSlotChange(this)" data-no-search>
<?= $optionsHtml ?>
</select>
</div>
<div class="ldi-device-preview <?= $device ? 'visible' : '' ?>" id="preview_0"></div>
<div class="ldi-slot-conflict" id="conflict_0">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<?= $this->te('Dieses Gerät ist im gewählten Zeitraum bereits belegt!') ?>
</div>
<div class="ldi-slot-acc" id="acc_0" style="display:none;">
<div class="ldi-slot-acc-title"><?= $this->te('Mitgegebenes Zubehör') ?></div>
<div id="acc-list_0"></div>
</div>
</div>
</div>
<button type="button" class="ldi-add-device" onclick="addDeviceSlot()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
<?= $this->te('Weiteres Gerät hinzufügen') ?>
</button>
</div>
<!-- Dates -->
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Zeitraum') ?></span></div>
<div class="detail-card-body">
<div class="ldi-grid">
<div class="ldi-field required">
<label for="planned_start"><?= $this->te('Ausgabedatum') ?></label>
<input type="date" name="planned_start" id="planned_start" value="<?= $today ?>" required onchange="onDateChange()">
<small style="color:var(--gray-500);font-size:0.75rem;"><?= $this->te('Vergangenes Datum möglich (Nacherfassung)') ?></small>
</div>
<div class="ldi-field required">
<label for="planned_end"><?= $this->te('Rückgabedatum') ?></label>
<input type="date" name="planned_end" id="planned_end" value="<?= $today ?>" min="<?= $today ?>" required onchange="onDateChange()">
</div>
</div>
<div class="ldi-handover" id="handoverChoice">
<span class="ldi-handover-label"><?= $this->te('Vorgang') ?></span>
<label class="ldi-handover-opt">
<input type="radio" name="handover" value="reserve" checked onchange="onHandoverChange()">
<span>
<strong><?= $this->te('Nur reservieren') ?></strong>
<small><?= $this->te('Zeitraum ist belegt. Bei der Abholung per „Herausgeben“ auf ausgegeben setzen.') ?></small>
</span>
</label>
<label class="ldi-handover-opt">
<input type="radio" name="handover" value="now" onchange="onHandoverChange()">
<span>
<strong><?= $this->te('Jetzt ausgeben') ?></strong>
<small><?= $this->te('Das Gerät wird jetzt übergeben und auf dich als ausgebende Person verbucht.') ?></small>
</span>
</label>
</div>
<div class="ldi-future-notice" id="futureNotice">
<?= $this->t('<strong>Reservierung:</strong> Das Ausgabedatum liegt in der Zukunft. Der Vorgang wird als <em>Reservierung</em> angelegt und kann später per „Herausgeben“ aktiviert werden.') ?>
</div>
<!-- per-slot conflict banners are inside each slot -->
</div>
</div>
<!-- Borrower -->
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Ausleiher') ?></span></div>
<div class="detail-card-body">
<input type="hidden" name="borrower_person_id" id="borrower_person_id" value="">
<div class="ldi-field" style="margin-bottom:1rem;">
<label><?= $this->te('Person aus Verzeichnis suchen') ?></label>
<div class="psx-wrap">
<input type="text" id="borrower_search" class="form-control" placeholder="<?= $this->te('Name oder E-Mail suchen…') ?>" autocomplete="off"
data-person-search="/api/persons/search"
data-ps-min="1"
data-ps-set="name"
data-ps-clear="input"
data-ps-fill-id="borrower_person_id"
data-ps-fill-name="borrower_name"
data-ps-fill-email="borrower_email"
data-ps-badge="ps-badge-borrower">
<span class="psx-selected" id="ps-badge-borrower" style="display:none;">
<?= $this->te('👤 Aus Verzeichnis') ?>
<button type="button" class="psx-selected-clear" aria-label="<?= $this->te('Verknüpfung aufheben') ?>">×</button>
</span>
</div>
</div>
<div class="ldi-grid" style="margin-bottom:1rem;">
<div class="ldi-field required" style="grid-column:1/-1;">
<label for="borrower_name"><?= $this->te('Name') ?></label>
<input type="text" name="borrower_name" id="borrower_name" placeholder="<?= $this->te('Vor- und Nachname') ?>" required>
</div>
<div class="ldi-field">
<label for="borrower_email"><?= $this->te('E-Mail (optional)') ?></label>
<input type="email" name="borrower_email" id="borrower_email" placeholder="<?= $this->te('name@firma.de') ?>">
</div>
<div class="ldi-field">
<label for="borrower_phone"><?= $this->te('Telefon (optional)') ?></label>
<input type="tel" name="borrower_phone" id="borrower_phone" placeholder="+49 …">
</div>
</div>
<div class="ldi-field">
<label for="purpose"><?= $this->te('Verwendungszweck (optional)') ?></label>
<input type="text" name="purpose" id="purpose" placeholder="<?= $this->te('Warum wird das Gerät benötigt?') ?>">
</div>
</div>
</div>
<!-- Notes -->
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Notizen (optional)') ?></span></div>
<div class="detail-card-body">
<div class="ldi-field">
<textarea name="notes" rows="4" placeholder="<?= $this->te('Besonderheiten, Hinweise zur Ausleihe, Zustand des Geräts bei Ausgabe…') ?>"></textarea>
</div>
</div>
</div>
<div class="detail-card">
<div class="ldi-submit-bar">
<a href="/loans" class="btn btn-default"><?= $this->te('Abbrechen') ?></a>
<button type="submit" class="btn btn-primary" id="submitBtn"><?= $this->te('Ausgeben / Reservieren') ?></button>
</div>
</div>
</div>
<!-- Sidebar: help -->
<div class="ldi-aside">
<div class="detail-card">
<div class="detail-card-header"><span class="detail-card-title"><?= $this->te('Hinweise') ?></span></div>
<div class="detail-card-body" style="font-size:0.83rem;color:var(--gray-600);line-height:1.6;">
<p style="margin-bottom:0.75rem;"><?= $this->t('<strong>Sofortausleihe:</strong> Ist das Ausgabedatum heute oder in der Vergangenheit, wird das Gerät direkt als <em>ausgeliehen</em> markiert.') ?></p>
<p style="margin-bottom:0.75rem;"><?= $this->t('<strong>Reservierung:</strong> Bei einem Datum in der <em>Zukunft</em> wird der Vorgang als <em>Reservierung</em> angelegt und kann später per „Herausgeben“ aktiviert werden.') ?></p>
<p><?= $this->t('<strong>Konflikt:</strong> Ist das Gerät im gewählten Zeitraum bereits belegt, kann der Vorgang nicht gespeichert werden.') ?></p>
</div>
</div>
</div>
</div>
</form>
</div>
<style>
</style>
<script>
var deviceData = <?= json_encode(array_column($devices, null, 'id'), JSON_HEX_TAG) ?>;
var deviceAccessories= <?= json_encode($allAccessories, JSON_HEX_TAG) ?>;
var deviceOptions = <?= json_encode($optionsHtml, JSON_HEX_TAG) ?>;
var today = '<?= $today ?>';
var slotCounter = 1;
var conflictTimers = {};
var conflictState = {}; // slotEl id => bool
// ── Slot change ─────────────────────────────────────────────────────────────
function onSlotChange(sel) {
var slot = sel.closest('.ldi-slot');
var id = parseInt(sel.value) || 0;
var preview = slot.querySelector('.ldi-device-preview');
var accWrap = slot.querySelector('.ldi-slot-acc');
var accList = slot.querySelector('[id^="acc-list"]');
if (!id) {
preview.classList.remove('visible');
preview.innerHTML = '';
accWrap.style.display = 'none';
setSlotConflict(slot, false);
updateSubmitBtn();
return;
}
// Render preview
var d = deviceData[id];
if (d) {
var statusLabel = d.status === 'reserved' ? 'Reserviert' : 'Verfügbar';
var statusClass = d.status === 'reserved' ? 'ldi-dev-chip-reserved' : 'ldi-dev-chip-avail';
var typeChip = d.type ? '<span class="ldi-dev-chip ldi-dev-chip-type">' + escHtml(d.type) + '</span>' : '';
var imgHtml = d.image_path
? '<img src="' + d.image_path + '" alt="">'
: '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#d97706" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>';
preview.innerHTML =
'<div class="ldi-dev-card">' +
'<div class="ldi-dev-img">' + imgHtml + '</div>' +
'<div class="ldi-dev-info">' +
'<div class="ldi-dev-title">' + escHtml(d.title) + '</div>' +
'<div class="ldi-dev-chips">' + typeChip +
'<span class="ldi-dev-chip ' + statusClass + '">' + statusLabel + '</span></div>' +
(d.serial_number ? '<div class="ldi-dev-serial">S/N: ' + escHtml(d.serial_number) + '</div>' : '') +
(d.description ? '<div class="ldi-dev-desc">' + escHtml(d.description) + '</div>' : '') +
'</div></div>';
preview.classList.add('visible');
}
// Render accessories
var accs = deviceAccessories[id] || [];
accList.innerHTML = '';
if (accs.length > 0) {
accs.forEach(function(acc) {
var label = document.createElement('label');
label.className = 'ldi-acc-item';
label.innerHTML =
'<input type="checkbox" name="acc_' + id + '[' + acc.id + ']" value="1" checked>' +
'<div><div class="ldi-acc-name">' + escHtml(acc.name) + '</div>' +
(acc.description ? '<div class="ldi-acc-desc">' + escHtml(acc.description) + '</div>' : '') +
'</div>';
accList.appendChild(label);
});
accWrap.style.display = 'block';
} else {
accWrap.style.display = 'none';
}
// Conflict check (debounced)
checkSlotConflict(slot, id);
}
// ── Conflict check ───────────────────────────────────────────────────────────
function checkSlotConflict(slot, deviceId) {
var start = document.getElementById('planned_start').value;
var end = document.getElementById('planned_end').value;
var key = slot.id;
clearTimeout(conflictTimers[key]);
if (!deviceId || !start || !end) return;
conflictTimers[key] = setTimeout(function() {
fetch('/loans/check-conflict?device_id=' + deviceId + '&start=' + start + '&end=' + end, {
headers: {'X-Requested-With': 'XMLHttpRequest'}
})
.then(function(r) { return r.json(); })
.then(function(data) { setSlotConflict(slot, data.conflict); })
.catch(function() { setSlotConflict(slot, false); });
}, 400);
}
function setSlotConflict(slot, hasConflict) {
slot.querySelector('.ldi-slot-conflict').classList.toggle('visible', hasConflict);
conflictState[slot.id] = hasConflict;
updateSubmitBtn();
}
function updateSubmitBtn() {
var anyConflict = Object.values(conflictState).some(Boolean);
document.getElementById('submitBtn').disabled = anyConflict;
}
// Absenden nur mit mindestens einem gewählten Gerät. Ersetzt das `required`
// am versteckten <select>, das die Übermittlung sonst lautlos blockieren würde
// (nicht fokussierbares Pflichtfeld → Browser bricht ohne Meldung ab).
document.getElementById('issueForm').addEventListener('submit', function(e) {
var hasDevice = false;
document.querySelectorAll('.slot-select').forEach(function(s) { if (s.value) hasDevice = true; });
if (!hasDevice) {
e.preventDefault();
var card = document.getElementById('devicesCard');
var warn = document.getElementById('noDeviceWarn');
if (!warn) {
warn = document.createElement('div');
warn.id = 'noDeviceWarn';
warn.style.cssText = 'color:var(--red-600);font-size:0.82rem;font-weight:600;padding:0 1.25rem 0.85rem;';
warn.textContent = 'Bitte mindestens ein Gerät auswählen.';
card.appendChild(warn);
}
var t = card.querySelector('.ldi-dp-trigger');
if (t) t.focus();
}
});
// ── Date change ──────────────────────────────────────────────────────────────
// Beschriftet die Absende-Schaltfläche nach dem gewählten Vorgang.
function onHandoverChange() {
var btn = document.getElementById('submitBtn');
if (!btn) return;
var gewaehlt = document.querySelector('input[name="handover"]:checked');
btn.textContent = (gewaehlt && gewaehlt.value === 'now') ? 'Jetzt ausgeben' : 'Reservieren';
}
function onDateChange() {
var start = document.getElementById('planned_start').value;
var end = document.getElementById('planned_end').value;
var notice = document.getElementById('futureNotice');
// End date must be >= start date
var endInput = document.getElementById('planned_end');
if (start) { endInput.min = start; }
if (end && start && end < start) endInput.value = start;
// Ein Datum in der Zukunft lässt nur eine Reservierung zu; die Wahl
// zwischen Reservierung und Sofortausgabe entfällt dann.
var kuenftig = !!start && start > today;
if (notice) notice.classList.toggle('visible', kuenftig);
var choice = document.getElementById('handoverChoice');
if (choice) {
choice.classList.toggle('hidden', kuenftig);
if (kuenftig) {
var res = document.querySelector('input[name="handover"][value="reserve"]');
if (res) res.checked = true;
}
}
onHandoverChange();
// Recheck all slots
document.querySelectorAll('.ldi-slot').forEach(function(slot) {
var sel = slot.querySelector('.slot-select');
var id = parseInt(sel ? sel.value : 0) || 0;
if (id) checkSlotConflict(slot, id);
});
}
// ── Add/remove slots ─────────────────────────────────────────────────────────
function addDeviceSlot() {
var slotsDiv = document.getElementById('deviceSlots');
var slotId = 'slot_' + slotCounter++;
var div = document.createElement('div');
div.className = 'ldi-slot';
div.id = slotId;
div.setAttribute('data-slot', slotCounter);
div.innerHTML =
'<div class="ldi-slot-header">' +
'<span class="ldi-slot-num"></span>' +
'<button type="button" class="ldi-slot-remove" onclick="removeSlot(this)" title="Entfernen" aria-label="Gerät entfernen">' +
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg></button>' +
'</div>' +
'<div class="ldi-field required"><label>Leihgerät <span style="color:var(--red-500)">*</span></label>' +
'<div class="ldi-devpicker"></div>' +
'<select name="device_id[]" class="ldi-device-select slot-select" onchange="onSlotChange(this)" data-no-search>' +
deviceOptions + '</select></div>' +
'<div class="ldi-device-preview"></div>' +
'<div class="ldi-slot-conflict"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg> Dieses Gerät ist im gewählten Zeitraum bereits belegt!</div>' +
'<div class="ldi-slot-acc" style="display:none;"><div class="ldi-slot-acc-title">Mitgegebenes Zubehör</div><div id="acc-list_' + slotId + '"></div></div>';
slotsDiv.appendChild(div);
initPicker(div);
renumberSlots();
div.querySelector('.ldi-dp-trigger').focus();
}
function removeSlot(btn) {
var slot = btn.closest('.ldi-slot');
if (slot._dpPanel) slot._dpPanel.remove();
delete conflictState[slot.id];
slot.remove();
renumberSlots();
updateSubmitBtn();
}
function renumberSlots() {
var slots = document.querySelectorAll('.ldi-slot');
slots.forEach(function(slot, i) {
slot.querySelector('.ldi-slot-num').textContent = 'Gerät ' + (i + 1);
var removeBtn = slot.querySelector('.ldi-slot-remove');
if (removeBtn) removeBtn.style.display = slots.length > 1 ? 'flex' : 'none';
});
// Badge
var badge = document.getElementById('deviceCountBadge');
if (slots.length > 1) {
badge.textContent = slots.length + ' Geräte';
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
}
function escHtml(s) {
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
// ── Custom device picker ─────────────────────────────────────────────────────
var dpBadgeLabels = { available:'Verfügbar', reserved:'Reserviert', lent:'Ausgeliehen', maintenance:'Wartung', retired:'Ausgemustert' };
var dpBadgeClasses = { available:'avail', reserved:'reserved', lent:'lent', maintenance:'other', retired:'other' };
function dpChevron() {
return '<svg class="ldi-dp-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>';
}
function dpSetTrigger(trigger, deviceId) {
var d = deviceId ? deviceData[deviceId] : null;
if (!d) {
trigger.innerHTML = '<span class="ldi-dp-label ldi-dp-placeholder">— Bitte Gerät auswählen —</span>' + dpChevron();
} else {
var bc = dpBadgeClasses[d.status] || 'other';
var bl = dpBadgeLabels[d.status] || d.status;
if (d.occupied_until) {
bl = (d.status === 'lent' ? 'Verliehen' : 'Reserviert') + ' bis ' + d.occupied_until;
}
trigger.innerHTML =
'<span class="ldi-dp-label"><span class="ldi-dp-sel-name">' + escHtml(d.title) + '</span>' +
(d.type ? '<span class="ldi-dp-sel-type">· ' + escHtml(d.type) + '</span>' : '') + '</span>' +
'<span class="ldi-dp-badge ' + bc + '">' + escHtml(bl) + '</span>' + dpChevron();
}
}
function initPicker(slot) {
var wrap = slot.querySelector('.ldi-devpicker');
var sel = slot.querySelector('.slot-select');
if (!wrap || !sel) return;
// Build DOM
var trigger = document.createElement('div');
trigger.className = 'ldi-dp-trigger';
trigger.setAttribute('tabindex', '0');
trigger.setAttribute('role', 'combobox');
var panel = document.createElement('div');
panel.className = 'ldi-dp-panel';
var search = document.createElement('input');
search.type = 'text';
search.className = 'ldi-dp-search';
search.placeholder = 'Gerät suchen…';
search.setAttribute('autocomplete', 'off');
var listEl = document.createElement('div');
listEl.className = 'ldi-dp-list';
panel.appendChild(search);
panel.appendChild(listEl);
wrap.appendChild(trigger);
// Append panel to body to escape isolation:isolate / overflow:hidden stacking contexts
document.body.appendChild(panel);
slot._dpPanel = panel; // reference for cleanup on removeSlot
dpSetTrigger(trigger, parseInt(sel.value) || 0);
function buildList(filter) {
listEl.innerHTML = '';
// In anderen Slots bereits gewählte Geräte ermitteln (verhindert Doppelauswahl)
var taken = {};
document.querySelectorAll('.slot-select').forEach(function(o) {
if (o !== sel && o.value) taken[o.value] = true;
});
var avail = [], occupied = [], unavail = [];
Object.values(deviceData).forEach(function(d) {
var searchStr = (d.title + ' ' + (d.type || '')).toLowerCase();
if (filter && searchStr.indexOf(filter) === -1) return;
if (d.status === 'available') avail.push(d);
else if (d.status === 'lent' || d.status === 'reserved') occupied.push(d);
else unavail.push(d); // maintenance, retired
});
if (!avail.length && !occupied.length && !unavail.length) {
listEl.innerHTML = '<div class="ldi-dp-empty">Keine Geräte gefunden</div>';
return;
}
function renderGroup(label, devices, disabled) {
if (!devices.length) return;
var gl = document.createElement('div');
gl.className = 'ldi-dp-group';
gl.textContent = label;
listEl.appendChild(gl);
devices.forEach(function(d) {
var isTaken = !!taken[d.id];
var dis = disabled || isTaken;
var bc = dpBadgeClasses[d.status] || 'other';
var bl = dpBadgeLabels[d.status] || d.status;
if (d.occupied_until) {
bl = (d.status === 'lent' ? 'Verliehen' : 'Reserviert') + ' bis ' + d.occupied_until;
}
var opt = document.createElement('div');
opt.className = 'ldi-dp-option' + (dis ? ' disabled' : '');
opt.innerHTML =
'<span class="ldi-dp-opt-name">' + escHtml(d.title) + '</span>' +
(d.type ? '<span class="ldi-dp-opt-type">' + escHtml(d.type) + '</span>' : '') +
(isTaken
? '<span class="ldi-dp-badge other">Bereits gewählt</span>'
: '<span class="ldi-dp-badge ' + bc + '">' + escHtml(bl) + '</span>');
if (!dis) {
opt.addEventListener('click', function() {
sel.value = d.id;
dpSetTrigger(trigger, d.id);
closePicker();
sel.dispatchEvent(new Event('change'));
});
}
listEl.appendChild(opt);
});
}
renderGroup('Verfügbar', avail, false);
renderGroup('Aktuell vergeben', occupied, false);
renderGroup('Nicht verfügbar', unavail, true);
}
function positionPanel() {
var rect = trigger.getBoundingClientRect();
panel.style.top = rect.bottom + 'px';
panel.style.left = rect.left + 'px';
panel.style.width = rect.width + 'px';
}
function openPicker() {
// Close any other open pickers
document.querySelectorAll('.ldi-devpicker.open').forEach(function(w) {
w.classList.remove('open');
var p = w._dpPanel || w.querySelector('.ldi-dp-panel');
if (p) p.classList.remove('dp-open');
});
positionPanel();
wrap.classList.add('open');
panel.classList.add('dp-open');
buildList('');
search.value = '';
setTimeout(function() { search.focus(); }, 30);
}
function closePicker() {
wrap.classList.remove('open');
panel.classList.remove('dp-open');
}
trigger.addEventListener('click', function() {
wrap.classList.contains('open') ? closePicker() : openPicker();
});
trigger.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') { openPicker(); e.preventDefault(); }
if (e.key === 'Escape') closePicker();
});
search.addEventListener('input', function() { buildList(this.value.toLowerCase().trim()); });
// Panel click-outside: panel is on body, not inside wrap
document.addEventListener('click', function(e) {
if (!wrap.contains(e.target) && !panel.contains(e.target)) closePicker();
});
// Beim Scrollen der Seite schließen (das fixe Panel würde sonst neben dem
// Trigger „hängen bleiben"); Scrollen innerhalb der Optionsliste bleibt davon
// unberührt, damit lange Listen durchscrollbar sind.
window.addEventListener('scroll', function(e) {
if (panel.contains(e.target)) return;
closePicker();
}, true);
window.addEventListener('resize', function() { if (wrap.classList.contains('open')) positionPanel(); });
}
// Init pickers for all existing slots
document.querySelectorAll('.ldi-slot').forEach(function(slot) { initPicker(slot); });
// Hinweis (Reservierung/Nacherfassung) initial passend zum Standarddatum setzen
onDateChange();
// Init: restore preselected device
<?php if ($device): ?>
(function() {
var slot = document.getElementById('slot_0');
var sel = slot.querySelector('.slot-select');
if (sel.value) onSlotChange(sel);
})();
<?php endif; ?>
// ── Verzeichnis-Auswahl (Ausleiher) ─────────────────────────────────────────
// Die Suche selbst liefert person-search.js; hier bleibt nur, was darüber
// hinausgeht: die Telefonnummer übernehmen und die Verknüpfung aufheben,
// sobald Name/E-Mail/Telefon manuell geändert werden.
(function() {
var searchInput = document.getElementById('borrower_search');
var personIdEl = document.getElementById('borrower_person_id');
var badgeEl = document.getElementById('ps-badge-borrower');
var phoneEl = document.getElementById('borrower_phone');
if (!searchInput) { return; }
searchInput.addEventListener('person-search:select', function(e) {
if (phoneEl) { phoneEl.value = (e.detail && e.detail.phone) || ''; }
});
[document.getElementById('borrower_name'), document.getElementById('borrower_email'), phoneEl]
.forEach(function(el) {
if (!el) { return; }
el.addEventListener('input', function() {
if (personIdEl.value !== '') {
personIdEl.value = '';
badgeEl.style.display = 'none';
}
});
});
})();
</script>