Quelltext des aktuellen Standes
<?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'] ?>)">×</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}%', ['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">›</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'">×</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'">×</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'">×</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(' ', (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; ?>