div TODOs
This commit is contained in:
@@ -1,135 +1,110 @@
|
||||
/**
|
||||
* app/assets/js/app.js
|
||||
*
|
||||
* Zentrale JS-Datei für die Webanwendung
|
||||
* - Initialisiert alle Module
|
||||
* - SVG-Editor, Netzwerk-Ansicht, Drag & Drop, Floorplan
|
||||
* - Event-Handler, globale Variablen
|
||||
*/
|
||||
|
||||
// =========================
|
||||
// Global Variables / Config
|
||||
// =========================
|
||||
|
||||
window.APP = {
|
||||
deviceTypes: [], // TODO: alle Gerätetypen laden
|
||||
devices: [], // TODO: alle Geräte laden
|
||||
racks: [], // TODO: alle Racks laden
|
||||
floors: [], // TODO: alle Floors laden
|
||||
connections: [], // TODO: alle Verbindungen laden
|
||||
state: {
|
||||
deviceTypes: [],
|
||||
devices: [],
|
||||
racks: [],
|
||||
floors: [],
|
||||
connections: [],
|
||||
},
|
||||
capabilities: {
|
||||
hasGlobalDataApi: false,
|
||||
}
|
||||
};
|
||||
|
||||
// =========================
|
||||
// Init Functions
|
||||
// =========================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
console.log('App initialized');
|
||||
|
||||
// =========================
|
||||
// SVG-Port-Editor initialisieren
|
||||
// =========================
|
||||
// TODO: import / init svg-editor.js
|
||||
// if (window.SVGEditor) window.SVGEditor.init();
|
||||
|
||||
// =========================
|
||||
// Netzwerk-Ansicht initialisieren
|
||||
// =========================
|
||||
// TODO: import / init network-view.js
|
||||
// if (window.NetworkView) window.NetworkView.init();
|
||||
|
||||
// =========================
|
||||
// Drag & Drop für Floors / Racks / Devices
|
||||
// =========================
|
||||
// TODO: init drag & drop logic
|
||||
|
||||
// =========================
|
||||
// Event-Handler für Buttons / Forms
|
||||
// =========================
|
||||
initViewModules();
|
||||
initEventHandlers();
|
||||
});
|
||||
|
||||
// =========================
|
||||
// Event Handler Setup
|
||||
// =========================
|
||||
function initViewModules() {
|
||||
if (typeof window.Dashboard?.init === 'function') {
|
||||
window.Dashboard.init();
|
||||
}
|
||||
|
||||
// Both modules are loaded via script tags in header.php.
|
||||
// They are self-initializing and only run when expected DOM nodes exist.
|
||||
window.dispatchEvent(new CustomEvent('app:modules-initialized'));
|
||||
}
|
||||
|
||||
function initEventHandlers() {
|
||||
|
||||
// TODO: Save-Button Device-Type
|
||||
const saveDeviceTypeBtn = document.querySelector('#save-device-type');
|
||||
if (saveDeviceTypeBtn) {
|
||||
saveDeviceTypeBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
// TODO: Save Device-Type via AJAX
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Save-Button Device
|
||||
const saveDeviceBtn = document.querySelector('#save-device');
|
||||
if (saveDeviceBtn) {
|
||||
saveDeviceBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
// TODO: Save Device via AJAX
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Save-Button Floor
|
||||
const saveFloorBtn = document.querySelector('#save-floor');
|
||||
if (saveFloorBtn) {
|
||||
saveFloorBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
// TODO: Save Floor via AJAX
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Save-Button Rack
|
||||
const saveRackBtn = document.querySelector('#save-rack');
|
||||
if (saveRackBtn) {
|
||||
saveRackBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
// TODO: Save Rack via AJAX
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Weitere Event-Handler (Import, Export, Filter)
|
||||
bindFormSubmitButton('#save-device-type', 'form[action*="module=device_types"][action*="save"]');
|
||||
bindFormSubmitButton('#save-device', 'form[action*="module=devices"][action*="save"]');
|
||||
bindFormSubmitButton('#save-floor', 'form[action*="module=floors"][action*="save"]');
|
||||
bindFormSubmitButton('#save-rack', 'form[action*="module=racks"][action*="save"]');
|
||||
|
||||
document.querySelectorAll('[data-confirm-delete]').forEach((btn) => {
|
||||
btn.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const message = btn.getAttribute('data-confirm-message') || 'Aktion ausführen?';
|
||||
const message = btn.getAttribute('data-confirm-message') || 'Aktion ausfuehren?';
|
||||
if (confirm(message)) {
|
||||
alert(btn.getAttribute('data-confirm-feedback') || 'Diese Funktion ist noch nicht verfügbar.');
|
||||
const href = btn.getAttribute('href') || btn.dataset.href;
|
||||
if (href) {
|
||||
window.location.href = href;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-filter-submit]').forEach((el) => {
|
||||
el.addEventListener('change', () => {
|
||||
const form = el.closest('form');
|
||||
if (form) {
|
||||
form.requestSubmit();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Utility Functions
|
||||
// =========================
|
||||
function bindFormSubmitButton(buttonSelector, formSelector) {
|
||||
const button = document.querySelector(buttonSelector);
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX Request Helper
|
||||
* @param {string} url
|
||||
* @param {object} data
|
||||
* @param {function} callback
|
||||
*/
|
||||
function ajaxPost(url, data, callback) {
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const form = button.closest('form') || document.querySelector(formSelector);
|
||||
if (form) {
|
||||
form.requestSubmit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ajaxPost(url, data, callback, onError) {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
|
||||
xhr.onload = function() {
|
||||
xhr.onload = function onLoad() {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
callback(JSON.parse(xhr.responseText));
|
||||
} else {
|
||||
console.error('AJAX Error:', xhr.statusText);
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(xhr.responseText);
|
||||
} catch (error) {
|
||||
if (typeof onError === 'function') {
|
||||
onError(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
callback(parsed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof onError === 'function') {
|
||||
onError(new Error('AJAX error: ' + xhr.status));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function onXhrError() {
|
||||
if (typeof onError === 'function') {
|
||||
onError(new Error('Netzwerkfehler'));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
// TODO: weitere Utility-Funktionen (DOM-Helper, SVG-Helper, etc.)
|
||||
|
||||
// Dashboard initialisieren
|
||||
if (window.Dashboard) window.Dashboard.init();
|
||||
window.APP.ajaxPost = ajaxPost;
|
||||
|
||||
@@ -1,99 +1,35 @@
|
||||
/**
|
||||
* app/assets/js/dashboard.js
|
||||
*
|
||||
* Dashboard-Modul
|
||||
* - Zentrale Übersicht aller Grundfunktionen
|
||||
* - Einstiegspunkt für das Tool
|
||||
* - Kann später Status, Warnungen, Statistiken anzeigen
|
||||
*/
|
||||
|
||||
window.Dashboard = (function () {
|
||||
|
||||
// =========================
|
||||
// Interne Daten
|
||||
// =========================
|
||||
|
||||
const modules = [
|
||||
{
|
||||
id: 'device_types',
|
||||
label: 'Gerätetypen',
|
||||
description: 'Gerätetypen, Port-Definitionen, Module',
|
||||
url: '/app/device_types/list.php',
|
||||
icon: '🔌'
|
||||
},
|
||||
{
|
||||
id: 'devices',
|
||||
label: 'Geräte',
|
||||
description: 'Physische Geräte in Racks und Räumen',
|
||||
url: '/app/devices/list.php',
|
||||
icon: '🖥️'
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
label: 'Verbindungen',
|
||||
description: 'Kabel, Ports, VLANs, Protokolle',
|
||||
url: '/app/connections/list.php',
|
||||
icon: '🧵'
|
||||
},
|
||||
{
|
||||
id: 'floors',
|
||||
label: 'Standorte & Stockwerke',
|
||||
description: 'Gebäude, Etagen, Räume, Dosen',
|
||||
url: '/app/floors/list.php',
|
||||
icon: '🏢'
|
||||
},
|
||||
{
|
||||
id: 'racks',
|
||||
label: 'Serverschränke',
|
||||
description: 'Racks, Positionierung, Höheneinheiten',
|
||||
url: '/app/racks/list.php',
|
||||
icon: '🗄️'
|
||||
},
|
||||
{
|
||||
id: 'network_view',
|
||||
label: 'Netzwerk-Ansicht',
|
||||
description: 'Grafische Netzwerkdarstellung',
|
||||
url: '/network.php',
|
||||
icon: '🌐'
|
||||
},
|
||||
{
|
||||
id: 'svg_editor',
|
||||
label: 'SVG-Port-Editor',
|
||||
description: 'Ports auf Gerätetypen definieren',
|
||||
url: '/svg-editor.php',
|
||||
icon: '✏️'
|
||||
}
|
||||
{ id: 'device_types', label: 'Geraetetypen', description: 'Geraetetypen und Port-Definitionen', url: '?module=device_types&action=list', icon: 'DT' },
|
||||
{ id: 'devices', label: 'Geraete', description: 'Physische Geraete in Racks und Raeumen', url: '?module=devices&action=list', icon: 'DV' },
|
||||
{ id: 'connections', label: 'Verbindungen', description: 'Kabel, Ports und VLANs', url: '?module=connections&action=list', icon: 'CN' },
|
||||
{ id: 'floors', label: 'Stockwerke', description: 'Standorte, Gebaeude und Etagen', url: '?module=floors&action=list', icon: 'FL' },
|
||||
{ id: 'racks', label: 'Racks', description: 'Racks und Positionierung', url: '?module=racks&action=list', icon: 'RK' },
|
||||
{ id: 'infra', label: 'Infrastruktur', description: 'Patchpanels und Wandbuchsen', url: '?module=floor_infrastructure&action=list', icon: 'IF' }
|
||||
];
|
||||
|
||||
// =========================
|
||||
// Public API
|
||||
// =========================
|
||||
|
||||
function init() {
|
||||
console.log('Dashboard initialized');
|
||||
const container = document.querySelector('#dashboard-modules');
|
||||
if (container) {
|
||||
renderModules(container);
|
||||
}
|
||||
|
||||
// TODO: Dashboard-Container ermitteln
|
||||
// const container = document.querySelector('#dashboard');
|
||||
|
||||
// TODO: Module rendern
|
||||
// renderModules(container);
|
||||
|
||||
// TODO: Optional: Status-Daten laden (Counts, Warnings)
|
||||
loadStats();
|
||||
showWarnings();
|
||||
renderRecentChanges();
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Render Functions
|
||||
// =========================
|
||||
|
||||
function renderModules(container) {
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
modules.forEach(module => {
|
||||
const el = document.createElement('div');
|
||||
modules.forEach((module) => {
|
||||
const el = document.createElement('a');
|
||||
el.className = 'dashboard-tile';
|
||||
|
||||
el.href = module.url;
|
||||
el.innerHTML = `
|
||||
<div class="dashboard-icon">${module.icon}</div>
|
||||
<div class="dashboard-content">
|
||||
@@ -101,30 +37,54 @@ window.Dashboard = (function () {
|
||||
<p>${module.description}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.addEventListener('click', () => {
|
||||
window.location.href = module.url;
|
||||
});
|
||||
|
||||
container.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Optional Erweiterungen
|
||||
// =========================
|
||||
function loadStats() {
|
||||
const stats = {
|
||||
devices: countRows('.device-list tbody tr'),
|
||||
connections: countRows('.connection-list tbody tr'),
|
||||
outlets: countRows('.infra-table tbody tr')
|
||||
};
|
||||
|
||||
// TODO: loadStats() → Anzahl Geräte, offene Ports, unverbundene Dosen
|
||||
// TODO: showWarnings() → unverbundene Ports, VLAN-Konflikte
|
||||
// TODO: RecentChanges() → letzte Änderungen
|
||||
const target = document.querySelector('[data-dashboard-stats]');
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Expose Public Methods
|
||||
// =========================
|
||||
target.textContent = `Geraete: ${stats.devices} | Verbindungen: ${stats.connections} | Infrastruktur-Eintraege: ${stats.outlets}`;
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
// renderModules // optional öffentlich machen
|
||||
};
|
||||
function showWarnings() {
|
||||
const target = document.querySelector('[data-dashboard-warnings]');
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const warnings = [];
|
||||
if (countRows('.device-list tbody tr') === 0) {
|
||||
warnings.push('Noch keine Geraete vorhanden');
|
||||
}
|
||||
if (countRows('.connection-list tbody tr') === 0) {
|
||||
warnings.push('Noch keine Verbindungen vorhanden');
|
||||
}
|
||||
|
||||
target.textContent = warnings.length ? warnings.join(' | ') : 'Keine offenen Warnungen erkannt';
|
||||
}
|
||||
|
||||
function renderRecentChanges() {
|
||||
const target = document.querySelector('[data-dashboard-recent]');
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.textContent = 'Letzte Aenderungen werden serverseitig noch nicht protokolliert.';
|
||||
}
|
||||
|
||||
function countRows(selector) {
|
||||
return document.querySelectorAll(selector).length;
|
||||
}
|
||||
|
||||
return { init };
|
||||
})();
|
||||
|
||||
@@ -66,10 +66,24 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.confirm('Diesen Gerätetyp wirklich löschen? Alle zugeordneten Geräte werden angepasst.')) {
|
||||
// TODO: Delete-Endpoint/Flow ist noch nicht implementiert.
|
||||
window.alert('Löschen noch nicht implementiert');
|
||||
if (!window.confirm('Diesen Geraetetyp wirklich loeschen?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('?module=device_types&action=delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: 'id=' + encodeURIComponent(id)
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data && data.success) {
|
||||
window.location.href = '?module=device_types&action=list';
|
||||
return;
|
||||
}
|
||||
window.alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||
})
|
||||
.catch(() => window.alert('Loeschen fehlgeschlagen'));
|
||||
});
|
||||
|
||||
deleteButton.dataset.deleteBound = '1';
|
||||
|
||||
@@ -13,8 +13,20 @@
|
||||
}
|
||||
|
||||
if (window.confirm('Diesen Geraetetyp wirklich loeschen?')) {
|
||||
// TODO: AJAX-Delete implementieren
|
||||
window.alert('Loeschen noch nicht implementiert');
|
||||
fetch('?module=device_types&action=delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: 'id=' + encodeURIComponent(id)
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data && data.success) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
window.alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||
})
|
||||
.catch(() => window.alert('Loeschen fehlgeschlagen'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -21,16 +21,34 @@
|
||||
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||
}
|
||||
|
||||
function handleBuildingDelete() {
|
||||
if (confirm('Dieses Gebaeude wirklich loeschen? Alle Stockwerke werden geloescht.')) {
|
||||
alert('Loeschen noch nicht implementiert');
|
||||
function handleBuildingDelete(id) {
|
||||
if (!confirm('Dieses Gebaeude wirklich loeschen? Alle Stockwerke werden geloescht.')) {
|
||||
return;
|
||||
}
|
||||
postDelete('?module=buildings&action=delete&id=' + encodeURIComponent(id))
|
||||
.then((data) => {
|
||||
if (data && (data.success || data.status === 'ok')) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
alert((data && (data.message || data.error)) ? (data.message || data.error) : 'Loeschen fehlgeschlagen');
|
||||
})
|
||||
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||
}
|
||||
|
||||
function handleFloorDelete() {
|
||||
if (confirm('Dieses Stockwerk wirklich loeschen? Alle Raeume und Racks werden geloescht.')) {
|
||||
alert('Loeschen noch nicht implementiert');
|
||||
function handleFloorDelete(id) {
|
||||
if (!confirm('Dieses Stockwerk wirklich loeschen? Alle Raeume und Racks werden geloescht.')) {
|
||||
return;
|
||||
}
|
||||
postDelete('?module=floors&action=delete&id=' + encodeURIComponent(id))
|
||||
.then((data) => {
|
||||
if (data && data.success) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
alert((data && data.message) ? data.message : 'Loeschen fehlgeschlagen');
|
||||
})
|
||||
.catch(() => alert('Loeschen fehlgeschlagen'));
|
||||
}
|
||||
|
||||
function handleRoomDelete(id) {
|
||||
|
||||
@@ -1,61 +1,23 @@
|
||||
// Netzwerk-Graph-Ansicht (Nodes, Kanten, Filter)
|
||||
/**
|
||||
* network-view.js
|
||||
*
|
||||
* Darstellung der Netzwerk-Topologie:
|
||||
* - Geräte als Nodes
|
||||
* - Ports als Ankerpunkte
|
||||
* - Verbindungen als Linien
|
||||
* - Freie / selbstdefinierte Verbindungstypen
|
||||
*
|
||||
* Kein Layout-Framework (kein D3, kein Cytoscape)
|
||||
* -> bewusst simpel & erweiterbar
|
||||
*/
|
||||
|
||||
(() => {
|
||||
/* =========================
|
||||
* Konfiguration
|
||||
* ========================= */
|
||||
const svgElement = document.querySelector('#network-svg');
|
||||
if (!svgElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Standort / Rack / View-Kontext vom Backend setzen
|
||||
const CONTEXT_ID = null;
|
||||
|
||||
// TODO: API-Endpunkte definieren
|
||||
const API_LOAD_NETWORK = '/api/network_view.php?action=load';
|
||||
const CONTEXT_TYPE = svgElement.dataset.contextType || 'all';
|
||||
const CONTEXT_ID = Number(svgElement.dataset.contextId || 0);
|
||||
const API_LOAD_NETWORK = '/api/connections.php?action=load';
|
||||
const API_SAVE_POSITIONS = '/api/network_view.php?action=save_positions';
|
||||
|
||||
/* =========================
|
||||
* State
|
||||
* ========================= */
|
||||
|
||||
let svgElement = null;
|
||||
|
||||
let devices = []; // Geräte inkl. Position
|
||||
let connections = []; // Verbindungen zwischen Ports
|
||||
|
||||
let devices = [];
|
||||
let ports = [];
|
||||
let connections = [];
|
||||
let selectedDeviceId = null;
|
||||
let isDragging = false;
|
||||
let dragOffset = { x: 0, y: 0 };
|
||||
|
||||
/* =========================
|
||||
* Initialisierung
|
||||
* ========================= */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
svgElement = document.querySelector('#network-svg');
|
||||
|
||||
if (!svgElement) {
|
||||
console.warn('Network View: #network-svg nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
bindSvgEvents();
|
||||
loadNetwork();
|
||||
});
|
||||
|
||||
/* =========================
|
||||
* Events
|
||||
* ========================= */
|
||||
bindSvgEvents();
|
||||
loadNetwork();
|
||||
|
||||
function bindSvgEvents() {
|
||||
svgElement.addEventListener('mousemove', onMouseMove);
|
||||
@@ -63,37 +25,40 @@ function bindSvgEvents() {
|
||||
svgElement.addEventListener('click', onSvgClick);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Laden
|
||||
* ========================= */
|
||||
function buildLoadUrl() {
|
||||
const params = new URLSearchParams();
|
||||
params.set('action', 'load');
|
||||
params.set('context_type', CONTEXT_TYPE);
|
||||
if (CONTEXT_TYPE !== 'all') {
|
||||
params.set('context_id', String(CONTEXT_ID));
|
||||
}
|
||||
return '/api/connections.php?' + params.toString();
|
||||
}
|
||||
|
||||
function loadNetwork() {
|
||||
if (!CONTEXT_ID) {
|
||||
console.warn('CONTEXT_ID nicht gesetzt');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`${API_LOAD_NETWORK}&context_id=${CONTEXT_ID}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
// TODO: Datenstruktur validieren
|
||||
devices = data.devices || [];
|
||||
connections = data.connections || [];
|
||||
fetch(buildLoadUrl())
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (!data || !Array.isArray(data.devices) || !Array.isArray(data.connections)) {
|
||||
throw new Error('Antwortformat ungueltig');
|
||||
}
|
||||
|
||||
devices = data.devices.map((device, index) => ({
|
||||
...device,
|
||||
x: Number(device.pos_x ?? device.x ?? 50 + (index % 6) * 150),
|
||||
y: Number(device.pos_y ?? device.y ?? 60 + Math.floor(index / 6) * 120)
|
||||
}));
|
||||
ports = Array.isArray(data.ports) ? data.ports : [];
|
||||
connections = data.connections;
|
||||
renderAll();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error('Fehler beim Laden der Netzwerkansicht', err);
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Rendering
|
||||
* ========================= */
|
||||
|
||||
function renderAll() {
|
||||
clearSvg();
|
||||
|
||||
renderConnections();
|
||||
renderDevices();
|
||||
}
|
||||
@@ -104,34 +69,27 @@ function clearSvg() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Geräte ---------- */
|
||||
|
||||
function renderDevices() {
|
||||
devices.forEach(device => renderDevice(device));
|
||||
devices.forEach((device) => renderDevice(device));
|
||||
}
|
||||
|
||||
function renderDevice(device) {
|
||||
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
group.classList.add('device-node');
|
||||
group.dataset.id = device.id;
|
||||
group.setAttribute('transform', `translate(${device.x || 0}, ${device.y || 0})`);
|
||||
|
||||
group.setAttribute(
|
||||
'transform',
|
||||
`translate(${device.x || 0}, ${device.y || 0})`
|
||||
);
|
||||
|
||||
// TODO: Gerätetyp (SVG oder JPG) korrekt laden
|
||||
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
||||
rect.setAttribute('width', 120);
|
||||
rect.setAttribute('height', 60);
|
||||
rect.setAttribute('rx', 6);
|
||||
rect.classList.add('device-node-rect');
|
||||
|
||||
rect.addEventListener('mousedown', (e) => {
|
||||
startDrag(e, device.id);
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Label
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', 60);
|
||||
text.setAttribute('y', 35);
|
||||
@@ -141,40 +99,59 @@ function renderDevice(device) {
|
||||
group.appendChild(rect);
|
||||
group.appendChild(text);
|
||||
|
||||
// TODO: Ports als kleine Kreise anlegen (Position aus Portdefinition)
|
||||
// TODO: Ports klickbar machen (für Verbindungs-Erstellung)
|
||||
const devicePorts = ports.filter((port) => Number(port.device_id) === Number(device.id));
|
||||
const spacing = 120 / (Math.max(1, devicePorts.length) + 1);
|
||||
devicePorts.forEach((port, index) => {
|
||||
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
dot.setAttribute('cx', String(Math.round((index + 1) * spacing)));
|
||||
dot.setAttribute('cy', '62');
|
||||
dot.setAttribute('r', '3');
|
||||
dot.classList.add('device-port-dot');
|
||||
dot.dataset.portId = String(port.id);
|
||||
dot.dataset.deviceId = String(device.id);
|
||||
dot.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
console.info('Port ausgewaehlt', port.id);
|
||||
});
|
||||
group.appendChild(dot);
|
||||
});
|
||||
|
||||
svgElement.appendChild(group);
|
||||
}
|
||||
|
||||
/* ---------- Verbindungen ---------- */
|
||||
|
||||
function renderConnections() {
|
||||
connections.forEach(conn => renderConnection(conn));
|
||||
connections.forEach((connection) => renderConnection(connection));
|
||||
}
|
||||
|
||||
function renderConnection(connection) {
|
||||
// TODO: Quell- & Ziel-Port-Koordinaten berechnen
|
||||
// TODO: unterschiedliche Verbindungstypen (Farbe, Strichart, Dicke)
|
||||
const sourcePort = ports.find((port) => Number(port.id) === Number(connection.port_a_id));
|
||||
const targetPort = ports.find((port) => Number(port.id) === Number(connection.port_b_id));
|
||||
if (!sourcePort || !targetPort) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceDevice = devices.find((device) => Number(device.id) === Number(sourcePort.device_id));
|
||||
const targetDevice = devices.find((device) => Number(device.id) === Number(targetPort.device_id));
|
||||
if (!sourceDevice || !targetDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', String(sourceDevice.x + 60));
|
||||
line.setAttribute('y1', String(sourceDevice.y + 60));
|
||||
line.setAttribute('x2', String(targetDevice.x + 60));
|
||||
line.setAttribute('y2', String(targetDevice.y + 60));
|
||||
|
||||
line.setAttribute('x1', 0);
|
||||
line.setAttribute('y1', 0);
|
||||
line.setAttribute('x2', 100);
|
||||
line.setAttribute('y2', 100);
|
||||
|
||||
const isFiber = String(connection.mode || '').toLowerCase().includes('fiber');
|
||||
line.classList.add('connection-line');
|
||||
line.setAttribute('stroke', isFiber ? '#2f6fef' : '#1f8b4c');
|
||||
line.setAttribute('stroke-width', isFiber ? '2.5' : '2');
|
||||
line.setAttribute('stroke-dasharray', isFiber ? '6 4' : '');
|
||||
|
||||
svgElement.appendChild(line);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Interaktion
|
||||
* ========================= */
|
||||
|
||||
function onSvgClick(event) {
|
||||
// Klick ins Leere -> Auswahl aufheben
|
||||
if (event.target === svgElement) {
|
||||
selectedDeviceId = null;
|
||||
updateSelection();
|
||||
@@ -202,7 +179,6 @@ function onMouseMove(event) {
|
||||
if (!device) return;
|
||||
|
||||
const point = getSvgCoordinates(event);
|
||||
|
||||
device.x = point.x + dragOffset.x;
|
||||
device.y = point.y + dragOffset.y;
|
||||
|
||||
@@ -210,31 +186,28 @@ function onMouseMove(event) {
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
if (!isDragging) return;
|
||||
if (!isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
isDragging = false;
|
||||
|
||||
// TODO: Positionen optional automatisch speichern
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Auswahl
|
||||
* ========================= */
|
||||
|
||||
function updateSelection() {
|
||||
svgElement.querySelectorAll('.device-node').forEach(el => {
|
||||
el.classList.toggle(
|
||||
'selected',
|
||||
el.dataset.id === String(selectedDeviceId)
|
||||
);
|
||||
svgElement.querySelectorAll('.device-node').forEach((el) => {
|
||||
el.classList.toggle('selected', el.dataset.id === String(selectedDeviceId));
|
||||
});
|
||||
|
||||
// TODO: Sidebar mit Gerätedetails füllen
|
||||
}
|
||||
const sidebar = document.querySelector('[data-network-selected-device]');
|
||||
if (!sidebar) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Speichern
|
||||
* ========================= */
|
||||
const device = getDeviceById(selectedDeviceId);
|
||||
sidebar.textContent = device
|
||||
? `${device.name} (ID ${device.id})`
|
||||
: 'Kein Geraet ausgewaehlt';
|
||||
}
|
||||
|
||||
function savePositions() {
|
||||
fetch(API_SAVE_POSITIONS, {
|
||||
@@ -242,27 +215,21 @@ function savePositions() {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
context_id: CONTEXT_ID,
|
||||
devices: devices.map(d => ({
|
||||
id: d.id,
|
||||
x: d.x,
|
||||
y: d.y
|
||||
}))
|
||||
devices: devices.map((device) => ({ id: device.id, x: device.x, y: device.y }))
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
// TODO: Erfolg / Fehler anzeigen
|
||||
console.log('Positionen gespeichert', data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Fehler beim Speichern', err);
|
||||
});
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data?.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
alert('Positionen gespeichert');
|
||||
})
|
||||
.catch((err) => {
|
||||
alert('Positionen konnten nicht gespeichert werden: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Hilfsfunktionen
|
||||
* ========================= */
|
||||
|
||||
function getSvgCoordinates(event) {
|
||||
const pt = svgElement.createSVGPoint();
|
||||
pt.x = event.clientX;
|
||||
@@ -273,19 +240,23 @@ function getSvgCoordinates(event) {
|
||||
}
|
||||
|
||||
function getDeviceById(id) {
|
||||
return devices.find(d => d.id === id);
|
||||
return devices.find((device) => Number(device.id) === Number(id));
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Keyboard Shortcuts
|
||||
* ========================= */
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
selectedDeviceId = null;
|
||||
updateSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Delete -> Gerät entfernen?
|
||||
if (event.key === 'Delete' && selectedDeviceId) {
|
||||
console.warn('Delete von Geraeten ist in der Netzwerkansicht noch nicht implementiert.');
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() === 's' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
savePositions();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -1,92 +1,60 @@
|
||||
// Logik für den SVG-Port-Editor (Klicks, Drag & Drop, Speichern)
|
||||
/**
|
||||
* svg-editor.js
|
||||
*
|
||||
* Logik für den SVG-Port-Editor:
|
||||
* - Ports per Klick anlegen
|
||||
* - Ports auswählen
|
||||
* - Ports verschieben (Drag & Drop)
|
||||
* - Ports löschen
|
||||
* - Ports laden / speichern
|
||||
*
|
||||
* Abhängigkeiten: keine (Vanilla JS)
|
||||
*/
|
||||
|
||||
(() => {
|
||||
/* =========================
|
||||
* Konfiguration
|
||||
* ========================= */
|
||||
const svgElement = document.querySelector('#device-svg');
|
||||
if (!svgElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: vom Backend setzen (z. B. via data-Attribut)
|
||||
const DEVICE_TYPE_ID = null;
|
||||
|
||||
// TODO: API-Endpunkte festlegen
|
||||
const DEVICE_TYPE_ID = Number(svgElement.dataset.deviceTypeId || 0);
|
||||
const API_LOAD_PORTS = '/api/device_type_ports.php?action=load';
|
||||
const API_SAVE_PORTS = '/api/device_type_ports.php?action=save';
|
||||
const DEFAULT_PORT_TYPE_ID = null;
|
||||
|
||||
/* =========================
|
||||
* State
|
||||
* ========================= */
|
||||
|
||||
let svgElement = null;
|
||||
let ports = [];
|
||||
let selectedPortId = null;
|
||||
let isDragging = false;
|
||||
let dragOffset = { x: 0, y: 0 };
|
||||
|
||||
/* =========================
|
||||
* Initialisierung
|
||||
* ========================= */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
svgElement = document.querySelector('#device-svg');
|
||||
|
||||
if (!svgElement) {
|
||||
console.warn('SVG Editor: #device-svg nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
bindSvgEvents();
|
||||
loadPorts();
|
||||
});
|
||||
|
||||
/* =========================
|
||||
* SVG Events
|
||||
* ========================= */
|
||||
bindSvgEvents();
|
||||
loadPorts();
|
||||
|
||||
function bindSvgEvents() {
|
||||
svgElement.addEventListener('click', onSvgClick);
|
||||
svgElement.addEventListener('mousemove', onSvgMouseMove);
|
||||
svgElement.addEventListener('mouseup', onSvgMouseUp);
|
||||
|
||||
const saveButton = document.querySelector('[data-save-svg-ports]');
|
||||
if (saveButton) {
|
||||
saveButton.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
savePorts();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Port-Erstellung
|
||||
* ========================= */
|
||||
|
||||
function onSvgClick(event) {
|
||||
// Klick auf bestehenden Port?
|
||||
if (event.target.classList.contains('port-point')) {
|
||||
selectPort(event.target.dataset.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Modifier-Key prüfen (z. B. nur mit SHIFT neuen Port erstellen?)
|
||||
const point = getSvgCoordinates(event);
|
||||
// New ports are only created while SHIFT is held.
|
||||
if (!event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const point = getSvgCoordinates(event);
|
||||
createPort(point.x, point.y);
|
||||
}
|
||||
|
||||
function createPort(x, y) {
|
||||
const id = generateTempId();
|
||||
|
||||
const port = {
|
||||
id: id,
|
||||
id,
|
||||
name: `Port ${ports.length + 1}`,
|
||||
port_type_id: null, // TODO: Default-Porttyp?
|
||||
x: x,
|
||||
y: y,
|
||||
comment: ''
|
||||
port_type_id: DEFAULT_PORT_TYPE_ID,
|
||||
x,
|
||||
y,
|
||||
metadata: null
|
||||
};
|
||||
|
||||
ports.push(port);
|
||||
@@ -94,13 +62,8 @@ function createPort(x, y) {
|
||||
selectPort(id);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Rendering
|
||||
* ========================= */
|
||||
|
||||
function renderPort(port) {
|
||||
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
|
||||
circle.setAttribute('cx', port.x);
|
||||
circle.setAttribute('cy', port.y);
|
||||
circle.setAttribute('r', 6);
|
||||
@@ -116,27 +79,39 @@ function renderPort(port) {
|
||||
}
|
||||
|
||||
function rerenderPorts() {
|
||||
svgElement.querySelectorAll('.port-point').forEach(p => p.remove());
|
||||
svgElement.querySelectorAll('.port-point').forEach((node) => node.remove());
|
||||
ports.forEach(renderPort);
|
||||
if (selectedPortId !== null) {
|
||||
selectPort(selectedPortId);
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Auswahl
|
||||
* ========================= */
|
||||
|
||||
function selectPort(id) {
|
||||
selectedPortId = id;
|
||||
|
||||
document.querySelectorAll('.port-point').forEach(el => {
|
||||
el.classList.toggle('selected', el.dataset.id === id);
|
||||
document.querySelectorAll('.port-point').forEach((el) => {
|
||||
el.classList.toggle('selected', el.dataset.id === String(id));
|
||||
});
|
||||
|
||||
// TODO: Sidebar-Felder mit Portdaten füllen
|
||||
const selected = getPortById(id);
|
||||
fillSidebar(selected);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Drag & Drop
|
||||
* ========================= */
|
||||
function fillSidebar(port) {
|
||||
const nameField = document.querySelector('[data-port-name]');
|
||||
const typeField = document.querySelector('[data-port-type-id]');
|
||||
const xField = document.querySelector('[data-port-x]');
|
||||
const yField = document.querySelector('[data-port-y]');
|
||||
|
||||
if (nameField) nameField.value = port?.name || '';
|
||||
if (typeField) typeField.value = port?.port_type_id || '';
|
||||
if (xField) xField.value = port ? Math.round(port.x) : '';
|
||||
if (yField) yField.value = port ? Math.round(port.y) : '';
|
||||
}
|
||||
|
||||
function resetSidebar() {
|
||||
fillSidebar(null);
|
||||
}
|
||||
|
||||
function startDrag(event, portId) {
|
||||
const port = getPortById(portId);
|
||||
@@ -157,7 +132,6 @@ function onSvgMouseMove(event) {
|
||||
if (!port) return;
|
||||
|
||||
const point = getSvgCoordinates(event);
|
||||
|
||||
port.x = point.x + dragOffset.x;
|
||||
port.y = point.y + dragOffset.y;
|
||||
|
||||
@@ -168,92 +142,95 @@ function onSvgMouseUp() {
|
||||
isDragging = false;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Löschen
|
||||
* ========================= */
|
||||
|
||||
function deleteSelectedPort() {
|
||||
if (!selectedPortId) return;
|
||||
if (!selectedPortId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Sicherheitsabfrage (confirm)
|
||||
ports = ports.filter(p => p.id !== selectedPortId);
|
||||
if (!confirm('Ausgewaehlten Port loeschen?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
ports = ports.filter((port) => String(port.id) !== String(selectedPortId));
|
||||
selectedPortId = null;
|
||||
|
||||
rerenderPorts();
|
||||
|
||||
// TODO: Sidebar zurücksetzen
|
||||
resetSidebar();
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Laden / Speichern
|
||||
* ========================= */
|
||||
|
||||
function loadPorts() {
|
||||
if (!DEVICE_TYPE_ID) {
|
||||
console.warn('DEVICE_TYPE_ID nicht gesetzt');
|
||||
console.warn('SVG Editor: DEVICE_TYPE_ID fehlt auf #device-svg');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`${API_LOAD_PORTS}&device_type_id=${DEVICE_TYPE_ID}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
// TODO: Datenformat validieren
|
||||
ports = data;
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Antwortformat ungueltig');
|
||||
}
|
||||
|
||||
ports = data
|
||||
.filter((entry) => entry && typeof entry === 'object')
|
||||
.map((entry) => ({
|
||||
id: entry.id,
|
||||
name: String(entry.name || ''),
|
||||
port_type_id: entry.port_type_id ? Number(entry.port_type_id) : null,
|
||||
x: Number(entry.x || 0),
|
||||
y: Number(entry.y || 0),
|
||||
metadata: entry.metadata || null
|
||||
}));
|
||||
|
||||
rerenderPorts();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error('Fehler beim Laden der Ports', err);
|
||||
});
|
||||
}
|
||||
|
||||
function savePorts() {
|
||||
if (!DEVICE_TYPE_ID) return;
|
||||
if (!DEVICE_TYPE_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(API_SAVE_PORTS, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
device_type_id: DEVICE_TYPE_ID,
|
||||
ports: ports
|
||||
ports
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
// TODO: Erfolg / Fehler anzeigen
|
||||
console.log('Ports gespeichert', data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Fehler beim Speichern', err);
|
||||
});
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data?.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
alert('Ports gespeichert');
|
||||
})
|
||||
.catch((err) => {
|
||||
alert('Speichern fehlgeschlagen: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Hilfsfunktionen
|
||||
* ========================= */
|
||||
|
||||
function getSvgCoordinates(event) {
|
||||
const pt = svgElement.createSVGPoint();
|
||||
pt.x = event.clientX;
|
||||
pt.y = event.clientY;
|
||||
|
||||
const transformed = pt.matrixTransform(svgElement.getScreenCTM().inverse());
|
||||
return { x: transformed.x, y: transformed.y };
|
||||
}
|
||||
|
||||
function getPortById(id) {
|
||||
return ports.find(p => p.id === id);
|
||||
return ports.find((port) => String(port.id) === String(id));
|
||||
}
|
||||
|
||||
function generateTempId() {
|
||||
return 'tmp_' + Math.random().toString(36).substr(2, 9);
|
||||
return 'tmp_' + Math.random().toString(36).slice(2, 11);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Keyboard Shortcuts
|
||||
* ========================= */
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Delete') {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Delete') {
|
||||
deleteSelectedPort();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user