OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

issue.php

43 KB
<?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&auml;te ausw&auml;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&auml;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&auml;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&auml;t ist im gew&auml;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&ouml;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&auml;t hinzuf&uuml;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&uuml;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 &bdquo;Herausgeben&ldquo; 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&auml;t wird jetzt &uuml;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&auml;ter per &bdquo;Herausgeben&ldquo; 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&auml;t ben&ouml;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&auml;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&auml;ter per &bdquo;Herausgeben&ldquo; aktiviert werden.') ?></p>
                        <p><?= $this->t('<strong>Konflikt:</strong> Ist das Ger&auml;t im gew&auml;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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

// ── 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>