Graph: Bearbeitungsmodus, Toolbar-Layout, Contact-Filter
- Nodes standardmäßig gesperrt (autoungrabify), nur per "Anordnen"-Button verschiebbar, explizites Speichern/Abbrechen - Graph-Toolbar unter die System-Tab-Borderlinie verschoben - Anordnen/Speichern/Abbrechen rechts in Zeile 2 (Spacer) - Contact-Filter in graph_data.php: Kunden-Ebene zeigt nur Elemente ohne Kontaktzuweisung (konsistent mit Baumansicht) - CSS: display ohne !important damit style="display:none" greift - Changelog und Sprachdateien aktualisiert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
840c0132c3
commit
e6b28fe85e
8 changed files with 520 additions and 75 deletions
13
ChangeLog.md
13
ChangeLog.md
|
|
@ -7,15 +7,18 @@
|
|||
- Raeume als Compound-Container, Geraete als Nodes darin
|
||||
- Kabelverbindungen als sichtbare Edges (auch raumuebergreifend)
|
||||
- Durchgeschleifte Leitungen als gestrichelte Linien
|
||||
- Zwei Layout-Modi per Knopfdruck:
|
||||
- **Raeumlich** (cose-bilkent): Geraete gruppiert nach Raeumen
|
||||
- **Technisch** (dagre): Hierarchischer Stromfluss top-down
|
||||
- Dagre-Layout: Hierarchischer Stromfluss top-down
|
||||
- Zoom/Pan/Fit-Controls, Mausrad-Zoom Toggle
|
||||
- Kabeltyp-Legende mit Farben
|
||||
- Node-Positionen speicherbar (Drag&Drop → AJAX-Speicherung)
|
||||
- **Bearbeitungsmodus**: Nodes nur per "Anordnen"-Button verschiebbar,
|
||||
Positionen per "Speichern"-Button fest, "Abbrechen" setzt zurueck
|
||||
- Viewport-Persistenz (Zoom/Pan bleibt beim Seitenwechsel)
|
||||
- Klick auf Node/Edge oeffnet Detail-/Bearbeitungsseite
|
||||
- Suche als Overlay im Graph-Container (Nodes hervorheben/abdunkeln)
|
||||
- Kontextmenue (Rechtsklick): Ansehen, Bearbeiten, Kopieren, Loeschen
|
||||
- PNG-Export des Graphen
|
||||
- Admin-Setting: Ansichtsmodus (Baum/Graph) in Setup waehlbar
|
||||
- Toolbar zweizeilig: Aktionen oben, Graph-Steuerung unten
|
||||
|
||||
- **Verbindungsformular verbessert**
|
||||
- Select-Dropdowns zeigen nur Geraete (keine Gebaeude/Raeume)
|
||||
|
|
@ -31,6 +34,8 @@
|
|||
- `js/cytoscape.min.js`, `js/dagre.min.js`, `js/cytoscape-dagre.js`, `js/cytoscape-cose-bilkent.js` - Bibliotheken
|
||||
|
||||
### Bugfixes
|
||||
- **Contact-Filter im Graph**: Graph zeigte faelschlicherweise Kontakt-Elemente auf Kunden-Ebene
|
||||
- Fix: `fk_contact` Filter in `graph_data.php` analog zum Baum
|
||||
- **Verbindung hinzufuegen**: Formular zeigte "Feld erforderlich"-Fehler beim Oeffnen
|
||||
- Ursache: `action=create` in URL triggerte Handler vor Formular-Anzeige
|
||||
- Fix: Korrekte Dolibarr-Konvention (create=Formular, add=Verarbeitung)
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ if ($resFields) {
|
|||
// Hierarchie kommt aus fk_parent (wie im Baum)
|
||||
$sql = "SELECT a.rowid, a.label, a.fk_parent, a.fk_system, a.fk_anlage_type,";
|
||||
$sql .= " a.field_values, a.fk_contact, a.graph_x, a.graph_y,";
|
||||
$sql .= " t.label as type_label, t.picto as type_picto,";
|
||||
$sql .= " t.label as type_label, t.picto as type_picto, t.color as type_color,";
|
||||
$sql .= " s.code as system_code, s.label as system_label,";
|
||||
$sql .= " ts.code as type_system_code,";
|
||||
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type IN ('image/jpeg','image/png','image/gif','image/webp')) as image_count,";
|
||||
|
|
@ -78,6 +78,9 @@ $sql .= " WHERE a.fk_soc = ".(int)$socId;
|
|||
$sql .= " AND s.code != 'GLOBAL'";
|
||||
if ($contactId > 0) {
|
||||
$sql .= " AND a.fk_contact = ".(int)$contactId;
|
||||
} else {
|
||||
// Auf Kunden-Ebene nur Elemente ohne Kontaktzuweisung (wie im Baum)
|
||||
$sql .= " AND (a.fk_contact IS NULL OR a.fk_contact = 0)";
|
||||
}
|
||||
$sql .= " AND a.status = 1";
|
||||
$sql .= " ORDER BY a.fk_parent, a.rang, a.rowid";
|
||||
|
|
@ -99,6 +102,7 @@ if ($resql) {
|
|||
'label' => $obj->label,
|
||||
'type_label' => $obj->type_label ?: '',
|
||||
'type_picto' => $obj->type_picto ?: '',
|
||||
'type_color' => $obj->type_color ?: '',
|
||||
'system_code' => $obj->system_code ?: '',
|
||||
'system_label' => $obj->system_label ?: '',
|
||||
'fk_parent' => (int) $obj->fk_parent,
|
||||
|
|
@ -133,13 +137,14 @@ if ($resql) {
|
|||
'value' => $val,
|
||||
'display' => $display,
|
||||
'color' => isset($fm['color']) ? $fm['color'] : '',
|
||||
'type' => isset($fm['type']) ? $fm['type'] : 'text',
|
||||
);
|
||||
}
|
||||
$nodeData['fields'] = $fields;
|
||||
}
|
||||
}
|
||||
|
||||
// Compound-Parent aus fk_parent (wie im Baum)
|
||||
// Compound-Parent aus fk_parent (Eltern-Kind-Verschachtelung)
|
||||
if ($obj->fk_parent > 0) {
|
||||
$nodeData['parent'] = 'n_'.$obj->fk_parent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -618,6 +618,21 @@ body.kundenkarte-drag-active * {
|
|||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
/* System hinzufügen Button - kleiner, knapp über der Linie */
|
||||
.kundenkarte-add-system-btn {
|
||||
font-size: 11px !important;
|
||||
padding: 4px 10px !important;
|
||||
margin-left: auto !important;
|
||||
align-self: flex-end !important;
|
||||
margin-bottom: -1px !important;
|
||||
height: auto !important;
|
||||
line-height: 1.3 !important;
|
||||
}
|
||||
|
||||
.kundenkarte-add-system-btn i {
|
||||
font-size: 9px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-system-tab {
|
||||
padding: 8px 16px !important;
|
||||
border: 1px solid #555 !important;
|
||||
|
|
@ -643,7 +658,7 @@ body.kundenkarte-drag-active * {
|
|||
}
|
||||
|
||||
.kundenkarte-system-tab-icon {
|
||||
font-size: 16px !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
|
|
|
|||
|
|
@ -33,27 +33,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Toolbar: Aktionen links, Zoom rechts */
|
||||
/* Toolbar: Zweizeilig */
|
||||
.kundenkarte-graph-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-actions .button {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-zoom-controls {
|
||||
.kundenkarte-graph-toolbar-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Alle Buttons in der Graph-Toolbar einheitlich */
|
||||
.kundenkarte-graph-toolbar .button,
|
||||
.kundenkarte-graph-toolbar button.button {
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center !important;
|
||||
gap: 4px !important;
|
||||
padding: 6px 12px !important;
|
||||
font-size: 12px !important;
|
||||
height: 30px !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Spacer: schiebt Anordnen/Speichern/Abbrechen nach rechts */
|
||||
.kundenkarte-graph-toolbar-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#btn-graph-wheel-zoom.active {
|
||||
|
|
@ -62,6 +72,33 @@
|
|||
border-color: var(--butactionbg, #4390dc) !important;
|
||||
}
|
||||
|
||||
/* Bearbeitungsmodus: Container-Rahmen */
|
||||
#kundenkarte-graph-container.graph-edit-mode {
|
||||
border-color: #5a9a6a;
|
||||
box-shadow: 0 0 0 2px rgba(90, 154, 106, 0.3);
|
||||
}
|
||||
|
||||
/* Speichern-Button grün, Abbrechen-Button rot */
|
||||
.btn-graph-save {
|
||||
background: #2e7d32 !important;
|
||||
border-color: #2e7d32 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.btn-graph-save:hover {
|
||||
background: #388e3c !important;
|
||||
}
|
||||
|
||||
.btn-graph-cancel {
|
||||
background: #c62828 !important;
|
||||
border-color: #c62828 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.btn-graph-cancel:hover {
|
||||
background: #d32f2f !important;
|
||||
}
|
||||
|
||||
/* Legende */
|
||||
.kundenkarte-graph-legend {
|
||||
display: flex;
|
||||
|
|
@ -199,31 +236,98 @@
|
|||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Kontextmenü (Rechtsklick auf Node) */
|
||||
.kundenkarte-graph-contextmenu {
|
||||
position: absolute;
|
||||
z-index: 200;
|
||||
background: var(--colorbacktabcard1, #1e2a3a);
|
||||
border: 1px solid var(--inputbordercolor, #3a6a8e);
|
||||
border-radius: 6px;
|
||||
padding: 4px 0;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.kundenkarte-graph-contextmenu .ctx-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
color: var(--colortext, #ddd);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-contextmenu .ctx-item:hover {
|
||||
background: var(--butactionbg, #4390dc);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-contextmenu .ctx-item i {
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-contextmenu .ctx-delete {
|
||||
color: #e57373;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-contextmenu .ctx-delete:hover {
|
||||
background: #c62828;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Suchfeld als Overlay im Graph-Container */
|
||||
.kundenkarte-graph-search-floating {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-search-floating input {
|
||||
padding: 6px 10px 6px 28px;
|
||||
border: 1px solid var(--inputbordercolor, #3a3a3a);
|
||||
border-radius: 4px;
|
||||
background: var(--colorbackbody, #1d1e20);
|
||||
color: var(--colortext, #ddd);
|
||||
font-size: 12px;
|
||||
width: 180px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s, width 0.2s;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 8px center;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-search-floating input:focus {
|
||||
outline: none;
|
||||
border-color: var(--butactionbg, #4390dc);
|
||||
opacity: 1;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
/* Mobile Anpassungen */
|
||||
@media (max-width: 768px) {
|
||||
.kundenkarte-graph-toolbar {
|
||||
flex-wrap: wrap;
|
||||
.kundenkarte-graph-toolbar-row {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-actions {
|
||||
flex: 1 1 100%;
|
||||
order: 2;
|
||||
.kundenkarte-graph-toolbar .button,
|
||||
.kundenkarte-graph-toolbar button.button {
|
||||
padding: 8px 8px !important;
|
||||
font-size: 11px !important;
|
||||
height: 28px !important;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-actions .button {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10px 8px !important;
|
||||
font-size: 12px !important;
|
||||
.kundenkarte-graph-search-floating input {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-zoom-controls {
|
||||
order: 1;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.kundenkarte-graph-zoom-controls .button {
|
||||
padding: 10px 8px !important;
|
||||
font-size: 12px !important;
|
||||
.kundenkarte-graph-search-floating input:focus {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,12 +61,20 @@
|
|||
ajaxUrl: '',
|
||||
saveUrl: '',
|
||||
moduleUrl: '',
|
||||
pageUrl: '',
|
||||
systemId: 0,
|
||||
socId: 0,
|
||||
contactId: 0,
|
||||
isContact: false,
|
||||
canEdit: false,
|
||||
canDelete: false,
|
||||
tooltipEl: null,
|
||||
contextMenuEl: null,
|
||||
contextMenuNodeId: null,
|
||||
wheelZoomEnabled: false,
|
||||
hasPositions: false,
|
||||
editMode: false,
|
||||
_savedPositions: {},
|
||||
_saveTimer: null,
|
||||
_dirtyNodes: {},
|
||||
_viewportTimer: null,
|
||||
|
|
@ -80,7 +88,9 @@
|
|||
this.isContact = config.isContact || false;
|
||||
|
||||
this.createTooltipElement();
|
||||
this.initContextMenu();
|
||||
this.bindZoomButtons();
|
||||
this.bindSearch();
|
||||
this.loadGraphData();
|
||||
},
|
||||
|
||||
|
|
@ -280,7 +290,7 @@
|
|||
var f = n.data.fields[i];
|
||||
var v = f.value;
|
||||
if (!v || v === '') continue;
|
||||
if (v === '1' && f.display === 'badge') v = '\u2713';
|
||||
if (f.type === 'checkbox' && (v === '1' || v === 'true')) v = '\u2713';
|
||||
|
||||
if (f.display === 'parentheses') {
|
||||
parenParts.push(v);
|
||||
|
|
@ -305,6 +315,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Datei-Indikatoren
|
||||
var fileInfo = [];
|
||||
if (n.data.image_count > 0) fileInfo.push('\ud83d\uddbc ' + n.data.image_count);
|
||||
if (n.data.doc_count > 0) fileInfo.push('\ud83d\udcc4 ' + n.data.doc_count);
|
||||
if (fileInfo.length > 0) {
|
||||
if (badgeLines.length === 0) lines.push('─────────────');
|
||||
lines.push(fileInfo.join(' '));
|
||||
}
|
||||
|
||||
n.data.display_label = lines.join('\n');
|
||||
}
|
||||
|
||||
|
|
@ -350,6 +369,7 @@
|
|||
userZoomingEnabled: false,
|
||||
userPanningEnabled: true,
|
||||
boxSelectionEnabled: false,
|
||||
autoungrabify: true,
|
||||
layout: { name: 'preset' }
|
||||
});
|
||||
|
||||
|
|
@ -385,11 +405,25 @@
|
|||
this.bindGraphEvents();
|
||||
},
|
||||
|
||||
/**
|
||||
* Dolibarr CSS-Variablen auslesen (Fallback für nicht-gesetzte Werte)
|
||||
*/
|
||||
getCssVar: function(name, fallback) {
|
||||
try {
|
||||
var val = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
return val || fallback;
|
||||
} catch(e) {
|
||||
return fallback;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Cytoscape Stylesheet
|
||||
*/
|
||||
getStylesheet: function() {
|
||||
var faFont = '"Font Awesome 5 Free", "FontAwesome", sans-serif';
|
||||
// Dolibarr-Schriftfarbe verwenden
|
||||
var textColor = this.getCssVar('--colortext', '#dcdcdc');
|
||||
|
||||
return [
|
||||
// Räume/Gebäude (Compound-Container)
|
||||
|
|
@ -400,7 +434,7 @@
|
|||
'background-color': '#2a2b2d',
|
||||
'background-opacity': 1,
|
||||
'border-width': 2,
|
||||
'border-color': '#4390dc',
|
||||
'border-color': function(node) { return node.data('type_color') || '#4390dc'; },
|
||||
'border-style': 'dashed',
|
||||
'padding': '25px',
|
||||
'label': 'data(display_label)',
|
||||
|
|
@ -409,7 +443,7 @@
|
|||
'text-valign': 'top',
|
||||
'text-halign': 'center',
|
||||
'font-size': '13px',
|
||||
'color': '#ffffff',
|
||||
'color': textColor,
|
||||
'text-margin-y': -8,
|
||||
'text-background-color': '#2a2b2d',
|
||||
'text-background-opacity': 0.95,
|
||||
|
|
@ -429,7 +463,7 @@
|
|||
'padding': '14px',
|
||||
'background-color': '#2d4a3a',
|
||||
'border-width': 2,
|
||||
'border-color': '#5a9a6a',
|
||||
'border-color': function(node) { return node.data('type_color') || '#5a9a6a'; },
|
||||
'label': 'data(display_label)',
|
||||
'font-family': faFont,
|
||||
'font-weight': 'bold',
|
||||
|
|
@ -437,7 +471,7 @@
|
|||
'text-halign': 'center',
|
||||
'text-justification': 'left',
|
||||
'font-size': '11px',
|
||||
'color': '#ffffff',
|
||||
'color': textColor,
|
||||
'text-wrap': 'wrap',
|
||||
'text-max-width': '220px',
|
||||
'line-height': 1.4
|
||||
|
|
@ -491,10 +525,122 @@
|
|||
'border-color': '#d4944a',
|
||||
'border-width': 3
|
||||
}
|
||||
},
|
||||
// Suche: abgedunkelte Nodes
|
||||
{
|
||||
selector: '.search-dimmed',
|
||||
style: {
|
||||
'opacity': 0.2
|
||||
}
|
||||
},
|
||||
// Suche: hervorgehobene Treffer
|
||||
{
|
||||
selector: '.search-highlight',
|
||||
style: {
|
||||
'border-color': '#e8a838',
|
||||
'border-width': 3,
|
||||
'opacity': 1
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* Kontextmenü initialisieren
|
||||
*/
|
||||
initContextMenu: function() {
|
||||
var self = this;
|
||||
this.contextMenuEl = document.getElementById('kundenkarte-graph-contextmenu');
|
||||
if (!this.contextMenuEl) return;
|
||||
|
||||
// Klick auf Menü-Eintrag
|
||||
$(this.contextMenuEl).on('click', '.ctx-item', function(e) {
|
||||
e.preventDefault();
|
||||
var action = $(this).data('action');
|
||||
var anlageId = self.contextMenuNodeId;
|
||||
if (!anlageId) return;
|
||||
|
||||
var baseUrl = self.pageUrl + '?id=' + self.socId + '&system=' + self.systemId;
|
||||
switch (action) {
|
||||
case 'view':
|
||||
window.location.href = baseUrl + '&action=view&anlage_id=' + anlageId;
|
||||
break;
|
||||
case 'edit':
|
||||
window.location.href = baseUrl + '&action=edit&anlage_id=' + anlageId;
|
||||
break;
|
||||
case 'copy':
|
||||
window.location.href = baseUrl + '&action=copy&anlage_id=' + anlageId;
|
||||
break;
|
||||
case 'delete':
|
||||
window.location.href = baseUrl + '&action=delete&anlage_id=' + anlageId;
|
||||
break;
|
||||
case 'add-child':
|
||||
window.location.href = baseUrl + '&action=create&parent_id=' + anlageId;
|
||||
break;
|
||||
}
|
||||
self.hideContextMenu();
|
||||
});
|
||||
|
||||
// Klick außerhalb schließt Menü
|
||||
$(document).on('click', function() {
|
||||
self.hideContextMenu();
|
||||
});
|
||||
},
|
||||
|
||||
showContextMenu: function(node, renderedPos) {
|
||||
if (!this.contextMenuEl) return;
|
||||
this.contextMenuNodeId = node.data('id').replace('n_', '');
|
||||
|
||||
var container = document.getElementById(this.containerId);
|
||||
var rect = container.getBoundingClientRect();
|
||||
var x = rect.left + renderedPos.x;
|
||||
var y = rect.top + renderedPos.y + window.scrollY;
|
||||
|
||||
// Nicht über Bildschirmrand hinaus
|
||||
this.contextMenuEl.style.display = 'block';
|
||||
var menuW = this.contextMenuEl.offsetWidth;
|
||||
var menuH = this.contextMenuEl.offsetHeight;
|
||||
if (x + menuW > window.innerWidth) x = window.innerWidth - menuW - 10;
|
||||
if (y + menuH > window.scrollY + window.innerHeight) y = y - menuH;
|
||||
|
||||
this.contextMenuEl.style.left = x + 'px';
|
||||
this.contextMenuEl.style.top = y + 'px';
|
||||
},
|
||||
|
||||
hideContextMenu: function() {
|
||||
if (this.contextMenuEl) {
|
||||
this.contextMenuEl.style.display = 'none';
|
||||
this.contextMenuNodeId = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Suchfunktion binden
|
||||
*/
|
||||
bindSearch: function() {
|
||||
var self = this;
|
||||
$(document).on('input', '#kundenkarte-graph-search', function() {
|
||||
var query = $(this).val().toLowerCase().trim();
|
||||
if (!self.cy) return;
|
||||
|
||||
if (query === '') {
|
||||
// Alle Nodes wieder normal anzeigen
|
||||
self.cy.nodes().removeClass('search-dimmed search-highlight');
|
||||
return;
|
||||
}
|
||||
|
||||
self.cy.nodes().forEach(function(node) {
|
||||
var label = (node.data('label') || '').toLowerCase();
|
||||
var typeLabel = (node.data('type_label') || '').toLowerCase();
|
||||
if (label.indexOf(query) !== -1 || typeLabel.indexOf(query) !== -1) {
|
||||
node.removeClass('search-dimmed').addClass('search-highlight');
|
||||
} else {
|
||||
node.removeClass('search-highlight').addClass('search-dimmed');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Graph-Events binden
|
||||
*/
|
||||
|
|
@ -539,6 +685,20 @@
|
|||
self.hideTooltip();
|
||||
});
|
||||
|
||||
// Rechtsklick → Kontextmenü
|
||||
this.cy.on('cxttap', 'node', function(evt) {
|
||||
evt.originalEvent.preventDefault();
|
||||
self.hideTooltip();
|
||||
self.showContextMenu(evt.target, evt.renderedPosition);
|
||||
});
|
||||
|
||||
// Klick auf Canvas schließt Kontextmenü
|
||||
this.cy.on('tap', function(evt) {
|
||||
if (evt.target === self.cy) {
|
||||
self.hideContextMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Drag: Begrenzung auf sichtbaren Bereich
|
||||
this.cy.on('drag', 'node', function(evt) {
|
||||
var node = evt.target;
|
||||
|
|
@ -553,8 +713,9 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Drag: Snap-to-Grid + Position merken (inkl. Kinder bei Compound-Nodes)
|
||||
// Drag: Snap-to-Grid + Position merken (nur im Bearbeitungsmodus)
|
||||
this.cy.on('dragfree', 'node', function(evt) {
|
||||
if (!self.editMode) return;
|
||||
var node = evt.target;
|
||||
var pos = node.position();
|
||||
var grid = self.gridSize;
|
||||
|
|
@ -584,11 +745,6 @@
|
|||
});
|
||||
}
|
||||
|
||||
// Debounced speichern (500ms nach letztem Drag)
|
||||
if (self._saveTimer) clearTimeout(self._saveTimer);
|
||||
self._saveTimer = setTimeout(function() {
|
||||
self.savePositions();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Viewport-Änderungen speichern (Pan + Zoom)
|
||||
|
|
@ -656,6 +812,106 @@
|
|||
e.preventDefault();
|
||||
self.resetLayout();
|
||||
});
|
||||
|
||||
// Bearbeitungsmodus: Anordnen ein/aus
|
||||
$(document).on('click', '#btn-graph-edit-mode', function(e) {
|
||||
e.preventDefault();
|
||||
self.enterEditMode();
|
||||
});
|
||||
|
||||
// Bearbeitungsmodus: Speichern
|
||||
$(document).on('click', '#btn-graph-save-positions', function(e) {
|
||||
e.preventDefault();
|
||||
self.savePositions();
|
||||
self.exitEditMode();
|
||||
});
|
||||
|
||||
// Bearbeitungsmodus: Abbrechen
|
||||
$(document).on('click', '#btn-graph-cancel-edit', function(e) {
|
||||
e.preventDefault();
|
||||
self.cancelEditMode();
|
||||
});
|
||||
|
||||
// PNG-Export
|
||||
$(document).on('click', '#btn-graph-export-png', function(e) {
|
||||
e.preventDefault();
|
||||
if (!self.cy) return;
|
||||
var png64 = self.cy.png({
|
||||
output: 'base64uri',
|
||||
bg: getComputedStyle(document.documentElement).getPropertyValue('--colorbackbody').trim() || '#1d1e20',
|
||||
full: true,
|
||||
scale: 2
|
||||
});
|
||||
var link = document.createElement('a');
|
||||
link.download = 'graph_export.png';
|
||||
link.href = png64;
|
||||
link.click();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Bearbeitungsmodus aktivieren - Nodes werden verschiebbar
|
||||
*/
|
||||
enterEditMode: function() {
|
||||
if (!this.cy || this.editMode) return;
|
||||
this.editMode = true;
|
||||
this._dirtyNodes = {};
|
||||
|
||||
// Aktuelle Positionen sichern (für Abbrechen)
|
||||
this._savedPositions = {};
|
||||
var self = this;
|
||||
this.cy.nodes().forEach(function(node) {
|
||||
var pos = node.position();
|
||||
self._savedPositions[node.data('id')] = { x: pos.x, y: pos.y };
|
||||
});
|
||||
|
||||
// Nodes verschiebbar machen
|
||||
this.cy.autoungrabify(false);
|
||||
|
||||
// Buttons umschalten
|
||||
$('#btn-graph-edit-mode').hide();
|
||||
$('#btn-graph-save-positions, #btn-graph-cancel-edit').show();
|
||||
|
||||
// Visuelles Feedback: Container-Rahmen
|
||||
$('#' + this.containerId).addClass('graph-edit-mode');
|
||||
},
|
||||
|
||||
/**
|
||||
* Bearbeitungsmodus beenden (nach Speichern)
|
||||
*/
|
||||
exitEditMode: function() {
|
||||
if (!this.cy) return;
|
||||
this.editMode = false;
|
||||
this._savedPositions = {};
|
||||
|
||||
// Nodes wieder sperren
|
||||
this.cy.autoungrabify(true);
|
||||
|
||||
// Buttons zurückschalten
|
||||
$('#btn-graph-edit-mode').show();
|
||||
$('#btn-graph-save-positions, #btn-graph-cancel-edit').hide();
|
||||
|
||||
// Visuelles Feedback entfernen
|
||||
$('#' + this.containerId).removeClass('graph-edit-mode');
|
||||
},
|
||||
|
||||
/**
|
||||
* Bearbeitungsmodus abbrechen - Positionen zurücksetzen
|
||||
*/
|
||||
cancelEditMode: function() {
|
||||
if (!this.cy) return;
|
||||
|
||||
// Positionen zurücksetzen
|
||||
var self = this;
|
||||
this.cy.nodes().forEach(function(node) {
|
||||
var saved = self._savedPositions[node.data('id')];
|
||||
if (saved) {
|
||||
node.position({ x: saved.x, y: saved.y });
|
||||
}
|
||||
});
|
||||
|
||||
this._dirtyNodes = {};
|
||||
this.exitEditMode();
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -755,7 +1011,7 @@
|
|||
var f = data.fields[i];
|
||||
if (!f.value || f.value === '') continue;
|
||||
var val = f.value;
|
||||
if (val === '1') val = '\u2713';
|
||||
if (f.type === 'checkbox' && (val === '1' || val === 'true')) val = '\u2713';
|
||||
html += '<div class="tooltip-field">';
|
||||
html += '<span class="tooltip-field-label">' + this.escapeHtml(f.label) + '</span>';
|
||||
if (f.display === 'badge') {
|
||||
|
|
@ -839,6 +1095,10 @@
|
|||
KundenKarte.Graph.ajaxUrl = container.getAttribute('data-ajax-url') || '';
|
||||
KundenKarte.Graph.saveUrl = container.getAttribute('data-save-url') || '';
|
||||
KundenKarte.Graph.moduleUrl = container.getAttribute('data-module-url') || '';
|
||||
KundenKarte.Graph.pageUrl = container.getAttribute('data-page-url') || '';
|
||||
KundenKarte.Graph.systemId = parseInt(container.getAttribute('data-systemid')) || 0;
|
||||
KundenKarte.Graph.canEdit = container.getAttribute('data-can-edit') === '1';
|
||||
KundenKarte.Graph.canDelete = container.getAttribute('data-can-delete') === '1';
|
||||
KundenKarte.Graph.init({
|
||||
socId: parseInt(container.getAttribute('data-socid')) || 0,
|
||||
contactId: parseInt(container.getAttribute('data-contactid')) || 0,
|
||||
|
|
|
|||
|
|
@ -539,3 +539,9 @@ NoConnections = Keine Verbindungen vorhanden
|
|||
ConnectionCreated = Verbindung erstellt
|
||||
ConnectionUpdated = Verbindung aktualisiert
|
||||
ConnectionDeleted = Verbindung geloescht
|
||||
|
||||
# Graph-Ansicht
|
||||
TreeView = Baumansicht
|
||||
GraphView = Graph-Ansicht
|
||||
GraphLoading = Graph wird geladen...
|
||||
SearchPlaceholder = Suchen...
|
||||
|
|
|
|||
|
|
@ -287,3 +287,9 @@ BuildingLevelWing = Wing
|
|||
BuildingLevelCorridor = Corridor / Hallway
|
||||
BuildingLevelRoom = Room
|
||||
BuildingLevelArea = Area / Zone
|
||||
|
||||
# Graph View
|
||||
TreeView = Tree View
|
||||
GraphView = Graph View
|
||||
GraphLoading = Loading graph...
|
||||
SearchPlaceholder = Search...
|
||||
|
|
|
|||
|
|
@ -366,8 +366,12 @@ if ($action == 'togglepin' && $permissiontoadd) {
|
|||
|
||||
$title = $langs->trans('TechnicalInstallations').' - '.$object->name;
|
||||
|
||||
// Ansichtsmodus (Admin-Setting)
|
||||
$viewMode = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree');
|
||||
// Ansichtsmodus: URL-Parameter hat Vorrang, sonst Admin-Setting
|
||||
$defaultView = getDolGlobalString('KUNDENKARTE_DEFAULT_VIEW', 'tree');
|
||||
$viewMode = GETPOST('view', 'aZ09');
|
||||
if (!in_array($viewMode, array('tree', 'graph'))) {
|
||||
$viewMode = $defaultView;
|
||||
}
|
||||
|
||||
$jsFiles = array('/kundenkarte/js/kundenkarte.js?v='.time());
|
||||
$cssFiles = array('/kundenkarte/css/kundenkarte.css?v='.time());
|
||||
|
|
@ -459,36 +463,27 @@ if ($permissiontoadd) {
|
|||
// Get systems not yet enabled for this customer
|
||||
$availableSystems = array_diff_key($allSystems, $customerSystems);
|
||||
if (!empty($availableSystems)) {
|
||||
print '<button type="button" class="button small" style="margin-left:auto;" onclick="document.getElementById(\'add-system-form\').style.display=\'block\';">';
|
||||
print '<button type="button" class="button small kundenkarte-add-system-btn" onclick="document.getElementById(\'add-system-form\').style.display=\'block\';">';
|
||||
print '<i class="fa fa-plus"></i> '.$langs->trans('AddSystem');
|
||||
print '</button>';
|
||||
}
|
||||
}
|
||||
print '</div>';
|
||||
|
||||
// Expand/Collapse buttons (only in tree view, not in create/edit/view/copy)
|
||||
// Steuerungs-Buttons (nur wenn kein Formular aktiv)
|
||||
$isTreeView = !in_array($action, array('create', 'edit', 'view', 'copy'));
|
||||
if ($isTreeView) {
|
||||
if ($viewMode === 'graph') {
|
||||
// Graph Controls: Aktionen + Zoom
|
||||
print '<div class="kundenkarte-graph-toolbar">';
|
||||
if ($user->hasRight('kundenkarte', 'write')) {
|
||||
print '<div class="kundenkarte-graph-actions">';
|
||||
print '<a class="button small" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&action=create&system='.$systemId.'"><i class="fa fa-plus"></i> '.$langs->trans('AddElement').'</a>';
|
||||
print '<a class="button small" href="'.dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?socid='.$id.'&system_id='.$systemId.'&action=create"><i class="fa fa-plug"></i> '.$langs->trans('AddConnection').'</a>';
|
||||
print '</div>';
|
||||
}
|
||||
print '<div class="kundenkarte-graph-zoom-controls">';
|
||||
print '<button type="button" class="button small" id="btn-graph-reset-layout" title="Layout zurücksetzen"><i class="fa fa-undo"></i></button>';
|
||||
print '<button type="button" class="button small" id="btn-graph-wheel-zoom" title="Mausrad-Zoom ein"><i class="fa fa-mouse-pointer"></i></button>';
|
||||
print '<button type="button" class="button small" id="btn-graph-zoom-in" title="Zoom +"><i class="fa fa-search-plus"></i></button>';
|
||||
print '<button type="button" class="button small" id="btn-graph-zoom-out" title="Zoom -"><i class="fa fa-search-minus"></i></button>';
|
||||
print '<button type="button" class="button small" id="btn-graph-fit" title="Standard-Zoom"><i class="fa fa-crosshairs"></i></button>';
|
||||
print '</div>';
|
||||
print '</div>';
|
||||
} else {
|
||||
$toggleView = ($viewMode === 'graph') ? 'tree' : 'graph';
|
||||
$toggleUrl = $_SERVER['PHP_SELF'].'?id='.$id.'&system='.$systemId.'&view='.$toggleView;
|
||||
$toggleIcon = ($viewMode === 'graph') ? 'fa-list' : 'fa-sitemap';
|
||||
$toggleLabel = ($viewMode === 'graph') ? $langs->trans('TreeView') : $langs->trans('GraphView');
|
||||
|
||||
if ($viewMode !== 'graph') {
|
||||
// Baumansicht: Controls auf gleicher Zeile wie System-Tabs (im Wrapper)
|
||||
print '<div class="kundenkarte-tree-controls">';
|
||||
// Compact mode toggle (visible on mobile)
|
||||
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'">';
|
||||
print '<i class="fa '.$toggleIcon.'"></i> '.$toggleLabel;
|
||||
print '</a>';
|
||||
print '<button type="button" class="kundenkarte-view-toggle" id="btn-compact-mode" title="Kompakte Ansicht">';
|
||||
print '<i class="fa fa-compress"></i> <span>Kompakt</span>';
|
||||
print '</button>';
|
||||
|
|
@ -510,6 +505,35 @@ if ($isTreeView) {
|
|||
|
||||
print '</div>'; // End kundenkarte-system-tabs-wrapper
|
||||
|
||||
// Graph-Toolbar: UNTER der System-Tab-Borderlinie
|
||||
if ($isTreeView && $viewMode === 'graph') {
|
||||
print '<div class="kundenkarte-graph-toolbar">';
|
||||
// Zeile 1: Ansicht-Wechsel + Aktionen
|
||||
print '<div class="kundenkarte-graph-toolbar-row">';
|
||||
print '<a class="button small" href="'.$toggleUrl.'" title="'.$toggleLabel.'"><i class="fa '.$toggleIcon.'"></i> '.$toggleLabel.'</a>';
|
||||
if ($user->hasRight('kundenkarte', 'write')) {
|
||||
print '<a class="button small" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&action=create&system='.$systemId.'"><i class="fa fa-plus"></i> '.$langs->trans('AddElement').'</a>';
|
||||
print '<a class="button small" href="'.dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?socid='.$id.'&system_id='.$systemId.'&action=create"><i class="fa fa-plug"></i> '.$langs->trans('AddConnection').'</a>';
|
||||
}
|
||||
print '</div>';
|
||||
// Zeile 2: Graph-Steuerung (Anordnen rechts)
|
||||
print '<div class="kundenkarte-graph-toolbar-row">';
|
||||
print '<button type="button" class="button small" id="btn-graph-reset-layout" title="Layout zurücksetzen"><i class="fa fa-refresh"></i> Layout</button>';
|
||||
print '<button type="button" class="button small" id="btn-graph-wheel-zoom" title="Mausrad-Zoom"><i class="fa fa-arrows"></i> Scroll</button>';
|
||||
print '<button type="button" class="button small" id="btn-graph-zoom-in" title="Vergrößern"><i class="fa fa-search-plus"></i></button>';
|
||||
print '<button type="button" class="button small" id="btn-graph-zoom-out" title="Verkleinern"><i class="fa fa-search-minus"></i></button>';
|
||||
print '<button type="button" class="button small" id="btn-graph-fit" title="Einpassen"><i class="fa fa-crosshairs"></i> Fit</button>';
|
||||
print '<button type="button" class="button small" id="btn-graph-export-png" title="PNG exportieren"><i class="fa fa-download"></i> PNG</button>';
|
||||
if ($permissiontoadd) {
|
||||
print '<span class="kundenkarte-graph-toolbar-spacer"></span>';
|
||||
print '<button type="button" class="button small" id="btn-graph-edit-mode" title="Elemente anordnen"><i class="fa fa-hand-paper-o"></i> Anordnen</button>';
|
||||
print '<button type="button" class="button small btn-graph-save" id="btn-graph-save-positions" title="Positionen speichern" style="display:none;"><i class="fa fa-check"></i> Speichern</button>';
|
||||
print '<button type="button" class="button small btn-graph-cancel" id="btn-graph-cancel-edit" title="Abbrechen" style="display:none;"><i class="fa fa-times"></i> Abbrechen</button>';
|
||||
}
|
||||
print '</div>';
|
||||
print '</div>';
|
||||
}
|
||||
|
||||
// Add system form (hidden by default)
|
||||
if ($permissiontoadd && !empty($availableSystems)) {
|
||||
print '<div id="add-system-form" class="kundenkarte-add-system-form" style="display:none;margin-bottom:15px;">';
|
||||
|
|
@ -1043,15 +1067,35 @@ if (empty($customerSystems)) {
|
|||
$graphModuleUrl = dol_buildpath('/kundenkarte', 1);
|
||||
|
||||
print '<div class="kundenkarte-graph-wrapper">';
|
||||
print '<div class="kundenkarte-graph-search-floating">';
|
||||
print '<input type="text" id="kundenkarte-graph-search" placeholder="'.$langs->trans('SearchPlaceholder').'" autocomplete="off">';
|
||||
print '</div>';
|
||||
print '<div id="kundenkarte-graph-container"';
|
||||
print ' data-ajax-url="'.dol_escape_htmltag($graphAjaxUrl).'"';
|
||||
print ' data-save-url="'.dol_escape_htmltag($graphSaveUrl).'"';
|
||||
print ' data-module-url="'.dol_escape_htmltag($graphModuleUrl).'"';
|
||||
print ' data-socid="'.$id.'"';
|
||||
print ' data-systemid="'.$systemId.'"';
|
||||
print ' data-can-edit="'.($permissiontoadd ? '1' : '0').'"';
|
||||
print ' data-can-delete="'.($permissiontodelete ? '1' : '0').'"';
|
||||
print ' data-page-url="'.dol_escape_htmltag($_SERVER['PHP_SELF']).'"';
|
||||
print '>';
|
||||
print '<div class="kundenkarte-graph-loading"><i class="fa fa-spinner fa-spin"></i> '.$langs->trans('GraphLoading').'</div>';
|
||||
print '</div>';
|
||||
|
||||
// Kontextmenü (Rechtsklick auf Node)
|
||||
print '<div id="kundenkarte-graph-contextmenu" class="kundenkarte-graph-contextmenu" style="display:none;">';
|
||||
print '<a class="ctx-item ctx-view" data-action="view"><i class="fa fa-eye"></i> '.$langs->trans('View').'</a>';
|
||||
if ($permissiontoadd) {
|
||||
print '<a class="ctx-item ctx-add-child" data-action="add-child"><i class="fa fa-plus"></i> '.$langs->trans('AddChild').'</a>';
|
||||
print '<a class="ctx-item ctx-edit" data-action="edit"><i class="fa fa-edit"></i> '.$langs->trans('Edit').'</a>';
|
||||
print '<a class="ctx-item ctx-copy" data-action="copy"><i class="fa fa-copy"></i> '.$langs->trans('Copy').'</a>';
|
||||
}
|
||||
if ($permissiontodelete) {
|
||||
print '<a class="ctx-item ctx-delete" data-action="delete"><i class="fa fa-trash"></i> '.$langs->trans('Delete').'</a>';
|
||||
}
|
||||
print '</div>';
|
||||
|
||||
// Legende - wird dynamisch vom JS befüllt (Kabeltypen mit Farben)
|
||||
print '<div id="kundenkarte-graph-legend" class="kundenkarte-graph-legend"></div>';
|
||||
print '</div>';
|
||||
|
|
|
|||
Loading…
Reference in a new issue