OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

location.php

37 KB
<?php
$this->layout('layouts/main', [
    'page_title'    => $page_title,
    'current_route' => $current_route,
    'full_width'    => true,
    'extra_css'     => ['/assets/css/infrastructure.css?v=' . filemtime(dirname(__DIR__, 2) . '/public/assets/css/infrastructure.css')],
]);
$loc = $view['location'];
$fp  = $view['floorplan'] ?? null;
$markerIcons = $markerIcons ?? [];
$markerAssets = $markerAssets ?? [];
?>

<div class="content-header">
    <div>
        <h1><?= $this->e($loc['name']) ?></h1>
        <ol class="breadcrumb">
            <li class="breadcrumb-item"><a href="/infrastructure"><?= $this->te('Infrastruktur') ?></a></li>
            <?php foreach ($view['breadcrumb'] as $b): ?>
                <?php if ((int)$b['id'] === (int)$loc['id']): ?>
                    <li class="breadcrumb-item active"><?= $this->e($b['name']) ?></li>
                <?php else: ?>
                    <li class="breadcrumb-item"><a href="/infrastructure/locations/<?= (int)$b['id'] ?>"><?= $this->e($b['name']) ?></a></li>
                <?php endif; ?>
            <?php endforeach; ?>
        </ol>
    </div>
    <?php if ($can_admin): ?>
    <div class="content-header-actions">
        <button type="button" class="btn btn-default btn-sm" onclick="document.getElementById('inf-plan-modal').style.display='flex'">
            <i class="fas fa-map"></i> <?= $fp ? 'Lageplan ersetzen' : 'Lageplan hinterlegen' ?>
        </button>
        <button type="button" class="btn btn-primary btn-sm" onclick="document.getElementById('inf-rack-modal').style.display='flex'">
            <i class="fas fa-plus"></i> <?= $this->te('Rack hier anlegen') ?>
        </button>
    </div>
    <?php endif; ?>
</div>

<div class="page-body">

    <?php if ($fp): ?>
    <div class="detail-card" id="inf-plan-card">
        <div class="detail-card-header">
            <span class="detail-card-title"><i class="fas fa-map"></i> <?= $this->te('Lageplan') ?></span>
            <span class="inf-plan-headright">
                <span class="inf-hint inf-plan-hint">
                    <?php if ($can_admin): ?><?= $this->te('Klick auf den Plan setzt einen Marker · Marker ziehen zum Verschieben · Klick auf einen Marker öffnet das Ziel') ?><?php else: ?><?= $this->te('Klick auf einen Marker öffnet das Gerät') ?><?php endif; ?>
                </span>
                <button type="button" class="btn btn-xs btn-default" onclick="infExportPlan()" title="<?= $this->te('Plan mit Markern als PNG exportieren') ?>"><i class="fas fa-download"></i> <?= $this->te('Exportieren') ?></button>
                <button type="button" class="btn btn-xs btn-default" id="inf-plan-toggle" onclick="infTogglePlan()"><i class="fas fa-chevron-up"></i> <?= $this->te('Einklappen') ?></button>
            </span>
        </div>
        <div class="detail-card-body" id="inf-plan-body">
            <div class="inf-plan" id="inf-plan"<?= $can_admin ? ' data-admin="1"' : '' ?>>
                <img src="<?= $this->e($fp['url']) ?>" alt="<?= $this->te('Lageplan {standort}', ['standort' => $loc['name']]) ?>" class="inf-plan-img" id="inf-plan-img" draggable="false">
                <?php foreach ($fp['markers'] as $mk): ?>
                    <div class="inf-marker inf-marker--<?= $this->e($mk['type']) ?>" data-id="<?= (int)$mk['id'] ?>"
                         data-type="<?= $this->e($mk['type']) ?>" data-link="<?= $this->e($mk['link'] ?? '') ?>"
                         data-rack="<?= (int)($mk['rack_id'] ?? 0) ?>" data-asset="<?= (int)($mk['asset_id'] ?? 0) ?>"
                         data-loc="<?= (int)($mk['loc_id'] ?? 0) ?>" data-label="<?= $this->e($mk['type'] === 'object' ? $mk['label'] : '') ?>"
                         data-icon="<?= $this->e($mk['icon']) ?>"
                         data-conn-panel-mount="<?= (int)($mk['conn']['panel_mount_id'] ?? 0) ?>" data-conn-panel-port="<?= $this->e($mk['conn']['panel_port'] ?? '') ?>"
                         data-conn-switch-mount="<?= (int)($mk['conn']['switch_mount_id'] ?? 0) ?>" data-conn-switch-port="<?= $this->e($mk['conn']['switch_port'] ?? '') ?>"
                         data-conn-cable="<?= $this->e($mk['conn']['cable'] ?? 'copper') ?>"
                         title="<?= $this->e($mk['label'] . ' · ' . $mk['type_label'] . ($mk['status'] === 'danger' ? ' · Gerät(e) offline' : '') . ($mk['conn'] ? ' · ↔ ' . $mk['conn']['panel_dev'] . ' [' . $mk['conn']['panel_port'] . ']' . ($mk['conn']['switch_mount_id'] ? ' → ' . $mk['conn']['switch_dev'] . ' [' . $mk['conn']['switch_port'] . ']' : '') : '')) ?>"
                         style="left:<?= $mk['x'] ?>%;top:<?= $mk['y'] ?>%">
                        <span class="inf-marker-pin">
                            <i class="fas <?= $this->e($mk['icon']) ?>"></i>
                            <?php if ($mk['status']): ?><span class="inf-marker-dot inf-marker-dot--<?= $this->e($mk['status']) ?>"></span><?php endif; ?>
                        </span>
                        <span class="inf-marker-lbl"><?= $this->e($mk['label']) ?></span>
                        <?php if ($can_admin): ?>
                        <button type="button" class="inf-marker-edit" title="<?= $this->te('Marker bearbeiten') ?>" onclick="infEditMarker(event, <?= (int)$mk['id'] ?>)"><i class="fas fa-pen"></i></button>
                        <button type="button" class="inf-marker-del" title="<?= $this->te('Marker entfernen') ?>" onclick="infDeleteMarker(event, <?= (int)$mk['id'] ?>)">&times;</button>
                        <?php endif; ?>
                    </div>
                <?php endforeach; ?>
                </div>
                <?php
                $mkCounts = ['rack' => 0, 'asset' => 0, 'location' => 0, 'object' => 0];
                foreach ($fp['markers'] as $mk) { $mkCounts[$mk['type']] = ($mkCounts[$mk['type']] ?? 0) + 1; }
                $legend = ['rack' => ['fa-server', 'Racks'], 'asset' => ['fa-network-wired', 'Geräte'], 'location' => ['fa-building', 'Standorte'], 'object' => ['fa-map-marker-alt', 'Objekte']];
                ?>
                <div class="inf-plan-legend" id="inf-plan-legend">
                    <?php foreach ($legend as $key => $meta): if (($mkCounts[$key] ?? 0) === 0) { continue; } ?>
                        <button type="button" class="inf-legend-chip inf-legend-chip--<?= $key ?>" data-type="<?= $key ?>" onclick="infToggleType('<?= $key ?>')">
                            <i class="fas <?= $meta[0] ?>"></i> <?= $meta[1] ?> <span class="inf-legend-n"><?= (int)$mkCounts[$key] ?></span>
                        </button>
                    <?php endforeach; ?>
            </div>
        </div>
    </div>
    <?php endif; ?>

    <h2 class="inf-section-title"><i class="fas fa-server"></i> <?= $this->te('Racks an diesem Standort') ?></h2>
    <?php if (empty($view['directRacks'])): ?>
        <div class="empty-state"><p><?= $this->te('Keine Racks direkt an diesem Standort.') ?></p></div>
    <?php else: ?>
        <div class="inf-rack-grid">
            <?php foreach ($view['directRacks'] as $r): ?>
            <a class="inf-rack-card" href="/infrastructure/racks/<?= (int)$r['id'] ?>">
                <span class="inf-rack-mini" title="<?= $this->te('Belegungsvorschau') ?>">
                    <?php if (!empty($r['mini'])): ?>
                        <?php foreach ($r['mini'] as $seg): ?>
                            <i class="inf-mini-seg inf-mini-seg--<?= $this->e($seg['type']) ?>" style="bottom:<?= $seg['bottom'] ?>%;height:<?= $seg['height'] ?>%"></i>
                        <?php endforeach; ?>
                    <?php else: ?>
                        <i class="fas fa-server inf-mini-empty"></i>
                    <?php endif; ?>
                </span>
                <span class="inf-rack-body">
                    <span class="inf-rack-nm"><?= $this->e($r['name']) ?></span>
                    <span class="inf-rack-meta"><?= $this->te('{he} HE · {geraete} Geräte · {auslastung}&#37;', ['he' => (int)$r['height_u'], 'geraete' => (int)$r['devices'], 'auslastung' => (int)$r['util']]) ?></span>
                    <?php if ((int)$r['offline'] > 0): ?>
                        <span class="status-pill status-pill--danger"><span class="status-dot status-dot--danger"></span> <?= $this->te('{anzahl} offline', ['anzahl' => (int)$r['offline']]) ?></span>
                    <?php else: ?>
                        <span class="status-pill status-pill--success"><span class="status-dot status-dot--success"></span> <?= $this->te('online') ?></span>
                    <?php endif; ?>
                </span>
            </a>
            <?php endforeach; ?>
        </div>
    <?php endif; ?>

    <?php if (!empty($view['subLocations'])): ?>
        <h2 class="inf-section-title"><i class="fas fa-project-diagram"></i> <?= $this->te('Unterstandorte') ?></h2>
        <div class="inf-subloc-grid">
            <?php foreach ($view['subLocations'] as $s): $sloc = $s['location']; ?>
            <a class="inf-subloc-card" href="/infrastructure/locations/<?= (int)$sloc['id'] ?>">
                <span class="inf-subloc-ic"><i class="fas fa-project-diagram"></i></span>
                <span>
                    <span class="inf-subloc-nm"><?= $this->e($sloc['name']) ?></span>
                    <span class="inf-subloc-meta"><?= $this->te('{racks} Rack(s) · {geraete} Geräte', ['racks' => (int)$s['racks'], 'geraete' => (int)$s['devices']]) ?></span>
                </span>
                <span class="inf-chev">&rsaquo;</span>
            </a>
            <?php endforeach; ?>
        </div>
    <?php endif; ?>

</div>

<?php if ($can_admin): ?>
<div class="modal-overlay" id="inf-rack-modal" style="display:none;" onclick="if(event.target===this)this.style.display='none'">
    <div class="modal-card">
        <div class="modal-header"><h2><?= $this->te('Rack anlegen') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" onclick="document.getElementById('inf-rack-modal').style.display='none'">&times;</button></div>
        <form method="POST" action="/infrastructure/racks">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
            <input type="hidden" name="location_id" value="<?= (int)$loc['id'] ?>">
            <div class="modal-body">
                <div class="form-group">
                    <label><?= $this->te('Name') ?> <span class="text-danger">*</span></label>
                    <input type="text" name="name" class="form-control form-control-sm" placeholder="<?= $this->te('z. B. SRV-01') ?>" required>
                </div>
                <div class="form-group">
                    <label for="f-0204-height_u"><?= $this->te('Höhe (HE)') ?></label>
                    <input id="f-0204-height_u" type="number" name="height_u" class="form-control form-control-sm" value="42" min="1" max="60">
                </div>
                <div class="form-group">
                    <label for="f-0204-numbering"><?= $this->te('Nummerierung') ?></label>
                    <select id="f-0204-numbering" name="numbering" class="form-control form-control-sm" data-searchable>
                        <option value="bottom_up"><?= $this->te('von unten (1 unten)') ?></option>
                        <option value="top_down"><?= $this->te('von oben (1 oben)') ?></option>
                    </select>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" onclick="document.getElementById('inf-rack-modal').style.display='none'"><?= $this->te('Abbrechen') ?></button>
                <button type="submit" class="btn btn-primary"><?= $this->te('Anlegen') ?></button>
            </div>
        </form>
    </div>
</div>

<!-- Modal: Lageplan hochladen / ersetzen / entfernen -->
<div class="modal-overlay" id="inf-plan-modal" style="display:none;" onclick="if(event.target===this)this.style.display='none'">
    <div class="modal-card">
        <div class="modal-header"><h2><?= $this->te('Lageplan') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" onclick="document.getElementById('inf-plan-modal').style.display='none'">&times;</button></div>
        <form method="POST" action="/infrastructure/locations/<?= (int)$loc['id'] ?>/floorplan" enctype="multipart/form-data">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
            <div class="modal-body">
                <div class="form-group">
                    <label for="f-0204-floorplan"><?= $this->te('Bild oder PDF') ?> <span class="text-danger">*</span></label>
                    <input id="f-0204-floorplan" type="file" name="floorplan" class="form-control form-control-sm" accept="image/png,image/jpeg,image/gif,image/webp,application/pdf" required>
                    <small class="form-text text-muted"><?= $this->te('PNG, JPG, GIF, WebP oder PDF · max. 20 MB. PDFs werden automatisch in ein Bild umgewandelt (erste Seite). Ein bestehender Plan wird ersetzt; die Marker bleiben erhalten.') ?></small>
                </div>
            </div>
            <div class="modal-footer">
                <?php if ($fp): ?>
                <button type="button" class="btn btn-outline-danger" style="margin-right:auto" onclick="if(confirm('Lageplan entfernen? Die Marker bleiben gespeichert.')) document.getElementById('inf-plan-del-form').submit();"><i class="fas fa-trash"></i> <?= $this->te('Entfernen') ?></button>
                <?php endif; ?>
                <button type="button" class="btn btn-default" onclick="document.getElementById('inf-plan-modal').style.display='none'"><?= $this->te('Abbrechen') ?></button>
                <button type="submit" class="btn btn-primary"><i class="fas fa-upload"></i> <?= $this->te('Hochladen') ?></button>
            </div>
        </form>
        <?php if ($fp): ?>
        <form method="POST" action="/infrastructure/locations/<?= (int)$loc['id'] ?>/floorplan/delete" id="inf-plan-del-form" style="display:none">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
        </form>
        <?php endif; ?>
    </div>
</div>

<!-- Modal: Marker setzen / bearbeiten -->
<div class="modal-overlay" id="inf-marker-modal" style="display:none;" onclick="if(event.target===this)this.style.display='none'">
    <div class="modal-card">
        <div class="modal-header"><h2 id="inf-marker-title"><?= $this->te('Marker setzen') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" onclick="document.getElementById('inf-marker-modal').style.display='none'">&times;</button></div>
        <form method="POST" action="" id="inf-marker-form">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
            <input type="hidden" name="x" id="inf-mk-x">
            <input type="hidden" name="y" id="inf-mk-y">
            <input type="hidden" name="icon" id="inf-mk-icon" value="fa-server">
            <div class="modal-body">
                <div class="form-group">
                    <label for="inf-mk-type"><?= $this->te('Typ') ?></label>
                    <select name="marker_type" id="inf-mk-type" class="form-control form-control-sm" onchange="infMkType(this.value)">
                        <option value="rack"><?= $this->te('Rack') ?></option>
                        <option value="asset"><?= $this->te('Inventar-Gerät (Accesspoint, Drucker …)') ?></option>
                        <option value="location"><?= $this->te('Standort / Raum') ?></option>
                        <option value="object"><?= $this->te('Freies Objekt') ?></option>
                    </select>
                </div>
                <div class="form-group" id="inf-mk-rack-wrap">
                    <label for="inf-mk-rack"><?= $this->te('Rack') ?></label>
                    <select name="rack_id" id="inf-mk-rack" class="form-control form-control-sm" data-searchable>
                        <option value=""><?= $this->te('— wählen —') ?></option>
                        <?php foreach ($view['directRacks'] as $r): ?>
                            <option value="<?= (int)$r['id'] ?>"><?= $this->e($r['name']) ?></option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <div class="form-group" id="inf-mk-asset-wrap" style="display:none">
                    <label for="inf-mk-asset"><?= $this->te('Inventar-Gerät') ?></label>
                    <select name="asset_id" id="inf-mk-asset" class="form-control form-control-sm" data-searchable onchange="infDetectAsset(this.value)">
                        <option value=""><?= $this->te('— wählen —') ?></option>
                        <?php foreach ($markerAssets as $a): ?>
                            <option value="<?= (int)$a['id'] ?>"><?= $this->e($a['name'] . ($a['inv'] !== '' ? ' (' . $a['inv'] . ')' : '')) ?></option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <div class="form-group" id="inf-mk-loc-wrap" style="display:none">
                    <label for="inf-mk-loc"><?= $this->te('Standort / Raum') ?></label>
                    <select name="target_location_id" id="inf-mk-loc" class="form-control form-control-sm" data-searchable>
                        <option value=""><?= $this->te('— wählen —') ?></option>
                        <?php foreach ($markerLocations as $o): ?>
                            <option value="<?= (int)$o['id'] ?>"><?= str_repeat('&nbsp;&nbsp;', (int)$o['depth']) ?><?= $this->e($o['name']) ?></option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <div class="form-group" id="inf-mk-label-wrap" style="display:none">
                    <label><?= $this->te('Bezeichnung') ?> <span class="text-muted"><?= $this->te('(z. B. „USV-Schrank")') ?></span></label>
                    <input type="text" name="label" id="inf-mk-label" class="form-control form-control-sm" placeholder="<?= $this->te('Bezeichnung') ?>">
                </div>
                <div class="form-group" id="inf-mk-icon-wrap" style="display:none">
                    <label><?= $this->te('Symbol') ?></label>
                    <div class="inf-icon-picker" id="inf-mk-icon-picker">
                        <?php foreach ($markerIcons as $ic): ?>
                            <button type="button" class="inf-icon-opt" data-icon="<?= $this->e($ic) ?>" title="<?= $this->e($ic) ?>" onclick="infPickIcon(this)"><i class="fas <?= $this->e($ic) ?>"></i></button>
                        <?php endforeach; ?>
                    </div>
                </div>
                <div id="inf-mk-conn-wrap" style="display:none">
                    <hr>
                    <p class="text-muted" style="font-size:.78rem;margin:0 0 .6rem"><?= $this->t('Verkabelung (optional): Das Gerät terminiert am <strong>Patchpanel</strong>; von dort geht ein Patchkabel auf den Switch.') ?></p>
                    <?php
                        // Alle möglichen Endpunkte (Racks/Geräte) – für Patchpanel- und Switch-Auswahl.
                        $renderTargetOptions = function () use ($markerTargets) {
                            foreach ($markerTargets as $g) {
                                foreach ($g['mounts'] as $tm) {
                                    echo '<option value="' . (int)$tm['id'] . '">'
                                        . $this->e(($g['location'] ? $g['location'] . ' · ' : '') . $g['rack'] . ' · ' . $tm['name'])
                                        . '</option>';
                                }
                            }
                        };
                    ?>
                    <div class="inf-form-row-2">
                        <div class="form-group">
                            <label for="inf-mk-conn-panel"><?= $this->te('Patchpanel') ?></label>
                            <select name="conn_panel_mount_id" id="inf-mk-conn-panel" class="form-control form-control-sm" data-searchable onchange="infConnPorts('panel', this.value)">
                                <option value=""><?= $this->te('— nicht verbunden —') ?></option>
                                <?php $renderTargetOptions(); ?>
                            </select>
                        </div>
                        <div class="form-group">
                            <label><?= $this->te('Panel-Port') ?></label>
                            <input type="text" name="conn_panel_port" id="inf-mk-conn-panel-port" class="form-control form-control-sm" list="inf-mk-conn-panel-portlist" placeholder="z. B. 12">
                            <datalist id="inf-mk-conn-panel-portlist"></datalist>
                        </div>
                    </div>
                    <div id="inf-mk-conn-hint" class="inf-conn-hint" style="display:none"></div>
                    <div class="inf-form-row-2">
                        <div class="form-group">
                            <label for="inf-mk-conn-switch"><?= $this->te('Switch') ?> <span class="text-muted"><?= $this->te('(Patchkabel)') ?></span></label>
                            <select name="conn_switch_mount_id" id="inf-mk-conn-switch" class="form-control form-control-sm" data-searchable onchange="infConnPorts('switch', this.value)">
                                <option value=""><?= $this->te('— kein Patchkabel —') ?></option>
                                <?php $renderTargetOptions(); ?>
                            </select>
                        </div>
                        <div class="form-group">
                            <label><?= $this->te('Switch-Port') ?></label>
                            <input type="text" name="conn_switch_port" id="inf-mk-conn-switch-port" class="form-control form-control-sm" list="inf-mk-conn-switch-portlist" placeholder="z. B. 24">
                            <datalist id="inf-mk-conn-switch-portlist"></datalist>
                        </div>
                    </div>
                    <div class="form-group">
                        <label for="inf-mk-conn-cable"><?= $this->te('Kabeltyp') ?></label>
                        <select name="conn_cable" id="inf-mk-conn-cable" class="form-control form-control-sm">
                            <option value="copper"><?= $this->te('Kupfer') ?></option>
                            <option value="fiber"><?= $this->te('Fiber') ?></option>
                            <option value="dac">DAC</option>
                            <option value="power"><?= $this->te('Strom') ?></option>
                            <option value="other"><?= $this->te('Sonstige') ?></option>
                        </select>
                    </div>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" onclick="document.getElementById('inf-marker-modal').style.display='none'"><?= $this->te('Abbrechen') ?></button>
                <button type="submit" class="btn btn-primary"><?= $this->te('Speichern') ?></button>
            </div>
        </form>
    </div>
</div>
<?php endif; ?>

<?php if ($fp): ?>
<script>
var INF_LOC_ID = <?= (int)$loc['id'] ?>;
var INF_CSRF = <?= json_encode($this->csrf()) ?>;
var INFRA_TARGET_PORTS = <?= json_encode((object)array_reduce($markerTargets ?? [], function ($c, $g) {
    foreach ($g['mounts'] as $tm) {
        $c[(int)$tm['id']] = ['kind' => $tm['port_kind'], 'pc' => (int)$tm['port_count'], 'sfp' => (int)$tm['sfp_count'], 'qsfp' => (int)$tm['qsfp_count'],
            'pcR' => (int)($tm['port_count_rear'] ?? 0), 'sfpR' => (int)($tm['sfp_count_rear'] ?? 0), 'qsfpR' => (int)($tm['qsfp_count_rear'] ?? 0)];
    }
    return $c;
}, []), JSON_UNESCAPED_UNICODE) ?>;

// Port-Vorschläge für den gewählten Ziel-Switch (Front + Rückseite „R…")
function infTargetPortList(p){
    var out = [];
    function add(kind, pc, sfp, qsfp, pfx){ pfx=pfx||''; pc=pc||0; sfp=sfp||0; qsfp=qsfp||0;
        if(kind==='switch'||kind==='firewall'){ for(var i=1;i<=pc;i++)out.push(pfx+i); for(var s=1;s<=sfp;s++)out.push(pfx+'SFP'+s); for(var x=1;x<=qsfp;x++)out.push(pfx+'QSFP'+x); }
        else if(kind==='patchpanel'||kind==='pbx'){ for(var i=1;i<=pc;i++)out.push(pfx+i); }
        else if(kind==='gateway'){ out.push(pfx+'WAN1'); for(var i=1;i<=Math.max(1,pc);i++)out.push(pfx+'LAN'+i); for(var s=1;s<=sfp;s++)out.push(pfx+'SFP'+s); for(var x=1;x<=qsfp;x++)out.push(pfx+'QSFP'+x); }
        else if(kind==='server'){ for(var i=1;i<=pc;i++)out.push(pfx+'NIC'+i); for(var s=1;s<=sfp;s++)out.push(pfx+'SFP'+s); for(var x=1;x<=qsfp;x++)out.push(pfx+'QSFP'+x); }
        else if(kind==='ups'){ var un=pc>0?pc:8; for(var i=1;i<=un;i++)out.push(pfx+'OUT'+i); }
    }
    if(!p) return out;
    add(p.kind, p.pc, p.sfp, p.qsfp, '');
    if((p.pcR||0)+(p.sfpR||0)+(p.qsfpR||0) > 0){ add(p.kind, p.pcR, p.sfpR, p.qsfpR, 'R'); }
    return out;
}
// Port-Vorschläge für Patchpanel- bzw. Switch-Auswahl (which = 'panel' | 'switch')
function infConnPorts(which, mountId){
    var dl = document.getElementById('inf-mk-conn-' + which + '-portlist'); if(!dl) return;
    dl.innerHTML = '';
    infTargetPortList(INFRA_TARGET_PORTS[mountId]).forEach(function(v){ var o=document.createElement('option'); o.value=v; dl.appendChild(o); });
}
// Automatische Switch-Port-Erkennung (Ubiquiti: Seriennummer = MAC → UniFi-Uplink).
// Belegt den Switch-Hop (Patchkabel) vor – das Patchpanel kennt UniFi nicht.
function infDetectAsset(assetId){
    var hint = document.getElementById('inf-mk-conn-hint');
    if(hint){ hint.style.display='none'; hint.textContent=''; }
    if(!assetId) return;
    fetch('/infrastructure/markers/detect?asset_id=' + encodeURIComponent(assetId), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function(r){ return r.json(); })
        .then(function(d){
            if(!d || !d.found) return;
            var sm = document.getElementById('inf-mk-conn-switch');
            if(sm && sm.querySelector('option[value="' + d.mount_id + '"]')){
                sm.value = d.mount_id; if(window.ssSync) window.ssSync(sm);
                infConnPorts('switch', d.mount_id);
                document.getElementById('inf-mk-conn-switch-port').value = d.port;
                if(hint){ hint.style.display=''; hint.innerHTML = '<i class="fas fa-bolt"></i> Switch automatisch erkannt: ' + (d.rack ? d.rack + ' · ' : '') + d.switch + ' · Port ' + d.port + ' – bitte noch das Patchpanel + Panel-Port angeben.'; }
            }
        })
        .catch(function(){});
}

// Plan ein-/ausklappen (pro Standort im Browser gemerkt)
function infTogglePlan(){
    var body = document.getElementById('inf-plan-body');
    var btn = document.getElementById('inf-plan-toggle');
    if(!body) return;
    var collapsed = body.style.display !== 'none';
    body.style.display = collapsed ? 'none' : '';
    if(btn){ btn.innerHTML = collapsed ? '<i class="fas fa-chevron-down"></i> Ausklappen' : '<i class="fas fa-chevron-up"></i> Einklappen'; }
    try { localStorage.setItem('inf-plan-collapsed-' + INF_LOC_ID, collapsed ? '1' : '0'); } catch(e){}
}
(function(){
    try { if(localStorage.getItem('inf-plan-collapsed-' + INF_LOC_ID) === '1'){ infTogglePlan(); } } catch(e){}
})();

document.addEventListener('keydown', function(e){
    if(e.key==='Escape'){ document.querySelectorAll('.modal-overlay').forEach(function(m){ m.style.display='none'; }); }
});

function infPostForm(action, fields){
    var f = document.createElement('form'); f.method='POST'; f.action=action;
    fields._csrf = INF_CSRF;
    Object.keys(fields).forEach(function(k){ var i=document.createElement('input'); i.type='hidden'; i.name=k; i.value=fields[k]==null?'':fields[k]; f.appendChild(i); });
    document.body.appendChild(f); f.submit();
}
window.infDeleteMarker = function(ev, id){
    ev.stopPropagation(); ev.preventDefault();
    if(confirm('Marker entfernen?')){ infPostForm('/infrastructure/markers/'+id+'/delete', {}); }
};

// Marker-Dialog: Typ-Umschaltung + Symbolauswahl
function infMkType(v){
    var s = function(id, on){ var e=document.getElementById(id); if(e){ e.style.display = on ? '' : 'none'; } };
    s('inf-mk-rack-wrap', v==='rack');
    s('inf-mk-asset-wrap', v==='asset');
    s('inf-mk-loc-wrap', v==='location');
    s('inf-mk-label-wrap', v==='object');
    s('inf-mk-icon-wrap', v!=='rack'); // Rack hat ein festes Symbol
    s('inf-mk-conn-wrap', v==='asset' || v==='object'); // Anschluss nur für Geräte/Objekte
    if(v==='rack'){ var h=document.getElementById('inf-mk-icon'); if(h){ h.value='fa-server'; } }
}
function infPickIcon(btn){
    document.getElementById('inf-mk-icon').value = btn.getAttribute('data-icon');
    Array.prototype.forEach.call(document.querySelectorAll('#inf-mk-icon-picker .inf-icon-opt'), function(b){ b.classList.toggle('inf-active', b===btn); });
}
function infMkSetIcon(icon){
    document.getElementById('inf-mk-icon').value = icon || '';
    Array.prototype.forEach.call(document.querySelectorAll('#inf-mk-icon-picker .inf-icon-opt'), function(b){ b.classList.toggle('inf-active', b.getAttribute('data-icon')===icon); });
}
window.infEditMarker = function(ev, id){
    ev.stopPropagation(); ev.preventDefault();
    var mk = document.querySelector('.inf-marker[data-id="'+id+'"]'); if(!mk) return;
    var type = mk.getAttribute('data-type') || 'rack';
    var f = document.getElementById('inf-marker-form');
    f.action = '/infrastructure/markers/' + id + '/update';
    document.getElementById('inf-marker-title').textContent = 'Marker bearbeiten';
    document.getElementById('inf-mk-type').value = type;
    infMkType(type);
    document.getElementById('inf-mk-x').value = parseFloat(mk.style.left) || 0;
    document.getElementById('inf-mk-y').value = parseFloat(mk.style.top) || 0;
    document.getElementById('inf-mk-rack').value  = mk.getAttribute('data-rack')  !== '0' ? mk.getAttribute('data-rack')  : '';
    document.getElementById('inf-mk-asset').value = mk.getAttribute('data-asset') !== '0' ? mk.getAttribute('data-asset') : '';
    document.getElementById('inf-mk-loc').value   = mk.getAttribute('data-loc')   !== '0' ? mk.getAttribute('data-loc')   : '';
    document.getElementById('inf-mk-label').value = mk.getAttribute('data-label') || '';
    infMkSetIcon(mk.getAttribute('data-icon') || '');
    var pm = mk.getAttribute('data-conn-panel-mount'); pm = (pm && pm !== '0') ? pm : '';
    document.getElementById('inf-mk-conn-panel').value = pm;
    infConnPorts('panel', pm);
    document.getElementById('inf-mk-conn-panel-port').value = mk.getAttribute('data-conn-panel-port') || '';
    var sm = mk.getAttribute('data-conn-switch-mount'); sm = (sm && sm !== '0') ? sm : '';
    document.getElementById('inf-mk-conn-switch').value = sm;
    infConnPorts('switch', sm);
    document.getElementById('inf-mk-conn-switch-port').value = mk.getAttribute('data-conn-switch-port') || '';
    document.getElementById('inf-mk-conn-cable').value = mk.getAttribute('data-conn-cable') || 'copper';
    if(window.ssSync){ ['inf-mk-type','inf-mk-rack','inf-mk-asset','inf-mk-loc','inf-mk-conn-panel','inf-mk-conn-switch'].forEach(function(i){ var el=document.getElementById(i); if(el) window.ssSync(el); }); }
    document.getElementById('inf-marker-modal').style.display='flex';
};

// Marker-Typen ein-/ausblenden (Filter/Legende), Zustand pro Standort gemerkt
function infToggleType(type){
    var plan = document.getElementById('inf-plan');
    var chip = document.querySelector('.inf-legend-chip[data-type="'+type+'"]');
    var hidden = plan.classList.toggle('inf-hide-' + type);
    if(chip){ chip.classList.toggle('inf-legend-off', hidden); }
    try { localStorage.setItem('inf-mk-hide-' + INF_LOC_ID + '-' + type, hidden ? '1' : '0'); } catch(e){}
}
(function(){
    var plan = document.getElementById('inf-plan'); if(!plan) return;
    ['rack','asset','location','object'].forEach(function(t){
        var off = false; try { off = localStorage.getItem('inf-mk-hide-' + INF_LOC_ID + '-' + t) === '1'; } catch(e){}
        if(off){ plan.classList.add('inf-hide-' + t); var chip=document.querySelector('.inf-legend-chip[data-type="'+t+'"]'); if(chip) chip.classList.add('inf-legend-off'); }
    });
})();

// Lageplan mit Markern als PNG exportieren (clientseitig via Canvas)
function infExportPlan(){
    var img = document.getElementById('inf-plan-img'); if(!img || !img.naturalWidth){ return; }
    var c = document.createElement('canvas'); c.width = img.naturalWidth; c.height = img.naturalHeight;
    var ctx = c.getContext('2d');
    try { ctx.drawImage(img, 0, 0, c.width, c.height); } catch(e){ alert('Export nicht möglich.'); return; }
    Array.prototype.forEach.call(document.querySelectorAll('#inf-plan .inf-marker'), function(mk){
        if(mk.offsetParent === null){ return; } // ausgeblendet (Filter)
        var x = (parseFloat(mk.style.left)||0)/100*c.width, y = (parseFloat(mk.style.top)||0)/100*c.height;
        var pinEl = mk.querySelector('.inf-marker-pin');
        var color = pinEl ? getComputedStyle(pinEl).backgroundColor : '#2563eb';
        var lblEl = mk.querySelector('.inf-marker-lbl'); var label = lblEl ? lblEl.textContent : '';
        var rr = Math.max(9, c.width/120);
        ctx.beginPath(); ctx.arc(x, y - rr*1.4, rr, 0, 2*Math.PI); ctx.fillStyle = color; ctx.fill();
        ctx.lineWidth = 2; ctx.strokeStyle = 'rgba(0,0,0,.45)'; ctx.stroke();
        ctx.beginPath(); ctx.moveTo(x - rr*0.6, y - rr*0.9); ctx.lineTo(x + rr*0.6, y - rr*0.9); ctx.lineTo(x, y); ctx.closePath(); ctx.fillStyle = color; ctx.fill();
        if(label){
            var fs = Math.max(12, c.width/90); ctx.font = 'bold ' + fs + 'px sans-serif'; ctx.textAlign = 'center';
            var w = ctx.measureText(label).width;
            ctx.fillStyle = 'rgba(255,255,255,.92)'; ctx.fillRect(x - w/2 - 4, y + 2, w + 8, fs + 6);
            ctx.fillStyle = '#111827'; ctx.fillText(label, x, y + fs + 2);
        }
    });
    var a = document.createElement('a');
    a.download = 'Lageplan_' + INF_LOC_ID + '.png';
    a.href = c.toDataURL('image/png'); a.click();
}

(function(){
    var plan = document.getElementById('inf-plan');
    if(!plan) return;
    var img = document.getElementById('inf-plan-img');
    var isAdmin = plan.getAttribute('data-admin') === '1';

    function pct(ev){
        var r = img.getBoundingClientRect();
        var cx = (ev.touches ? ev.touches[0].clientX : ev.clientX);
        var cy = (ev.touches ? ev.touches[0].clientY : ev.clientY);
        return { x: Math.max(0, Math.min(100, (cx - r.left) / r.width * 100)),
                 y: Math.max(0, Math.min(100, (cy - r.top) / r.height * 100)) };
    }

    // Klick auf leeren Plan → neuen Marker anlegen (nur Admin)
    if(isAdmin){
        img.addEventListener('click', function(ev){
            var p = pct(ev);
            var f = document.getElementById('inf-marker-form');
            f.action = '/infrastructure/locations/' + INF_LOC_ID + '/markers';
            document.getElementById('inf-marker-title').textContent = 'Marker setzen';
            document.getElementById('inf-mk-type').value = 'rack';
            infMkType('rack');
            document.getElementById('inf-mk-x').value = p.x.toFixed(2);
            document.getElementById('inf-mk-y').value = p.y.toFixed(2);
            document.getElementById('inf-mk-rack').value = '';
            document.getElementById('inf-mk-asset').value = '';
            document.getElementById('inf-mk-loc').value = '';
            document.getElementById('inf-mk-label').value = '';
            document.getElementById('inf-mk-conn-panel').value = '';
            document.getElementById('inf-mk-conn-panel-port').value = '';
            document.getElementById('inf-mk-conn-switch').value = '';
            document.getElementById('inf-mk-conn-switch-port').value = '';
            infConnPorts('panel', ''); infConnPorts('switch', '');
            var h = document.getElementById('inf-mk-conn-hint'); if(h){ h.style.display='none'; }
            if(window.ssSync){ ['inf-mk-type','inf-mk-rack','inf-mk-asset','inf-mk-loc','inf-mk-conn-panel','inf-mk-conn-switch'].forEach(function(i){ var el=document.getElementById(i); if(el) window.ssSync(el); }); }
            document.getElementById('inf-marker-modal').style.display='flex';
        });
    }

    // Marker: Klick öffnet das Ziel (alle) · Admin kann zusätzlich ziehen
    Array.prototype.forEach.call(plan.querySelectorAll('.inf-marker'), function(mk){
        var moved = false, dragging = false, start = null, id = mk.getAttribute('data-id');

        if(isAdmin){
            mk.addEventListener('pointerdown', function(ev){
                if(ev.target.closest('.inf-marker-del, .inf-marker-edit')) return;
                dragging = true; moved = false; start = {x:ev.clientX, y:ev.clientY};
                mk.setPointerCapture(ev.pointerId); mk.classList.add('inf-marker--drag');
                ev.preventDefault();
            });
            mk.addEventListener('pointermove', function(ev){
                if(!dragging) return;
                if(Math.abs(ev.clientX-start.x) > 3 || Math.abs(ev.clientY-start.y) > 3) moved = true;
                if(moved){ var p = pct(ev); mk.style.left = p.x+'%'; mk.style.top = p.y+'%'; }
            });
            mk.addEventListener('pointerup', function(ev){
                mk.classList.remove('inf-marker--drag');
                if(dragging && moved){
                    var p = pct(ev);
                    infPostForm('/infrastructure/markers/'+id+'/update', { x:p.x.toFixed(2), y:p.y.toFixed(2) });
                }
                dragging = false;
            });
        }

        mk.addEventListener('click', function(ev){
            if(ev.target.closest('.inf-marker-del, .inf-marker-edit')) return;
            if(moved){ ev.preventDefault(); return; } // war ein Drag
            var link = mk.getAttribute('data-link');
            if(link){ window.location.href = link; }
        });
    });
})();
</script>
<?php endif; ?>