Version 3.3.1 - Visuelle Kabel-Darstellung & Verbindungen

Neue Features:
- Kabelverbindungen zwischen Anlagen-Elementen dokumentieren
- Visuelle Baum-Darstellung mit parallelen vertikalen Linien
- Jedes Element mit eigenem Kabel bekommt eigene Linie
- Horizontale Verbindungslinien zum Element
- Automatische Abstände zwischen Kabel-Gruppen
- Kabeltypen (Medium Types) verwalten
- Gebäude-Typen für Anlagen-Struktur
- Tree-Display-Konfiguration pro System
- Audit-Log für Änderungsverfolgung

Verbesserungen:
- Erste Kabel-Linie rechts, letzte links (korrekte Reihenfolge)
- Horizontale Linien enden am Element-Rand
- Spacer-Zeilen für bessere Übersichtlichkeit
- BOM-Generator für Stücklisten (Prototyp)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-16 15:14:36 +01:00
parent 1360b061b7
commit 1f73e814ff
39 changed files with 6397 additions and 46 deletions

1
CLAUDE.md Normal file
View file

@ -0,0 +1 @@
CLAUDE_CODE_DISABLE_AUTO_MEMORY=0

View file

@ -16,6 +16,9 @@ Das KundenKarte-Modul erweitert Dolibarr um zwei wichtige Funktionen fuer Kunden
- Konfigurierbare Element-Typen mit individuellen Feldern
- Datei-Upload mit Bild-Vorschau und PDF-Anzeige
- Separate Verwaltung pro Kunde oder pro Kontakt/Adresse (z.B. verschiedene Gebaeude)
- Kabelverbindungen zwischen Anlagen-Elementen dokumentieren
- Visuelle Darstellung mit parallelen vertikalen Linien fuer jedes Kabel
- Automatische Gruppierung mit Abstaenden zwischen Kabel-Gruppen
### Verteilungsdokumentation (Schaltplan-Editor)
- Interaktiver SVG-basierter Schaltplan-Editor

View file

@ -37,15 +37,29 @@ if ($action == 'add') {
$color = GETPOST('color', 'alphanohtml');
$position = GETPOSTINT('position');
// Tree display config
$treeConfig = array(
'show_ref' => GETPOSTINT('show_ref'),
'show_label' => GETPOSTINT('show_label'),
'show_type' => GETPOSTINT('show_type'),
'show_icon' => GETPOSTINT('show_icon'),
'show_status' => GETPOSTINT('show_status'),
'show_fields' => GETPOSTINT('show_fields'),
'expand_default' => GETPOSTINT('expand_default'),
'indent_style' => GETPOST('indent_style', 'alpha') ?: 'lines'
);
$treeConfigJson = json_encode($treeConfig);
if (empty($code) || empty($label)) {
setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors');
} else {
$sql = "INSERT INTO ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system";
$sql .= " (code, label, picto, color, position, active, entity)";
$sql .= " (code, label, picto, color, position, active, entity, tree_display_config)";
$sql .= " VALUES ('".$db->escape(strtoupper($code))."', '".$db->escape($label)."',";
$sql .= " ".($picto ? "'".$db->escape($picto)."'" : "NULL").",";
$sql .= " ".($color ? "'".$db->escape($color)."'" : "NULL").",";
$sql .= " ".((int) $position).", 1, 0)";
$sql .= " ".((int) $position).", 1, 0,";
$sql .= " '".$db->escape($treeConfigJson)."')";
$result = $db->query($sql);
if ($result) {
@ -64,12 +78,26 @@ if ($action == 'update') {
$color = GETPOST('color', 'alphanohtml');
$position = GETPOSTINT('position');
// Tree display config
$treeConfig = array(
'show_ref' => GETPOSTINT('show_ref'),
'show_label' => GETPOSTINT('show_label'),
'show_type' => GETPOSTINT('show_type'),
'show_icon' => GETPOSTINT('show_icon'),
'show_status' => GETPOSTINT('show_status'),
'show_fields' => GETPOSTINT('show_fields'),
'expand_default' => GETPOSTINT('expand_default'),
'indent_style' => GETPOST('indent_style', 'alpha') ?: 'lines'
);
$treeConfigJson = json_encode($treeConfig);
$sql = "UPDATE ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system SET";
$sql .= " code = '".$db->escape(strtoupper($code))."'";
$sql .= ", label = '".$db->escape($label)."'";
$sql .= ", picto = ".($picto ? "'".$db->escape($picto)."'" : "NULL");
$sql .= ", color = ".($color ? "'".$db->escape($color)."'" : "NULL");
$sql .= ", position = ".((int) $position);
$sql .= ", tree_display_config = '".$db->escape($treeConfigJson)."'";
$sql .= " WHERE rowid = ".((int) $systemId);
$result = $db->query($sql);
@ -186,6 +214,73 @@ if ($action == 'create' || $action == 'edit') {
print '</table>';
// Tree display configuration
print '<br><h3>'.$langs->trans('TreeDisplayConfig').'</h3>';
// Parse existing config
$treeConfig = array(
'show_ref' => 1,
'show_label' => 1,
'show_type' => 1,
'show_icon' => 1,
'show_status' => 1,
'show_fields' => 0,
'expand_default' => 1,
'indent_style' => 'lines'
);
if ($system && !empty($system->tree_display_config)) {
$savedConfig = json_decode($system->tree_display_config, true);
if (is_array($savedConfig)) {
$treeConfig = array_merge($treeConfig, $savedConfig);
}
}
print '<table class="border centpercent">';
print '<tr><td class="titlefield">'.$langs->trans('TreeShowRef').'</td>';
print '<td><input type="checkbox" name="show_ref" value="1"'.($treeConfig['show_ref'] ? ' checked' : '').'> ';
print '<span class="opacitymedium">'.$langs->trans('TreeShowRefHelp').'</span></td></tr>';
print '<tr><td>'.$langs->trans('TreeShowLabel').'</td>';
print '<td><input type="checkbox" name="show_label" value="1"'.($treeConfig['show_label'] ? ' checked' : '').'> ';
print '<span class="opacitymedium">'.$langs->trans('TreeShowLabelHelp').'</span></td></tr>';
print '<tr><td>'.$langs->trans('TreeShowType').'</td>';
print '<td><input type="checkbox" name="show_type" value="1"'.($treeConfig['show_type'] ? ' checked' : '').'> ';
print '<span class="opacitymedium">'.$langs->trans('TreeShowTypeHelp').'</span></td></tr>';
print '<tr><td>'.$langs->trans('TreeShowIcon').'</td>';
print '<td><input type="checkbox" name="show_icon" value="1"'.($treeConfig['show_icon'] ? ' checked' : '').'> ';
print '<span class="opacitymedium">'.$langs->trans('TreeShowIconHelp').'</span></td></tr>';
print '<tr><td>'.$langs->trans('TreeShowStatus').'</td>';
print '<td><input type="checkbox" name="show_status" value="1"'.($treeConfig['show_status'] ? ' checked' : '').'> ';
print '<span class="opacitymedium">'.$langs->trans('TreeShowStatusHelp').'</span></td></tr>';
print '<tr><td>'.$langs->trans('TreeShowFields').'</td>';
print '<td><input type="checkbox" name="show_fields" value="1"'.($treeConfig['show_fields'] ? ' checked' : '').'> ';
print '<span class="opacitymedium">'.$langs->trans('TreeShowFieldsHelp').'</span></td></tr>';
print '<tr><td>'.$langs->trans('TreeExpandDefault').'</td>';
print '<td><input type="checkbox" name="expand_default" value="1"'.($treeConfig['expand_default'] ? ' checked' : '').'> ';
print '<span class="opacitymedium">'.$langs->trans('TreeExpandDefaultHelp').'</span></td></tr>';
print '<tr><td>'.$langs->trans('TreeIndentStyle').'</td>';
print '<td><select name="indent_style" class="flat">';
$styles = array(
'lines' => $langs->trans('TreeIndentLines'),
'dots' => $langs->trans('TreeIndentDots'),
'arrows' => $langs->trans('TreeIndentArrows'),
'simple' => $langs->trans('TreeIndentSimple')
);
foreach ($styles as $code => $label) {
$selected = ($treeConfig['indent_style'] == $code) ? ' selected' : '';
print '<option value="'.$code.'"'.$selected.'>'.$label.'</option>';
}
print '</select></td></tr>';
print '</table>';
print '<div class="center" style="margin-top:20px;">';
print '<button type="submit" class="button">'.$langs->trans('Save').'</button>';
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'">'.$langs->trans('Cancel').'</a>';

View file

@ -327,7 +327,8 @@ if (in_array($action, array('create', 'edit'))) {
}
// Get all types for parent selection
$allTypes = $anlageType->fetchAllBySystem(0, 0);
// We need to filter by the same system OR show all types if this type is for all systems
$allTypes = $anlageType->fetchAllBySystem(0, 0); // Get all types, we'll filter in the template
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
@ -393,13 +394,43 @@ if (in_array($action, array('create', 'edit'))) {
print '<div class="kundenkarte-parent-types-selector">';
// Select dropdown with add button
// Get current type's system for filtering (when editing)
$currentTypeSystem = ($action == 'edit') ? $anlageType->fk_system : GETPOSTINT('fk_system');
print '<div style="display:flex;gap:5px;margin-bottom:10px;align-items:center;">';
print '<select id="parent_type_select" class="flat" style="height:30px;">';
print '<option value="">'.$langs->trans('SelectType').'</option>';
foreach ($allTypes as $t) {
// Don't show current type in list (can't be parent of itself unless can_be_nested)
if ($action == 'edit' && $t->id == $typeId) continue;
print '<option value="'.dol_escape_htmltag($t->ref).'" data-label="'.dol_escape_htmltag($t->label).'">'.dol_escape_htmltag($t->ref).' - '.dol_escape_htmltag($t->label).'</option>';
// Filter by system: Show type if:
// 1. Current type is for all systems (fk_system = 0) - show all parent types
// 2. Parent type is for all systems (fk_system = 0) - always available
// 3. Parent type has same system as current type
$showType = false;
if (empty($currentTypeSystem)) {
// Current type is for all systems - show all possible parent types
$showType = true;
} elseif (empty($t->fk_system)) {
// Parent type is global (all systems) - always show
$showType = true;
} elseif ($t->fk_system == $currentTypeSystem) {
// Same system
$showType = true;
}
if (!$showType) continue;
// Add system indicator for clarity
$systemHint = '';
if (empty($t->fk_system)) {
$systemHint = ' ['.$langs->trans('AllSystems').']';
} elseif (isset($systems[$t->fk_system])) {
$systemHint = ' ['.$systems[$t->fk_system]->label.']';
}
print '<option value="'.dol_escape_htmltag($t->ref).'" data-label="'.dol_escape_htmltag($t->label).'" data-system="'.$t->fk_system.'">'.dol_escape_htmltag($t->ref).' - '.dol_escape_htmltag($t->label).$systemHint.'</option>';
}
print '</select>';
print '<button type="button" class="button" id="add_parent_type_btn"><i class="fa fa-plus"></i> '.$langs->trans('Add').'</button>';
@ -748,7 +779,45 @@ $(document).ready(function() {
$select.val("");
});
// Filter parent type options when system changes
$("select[name=fk_system]").on("change", function() {
var selectedSystem = $(this).val();
$select.find("option").each(function() {
var optSystem = $(this).data("system");
if ($(this).val() === "") {
// Keep the placeholder option visible
return;
}
// Show option if:
// 1. Selected system is 0 (all systems) - show all options
// 2. Option system is 0 (global type) - always show
// 3. Option system matches selected system
var show = false;
if (selectedSystem == "0" || selectedSystem === "") {
show = true;
} else if (optSystem == 0 || optSystem === "" || optSystem === undefined) {
show = true;
} else if (optSystem == selectedSystem) {
show = true;
}
if (show) {
$(this).show();
} else {
$(this).hide();
// Deselect if hidden
if ($(this).is(":selected")) {
$select.val("");
}
}
});
});
initSelected();
// Trigger initial filtering
$("select[name=fk_system]").trigger("change");
});
</script>';

351
admin/building_types.php Normal file
View file

@ -0,0 +1,351 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* Admin page for Building Types (Gebäudetypen)
*/
// Load Dolibarr environment
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/html.formadmin.class.php';
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
dol_include_once('/kundenkarte/class/buildingtype.class.php');
// Load translation files
$langs->loadLangs(array('admin', 'kundenkarte@kundenkarte'));
// Security check
if (!$user->admin) {
accessforbidden();
}
$action = GETPOST('action', 'aZ09');
$confirm = GETPOST('confirm', 'alpha');
$id = GETPOSTINT('id');
$levelFilter = GETPOST('level_filter', 'alpha');
$buildingType = new BuildingType($db);
$error = 0;
// Actions
if ($action == 'add' && $user->admin) {
$buildingType->ref = GETPOST('ref', 'alphanohtml');
$buildingType->label = GETPOST('label', 'alphanohtml');
$buildingType->label_short = GETPOST('label_short', 'alphanohtml');
$buildingType->description = GETPOST('description', 'restricthtml');
$buildingType->fk_parent = GETPOSTINT('fk_parent');
$buildingType->level_type = GETPOST('level_type', 'alpha');
$buildingType->icon = GETPOST('icon', 'alphanohtml');
$buildingType->color = GETPOST('color', 'alphanohtml');
$buildingType->can_have_children = GETPOSTINT('can_have_children');
$buildingType->position = GETPOSTINT('position');
$buildingType->active = GETPOSTINT('active');
if (empty($buildingType->ref)) {
setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentitiesaliases('Ref')), null, 'errors');
$error++;
}
if (empty($buildingType->label)) {
setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentitiesaliases('Label')), null, 'errors');
$error++;
}
if (!$error) {
$result = $buildingType->create($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordCreatedSuccessfully'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
} else {
setEventMessages($buildingType->error, $buildingType->errors, 'errors');
}
}
$action = 'create';
}
if ($action == 'update' && $user->admin) {
$result = $buildingType->fetch($id);
if ($result > 0) {
// Don't allow editing ref of system types
if (!$buildingType->is_system) {
$buildingType->ref = GETPOST('ref', 'alphanohtml');
}
$buildingType->label = GETPOST('label', 'alphanohtml');
$buildingType->label_short = GETPOST('label_short', 'alphanohtml');
$buildingType->description = GETPOST('description', 'restricthtml');
$buildingType->fk_parent = GETPOSTINT('fk_parent');
$buildingType->level_type = GETPOST('level_type', 'alpha');
$buildingType->icon = GETPOST('icon', 'alphanohtml');
$buildingType->color = GETPOST('color', 'alphanohtml');
$buildingType->can_have_children = GETPOSTINT('can_have_children');
$buildingType->position = GETPOSTINT('position');
$buildingType->active = GETPOSTINT('active');
$result = $buildingType->update($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordModifiedSuccessfully'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
} else {
setEventMessages($buildingType->error, $buildingType->errors, 'errors');
}
}
}
if ($action == 'confirm_delete' && $confirm == 'yes' && $user->admin) {
$result = $buildingType->fetch($id);
if ($result > 0) {
$result = $buildingType->delete($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
} else {
setEventMessages($langs->trans($buildingType->error), $buildingType->errors, 'errors');
}
}
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
// Load data for edit
if (($action == 'edit' || $action == 'delete') && $id > 0) {
$result = $buildingType->fetch($id);
}
/*
* View
*/
$page_name = "BuildingTypesSetup";
llxHeader('', $langs->trans($page_name), '', '', 0, 0, '', '', '', 'mod-kundenkarte page-admin-building_types');
$linkback = '<a href="'.DOL_URL_ROOT.'/admin/modules.php?restore_lastsearch_values=1">'.$langs->trans("BackToModuleList").'</a>';
print load_fiche_titre($langs->trans($page_name), $linkback, 'object_kundenkarte@kundenkarte');
print '<div class="fichecenter">';
$head = kundenkarteAdminPrepareHead();
print dol_get_fiche_head($head, 'building_types', $langs->trans("Module500015Name"), -1, 'kundenkarte@kundenkarte');
// Delete confirmation
if ($action == 'delete') {
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?id='.$buildingType->id,
$langs->trans('DeleteBuildingType'),
$langs->trans('ConfirmDeleteBuildingType', $buildingType->label),
'confirm_delete',
'',
0,
1
);
}
// Level type filter
$levelTypes = BuildingType::getLevelTypes();
print '<div class="div-table-responsive-no-min">';
print '<form method="get" action="'.$_SERVER['PHP_SELF'].'">';
print '<div class="inline-block valignmiddle" style="margin-bottom: 10px;">';
print '<label for="level_filter">'.$langs->trans('FilterByLevel').': </label>';
print '<select name="level_filter" id="level_filter" class="flat minwidth200" onchange="this.form.submit()">';
print '<option value="">'.$langs->trans('All').'</option>';
foreach ($levelTypes as $code => $label) {
$selected = ($levelFilter == $code) ? ' selected' : '';
print '<option value="'.$code.'"'.$selected.'>'.$label.'</option>';
}
print '</select>';
print '</div>';
print '</form>';
print '</div>';
// Add/Edit form
if ($action == 'create' || $action == 'edit') {
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="'.($action == 'edit' ? 'update' : 'add').'">';
if ($action == 'edit') {
print '<input type="hidden" name="id" value="'.$buildingType->id.'">';
}
print '<table class="border centpercent tableforfield">';
// Ref
print '<tr><td class="titlefieldcreate fieldrequired">'.$langs->trans('Ref').'</td><td>';
if ($action == 'edit' && $buildingType->is_system) {
print '<input type="hidden" name="ref" value="'.$buildingType->ref.'">';
print $buildingType->ref.' <span class="opacitymedium">('.$langs->trans('SystemType').')</span>';
} else {
print '<input type="text" name="ref" class="flat minwidth200" value="'.($buildingType->ref ?: '').'" maxlength="50">';
}
print '</td></tr>';
// Label
print '<tr><td class="fieldrequired">'.$langs->trans('Label').'</td><td>';
print '<input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($buildingType->label ?: '').'">';
print '</td></tr>';
// Label Short
print '<tr><td>'.$langs->trans('LabelShort').'</td><td>';
print '<input type="text" name="label_short" class="flat minwidth150" value="'.dol_escape_htmltag($buildingType->label_short ?: '').'" maxlength="32">';
print '</td></tr>';
// Level Type
print '<tr><td>'.$langs->trans('LevelType').'</td><td>';
print '<select name="level_type" class="flat minwidth200">';
print '<option value="">--</option>';
foreach ($levelTypes as $code => $label) {
$selected = ($buildingType->level_type == $code) ? ' selected' : '';
print '<option value="'.$code.'"'.$selected.'>'.$label.'</option>';
}
print '</select>';
print '</td></tr>';
// Icon
print '<tr><td>'.$langs->trans('Icon').'</td><td>';
print '<input type="text" name="icon" class="flat minwidth200" value="'.dol_escape_htmltag($buildingType->icon ?: '').'" placeholder="fa-home">';
if ($buildingType->icon) {
print ' <i class="fas '.$buildingType->icon.'"></i>';
}
print ' <span class="opacitymedium">(FontAwesome, z.B. fa-home, fa-building)</span>';
print '</td></tr>';
// Color
print '<tr><td>'.$langs->trans('Color').'</td><td>';
print '<input type="color" name="color" id="color_picker" value="'.($buildingType->color ?: '#3498db').'" style="width:50px;height:30px;vertical-align:middle;">';
print ' <input type="text" name="color_text" id="color_text" class="flat" value="'.($buildingType->color ?: '#3498db').'" size="10" onchange="document.getElementById(\'color_picker\').value=this.value;">';
print '</td></tr>';
// Can have children
print '<tr><td>'.$langs->trans('CanHaveChildren').'</td><td>';
print '<input type="checkbox" name="can_have_children" value="1"'.($buildingType->can_have_children || $action != 'edit' ? ' checked' : '').'>';
print '</td></tr>';
// Position
print '<tr><td>'.$langs->trans('Position').'</td><td>';
$defaultPos = $action == 'create' ? $buildingType->getNextPosition() : $buildingType->position;
print '<input type="number" name="position" class="flat" value="'.(int)$defaultPos.'" min="0" step="10">';
print '</td></tr>';
// Active
print '<tr><td>'.$langs->trans('Active').'</td><td>';
print '<input type="checkbox" name="active" value="1"'.($buildingType->active || $action != 'edit' ? ' checked' : '').'>';
print '</td></tr>';
// Description
print '<tr><td>'.$langs->trans('Description').'</td><td>';
print '<textarea name="description" class="flat" rows="3" cols="60">'.$buildingType->description.'</textarea>';
print '</td></tr>';
print '</table>';
print '<div class="center" style="margin-top: 10px;">';
print '<input type="submit" class="button button-save" value="'.$langs->trans('Save').'">';
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'">'.$langs->trans('Cancel').'</a>';
print '</div>';
print '</form>';
// Sync color inputs
print '<script>
document.getElementById("color_picker").addEventListener("input", function() {
document.getElementById("color_text").value = this.value;
});
</script>';
} else {
// List of building types
print '<div class="div-table-responsive-no-min">';
print '<table class="noborder centpercent">';
// Header
print '<tr class="liste_titre">';
print '<td>'.$langs->trans('Ref').'</td>';
print '<td>'.$langs->trans('Label').'</td>';
print '<td>'.$langs->trans('LevelType').'</td>';
print '<td class="center">'.$langs->trans('Icon').'</td>';
print '<td class="center">'.$langs->trans('Color').'</td>';
print '<td class="center">'.$langs->trans('Position').'</td>';
print '<td class="center">'.$langs->trans('Active').'</td>';
print '<td class="center">'.$langs->trans('Actions').'</td>';
print '</tr>';
// Fetch types
$types = $buildingType->fetchAll(0, $levelFilter);
if (count($types) > 0) {
foreach ($types as $type) {
print '<tr class="oddeven">';
// Ref
print '<td>'.$type->ref;
if ($type->is_system) {
print ' <span class="badge badge-secondary">'.$langs->trans('System').'</span>';
}
print '</td>';
// Label
print '<td>';
if ($type->icon) {
print '<i class="fas '.$type->icon.'" style="color:'.$type->color.';margin-right:5px;"></i> ';
}
print dol_escape_htmltag($type->label);
if ($type->label_short) {
print ' <span class="opacitymedium">('.$type->label_short.')</span>';
}
print '</td>';
// Level Type
print '<td>'.$type->getLevelTypeLabel().'</td>';
// Icon
print '<td class="center">';
if ($type->icon) {
print '<i class="fas '.$type->icon.'"></i> '.$type->icon;
}
print '</td>';
// Color
print '<td class="center">';
if ($type->color) {
print '<span style="display:inline-block;width:20px;height:20px;background:'.$type->color.';border:1px solid #ccc;border-radius:3px;vertical-align:middle;"></span> '.$type->color;
}
print '</td>';
// Position
print '<td class="center">'.$type->position.'</td>';
// Active
print '<td class="center">';
print $type->active ? '<span class="badge badge-status4">'.$langs->trans('Yes').'</span>' : '<span class="badge badge-status5">'.$langs->trans('No').'</span>';
print '</td>';
// Actions
print '<td class="center nowrap">';
print '<a class="editfielda" href="'.$_SERVER['PHP_SELF'].'?action=edit&id='.$type->id.'">'.img_edit().'</a>';
if (!$type->is_system) {
print ' <a class="deletefielda" href="'.$_SERVER['PHP_SELF'].'?action=delete&id='.$type->id.'">'.img_delete().'</a>';
}
print '</td>';
print '</tr>';
}
} else {
print '<tr class="oddeven"><td colspan="8" class="opacitymedium">'.$langs->trans('NoRecordFound').'</td></tr>';
}
print '</table>';
print '</div>';
// Add button
print '<div class="tabsAction">';
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?action=create">'.$langs->trans('AddBuildingType').'</a>';
print '</div>';
}
print dol_get_fiche_end();
print '</div>';
llxFooter();
$db->close();

332
admin/medium_types.php Normal file
View file

@ -0,0 +1,332 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* Admin page for managing Medium Types (Kabeltypen)
*/
// Load Dolibarr environment
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
dol_include_once('/kundenkarte/class/mediumtype.class.php');
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
$langs->loadLangs(array("admin", "kundenkarte@kundenkarte"));
// Security check
if (!$user->admin) {
accessforbidden();
}
$action = GETPOST('action', 'aZ09');
$typeId = GETPOSTINT('typeid');
$mediumType = new MediumType($db);
// Load systems for dropdown
$systems = array();
$sql = "SELECT rowid, code, label FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position, label";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$systems[$obj->rowid] = $obj;
}
}
$error = 0;
$message = '';
// Actions
if ($action == 'add' && $user->admin) {
$mediumType->ref = GETPOST('ref', 'alphanohtml');
$mediumType->label = GETPOST('label', 'alphanohtml');
$mediumType->label_short = GETPOST('label_short', 'alphanohtml');
$mediumType->description = GETPOST('description', 'restricthtml');
$mediumType->fk_system = GETPOSTINT('fk_system');
$mediumType->category = GETPOST('category', 'alphanohtml');
$mediumType->default_spec = GETPOST('default_spec', 'alphanohtml');
$mediumType->color = GETPOST('color', 'alphanohtml');
$mediumType->position = GETPOSTINT('position');
$mediumType->active = GETPOSTINT('active');
// Available specs as JSON array
$specsText = GETPOST('available_specs', 'nohtml');
if ($specsText) {
$specsArray = array_map('trim', explode(',', $specsText));
$mediumType->available_specs = json_encode($specsArray);
}
$result = $mediumType->create($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
} else {
setEventMessages($mediumType->error, $mediumType->errors, 'errors');
$action = 'create';
}
}
if ($action == 'update' && $user->admin) {
if ($mediumType->fetch($typeId) > 0) {
$mediumType->ref = GETPOST('ref', 'alphanohtml');
$mediumType->label = GETPOST('label', 'alphanohtml');
$mediumType->label_short = GETPOST('label_short', 'alphanohtml');
$mediumType->description = GETPOST('description', 'restricthtml');
$mediumType->fk_system = GETPOSTINT('fk_system');
$mediumType->category = GETPOST('category', 'alphanohtml');
$mediumType->default_spec = GETPOST('default_spec', 'alphanohtml');
$mediumType->color = GETPOST('color', 'alphanohtml');
$mediumType->position = GETPOSTINT('position');
$mediumType->active = GETPOSTINT('active');
$specsText = GETPOST('available_specs', 'nohtml');
if ($specsText) {
$specsArray = array_map('trim', explode(',', $specsText));
$mediumType->available_specs = json_encode($specsArray);
} else {
$mediumType->available_specs = '';
}
$result = $mediumType->update($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
} else {
setEventMessages($mediumType->error, $mediumType->errors, 'errors');
$action = 'edit';
}
}
}
if ($action == 'confirm_delete' && GETPOST('confirm') == 'yes' && $user->admin) {
if ($mediumType->fetch($typeId) > 0) {
$result = $mediumType->delete($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
} else {
setEventMessages($mediumType->error, $mediumType->errors, 'errors');
}
}
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
/*
* View
*/
$title = $langs->trans('MediumTypes');
llxHeader('', $title);
$linkback = '<a href="'.DOL_URL_ROOT.'/admin/modules.php?restore_lastsearch_values=1">'.$langs->trans("BackToModuleList").'</a>';
print load_fiche_titre($title, $linkback, 'title_setup');
// Admin tabs
$head = kundenkarteAdminPrepareHead();
print dol_get_fiche_head($head, 'medium_types', $langs->trans('KundenKarte'), -1, 'kundenkarte@kundenkarte');
// Delete confirmation
if ($action == 'delete') {
if ($mediumType->fetch($typeId) > 0) {
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?typeid='.$typeId,
$langs->trans('DeleteMediumType'),
$langs->trans('ConfirmDeleteMediumType', $mediumType->label),
'confirm_delete',
'',
0,
1
);
}
}
// Add/Edit form
if (in_array($action, array('create', 'edit'))) {
if ($action == 'edit' && $typeId > 0) {
$mediumType->fetch($typeId);
}
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="'.($action == 'edit' ? 'update' : 'add').'">';
if ($action == 'edit') {
print '<input type="hidden" name="typeid" value="'.$typeId.'">';
}
print '<table class="border centpercent">';
// Ref
print '<tr><td class="titlefieldcreate fieldrequired">'.$langs->trans('Ref').'</td>';
print '<td><input type="text" name="ref" value="'.dol_escape_htmltag($mediumType->ref ?: GETPOST('ref')).'" size="20" maxlength="50" required></td></tr>';
// Label
print '<tr><td class="fieldrequired">'.$langs->trans('Label').'</td>';
print '<td><input type="text" name="label" value="'.dol_escape_htmltag($mediumType->label ?: GETPOST('label')).'" size="40" maxlength="128" required></td></tr>';
// Label short
print '<tr><td>'.$langs->trans('LabelShort').'</td>';
print '<td><input type="text" name="label_short" value="'.dol_escape_htmltag($mediumType->label_short ?: GETPOST('label_short')).'" size="20" maxlength="32"></td></tr>';
// System
print '<tr><td>'.$langs->trans('System').'</td>';
print '<td><select name="fk_system" class="flat">';
print '<option value="0">'.$langs->trans('AllSystems').'</option>';
foreach ($systems as $sys) {
$selected = ($mediumType->fk_system == $sys->rowid) ? ' selected' : '';
print '<option value="'.$sys->rowid.'"'.$selected.'>'.dol_escape_htmltag($sys->label).'</option>';
}
print '</select></td></tr>';
// Category
print '<tr><td>'.$langs->trans('Category').'</td>';
print '<td><select name="category" class="flat">';
$categories = MediumType::getCategoryOptions();
foreach ($categories as $code => $label) {
$selected = ($mediumType->category == $code) ? ' selected' : '';
print '<option value="'.$code.'"'.$selected.'>'.dol_escape_htmltag($label).'</option>';
}
print '</select></td></tr>';
// Default spec
print '<tr><td>'.$langs->trans('DefaultSpec').'</td>';
print '<td><input type="text" name="default_spec" value="'.dol_escape_htmltag($mediumType->default_spec ?: GETPOST('default_spec')).'" size="20" maxlength="100">';
print '<br><span class="opacitymedium">'.$langs->trans('DefaultSpecHelp').'</span></td></tr>';
// Available specs
$specsText = '';
if ($mediumType->available_specs) {
$specsArray = json_decode($mediumType->available_specs, true);
if (is_array($specsArray)) {
$specsText = implode(', ', $specsArray);
}
}
print '<tr><td>'.$langs->trans('AvailableSpecs').'</td>';
print '<td><input type="text" name="available_specs" value="'.dol_escape_htmltag($specsText ?: GETPOST('available_specs')).'" size="60">';
print '<br><span class="opacitymedium">'.$langs->trans('AvailableSpecsHelp').'</span></td></tr>';
// Color
print '<tr><td>'.$langs->trans('Color').'</td>';
print '<td><input type="color" name="color" value="'.dol_escape_htmltag($mediumType->color ?: '#666666').'" style="width:50px;height:30px;">';
print ' <input type="text" name="color_text" value="'.dol_escape_htmltag($mediumType->color ?: '#666666').'" size="10" onchange="this.form.color.value=this.value" placeholder="#RRGGBB"></td></tr>';
// Position
print '<tr><td>'.$langs->trans('Position').'</td>';
print '<td><input type="number" name="position" value="'.((int) ($mediumType->position ?: GETPOSTINT('position'))).'" min="0" max="999"></td></tr>';
// Status
print '<tr><td>'.$langs->trans('Status').'</td>';
print '<td><select name="active" class="flat">';
$activeValue = ($action == 'edit') ? $mediumType->active : 1;
print '<option value="1"'.($activeValue == 1 ? ' selected' : '').'>'.$langs->trans('Enabled').'</option>';
print '<option value="0"'.($activeValue == 0 ? ' selected' : '').'>'.$langs->trans('Disabled').'</option>';
print '</select></td></tr>';
// Description
print '<tr><td>'.$langs->trans('Description').'</td>';
print '<td><textarea name="description" rows="3" cols="60">'.dol_escape_htmltag($mediumType->description ?: GETPOST('description')).'</textarea></td></tr>';
print '</table>';
print '<div class="center" style="margin-top:15px;">';
print '<input type="submit" class="button button-save" value="'.$langs->trans('Save').'">';
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'">'.$langs->trans('Cancel').'</a>';
print '</div>';
print '</form>';
} else {
// List view
// Button to add
print '<div class="tabsAction">';
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?action=create">'.$langs->trans('AddMediumType').'</a>';
print '</div>';
// Filter by category
$filterCategory = GETPOST('filter_category', 'alphanohtml');
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'" style="margin-bottom:15px;">';
print '<label>'.$langs->trans('FilterByCategory').': </label>';
print '<select name="filter_category" class="flat" onchange="this.form.submit()">';
print '<option value="">'.$langs->trans('All').'</option>';
$categories = MediumType::getCategoryOptions();
foreach ($categories as $code => $label) {
$selected = ($filterCategory == $code) ? ' selected' : '';
print '<option value="'.$code.'"'.$selected.'>'.dol_escape_htmltag($label).'</option>';
}
print '</select>';
print '</form>';
// List
$allTypes = $mediumType->fetchAllBySystem(0, 0);
print '<table class="noborder centpercent">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('Ref').'</th>';
print '<th>'.$langs->trans('Label').'</th>';
print '<th>'.$langs->trans('Category').'</th>';
print '<th>'.$langs->trans('System').'</th>';
print '<th>'.$langs->trans('DefaultSpec').'</th>';
print '<th class="center">'.$langs->trans('Color').'</th>';
print '<th class="center">'.$langs->trans('Position').'</th>';
print '<th class="center">'.$langs->trans('Status').'</th>';
print '<th class="center">'.$langs->trans('Actions').'</th>';
print '</tr>';
if (empty($allTypes)) {
print '<tr><td colspan="9" class="opacitymedium">'.$langs->trans('NoRecords').'</td></tr>';
} else {
$i = 0;
foreach ($allTypes as $t) {
// Filter
if ($filterCategory && $t->category != $filterCategory) continue;
print '<tr class="oddeven">';
print '<td><strong>'.dol_escape_htmltag($t->ref).'</strong></td>';
print '<td>'.dol_escape_htmltag($t->label);
if ($t->label_short) print ' <span class="opacitymedium">('.dol_escape_htmltag($t->label_short).')</span>';
print '</td>';
print '<td>'.dol_escape_htmltag($t->getCategoryLabel()).'</td>';
print '<td>';
if ($t->fk_system > 0 && $t->system_label) {
print dol_escape_htmltag($t->system_label);
} else {
print '<em>'.$langs->trans('AllSystems').'</em>';
}
print '</td>';
print '<td>'.dol_escape_htmltag($t->default_spec).'</td>';
print '<td class="center"><span style="display:inline-block;width:20px;height:20px;background:'.dol_escape_htmltag($t->color ?: '#ccc').';border:1px solid #999;border-radius:3px;"></span></td>';
print '<td class="center">'.$t->position.'</td>';
print '<td class="center">';
print $t->active ? '<span class="badge badge-status4">'.$langs->trans('Enabled').'</span>' : '<span class="badge badge-status5">'.$langs->trans('Disabled').'</span>';
print '</td>';
print '<td class="center nowraponall">';
print '<a href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$t->id.'"><i class="fa fa-edit" title="'.$langs->trans('Edit').'"></i></a>';
if (!$t->is_system) {
print ' <a href="'.$_SERVER['PHP_SELF'].'?action=delete&typeid='.$t->id.'"><i class="fa fa-trash" style="color:#c00;" title="'.$langs->trans('Delete').'"></i></a>';
}
print '</td>';
print '</tr>';
$i++;
}
}
print '</table>';
}
print dol_get_fiche_end();
// JavaScript for color picker sync
print '<script>
document.querySelector("input[name=color]").addEventListener("input", function() {
document.querySelector("input[name=color_text]").value = this.value;
});
</script>';
llxFooter();
$db->close();

137
ajax/anlage.php Normal file
View file

@ -0,0 +1,137 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX endpoint for Anlage operations
*/
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
dol_include_once('/kundenkarte/class/anlage.class.php');
header('Content-Type: application/json; charset=UTF-8');
$langs->loadLangs(array('kundenkarte@kundenkarte'));
$action = GETPOST('action', 'aZ09');
$socId = GETPOSTINT('socid');
$contactId = GETPOSTINT('contactid');
$systemId = GETPOSTINT('system_id');
$anlageId = GETPOSTINT('anlage_id');
$response = array('success' => false, 'error' => '');
// Security check
if (!$user->hasRight('kundenkarte', 'read')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
echo json_encode($response);
exit;
}
$anlage = new Anlage($db);
// Helper function to convert tree objects to clean arrays
function treeToArray($nodes) {
$result = array();
foreach ($nodes as $node) {
$item = array(
'id' => $node->id,
'ref' => $node->ref,
'label' => $node->label,
'fk_parent' => $node->fk_parent,
'fk_system' => $node->fk_system,
'type_label' => $node->type_label,
'status' => $node->status
);
if (!empty($node->children)) {
$item['children'] = treeToArray($node->children);
} else {
$item['children'] = array();
}
$result[] = $item;
}
return $result;
}
switch ($action) {
case 'tree':
// Get tree structure for a customer/system
if ($socId > 0) {
if ($contactId > 0) {
$tree = $anlage->fetchTreeByContact($socId, $contactId, $systemId);
} else {
$tree = $anlage->fetchTree($socId, $systemId);
}
// Convert to clean array (removes db connection and other internal data)
$response['success'] = true;
$response['tree'] = treeToArray($tree);
} else {
$response['error'] = 'Missing socid';
}
break;
case 'list':
// Get flat list of anlagen for a customer/system (derived from tree)
if ($socId > 0) {
$tree = $anlage->fetchTree($socId, $systemId);
// Flatten tree to list
$result = array();
$flattenTree = function($nodes, $prefix = '') use (&$flattenTree, &$result) {
foreach ($nodes as $node) {
$result[] = array(
'id' => $node->id,
'ref' => $node->ref,
'label' => $node->label,
'display_label' => $prefix . $node->label,
'fk_parent' => $node->fk_parent,
'type_label' => $node->type_label,
'status' => $node->status
);
if (!empty($node->children)) {
$flattenTree($node->children, $prefix . ' ');
}
}
};
$flattenTree($tree);
$response['success'] = true;
$response['anlagen'] = $result;
} else {
$response['error'] = 'Missing socid';
}
break;
case 'get':
// Get single anlage
if ($anlageId > 0 && $anlage->fetch($anlageId) > 0) {
$response['success'] = true;
$response['anlage'] = array(
'id' => $anlage->id,
'ref' => $anlage->ref,
'label' => $anlage->label,
'fk_parent' => $anlage->fk_parent,
'fk_anlage_type' => $anlage->fk_anlage_type,
'type_label' => $anlage->type_label,
'fk_system' => $anlage->fk_system,
'status' => $anlage->status,
'field_values' => $anlage->getFieldValues()
);
} else {
$response['error'] = $langs->trans('ErrorRecordNotFound');
}
break;
default:
$response['error'] = 'Unknown action';
}
echo json_encode($response);

193
ajax/anlage_connection.php Normal file
View file

@ -0,0 +1,193 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX endpoint for anlage connections (Verbindungen zwischen Anlagen-Elementen)
*/
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
dol_include_once('/kundenkarte/class/anlageconnection.class.php');
dol_include_once('/kundenkarte/class/mediumtype.class.php');
header('Content-Type: application/json; charset=UTF-8');
$langs->loadLangs(array('kundenkarte@kundenkarte'));
$action = GETPOST('action', 'aZ09');
$connectionId = GETPOSTINT('connection_id');
$anlageId = GETPOSTINT('anlage_id');
$socId = GETPOSTINT('soc_id');
$systemId = GETPOSTINT('system_id');
$response = array('success' => false, 'error' => '');
// Security check
if (!$user->hasRight('kundenkarte', 'read')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
echo json_encode($response);
exit;
}
$connection = new AnlageConnection($db);
switch ($action) {
case 'list':
// List connections for an anlage or customer
if ($anlageId > 0) {
$connections = $connection->fetchByAnlage($anlageId);
} elseif ($socId > 0) {
$connections = $connection->fetchBySociete($socId, $systemId);
} else {
$response['error'] = 'Missing anlage_id or soc_id';
break;
}
$result = array();
foreach ($connections as $c) {
$result[] = array(
'id' => $c->id,
'fk_source' => $c->fk_source,
'source_label' => $c->source_label,
'source_ref' => $c->source_ref,
'fk_target' => $c->fk_target,
'target_label' => $c->target_label,
'target_ref' => $c->target_ref,
'label' => $c->label,
'fk_medium_type' => $c->fk_medium_type,
'medium_type_label' => $c->medium_type_label,
'medium_type_text' => $c->medium_type_text,
'medium_spec' => $c->medium_spec,
'medium_length' => $c->medium_length,
'medium_color' => $c->medium_color,
'route_description' => $c->route_description,
'installation_date' => $c->installation_date,
'status' => $c->status,
'display_label' => $c->getDisplayLabel()
);
}
$response['success'] = true;
$response['connections'] = $result;
break;
case 'get':
// Get single connection
if ($connectionId > 0 && $connection->fetch($connectionId) > 0) {
$response['success'] = true;
$response['connection'] = array(
'id' => $connection->id,
'fk_source' => $connection->fk_source,
'source_label' => $connection->source_label,
'source_ref' => $connection->source_ref,
'fk_target' => $connection->fk_target,
'target_label' => $connection->target_label,
'target_ref' => $connection->target_ref,
'label' => $connection->label,
'fk_medium_type' => $connection->fk_medium_type,
'medium_type_label' => $connection->medium_type_label,
'medium_type_text' => $connection->medium_type_text,
'medium_spec' => $connection->medium_spec,
'medium_length' => $connection->medium_length,
'medium_color' => $connection->medium_color,
'route_description' => $connection->route_description,
'installation_date' => $connection->installation_date,
'status' => $connection->status
);
} else {
$response['error'] = $langs->trans('ErrorRecordNotFound');
}
break;
case 'create':
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
$connection->fk_source = GETPOSTINT('fk_source');
$connection->fk_target = GETPOSTINT('fk_target');
$connection->label = GETPOST('label', 'alphanohtml');
$connection->fk_medium_type = GETPOSTINT('fk_medium_type');
$connection->medium_type_text = GETPOST('medium_type_text', 'alphanohtml');
$connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
$connection->medium_length = GETPOST('medium_length', 'alphanohtml');
$connection->medium_color = GETPOST('medium_color', 'alphanohtml');
$connection->route_description = GETPOST('route_description', 'restricthtml');
$connection->installation_date = GETPOST('installation_date', 'alpha');
$connection->status = 1;
if (empty($connection->fk_source) || empty($connection->fk_target)) {
$response['error'] = $langs->trans('ErrorFieldRequired', 'Source/Target');
break;
}
$result = $connection->create($user);
if ($result > 0) {
$response['success'] = true;
$response['connection_id'] = $result;
} else {
$response['error'] = $connection->error;
}
break;
case 'update':
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
if ($connection->fetch($connectionId) > 0) {
if (GETPOSTISSET('fk_source')) $connection->fk_source = GETPOSTINT('fk_source');
if (GETPOSTISSET('fk_target')) $connection->fk_target = GETPOSTINT('fk_target');
if (GETPOSTISSET('label')) $connection->label = GETPOST('label', 'alphanohtml');
if (GETPOSTISSET('fk_medium_type')) $connection->fk_medium_type = GETPOSTINT('fk_medium_type');
if (GETPOSTISSET('medium_type_text')) $connection->medium_type_text = GETPOST('medium_type_text', 'alphanohtml');
if (GETPOSTISSET('medium_spec')) $connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
if (GETPOSTISSET('medium_length')) $connection->medium_length = GETPOST('medium_length', 'alphanohtml');
if (GETPOSTISSET('medium_color')) $connection->medium_color = GETPOST('medium_color', 'alphanohtml');
if (GETPOSTISSET('route_description')) $connection->route_description = GETPOST('route_description', 'restricthtml');
if (GETPOSTISSET('installation_date')) $connection->installation_date = GETPOST('installation_date', 'alpha');
if (GETPOSTISSET('status')) $connection->status = GETPOSTINT('status');
$result = $connection->update($user);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = $connection->error;
}
} else {
$response['error'] = $langs->trans('ErrorRecordNotFound');
}
break;
case 'delete':
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
if ($connection->fetch($connectionId) > 0) {
$result = $connection->delete($user);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = $connection->error;
}
} else {
$response['error'] = $langs->trans('ErrorRecordNotFound');
}
break;
default:
$response['error'] = 'Unknown action';
}
echo json_encode($response);

153
ajax/audit_log.php Normal file
View file

@ -0,0 +1,153 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX endpoint for Audit Log
*/
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/core/lib/date.lib.php';
dol_include_once('/kundenkarte/class/auditlog.class.php');
header('Content-Type: application/json; charset=UTF-8');
$langs->loadLangs(array('kundenkarte@kundenkarte'));
$action = GETPOST('action', 'aZ09');
$objectType = GETPOST('object_type', 'aZ09');
$objectId = GETPOSTINT('object_id');
$anlageId = GETPOSTINT('anlage_id');
$socid = GETPOSTINT('socid');
$limit = GETPOSTINT('limit') ?: 50;
$response = array('success' => false, 'error' => '');
// Security check
if (!$user->hasRight('kundenkarte', 'read')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
echo json_encode($response);
exit;
}
$auditLog = new AuditLog($db);
switch ($action) {
case 'fetch_object':
// Fetch logs for a specific object
if (empty($objectType) || $objectId <= 0) {
$response['error'] = $langs->trans('ErrorMissingParameters');
break;
}
$logs = $auditLog->fetchByObject($objectType, $objectId, $limit);
$response['success'] = true;
$response['logs'] = array();
foreach ($logs as $log) {
$response['logs'][] = array(
'id' => $log->id,
'object_type' => $log->object_type,
'object_type_label' => $log->getObjectTypeLabel(),
'object_id' => $log->object_id,
'object_ref' => $log->object_ref,
'action' => $log->action,
'action_label' => $log->getActionLabel(),
'action_icon' => $log->getActionIcon(),
'action_color' => $log->getActionColor(),
'field_changed' => $log->field_changed,
'old_value' => $log->old_value,
'new_value' => $log->new_value,
'user_login' => $log->user_login,
'user_name' => $log->user_name ?: $log->user_login,
'date_action' => dol_print_date($log->date_action, 'dayhour'),
'timestamp' => $log->date_action,
'note' => $log->note
);
}
break;
case 'fetch_anlage':
// Fetch logs for an Anlage
if ($anlageId <= 0) {
$response['error'] = $langs->trans('ErrorMissingParameters');
break;
}
$logs = $auditLog->fetchByAnlage($anlageId, $limit);
$response['success'] = true;
$response['logs'] = array();
foreach ($logs as $log) {
$response['logs'][] = array(
'id' => $log->id,
'object_type' => $log->object_type,
'object_type_label' => $log->getObjectTypeLabel(),
'object_id' => $log->object_id,
'object_ref' => $log->object_ref,
'action' => $log->action,
'action_label' => $log->getActionLabel(),
'action_icon' => $log->getActionIcon(),
'action_color' => $log->getActionColor(),
'field_changed' => $log->field_changed,
'old_value' => $log->old_value,
'new_value' => $log->new_value,
'user_login' => $log->user_login,
'user_name' => $log->user_name ?: $log->user_login,
'date_action' => dol_print_date($log->date_action, 'dayhour'),
'timestamp' => $log->date_action,
'note' => $log->note
);
}
break;
case 'fetch_societe':
// Fetch logs for a customer
if ($socid <= 0) {
$response['error'] = $langs->trans('ErrorMissingParameters');
break;
}
$logs = $auditLog->fetchBySociete($socid, $limit);
$response['success'] = true;
$response['logs'] = array();
foreach ($logs as $log) {
$response['logs'][] = array(
'id' => $log->id,
'object_type' => $log->object_type,
'object_type_label' => $log->getObjectTypeLabel(),
'object_id' => $log->object_id,
'object_ref' => $log->object_ref,
'fk_anlage' => $log->fk_anlage,
'action' => $log->action,
'action_label' => $log->getActionLabel(),
'action_icon' => $log->getActionIcon(),
'action_color' => $log->getActionColor(),
'field_changed' => $log->field_changed,
'old_value' => $log->old_value,
'new_value' => $log->new_value,
'user_login' => $log->user_login,
'user_name' => $log->user_name ?: $log->user_login,
'date_action' => dol_print_date($log->date_action, 'dayhour'),
'timestamp' => $log->date_action,
'note' => $log->note
);
}
break;
default:
$response['error'] = 'Unknown action';
}
echo json_encode($response);

250
ajax/bom_generator.php Normal file
View file

@ -0,0 +1,250 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX endpoint for Bill of Materials (Stückliste) generation from schematic
*/
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
header('Content-Type: application/json; charset=UTF-8');
$langs->loadLangs(array('kundenkarte@kundenkarte', 'products'));
$action = GETPOST('action', 'aZ09');
$anlageId = GETPOSTINT('anlage_id');
$response = array('success' => false, 'error' => '');
// Security check
if (!$user->hasRight('kundenkarte', 'read')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
echo json_encode($response);
exit;
}
switch ($action) {
case 'generate':
// Generate BOM from all equipment in this installation (anlage)
if ($anlageId <= 0) {
$response['error'] = $langs->trans('ErrorRecordNotFound');
break;
}
// Get all equipment for this anlage through carriers and panels
$sql = "SELECT e.rowid as equipment_id, e.label as equipment_label, e.width_te, e.fk_product,";
$sql .= " et.rowid as type_id, et.ref as type_ref, et.label as type_label, et.fk_product as type_product,";
$sql .= " p.rowid as product_id, p.ref as product_ref, p.label as product_label, p.price, p.tva_tx,";
$sql .= " c.label as carrier_label, c.rowid as carrier_id,";
$sql .= " pan.label as panel_label, pan.rowid as panel_id";
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment e";
$sql .= " JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_carrier c ON e.fk_carrier = c.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_panel pan ON c.fk_panel = pan.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_type et ON e.fk_equipment_type = et.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON COALESCE(e.fk_product, et.fk_product) = p.rowid";
$sql .= " WHERE (pan.fk_anlage = ".((int) $anlageId)." OR c.fk_anlage = ".((int) $anlageId).")";
$sql .= " AND e.status = 1";
$sql .= " ORDER BY pan.position ASC, c.position ASC, e.position_te ASC";
$resql = $db->query($sql);
if (!$resql) {
$response['error'] = $db->lasterror();
break;
}
$items = array();
$summary = array(); // Grouped by product
while ($obj = $db->fetch_object($resql)) {
$item = array(
'equipment_id' => $obj->equipment_id,
'equipment_label' => $obj->equipment_label ?: $obj->type_label,
'type_ref' => $obj->type_ref,
'type_label' => $obj->type_label,
'width_te' => $obj->width_te,
'carrier_label' => $obj->carrier_label,
'panel_label' => $obj->panel_label,
'product_id' => $obj->product_id,
'product_ref' => $obj->product_ref,
'product_label' => $obj->product_label,
'price' => $obj->price,
'tva_tx' => $obj->tva_tx
);
$items[] = $item;
// Group by product for summary
if ($obj->product_id) {
$key = $obj->product_id;
if (!isset($summary[$key])) {
$summary[$key] = array(
'product_id' => $obj->product_id,
'product_ref' => $obj->product_ref,
'product_label' => $obj->product_label,
'price' => $obj->price,
'tva_tx' => $obj->tva_tx,
'quantity' => 0,
'total' => 0
);
}
$summary[$key]['quantity']++;
$summary[$key]['total'] = $summary[$key]['quantity'] * $summary[$key]['price'];
} else {
// Group by type if no product linked
$key = 'type_'.$obj->type_id;
if (!isset($summary[$key])) {
$summary[$key] = array(
'product_id' => null,
'product_ref' => $obj->type_ref,
'product_label' => $obj->type_label.' (kein Produkt)',
'price' => 0,
'tva_tx' => 0,
'quantity' => 0,
'total' => 0
);
}
$summary[$key]['quantity']++;
}
}
$db->free($resql);
// Also include busbar types (connections with is_rail = 1)
$sql = "SELECT conn.rowid as connection_id, conn.rail_phases, conn.rail_start_te, conn.rail_end_te,";
$sql .= " bt.rowid as busbar_type_id, bt.ref as busbar_ref, bt.label as busbar_label, bt.fk_product,";
$sql .= " p.rowid as product_id, p.ref as product_ref, p.label as product_label, p.price, p.tva_tx,";
$sql .= " c.label as carrier_label, pan.label as panel_label";
$sql .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection conn";
$sql .= " JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_carrier c ON conn.fk_carrier = c.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_panel pan ON c.fk_panel = pan.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_busbar_type bt ON conn.fk_busbar_type = bt.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON bt.fk_product = p.rowid";
$sql .= " WHERE (pan.fk_anlage = ".((int) $anlageId)." OR c.fk_anlage = ".((int) $anlageId).")";
$sql .= " AND conn.is_rail = 1";
$sql .= " AND conn.status = 1";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
// Calculate busbar length in TE
$lengthTE = max(1, intval($obj->rail_end_te) - intval($obj->rail_start_te) + 1);
$item = array(
'equipment_id' => 'busbar_'.$obj->connection_id,
'equipment_label' => $obj->busbar_label ?: 'Sammelschiene '.$obj->rail_phases,
'type_ref' => $obj->busbar_ref ?: 'BUSBAR',
'type_label' => 'Sammelschiene',
'width_te' => $lengthTE,
'carrier_label' => $obj->carrier_label,
'panel_label' => $obj->panel_label,
'product_id' => $obj->product_id,
'product_ref' => $obj->product_ref,
'product_label' => $obj->product_label,
'price' => $obj->price,
'tva_tx' => $obj->tva_tx
);
$items[] = $item;
// Add to summary
if ($obj->product_id) {
$key = $obj->product_id;
if (!isset($summary[$key])) {
$summary[$key] = array(
'product_id' => $obj->product_id,
'product_ref' => $obj->product_ref,
'product_label' => $obj->product_label,
'price' => $obj->price,
'tva_tx' => $obj->tva_tx,
'quantity' => 0,
'total' => 0
);
}
$summary[$key]['quantity']++;
$summary[$key]['total'] = $summary[$key]['quantity'] * $summary[$key]['price'];
}
}
$db->free($resql);
}
// Calculate totals
$totalQuantity = 0;
$totalPrice = 0;
foreach ($summary as $s) {
$totalQuantity += $s['quantity'];
$totalPrice += $s['total'];
}
$response['success'] = true;
$response['items'] = $items;
$response['summary'] = array_values($summary);
$response['total_quantity'] = $totalQuantity;
$response['total_price'] = $totalPrice;
break;
case 'create_order':
// Create a Dolibarr order from the BOM
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
$socid = GETPOSTINT('socid');
$productData = GETPOST('products', 'array');
if ($socid <= 0 || empty($productData)) {
$response['error'] = $langs->trans('ErrorMissingParameters');
break;
}
require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
$order = new Commande($db);
$order->socid = $socid;
$order->date_commande = dol_now();
$order->note_private = 'Generiert aus Schaltplan-Stückliste';
$order->source = 1; // Web
$result = $order->create($user);
if ($result <= 0) {
$response['error'] = $order->error ?: 'Fehler beim Erstellen der Bestellung';
break;
}
// Add lines
$lineErrors = 0;
foreach ($productData as $prod) {
$productId = intval($prod['product_id']);
$qty = floatval($prod['quantity']);
if ($productId <= 0 || $qty <= 0) continue;
$result = $order->addline(
'', // Description (auto from product)
0, // Unit price (auto from product)
$qty,
0, // TVA rate (auto)
0, 0, // Remise
$productId
);
if ($result < 0) {
$lineErrors++;
}
}
$response['success'] = true;
$response['order_id'] = $order->id;
$response['order_ref'] = $order->ref;
$response['line_errors'] = $lineErrors;
break;
default:
$response['error'] = 'Unknown action';
}
echo json_encode($response);

121
ajax/building_types.php Normal file
View file

@ -0,0 +1,121 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX endpoint for building types (Gebäudetypen)
*/
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
dol_include_once('/kundenkarte/class/buildingtype.class.php');
header('Content-Type: application/json; charset=UTF-8');
$langs->loadLangs(array('kundenkarte@kundenkarte'));
$action = GETPOST('action', 'aZ09');
$levelType = GETPOST('level_type', 'alpha');
$response = array('success' => false, 'error' => '');
// Security check
if (!$user->hasRight('kundenkarte', 'read')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
echo json_encode($response);
exit;
}
$buildingType = new BuildingType($db);
switch ($action) {
case 'list':
// Get all building types
$types = $buildingType->fetchAll(1, $levelType);
$result = array();
foreach ($types as $t) {
$result[] = array(
'id' => $t->id,
'ref' => $t->ref,
'label' => $t->label,
'label_short' => $t->label_short,
'level_type' => $t->level_type,
'level_type_label' => $t->getLevelTypeLabel(),
'icon' => $t->icon,
'color' => $t->color,
'can_have_children' => $t->can_have_children,
'is_system' => $t->is_system
);
}
$response['success'] = true;
$response['types'] = $result;
break;
case 'list_grouped':
// Get types grouped by level
$grouped = $buildingType->fetchGroupedByLevel(1);
$result = array();
$levelTypes = BuildingType::getLevelTypes();
foreach ($grouped as $level => $types) {
$levelLabel = isset($levelTypes[$level]) ? $levelTypes[$level] : $level;
$levelTypes_data = array();
foreach ($types as $t) {
$levelTypes_data[] = array(
'id' => $t->id,
'ref' => $t->ref,
'label' => $t->label,
'label_short' => $t->label_short,
'icon' => $t->icon,
'color' => $t->color,
'can_have_children' => $t->can_have_children
);
}
$result[] = array(
'level_type' => $level,
'level_type_label' => $levelLabel,
'types' => $levelTypes_data
);
}
$response['success'] = true;
$response['groups'] = $result;
break;
case 'get':
// Get single type details
$typeId = GETPOSTINT('type_id');
if ($typeId > 0 && $buildingType->fetch($typeId) > 0) {
$response['success'] = true;
$response['type'] = array(
'id' => $buildingType->id,
'ref' => $buildingType->ref,
'label' => $buildingType->label,
'label_short' => $buildingType->label_short,
'description' => $buildingType->description,
'level_type' => $buildingType->level_type,
'level_type_label' => $buildingType->getLevelTypeLabel(),
'icon' => $buildingType->icon,
'color' => $buildingType->color,
'can_have_children' => $buildingType->can_have_children,
'is_system' => $buildingType->is_system
);
} else {
$response['error'] = $langs->trans('ErrorRecordNotFound');
}
break;
default:
$response['error'] = 'Unknown action';
}
echo json_encode($response);

View file

@ -17,6 +17,7 @@ if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php';
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmenttype.class.php';
dol_include_once('/kundenkarte/class/auditlog.class.php');
header('Content-Type: application/json; charset=UTF-8');
@ -25,6 +26,7 @@ $equipmentId = GETPOSTINT('equipment_id');
$carrierId = GETPOSTINT('carrier_id');
$equipment = new Equipment($db);
$auditLog = new AuditLog($db);
$response = array('success' => false, 'error' => '');
@ -199,6 +201,23 @@ switch ($action) {
$response['success'] = true;
$response['equipment_id'] = $result;
$response['block_label'] = $equipment->getBlockLabel();
// Audit log
$anlageId = 0;
if ($carrier->fk_panel > 0) {
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.php';
$panel = new EquipmentPanel($db);
if ($panel->fetch($carrier->fk_panel) > 0) {
$anlageId = $panel->fk_anlage;
}
} else {
$anlageId = $carrier->fk_anlage;
}
$auditLog->logCreate($user, AuditLog::TYPE_EQUIPMENT, $result, $equipment->label ?: $equipment->type_label, 0, $anlageId, array(
'type_id' => $equipment->fk_equipment_type,
'position_te' => $equipment->position_te,
'width_te' => $equipment->width_te
));
} else {
$response['error'] = $equipment->error;
}
@ -237,10 +256,28 @@ switch ($action) {
$equipment->field_values = $fieldValues;
}
$oldLabel = isset($oldLabel) ? $oldLabel : $equipment->label;
$oldPosition = isset($oldPosition) ? $oldPosition : $equipment->position_te;
$result = $equipment->update($user);
if ($result > 0) {
$response['success'] = true;
$response['block_label'] = $equipment->getBlockLabel();
// Audit log
$anlageId = 0;
$carrier = new EquipmentCarrier($db);
if ($carrier->fetch($equipment->fk_carrier) > 0) {
if ($carrier->fk_panel > 0) {
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.php';
$panel = new EquipmentPanel($db);
if ($panel->fetch($carrier->fk_panel) > 0) {
$anlageId = $panel->fk_anlage;
}
} else {
$anlageId = $carrier->fk_anlage;
}
}
$auditLog->logUpdate($user, AuditLog::TYPE_EQUIPMENT, $equipment->id, $equipment->label ?: $equipment->type_label, 'properties', null, null, 0, $anlageId);
} else {
$response['error'] = $equipment->error;
}
@ -325,9 +362,36 @@ switch ($action) {
break;
}
if ($equipment->fetch($equipmentId) > 0) {
// Get anlage_id before deletion for audit log
$anlageId = 0;
$deletedLabel = $equipment->label ?: $equipment->type_label;
$deletedData = array(
'type_id' => $equipment->fk_equipment_type,
'type_label' => $equipment->type_label,
'position_te' => $equipment->position_te,
'width_te' => $equipment->width_te,
'carrier_id' => $equipment->fk_carrier
);
$carrier = new EquipmentCarrier($db);
if ($carrier->fetch($equipment->fk_carrier) > 0) {
if ($carrier->fk_panel > 0) {
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.php';
$panel = new EquipmentPanel($db);
if ($panel->fetch($carrier->fk_panel) > 0) {
$anlageId = $panel->fk_anlage;
}
} else {
$anlageId = $carrier->fk_anlage;
}
}
$result = $equipment->delete($user);
if ($result > 0) {
$response['success'] = true;
// Audit log
$auditLog->logDelete($user, AuditLog::TYPE_EQUIPMENT, $equipmentId, $deletedLabel, 0, $anlageId, $deletedData);
} else {
$response['error'] = $equipment->error;
}
@ -342,6 +406,7 @@ switch ($action) {
break;
}
if ($equipment->fetch($equipmentId) > 0) {
$sourceId = $equipmentId;
$newId = $equipment->duplicate($user);
if ($newId > 0) {
$response['success'] = true;
@ -368,6 +433,22 @@ switch ($action) {
'field_values' => $newEquipment->getFieldValues(),
'fk_product' => $newEquipment->fk_product
);
// Audit log
$anlageId = 0;
$carrier = new EquipmentCarrier($db);
if ($carrier->fetch($newEquipment->fk_carrier) > 0) {
if ($carrier->fk_panel > 0) {
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentpanel.class.php';
$panel = new EquipmentPanel($db);
if ($panel->fetch($carrier->fk_panel) > 0) {
$anlageId = $panel->fk_anlage;
}
} else {
$anlageId = $carrier->fk_anlage;
}
}
$auditLog->logDuplicate($user, AuditLog::TYPE_EQUIPMENT, $newId, $newEquipment->label ?: $newEquipment->type_label, $sourceId, 0, $anlageId);
}
} else {
$response['error'] = $equipment->error ?: 'Duplication failed';

119
ajax/medium_types.php Normal file
View file

@ -0,0 +1,119 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX endpoint for medium types (Kabeltypen)
*/
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
dol_include_once('/kundenkarte/class/mediumtype.class.php');
header('Content-Type: application/json; charset=UTF-8');
$langs->loadLangs(array('kundenkarte@kundenkarte'));
$action = GETPOST('action', 'aZ09');
$systemId = GETPOSTINT('system_id');
$response = array('success' => false, 'error' => '');
// Security check
if (!$user->hasRight('kundenkarte', 'read')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
echo json_encode($response);
exit;
}
$mediumType = new MediumType($db);
switch ($action) {
case 'list':
// Get all medium types for a system (or all)
$types = $mediumType->fetchAllBySystem($systemId, 1);
$result = array();
foreach ($types as $t) {
$specs = $t->getAvailableSpecsArray();
$result[] = array(
'id' => $t->id,
'ref' => $t->ref,
'label' => $t->label,
'label_short' => $t->label_short,
'category' => $t->category,
'category_label' => $t->getCategoryLabel(),
'fk_system' => $t->fk_system,
'system_label' => $t->system_label,
'default_spec' => $t->default_spec,
'available_specs' => $specs,
'color' => $t->color
);
}
$response['success'] = true;
$response['types'] = $result;
break;
case 'list_grouped':
// Get types grouped by category
$grouped = $mediumType->fetchGroupedByCategory($systemId);
$result = array();
foreach ($grouped as $category => $types) {
$catTypes = array();
foreach ($types as $t) {
$catTypes[] = array(
'id' => $t->id,
'ref' => $t->ref,
'label' => $t->label,
'label_short' => $t->label_short,
'default_spec' => $t->default_spec,
'available_specs' => $t->getAvailableSpecsArray(),
'color' => $t->color
);
}
$result[] = array(
'category' => $category,
'category_label' => $types[0]->getCategoryLabel(),
'types' => $catTypes
);
}
$response['success'] = true;
$response['groups'] = $result;
break;
case 'get':
// Get single type details
$typeId = GETPOSTINT('type_id');
if ($typeId > 0 && $mediumType->fetch($typeId) > 0) {
$response['success'] = true;
$response['type'] = array(
'id' => $mediumType->id,
'ref' => $mediumType->ref,
'label' => $mediumType->label,
'label_short' => $mediumType->label_short,
'category' => $mediumType->category,
'category_label' => $mediumType->getCategoryLabel(),
'default_spec' => $mediumType->default_spec,
'available_specs' => $mediumType->getAvailableSpecsArray(),
'color' => $mediumType->color,
'description' => $mediumType->description
);
} else {
$response['error'] = $langs->trans('ErrorRecordNotFound');
}
break;
default:
$response['error'] = 'Unknown action';
}
echo json_encode($response);

107
ajax/tree_config.php Normal file
View file

@ -0,0 +1,107 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX endpoint for tree display configuration
*/
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
header('Content-Type: application/json; charset=UTF-8');
$langs->loadLangs(array('kundenkarte@kundenkarte'));
$action = GETPOST('action', 'aZ09');
$systemId = GETPOSTINT('system_id');
$response = array('success' => false, 'error' => '');
// Security check
if (!$user->hasRight('kundenkarte', 'read')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
echo json_encode($response);
exit;
}
switch ($action) {
case 'get':
// Get tree config for a system
$defaultConfig = array(
'show_ref' => true,
'show_label' => true,
'show_type' => true,
'show_icon' => true,
'show_status' => true,
'show_fields' => false,
'expand_default' => true,
'indent_style' => 'lines'
);
if ($systemId > 0) {
$sql = "SELECT tree_display_config FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE rowid = ".(int)$systemId;
$resql = $db->query($sql);
if ($resql && $obj = $db->fetch_object($resql)) {
if (!empty($obj->tree_display_config)) {
$savedConfig = json_decode($obj->tree_display_config, true);
if (is_array($savedConfig)) {
$defaultConfig = array_merge($defaultConfig, $savedConfig);
}
}
}
}
$response['success'] = true;
$response['config'] = $defaultConfig;
break;
case 'list':
// Get all system configs
$sql = "SELECT rowid, code, label, tree_display_config FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position";
$resql = $db->query($sql);
$configs = array();
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$config = array(
'show_ref' => true,
'show_label' => true,
'show_type' => true,
'show_icon' => true,
'show_status' => true,
'show_fields' => false,
'expand_default' => true,
'indent_style' => 'lines'
);
if (!empty($obj->tree_display_config)) {
$savedConfig = json_decode($obj->tree_display_config, true);
if (is_array($savedConfig)) {
$config = array_merge($config, $savedConfig);
}
}
$configs[$obj->rowid] = array(
'id' => $obj->rowid,
'code' => $obj->code,
'label' => $obj->label,
'config' => $config
);
}
}
$response['success'] = true;
$response['systems'] = $configs;
break;
default:
$response['error'] = 'Unknown action';
}
echo json_encode($response);

236
anlage_connection.php Normal file
View file

@ -0,0 +1,236 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* Edit page for Anlage Connection (cable/wire between elements)
*/
$res = 0;
if (!$res && file_exists("../main.inc.php")) $res = @include "../main.inc.php";
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
dol_include_once('/kundenkarte/class/anlageconnection.class.php');
dol_include_once('/kundenkarte/class/anlage.class.php');
dol_include_once('/kundenkarte/class/mediumtype.class.php');
$langs->loadLangs(array('kundenkarte@kundenkarte'));
$id = GETPOSTINT('id');
$socId = GETPOSTINT('socid');
$systemId = GETPOSTINT('system_id');
$sourceId = GETPOSTINT('source_id');
$action = GETPOST('action', 'aZ09');
// Security check
if (!$user->hasRight('kundenkarte', 'read')) {
accessforbidden();
}
$connection = new AnlageConnection($db);
$anlage = new Anlage($db);
$form = new Form($db);
// Load existing connection
if ($id > 0) {
$result = $connection->fetch($id);
if ($result <= 0) {
setEventMessages($langs->trans('ErrorRecordNotFound'), null, 'errors');
header('Location: '.DOL_URL_ROOT.'/societe/card.php?socid='.$socId);
exit;
}
// Get socId from source anlage if not provided
if (empty($socId)) {
$tmpAnlage = new Anlage($db);
if ($tmpAnlage->fetch($connection->fk_source) > 0) {
$socId = $tmpAnlage->fk_soc;
$systemId = $tmpAnlage->fk_system;
}
}
}
/*
* Actions
*/
if ($action == 'update' && $user->hasRight('kundenkarte', 'write')) {
$connection->fk_source = GETPOSTINT('fk_source');
$connection->fk_target = GETPOSTINT('fk_target');
$connection->label = GETPOST('label', 'alphanohtml');
$connection->fk_medium_type = GETPOSTINT('fk_medium_type');
$connection->medium_type_text = GETPOST('medium_type_text', 'alphanohtml');
$connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
$connection->medium_length = GETPOST('medium_length', 'alphanohtml');
$connection->medium_color = GETPOST('medium_color', 'alphanohtml');
$connection->route_description = GETPOST('route_description', 'restricthtml');
$connection->installation_date = GETPOST('installation_date', 'alpha');
if (empty($connection->fk_source) || empty($connection->fk_target)) {
setEventMessages($langs->trans('ErrorFieldRequired', 'Quelle/Ziel'), null, 'errors');
} else {
$result = $connection->update($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.dol_buildpath('/kundenkarte/tabs/anlagen.php', 1).'?id='.$socId.'&system='.$systemId);
exit;
} else {
setEventMessages($connection->error, null, 'errors');
}
}
}
if ($action == 'create' && $user->hasRight('kundenkarte', 'write')) {
$connection->fk_source = GETPOSTINT('fk_source');
$connection->fk_target = GETPOSTINT('fk_target');
$connection->label = GETPOST('label', 'alphanohtml');
$connection->fk_medium_type = GETPOSTINT('fk_medium_type');
$connection->medium_type_text = GETPOST('medium_type_text', 'alphanohtml');
$connection->medium_spec = GETPOST('medium_spec', 'alphanohtml');
$connection->medium_length = GETPOST('medium_length', 'alphanohtml');
$connection->medium_color = GETPOST('medium_color', 'alphanohtml');
$connection->route_description = GETPOST('route_description', 'restricthtml');
$connection->installation_date = GETPOST('installation_date', 'alpha');
$connection->status = 1;
if (empty($connection->fk_source) || empty($connection->fk_target)) {
setEventMessages($langs->trans('ErrorFieldRequired', 'Quelle/Ziel'), null, 'errors');
} else {
$result = $connection->create($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.dol_buildpath('/kundenkarte/tabs/anlagen.php', 1).'?id='.$socId.'&system='.$systemId);
exit;
} else {
setEventMessages($connection->error, null, 'errors');
}
}
}
if ($action == 'delete' && $user->hasRight('kundenkarte', 'write')) {
$result = $connection->delete($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
header('Location: '.dol_buildpath('/kundenkarte/tabs/anlagen.php', 1).'?id='.$socId.'&system='.$systemId);
exit;
} else {
setEventMessages($connection->error, null, 'errors');
}
}
/*
* View
*/
$title = $id > 0 ? 'Verbindung bearbeiten' : 'Neue Verbindung';
llxHeader('', $title);
// Load anlagen for dropdowns
$anlagenList = array();
if ($socId > 0) {
$tree = $anlage->fetchTree($socId, $systemId);
// Flatten tree
$flattenTree = function($nodes, $prefix = '') use (&$flattenTree, &$anlagenList) {
foreach ($nodes as $node) {
$anlagenList[$node->id] = $prefix . $node->label;
if (!empty($node->children)) {
$flattenTree($node->children, $prefix . ' ');
}
}
};
$flattenTree($tree);
}
// Load medium types
$mediumTypes = array();
$sql = "SELECT rowid, label, category FROM ".MAIN_DB_PREFIX."kundenkarte_medium_type WHERE active = 1 ORDER BY category, label";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$mediumTypes[$obj->rowid] = $obj->label;
}
}
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="id" value="'.$id.'">';
print '<input type="hidden" name="socid" value="'.$socId.'">';
print '<input type="hidden" name="system_id" value="'.$systemId.'">';
print '<input type="hidden" name="action" value="'.($id > 0 ? 'update' : 'create').'">';
print load_fiche_titre($title, '', 'object_kundenkarte@kundenkarte');
print '<table class="border centpercent">';
// Source
print '<tr><td class="titlefield fieldrequired">'.$langs->trans('Von (Quelle)').'</td>';
print '<td><select name="fk_source" class="flat minwidth300">';
print '<option value="">-- Quelle wählen --</option>';
foreach ($anlagenList as $aid => $alabel) {
$selected = ($connection->fk_source == $aid || $sourceId == $aid) ? ' selected' : '';
print '<option value="'.$aid.'"'.$selected.'>'.dol_escape_htmltag($alabel).'</option>';
}
print '</select></td></tr>';
// Target
print '<tr><td class="fieldrequired">'.$langs->trans('Nach (Ziel)').'</td>';
print '<td><select name="fk_target" class="flat minwidth300">';
print '<option value="">-- Ziel wählen --</option>';
foreach ($anlagenList as $aid => $alabel) {
$selected = ($connection->fk_target == $aid) ? ' selected' : '';
print '<option value="'.$aid.'"'.$selected.'>'.dol_escape_htmltag($alabel).'</option>';
}
print '</select></td></tr>';
// Medium type
print '<tr><td>'.$langs->trans('Kabeltyp').'</td>';
print '<td><select name="fk_medium_type" class="flat minwidth200">';
print '<option value="">-- oder Freitext unten --</option>';
foreach ($mediumTypes as $mid => $mlabel) {
$selected = ($connection->fk_medium_type == $mid) ? ' selected' : '';
print '<option value="'.$mid.'"'.$selected.'>'.dol_escape_htmltag($mlabel).'</option>';
}
print '</select></td></tr>';
// Medium type text (free text)
print '<tr><td>'.$langs->trans('Kabeltyp (Freitext)').'</td>';
print '<td><input type="text" name="medium_type_text" class="flat minwidth300" value="'.dol_escape_htmltag($connection->medium_type_text).'" placeholder="z.B. NYM-J"></td></tr>';
// Medium spec
print '<tr><td>'.$langs->trans('Querschnitt/Typ').'</td>';
print '<td><input type="text" name="medium_spec" class="flat minwidth200" value="'.dol_escape_htmltag($connection->medium_spec).'" placeholder="z.B. 5x2,5mm²"></td></tr>';
// Length
print '<tr><td>'.$langs->trans('Länge').'</td>';
print '<td><input type="text" name="medium_length" class="flat minwidth150" value="'.dol_escape_htmltag($connection->medium_length).'" placeholder="z.B. 15m"></td></tr>';
// Color
print '<tr><td>'.$langs->trans('Farbe').'</td>';
print '<td><input type="text" name="medium_color" class="flat minwidth150" value="'.dol_escape_htmltag($connection->medium_color).'" placeholder="z.B. grau"></td></tr>';
// Label
print '<tr><td>'.$langs->trans('Bezeichnung').'</td>';
print '<td><input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($connection->label).'" placeholder="z.B. Zuleitung HAK"></td></tr>';
// Route description
print '<tr><td>'.$langs->trans('Verlegungsweg').'</td>';
print '<td><textarea name="route_description" class="flat" rows="3" style="width:90%;">'.dol_escape_htmltag($connection->route_description).'</textarea></td></tr>';
// Installation date
print '<tr><td>'.$langs->trans('Installationsdatum').'</td>';
print '<td><input type="date" name="installation_date" class="flat" value="'.dol_escape_htmltag($connection->installation_date).'"></td></tr>';
print '</table>';
print '<div class="center" style="margin-top:20px;">';
print '<button type="submit" class="button button-save">'.$langs->trans('Save').'</button>';
print ' <a class="button button-cancel" href="'.dol_buildpath('/kundenkarte/tabs/anlagen.php', 1).'?id='.$socId.'&system='.$systemId.'">'.$langs->trans('Cancel').'</a>';
if ($id > 0 && $user->hasRight('kundenkarte', 'write')) {
print ' <a class="button button-delete" style="margin-left:20px;" href="'.$_SERVER['PHP_SELF'].'?id='.$id.'&socid='.$socId.'&system_id='.$systemId.'&action=delete&token='.newToken().'" onclick="return confirm(\'Verbindung wirklich löschen?\');">'.$langs->trans('Delete').'</a>';
}
print '</div>';
print '</form>';
llxFooter();
$db->close();

View file

@ -79,6 +79,8 @@ class Anlage extends CommonObject
return -1;
}
// Note: Circular reference check not needed on create since element doesn't exist yet
// Calculate level
$this->level = 0;
if ($this->fk_parent > 0) {
@ -244,6 +246,13 @@ class Anlage extends CommonObject
{
$error = 0;
// Check for circular reference
if ($this->fk_parent > 0 && $this->wouldCreateCircularReference($this->fk_parent)) {
$this->error = 'ErrorCircularReference';
$this->errors[] = 'Das Element kann nicht unter sich selbst oder einem seiner Unterelemente platziert werden.';
return -2;
}
// Recalculate level if parent changed
$this->level = 0;
if ($this->fk_parent > 0) {
@ -505,6 +514,95 @@ class Anlage extends CommonObject
return isset($values[$fieldCode]) ? $values[$fieldCode] : null;
}
/**
* Check if setting a parent would create a circular reference
*
* @param int $newParentId The proposed new parent ID
* @return bool True if circular reference would be created, false otherwise
*/
public function wouldCreateCircularReference($newParentId)
{
if (empty($this->id) || empty($newParentId)) {
return false;
}
// Cannot be own parent
if ($newParentId == $this->id) {
return true;
}
// Check if newParentId is a descendant of this element
return $this->isDescendant($newParentId, $this->id);
}
/**
* Check if an element is a descendant of another
*
* @param int $elementId Element to check
* @param int $ancestorId Potential ancestor
* @param int $maxDepth Maximum depth to check (prevent infinite loops)
* @return bool True if elementId is a descendant of ancestorId
*/
private function isDescendant($elementId, $ancestorId, $maxDepth = 50)
{
if ($maxDepth <= 0) {
return true; // Safety: assume circular if too deep
}
// Get all children of ancestorId
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE fk_parent = ".((int) $ancestorId);
$sql .= " AND status = 1";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
if ($obj->rowid == $elementId) {
return true;
}
// Recursively check children
if ($this->isDescendant($elementId, $obj->rowid, $maxDepth - 1)) {
return true;
}
}
$this->db->free($resql);
}
return false;
}
/**
* Get all ancestor IDs of this element
*
* @param int $maxDepth Maximum depth to check
* @return array Array of ancestor IDs
*/
public function getAncestorIds($maxDepth = 50)
{
$ancestors = array();
$currentParentId = $this->fk_parent;
$depth = 0;
while ($currentParentId > 0 && $depth < $maxDepth) {
$ancestors[] = $currentParentId;
$sql = "SELECT fk_parent FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE rowid = ".((int) $currentParentId);
$resql = $this->db->query($sql);
if ($resql && $this->db->num_rows($resql) > 0) {
$obj = $this->db->fetch_object($resql);
$currentParentId = $obj->fk_parent;
$this->db->free($resql);
} else {
break;
}
$depth++;
}
return $ancestors;
}
/**
* Get info for tree display
*

View file

@ -0,0 +1,341 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* Anlage Connection class - connections between Anlage elements in tree
* Describes cables/wires between structure elements like HAK Zählerschrank
*/
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
class AnlageConnection extends CommonObject
{
public $element = 'anlageconnection';
public $table_element = 'kundenkarte_anlage_connection';
public $id;
public $entity;
public $fk_source;
public $fk_target;
public $label;
public $fk_medium_type;
public $medium_type_text;
public $medium_spec;
public $medium_length;
public $medium_color;
public $route_description;
public $installation_date;
public $status;
public $note_private;
public $note_public;
public $date_creation;
public $fk_user_creat;
public $fk_user_modif;
// Loaded related data
public $source_label;
public $source_ref;
public $target_label;
public $target_ref;
public $medium_type_label;
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Create connection
*
* @param User $user User object
* @return int >0 if OK, <0 if KO
*/
public function create($user)
{
global $conf;
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, fk_source, fk_target, label,";
$sql .= "fk_medium_type, medium_type_text, medium_spec, medium_length, medium_color,";
$sql .= "route_description, installation_date, status,";
$sql .= "note_private, note_public, date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= (int)$conf->entity;
$sql .= ", ".(int)$this->fk_source;
$sql .= ", ".(int)$this->fk_target;
$sql .= ", ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
$sql .= ", ".($this->fk_medium_type > 0 ? (int)$this->fk_medium_type : "NULL");
$sql .= ", ".($this->medium_type_text ? "'".$this->db->escape($this->medium_type_text)."'" : "NULL");
$sql .= ", ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL");
$sql .= ", ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL");
$sql .= ", ".($this->medium_color ? "'".$this->db->escape($this->medium_color)."'" : "NULL");
$sql .= ", ".($this->route_description ? "'".$this->db->escape($this->route_description)."'" : "NULL");
$sql .= ", ".($this->installation_date ? "'".$this->db->escape($this->installation_date)."'" : "NULL");
$sql .= ", ".(int)($this->status ?: 1);
$sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
$sql .= ", ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
$sql .= ", NOW()";
$sql .= ", ".(int)$user->id;
$sql .= ")";
$resql = $this->db->query($sql);
if ($resql) {
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
return $this->id;
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Fetch connection
*
* @param int $id ID
* @return int >0 if OK, <0 if KO
*/
public function fetch($id)
{
$sql = "SELECT c.*,";
$sql .= " src.label as source_label, src.ref as source_ref,";
$sql .= " tgt.label as target_label, tgt.ref as target_ref,";
$sql .= " mt.label as medium_type_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as src ON c.fk_source = src.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as tgt ON c.fk_target = tgt.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_medium_type as mt ON c.fk_medium_type = mt.rowid";
$sql .= " WHERE c.rowid = ".(int)$id;
$resql = $this->db->query($sql);
if ($resql) {
if ($obj = $this->db->fetch_object($resql)) {
$this->id = $obj->rowid;
$this->entity = $obj->entity;
$this->fk_source = $obj->fk_source;
$this->fk_target = $obj->fk_target;
$this->label = $obj->label;
$this->fk_medium_type = $obj->fk_medium_type;
$this->medium_type_text = $obj->medium_type_text;
$this->medium_spec = $obj->medium_spec;
$this->medium_length = $obj->medium_length;
$this->medium_color = $obj->medium_color;
$this->route_description = $obj->route_description;
$this->installation_date = $obj->installation_date;
$this->status = $obj->status;
$this->note_private = $obj->note_private;
$this->note_public = $obj->note_public;
$this->date_creation = $this->db->jdate($obj->date_creation);
$this->fk_user_creat = $obj->fk_user_creat;
$this->fk_user_modif = $obj->fk_user_modif;
$this->source_label = $obj->source_label;
$this->source_ref = $obj->source_ref;
$this->target_label = $obj->target_label;
$this->target_ref = $obj->target_ref;
$this->medium_type_label = $obj->medium_type_label;
$this->db->free($resql);
return 1;
}
$this->db->free($resql);
return 0;
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Update connection
*
* @param User $user User object
* @return int >0 if OK, <0 if KO
*/
public function update($user)
{
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
$sql .= " fk_source = ".(int)$this->fk_source;
$sql .= ", fk_target = ".(int)$this->fk_target;
$sql .= ", label = ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
$sql .= ", fk_medium_type = ".($this->fk_medium_type > 0 ? (int)$this->fk_medium_type : "NULL");
$sql .= ", medium_type_text = ".($this->medium_type_text ? "'".$this->db->escape($this->medium_type_text)."'" : "NULL");
$sql .= ", medium_spec = ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL");
$sql .= ", medium_length = ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL");
$sql .= ", medium_color = ".($this->medium_color ? "'".$this->db->escape($this->medium_color)."'" : "NULL");
$sql .= ", route_description = ".($this->route_description ? "'".$this->db->escape($this->route_description)."'" : "NULL");
$sql .= ", installation_date = ".($this->installation_date ? "'".$this->db->escape($this->installation_date)."'" : "NULL");
$sql .= ", status = ".(int)$this->status;
$sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
$sql .= ", note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
$sql .= ", fk_user_modif = ".(int)$user->id;
$sql .= " WHERE rowid = ".(int)$this->id;
$resql = $this->db->query($sql);
if ($resql) {
return 1;
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Delete connection
*
* @param User $user User object
* @return int >0 if OK, <0 if KO
*/
public function delete($user)
{
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE rowid = ".(int)$this->id;
$resql = $this->db->query($sql);
if ($resql) {
return 1;
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Fetch all connections for an Anlage (as source or target)
*
* @param int $anlageId Anlage ID
* @return array Array of AnlageConnection objects
*/
public function fetchByAnlage($anlageId)
{
$result = array();
$sql = "SELECT c.*,";
$sql .= " src.label as source_label, src.ref as source_ref,";
$sql .= " tgt.label as target_label, tgt.ref as target_ref,";
$sql .= " mt.label as medium_type_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as src ON c.fk_source = src.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as tgt ON c.fk_target = tgt.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_medium_type as mt ON c.fk_medium_type = mt.rowid";
$sql .= " WHERE c.fk_source = ".(int)$anlageId." OR c.fk_target = ".(int)$anlageId;
$sql .= " ORDER BY c.rowid";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$conn = new AnlageConnection($this->db);
$conn->id = $obj->rowid;
$conn->entity = $obj->entity;
$conn->fk_source = $obj->fk_source;
$conn->fk_target = $obj->fk_target;
$conn->label = $obj->label;
$conn->fk_medium_type = $obj->fk_medium_type;
$conn->medium_type_text = $obj->medium_type_text;
$conn->medium_spec = $obj->medium_spec;
$conn->medium_length = $obj->medium_length;
$conn->medium_color = $obj->medium_color;
$conn->route_description = $obj->route_description;
$conn->installation_date = $obj->installation_date;
$conn->status = $obj->status;
$conn->source_label = $obj->source_label;
$conn->source_ref = $obj->source_ref;
$conn->target_label = $obj->target_label;
$conn->target_ref = $obj->target_ref;
$conn->medium_type_label = $obj->medium_type_label;
$result[] = $conn;
}
$this->db->free($resql);
}
return $result;
}
/**
* Fetch all connections for a customer (across all anlagen)
*
* @param int $socId Societe ID
* @param int $systemId Optional system filter
* @return array Array of AnlageConnection objects
*/
public function fetchBySociete($socId, $systemId = 0)
{
$result = array();
$sql = "SELECT c.*,";
$sql .= " src.label as source_label, src.ref as source_ref,";
$sql .= " tgt.label as target_label, tgt.ref as target_ref,";
$sql .= " mt.label as medium_type_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c";
$sql .= " JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as src ON c.fk_source = src.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as tgt ON c.fk_target = tgt.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_medium_type as mt ON c.fk_medium_type = mt.rowid";
$sql .= " WHERE src.fk_soc = ".(int)$socId;
if ($systemId > 0) {
$sql .= " AND src.fk_system = ".(int)$systemId;
}
$sql .= " ORDER BY src.label, c.rowid";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$conn = new AnlageConnection($this->db);
$conn->id = $obj->rowid;
$conn->entity = $obj->entity;
$conn->fk_source = $obj->fk_source;
$conn->fk_target = $obj->fk_target;
$conn->label = $obj->label;
$conn->fk_medium_type = $obj->fk_medium_type;
$conn->medium_type_text = $obj->medium_type_text;
$conn->medium_spec = $obj->medium_spec;
$conn->medium_length = $obj->medium_length;
$conn->medium_color = $obj->medium_color;
$conn->route_description = $obj->route_description;
$conn->installation_date = $obj->installation_date;
$conn->status = $obj->status;
$conn->source_label = $obj->source_label;
$conn->source_ref = $obj->source_ref;
$conn->target_label = $obj->target_label;
$conn->target_ref = $obj->target_ref;
$conn->medium_type_label = $obj->medium_type_label;
$result[] = $conn;
}
$this->db->free($resql);
}
return $result;
}
/**
* Get display label for connection
*
* @return string Display label
*/
public function getDisplayLabel()
{
$parts = array();
// Medium type
$medium = $this->medium_type_label ?: $this->medium_type_text;
if ($medium) {
$mediumInfo = $medium;
if ($this->medium_spec) {
$mediumInfo .= ' '.$this->medium_spec;
}
if ($this->medium_length) {
$mediumInfo .= ' ('.$this->medium_length.')';
}
$parts[] = $mediumInfo;
}
// Label
if ($this->label) {
$parts[] = $this->label;
}
return implode(' - ', $parts);
}
}

455
class/auditlog.class.php Normal file
View file

@ -0,0 +1,455 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*/
/**
* Class AuditLog
* Manages audit logging for KundenKarte module
*/
class AuditLog extends CommonObject
{
public $element = 'auditlog';
public $table_element = 'kundenkarte_audit_log';
public $object_type;
public $object_id;
public $object_ref;
public $fk_societe;
public $fk_anlage;
public $action;
public $field_changed;
public $old_value;
public $new_value;
public $fk_user;
public $user_login;
public $date_action;
public $note;
public $ip_address;
// Loaded properties
public $user_name;
public $societe_name;
// Action constants
const ACTION_CREATE = 'create';
const ACTION_UPDATE = 'update';
const ACTION_DELETE = 'delete';
const ACTION_MOVE = 'move';
const ACTION_DUPLICATE = 'duplicate';
const ACTION_STATUS_CHANGE = 'status';
// Object type constants
const TYPE_EQUIPMENT = 'equipment';
const TYPE_CARRIER = 'carrier';
const TYPE_PANEL = 'panel';
const TYPE_ANLAGE = 'anlage';
const TYPE_CONNECTION = 'connection';
const TYPE_BUSBAR = 'busbar';
const TYPE_EQUIPMENT_TYPE = 'equipment_type';
const TYPE_BUSBAR_TYPE = 'busbar_type';
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Log an action
*
* @param User $user User performing the action
* @param string $objectType Type of object (equipment, carrier, panel, etc.)
* @param int $objectId ID of the object
* @param string $action Action performed (create, update, delete, etc.)
* @param string $objectRef Reference/label of the object (optional)
* @param string $fieldChanged Specific field changed (optional)
* @param mixed $oldValue Previous value (optional)
* @param mixed $newValue New value (optional)
* @param int $socid Customer ID (optional)
* @param int $anlageId Anlage ID (optional)
* @param string $note Additional note (optional)
* @return int Log entry ID or <0 on error
*/
public function log($user, $objectType, $objectId, $action, $objectRef = '', $fieldChanged = '', $oldValue = null, $newValue = null, $socid = 0, $anlageId = 0, $note = '')
{
global $conf;
$now = dol_now();
// Serialize complex values
if (is_array($oldValue) || is_object($oldValue)) {
$oldValue = json_encode($oldValue);
}
if (is_array($newValue) || is_object($newValue)) {
$newValue = json_encode($newValue);
}
// Get IP address
$ipAddress = '';
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ipAddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
$ipAddress = $_SERVER['REMOTE_ADDR'];
}
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, object_type, object_id, object_ref, fk_societe, fk_anlage,";
$sql .= " action, field_changed, old_value, new_value,";
$sql .= " fk_user, user_login, date_action, note, ip_address";
$sql .= ") VALUES (";
$sql .= ((int) $conf->entity);
$sql .= ", '".$this->db->escape($objectType)."'";
$sql .= ", ".((int) $objectId);
$sql .= ", ".($objectRef ? "'".$this->db->escape($objectRef)."'" : "NULL");
$sql .= ", ".($socid > 0 ? ((int) $socid) : "NULL");
$sql .= ", ".($anlageId > 0 ? ((int) $anlageId) : "NULL");
$sql .= ", '".$this->db->escape($action)."'";
$sql .= ", ".($fieldChanged ? "'".$this->db->escape($fieldChanged)."'" : "NULL");
$sql .= ", ".($oldValue !== null ? "'".$this->db->escape($oldValue)."'" : "NULL");
$sql .= ", ".($newValue !== null ? "'".$this->db->escape($newValue)."'" : "NULL");
$sql .= ", ".((int) $user->id);
$sql .= ", '".$this->db->escape($user->login)."'";
$sql .= ", '".$this->db->idate($now)."'";
$sql .= ", ".($note ? "'".$this->db->escape($note)."'" : "NULL");
$sql .= ", ".($ipAddress ? "'".$this->db->escape($ipAddress)."'" : "NULL");
$sql .= ")";
$resql = $this->db->query($sql);
if (!$resql) {
$this->error = $this->db->lasterror();
return -1;
}
return $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
}
/**
* Log object creation
*/
public function logCreate($user, $objectType, $objectId, $objectRef = '', $socid = 0, $anlageId = 0, $data = null)
{
return $this->log($user, $objectType, $objectId, self::ACTION_CREATE, $objectRef, '', null, $data, $socid, $anlageId);
}
/**
* Log object update
*/
public function logUpdate($user, $objectType, $objectId, $objectRef = '', $fieldChanged = '', $oldValue = null, $newValue = null, $socid = 0, $anlageId = 0)
{
return $this->log($user, $objectType, $objectId, self::ACTION_UPDATE, $objectRef, $fieldChanged, $oldValue, $newValue, $socid, $anlageId);
}
/**
* Log object deletion
*/
public function logDelete($user, $objectType, $objectId, $objectRef = '', $socid = 0, $anlageId = 0, $data = null)
{
return $this->log($user, $objectType, $objectId, self::ACTION_DELETE, $objectRef, '', $data, null, $socid, $anlageId);
}
/**
* Log object move (position change)
*/
public function logMove($user, $objectType, $objectId, $objectRef = '', $oldPosition = null, $newPosition = null, $socid = 0, $anlageId = 0)
{
return $this->log($user, $objectType, $objectId, self::ACTION_MOVE, $objectRef, 'position', $oldPosition, $newPosition, $socid, $anlageId);
}
/**
* Log object duplication
*/
public function logDuplicate($user, $objectType, $objectId, $objectRef = '', $sourceId = 0, $socid = 0, $anlageId = 0)
{
return $this->log($user, $objectType, $objectId, self::ACTION_DUPLICATE, $objectRef, '', $sourceId, $objectId, $socid, $anlageId, 'Kopiert von ID '.$sourceId);
}
/**
* Fetch audit log entries for an object
*
* @param string $objectType Object type
* @param int $objectId Object ID
* @param int $limit Max entries (0 = no limit)
* @return array Array of AuditLog objects
*/
public function fetchByObject($objectType, $objectId, $limit = 50)
{
$results = array();
$sql = "SELECT a.*, u.firstname, u.lastname, s.nom as societe_name";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as u ON a.fk_user = u.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON a.fk_societe = s.rowid";
$sql .= " WHERE a.object_type = '".$this->db->escape($objectType)."'";
$sql .= " AND a.object_id = ".((int) $objectId);
$sql .= " ORDER BY a.date_action DESC";
if ($limit > 0) {
$sql .= " LIMIT ".((int) $limit);
}
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$log = new AuditLog($this->db);
$log->id = $obj->rowid;
$log->object_type = $obj->object_type;
$log->object_id = $obj->object_id;
$log->object_ref = $obj->object_ref;
$log->fk_societe = $obj->fk_societe;
$log->fk_anlage = $obj->fk_anlage;
$log->action = $obj->action;
$log->field_changed = $obj->field_changed;
$log->old_value = $obj->old_value;
$log->new_value = $obj->new_value;
$log->fk_user = $obj->fk_user;
$log->user_login = $obj->user_login;
$log->date_action = $this->db->jdate($obj->date_action);
$log->note = $obj->note;
$log->ip_address = $obj->ip_address;
$log->user_name = trim($obj->firstname.' '.$obj->lastname);
$log->societe_name = $obj->societe_name;
$results[] = $log;
}
$this->db->free($resql);
}
return $results;
}
/**
* Fetch audit log entries for an Anlage (installation)
*
* @param int $anlageId Anlage ID
* @param int $limit Max entries
* @return array Array of AuditLog objects
*/
public function fetchByAnlage($anlageId, $limit = 100)
{
$results = array();
$sql = "SELECT a.*, u.firstname, u.lastname";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as u ON a.fk_user = u.rowid";
$sql .= " WHERE a.fk_anlage = ".((int) $anlageId);
$sql .= " ORDER BY a.date_action DESC";
if ($limit > 0) {
$sql .= " LIMIT ".((int) $limit);
}
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$log = new AuditLog($this->db);
$log->id = $obj->rowid;
$log->object_type = $obj->object_type;
$log->object_id = $obj->object_id;
$log->object_ref = $obj->object_ref;
$log->fk_societe = $obj->fk_societe;
$log->fk_anlage = $obj->fk_anlage;
$log->action = $obj->action;
$log->field_changed = $obj->field_changed;
$log->old_value = $obj->old_value;
$log->new_value = $obj->new_value;
$log->fk_user = $obj->fk_user;
$log->user_login = $obj->user_login;
$log->date_action = $this->db->jdate($obj->date_action);
$log->note = $obj->note;
$log->ip_address = $obj->ip_address;
$log->user_name = trim($obj->firstname.' '.$obj->lastname);
$results[] = $log;
}
$this->db->free($resql);
}
return $results;
}
/**
* Fetch audit log entries for a customer
*
* @param int $socid Societe ID
* @param int $limit Max entries
* @return array Array of AuditLog objects
*/
public function fetchBySociete($socid, $limit = 100)
{
$results = array();
$sql = "SELECT a.*, u.firstname, u.lastname";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as u ON a.fk_user = u.rowid";
$sql .= " WHERE a.fk_societe = ".((int) $socid);
$sql .= " ORDER BY a.date_action DESC";
if ($limit > 0) {
$sql .= " LIMIT ".((int) $limit);
}
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$log = new AuditLog($this->db);
$log->id = $obj->rowid;
$log->object_type = $obj->object_type;
$log->object_id = $obj->object_id;
$log->object_ref = $obj->object_ref;
$log->fk_societe = $obj->fk_societe;
$log->fk_anlage = $obj->fk_anlage;
$log->action = $obj->action;
$log->field_changed = $obj->field_changed;
$log->old_value = $obj->old_value;
$log->new_value = $obj->new_value;
$log->fk_user = $obj->fk_user;
$log->user_login = $obj->user_login;
$log->date_action = $this->db->jdate($obj->date_action);
$log->note = $obj->note;
$log->ip_address = $obj->ip_address;
$log->user_name = trim($obj->firstname.' '.$obj->lastname);
$results[] = $log;
}
$this->db->free($resql);
}
return $results;
}
/**
* Get human-readable action label
*
* @return string Translated action label
*/
public function getActionLabel()
{
global $langs;
switch ($this->action) {
case self::ACTION_CREATE:
return $langs->trans('AuditActionCreate');
case self::ACTION_UPDATE:
return $langs->trans('AuditActionUpdate');
case self::ACTION_DELETE:
return $langs->trans('AuditActionDelete');
case self::ACTION_MOVE:
return $langs->trans('AuditActionMove');
case self::ACTION_DUPLICATE:
return $langs->trans('AuditActionDuplicate');
case self::ACTION_STATUS_CHANGE:
return $langs->trans('AuditActionStatus');
default:
return $this->action;
}
}
/**
* Get human-readable object type label
*
* @return string Translated object type label
*/
public function getObjectTypeLabel()
{
global $langs;
switch ($this->object_type) {
case self::TYPE_EQUIPMENT:
return $langs->trans('Equipment');
case self::TYPE_CARRIER:
return $langs->trans('CarrierLabel');
case self::TYPE_PANEL:
return $langs->trans('PanelLabel');
case self::TYPE_ANLAGE:
return $langs->trans('Installation');
case self::TYPE_CONNECTION:
return $langs->trans('Connection');
case self::TYPE_BUSBAR:
return $langs->trans('Busbar');
case self::TYPE_EQUIPMENT_TYPE:
return $langs->trans('EquipmentType');
case self::TYPE_BUSBAR_TYPE:
return $langs->trans('BusbarType');
default:
return $this->object_type;
}
}
/**
* Get action icon
*
* @return string FontAwesome icon class
*/
public function getActionIcon()
{
switch ($this->action) {
case self::ACTION_CREATE:
return 'fa-plus-circle';
case self::ACTION_UPDATE:
return 'fa-edit';
case self::ACTION_DELETE:
return 'fa-trash';
case self::ACTION_MOVE:
return 'fa-arrows';
case self::ACTION_DUPLICATE:
return 'fa-copy';
case self::ACTION_STATUS_CHANGE:
return 'fa-toggle-on';
default:
return 'fa-question';
}
}
/**
* Get action color
*
* @return string CSS color
*/
public function getActionColor()
{
switch ($this->action) {
case self::ACTION_CREATE:
return '#27ae60';
case self::ACTION_UPDATE:
return '#3498db';
case self::ACTION_DELETE:
return '#e74c3c';
case self::ACTION_MOVE:
return '#9b59b6';
case self::ACTION_DUPLICATE:
return '#f39c12';
case self::ACTION_STATUS_CHANGE:
return '#1abc9c';
default:
return '#95a5a6';
}
}
/**
* Clean old audit log entries
*
* @param int $daysToKeep Number of days to keep (default: 365)
* @return int Number of deleted entries or -1 on error
*/
public function cleanOldEntries($daysToKeep = 365)
{
$cutoffDate = dol_now() - ($daysToKeep * 24 * 60 * 60);
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE date_action < '".$this->db->idate($cutoffDate)."'";
$resql = $this->db->query($sql);
if (!$resql) {
$this->error = $this->db->lasterror();
return -1;
}
return $this->db->affected_rows($resql);
}
}

View file

@ -0,0 +1,362 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* Building Type class (Gebäudetypen)
* Global structure elements for all systems
*/
require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
class BuildingType extends CommonObject
{
public $element = 'buildingtype';
public $table_element = 'kundenkarte_building_type';
// Level type constants
const LEVEL_BUILDING = 'building';
const LEVEL_FLOOR = 'floor';
const LEVEL_WING = 'wing';
const LEVEL_CORRIDOR = 'corridor';
const LEVEL_ROOM = 'room';
const LEVEL_AREA = 'area';
public $id;
public $entity;
public $ref;
public $label;
public $label_short;
public $description;
public $fk_parent;
public $level_type;
public $icon;
public $color;
public $picto;
public $is_system;
public $can_have_children;
public $position;
public $active;
public $date_creation;
public $tms;
public $fk_user_creat;
public $fk_user_modif;
// Loaded parent info
public $parent_label;
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Create building type
*
* @param User $user User object
* @return int >0 if OK, <0 if KO
*/
public function create($user)
{
global $conf;
$this->ref = trim($this->ref);
$this->label = trim($this->label);
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, ref, label, label_short, description, fk_parent, level_type,";
$sql .= "icon, color, picto, is_system, can_have_children, position, active,";
$sql .= "date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= (int)$conf->entity;
$sql .= ", '".$this->db->escape($this->ref)."'";
$sql .= ", '".$this->db->escape($this->label)."'";
$sql .= ", ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
$sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
$sql .= ", ".(int)($this->fk_parent ?: 0);
$sql .= ", ".($this->level_type ? "'".$this->db->escape($this->level_type)."'" : "NULL");
$sql .= ", ".($this->icon ? "'".$this->db->escape($this->icon)."'" : "NULL");
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", ".(int)($this->is_system ?: 0);
$sql .= ", ".(int)($this->can_have_children !== null ? $this->can_have_children : 1);
$sql .= ", ".(int)($this->position ?: 0);
$sql .= ", ".(int)($this->active !== null ? $this->active : 1);
$sql .= ", NOW()";
$sql .= ", ".(int)$user->id;
$sql .= ")";
$resql = $this->db->query($sql);
if ($resql) {
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
return $this->id;
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Fetch building type
*
* @param int $id ID
* @param string $ref Reference
* @return int >0 if OK, <0 if KO
*/
public function fetch($id, $ref = '')
{
$sql = "SELECT t.*, p.label as parent_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$this->table_element." as p ON t.fk_parent = p.rowid";
$sql .= " WHERE ";
if ($id > 0) {
$sql .= "t.rowid = ".(int)$id;
} else {
$sql .= "t.ref = '".$this->db->escape($ref)."'";
}
$resql = $this->db->query($sql);
if ($resql) {
if ($obj = $this->db->fetch_object($resql)) {
$this->id = $obj->rowid;
$this->entity = $obj->entity;
$this->ref = $obj->ref;
$this->label = $obj->label;
$this->label_short = $obj->label_short;
$this->description = $obj->description;
$this->fk_parent = $obj->fk_parent;
$this->level_type = $obj->level_type;
$this->icon = $obj->icon;
$this->color = $obj->color;
$this->picto = $obj->picto;
$this->is_system = $obj->is_system;
$this->can_have_children = $obj->can_have_children;
$this->position = $obj->position;
$this->active = $obj->active;
$this->date_creation = $this->db->jdate($obj->date_creation);
$this->fk_user_creat = $obj->fk_user_creat;
$this->fk_user_modif = $obj->fk_user_modif;
$this->parent_label = $obj->parent_label;
$this->db->free($resql);
return 1;
}
$this->db->free($resql);
return 0;
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Update building type
*
* @param User $user User object
* @return int >0 if OK, <0 if KO
*/
public function update($user)
{
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
$sql .= " ref = '".$this->db->escape($this->ref)."'";
$sql .= ", label = '".$this->db->escape($this->label)."'";
$sql .= ", label_short = ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
$sql .= ", description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
$sql .= ", fk_parent = ".(int)($this->fk_parent ?: 0);
$sql .= ", level_type = ".($this->level_type ? "'".$this->db->escape($this->level_type)."'" : "NULL");
$sql .= ", icon = ".($this->icon ? "'".$this->db->escape($this->icon)."'" : "NULL");
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", can_have_children = ".(int)$this->can_have_children;
$sql .= ", position = ".(int)$this->position;
$sql .= ", active = ".(int)$this->active;
$sql .= ", fk_user_modif = ".(int)$user->id;
$sql .= " WHERE rowid = ".(int)$this->id;
$resql = $this->db->query($sql);
if ($resql) {
return 1;
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Delete building type
*
* @param User $user User object
* @return int >0 if OK, <0 if KO
*/
public function delete($user)
{
// Don't allow deleting system types
if ($this->is_system) {
$this->error = 'CannotDeleteSystemType';
return -1;
}
// Check if type is used as parent
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE fk_parent = ".(int)$this->id;
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
if ($obj->cnt > 0) {
$this->error = 'CannotDeleteTypeWithChildren';
return -2;
}
}
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE rowid = ".(int)$this->id;
$resql = $this->db->query($sql);
if ($resql) {
return 1;
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Fetch all building types
*
* @param int $activeOnly Only active types
* @param string $levelType Filter by level type
* @return array Array of BuildingType objects
*/
public function fetchAll($activeOnly = 1, $levelType = '')
{
global $conf;
$result = array();
$sql = "SELECT t.*, p.label as parent_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$this->table_element." as p ON t.fk_parent = p.rowid";
$sql .= " WHERE (t.entity = ".(int)$conf->entity." OR t.entity = 0)";
if ($activeOnly) {
$sql .= " AND t.active = 1";
}
if ($levelType) {
$sql .= " AND t.level_type = '".$this->db->escape($levelType)."'";
}
$sql .= " ORDER BY t.level_type, t.position, t.label";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$type = new BuildingType($this->db);
$type->id = $obj->rowid;
$type->entity = $obj->entity;
$type->ref = $obj->ref;
$type->label = $obj->label;
$type->label_short = $obj->label_short;
$type->description = $obj->description;
$type->fk_parent = $obj->fk_parent;
$type->level_type = $obj->level_type;
$type->icon = $obj->icon;
$type->color = $obj->color;
$type->picto = $obj->picto;
$type->is_system = $obj->is_system;
$type->can_have_children = $obj->can_have_children;
$type->position = $obj->position;
$type->active = $obj->active;
$type->parent_label = $obj->parent_label;
$result[] = $type;
}
$this->db->free($resql);
}
return $result;
}
/**
* Fetch types grouped by level type
*
* @param int $activeOnly Only active types
* @return array Array grouped by level_type
*/
public function fetchGroupedByLevel($activeOnly = 1)
{
$all = $this->fetchAll($activeOnly);
$grouped = array();
foreach ($all as $type) {
$level = $type->level_type ?: 'other';
if (!isset($grouped[$level])) {
$grouped[$level] = array();
}
$grouped[$level][] = $type;
}
return $grouped;
}
/**
* Get level type label
*
* @return string Translated label
*/
public function getLevelTypeLabel()
{
global $langs;
$langs->load('kundenkarte@kundenkarte');
$labels = array(
self::LEVEL_BUILDING => $langs->trans('BuildingLevelBuilding'),
self::LEVEL_FLOOR => $langs->trans('BuildingLevelFloor'),
self::LEVEL_WING => $langs->trans('BuildingLevelWing'),
self::LEVEL_CORRIDOR => $langs->trans('BuildingLevelCorridor'),
self::LEVEL_ROOM => $langs->trans('BuildingLevelRoom'),
self::LEVEL_AREA => $langs->trans('BuildingLevelArea'),
);
return isset($labels[$this->level_type]) ? $labels[$this->level_type] : $this->level_type;
}
/**
* Get all level types with labels
*
* @return array Array of level_type => label
*/
public static function getLevelTypes()
{
global $langs;
$langs->load('kundenkarte@kundenkarte');
return array(
self::LEVEL_BUILDING => $langs->trans('BuildingLevelBuilding'),
self::LEVEL_FLOOR => $langs->trans('BuildingLevelFloor'),
self::LEVEL_WING => $langs->trans('BuildingLevelWing'),
self::LEVEL_CORRIDOR => $langs->trans('BuildingLevelCorridor'),
self::LEVEL_ROOM => $langs->trans('BuildingLevelRoom'),
self::LEVEL_AREA => $langs->trans('BuildingLevelArea'),
);
}
/**
* Get next available position
*
* @param string $levelType Level type
* @return int Next position
*/
public function getNextPosition($levelType = '')
{
$sql = "SELECT MAX(position) as maxpos FROM ".MAIN_DB_PREFIX.$this->table_element;
if ($levelType) {
$sql .= " WHERE level_type = '".$this->db->escape($levelType)."'";
}
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
return ($obj->maxpos ?: 0) + 10;
}
return 10;
}
}

View file

@ -416,6 +416,9 @@ class EquipmentConnection extends CommonObject
if ($this->medium_spec) {
$mediumInfo .= ' '.$this->medium_spec;
}
if ($this->medium_length) {
$mediumInfo .= ' ('.$this->medium_length.')';
}
$parts[] = $mediumInfo;
}

383
class/mediumtype.class.php Normal file
View file

@ -0,0 +1,383 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*/
/**
* Class MediumType
* Manages cable/wire types (Kabeltypen) for connections
*/
class MediumType extends CommonObject
{
public $element = 'mediumtype';
public $table_element = 'kundenkarte_medium_type';
public $ref;
public $label;
public $label_short;
public $description;
public $fk_system;
public $category;
public $default_spec;
public $available_specs; // JSON array
public $color;
public $picto;
public $fk_product;
public $is_system;
public $position;
public $active;
public $date_creation;
public $fk_user_creat;
public $fk_user_modif;
// Loaded properties
public $system_label;
public $system_code;
// Category constants
const CAT_STROMKABEL = 'stromkabel';
const CAT_NETZWERKKABEL = 'netzwerkkabel';
const CAT_LWL = 'lwl';
const CAT_KOAX = 'koax';
const CAT_SONSTIGES = 'sonstiges';
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Create object in database
*
* @param User $user User that creates
* @return int Return integer <0 if KO, Id of created object if OK
*/
public function create($user)
{
global $conf;
$error = 0;
$now = dol_now();
if (empty($this->ref) || empty($this->label)) {
$this->error = 'ErrorMissingParameters';
return -1;
}
$this->db->begin();
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, ref, label, label_short, description, fk_system, category,";
$sql .= " default_spec, available_specs, color, picto, fk_product,";
$sql .= " is_system, position, active, date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= "0"; // entity 0 = global
$sql .= ", '".$this->db->escape($this->ref)."'";
$sql .= ", '".$this->db->escape($this->label)."'";
$sql .= ", ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
$sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
$sql .= ", ".((int) $this->fk_system);
$sql .= ", ".($this->category ? "'".$this->db->escape($this->category)."'" : "NULL");
$sql .= ", ".($this->default_spec ? "'".$this->db->escape($this->default_spec)."'" : "NULL");
$sql .= ", ".($this->available_specs ? "'".$this->db->escape($this->available_specs)."'" : "NULL");
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", 0"; // is_system = 0 for user-created
$sql .= ", ".((int) $this->position);
$sql .= ", ".((int) ($this->active !== null ? $this->active : 1));
$sql .= ", '".$this->db->idate($now)."'";
$sql .= ", ".((int) $user->id);
$sql .= ")";
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if (!$error) {
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return $this->id;
}
}
/**
* Load object from database
*
* @param int $id ID of record
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
*/
public function fetch($id)
{
$sql = "SELECT t.*, s.label as system_label, s.code as system_code";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
$sql .= " WHERE t.rowid = ".((int) $id);
$resql = $this->db->query($sql);
if ($resql) {
if ($this->db->num_rows($resql)) {
$obj = $this->db->fetch_object($resql);
$this->id = $obj->rowid;
$this->entity = $obj->entity;
$this->ref = $obj->ref;
$this->label = $obj->label;
$this->label_short = $obj->label_short;
$this->description = $obj->description;
$this->fk_system = $obj->fk_system;
$this->category = $obj->category;
$this->default_spec = $obj->default_spec;
$this->available_specs = $obj->available_specs;
$this->color = $obj->color;
$this->picto = $obj->picto;
$this->fk_product = $obj->fk_product;
$this->is_system = $obj->is_system;
$this->position = $obj->position;
$this->active = $obj->active;
$this->date_creation = $this->db->jdate($obj->date_creation);
$this->fk_user_creat = $obj->fk_user_creat;
$this->fk_user_modif = $obj->fk_user_modif;
$this->system_label = $obj->system_label;
$this->system_code = $obj->system_code;
$this->db->free($resql);
return 1;
} else {
$this->db->free($resql);
return 0;
}
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Update object in database
*
* @param User $user User that modifies
* @return int Return integer <0 if KO, >0 if OK
*/
public function update($user)
{
$error = 0;
$this->db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
$sql .= " ref = '".$this->db->escape($this->ref)."'";
$sql .= ", label = '".$this->db->escape($this->label)."'";
$sql .= ", label_short = ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
$sql .= ", description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
$sql .= ", fk_system = ".((int) $this->fk_system);
$sql .= ", category = ".($this->category ? "'".$this->db->escape($this->category)."'" : "NULL");
$sql .= ", default_spec = ".($this->default_spec ? "'".$this->db->escape($this->default_spec)."'" : "NULL");
$sql .= ", available_specs = ".($this->available_specs ? "'".$this->db->escape($this->available_specs)."'" : "NULL");
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", position = ".((int) $this->position);
$sql .= ", active = ".((int) $this->active);
$sql .= ", fk_user_modif = ".((int) $user->id);
$sql .= " WHERE rowid = ".((int) $this->id);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return 1;
}
}
/**
* Delete object in database
*
* @param User $user User that deletes
* @return int Return integer <0 if KO, >0 if OK
*/
public function delete($user)
{
// Cannot delete system types
if ($this->is_system) {
$this->error = 'ErrorCannotDeleteSystemType';
return -2;
}
$error = 0;
$this->db->begin();
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return 1;
}
}
/**
* Fetch all medium types for a system
*
* @param int $systemId System ID (0 = all)
* @param int $activeOnly Only active types
* @return array Array of MediumType objects
*/
public function fetchAllBySystem($systemId = 0, $activeOnly = 1)
{
$results = array();
$sql = "SELECT t.*, s.label as system_label, s.code as system_code";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
$sql .= " WHERE 1 = 1";
if ($systemId > 0) {
// Show types for this system AND global types (fk_system = 0)
$sql .= " AND (t.fk_system = ".((int) $systemId)." OR t.fk_system = 0)";
}
if ($activeOnly) {
$sql .= " AND t.active = 1";
}
$sql .= " ORDER BY t.category ASC, t.position ASC, t.label ASC";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$type = new MediumType($this->db);
$type->id = $obj->rowid;
$type->ref = $obj->ref;
$type->label = $obj->label;
$type->label_short = $obj->label_short;
$type->description = $obj->description;
$type->fk_system = $obj->fk_system;
$type->category = $obj->category;
$type->default_spec = $obj->default_spec;
$type->available_specs = $obj->available_specs;
$type->color = $obj->color;
$type->picto = $obj->picto;
$type->fk_product = $obj->fk_product;
$type->is_system = $obj->is_system;
$type->position = $obj->position;
$type->active = $obj->active;
$type->system_label = $obj->system_label;
$type->system_code = $obj->system_code;
$results[] = $type;
}
$this->db->free($resql);
}
return $results;
}
/**
* Fetch all types grouped by category
*
* @param int $systemId System ID (0 = all)
* @return array Associative array: category => array of MediumType objects
*/
public function fetchGroupedByCategory($systemId = 0)
{
$all = $this->fetchAllBySystem($systemId, 1);
$grouped = array();
foreach ($all as $type) {
$cat = $type->category ?: 'sonstiges';
if (!isset($grouped[$cat])) {
$grouped[$cat] = array();
}
$grouped[$cat][] = $type;
}
return $grouped;
}
/**
* Get available specs as array
*
* @return array Array of specification strings
*/
public function getAvailableSpecsArray()
{
if (empty($this->available_specs)) {
return array();
}
$specs = json_decode($this->available_specs, true);
return is_array($specs) ? $specs : array();
}
/**
* Get category label
*
* @return string Translated category label
*/
public function getCategoryLabel()
{
global $langs;
switch ($this->category) {
case self::CAT_STROMKABEL:
return $langs->trans('MediumCatStromkabel');
case self::CAT_NETZWERKKABEL:
return $langs->trans('MediumCatNetzwerkkabel');
case self::CAT_LWL:
return $langs->trans('MediumCatLWL');
case self::CAT_KOAX:
return $langs->trans('MediumCatKoax');
case self::CAT_SONSTIGES:
default:
return $langs->trans('MediumCatSonstiges');
}
}
/**
* Get all category options
*
* @return array category_code => translated_label
*/
public static function getCategoryOptions()
{
global $langs;
return array(
self::CAT_STROMKABEL => $langs->trans('MediumCatStromkabel'),
self::CAT_NETZWERKKABEL => $langs->trans('MediumCatNetzwerkkabel'),
self::CAT_LWL => $langs->trans('MediumCatLWL'),
self::CAT_KOAX => $langs->trans('MediumCatKoax'),
self::CAT_SONSTIGES => $langs->trans('MediumCatSonstiges')
);
}
}

View file

@ -76,7 +76,7 @@ class modKundenKarte extends DolibarrModules
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@kundenkarte'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '3.2.1';
$this->version = '3.3.1';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';

View file

@ -4,7 +4,7 @@
*/
/* ========================================
TREE STRUCTURE
TREE STRUCTURE - Multiple parallel cable lines
======================================== */
.kundenkarte-tree {
@ -12,10 +12,153 @@
padding: 10px 0 !important;
}
/* Row container - holds cable lines + content */
.kundenkarte-tree-row {
display: flex !important;
align-items: stretch !important;
min-height: 36px !important;
}
/* Spacer row between cable groups */
.kundenkarte-tree-row.spacer-row {
min-height: 12px !important;
}
/* Cable line column - vertical line placeholder */
.cable-line {
width: 15px !important;
min-width: 15px !important;
position: relative !important;
flex-shrink: 0 !important;
}
/* Active vertical line (passes through this row to children below) */
.cable-line.active::before {
content: '' !important;
position: absolute !important;
left: 6px !important;
top: 0 !important;
width: 2px !important;
height: 100% !important;
background: #555 !important;
}
/* My cable line on connection row - vertical line + continues down */
.cable-line.my-line.conn-line::before {
content: '' !important;
position: absolute !important;
left: 6px !important;
top: 0 !important;
width: 2px !important;
height: 100% !important;
background: #8bc34a !important;
}
/* Horizontal connector from line - width set via CSS variable */
.cable-line.my-line.conn-line::after {
content: '' !important;
position: absolute !important;
left: 6px !important;
top: 50% !important;
width: var(--h-width, 8px) !important;
height: 2px !important;
background: #8bc34a !important;
z-index: 0 !important;
}
/* My cable line on node row - vertical line ends at center */
.cable-line.my-line.node-line::before {
content: '' !important;
position: absolute !important;
left: 6px !important;
top: 0 !important;
width: 2px !important;
height: 50% !important;
background: #8bc34a !important;
}
/* Horizontal connector to node - width set via CSS variable */
.cable-line.my-line.node-line::after {
content: '' !important;
position: absolute !important;
left: 6px !important;
top: 50% !important;
width: var(--h-width, 8px) !important;
height: 2px !important;
background: #8bc34a !important;
z-index: 0 !important;
}
/* Node content container - above horizontal lines */
.kundenkarte-tree-node-content {
flex: 1 !important;
position: relative !important;
z-index: 1 !important;
}
/* Connection content - above horizontal lines */
.kundenkarte-tree-conn-content {
position: relative !important;
z-index: 1 !important;
}
/* Legacy node styles (for root level) */
.kundenkarte-tree-node {
position: relative !important;
padding-left: 20px !important;
margin: 2px 0 !important;
padding-left: 30px !important;
}
/* Horizontaler Strich zum Element */
.kundenkarte-tree-node::after {
content: '' !important;
position: absolute !important;
left: 8px !important;
top: 18px !important;
width: 22px !important;
height: 2px !important;
background: #555 !important;
}
/* Senkrechter Strich (durchgehend für alle Geschwister) */
.kundenkarte-tree-node::before {
content: '' !important;
position: absolute !important;
left: 8px !important;
top: 0 !important;
width: 2px !important;
height: 100% !important;
background: #555 !important;
}
/* Letztes Kind: senkrechter Strich nur bis zur Mitte */
.kundenkarte-tree-node:last-child::before {
height: 20px !important;
}
/* Root-Ebene: keine Linien */
.kundenkarte-tree > .kundenkarte-tree-node::after,
.kundenkarte-tree > .kundenkarte-tree-node::before {
display: none !important;
}
.kundenkarte-tree > .kundenkarte-tree-node {
padding-left: 0 !important;
}
/* Tree-row at root level - no lines */
.kundenkarte-tree > .kundenkarte-tree-row .cable-line {
display: none !important;
}
/* Durchgeschleifte Elemente (kein eigenes Kabel) - am gleichen senkrechten Strich */
.kundenkarte-tree-node.no-cable {
/* Kein eigener Strich - nutzt den vorherigen */
}
/* Elemente mit eigenem Kabel - eigener senkrechter Strich, eingerückt */
.kundenkarte-tree-node:not(.no-cable) {
/* Standard-Darstellung mit eigenem Strich */
}
.kundenkarte-tree-item {
@ -27,6 +170,8 @@
border: 1px solid #444 !important;
cursor: pointer !important;
color: #e0e0e0 !important;
position: relative !important;
z-index: 1 !important;
}
.kundenkarte-tree-item:hover {
@ -100,9 +245,9 @@
}
.kundenkarte-tree-children {
margin-left: 10px !important;
border-left: 2px solid #444 !important;
padding-left: 10px !important;
position: relative !important;
margin-left: 20px !important;
padding-left: 0 !important;
}
.kundenkarte-tree-children.collapsed {
@ -1825,3 +1970,167 @@
.schematic-editor-canvas::-webkit-scrollbar-thumb:hover {
background: #555 !important;
}
/* ========================================
ANLAGE CONNECTIONS IN TREE VIEW
Simple cable connection display
======================================== */
/* Cable connection content (link inside row) */
.kundenkarte-tree-conn-content {
display: flex !important;
align-items: center !important;
gap: 6px !important;
flex: 1 !important;
padding: 4px 12px !important;
background: rgba(139, 195, 74, 0.1) !important;
border: 1px dashed #555 !important;
border-radius: 4px !important;
font-size: 12px !important;
cursor: pointer !important;
transition: all 0.15s ease !important;
text-decoration: none !important;
}
.kundenkarte-tree-conn-content:hover {
background: rgba(85, 85, 85, 0.25) !important;
}
/* Legacy conn styles (for root level - used by printTree) */
.kundenkarte-tree-conn {
display: flex !important;
align-items: center !important;
gap: 6px !important;
padding: 4px 12px 4px 40px !important;
margin: 2px 0 !important;
background: rgba(139, 195, 74, 0.1) !important;
border: 1px dashed #555 !important;
border-radius: 4px !important;
font-size: 12px !important;
cursor: pointer !important;
transition: all 0.15s ease !important;
text-decoration: none !important;
position: relative !important;
}
/* Senkrechter Strich durch die Kabelzeile (links, durchgehend nach unten) */
.kundenkarte-tree-conn::before {
content: '' !important;
position: absolute !important;
left: 8px !important;
top: 0 !important;
width: 2px !important;
height: 100% !important;
background: #555 !important;
}
/* Horizontaler Strich zur Kabelzeile */
.kundenkarte-tree-conn::after {
content: '' !important;
position: absolute !important;
left: 8px !important;
top: 50% !important;
width: 24px !important;
height: 2px !important;
background: #555 !important;
}
.kundenkarte-tree-conn:hover {
background: rgba(85, 85, 85, 0.25) !important;
}
.kundenkarte-tree-conn .conn-icon,
.kundenkarte-tree-conn-content .conn-icon {
color: #888 !important;
font-size: 11px !important;
}
.kundenkarte-tree-conn .conn-main,
.kundenkarte-tree-conn-content .conn-main {
color: #e0e0e0 !important;
font-weight: 500 !important;
}
.kundenkarte-tree-conn .conn-label,
.kundenkarte-tree-conn-content .conn-label {
color: #8bc34a !important;
background: rgba(139, 195, 74, 0.15) !important;
padding: 2px 8px !important;
border-radius: 3px !important;
font-size: 11px !important;
margin-left: auto !important;
}
.kundenkarte-tree-conn:hover .conn-label,
.kundenkarte-tree-conn-content:hover .conn-label {
background: rgba(139, 195, 74, 0.25) !important;
}
/* Connection tooltip (shown on hover) */
.kundenkarte-conn-tooltip {
position: absolute !important;
z-index: 10000 !important;
background: #1e1e1e !important;
border: 1px solid #27ae60 !important;
border-radius: 6px !important;
box-shadow: 0 4px 16px rgba(0,0,0,0.5) !important;
padding: 12px 15px !important;
min-width: 220px !important;
max-width: 320px !important;
display: none !important;
pointer-events: none !important;
font-family: inherit !important;
font-size: 13px !important;
}
.kundenkarte-conn-tooltip.visible {
display: block !important;
}
.kundenkarte-conn-tooltip-header {
display: flex !important;
align-items: center !important;
gap: 8px !important;
margin-bottom: 10px !important;
padding-bottom: 8px !important;
border-bottom: 1px solid #333 !important;
}
.kundenkarte-conn-tooltip-header i {
color: #27ae60 !important;
font-size: 16px !important;
}
.kundenkarte-conn-tooltip-header .conn-route {
color: #e0e0e0 !important;
font-weight: 500 !important;
}
.kundenkarte-conn-tooltip-fields {
display: grid !important;
grid-template-columns: auto 1fr !important;
gap: 4px 10px !important;
}
.kundenkarte-conn-tooltip-fields .field-label {
color: #888 !important;
font-size: 0.9em !important;
}
.kundenkarte-conn-tooltip-fields .field-value {
color: #e0e0e0 !important;
}
.kundenkarte-conn-tooltip-hint {
margin-top: 10px !important;
padding-top: 8px !important;
border-top: 1px solid #333 !important;
color: #666 !important;
font-size: 0.85em !important;
text-align: center !important;
}
.kundenkarte-conn-tooltip-hint i {
margin-right: 5px !important;
}

File diff suppressed because it is too large Load diff

View file

@ -165,6 +165,31 @@ ErrorSystemInUse = System kann nicht geloescht werden, da es noch verwendet wird
ErrorTypeInUse = Typ kann nicht geloescht werden, da er noch verwendet wird
ErrorParentNotAllowed = Dieses Element kann nicht unter dem ausgewaehlten Elternelement platziert werden
ErrorFieldRequired = Pflichtfeld nicht ausgefuellt
ErrorCircularReference = Zirkulaere Referenz: Das Element kann nicht unter sich selbst oder einem seiner Unterelemente platziert werden
ErrorMaxDepthExceeded = Maximale Verschachtelungstiefe ueberschritten
ErrorMissingParameters = Pflichtfelder fehlen. Bitte fuellen Sie alle erforderlichen Felder aus.
ErrorDatabaseConnection = Datenbankfehler. Bitte versuchen Sie es erneut.
ErrorFileUpload = Datei-Upload fehlgeschlagen. Bitte pruefen Sie Dateityp und Groesse.
ErrorFileTooLarge = Die Datei ist zu gross (max. %s MB)
ErrorInvalidFileType = Ungueltiger Dateityp. Erlaubt: %s
ErrorPermissionDenied = Sie haben keine Berechtigung fuer diese Aktion
ErrorRecordNotFound = Eintrag nicht gefunden
ErrorEquipmentNoSpace = Kein Platz auf der Hutschiene an dieser Position
ErrorCarrierFull = Die Hutschiene ist voll belegt
ErrorDuplicateRef = Diese Referenz existiert bereits
ErrorInvalidJson = Ungueltige JSON-Daten
ErrorSaveFailedDetails = Speichern fehlgeschlagen: %s
# Keyboard Shortcuts
KeyboardShortcuts = Tastenkuerzel
ShortcutSave = Speichern/Aktualisieren
ShortcutEscape = Abbrechen/Schliessen
ShortcutDelete = Loeschen
ShortcutZoomIn = Vergroessern
ShortcutZoomOut = Verkleinern
ShortcutZoomReset = Zoom zuruecksetzen
ShortcutZoomFit = An Fenster anpassen
ShortcutRefresh = Neu laden
# Setup Page
KundenKarteSetup = KundenKarte Einstellungen
@ -228,6 +253,7 @@ Circuit = Stromkreis
Connections = Verbindungen
Connection = Verbindung
AddConnection = Verbindung hinzufuegen
AddCableConnection = Kabelverbindung hinzufuegen
AddOutput = Abgang hinzufuegen
AddRail = Sammelschiene hinzufuegen
AddBusbar = Sammelschiene hinzufuegen
@ -317,3 +343,129 @@ Close = Schliessen
Confirm = Bestaetigen
Yes = Ja
No = Nein
# Bill of Materials (Stueckliste)
BillOfMaterials = Stueckliste
BOMFromSchematic = Stueckliste aus Schaltplan
GenerateBOM = Stueckliste generieren
BOMSummary = Zusammenfassung
BOMDetails = Detailliste
BOMReference = Referenz
BOMDescription = Beschreibung
BOMQuantity = Menge
BOMUnitPrice = Stueckpreis
BOMTotal = Gesamt
BOMTotalQuantity = Gesamtmenge
BOMEstimatedTotal = Geschaetzter Gesamtpreis
BOMNoProduct = Kein Produkt verknuepft
BOMCopyClipboard = In Zwischenablage kopieren
BOMCopied = Stueckliste in Zwischenablage kopiert
BOMCreateOrder = Bestellung erstellen
BOMNoItems = Keine Komponenten im Schaltplan gefunden
BOMLocation = Position
# Audit Log
AuditLog = Aenderungsprotokoll
AuditLogHistory = Aenderungsverlauf
AuditActionCreate = Erstellt
AuditActionUpdate = Geaendert
AuditActionDelete = Geloescht
AuditActionMove = Verschoben
AuditActionDuplicate = Kopiert
AuditActionStatus = Status geaendert
AuditFieldChanged = Geaendertes Feld
AuditOldValue = Alter Wert
AuditNewValue = Neuer Wert
AuditUser = Benutzer
AuditDate = Datum
AuditNoEntries = Keine Aenderungen protokolliert
AuditShowMore = Mehr anzeigen
Installation = Anlage
EquipmentType = Equipment-Typ
BusbarType = Sammelschienen-Typ
# Medium Types (Kabeltypen)
MediumTypes = Kabeltypen
MediumType = Kabeltyp
AddMediumType = Kabeltyp hinzufuegen
DeleteMediumType = Kabeltyp loeschen
ConfirmDeleteMediumType = Moechten Sie den Kabeltyp "%s" wirklich loeschen?
DefaultSpec = Standard-Spezifikation
DefaultSpecHelp = z.B. "3x1,5" fuer NYM
AvailableSpecs = Verfuegbare Spezifikationen
AvailableSpecsHelp = Kommagetrennt, z.B.: 3x1,5, 3x2,5, 5x1,5, 5x2,5
LabelShort = Kurzbezeichnung
MediumCatStromkabel = Stromkabel
MediumCatNetzwerkkabel = Netzwerkkabel
MediumCatLWL = Lichtwellenleiter
MediumCatKoax = Koaxialkabel
MediumCatSonstiges = Sonstiges
CableType = Kabeltyp
CableSpec = Querschnitt/Typ
CableLength = Laenge
CableLengthUnit = m
# Building Types (Gebaeudetypen)
BuildingTypes = Gebaeudetypen
BuildingType = Gebaeudetyp
AddBuildingType = Gebaeudetyp hinzufuegen
DeleteBuildingType = Gebaeudetyp loeschen
ConfirmDeleteBuildingType = Moechten Sie den Gebaeudetyp "%s" wirklich loeschen?
BuildingStructure = Gebaeudestruktur
IsGlobal = Global (alle Systeme)
AvailableForAllSystems = Verfuegbar fuer alle Systeme
BuildingTypesSetup = Gebaeudetypen
LevelType = Ebenen-Typ
FilterByLevel = Nach Ebene filtern
BuildingLevelBuilding = Gebaeude
BuildingLevelFloor = Etage/Geschoss
BuildingLevelWing = Gebaeudefluegel
BuildingLevelCorridor = Flur/Gang
BuildingLevelRoom = Raum
BuildingLevelArea = Bereich/Zone
CanHaveChildren = Kann Unterelemente haben
CannotDeleteTypeWithChildren = Kann nicht geloescht werden - wird als Eltern-Typ verwendet
CannotDeleteSystemType = System-Typen koennen nicht geloescht werden
# Tree Display Configuration
TreeDisplayConfig = Baum-Anzeige Konfiguration
TreeShowRef = Referenz anzeigen
TreeShowRefHelp = Zeigt die Referenznummer im Baum an
TreeShowLabel = Bezeichnung anzeigen
TreeShowLabelHelp = Zeigt die Bezeichnung/Namen im Baum an
TreeShowType = Typ anzeigen
TreeShowTypeHelp = Zeigt den Anlagentyp im Baum an
TreeShowIcon = Icon anzeigen
TreeShowIconHelp = Zeigt das Typ-Icon im Baum an
TreeShowStatus = Status anzeigen
TreeShowStatusHelp = Zeigt den Status (aktiv/inaktiv) im Baum an
TreeShowFields = Felder anzeigen
TreeShowFieldsHelp = Zeigt zusaetzliche Typ-Felder direkt im Baum an
TreeExpandDefault = Standardmaessig erweitert
TreeExpandDefaultHelp = Baum wird beim Laden automatisch erweitert
TreeIndentStyle = Einrueckungsstil
TreeIndentLines = Linien (Standard)
TreeIndentDots = Punkte
TreeIndentArrows = Pfeile
TreeIndentSimple = Einfach (nur Einrueckung)
# Anlagen-Verbindungen
AnlageConnections = Verbindungen
AnlageConnection = Verbindung
AddConnection = Verbindung hinzufuegen
EditConnection = Verbindung bearbeiten
DeleteConnection = Verbindung loeschen
ConfirmDeleteConnection = Moechten Sie diese Verbindung wirklich loeschen?
ConnectionFrom = Von
ConnectionTo = Nach
ConnectionSource = Quelle
ConnectionTarget = Ziel
ConnectionLabel = Bezeichnung
RouteDescription = Verlegungsweg
InstallationDate = Installationsdatum
SelectSource = Quelle waehlen...
SelectTarget = Ziel waehlen...
NoConnections = Keine Verbindungen vorhanden
ConnectionCreated = Verbindung erstellt
ConnectionUpdated = Verbindung aktualisiert
ConnectionDeleted = Verbindung geloescht

View file

@ -64,6 +64,16 @@ function kundenkarteAdminPrepareHead()
$head[$h][2] = 'busbar_types';
$h++;
$head[$h][0] = dol_buildpath("/kundenkarte/admin/medium_types.php", 1);
$head[$h][1] = $langs->trans("MediumTypes");
$head[$h][2] = 'medium_types';
$h++;
$head[$h][0] = dol_buildpath("/kundenkarte/admin/building_types.php", 1);
$head[$h][1] = $langs->trans("BuildingTypes");
$head[$h][2] = 'building_types';
$h++;
/*
$head[$h][0] = dol_buildpath("/kundenkarte/admin/myobject_extrafields.php", 1);
$head[$h][1] = $langs->trans("ExtraFields");

140
sql/data_building_types.sql Normal file
View file

@ -0,0 +1,140 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Default Building Types (Gebäudetypen)
-- ============================================================================
-- Clear existing system entries
DELETE FROM llx_kundenkarte_building_type WHERE is_system = 1;
-- ============================================================================
-- BUILDING LEVEL (Gebäude)
-- ============================================================================
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'HAUS', 'Haus', 'Haus', 'building', 'fa-home', '#3498db', 1, 1, 10, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'HALLE', 'Halle', 'Halle', 'building', 'fa-warehouse', '#e67e22', 1, 1, 20, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'STALL', 'Stall', 'Stall', 'building', 'fa-horse', '#8e44ad', 1, 1, 30, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'GARAGE', 'Garage', 'Garage', 'building', 'fa-car', '#2c3e50', 1, 1, 40, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'SCHUPPEN', 'Schuppen', 'Schuppen', 'building', 'fa-box', '#7f8c8d', 1, 1, 50, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'BUEROGEBAEUDE', 'Bürogebäude', 'Büro', 'building', 'fa-building', '#1abc9c', 1, 1, 60, 1, NOW());
-- ============================================================================
-- FLOOR LEVEL (Etage/Geschoss)
-- ============================================================================
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'KELLER', 'Keller', 'KG', 'floor', 'fa-level-down-alt', '#34495e', 1, 1, 100, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'ERDGESCHOSS', 'Erdgeschoss', 'EG', 'floor', 'fa-layer-group', '#27ae60', 1, 1, 110, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'OBERGESCHOSS', 'Obergeschoss', 'OG', 'floor', 'fa-level-up-alt', '#2980b9', 1, 1, 120, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'DACHGESCHOSS', 'Dachgeschoss', 'DG', 'floor', 'fa-home', '#9b59b6', 1, 1, 130, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'SPITZBODEN', 'Spitzboden', 'SB', 'floor', 'fa-mountain', '#95a5a6', 1, 1, 140, 1, NOW());
-- ============================================================================
-- WING LEVEL (Gebäudeflügel/Trakt)
-- ============================================================================
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'NORDFLUEGL', 'Nordflügel', 'Nord', 'wing', 'fa-compass', '#3498db', 1, 1, 200, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'SUEDFLUEGL', 'Südflügel', 'Süd', 'wing', 'fa-compass', '#e74c3c', 1, 1, 210, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'OSTFLUEGL', 'Ostflügel', 'Ost', 'wing', 'fa-compass', '#f39c12', 1, 1, 220, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'WESTFLUEGL', 'Westflügel', 'West', 'wing', 'fa-compass', '#2ecc71', 1, 1, 230, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'ANBAU', 'Anbau', 'Anbau', 'wing', 'fa-plus-square', '#1abc9c', 1, 1, 240, 1, NOW());
-- ============================================================================
-- CORRIDOR LEVEL (Flur/Gang)
-- ============================================================================
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'FLUR', 'Flur', 'Flur', 'corridor', 'fa-arrows-alt-h', '#16a085', 1, 1, 300, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'EINGANGSHALLE', 'Eingangshalle', 'Eingang', 'corridor', 'fa-door-open', '#2c3e50', 1, 1, 310, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'TREPPENHAUS', 'Treppenhaus', 'Treppe', 'corridor', 'fa-stairs', '#8e44ad', 1, 1, 320, 1, NOW());
-- ============================================================================
-- ROOM LEVEL (Räume)
-- ============================================================================
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'ZIMMER', 'Zimmer', 'Zi', 'room', 'fa-door-closed', '#3498db', 1, 0, 400, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'WOHNZIMMER', 'Wohnzimmer', 'WoZi', 'room', 'fa-couch', '#e67e22', 1, 0, 410, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'SCHLAFZIMMER', 'Schlafzimmer', 'SchZi', 'room', 'fa-bed', '#9b59b6', 1, 0, 420, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'KINDERZIMMER', 'Kinderzimmer', 'KiZi', 'room', 'fa-child', '#1abc9c', 1, 0, 430, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'KUECHE', 'Küche', '', 'room', 'fa-utensils', '#27ae60', 1, 0, 440, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'BAD', 'Badezimmer', 'Bad', 'room', 'fa-bath', '#3498db', 1, 0, 450, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'WC', 'WC/Toilette', 'WC', 'room', 'fa-toilet', '#95a5a6', 1, 0, 460, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'BUERO', 'Büro', 'Büro', 'room', 'fa-desktop', '#2c3e50', 1, 0, 470, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'ABSTELLRAUM', 'Abstellraum', 'Abst', 'room', 'fa-box', '#7f8c8d', 1, 0, 480, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'HAUSWIRTSCHAFT', 'Hauswirtschaftsraum', 'HWR', 'room', 'fa-tshirt', '#16a085', 1, 0, 490, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'WERKSTATT', 'Werkstatt', 'Werkst', 'room', 'fa-tools', '#f39c12', 1, 0, 500, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'TECHNIKRAUM', 'Technikraum', 'Tech', 'room', 'fa-cogs', '#e74c3c', 1, 1, 510, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'SERVERRAUM', 'Serverraum', 'Server', 'room', 'fa-server', '#8e44ad', 1, 1, 520, 1, NOW());
-- ============================================================================
-- AREA LEVEL (Bereiche/Zonen)
-- ============================================================================
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'AUSSENBEREICH', 'Außenbereich', 'Außen', 'area', 'fa-tree', '#27ae60', 1, 1, 600, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'TERRASSE', 'Terrasse', 'Terrasse', 'area', 'fa-sun', '#f1c40f', 1, 0, 610, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'GARTEN', 'Garten', 'Garten', 'area', 'fa-leaf', '#2ecc71', 1, 1, 620, 1, NOW());
INSERT INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'CARPORT', 'Carport', 'Carport', 'area', 'fa-car-side', '#34495e', 1, 0, 630, 1, NOW());

92
sql/data_medium_types.sql Normal file
View file

@ -0,0 +1,92 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Default Medium Types (Kabeltypen)
-- ============================================================================
-- Clear existing system entries
DELETE FROM llx_kundenkarte_medium_type WHERE is_system = 1;
-- ============================================================================
-- STROM - Elektrokabel (System: Strom, fk_system from c_kundenkarte_anlage_system)
-- ============================================================================
-- NYM-J (Mantelleitung für Innenbereich)
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'NYM-J', 'NYM-J Mantelleitung', 'NYM', 'stromkabel', 1, '3x1,5', '["1,5", "2,5", "4", "6", "10", "16", "3x1,5", "3x2,5", "3x4", "3x6", "5x1,5", "5x2,5", "5x4", "5x6", "5x10", "5x16"]', '#666666', 1, 10, 1, NOW());
-- NYY-J (Erdkabel)
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'NYY-J', 'NYY-J Erdkabel', 'NYY', 'stromkabel', 1, '3x1,5', '["3x1,5", "3x2,5", "3x4", "3x6", "5x1,5", "5x2,5", "5x4", "5x6", "5x10", "5x16", "5x25"]', '#333333', 1, 20, 1, NOW());
-- H07V-U (Aderleitung starr)
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'H07V-U', 'H07V-U Aderleitung starr', 'H07V-U', 'stromkabel', 1, '1,5', '["1,5", "2,5", "4", "6", "10"]', '#0066cc', 1, 30, 1, NOW());
-- H07V-K (Aderleitung flexibel)
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'H07V-K', 'H07V-K Aderleitung flexibel', 'H07V-K', 'stromkabel', 1, '1,5', '["1,5", "2,5", "4", "6", "10", "16", "25"]', '#3498db', 1, 40, 1, NOW());
-- H05VV-F (Schlauchleitung)
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'H05VV-F', 'H05VV-F Schlauchleitung', 'H05VV-F', 'stromkabel', 1, '3x1,0', '["2x0,75", "2x1,0", "3x0,75", "3x1,0", "3x1,5", "5x1,0", "5x1,5"]', '#ffffff', 1, 50, 1, NOW());
-- ============================================================================
-- NETZWERK - Datenkabel (System: Internet/Netzwerk)
-- ============================================================================
-- CAT5e
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'CAT5E', 'CAT5e Netzwerkkabel', 'CAT5e', 'netzwerkkabel', 2, 'U/UTP', '["U/UTP", "F/UTP", "S/FTP"]', '#f39c12', 1, 100, 1, NOW());
-- CAT6
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'CAT6', 'CAT6 Netzwerkkabel', 'CAT6', 'netzwerkkabel', 2, 'U/UTP', '["U/UTP", "F/UTP", "S/FTP", "S/STP"]', '#e67e22', 1, 110, 1, NOW());
-- CAT6a
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'CAT6A', 'CAT6a Netzwerkkabel', 'CAT6a', 'netzwerkkabel', 2, 'S/FTP', '["U/UTP", "F/UTP", "S/FTP", "S/STP"]', '#d35400', 1, 120, 1, NOW());
-- CAT7
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'CAT7', 'CAT7 Netzwerkkabel', 'CAT7', 'netzwerkkabel', 2, 'S/FTP', '["S/FTP", "S/STP"]', '#c0392b', 1, 130, 1, NOW());
-- CAT8
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'CAT8', 'CAT8 Netzwerkkabel', 'CAT8', 'netzwerkkabel', 2, 'S/FTP', '["S/FTP"]', '#8e44ad', 1, 140, 1, NOW());
-- LWL Singlemode
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'LWL-SM', 'LWL Singlemode', 'SM', 'lwl', 2, 'OS2 9/125', '["OS2 9/125", "2 Fasern", "4 Fasern", "8 Fasern", "12 Fasern", "24 Fasern"]', '#f1c40f', 1, 150, 1, NOW());
-- LWL Multimode
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'LWL-MM', 'LWL Multimode', 'MM', 'lwl', 2, 'OM3 50/125', '["OM1 62.5/125", "OM2 50/125", "OM3 50/125", "OM4 50/125", "OM5 50/125"]', '#2ecc71', 1, 160, 1, NOW());
-- ============================================================================
-- KOAX - Koaxialkabel (System: Kabelfernsehen/SAT)
-- ============================================================================
-- RG6
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'RG6', 'RG6 Koaxialkabel', 'RG6', 'koax', 3, '75 Ohm', '["75 Ohm", "Quad-Shield"]', '#1abc9c', 1, 200, 1, NOW());
-- RG59
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'RG59', 'RG59 Koaxialkabel', 'RG59', 'koax', 3, '75 Ohm', '["75 Ohm"]', '#16a085', 1, 210, 1, NOW());
-- SAT-Kabel
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'SAT-KOAX', 'SAT Koaxialkabel', 'SAT', 'koax', 4, '120dB', '["90dB", "100dB", "110dB", "120dB", "130dB"]', '#9b59b6', 1, 220, 1, NOW());
-- ============================================================================
-- GLOBAL - Für alle Systeme
-- ============================================================================
-- Leerrohr
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'LEERROHR', 'Leerrohr/Kabelkanal', 'Rohr', 'sonstiges', 0, 'M20', '["M16", "M20", "M25", "M32", "M40", "M50", "DN50", "DN75", "DN100"]', '#95a5a6', 1, 300, 1, NOW());
-- Kabelrinne
INSERT INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'KABELRINNE', 'Kabelrinne/Kabeltrasse', 'Rinne', 'sonstiges', 0, '100x60', '["60x40", "100x60", "200x60", "300x60", "400x60", "500x100"]', '#7f8c8d', 1, 310, 1, NOW());

View file

@ -16,6 +16,9 @@ CREATE TABLE llx_c_kundenkarte_anlage_system
picto varchar(64),
color varchar(8),
-- Tree display configuration (JSON)
tree_display_config text COMMENT 'JSON config for tree display options',
position integer DEFAULT 0,
active tinyint DEFAULT 1 NOT NULL
) ENGINE=innodb;

View file

@ -0,0 +1,7 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Keys for Anlage Connections table
-- ============================================================================
-- Foreign keys already defined in main table

View file

@ -0,0 +1,45 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Anlage Connections (Verbindungen zwischen Anlagen-Elementen im Baum)
-- Beschreibt Kabel/Leitungen zwischen Strukturelementen wie HAK → Zählerschrank
-- ============================================================================
CREATE TABLE IF NOT EXISTS llx_kundenkarte_anlage_connection
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
entity integer DEFAULT 1 NOT NULL,
-- Source and target anlagen
fk_source integer NOT NULL COMMENT 'Source Anlage ID',
fk_target integer NOT NULL COMMENT 'Target Anlage ID',
-- Connection description
label varchar(255) COMMENT 'Connection label/description',
-- Medium/Cable info (references medium_type table or free text)
fk_medium_type integer COMMENT 'Reference to medium_type table',
medium_type_text varchar(100) COMMENT 'Free text if no type selected',
medium_spec varchar(100) COMMENT 'Specification (e.g., 5x16)',
medium_length varchar(50) COMMENT 'Length (e.g., 15m)',
medium_color varchar(50) COMMENT 'Wire/cable color',
-- Additional info
route_description text COMMENT 'Description of cable route',
installation_date date COMMENT 'When was this installed',
-- Status
status integer DEFAULT 1,
note_private text,
note_public text,
date_creation datetime,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
fk_user_creat integer,
fk_user_modif integer,
INDEX idx_anlage_conn_source (fk_source),
INDEX idx_anlage_conn_target (fk_target),
CONSTRAINT fk_anlage_conn_source FOREIGN KEY (fk_source) REFERENCES llx_kundenkarte_anlage(rowid) ON DELETE CASCADE,
CONSTRAINT fk_anlage_conn_target FOREIGN KEY (fk_target) REFERENCES llx_kundenkarte_anlage(rowid) ON DELETE CASCADE
) ENGINE=innodb;

View file

@ -0,0 +1,8 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Keys for audit log table
-- Note: No foreign keys to avoid issues on module reactivation
-- ============================================================================
-- No additional keys needed - indexes are defined in the main table definition

View file

@ -0,0 +1,43 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Audit Log table for tracking changes to all KundenKarte objects
-- ============================================================================
CREATE TABLE IF NOT EXISTS llx_kundenkarte_audit_log
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
entity integer DEFAULT 1 NOT NULL,
-- Object identification
object_type varchar(64) NOT NULL COMMENT 'Type: equipment, carrier, panel, anlage, connection',
object_id integer NOT NULL COMMENT 'ID of the object',
object_ref varchar(128) COMMENT 'Reference/label of the object (for history)',
-- Related context
fk_societe integer COMMENT 'Customer/Third party',
fk_anlage integer COMMENT 'Installation/Anlage',
-- Action tracking
action varchar(32) NOT NULL COMMENT 'create, update, delete, move, duplicate',
field_changed varchar(64) COMMENT 'Specific field that was changed',
old_value text COMMENT 'Previous value (JSON for complex data)',
new_value text COMMENT 'New value (JSON for complex data)',
-- User and timestamp
fk_user integer NOT NULL COMMENT 'User who made the change',
user_login varchar(64) COMMENT 'Login name (cached for history)',
date_action datetime NOT NULL,
-- Optional notes
note text COMMENT 'Additional context',
-- IP tracking (optional, for security)
ip_address varchar(45) COMMENT 'IP address of the user',
INDEX idx_audit_object (object_type, object_id),
INDEX idx_audit_societe (fk_societe),
INDEX idx_audit_anlage (fk_anlage),
INDEX idx_audit_date (date_action),
INDEX idx_audit_user (fk_user)
) ENGINE=innodb;

View file

@ -0,0 +1,7 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Keys for Building Types table
-- ============================================================================
-- No foreign keys needed - fk_parent is self-referencing which is already defined

View file

@ -0,0 +1,42 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Building Types (Gebäudetypen) - global structure elements
-- These are system-independent and can be used across all installations
-- Examples: Haus, Stall, Halle, Saal, Eingangshalle, Flur, Zimmer, Südflügel
-- ============================================================================
CREATE TABLE IF NOT EXISTS llx_kundenkarte_building_type
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
entity integer DEFAULT 0 NOT NULL,
ref varchar(50) NOT NULL,
label varchar(128) NOT NULL,
label_short varchar(32),
description text,
-- Hierarchy
fk_parent integer DEFAULT 0, -- Parent building type (0 = root)
level_type varchar(32), -- 'building', 'floor', 'wing', 'room', 'area'
-- Display
icon varchar(64), -- FontAwesome icon class
color varchar(20),
picto varchar(128),
-- Settings
is_system tinyint DEFAULT 0, -- System-defined (not deletable)
can_have_children tinyint DEFAULT 1, -- Can contain sub-elements
position integer DEFAULT 0,
active tinyint DEFAULT 1,
date_creation datetime,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
fk_user_creat integer,
fk_user_modif integer,
UNIQUE KEY uk_building_type_ref (ref, entity),
INDEX idx_building_parent (fk_parent),
INDEX idx_building_level (level_type)
) ENGINE=innodb;

View file

@ -0,0 +1,7 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Keys for medium type table
-- ============================================================================
-- No additional keys needed

View file

@ -0,0 +1,47 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Medium Types (Kabeltypen, Leitungsarten) for connections
-- System-specific: NYM/NYY for electrical, CAT5/CAT6/LWL for network, etc.
-- ============================================================================
CREATE TABLE IF NOT EXISTS llx_kundenkarte_medium_type
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
entity integer DEFAULT 0 NOT NULL,
ref varchar(50) NOT NULL,
label varchar(128) NOT NULL,
label_short varchar(32),
description text,
-- System assignment (0 = all systems)
fk_system integer DEFAULT 0,
-- Category for grouping
category varchar(50), -- 'stromkabel', 'netzwerkkabel', 'lwl', 'koax', etc.
-- Default specifications
default_spec varchar(100), -- e.g., "3x1,5" for NYM
available_specs text, -- JSON array: ["3x1,5", "3x2,5", "5x1,5", "5x2,5", "5x4"]
-- Display
color varchar(20), -- Default wire color for display
picto varchar(128),
-- Linked product (optional)
fk_product integer,
is_system tinyint DEFAULT 0, -- System-defined (not deletable)
position integer DEFAULT 0,
active tinyint DEFAULT 1,
date_creation datetime,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
fk_user_creat integer,
fk_user_modif integer,
UNIQUE KEY uk_medium_type_ref (ref, entity),
INDEX idx_medium_system (fk_system),
INDEX idx_medium_category (category)
) ENGINE=innodb;

View file

@ -1,8 +1,123 @@
-- ============================================================================
-- KundenKarte Module Update 3.3.0
-- Correct terminal configurations (bidirectional format)
-- Audit Log table for tracking changes
-- Medium Types for connections (cable types)
-- Building Types (global structure elements)
-- Tree view display options per system
-- ============================================================================
-- Add tree display configuration to system table
ALTER TABLE llx_c_kundenkarte_anlage_system
ADD COLUMN tree_display_config TEXT COMMENT 'JSON config for tree display options';
-- Anlage Connections table (Verbindungen zwischen Anlagen-Elementen)
CREATE TABLE IF NOT EXISTS llx_kundenkarte_anlage_connection
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
entity integer DEFAULT 1 NOT NULL,
fk_source integer NOT NULL,
fk_target integer NOT NULL,
label varchar(255),
fk_medium_type integer,
medium_type_text varchar(100),
medium_spec varchar(100),
medium_length varchar(50),
medium_color varchar(50),
route_description text,
installation_date date,
status integer DEFAULT 1,
note_private text,
note_public text,
date_creation datetime,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
fk_user_creat integer,
fk_user_modif integer,
INDEX idx_anlage_conn_source (fk_source),
INDEX idx_anlage_conn_target (fk_target)
) ENGINE=innodb;
-- Building Types table (Gebäudetypen)
CREATE TABLE IF NOT EXISTS llx_kundenkarte_building_type
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
entity integer DEFAULT 0 NOT NULL,
ref varchar(50) NOT NULL,
label varchar(128) NOT NULL,
label_short varchar(32),
description text,
fk_parent integer DEFAULT 0,
level_type varchar(32),
icon varchar(64),
color varchar(20),
picto varchar(128),
is_system tinyint DEFAULT 0,
can_have_children tinyint DEFAULT 1,
position integer DEFAULT 0,
active tinyint DEFAULT 1,
date_creation datetime,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
fk_user_creat integer,
fk_user_modif integer,
UNIQUE KEY uk_building_type_ref (ref, entity),
INDEX idx_building_parent (fk_parent),
INDEX idx_building_level (level_type)
) ENGINE=innodb;
-- Medium Types table (Kabeltypen)
CREATE TABLE IF NOT EXISTS llx_kundenkarte_medium_type
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
entity integer DEFAULT 0 NOT NULL,
ref varchar(50) NOT NULL,
label varchar(128) NOT NULL,
label_short varchar(32),
description text,
fk_system integer DEFAULT 0,
category varchar(50),
default_spec varchar(100),
available_specs text,
color varchar(20),
picto varchar(128),
fk_product integer,
is_system tinyint DEFAULT 0,
position integer DEFAULT 0,
active tinyint DEFAULT 1,
date_creation datetime,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
fk_user_creat integer,
fk_user_modif integer,
UNIQUE KEY uk_medium_type_ref (ref, entity),
INDEX idx_medium_system (fk_system),
INDEX idx_medium_category (category)
) ENGINE=innodb;
-- Audit Log table
CREATE TABLE IF NOT EXISTS llx_kundenkarte_audit_log
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
entity integer DEFAULT 1 NOT NULL,
object_type varchar(64) NOT NULL,
object_id integer NOT NULL,
object_ref varchar(128),
fk_societe integer,
fk_anlage integer,
action varchar(32) NOT NULL,
field_changed varchar(64),
old_value text,
new_value text,
fk_user integer NOT NULL,
user_login varchar(64),
date_action datetime NOT NULL,
note text,
ip_address varchar(45),
INDEX idx_audit_object (object_type, object_id),
INDEX idx_audit_societe (fk_societe),
INDEX idx_audit_anlage (fk_anlage),
INDEX idx_audit_date (date_action),
INDEX idx_audit_user (fk_user)
) ENGINE=innodb;
-- FI (Fehlerstromschutzschalter) - 4 Terminals (2 oben: L+N, 2 unten: L+N)
UPDATE llx_kundenkarte_equipment_type
SET terminals_config = '{"terminals":[{"id":"t1","label":"L","pos":"top"},{"id":"t2","label":"N","pos":"top"},{"id":"t3","label":"L","pos":"bottom"},{"id":"t4","label":"N","pos":"bottom"}]}'
@ -52,3 +167,164 @@ WHERE ref IN ('SPD', 'UESP') AND (terminals_config IS NULL OR terminals_config L
UPDATE llx_kundenkarte_equipment_type
SET terminals_config = '{"terminals":[{"id":"t1","label":"●","pos":"top"},{"id":"t2","label":"●","pos":"bottom"}]}'
WHERE terminals_config IS NULL OR terminals_config = '';
-- ============================================================================
-- Default Medium Types (Kabeltypen)
-- Insert only if not exists
-- ============================================================================
-- NYM-J (Mantelleitung für Innenbereich)
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'NYM-J', 'NYM-J Mantelleitung', 'NYM', 'stromkabel', 1, '3x1,5', '["1,5", "2,5", "4", "6", "10", "16", "3x1,5", "3x2,5", "3x4", "3x6", "5x1,5", "5x2,5", "5x4", "5x6", "5x10", "5x16"]', '#666666', 1, 10, 1, NOW());
-- NYY-J (Erdkabel)
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'NYY-J', 'NYY-J Erdkabel', 'NYY', 'stromkabel', 1, '3x1,5', '["3x1,5", "3x2,5", "3x4", "3x6", "5x1,5", "5x2,5", "5x4", "5x6", "5x10", "5x16", "5x25"]', '#333333', 1, 20, 1, NOW());
-- H07V-U (Aderleitung starr)
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'H07V-U', 'H07V-U Aderleitung starr', 'H07V-U', 'stromkabel', 1, '1,5', '["1,5", "2,5", "4", "6", "10"]', '#0066cc', 1, 30, 1, NOW());
-- H07V-K (Aderleitung flexibel)
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'H07V-K', 'H07V-K Aderleitung flexibel', 'H07V-K', 'stromkabel', 1, '1,5', '["1,5", "2,5", "4", "6", "10", "16", "25"]', '#3498db', 1, 40, 1, NOW());
-- H05VV-F (Schlauchleitung)
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'H05VV-F', 'H05VV-F Schlauchleitung', 'H05VV-F', 'stromkabel', 1, '3x1,0', '["2x0,75", "2x1,0", "3x0,75", "3x1,0", "3x1,5", "5x1,0", "5x1,5"]', '#ffffff', 1, 50, 1, NOW());
-- CAT5e
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'CAT5E', 'CAT5e Netzwerkkabel', 'CAT5e', 'netzwerkkabel', 2, 'U/UTP', '["U/UTP", "F/UTP", "S/FTP"]', '#f39c12', 1, 100, 1, NOW());
-- CAT6
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'CAT6', 'CAT6 Netzwerkkabel', 'CAT6', 'netzwerkkabel', 2, 'U/UTP', '["U/UTP", "F/UTP", "S/FTP", "S/STP"]', '#e67e22', 1, 110, 1, NOW());
-- CAT6a
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'CAT6A', 'CAT6a Netzwerkkabel', 'CAT6a', 'netzwerkkabel', 2, 'S/FTP', '["U/UTP", "F/UTP", "S/FTP", "S/STP"]', '#d35400', 1, 120, 1, NOW());
-- CAT7
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'CAT7', 'CAT7 Netzwerkkabel', 'CAT7', 'netzwerkkabel', 2, 'S/FTP', '["S/FTP", "S/STP"]', '#c0392b', 1, 130, 1, NOW());
-- CAT8
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'CAT8', 'CAT8 Netzwerkkabel', 'CAT8', 'netzwerkkabel', 2, 'S/FTP', '["S/FTP"]', '#8e44ad', 1, 140, 1, NOW());
-- LWL Singlemode
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'LWL-SM', 'LWL Singlemode', 'SM', 'lwl', 2, 'OS2 9/125', '["OS2 9/125", "2 Fasern", "4 Fasern", "8 Fasern", "12 Fasern", "24 Fasern"]', '#f1c40f', 1, 150, 1, NOW());
-- LWL Multimode
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'LWL-MM', 'LWL Multimode', 'MM', 'lwl', 2, 'OM3 50/125', '["OM1 62.5/125", "OM2 50/125", "OM3 50/125", "OM4 50/125", "OM5 50/125"]', '#2ecc71', 1, 160, 1, NOW());
-- RG6
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'RG6', 'RG6 Koaxialkabel', 'RG6', 'koax', 3, '75 Ohm', '["75 Ohm", "Quad-Shield"]', '#1abc9c', 1, 200, 1, NOW());
-- RG59
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'RG59', 'RG59 Koaxialkabel', 'RG59', 'koax', 3, '75 Ohm', '["75 Ohm"]', '#16a085', 1, 210, 1, NOW());
-- SAT-Kabel
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'SAT-KOAX', 'SAT Koaxialkabel', 'SAT', 'koax', 4, '120dB', '["90dB", "100dB", "110dB", "120dB", "130dB"]', '#9b59b6', 1, 220, 1, NOW());
-- Leerrohr
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'LEERROHR', 'Leerrohr/Kabelkanal', 'Rohr', 'sonstiges', 0, 'M20', '["M16", "M20", "M25", "M32", "M40", "M50", "DN50", "DN75", "DN100"]', '#95a5a6', 1, 300, 1, NOW());
-- Kabelrinne
INSERT IGNORE INTO llx_kundenkarte_medium_type (entity, ref, label, label_short, category, fk_system, default_spec, available_specs, color, is_system, position, active, date_creation)
VALUES (0, 'KABELRINNE', 'Kabelrinne/Kabeltrasse', 'Rinne', 'sonstiges', 0, '100x60', '["60x40", "100x60", "200x60", "300x60", "400x60", "500x100"]', '#7f8c8d', 1, 310, 1, NOW());
-- ============================================================================
-- Default Building Types (Gebaeudetypen)
-- ============================================================================
-- BUILDING LEVEL
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'HAUS', 'Haus', 'Haus', 'building', 'fa-home', '#3498db', 1, 1, 10, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'HALLE', 'Halle', 'Halle', 'building', 'fa-warehouse', '#e67e22', 1, 1, 20, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'STALL', 'Stall', 'Stall', 'building', 'fa-horse', '#8e44ad', 1, 1, 30, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'GARAGE', 'Garage', 'Garage', 'building', 'fa-car', '#2c3e50', 1, 1, 40, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'BUEROGEBAEUDE', 'Buerogebaeude', 'Buero', 'building', 'fa-building', '#1abc9c', 1, 1, 50, 1, NOW());
-- FLOOR LEVEL
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'KELLER', 'Keller', 'KG', 'floor', 'fa-level-down-alt', '#34495e', 1, 1, 100, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'ERDGESCHOSS', 'Erdgeschoss', 'EG', 'floor', 'fa-layer-group', '#27ae60', 1, 1, 110, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'OBERGESCHOSS', 'Obergeschoss', 'OG', 'floor', 'fa-level-up-alt', '#2980b9', 1, 1, 120, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'DACHGESCHOSS', 'Dachgeschoss', 'DG', 'floor', 'fa-home', '#9b59b6', 1, 1, 130, 1, NOW());
-- WING LEVEL
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'NORDFLUEGL', 'Nordfluegel', 'Nord', 'wing', 'fa-compass', '#3498db', 1, 1, 200, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'SUEDFLUEGL', 'Suedfluegel', 'Sued', 'wing', 'fa-compass', '#e74c3c', 1, 1, 210, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'ANBAU', 'Anbau', 'Anbau', 'wing', 'fa-plus-square', '#1abc9c', 1, 1, 220, 1, NOW());
-- CORRIDOR LEVEL
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'FLUR', 'Flur', 'Flur', 'corridor', 'fa-arrows-alt-h', '#16a085', 1, 1, 300, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'EINGANGSHALLE', 'Eingangshalle', 'Eingang', 'corridor', 'fa-door-open', '#2c3e50', 1, 1, 310, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'TREPPENHAUS', 'Treppenhaus', 'Treppe', 'corridor', 'fa-stairs', '#8e44ad', 1, 1, 320, 1, NOW());
-- ROOM LEVEL
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'ZIMMER', 'Zimmer', 'Zi', 'room', 'fa-door-closed', '#3498db', 1, 0, 400, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'WOHNZIMMER', 'Wohnzimmer', 'WoZi', 'room', 'fa-couch', '#e67e22', 1, 0, 410, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'SCHLAFZIMMER', 'Schlafzimmer', 'SchZi', 'room', 'fa-bed', '#9b59b6', 1, 0, 420, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'KUECHE', 'Kueche', 'Kue', 'room', 'fa-utensils', '#27ae60', 1, 0, 430, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'BAD', 'Badezimmer', 'Bad', 'room', 'fa-bath', '#3498db', 1, 0, 440, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'BUERO', 'Buero', 'Buero', 'room', 'fa-desktop', '#2c3e50', 1, 0, 450, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'TECHNIKRAUM', 'Technikraum', 'Tech', 'room', 'fa-cogs', '#e74c3c', 1, 1, 460, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'SERVERRAUM', 'Serverraum', 'Server', 'room', 'fa-server', '#8e44ad', 1, 1, 470, 1, NOW());
-- AREA LEVEL
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'AUSSENBEREICH', 'Aussenbereich', 'Aussen', 'area', 'fa-tree', '#27ae60', 1, 1, 500, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'TERRASSE', 'Terrasse', 'Terrasse', 'area', 'fa-sun', '#f1c40f', 1, 0, 510, 1, NOW());
INSERT IGNORE INTO llx_kundenkarte_building_type (entity, ref, label, label_short, level_type, icon, color, is_system, can_have_children, position, active, date_creation)
VALUES (0, 'GARTEN', 'Garten', 'Garten', 'area', 'fa-leaf', '#2ecc71', 1, 1, 520, 1, NOW());

View file

@ -578,6 +578,14 @@ if (empty($customerSystems)) {
print '<button type="button" class="schematic-clear-connections" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#e74c3c;cursor:pointer;">';
print '<i class="fa fa-trash"></i> Alle Verbindungen löschen';
print '</button>';
// BOM (Stückliste) button
print '<button type="button" class="schematic-bom-generate" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#9b59b6;cursor:pointer;" title="Stückliste (Material) aus Schaltplan generieren">';
print '<i class="fa fa-list-alt"></i> Stückliste';
print '</button>';
// Audit Log button
print '<button type="button" class="schematic-audit-log" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#95a5a6;cursor:pointer;" title="Änderungsprotokoll anzeigen">';
print '<i class="fa fa-history"></i> Protokoll';
print '</button>';
// PDF Export button
$pdfExportUrl = dol_buildpath('/kundenkarte/ajax/export_schematic_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A4&orientation=L';
print '<a href="'.$pdfExportUrl.'" target="_blank" class="schematic-export-pdf" style="padding:5px 10px;background:#333;border:1px solid #555;border-radius:3px;color:#3498db;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:5px;" title="PDF Export (Leitungslaufplan nach DIN EN 61082)">';
@ -715,9 +723,22 @@ if (empty($customerSystems)) {
$db->free($resql);
}
// Pre-load all connections for this customer/system
dol_include_once('/kundenkarte/class/anlageconnection.class.php');
$connObj = new AnlageConnection($db);
$allConnections = $connObj->fetchBySociete($id, $systemId);
// Index by target_id for quick lookup (connection shows ABOVE the target element)
$connectionsByTarget = array();
foreach ($allConnections as $conn) {
if (!isset($connectionsByTarget[$conn->fk_target])) {
$connectionsByTarget[$conn->fk_target] = array();
}
$connectionsByTarget[$conn->fk_target][] = $conn;
}
if (!empty($tree)) {
print '<div class="kundenkarte-tree" data-system="'.$systemId.'">';
printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap);
print '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$id.'">';
printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget);
print '</div>';
} else {
print '<div class="opacitymedium">'.$langs->trans('NoInstallations').'</div>';
@ -738,7 +759,7 @@ $db->close();
/**
* Print tree recursively
*/
function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array())
function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array(), $connectionsByTarget = array())
{
foreach ($nodes as $node) {
$hasChildren = !empty($node->children);
@ -797,7 +818,45 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
}
}
print '<div class="kundenkarte-tree-node" style="margin-left:'.($level * 20).'px;">';
// Show cable connections TO this node (BEFORE the element - cable appears above verbraucher)
$hasConnection = !empty($connectionsByTarget[$node->id]);
if ($hasConnection) {
foreach ($connectionsByTarget[$node->id] as $conn) {
// Build cable info (type + spec + length)
$cableInfo = '';
if ($conn->medium_type_label || $conn->medium_type_text) {
$cableInfo = $conn->medium_type_label ?: $conn->medium_type_text;
}
if ($conn->medium_spec) {
$cableInfo .= ($cableInfo ? ' ' : '').$conn->medium_spec;
}
if ($conn->medium_length) {
$cableInfo .= ' ('.$conn->medium_length.')';
}
// If label exists, show cable info as badge. Otherwise show cable info as main text
$mainText = $conn->label ? $conn->label : $cableInfo;
$badgeText = $conn->label ? $cableInfo : '';
$connEditUrl = dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?id='.$conn->id.'&socid='.$socid.'&system_id='.$systemId;
print '<a href="'.$connEditUrl.'" class="kundenkarte-tree-conn">';
print '<span class="conn-icon"><i class="fa fa-plug"></i></span>';
if ($mainText) {
print '<span class="conn-main">'.dol_escape_htmltag($mainText).'</span>';
}
if ($badgeText) {
print ' <span class="conn-label">'.dol_escape_htmltag($badgeText).'</span>';
}
print '</a>';
}
}
// CSS class based on whether node has its own cable connection
$nodeClass = 'kundenkarte-tree-node';
if (!$hasConnection && $level > 0) {
$nodeClass .= ' no-cable'; // durchgeschleift - kein eigenes Kabel
}
print '<div class="'.$nodeClass.'">';
print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">';
// Toggle
@ -851,6 +910,7 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
if ($canEdit) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>';
print '<a href="#" class="anlage-connection-add" data-anlage-id="'.$node->id.'" data-soc-id="'.$socid.'" data-system-id="'.$systemId.'" title="'.$langs->trans('AddCableConnection').'"><i class="fa fa-plug"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
}
@ -861,10 +921,40 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
print '</div>';
// Children
// Children - vertical tree layout with multiple parallel cable lines
if ($hasChildren) {
print '<div class="kundenkarte-tree-children">';
printTree($node->children, $socid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap);
// First pass: assign cable index to each child with cable
$cableCount = 0;
$childCableIndex = array(); // child_id => cable_index
foreach ($node->children as $child) {
if (!empty($connectionsByTarget[$child->id])) {
$cableCount++;
$childCableIndex[$child->id] = $cableCount;
}
}
print '<div class="kundenkarte-tree-children" data-cable-count="'.$cableCount.'">';
// Render children - each row has cable line columns on the left
foreach ($node->children as $child) {
$myCableIdx = isset($childCableIndex[$child->id]) ? $childCableIndex[$child->id] : 0;
// Find which cable lines are still active (run past this row to children below)
// These are cables from children that appear AFTER this one in the list
$activeLinesAfter = array();
$foundCurrent = false;
foreach ($node->children as $c) {
if ($c->id == $child->id) {
$foundCurrent = true;
continue;
}
if ($foundCurrent && isset($childCableIndex[$c->id])) {
$activeLinesAfter[] = $childCableIndex[$c->id];
}
}
printTreeWithCableLines(array($child), $socid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap, $connectionsByTarget, $myCableIdx, $cableCount, $activeLinesAfter);
}
print '</div>';
}
@ -872,6 +962,271 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
}
}
/**
* Print tree with multiple parallel cable lines
* Each child with its own cable gets a vertical line that runs from top past all siblings
*/
function printTreeWithCableLines($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array(), $connectionsByTarget = array(), $myCableIndex = 0, $totalCables = 0, $activeLinesAfter = array())
{
foreach ($nodes as $node) {
$hasChildren = !empty($node->children);
$hasConnection = !empty($connectionsByTarget[$node->id]);
$fieldValues = $node->getFieldValues();
// Build tooltip data
$tooltipData = array(
'label' => $node->label,
'type' => $node->type_label,
'note_html' => $node->note_private ? nl2br(htmlspecialchars($node->note_private, ENT_QUOTES, 'UTF-8')) : '',
'fields' => array()
);
$treeInfo = array();
if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
if ($fieldDef->field_type === 'header') continue;
$value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
if ($fieldDef->show_in_hover && $value !== '') {
$displayValue = $value;
if ($fieldDef->field_type === 'date' && $value) {
$displayValue = dol_print_date(strtotime($value), 'day');
}
$tooltipData['fields'][$fieldDef->field_code] = array(
'label' => $fieldDef->field_label,
'value' => $displayValue,
'type' => $fieldDef->field_type
);
}
if ($fieldDef->show_in_tree && $value !== '') {
$treeInfo[] = ($fieldDef->field_type === 'date' && $value) ? dol_print_date(strtotime($value), 'day') : $value;
}
}
}
// All lines that need to be drawn for this row (active lines passing through + my line if I have cable)
$allLines = $activeLinesAfter;
if ($hasConnection && $myCableIndex > 0) {
$allLines[] = $myCableIndex;
}
sort($allLines);
// Cable connection row (BEFORE the element)
if ($hasConnection) {
foreach ($connectionsByTarget[$node->id] as $conn) {
$cableInfo = '';
if ($conn->medium_type_label || $conn->medium_type_text) {
$cableInfo = $conn->medium_type_label ?: $conn->medium_type_text;
}
if ($conn->medium_spec) {
$cableInfo .= ($cableInfo ? ' ' : '').$conn->medium_spec;
}
if ($conn->medium_length) {
$cableInfo .= ' ('.$conn->medium_length.')';
}
$mainText = $conn->label ? $conn->label : $cableInfo;
$badgeText = $conn->label ? $cableInfo : '';
$connEditUrl = dol_buildpath('/kundenkarte/anlage_connection.php', 1).'?id='.$conn->id.'&socid='.$socid.'&system_id='.$systemId;
print '<div class="kundenkarte-tree-row">';
// Draw vertical line columns (for cables passing through)
// Reverse order: index 1 (first element) = rightmost, highest index = leftmost
for ($i = $totalCables; $i >= 1; $i--) {
$isActive = in_array($i, $activeLinesAfter);
$isMyLine = ($i == $myCableIndex);
$lineClass = 'cable-line';
$inlineStyle = '';
if ($isActive) {
$lineClass .= ' active'; // Line continues down
}
if ($isMyLine) {
$lineClass .= ' my-line conn-line'; // This is my cable line - ends here with horizontal
// Calculate width: columns to the right * 15px + 8px to reach element border
$columnsToRight = $i - 1;
$horizontalWidth = ($columnsToRight * 15) + 8;
$inlineStyle = ' style="--h-width: '.$horizontalWidth.'px;"';
}
print '<span class="'.$lineClass.'"'.$inlineStyle.' data-line="'.$i.'"></span>';
}
print '<a href="'.$connEditUrl.'" class="kundenkarte-tree-conn-content">';
print '<span class="conn-icon"><i class="fa fa-plug"></i></span>';
if ($mainText) {
print '<span class="conn-main">'.dol_escape_htmltag($mainText).'</span>';
}
if ($badgeText) {
print ' <span class="conn-label">'.dol_escape_htmltag($badgeText).'</span>';
}
print '</a>';
print '</div>';
}
}
// Node row
$nodeClass = 'kundenkarte-tree-row node-row';
if (!$hasConnection && $level > 0) {
$nodeClass .= ' no-cable';
}
print '<div class="'.$nodeClass.'">';
// Draw vertical line columns (for cables passing through)
// Reverse order: index 1 (first element) = rightmost, highest index = leftmost
for ($i = $totalCables; $i >= 1; $i--) {
$isActive = in_array($i, $activeLinesAfter);
$isMyLine = ($i == $myCableIndex && $hasConnection);
$lineClass = 'cable-line';
$inlineStyle = '';
if ($isActive) {
$lineClass .= ' active';
}
if ($isMyLine) {
$lineClass .= ' my-line node-line'; // Horizontal connector to this node
// Calculate width: columns to the right * 15px + 8px to reach element border
$columnsToRight = $i - 1;
$horizontalWidth = ($columnsToRight * 15) + 8;
$inlineStyle = ' style="--h-width: '.$horizontalWidth.'px;"';
}
print '<span class="'.$lineClass.'"'.$inlineStyle.' data-line="'.$i.'"></span>';
}
print '<div class="kundenkarte-tree-node-content">';
print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">';
if ($hasChildren) {
print '<span class="kundenkarte-tree-toggle"><i class="fa fa-chevron-down"></i></span>';
} else {
print '<span class="kundenkarte-tree-toggle" style="visibility:hidden;"><i class="fa fa-chevron-down"></i></span>';
}
$picto = $node->type_picto ? $node->type_picto : 'fa-cube';
print '<span class="kundenkarte-tree-icon kundenkarte-tooltip-trigger" data-tooltip="'.htmlspecialchars(json_encode($tooltipData), ENT_QUOTES, 'UTF-8').'" data-anlage-id="'.$node->id.'">'.kundenkarte_render_icon($picto).'</span>';
$viewUrl = $_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
print '<span class="kundenkarte-tree-label">'.dol_escape_htmltag($node->label);
if (!empty($treeInfo)) {
print ' <span class="kundenkarte-tree-label-info">('.dol_escape_htmltag(implode(', ', $treeInfo)).')</span>';
}
if ($node->image_count > 0 || $node->doc_count > 0) {
print ' <span class="kundenkarte-tree-files">';
if ($node->image_count > 0) {
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-images" data-anlage-id="'.$node->id.'">';
print '<i class="fa fa-image"></i>';
if ($node->image_count > 1) print ' '.$node->image_count;
print '</a>';
}
if ($node->doc_count > 0) {
print '<a href="'.$viewUrl.'#files" class="kundenkarte-tree-file-badge kundenkarte-tree-file-docs" data-anlage-id="'.$node->id.'">';
print '<i class="fa fa-file-pdf-o"></i>';
if ($node->doc_count > 1) print ' '.$node->doc_count;
print '</a>';
}
print '</span>';
}
print '</span>';
if ($node->type_short || $node->type_label) {
$typeDisplay = $node->type_short ? $node->type_short : $node->type_label;
print '<span class="kundenkarte-tree-type badge badge-secondary">'.dol_escape_htmltag($typeDisplay).'</span>';
}
print '<span class="kundenkarte-tree-actions">';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
if ($canEdit) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>';
print '<a href="#" class="anlage-connection-add" data-anlage-id="'.$node->id.'" data-soc-id="'.$socid.'" data-system-id="'.$systemId.'" title="'.$langs->trans('AddCableConnection').'"><i class="fa fa-plug"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=copy&anlage_id='.$node->id.'" title="'.$langs->trans('Copy').'"><i class="fa fa-copy"></i></a>';
}
if ($canDelete) {
print '<a href="'.$_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=delete&anlage_id='.$node->id.'" title="'.$langs->trans('Delete').'" class="deletelink"><i class="fa fa-trash"></i></a>';
}
print '</span>';
print '</div>';
print '</div>'; // node-content
print '</div>'; // tree-row
// Children
if ($hasChildren) {
// First pass: assign cable index to each child with cable
$childCableCount = 0;
$childCableIndex = array(); // child_id => cable_index
foreach ($node->children as $child) {
if (!empty($connectionsByTarget[$child->id])) {
$childCableCount++;
$childCableIndex[$child->id] = $childCableCount;
}
}
print '<div class="kundenkarte-tree-children" data-cable-count="'.$childCableCount.'">';
$prevHadCable = false;
$isFirst = true;
foreach ($node->children as $child) {
$myCableIdx = isset($childCableIndex[$child->id]) ? $childCableIndex[$child->id] : 0;
$hasOwnCable = !empty($connectionsByTarget[$child->id]);
// Add spacer row before elements with their own cable (except first)
if ($hasOwnCable && !$isFirst) {
// Spacer row with active cable lines passing through
$spacerActiveLines = array();
$foundCurrent = false;
foreach ($node->children as $c) {
if ($c->id == $child->id) {
$foundCurrent = true;
}
if ($foundCurrent && isset($childCableIndex[$c->id])) {
$spacerActiveLines[] = $childCableIndex[$c->id];
}
}
print '<div class="kundenkarte-tree-row spacer-row">';
for ($i = $childCableCount; $i >= 1; $i--) {
$isActive = in_array($i, $spacerActiveLines);
$lineClass = 'cable-line';
if ($isActive) {
$lineClass .= ' active';
}
print '<span class="'.$lineClass.'" data-line="'.$i.'"></span>';
}
print '<div class="kundenkarte-tree-node-content"></div>';
print '</div>';
}
// Find which cable lines are still active (run past this row to children below)
$childActiveLinesAfter = array();
$foundCurrent = false;
foreach ($node->children as $c) {
if ($c->id == $child->id) {
$foundCurrent = true;
continue;
}
if ($foundCurrent && isset($childCableIndex[$c->id])) {
$childActiveLinesAfter[] = $childCableIndex[$c->id];
}
}
printTreeWithCableLines(array($child), $socid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap, $connectionsByTarget, $myCableIdx, $childCableCount, $childActiveLinesAfter);
$prevHadCable = $hasOwnCable;
$isFirst = false;
}
print '</div>';
}
}
}
/**
* Print single tree node (helper for connection line rendering)
*/
function printTreeNode($node, $socid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array(), $connectionsByTarget = array())
{
// Wrap single node in array and call printTree
printTree(array($node), $socid, $systemId, $canEdit, $canDelete, $langs, $level, $typeFieldsMap, $connectionsByTarget);
}
/**
* Print tree options for select
*/