defekter floorplan
This commit is contained in:
@@ -50,3 +50,6 @@ Wenn ein Skill genannt wird (z. B. `$skill-creator`) oder die Aufgabe exakt zu
|
||||
## Einschränkungen
|
||||
- Sandbox ist lesend; bitte selbst `AGENTS.md` anlegen.
|
||||
- Jegliche Ausgaben/Antworten sollten den Developer-Guidelines folgen (kurz, teamorientiert, klare nächste Schritte).
|
||||
|
||||
## Wichtig:
|
||||
- Nutze UTF-8 wenn nicht anders angegeben
|
||||
3
TODO.md
3
TODO.md
@@ -162,7 +162,7 @@ Hinweis: Die Eintraege sind direkt aus den Quelldateien aggregiert.
|
||||
|
||||
## app\modules\floor_infrastructure\list.php
|
||||
|
||||
- [ ] L143: <p><small>TODO: SVG-Editor mit Drag & Drop für diese Objekte erweitern (siehe Stockwerke-Modul).</small></p>
|
||||
- [ ] L143: <p><small>//TODO: SVG-Editor mit Drag & Drop für diese Objekte erweitern (siehe Stockwerke-Modul).</small></p>
|
||||
|
||||
## app\modules\floors\list.php
|
||||
|
||||
@@ -176,7 +176,6 @@ Hinweis: Die Eintraege sind direkt aus den Quelldateien aggregiert.
|
||||
|
||||
- [ ] L134: //TODO design schlecht, mach es hübscher
|
||||
- [ ] L208: //TODO style in css file
|
||||
- [ ] L406: // TODO: AJAX-Delete implementieren
|
||||
|
||||
## app\modules\racks\edit.php
|
||||
|
||||
|
||||
@@ -34,8 +34,7 @@ $validActions = ['list', 'edit', 'save', 'ports', 'delete'];
|
||||
|
||||
// Prüfen auf gültige Werte
|
||||
if (!in_array($module, $validModules)) {
|
||||
// TODO: Fehlerseite anzeigen, nutze renderClientError(...)
|
||||
die('Ungültiges Modul');
|
||||
renderClientError(400, 'Ungültiges Modul');
|
||||
}
|
||||
|
||||
if (!in_array($action, $validActions)) {
|
||||
|
||||
@@ -68,6 +68,66 @@ $connections = $sql->get(
|
||||
// =========================
|
||||
$devices = $sql->get("SELECT id, name FROM devices ORDER BY name", "", []);
|
||||
|
||||
$selectedDevice = null;
|
||||
$selectedDevicePorts = [];
|
||||
$selectedDeviceVlans = [];
|
||||
|
||||
if ($deviceId > 0) {
|
||||
$selectedDevice = $sql->single(
|
||||
"SELECT d.id, d.name, dt.name AS type_name
|
||||
FROM devices d
|
||||
LEFT JOIN device_types dt ON d.device_type_id = dt.id
|
||||
WHERE d.id = ?",
|
||||
"i",
|
||||
[$deviceId]
|
||||
);
|
||||
|
||||
if ($selectedDevice) {
|
||||
$selectedDevice['port_count'] = (int)($sql->single(
|
||||
"SELECT COUNT(*) AS cnt FROM device_ports WHERE device_id = ?",
|
||||
"i",
|
||||
[$deviceId]
|
||||
)['cnt'] ?? 0);
|
||||
|
||||
$selectedDevice['connection_count'] = (int)($sql->single(
|
||||
"SELECT COUNT(DISTINCT c.id) AS cnt
|
||||
FROM connections c
|
||||
LEFT JOIN device_ports dpt1 ON c.port_a_type = 'device' AND c.port_a_id = dpt1.id
|
||||
LEFT JOIN device_ports dpt2 ON c.port_b_type = 'device' AND c.port_b_id = dpt2.id
|
||||
WHERE dpt1.device_id = ? OR dpt2.device_id = ?",
|
||||
"ii",
|
||||
[$deviceId, $deviceId]
|
||||
)['cnt'] ?? 0);
|
||||
|
||||
$selectedDevicePorts = $sql->get(
|
||||
"SELECT name, vlan_config
|
||||
FROM device_ports
|
||||
WHERE device_id = ?
|
||||
ORDER BY id
|
||||
LIMIT 12",
|
||||
"i",
|
||||
[$deviceId]
|
||||
);
|
||||
|
||||
foreach ($selectedDevicePorts as $port) {
|
||||
if (empty($port['vlan_config'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$vlans = json_decode($port['vlan_config'], true);
|
||||
foreach ((array)$vlans as $vlan) {
|
||||
$vlan = trim((string)$vlan);
|
||||
if ($vlan !== '') {
|
||||
$selectedDeviceVlans[$vlan] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$selectedDeviceVlans = array_keys($selectedDeviceVlans);
|
||||
natcasesort($selectedDeviceVlans);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="connections-container">
|
||||
@@ -195,14 +255,31 @@ $devices = $sql->get("SELECT id, name FROM devices ORDER BY name", "", []);
|
||||
========================= -->
|
||||
|
||||
<aside class="sidebar">
|
||||
<!-- TODO: Details zum ausgewählten Gerät anzeigen -->
|
||||
<!--
|
||||
- Gerätename
|
||||
- Gerätetyp
|
||||
- Ports
|
||||
- VLANs
|
||||
- Verbindungen
|
||||
-->
|
||||
<?php if ($selectedDevice): ?>
|
||||
<h3>Ausgewähltes Gerät</h3>
|
||||
<p><strong><?php echo htmlspecialchars($selectedDevice['name']); ?></strong></p>
|
||||
<p>Typ: <?php echo htmlspecialchars($selectedDevice['type_name'] ?? '—'); ?></p>
|
||||
<p>Ports: <?php echo (int)$selectedDevice['port_count']; ?></p>
|
||||
<p>Verbindungen: <?php echo (int)$selectedDevice['connection_count']; ?></p>
|
||||
<p>
|
||||
VLANs:
|
||||
<?php if (!empty($selectedDeviceVlans)): ?>
|
||||
<?php echo htmlspecialchars(implode(', ', $selectedDeviceVlans)); ?>
|
||||
<?php else: ?>
|
||||
—
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php if (!empty($selectedDevicePorts)): ?>
|
||||
<h4>Ports (max. 12)</h4>
|
||||
<ul>
|
||||
<?php foreach ($selectedDevicePorts as $port): ?>
|
||||
<li><?php echo htmlspecialchars($port['name']); ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<p><em>Bitte ein Gerät im Filter auswählen.</em></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- TODO: Verbindung bearbeiten / löschen -->
|
||||
</aside>
|
||||
|
||||
@@ -94,6 +94,7 @@ if ($type === 'patchpanel') {
|
||||
|
||||
$defaultPanelSize = ['width' => 140, 'height' => 40];
|
||||
$defaultOutletSize = 32;
|
||||
$showPanelPlacementFields = $type === 'patchpanel' && $selectedFloorId > 0;
|
||||
|
||||
if ($type === 'patchpanel') {
|
||||
$panel['width'] = $panel['width'] ?? $defaultPanelSize['width'];
|
||||
@@ -160,26 +161,26 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<div id="panel-placement-fields" class="form-grid" <?php echo $showPanelPlacementFields ? '' : 'hidden'; ?>>
|
||||
<div class="form-group">
|
||||
<label>X</label>
|
||||
<input type="number" name="pos_x" value="<?php echo $panel['pos_x'] ?? 0; ?>" required>
|
||||
<input type="number" name="pos_x" value="<?php echo $panel['pos_x'] ?? 0; ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Y</label>
|
||||
<input type="number" name="pos_y" value="<?php echo $panel['pos_y'] ?? 0; ?>" required>
|
||||
<input type="number" name="pos_y" value="<?php echo $panel['pos_y'] ?? 0; ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Breite</label>
|
||||
<input type="number" name="width" value="<?php echo $panel['width']; ?>" required readonly title="Breite wird automatisch nach Standardwerten vorgegeben.">
|
||||
<input type="number" name="width" value="<?php echo $panel['width']; ?>" readonly title="Breite wird automatisch nach Standardwerten vorgegeben.">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Höhe</label>
|
||||
<input type="number" name="height" value="<?php echo $panel['height']; ?>" required readonly title="Höhe wird automatisch nach Standardwerten vorgegeben.">
|
||||
<input type="number" name="height" value="<?php echo $panel['height']; ?>" readonly title="Höhe wird automatisch nach Standardwerten vorgegeben.">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div id="panel-floor-plan-group" class="form-group" <?php echo $showPanelPlacementFields ? '' : 'hidden'; ?>>
|
||||
<label>Stockwerkskarte</label>
|
||||
<div class="floor-plan-block">
|
||||
<div id="floor-plan-canvas" class="floor-plan-canvas"
|
||||
@@ -202,6 +203,10 @@ $markerHeight = $type === 'patchpanel' ? $panel['height'] : $defaultOutletSize;
|
||||
<input type="number" name="port_count" value="<?php echo $panel['port_count'] ?? 0; ?>" min="0">
|
||||
</div>
|
||||
|
||||
<p id="panel-floor-missing-hint" class="info" <?php echo $showPanelPlacementFields ? 'hidden' : ''; ?>>
|
||||
Position, Groesse und Stockwerkskarte werden erst angezeigt, sobald ein Stockwerk ausgewaehlt ist.
|
||||
</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Kommentar</label>
|
||||
<textarea name="comment"><?php echo htmlspecialchars($panel['comment'] ?? ''); ?></textarea>
|
||||
@@ -476,6 +481,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const panelFloorSelect = document.getElementById('panel-floor-select');
|
||||
const outletRoomSelect = document.getElementById('outlet-room-select');
|
||||
const floorPlanSvg = document.getElementById('floor-plan-svg');
|
||||
const panelPlacementFields = document.getElementById('panel-placement-fields');
|
||||
const panelFloorPlanGroup = document.getElementById('panel-floor-plan-group');
|
||||
const panelFloorMissingHint = document.getElementById('panel-floor-missing-hint');
|
||||
|
||||
const buildingOptions = panelBuildingSelect ? Array.from(panelBuildingSelect.options).filter((option) => option.value !== '') : [];
|
||||
const floorOptions = panelFloorSelect ? Array.from(panelFloorSelect.options).filter((option) => option.value !== '') : [];
|
||||
@@ -491,8 +499,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (svgUrl) {
|
||||
floorPlanSvg.src = svgUrl;
|
||||
floorPlanSvg.hidden = false;
|
||||
} else {
|
||||
floorPlanSvg.removeAttribute('src');
|
||||
floorPlanSvg.hidden = true;
|
||||
}
|
||||
};
|
||||
|
||||
if (floorPlanSvg) {
|
||||
floorPlanSvg.addEventListener('error', () => {
|
||||
floorPlanSvg.removeAttribute('src');
|
||||
floorPlanSvg.hidden = true;
|
||||
});
|
||||
}
|
||||
|
||||
const updatePanelPlacementVisibility = () => {
|
||||
if (!panelFloorSelect || !panelPlacementFields || !panelFloorPlanGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasFloor = !!panelFloorSelect.value;
|
||||
panelPlacementFields.hidden = !hasFloor;
|
||||
panelFloorPlanGroup.hidden = !hasFloor;
|
||||
if (panelFloorMissingHint) {
|
||||
panelFloorMissingHint.hidden = hasFloor;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -517,6 +547,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
panelFloorSelect.value = firstMatch;
|
||||
}
|
||||
|
||||
updatePanelPlacementVisibility();
|
||||
updateFloorPlanImage();
|
||||
};
|
||||
|
||||
@@ -557,6 +588,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (panelFloorSelect) {
|
||||
panelFloorSelect.addEventListener('change', () => {
|
||||
updatePanelPlacementVisibility();
|
||||
updateFloorPlanImage();
|
||||
});
|
||||
}
|
||||
@@ -573,5 +605,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
} else {
|
||||
updateFloorPlanImage();
|
||||
}
|
||||
|
||||
updatePanelPlacementVisibility();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
/**
|
||||
* app/modules/floor_infrastructure/list.php
|
||||
*
|
||||
* Übersicht über Patchpanels und Netzwerkbuchsen auf Stockwerken
|
||||
* Uebersicht ueber Patchpanels und Netzwerkbuchsen auf Stockwerken
|
||||
*/
|
||||
|
||||
$floorId = (int)($_GET['floor_id'] ?? 0);
|
||||
|
||||
$floors = $sql->get("SELECT id, name FROM floors ORDER BY name", "", []);
|
||||
$floors = $sql->get("SELECT id, name, svg_path FROM floors ORDER BY name", "", []);
|
||||
|
||||
$where = '';
|
||||
$types = '';
|
||||
@@ -30,7 +30,7 @@ $patchPanels = $sql->get(
|
||||
);
|
||||
|
||||
$networkOutlets = $sql->get(
|
||||
"SELECT o.*, r.name AS room_name, f.name AS floor_name
|
||||
"SELECT o.*, r.name AS room_name, f.name AS floor_name, f.id AS floor_id
|
||||
FROM network_outlets o
|
||||
LEFT JOIN rooms r ON r.id = o.room_id
|
||||
LEFT JOIN floors f ON f.id = r.floor_id
|
||||
@@ -38,6 +38,62 @@ $networkOutlets = $sql->get(
|
||||
"",
|
||||
[]
|
||||
);
|
||||
|
||||
$floorMap = [];
|
||||
foreach ($floors as $floor) {
|
||||
$id = (int)$floor['id'];
|
||||
$svgPath = trim((string)($floor['svg_path'] ?? ''));
|
||||
$floorMap[$id] = [
|
||||
'id' => $id,
|
||||
'name' => (string)($floor['name'] ?? ''),
|
||||
'svg_url' => $svgPath !== '' ? '/' . ltrim($svgPath, '/\\') : ''
|
||||
];
|
||||
}
|
||||
|
||||
$editorFloorId = $floorId;
|
||||
if ($editorFloorId <= 0) {
|
||||
foreach ($floorMap as $candidate) {
|
||||
if ($candidate['svg_url'] !== '') {
|
||||
$editorFloorId = (int)$candidate['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$editorFloor = $floorMap[$editorFloorId] ?? null;
|
||||
$editorPatchPanels = [];
|
||||
$editorOutlets = [];
|
||||
|
||||
if ($editorFloorId > 0) {
|
||||
foreach ($patchPanels as $panel) {
|
||||
if ((int)$panel['floor_id'] === $editorFloorId) {
|
||||
$editorPatchPanels[] = [
|
||||
'id' => (int)$panel['id'],
|
||||
'name' => (string)$panel['name'],
|
||||
'floor_id' => (int)$panel['floor_id'],
|
||||
'x' => (int)$panel['pos_x'],
|
||||
'y' => (int)$panel['pos_y'],
|
||||
'width' => max(1, (int)$panel['width']),
|
||||
'height' => max(1, (int)$panel['height']),
|
||||
'port_count' => (int)$panel['port_count'],
|
||||
'comment' => (string)($panel['comment'] ?? '')
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($networkOutlets as $outlet) {
|
||||
if ((int)$outlet['floor_id'] === $editorFloorId) {
|
||||
$editorOutlets[] = [
|
||||
'id' => (int)$outlet['id'],
|
||||
'name' => (string)$outlet['name'],
|
||||
'room_id' => (int)$outlet['room_id'],
|
||||
'x' => (int)$outlet['x'],
|
||||
'y' => (int)$outlet['y'],
|
||||
'comment' => (string)($outlet['comment'] ?? '')
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="floor-infra">
|
||||
@@ -45,10 +101,10 @@ $networkOutlets = $sql->get(
|
||||
|
||||
<div class="toolbar">
|
||||
<a href="?module=floor_infrastructure&action=edit&type=patchpanel" class="button button-primary">
|
||||
+ Patchpanel hinzufügen
|
||||
+ Patchpanel hinzufuegen
|
||||
</a>
|
||||
<a href="?module=floor_infrastructure&action=edit&type=outlet" class="button">
|
||||
+ Wandbuchse hinzufügen
|
||||
+ Wandbuchse hinzufuegen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +122,7 @@ $networkOutlets = $sql->get(
|
||||
</select>
|
||||
|
||||
<button class="button">Filter</button>
|
||||
<a href="?module=floor_infrastructure&action=list" class="button">Zurücksetzen</a>
|
||||
<a href="?module=floor_infrastructure&action=list" class="button">Zuruecksetzen</a>
|
||||
</form>
|
||||
|
||||
<section class="infra-section">
|
||||
@@ -78,7 +134,7 @@ $networkOutlets = $sql->get(
|
||||
<th>Name</th>
|
||||
<th>Stockwerk</th>
|
||||
<th>Position</th>
|
||||
<th>Größe</th>
|
||||
<th>Groesse</th>
|
||||
<th>Ports</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
@@ -87,9 +143,9 @@ $networkOutlets = $sql->get(
|
||||
<?php foreach ($patchPanels as $panel): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($panel['name']); ?></td>
|
||||
<td><?php echo htmlspecialchars($panel['floor_name'] ?? '—'); ?></td>
|
||||
<td><?php echo $panel['pos_x'] . ' × ' . $panel['pos_y']; ?></td>
|
||||
<td><?php echo $panel['width'] . ' × ' . $panel['height']; ?></td>
|
||||
<td><?php echo htmlspecialchars($panel['floor_name'] ?? '-'); ?></td>
|
||||
<td><?php echo $panel['pos_x'] . ' x ' . $panel['pos_y']; ?></td>
|
||||
<td><?php echo $panel['width'] . ' x ' . $panel['height']; ?></td>
|
||||
<td><?php echo $panel['port_count']; ?></td>
|
||||
<td class="actions">
|
||||
<a href="?module=floor_infrastructure&action=edit&type=patchpanel&id=<?php echo $panel['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||
@@ -121,9 +177,9 @@ $networkOutlets = $sql->get(
|
||||
<?php foreach ($networkOutlets as $outlet): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($outlet['name']); ?></td>
|
||||
<td><?php echo htmlspecialchars($outlet['floor_name'] ?? '—'); ?></td>
|
||||
<td><?php echo htmlspecialchars($outlet['room_name'] ?? '—'); ?></td>
|
||||
<td><?php echo $outlet['x'] . ' × ' . $outlet['y']; ?></td>
|
||||
<td><?php echo htmlspecialchars($outlet['floor_name'] ?? '-'); ?></td>
|
||||
<td><?php echo htmlspecialchars($outlet['room_name'] ?? '-'); ?></td>
|
||||
<td><?php echo $outlet['x'] . ' x ' . $outlet['y']; ?></td>
|
||||
<td><?php echo htmlspecialchars($outlet['comment']); ?></td>
|
||||
<td class="actions">
|
||||
<a href="?module=floor_infrastructure&action=edit&type=outlet&id=<?php echo $outlet['id']; ?>" class="button button-small">Bearbeiten</a>
|
||||
@@ -139,8 +195,20 @@ $networkOutlets = $sql->get(
|
||||
|
||||
<section class="infra-plan">
|
||||
<h2>Stockwerksplan-Verortung</h2>
|
||||
<p>Die eingetragenen Patchpanels und Wandbuchsen erscheinen später als feste Objekte auf dem Stockwerks-SVG. Die Polygon-Positionen werden momentan noch durch numerische X/Y-Werte gesteuert.</p>
|
||||
<p><small>TODO: SVG-Editor mit Drag & Drop für diese Objekte erweitern (siehe Stockwerke-Modul).</small></p>
|
||||
<?php if (!$editorFloor): ?>
|
||||
<p class="empty-state">Kein Stockwerk fuer die Planansicht verfuegbar.</p>
|
||||
<?php elseif (($editorFloor['svg_url'] ?? '') === ''): ?>
|
||||
<p class="empty-state">Das gewaehlte Stockwerk hat noch keinen SVG-Plan hinterlegt.</p>
|
||||
<?php else: ?>
|
||||
<p>Drag & Drop ist aktiv fuer <strong><?php echo htmlspecialchars($editorFloor['name']); ?></strong>. Aenderungen werden direkt gespeichert.</p>
|
||||
<div id="infra-floor-canvas"
|
||||
class="infra-floor-canvas"
|
||||
data-save-url="?module=floor_infrastructure&action=save"
|
||||
data-floor-id="<?php echo (int)$editorFloor['id']; ?>">
|
||||
<img src="<?php echo htmlspecialchars($editorFloor['svg_url']); ?>" class="infra-floor-svg" alt="Stockwerksplan">
|
||||
</div>
|
||||
<p class="floor-plan-hint">Patchpanels: blau, Wandbuchsen: gruen. Ziehen und loslassen zum Speichern.</p>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -184,6 +252,54 @@ $networkOutlets = $sql->get(
|
||||
border: 1px dashed #ccc;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.infra-floor-canvas {
|
||||
position: relative;
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
min-height: 420px;
|
||||
border: 1px solid #d4d4d4;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.infra-floor-svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.infra-marker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
cursor: move;
|
||||
z-index: 2;
|
||||
}
|
||||
.infra-marker.patchpanel {
|
||||
background: rgba(13, 110, 253, 0.25);
|
||||
border: 2px solid #0d6efd;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.infra-marker.outlet {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(25, 135, 84, 0.25);
|
||||
border: 2px solid #198754;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.infra-marker.is-saving {
|
||||
opacity: 0.65;
|
||||
}
|
||||
.floor-plan-hint {
|
||||
margin: 8px 0 0;
|
||||
font-size: 0.85em;
|
||||
color: #444;
|
||||
}
|
||||
.empty-state {
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
@@ -194,3 +310,125 @@ $networkOutlets = $sql->get(
|
||||
margin-right: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<?php if ($editorFloor && ($editorFloor['svg_url'] ?? '') !== ''): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const canvas = document.getElementById('infra-floor-canvas');
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
const saveUrl = canvas.dataset.saveUrl;
|
||||
const floorId = Number(canvas.dataset.floorId || 0);
|
||||
const outletSize = 32;
|
||||
const patchPanels = <?php echo json_encode($editorPatchPanels, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
|
||||
const outlets = <?php echo json_encode($editorOutlets, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
|
||||
|
||||
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
||||
|
||||
const savePosition = async (entry, type, marker) => {
|
||||
const form = new FormData();
|
||||
|
||||
if (type === 'patchpanel') {
|
||||
form.append('type', 'patchpanel');
|
||||
form.append('id', String(entry.id));
|
||||
form.append('name', entry.name || '');
|
||||
form.append('floor_id', String(floorId));
|
||||
form.append('pos_x', String(entry.x));
|
||||
form.append('pos_y', String(entry.y));
|
||||
form.append('width', String(entry.width));
|
||||
form.append('height', String(entry.height));
|
||||
form.append('port_count', String(entry.port_count || 0));
|
||||
form.append('comment', entry.comment || '');
|
||||
} else {
|
||||
form.append('type', 'outlet');
|
||||
form.append('id', String(entry.id));
|
||||
form.append('name', entry.name || '');
|
||||
form.append('room_id', String(entry.room_id || 0));
|
||||
form.append('x', String(entry.x));
|
||||
form.append('y', String(entry.y));
|
||||
form.append('comment', entry.comment || '');
|
||||
}
|
||||
|
||||
marker.classList.add('is-saving');
|
||||
try {
|
||||
await fetch(saveUrl, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
} finally {
|
||||
marker.classList.remove('is-saving');
|
||||
}
|
||||
};
|
||||
|
||||
const bindDrag = (marker, entry, type) => {
|
||||
let dragging = false;
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
|
||||
marker.addEventListener('pointerdown', (event) => {
|
||||
dragging = true;
|
||||
const rect = marker.getBoundingClientRect();
|
||||
offsetX = event.clientX - rect.left;
|
||||
offsetY = event.clientY - rect.top;
|
||||
marker.setPointerCapture(event.pointerId);
|
||||
});
|
||||
|
||||
marker.addEventListener('pointermove', (event) => {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const width = type === 'patchpanel' ? entry.width : outletSize;
|
||||
const height = type === 'patchpanel' ? entry.height : outletSize;
|
||||
const maxX = Math.max(0, rect.width - width);
|
||||
const maxY = Math.max(0, rect.height - height);
|
||||
|
||||
entry.x = Math.round(clamp(event.clientX - rect.left - offsetX, 0, maxX));
|
||||
entry.y = Math.round(clamp(event.clientY - rect.top - offsetY, 0, maxY));
|
||||
marker.style.left = `${entry.x}px`;
|
||||
marker.style.top = `${entry.y}px`;
|
||||
});
|
||||
|
||||
const stopDrag = (event) => {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
dragging = false;
|
||||
if (marker.hasPointerCapture(event.pointerId)) {
|
||||
marker.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
savePosition(entry, type, marker);
|
||||
};
|
||||
|
||||
marker.addEventListener('pointerup', stopDrag);
|
||||
marker.addEventListener('pointercancel', stopDrag);
|
||||
};
|
||||
|
||||
const createMarker = (entry, type) => {
|
||||
const marker = document.createElement('div');
|
||||
marker.className = `infra-marker ${type}`;
|
||||
marker.dataset.id = String(entry.id);
|
||||
marker.dataset.type = type;
|
||||
marker.title = entry.name || '';
|
||||
|
||||
if (type === 'patchpanel') {
|
||||
marker.style.width = `${entry.width}px`;
|
||||
marker.style.height = `${entry.height}px`;
|
||||
}
|
||||
|
||||
marker.style.left = `${entry.x}px`;
|
||||
marker.style.top = `${entry.y}px`;
|
||||
canvas.appendChild(marker);
|
||||
|
||||
bindDrag(marker, entry, type);
|
||||
};
|
||||
|
||||
patchPanels.forEach((entry) => createMarker(entry, 'patchpanel'));
|
||||
outlets.forEach((entry) => createMarker(entry, 'outlet'));
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -18,6 +18,13 @@ if ($type === 'patchpanel') {
|
||||
$portCount = (int)($_POST['port_count'] ?? 0);
|
||||
$comment = trim($_POST['comment'] ?? '');
|
||||
|
||||
if ($width <= 0) {
|
||||
$width = 140;
|
||||
}
|
||||
if ($height <= 0) {
|
||||
$height = 40;
|
||||
}
|
||||
|
||||
if ($id > 0) {
|
||||
$sql->set(
|
||||
"UPDATE floor_patchpanels SET name = ?, floor_id = ?, pos_x = ?, pos_y = ?, width = ?, height = ?, port_count = ?, comment = ? WHERE id = ?",
|
||||
|
||||
55
app/modules/locations/delete.php
Normal file
55
app/modules/locations/delete.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* app/modules/locations/delete.php
|
||||
*
|
||||
* Loescht einen Standort.
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$locationId = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
|
||||
|
||||
if ($locationId <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Ungueltige Standort-ID'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$location = $sql->single(
|
||||
"SELECT id, name FROM locations WHERE id = ?",
|
||||
"i",
|
||||
[$locationId]
|
||||
);
|
||||
|
||||
if (!$location) {
|
||||
http_response_code(404);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Standort nicht gefunden'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$deleted = $sql->set(
|
||||
"DELETE FROM locations WHERE id = ?",
|
||||
"i",
|
||||
[$locationId]
|
||||
);
|
||||
|
||||
if ($deleted <= 0) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Standort konnte nicht geloescht werden'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Standort geloescht'
|
||||
]);
|
||||
exit;
|
||||
@@ -403,8 +403,23 @@ foreach ($floors as $floor) {
|
||||
<script>
|
||||
function confirmDelete(id) {
|
||||
if (confirm('Diesen Standort wirklich löschen?')) {
|
||||
// TODO: AJAX-Delete implementieren
|
||||
alert('Löschen noch nicht implementiert');
|
||||
fetch('?module=locations&action=delete&id=' + encodeURIComponent(id), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data && data.success) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
alert((data && data.message) ? data.message : 'Löschen fehlgeschlagen');
|
||||
})
|
||||
.catch(() => {
|
||||
alert('Löschen fehlgeschlagen');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,3 +435,4 @@ function confirmFloorDelete(id) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user