';
// Select dropdown with add button
+ // Get current type's system for filtering (when editing)
+ $currentTypeSystem = ($action == 'edit') ? $anlageType->fk_system : GETPOSTINT('fk_system');
+
print '
';
print '
';
print ''.$langs->trans('SelectType').' ';
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 ''.dol_escape_htmltag($t->ref).' - '.dol_escape_htmltag($t->label).' ';
+
+ // 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 ''.dol_escape_htmltag($t->ref).' - '.dol_escape_htmltag($t->label).$systemHint.' ';
}
print ' ';
print '
'.$langs->trans('Add').'';
@@ -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");
});
';
diff --git a/admin/building_types.php b/admin/building_types.php
new file mode 100644
index 0000000..85939af
--- /dev/null
+++ b/admin/building_types.php
@@ -0,0 +1,351 @@
+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 = '
'.$langs->trans("BackToModuleList").' ';
+print load_fiche_titre($langs->trans($page_name), $linkback, 'object_kundenkarte@kundenkarte');
+
+print '
';
+
+$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 '
';
+
+// Add/Edit form
+if ($action == 'create' || $action == 'edit') {
+ print '
';
+
+ // Sync color inputs
+ print '';
+
+} else {
+ // List of building types
+ print '
';
+ print '
';
+
+ // Header
+ print '';
+ print ''.$langs->trans('Ref').' ';
+ print ''.$langs->trans('Label').' ';
+ print ''.$langs->trans('LevelType').' ';
+ print ''.$langs->trans('Icon').' ';
+ print ''.$langs->trans('Color').' ';
+ print ''.$langs->trans('Position').' ';
+ print ''.$langs->trans('Active').' ';
+ print ''.$langs->trans('Actions').' ';
+ print ' ';
+
+ // Fetch types
+ $types = $buildingType->fetchAll(0, $levelFilter);
+
+ if (count($types) > 0) {
+ foreach ($types as $type) {
+ print '';
+
+ // Ref
+ print ''.$type->ref;
+ if ($type->is_system) {
+ print ' '.$langs->trans('System').' ';
+ }
+ print ' ';
+
+ // Label
+ print '';
+ if ($type->icon) {
+ print ' ';
+ }
+ print dol_escape_htmltag($type->label);
+ if ($type->label_short) {
+ print ' ('.$type->label_short.') ';
+ }
+ print ' ';
+
+ // Level Type
+ print ''.$type->getLevelTypeLabel().' ';
+
+ // Icon
+ print '';
+ if ($type->icon) {
+ print ' '.$type->icon;
+ }
+ print ' ';
+
+ // Color
+ print '';
+ if ($type->color) {
+ print ' '.$type->color;
+ }
+ print ' ';
+
+ // Position
+ print ''.$type->position.' ';
+
+ // Active
+ print '';
+ print $type->active ? ''.$langs->trans('Yes').' ' : ''.$langs->trans('No').' ';
+ print ' ';
+
+ // Actions
+ print '';
+ print ''.img_edit().' ';
+ if (!$type->is_system) {
+ print ' '.img_delete().' ';
+ }
+ print ' ';
+
+ print ' ';
+ }
+ } else {
+ print ''.$langs->trans('NoRecordFound').' ';
+ }
+
+ print '
';
+ print '
';
+
+ // Add button
+ print '
';
+}
+
+print dol_get_fiche_end();
+print '
';
+
+llxFooter();
+$db->close();
diff --git a/admin/medium_types.php b/admin/medium_types.php
new file mode 100644
index 0000000..99cf839
--- /dev/null
+++ b/admin/medium_types.php
@@ -0,0 +1,332 @@
+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 = '
'.$langs->trans("BackToModuleList").' ';
+
+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 '
';
+ print ' ';
+ print ' ';
+ if ($action == 'edit') {
+ print ' ';
+ }
+
+ print '';
+
+ print '';
+
+ print ' ';
+
+} else {
+ // List view
+
+ // Button to add
+ print '
';
+
+ // Filter by category
+ $filterCategory = GETPOST('filter_category', 'alphanohtml');
+ print '
';
+ print ''.$langs->trans('FilterByCategory').': ';
+ print '';
+ print ''.$langs->trans('All').' ';
+ $categories = MediumType::getCategoryOptions();
+ foreach ($categories as $code => $label) {
+ $selected = ($filterCategory == $code) ? ' selected' : '';
+ print ''.dol_escape_htmltag($label).' ';
+ }
+ print ' ';
+ print ' ';
+
+ // List
+ $allTypes = $mediumType->fetchAllBySystem(0, 0);
+
+ print '
';
+ print '';
+ print ''.$langs->trans('Ref').' ';
+ print ''.$langs->trans('Label').' ';
+ print ''.$langs->trans('Category').' ';
+ print ''.$langs->trans('System').' ';
+ print ''.$langs->trans('DefaultSpec').' ';
+ print ''.$langs->trans('Color').' ';
+ print ''.$langs->trans('Position').' ';
+ print ''.$langs->trans('Status').' ';
+ print ''.$langs->trans('Actions').' ';
+ print ' ';
+
+ if (empty($allTypes)) {
+ print ''.$langs->trans('NoRecords').' ';
+ } else {
+ $i = 0;
+ foreach ($allTypes as $t) {
+ // Filter
+ if ($filterCategory && $t->category != $filterCategory) continue;
+
+ print '';
+ print ''.dol_escape_htmltag($t->ref).' ';
+ print ''.dol_escape_htmltag($t->label);
+ if ($t->label_short) print ' ('.dol_escape_htmltag($t->label_short).') ';
+ print ' ';
+ print ''.dol_escape_htmltag($t->getCategoryLabel()).' ';
+ print '';
+ if ($t->fk_system > 0 && $t->system_label) {
+ print dol_escape_htmltag($t->system_label);
+ } else {
+ print ''.$langs->trans('AllSystems').' ';
+ }
+ print ' ';
+ print ''.dol_escape_htmltag($t->default_spec).' ';
+ print ' ';
+ print ''.$t->position.' ';
+ print '';
+ print $t->active ? ''.$langs->trans('Enabled').' ' : ''.$langs->trans('Disabled').' ';
+ print ' ';
+ print '';
+ print ' ';
+ if (!$t->is_system) {
+ print ' ';
+ }
+ print ' ';
+ print ' ';
+ $i++;
+ }
+ }
+
+ print '
';
+}
+
+print dol_get_fiche_end();
+
+// JavaScript for color picker sync
+print '';
+
+llxFooter();
+$db->close();
diff --git a/ajax/anlage.php b/ajax/anlage.php
new file mode 100644
index 0000000..35675a2
--- /dev/null
+++ b/ajax/anlage.php
@@ -0,0 +1,137 @@
+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);
diff --git a/ajax/anlage_connection.php b/ajax/anlage_connection.php
new file mode 100644
index 0000000..7dad348
--- /dev/null
+++ b/ajax/anlage_connection.php
@@ -0,0 +1,193 @@
+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);
diff --git a/ajax/audit_log.php b/ajax/audit_log.php
new file mode 100644
index 0000000..9480657
--- /dev/null
+++ b/ajax/audit_log.php
@@ -0,0 +1,153 @@
+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);
diff --git a/ajax/bom_generator.php b/ajax/bom_generator.php
new file mode 100644
index 0000000..50f9557
--- /dev/null
+++ b/ajax/bom_generator.php
@@ -0,0 +1,250 @@
+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);
diff --git a/ajax/building_types.php b/ajax/building_types.php
new file mode 100644
index 0000000..36b1d00
--- /dev/null
+++ b/ajax/building_types.php
@@ -0,0 +1,121 @@
+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);
diff --git a/ajax/equipment.php b/ajax/equipment.php
index 4860023..0a5b316 100644
--- a/ajax/equipment.php
+++ b/ajax/equipment.php
@@ -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';
diff --git a/ajax/medium_types.php b/ajax/medium_types.php
new file mode 100644
index 0000000..261b9e3
--- /dev/null
+++ b/ajax/medium_types.php
@@ -0,0 +1,119 @@
+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);
diff --git a/ajax/tree_config.php b/ajax/tree_config.php
new file mode 100644
index 0000000..e18eaf2
--- /dev/null
+++ b/ajax/tree_config.php
@@ -0,0 +1,107 @@
+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);
diff --git a/anlage_connection.php b/anlage_connection.php
new file mode 100644
index 0000000..72c5902
--- /dev/null
+++ b/anlage_connection.php
@@ -0,0 +1,236 @@
+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 '
';
+print ' ';
+print ' ';
+print ' ';
+print ' ';
+print ' ';
+
+print load_fiche_titre($title, '', 'object_kundenkarte@kundenkarte');
+
+print '';
+
+print '';
+
+print ' ';
+
+llxFooter();
+$db->close();
diff --git a/class/anlage.class.php b/class/anlage.class.php
index 73b30b3..36f2415 100755
--- a/class/anlage.class.php
+++ b/class/anlage.class.php
@@ -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
*
diff --git a/class/anlageconnection.class.php b/class/anlageconnection.class.php
new file mode 100644
index 0000000..3d33973
--- /dev/null
+++ b/class/anlageconnection.class.php
@@ -0,0 +1,341 @@
+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);
+ }
+}
diff --git a/class/auditlog.class.php b/class/auditlog.class.php
new file mode 100644
index 0000000..fd049ab
--- /dev/null
+++ b/class/auditlog.class.php
@@ -0,0 +1,455 @@
+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);
+ }
+}
diff --git a/class/buildingtype.class.php b/class/buildingtype.class.php
new file mode 100644
index 0000000..e2473e6
--- /dev/null
+++ b/class/buildingtype.class.php
@@ -0,0 +1,362 @@
+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;
+ }
+}
diff --git a/class/equipmentconnection.class.php b/class/equipmentconnection.class.php
index c355b30..d1e7365 100644
--- a/class/equipmentconnection.class.php
+++ b/class/equipmentconnection.class.php
@@ -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;
}
diff --git a/class/mediumtype.class.php b/class/mediumtype.class.php
new file mode 100644
index 0000000..a417b6a
--- /dev/null
+++ b/class/mediumtype.class.php
@@ -0,0 +1,383 @@
+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')
+ );
+ }
+}
diff --git a/core/modules/modKundenKarte.class.php b/core/modules/modKundenKarte.class.php
index 04529f6..c748633 100755
--- a/core/modules/modKundenKarte.class.php
+++ b/core/modules/modKundenKarte.class.php
@@ -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';
diff --git a/css/kundenkarte.css b/css/kundenkarte.css
index 3a66628..77460e1 100755
--- a/css/kundenkarte.css
+++ b/css/kundenkarte.css
@@ -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;
+}
diff --git a/js/kundenkarte.js b/js/kundenkarte.js
index 43a6ed9..f79acfc 100755
--- a/js/kundenkarte.js
+++ b/js/kundenkarte.js
@@ -99,6 +99,56 @@
});
};
+ // Global error display with details
+ KundenKarte.showError = function(title, message, details) {
+ $('#kundenkarte-error-dialog').remove();
+
+ var html = '
';
+ html += '
';
+ html += '';
+ html += '
';
+ html += '
' + escapeHtml(message || 'Ein unbekannter Fehler ist aufgetreten.') + '
';
+ if (details) {
+ html += '
Technische Details ';
+ html += '' + escapeHtml(details) + ' ';
+ html += '';
+ }
+ html += '
';
+ html += '';
+ html += '
';
+
+ $('body').append(html);
+ $('#kundenkarte-error-dialog').addClass('visible');
+ $('#error-ok').focus();
+
+ var closeDialog = function() {
+ $('#kundenkarte-error-dialog').remove();
+ $(document).off('keydown.errorDialog');
+ };
+
+ $('#error-ok, #kundenkarte-error-dialog .kundenkarte-modal-close').on('click', closeDialog);
+ $(document).on('keydown.errorDialog', function(e) {
+ if (e.key === 'Escape' || e.key === 'Enter') closeDialog();
+ });
+ };
+
+ // Global success/info notification (non-blocking)
+ KundenKarte.showNotification = function(message, type) {
+ type = type || 'success';
+ var bgColor = type === 'success' ? '#27ae60' : (type === 'warning' ? '#f39c12' : (type === 'error' ? '#e74c3c' : '#3498db'));
+ var icon = type === 'success' ? 'fa-check' : (type === 'warning' ? 'fa-exclamation' : (type === 'error' ? 'fa-times' : 'fa-info'));
+
+ var $note = $('
' + escapeHtml(message) + '
');
+ $('body').append($note);
+
+ setTimeout(function() {
+ $note.fadeOut(300, function() { $(this).remove(); });
+ }, 3000);
+ };
+
// Get base URL for AJAX calls
var baseUrl = (typeof DOL_URL_ROOT !== 'undefined') ? DOL_URL_ROOT : '';
if (!baseUrl) {
@@ -3073,6 +3123,283 @@
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
+ },
+
+ // BOM (Bill of Materials) / Stückliste Dialog
+ showBOMDialog: function() {
+ var self = this;
+ var anlageId = this.anlageId;
+
+ // Show loading
+ var html = '
';
+ html += '
';
+ html += '';
+ html += '
';
+ html += '
Lade Stückliste...
';
+ html += '
';
+ html += '';
+ html += '
';
+
+ $('body').append(html);
+ $('#kundenkarte-bom-dialog').addClass('visible');
+
+ // Close handler
+ $('#bom-close, #kundenkarte-bom-dialog .kundenkarte-modal-close').on('click', function() {
+ $('#kundenkarte-bom-dialog').remove();
+ });
+
+ $(document).on('keydown.bomDialog', function(e) {
+ if (e.key === 'Escape') {
+ $('#kundenkarte-bom-dialog').remove();
+ $(document).off('keydown.bomDialog');
+ }
+ });
+
+ // Load BOM data
+ $.ajax({
+ url: baseUrl + '/custom/kundenkarte/ajax/bom_generator.php',
+ data: { action: 'generate', anlage_id: anlageId },
+ dataType: 'json',
+ success: function(response) {
+ if (response.success) {
+ self.renderBOMContent(response);
+ } else {
+ $('.bom-loading').html('
' + (response.error || 'Fehler beim Laden') + '
');
+ }
+ },
+ error: function() {
+ $('.bom-loading').html('
AJAX Fehler
');
+ }
+ });
+ },
+
+ renderBOMContent: function(data) {
+ var self = this;
+ var $body = $('#kundenkarte-bom-dialog .kundenkarte-modal-body');
+
+ if (!data.summary || data.summary.length === 0) {
+ $body.html('
Keine Materialien im Schaltplan gefunden. Fügen Sie Equipment hinzu oder verknüpfen Sie Produkte.
');
+ return;
+ }
+
+ var html = '';
+
+ // Summary table (grouped by product)
+ html += '
Zusammenfassung nach Produkt';
+ html += '
';
+ html += '';
+ html += 'Referenz ';
+ html += 'Bezeichnung ';
+ html += 'Menge ';
+ html += 'Einzelpreis ';
+ html += 'Gesamt ';
+ html += ' ';
+
+ var totalWithPrice = 0;
+ var totalQuantity = 0;
+
+ data.summary.forEach(function(item, index) {
+ var rowClass = index % 2 === 0 ? 'oddeven' : 'oddeven';
+ var hasProduct = item.product_id ? true : false;
+ var priceCell = hasProduct && item.price ? self.formatPrice(item.price) + ' €' : '-';
+ var totalCell = hasProduct && item.total ? self.formatPrice(item.total) + ' €' : '-';
+
+ html += '';
+ html += '' + self.escapeHtml(item.product_ref || '-') + ' ';
+ html += '' + self.escapeHtml(item.product_label || '-');
+ if (!hasProduct) {
+ html += ' (kein Produkt verknüpft) ';
+ }
+ html += ' ';
+ html += '' + item.quantity + ' ';
+ html += '' + priceCell + ' ';
+ html += '' + totalCell + ' ';
+ html += ' ';
+
+ totalQuantity += item.quantity;
+ if (hasProduct && item.total) {
+ totalWithPrice += item.total;
+ }
+ });
+
+ // Totals row
+ html += '';
+ html += 'Summe ';
+ html += '' + totalQuantity + ' Stück ';
+ html += ' ';
+ html += '' + self.formatPrice(totalWithPrice) + ' € ';
+ html += ' ';
+
+ html += '
';
+
+ // Detailed list (collapsible)
+ if (data.items && data.items.length > 0) {
+ html += '
';
+ html += ' Detailliste (' + data.items.length + ' Einträge) ';
+ html += '';
+ html += '';
+ html += 'Equipment ';
+ html += 'Typ ';
+ html += 'Feld/Hutschiene ';
+ html += 'Breite ';
+ html += 'Produkt ';
+ html += ' ';
+
+ data.items.forEach(function(item, index) {
+ var rowClass = index % 2 === 0 ? 'oddeven' : 'oddeven';
+ html += '';
+ html += '' + self.escapeHtml(item.equipment_label || '-') + ' ';
+ html += '' + self.escapeHtml(item.type_ref || '') + ' ' + self.escapeHtml(item.type_label || '-') + ' ';
+ html += '' + self.escapeHtml(item.panel_label || '-') + ' / ' + self.escapeHtml(item.carrier_label || '-') + ' ';
+ html += '' + (item.width_te || 1) + ' TE ';
+ html += '' + (item.product_ref ? self.escapeHtml(item.product_ref) : '- ') + ' ';
+ html += ' ';
+ });
+
+ html += '
';
+ html += ' ';
+ }
+
+ // Export buttons
+ html += '
';
+ html += ' In Zwischenablage kopieren ';
+ html += '
';
+
+ $body.html(html);
+
+ // Update totals in footer
+ $('.bom-totals').html('
' + totalQuantity + ' Artikel |
' + self.formatPrice(totalWithPrice) + ' € (geschätzt)');
+
+ // Copy to clipboard
+ $body.find('.bom-copy-clipboard').on('click', function() {
+ var text = 'Stückliste\n\n';
+ text += 'Referenz\tBezeichnung\tMenge\tEinzelpreis\tGesamt\n';
+ data.summary.forEach(function(item) {
+ text += (item.product_ref || '-') + '\t';
+ text += (item.product_label || '-') + '\t';
+ text += item.quantity + '\t';
+ text += (item.price ? self.formatPrice(item.price) + ' €' : '-') + '\t';
+ text += (item.total ? self.formatPrice(item.total) + ' €' : '-') + '\n';
+ });
+ text += '\nSumme:\t\t' + totalQuantity + ' Stück\t\t' + self.formatPrice(totalWithPrice) + ' €';
+
+ navigator.clipboard.writeText(text).then(function() {
+ KundenKarte.showNotification('In Zwischenablage kopiert', 'success');
+ }).catch(function() {
+ KundenKarte.showError('Fehler', 'Kopieren nicht möglich');
+ });
+ });
+ },
+
+ formatPrice: function(price) {
+ if (!price) return '0,00';
+ return parseFloat(price).toFixed(2).replace('.', ',');
+ },
+
+ // Audit Log Dialog
+ showAuditLogDialog: function() {
+ var self = this;
+ var anlageId = this.anlageId;
+
+ var html = '
';
+ html += '
';
+ html += '';
+ html += '
';
+ html += '
Lade Protokoll...
';
+ html += '
';
+ html += '';
+ html += '
';
+
+ $('body').append(html);
+ $('#kundenkarte-audit-dialog').addClass('visible');
+
+ // Close handler
+ $('#audit-close, #kundenkarte-audit-dialog .kundenkarte-modal-close').on('click', function() {
+ $('#kundenkarte-audit-dialog').remove();
+ });
+
+ $(document).on('keydown.auditDialog', function(e) {
+ if (e.key === 'Escape') {
+ $('#kundenkarte-audit-dialog').remove();
+ $(document).off('keydown.auditDialog');
+ }
+ });
+
+ // Load audit data
+ $.ajax({
+ url: baseUrl + '/custom/kundenkarte/ajax/audit_log.php',
+ data: { action: 'fetch_anlage', anlage_id: anlageId, limit: 100 },
+ dataType: 'json',
+ success: function(response) {
+ if (response.success) {
+ self.renderAuditContent(response.logs);
+ } else {
+ $('.audit-loading').html('
' + (response.error || 'Fehler beim Laden') + '
');
+ }
+ },
+ error: function() {
+ $('.audit-loading').html('
AJAX Fehler
');
+ }
+ });
+ },
+
+ renderAuditContent: function(logs) {
+ var self = this;
+ var $body = $('#kundenkarte-audit-dialog .kundenkarte-modal-body');
+
+ if (!logs || logs.length === 0) {
+ $body.html('
Keine Änderungen protokolliert.
');
+ return;
+ }
+
+ var html = '
';
+
+ logs.forEach(function(log) {
+ html += '
';
+ html += '
';
+ html += ' ';
+ html += '
';
+ html += '
';
+ html += '';
+ html += '
';
+ html += '' + self.escapeHtml(log.object_type_label) + ' ';
+ html += self.escapeHtml(log.object_ref || 'ID ' + log.object_id);
+ html += '
';
+
+ if (log.field_changed) {
+ html += '
';
+ html += 'Feld: ' + self.escapeHtml(log.field_changed);
+ if (log.old_value || log.new_value) {
+ html += ' (' + self.escapeHtml(log.old_value || '-') + ' → ' + self.escapeHtml(log.new_value || '-') + ' )';
+ }
+ html += '
';
+ }
+
+ if (log.note) {
+ html += '
' + self.escapeHtml(log.note) + '
';
+ }
+
+ html += '
';
+ html += ' ' + self.escapeHtml(log.user_name || log.user_login);
+ html += '
';
+ html += '
';
+ });
+
+ html += '
';
+
+ $body.html(html);
}
};
@@ -4552,6 +4879,18 @@
});
});
+ // BOM (Bill of Materials) / Stückliste
+ $(document).off('click.bomGenerate').on('click.bomGenerate', '.schematic-bom-generate', function(e) {
+ e.preventDefault();
+ self.showBOMDialog();
+ });
+
+ // Audit Log
+ $(document).off('click.auditLog').on('click.auditLog', '.schematic-audit-log', function(e) {
+ e.preventDefault();
+ self.showAuditLogDialog();
+ });
+
// Escape key - no longer needed for auto-selection, wire draw has its own handler
// Zoom with mouse wheel
@@ -4581,6 +4920,88 @@
self.zoomToFit();
});
+ // ======================================
+ // Keyboard Shortcuts
+ // ======================================
+ $(document).off('keydown.schematicShortcuts').on('keydown.schematicShortcuts', function(e) {
+ // Only handle shortcuts when we're in the schematic editor
+ if (!$('.schematic-editor-canvas').length) return;
+
+ // Don't handle if user is typing in an input/textarea
+ if ($(e.target).is('input, textarea, select, [contenteditable]')) return;
+
+ // Ctrl+S / Cmd+S - Save (reload data to ensure fresh state)
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
+ e.preventDefault();
+ self.showSaveIndicator();
+ return;
+ }
+
+ // Escape - Close all popups, cancel wire draw mode
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ // Close all popups
+ $('.schematic-equipment-popup, .schematic-carrier-popup, .schematic-connection-popup, .schematic-busbar-popup, .schematic-bridge-popup').remove();
+
+ // Cancel wire draw mode
+ if (self.wireDrawMode) {
+ self.cancelWireDraw();
+ }
+ return;
+ }
+
+ // Delete/Backspace - Delete selected equipment (if popup is open)
+ if (e.key === 'Delete' || e.key === 'Backspace') {
+ var $popup = $('.schematic-equipment-popup');
+ if ($popup.length) {
+ e.preventDefault();
+ var equipmentId = $popup.data('equipment-id');
+ if (equipmentId) {
+ $popup.remove();
+ self.deleteEquipment(equipmentId);
+ }
+ }
+ return;
+ }
+
+ // + / = - Zoom in
+ if (e.key === '+' || e.key === '=') {
+ e.preventDefault();
+ self.setZoom(self.scale + 0.1);
+ return;
+ }
+
+ // - - Zoom out
+ if (e.key === '-') {
+ e.preventDefault();
+ self.setZoom(self.scale - 0.1);
+ return;
+ }
+
+ // 0 - Reset zoom
+ if (e.key === '0' && !e.ctrlKey && !e.metaKey) {
+ e.preventDefault();
+ self.setZoom(1);
+ return;
+ }
+
+ // F - Fit to screen
+ if (e.key === 'f' || e.key === 'F') {
+ e.preventDefault();
+ self.zoomToFit();
+ return;
+ }
+
+ // R - Reload/Refresh data
+ if (e.key === 'r' || e.key === 'R') {
+ if (!e.ctrlKey && !e.metaKey) {
+ e.preventDefault();
+ self.loadData();
+ return;
+ }
+ }
+ });
+
// Bridge click - show delete popup
$(document).off('click.bridge').on('click.bridge', '.schematic-bridge', function(e) {
e.preventDefault();
@@ -4864,6 +5285,34 @@
$('.schematic-busbar-popup').remove();
},
+ // Show a brief save/refresh indicator
+ showSaveIndicator: function() {
+ var self = this;
+ var $indicator = $('
Gespeichert
');
+ $('body').append($indicator);
+
+ // Reload data to ensure fresh state
+ this.loadData();
+
+ setTimeout(function() {
+ $indicator.fadeOut(300, function() { $(this).remove(); });
+ }, 1500);
+ },
+
+ // Cancel wire draw mode
+ cancelWireDraw: function() {
+ this.wireDrawMode = false;
+ this.wireDrawPoints = [];
+ this.wireDrawSourceEq = null;
+ this.wireDrawSourceTerm = null;
+ // Remove any temporary wire preview
+ $('.schematic-wire-preview').remove();
+ // Remove the preview path if exists
+ if (this.svgElement) {
+ $(this.svgElement).find('.temp-wire-path').remove();
+ }
+ },
+
showBridgePopup: function(bridgeId, x, y) {
var self = this;
this.hideBridgePopup();
@@ -8235,10 +8684,29 @@
var self = this;
$('.schematic-output-dialog').remove();
+ // First load medium types from database
+ $.ajax({
+ url: baseUrl + '/custom/kundenkarte/ajax/medium_types.php',
+ data: { action: 'list_grouped', system_id: 0 },
+ dataType: 'json',
+ success: function(response) {
+ self.renderAbgangDialog(eqId, termId, x, y, existingOutput, response.success ? response.groups : []);
+ },
+ error: function() {
+ // Fallback with empty types
+ self.renderAbgangDialog(eqId, termId, x, y, existingOutput, []);
+ }
+ });
+ },
+
+ // Render the Abgang dialog with loaded medium types
+ renderAbgangDialog: function(eqId, termId, x, y, existingOutput, mediumGroups) {
+ var self = this;
+
var html = '
';
+ 'box-shadow:0 4px 20px rgba(0,0,0,0.5);z-index:100001;min-width:320px;max-width:400px;">';
html += '
' +
' Abgang (Ausgang) ';
@@ -8251,40 +8719,57 @@
'style="width:100%;padding:8px;border:1px solid #555;border-radius:4px;background:#1e1e1e;color:#fff;box-sizing:border-box;">';
html += '';
- // Kabeltyp
+ // Kabeltyp (from database)
html += '
';
html += 'Kabeltyp: ';
html += '';
html += '-- Auswählen -- ';
- ['NYM-J', 'NYY-J', 'H07V-U', 'H07V-K', 'H05VV-F', 'CAT6', 'CAT7', 'J-Y(St)Y'].forEach(function(t) {
- var selected = existingOutput && existingOutput.medium_type === t ? ' selected' : '';
- html += '' + t + ' ';
- });
+
+ if (mediumGroups && mediumGroups.length > 0) {
+ mediumGroups.forEach(function(group) {
+ html += '';
+ group.types.forEach(function(t) {
+ var selected = existingOutput && existingOutput.medium_type === t.ref ? ' selected' : '';
+ var specs = t.available_specs && t.available_specs.length > 0 ? ' data-specs=\'' + JSON.stringify(t.available_specs) + '\'' : '';
+ var defSpec = t.default_spec ? ' data-default="' + self.escapeHtml(t.default_spec) + '"' : '';
+ html += '' +
+ self.escapeHtml(t.label) + ' ';
+ });
+ html += ' ';
+ });
+ } else {
+ // Fallback
+ ['NYM-J', 'NYY-J', 'H07V-K', 'CAT6', 'CAT7'].forEach(function(t) {
+ var selected = existingOutput && existingOutput.medium_type === t ? ' selected' : '';
+ html += '' + t + ' ';
+ });
+ }
html += '
';
- // Kabelgröße
+ // Spezifikation (Querschnitt) - dynamic based on selected cable type
html += '
';
- html += 'Querschnitt: ';
- html += '';
- ['1.5', '2.5', '4', '6', '10', '16'].forEach(function(s) {
- var selected = existingOutput && existingOutput.medium_spec && existingOutput.medium_spec.indexOf(s) !== -1 ? ' selected' : '';
- html += '' + s + ' mm² ';
- });
+ html += 'Querschnitt/Typ: ';
+ html += '';
+ html += '-- Zuerst Kabeltyp wählen -- ';
html += '
';
- // Adernzahl + Phasentyp in einer Zeile
- html += '
';
- html += '
Adern: ';
- html += '';
- html += '3 5 2 ';
- html += '
';
- html += '
Phase: ';
+ // Länge
+ html += '
';
+ html += 'Länge (m): ';
+ html += ' ';
+ html += '
';
+
+ // Phase type
+ html += '
';
+ html += 'Phase/Anschluss: ';
html += '';
- ['L1N', 'L2N', 'L3N', 'N', '3P+N'].forEach(function(p) {
+ ['L1N', 'L2N', 'L3N', 'N', '3P+N', 'PE', 'DATA'].forEach(function(p) {
var selected = existingOutput && existingOutput.connection_type === p ? ' selected' : '';
html += '' + p + ' ';
});
- html += '
';
+ html += '
';
// Buttons
html += '
';
@@ -8303,6 +8788,32 @@
$('body').append(html);
$('.output-label').focus();
+ // Update specs dropdown when cable type changes
+ $('.output-cable-type').on('change', function() {
+ var $opt = $(this).find('option:selected');
+ var specs = $opt.data('specs');
+ var defSpec = $opt.data('default');
+ var $specSelect = $('.output-cable-spec');
+
+ $specSelect.empty();
+ if (specs && specs.length > 0) {
+ specs.forEach(function(s) {
+ var selected = (existingOutput && existingOutput.medium_spec === s) || (!existingOutput && s === defSpec) ? ' selected' : '';
+ $specSelect.append('
' + self.escapeHtml(s) + ' ');
+ });
+ } else {
+ $specSelect.append('
-- ');
+ }
+ });
+
+ // Trigger change to populate specs if editing
+ if (existingOutput && existingOutput.medium_type) {
+ $('.output-cable-type').val(existingOutput.medium_type).trigger('change');
+ if (existingOutput.medium_spec) {
+ $('.output-cable-spec').val(existingOutput.medium_spec);
+ }
+ }
+
$('.output-cancel-btn').on('click', function() { $('.schematic-output-dialog').remove(); });
$('.output-delete-btn').on('click', function() {
var id = $(this).data('id');
@@ -8312,15 +8823,14 @@
$('.output-save-btn').on('click', function() {
var label = $('.output-label').val();
var cableType = $('.output-cable-type').val();
- var cableSize = $('.output-cable-size').val();
- var cableCores = $('.output-cable-cores').val();
+ var cableSpec = $('.output-cable-spec').val();
+ var cableLength = $('.output-length').val();
var phaseType = $('.output-phase-type').val();
- var mediumSpec = cableCores + 'x' + (cableSize || '1.5');
if (existingOutput) {
- self.updateOutput(existingOutput.id, label, cableType, mediumSpec, phaseType);
+ self.updateOutput(existingOutput.id, label, cableType, cableSpec, phaseType, cableLength);
} else {
- self.createOutput(eqId, termId, label, cableType, mediumSpec, phaseType);
+ self.createOutput(eqId, termId, label, cableType, cableSpec, phaseType, cableLength);
}
$('.schematic-output-dialog').remove();
});
@@ -8393,7 +8903,7 @@
},
// Create a new cable output (no target, fk_target = NULL)
- createOutput: function(eqId, termId, label, cableType, cableSpec, phaseType) {
+ createOutput: function(eqId, termId, label, cableType, cableSpec, phaseType, cableLength) {
var self = this;
$.ajax({
@@ -8409,6 +8919,7 @@
output_label: label,
medium_type: cableType,
medium_spec: cableSpec,
+ medium_length: cableLength || '',
token: $('input[name="token"]').val()
},
dataType: 'json',
@@ -8427,7 +8938,7 @@
},
// Update existing output
- updateOutput: function(connId, label, cableType, cableSpec, phaseType) {
+ updateOutput: function(connId, label, cableType, cableSpec, phaseType, cableLength) {
var self = this;
$.ajax({
@@ -8440,6 +8951,7 @@
output_label: label,
medium_type: cableType,
medium_spec: cableSpec,
+ medium_length: cableLength || '',
token: $('input[name="token"]').val()
},
dataType: 'json',
@@ -9466,6 +9978,412 @@
// Initialize Schematic Editor
KundenKarte.SchematicEditor.init(anlageId);
}
+
+ // Initialize Anlage Connection handlers
+ KundenKarte.AnlageConnection.init();
});
+ // ===========================================
+ // Anlage Connections (cable connections between tree elements)
+ // ===========================================
+ KundenKarte.AnlageConnection = {
+ baseUrl: '',
+
+ init: function() {
+ var self = this;
+ this.baseUrl = (typeof baseUrl !== 'undefined') ? baseUrl : '';
+ this.tooltipTimer = null;
+
+ // Create tooltip container if not exists
+ if ($('#kundenkarte-conn-tooltip').length === 0) {
+ $('body').append('
');
+ }
+
+ // Add connection button
+ $(document).on('click', '.anlage-connection-add', function(e) {
+ e.preventDefault();
+ var anlageId = $(this).data('anlage-id');
+ var socId = $(this).data('soc-id');
+ var systemId = $(this).data('system-id');
+ self.showConnectionDialog(anlageId, socId, systemId, null);
+ });
+
+ // Edit connection (old style)
+ $(document).on('click', '.anlage-connection-edit', function(e) {
+ e.preventDefault();
+ var connId = $(this).data('id');
+ self.loadAndEditConnection(connId);
+ });
+
+ // Delete connection (old style)
+ $(document).on('click', '.anlage-connection-delete', function(e) {
+ e.preventDefault();
+ var connId = $(this).data('id');
+ self.deleteConnection(connId);
+ });
+
+ // Hover on connection row - show tooltip
+ $(document).on('mouseenter', '.kundenkarte-tree-conn', function(e) {
+ var $el = $(this);
+ var tooltipData = $el.data('conn-tooltip');
+ if (tooltipData) {
+ self.showConnTooltip(e, tooltipData);
+ }
+ });
+
+ $(document).on('mousemove', '.kundenkarte-tree-conn', function(e) {
+ self.moveConnTooltip(e);
+ });
+
+ $(document).on('mouseleave', '.kundenkarte-tree-conn', function() {
+ self.hideConnTooltip();
+ });
+ },
+
+ showConnTooltip: function(e, data) {
+ var $tooltip = $('#kundenkarte-conn-tooltip');
+
+ var html = '';
+
+ html += '
';
+
+ if (data.medium_type) {
+ html += 'Kabeltyp: ';
+ html += '' + this.escapeHtml(data.medium_type) + ' ';
+ }
+ if (data.medium_spec) {
+ html += 'Querschnitt: ';
+ html += '' + this.escapeHtml(data.medium_spec) + ' ';
+ }
+ if (data.medium_length) {
+ html += 'Länge: ';
+ html += '' + this.escapeHtml(data.medium_length) + ' ';
+ }
+ if (data.medium_color) {
+ html += 'Farbe: ';
+ html += '' + this.escapeHtml(data.medium_color) + ' ';
+ }
+ if (data.route_description) {
+ html += 'Route: ';
+ html += '' + this.escapeHtml(data.route_description) + ' ';
+ }
+ if (data.installation_date) {
+ html += 'Installiert: ';
+ html += '' + this.escapeHtml(data.installation_date) + ' ';
+ }
+ if (data.label) {
+ html += 'Beschreibung: ';
+ html += '' + this.escapeHtml(data.label) + ' ';
+ }
+
+ html += '
';
+ html += '
Klicken zum Bearbeiten
';
+
+ $tooltip.html(html);
+ this.moveConnTooltip(e);
+ $tooltip.addClass('visible');
+ },
+
+ moveConnTooltip: function(e) {
+ var $tooltip = $('#kundenkarte-conn-tooltip');
+ var x = e.pageX + 15;
+ var y = e.pageY + 10;
+
+ // Keep tooltip in viewport
+ var tooltipWidth = $tooltip.outerWidth();
+ var tooltipHeight = $tooltip.outerHeight();
+ var windowWidth = $(window).width();
+ var windowHeight = $(window).height();
+ var scrollTop = $(window).scrollTop();
+
+ if (x + tooltipWidth > windowWidth - 20) {
+ x = e.pageX - tooltipWidth - 15;
+ }
+ if (y + tooltipHeight > scrollTop + windowHeight - 20) {
+ y = e.pageY - tooltipHeight - 10;
+ }
+
+ $tooltip.css({ left: x, top: y });
+ },
+
+ hideConnTooltip: function() {
+ $('#kundenkarte-conn-tooltip').removeClass('visible');
+ },
+
+ loadAndEditConnection: function(connId) {
+ var self = this;
+ console.log('loadAndEditConnection called with connId:', connId);
+ $.ajax({
+ url: this.baseUrl + '/custom/kundenkarte/ajax/anlage_connection.php',
+ data: { action: 'get', connection_id: connId },
+ dataType: 'json',
+ success: function(response) {
+ console.log('Connection fetch response:', response);
+ if (response.success) {
+ // Get soc_id and system_id from the tree container or add button
+ var $tree = $('.kundenkarte-tree');
+ var socId = $tree.data('socid');
+ var systemId = $tree.data('system');
+ console.log('From tree element - socId:', socId, 'systemId:', systemId, '$tree.length:', $tree.length);
+
+ // Fallback to add button if tree doesn't have data
+ if (!socId) {
+ var $btn = $('.anlage-connection-add').first();
+ socId = $btn.data('soc-id');
+ systemId = $btn.data('system-id');
+ console.log('From fallback button - socId:', socId, 'systemId:', systemId);
+ }
+
+ self.showConnectionDialog(response.connection.fk_source, socId, systemId, response.connection);
+ } else {
+ KundenKarte.showAlert('Fehler', response.error || 'Fehler beim Laden');
+ }
+ }
+ });
+ },
+
+ showConnectionDialog: function(anlageId, socId, systemId, existingConn) {
+ var self = this;
+ console.log('showConnectionDialog called:', {anlageId: anlageId, socId: socId, systemId: systemId, existingConn: existingConn});
+
+ // Ensure systemId is a number (0 if not set)
+ systemId = systemId || 0;
+
+ // First load anlagen list and medium types
+ $.when(
+ $.ajax({
+ url: this.baseUrl + '/custom/kundenkarte/ajax/anlage.php',
+ data: { action: 'tree', socid: socId, system_id: systemId },
+ dataType: 'json'
+ }),
+ $.ajax({
+ url: this.baseUrl + '/custom/kundenkarte/ajax/medium_types.php',
+ data: { action: 'list_grouped', system_id: systemId },
+ dataType: 'json'
+ })
+ ).done(function(anlagenResp, mediumResp) {
+ console.log('AJAX responses:', {anlagenResp: anlagenResp, mediumResp: mediumResp});
+ var anlagen = anlagenResp[0].success ? anlagenResp[0].tree : [];
+ var mediumGroups = mediumResp[0].success ? mediumResp[0].groups : [];
+ console.log('Parsed data:', {anlagen: anlagen, mediumGroups: mediumGroups});
+ self.renderConnectionDialog(anlageId, anlagen, mediumGroups, existingConn);
+ });
+ },
+
+ flattenTree: function(tree, result, prefix) {
+ result = result || [];
+ prefix = prefix || '';
+ for (var i = 0; i < tree.length; i++) {
+ var node = tree[i];
+ result.push({
+ id: node.id,
+ label: prefix + (node.label || node.ref),
+ ref: node.ref
+ });
+ if (node.children && node.children.length > 0) {
+ this.flattenTree(node.children, result, prefix + ' ');
+ }
+ }
+ return result;
+ },
+
+ renderConnectionDialog: function(anlageId, anlagenTree, mediumGroups, existingConn) {
+ var self = this;
+ console.log('renderConnectionDialog - anlagenTree:', anlagenTree);
+ var anlagen = this.flattenTree(anlagenTree);
+ console.log('renderConnectionDialog - flattened anlagen:', anlagen);
+
+ // Remove existing dialog
+ $('#anlage-connection-dialog').remove();
+
+ var isEdit = existingConn !== null;
+ var title = isEdit ? 'Verbindung bearbeiten' : 'Neue Verbindung';
+
+ var html = '
';
+ html += '
';
+ html += '';
+ html += '
';
+
+ // Source
+ html += '
';
+ html += 'Von (Quelle): ';
+ html += '';
+ for (var i = 0; i < anlagen.length; i++) {
+ var sel = (existingConn && parseInt(existingConn.fk_source) === parseInt(anlagen[i].id)) || (!existingConn && parseInt(anlagen[i].id) === parseInt(anlageId)) ? ' selected' : '';
+ html += '' + self.escapeHtml(anlagen[i].label) + ' ';
+ }
+ html += '
';
+
+ // Target
+ html += '
';
+ html += 'Nach (Ziel): ';
+ html += '';
+ html += '-- Ziel wählen -- ';
+ for (var i = 0; i < anlagen.length; i++) {
+ var sel = (existingConn && parseInt(existingConn.fk_target) === parseInt(anlagen[i].id)) ? ' selected' : '';
+ html += '' + self.escapeHtml(anlagen[i].label) + ' ';
+ }
+ html += '
';
+
+ // Medium type
+ html += '
';
+ html += 'Kabeltyp: ';
+ html += '';
+ html += '-- Freitext oder wählen -- ';
+ for (var g = 0; g < mediumGroups.length; g++) {
+ html += '';
+ var types = mediumGroups[g].types;
+ for (var t = 0; t < types.length; t++) {
+ var sel = (existingConn && existingConn.fk_medium_type == types[t].id) ? ' selected' : '';
+ html += '' + self.escapeHtml(types[t].label) + ' ';
+ }
+ html += ' ';
+ }
+ html += ' ';
+ html += ' ';
+ html += '
';
+
+ // Spec
+ html += '
';
+ html += 'Querschnitt/Typ: ';
+ html += '-- ';
+ html += ' ';
+ html += '
';
+
+ // Length
+ html += '
';
+ html += 'Länge: ';
+ html += ' ';
+ html += '
';
+
+ // Label
+ html += '
';
+ html += 'Bezeichnung (optional): ';
+ html += ' ';
+ html += '
';
+
+ // Route description
+ html += '
';
+ html += 'Verlegungsweg (optional): ';
+ html += '' + self.escapeHtml(existingConn ? existingConn.route_description || '' : '') + ' ';
+ html += '
';
+
+ html += '
'; // modal-body
+
+ // Footer
+ html += '';
+
+ html += '
';
+
+ $('body').append(html);
+
+ // Spec dropdown update
+ $('.conn-medium-type').on('change', function() {
+ var $opt = $(this).find('option:selected');
+ var specs = $opt.data('specs') || [];
+ var defSpec = $opt.data('default') || '';
+ var $specSelect = $('.conn-spec');
+ $specSelect.empty().append('
-- ');
+ for (var i = 0; i < specs.length; i++) {
+ var sel = (existingConn && existingConn.medium_spec === specs[i]) || (!existingConn && specs[i] === defSpec) ? ' selected' : '';
+ $specSelect.append('
' + self.escapeHtml(specs[i]) + ' ');
+ }
+ });
+
+ // Trigger to populate specs if editing
+ if (existingConn && existingConn.fk_medium_type) {
+ $('.conn-medium-type').trigger('change');
+ if (existingConn.medium_spec) {
+ $('.conn-spec').val(existingConn.medium_spec);
+ }
+ }
+
+ // Close handlers
+ $('.conn-cancel, #anlage-connection-dialog .kundenkarte-modal-close').on('click', function() {
+ $('#anlage-connection-dialog').remove();
+ });
+
+ // Save handler
+ $('.conn-save').on('click', function() {
+ var data = {
+ fk_source: $('.conn-source').val(),
+ fk_target: $('.conn-target').val(),
+ fk_medium_type: $('.conn-medium-type').val(),
+ medium_type_text: $('.conn-medium-text').val(),
+ medium_spec: $('.conn-spec').val() || $('.conn-spec-text').val(),
+ medium_length: $('.conn-length').val(),
+ label: $('.conn-label').val(),
+ route_description: $('.conn-route').val(),
+ token: $('input[name="token"]').val()
+ };
+
+ if (!data.fk_target) {
+ KundenKarte.showAlert('Fehler', 'Bitte wählen Sie ein Ziel aus.');
+ return;
+ }
+
+ if (isEdit) {
+ data.action = 'update';
+ data.connection_id = existingConn.id;
+ } else {
+ data.action = 'create';
+ }
+
+ $.ajax({
+ url: self.baseUrl + '/custom/kundenkarte/ajax/anlage_connection.php',
+ method: 'POST',
+ data: data,
+ dataType: 'json',
+ success: function(response) {
+ if (response.success) {
+ $('#anlage-connection-dialog').remove();
+ location.reload();
+ } else {
+ KundenKarte.showAlert('Fehler', response.error || 'Speichern fehlgeschlagen');
+ }
+ }
+ });
+ });
+ },
+
+ deleteConnection: function(connId) {
+ var self = this;
+ KundenKarte.showConfirm('Verbindung löschen', 'Möchten Sie diese Verbindung wirklich löschen?', function(confirmed) {
+ if (confirmed) {
+ $.ajax({
+ url: self.baseUrl + '/custom/kundenkarte/ajax/anlage_connection.php',
+ method: 'POST',
+ data: {
+ action: 'delete',
+ connection_id: connId,
+ token: $('input[name="token"]').val()
+ },
+ dataType: 'json',
+ success: function(response) {
+ if (response.success) {
+ location.reload();
+ } else {
+ KundenKarte.showAlert('Fehler', response.error || 'Löschen fehlgeschlagen');
+ }
+ }
+ });
+ }
+ });
+ },
+
+ escapeHtml: function(text) {
+ if (!text) return '';
+ var div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+ };
+
})();
diff --git a/langs/de_DE/kundenkarte.lang b/langs/de_DE/kundenkarte.lang
index c841f1e..6a9e5ae 100755
--- a/langs/de_DE/kundenkarte.lang
+++ b/langs/de_DE/kundenkarte.lang
@@ -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
diff --git a/lib/kundenkarte.lib.php b/lib/kundenkarte.lib.php
index 0b2c08a..a79dc05 100755
--- a/lib/kundenkarte.lib.php
+++ b/lib/kundenkarte.lib.php
@@ -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");
diff --git a/sql/data_building_types.sql b/sql/data_building_types.sql
new file mode 100644
index 0000000..f7c29d0
--- /dev/null
+++ b/sql/data_building_types.sql
@@ -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', 'Kü', '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());
diff --git a/sql/data_medium_types.sql b/sql/data_medium_types.sql
new file mode 100644
index 0000000..c7c6afd
--- /dev/null
+++ b/sql/data_medium_types.sql
@@ -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());
diff --git a/sql/llx_c_kundenkarte_anlage_system.sql b/sql/llx_c_kundenkarte_anlage_system.sql
index 30ba6a6..3889add 100755
--- a/sql/llx_c_kundenkarte_anlage_system.sql
+++ b/sql/llx_c_kundenkarte_anlage_system.sql
@@ -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;
diff --git a/sql/llx_kundenkarte_anlage_connection.key.sql b/sql/llx_kundenkarte_anlage_connection.key.sql
new file mode 100644
index 0000000..13ee7d9
--- /dev/null
+++ b/sql/llx_kundenkarte_anlage_connection.key.sql
@@ -0,0 +1,7 @@
+-- ============================================================================
+-- Copyright (C) 2026 Alles Watt lauft
+--
+-- Keys for Anlage Connections table
+-- ============================================================================
+
+-- Foreign keys already defined in main table
diff --git a/sql/llx_kundenkarte_anlage_connection.sql b/sql/llx_kundenkarte_anlage_connection.sql
new file mode 100644
index 0000000..cf0278a
--- /dev/null
+++ b/sql/llx_kundenkarte_anlage_connection.sql
@@ -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;
diff --git a/sql/llx_kundenkarte_audit_log.key.sql b/sql/llx_kundenkarte_audit_log.key.sql
new file mode 100644
index 0000000..d0675f4
--- /dev/null
+++ b/sql/llx_kundenkarte_audit_log.key.sql
@@ -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
diff --git a/sql/llx_kundenkarte_audit_log.sql b/sql/llx_kundenkarte_audit_log.sql
new file mode 100644
index 0000000..d10ef67
--- /dev/null
+++ b/sql/llx_kundenkarte_audit_log.sql
@@ -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;
diff --git a/sql/llx_kundenkarte_building_type.key.sql b/sql/llx_kundenkarte_building_type.key.sql
new file mode 100644
index 0000000..2910976
--- /dev/null
+++ b/sql/llx_kundenkarte_building_type.key.sql
@@ -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
diff --git a/sql/llx_kundenkarte_building_type.sql b/sql/llx_kundenkarte_building_type.sql
new file mode 100644
index 0000000..3a9707a
--- /dev/null
+++ b/sql/llx_kundenkarte_building_type.sql
@@ -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;
diff --git a/sql/llx_kundenkarte_medium_type.key.sql b/sql/llx_kundenkarte_medium_type.key.sql
new file mode 100644
index 0000000..7013e36
--- /dev/null
+++ b/sql/llx_kundenkarte_medium_type.key.sql
@@ -0,0 +1,7 @@
+-- ============================================================================
+-- Copyright (C) 2026 Alles Watt lauft
+--
+-- Keys for medium type table
+-- ============================================================================
+
+-- No additional keys needed
diff --git a/sql/llx_kundenkarte_medium_type.sql b/sql/llx_kundenkarte_medium_type.sql
new file mode 100644
index 0000000..0c41e68
--- /dev/null
+++ b/sql/llx_kundenkarte_medium_type.sql
@@ -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;
diff --git a/sql/update_3.3.0.sql b/sql/update_3.3.0.sql
index f0362d9..8de62c6 100644
--- a/sql/update_3.3.0.sql
+++ b/sql/update_3.3.0.sql
@@ -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());
diff --git a/tabs/anlagen.php b/tabs/anlagen.php
index a91073c..eaa7875 100755
--- a/tabs/anlagen.php
+++ b/tabs/anlagen.php
@@ -578,6 +578,14 @@ if (empty($customerSystems)) {
print '
';
print ' Alle Verbindungen löschen';
print ' ';
+ // BOM (Stückliste) button
+ print '
';
+ print ' Stückliste';
+ print ' ';
+ // Audit Log button
+ print '
';
+ print ' Protokoll';
+ print ' ';
// PDF Export button
$pdfExportUrl = dol_buildpath('/kundenkarte/ajax/export_schematic_pdf.php', 1).'?anlage_id='.$anlageId.'&format=A4&orientation=L';
print '
';
@@ -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 '';
- printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap);
+ print '
';
+ printTree($tree, $id, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap, $connectionsByTarget);
print '
';
} else {
print '
'.$langs->trans('NoInstallations').'
';
@@ -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 '
';
+ // 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 '
';
+ print ' ';
+ if ($mainText) {
+ print ''.dol_escape_htmltag($mainText).' ';
+ }
+ if ($badgeText) {
+ print ' '.dol_escape_htmltag($badgeText).' ';
+ }
+ print ' ';
+ }
+ }
+
+ // 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 '
';
print '
';
// Toggle
@@ -851,6 +910,7 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
print '
';
if ($canEdit) {
print '
';
+ print '
';
print '
';
print '
';
}
@@ -861,10 +921,40 @@ function printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $lev
print '
';
- // Children
+ // Children - vertical tree layout with multiple parallel cable lines
if ($hasChildren) {
- print '
';
- 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 '
';
+
+ // 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 '
';
}
@@ -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 '
';
+
+ // 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 '
';
+ }
+
+ print '
';
+ print ' ';
+ if ($mainText) {
+ print ''.dol_escape_htmltag($mainText).' ';
+ }
+ if ($badgeText) {
+ print ' '.dol_escape_htmltag($badgeText).' ';
+ }
+ print ' ';
+ print '
';
+ }
+ }
+
+ // Node row
+ $nodeClass = 'kundenkarte-tree-row node-row';
+ if (!$hasConnection && $level > 0) {
+ $nodeClass .= ' no-cable';
+ }
+
+ print '
';
+
+ // 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 '
';
+ }
+
+ print '
';
+ print '
';
+
+ if ($hasChildren) {
+ print '
';
+ } else {
+ print '
';
+ }
+
+ $picto = $node->type_picto ? $node->type_picto : 'fa-cube';
+ print '
'.kundenkarte_render_icon($picto).' ';
+
+ $viewUrl = $_SERVER['PHP_SELF'].'?id='.$socid.'&system='.$systemId.'&action=view&anlage_id='.$node->id;
+ print '
'.dol_escape_htmltag($node->label);
+ if (!empty($treeInfo)) {
+ print ' ('.dol_escape_htmltag(implode(', ', $treeInfo)).') ';
+ }
+ if ($node->image_count > 0 || $node->doc_count > 0) {
+ print ' ';
+ if ($node->image_count > 0) {
+ print '';
+ print ' ';
+ if ($node->image_count > 1) print ' '.$node->image_count;
+ print ' ';
+ }
+ if ($node->doc_count > 0) {
+ print '';
+ print ' ';
+ if ($node->doc_count > 1) print ' '.$node->doc_count;
+ print ' ';
+ }
+ print ' ';
+ }
+ print ' ';
+
+ if ($node->type_short || $node->type_label) {
+ $typeDisplay = $node->type_short ? $node->type_short : $node->type_label;
+ print '
'.dol_escape_htmltag($typeDisplay).' ';
+ }
+
+ print '
';
+ print ' ';
+ if ($canEdit) {
+ print ' ';
+ print ' ';
+ print ' ';
+ print ' ';
+ }
+ if ($canDelete) {
+ print ' ';
+ }
+ print ' ';
+ print '
';
+ print '
'; // node-content
+
+ print '
'; // 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 '
';
+
+ $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 '
';
+ for ($i = $childCableCount; $i >= 1; $i--) {
+ $isActive = in_array($i, $spacerActiveLines);
+ $lineClass = 'cable-line';
+ if ($isActive) {
+ $lineClass .= ' active';
+ }
+ print '
';
+ }
+ print '
';
+ print '
';
+ }
+
+ // 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 '
';
+ }
+ }
+}
+
+/**
+ * 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
*/