diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c6c0113 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +CLAUDE_CODE_DISABLE_AUTO_MEMORY=0 \ No newline at end of file diff --git a/README.md b/README.md index 70b2019..87bf1c8 100755 --- a/README.md +++ b/README.md @@ -16,6 +16,9 @@ Das KundenKarte-Modul erweitert Dolibarr um zwei wichtige Funktionen fuer Kunden - Konfigurierbare Element-Typen mit individuellen Feldern - Datei-Upload mit Bild-Vorschau und PDF-Anzeige - Separate Verwaltung pro Kunde oder pro Kontakt/Adresse (z.B. verschiedene Gebaeude) +- Kabelverbindungen zwischen Anlagen-Elementen dokumentieren +- Visuelle Darstellung mit parallelen vertikalen Linien fuer jedes Kabel +- Automatische Gruppierung mit Abstaenden zwischen Kabel-Gruppen ### Verteilungsdokumentation (Schaltplan-Editor) - Interaktiver SVG-basierter Schaltplan-Editor diff --git a/admin/anlage_systems.php b/admin/anlage_systems.php index c95eff0..b4054a1 100755 --- a/admin/anlage_systems.php +++ b/admin/anlage_systems.php @@ -37,15 +37,29 @@ if ($action == 'add') { $color = GETPOST('color', 'alphanohtml'); $position = GETPOSTINT('position'); + // Tree display config + $treeConfig = array( + 'show_ref' => GETPOSTINT('show_ref'), + 'show_label' => GETPOSTINT('show_label'), + 'show_type' => GETPOSTINT('show_type'), + 'show_icon' => GETPOSTINT('show_icon'), + 'show_status' => GETPOSTINT('show_status'), + 'show_fields' => GETPOSTINT('show_fields'), + 'expand_default' => GETPOSTINT('expand_default'), + 'indent_style' => GETPOST('indent_style', 'alpha') ?: 'lines' + ); + $treeConfigJson = json_encode($treeConfig); + if (empty($code) || empty($label)) { setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors'); } else { $sql = "INSERT INTO ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system"; - $sql .= " (code, label, picto, color, position, active, entity)"; + $sql .= " (code, label, picto, color, position, active, entity, tree_display_config)"; $sql .= " VALUES ('".$db->escape(strtoupper($code))."', '".$db->escape($label)."',"; $sql .= " ".($picto ? "'".$db->escape($picto)."'" : "NULL").","; $sql .= " ".($color ? "'".$db->escape($color)."'" : "NULL").","; - $sql .= " ".((int) $position).", 1, 0)"; + $sql .= " ".((int) $position).", 1, 0,"; + $sql .= " '".$db->escape($treeConfigJson)."')"; $result = $db->query($sql); if ($result) { @@ -64,12 +78,26 @@ if ($action == 'update') { $color = GETPOST('color', 'alphanohtml'); $position = GETPOSTINT('position'); + // Tree display config + $treeConfig = array( + 'show_ref' => GETPOSTINT('show_ref'), + 'show_label' => GETPOSTINT('show_label'), + 'show_type' => GETPOSTINT('show_type'), + 'show_icon' => GETPOSTINT('show_icon'), + 'show_status' => GETPOSTINT('show_status'), + 'show_fields' => GETPOSTINT('show_fields'), + 'expand_default' => GETPOSTINT('expand_default'), + 'indent_style' => GETPOST('indent_style', 'alpha') ?: 'lines' + ); + $treeConfigJson = json_encode($treeConfig); + $sql = "UPDATE ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system SET"; $sql .= " code = '".$db->escape(strtoupper($code))."'"; $sql .= ", label = '".$db->escape($label)."'"; $sql .= ", picto = ".($picto ? "'".$db->escape($picto)."'" : "NULL"); $sql .= ", color = ".($color ? "'".$db->escape($color)."'" : "NULL"); $sql .= ", position = ".((int) $position); + $sql .= ", tree_display_config = '".$db->escape($treeConfigJson)."'"; $sql .= " WHERE rowid = ".((int) $systemId); $result = $db->query($sql); @@ -186,6 +214,73 @@ if ($action == 'create' || $action == 'edit') { print ''; + // Tree display configuration + print '

'.$langs->trans('TreeDisplayConfig').'

'; + + // Parse existing config + $treeConfig = array( + 'show_ref' => 1, + 'show_label' => 1, + 'show_type' => 1, + 'show_icon' => 1, + 'show_status' => 1, + 'show_fields' => 0, + 'expand_default' => 1, + 'indent_style' => 'lines' + ); + if ($system && !empty($system->tree_display_config)) { + $savedConfig = json_decode($system->tree_display_config, true); + if (is_array($savedConfig)) { + $treeConfig = array_merge($treeConfig, $savedConfig); + } + } + + print ''; + + print ''; + print ''; + + print ''; + print ''; + + print ''; + print ''; + + print ''; + print ''; + + print ''; + print ''; + + print ''; + print ''; + + print ''; + print ''; + + print ''; + print ''; + + print '
'.$langs->trans('TreeShowRef').' '; + print ''.$langs->trans('TreeShowRefHelp').'
'.$langs->trans('TreeShowLabel').' '; + print ''.$langs->trans('TreeShowLabelHelp').'
'.$langs->trans('TreeShowType').' '; + print ''.$langs->trans('TreeShowTypeHelp').'
'.$langs->trans('TreeShowIcon').' '; + print ''.$langs->trans('TreeShowIconHelp').'
'.$langs->trans('TreeShowStatus').' '; + print ''.$langs->trans('TreeShowStatusHelp').'
'.$langs->trans('TreeShowFields').' '; + print ''.$langs->trans('TreeShowFieldsHelp').'
'.$langs->trans('TreeExpandDefault').' '; + print ''.$langs->trans('TreeExpandDefaultHelp').'
'.$langs->trans('TreeIndentStyle').'
'; + print '
'; print ''; print ' '.$langs->trans('Cancel').''; diff --git a/admin/anlage_types.php b/admin/anlage_types.php index 742da01..23cbee3 100755 --- a/admin/anlage_types.php +++ b/admin/anlage_types.php @@ -327,7 +327,8 @@ if (in_array($action, array('create', 'edit'))) { } // Get all types for parent selection - $allTypes = $anlageType->fetchAllBySystem(0, 0); + // We need to filter by the same system OR show all types if this type is for all systems + $allTypes = $anlageType->fetchAllBySystem(0, 0); // Get all types, we'll filter in the template print '
'; print ''; @@ -393,13 +394,43 @@ if (in_array($action, array('create', 'edit'))) { print '
'; // 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 ''; @@ -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 '
'; +print ''; +print '
'; +print ''; +print ''; +print '
'; +print ''; +print '
'; + +// Add/Edit form +if ($action == 'create' || $action == 'edit') { + print '
'; + print ''; + print ''; + if ($action == 'edit') { + print ''; + } + + print ''; + + // Ref + print ''; + + // Label + print ''; + + // Label Short + print ''; + + // Level Type + print ''; + + // Icon + print ''; + + // Color + print ''; + + // Can have children + print ''; + + // Position + print ''; + + // Active + print ''; + + // Description + print ''; + + print '
'.$langs->trans('Ref').''; + if ($action == 'edit' && $buildingType->is_system) { + print ''; + print $buildingType->ref.' ('.$langs->trans('SystemType').')'; + } else { + print ''; + } + print '
'.$langs->trans('Label').''; + print ''; + print '
'.$langs->trans('LabelShort').''; + print ''; + print '
'.$langs->trans('LevelType').''; + print ''; + print '
'.$langs->trans('Icon').''; + print ''; + if ($buildingType->icon) { + print ' '; + } + print ' (FontAwesome, z.B. fa-home, fa-building)'; + print '
'.$langs->trans('Color').''; + print ''; + print ' '; + print '
'.$langs->trans('CanHaveChildren').''; + print 'can_have_children || $action != 'edit' ? ' checked' : '').'>'; + print '
'.$langs->trans('Position').''; + $defaultPos = $action == 'create' ? $buildingType->getNextPosition() : $buildingType->position; + print ''; + print '
'.$langs->trans('Active').''; + print 'active || $action != 'edit' ? ' checked' : '').'>'; + print '
'.$langs->trans('Description').''; + print ''; + print '
'; + + print '
'; + print ''; + print ' '.$langs->trans('Cancel').''; + print '
'; + + print '
'; + + // Sync color inputs + print ''; + +} else { + // List of building types + print '
'; + print ''; + + // Header + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + // Fetch types + $types = $buildingType->fetchAll(0, $levelFilter); + + if (count($types) > 0) { + foreach ($types as $type) { + print ''; + + // Ref + print ''; + + // Label + print ''; + + // Level Type + print ''; + + // Icon + print ''; + + // Color + print ''; + + // Position + print ''; + + // Active + print ''; + + // Actions + print ''; + + print ''; + } + } else { + print ''; + } + + print '
'.$langs->trans('Ref').''.$langs->trans('Label').''.$langs->trans('LevelType').''.$langs->trans('Icon').''.$langs->trans('Color').''.$langs->trans('Position').''.$langs->trans('Active').''.$langs->trans('Actions').'
'.$type->ref; + if ($type->is_system) { + print ' '.$langs->trans('System').''; + } + print ''; + if ($type->icon) { + print ' '; + } + print dol_escape_htmltag($type->label); + if ($type->label_short) { + print ' ('.$type->label_short.')'; + } + print ''.$type->getLevelTypeLabel().''; + if ($type->icon) { + print ' '.$type->icon; + } + print ''; + if ($type->color) { + print ' '.$type->color; + } + print ''.$type->position.''; + print $type->active ? ''.$langs->trans('Yes').'' : ''.$langs->trans('No').''; + print ''; + print ''.img_edit().''; + if (!$type->is_system) { + print ' '.img_delete().''; + } + print '
'.$langs->trans('NoRecordFound').'
'; + print '
'; + + // Add button + print '
'; + print ''.$langs->trans('AddBuildingType').''; + 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 ''; + + // Ref + print ''; + print ''; + + // Label + print ''; + print ''; + + // Label short + print ''; + print ''; + + // System + print ''; + print ''; + + // Category + print ''; + print ''; + + // Default spec + print ''; + print ''; + + // Available specs + $specsText = ''; + if ($mediumType->available_specs) { + $specsArray = json_decode($mediumType->available_specs, true); + if (is_array($specsArray)) { + $specsText = implode(', ', $specsArray); + } + } + print ''; + print ''; + + // Color + print ''; + print ''; + + // Position + print ''; + print ''; + + // Status + print ''; + print ''; + + // Description + print ''; + print ''; + + print '
'.$langs->trans('Ref').'
'.$langs->trans('Label').'
'.$langs->trans('LabelShort').'
'.$langs->trans('System').'
'.$langs->trans('Category').'
'.$langs->trans('DefaultSpec').''; + print '
'.$langs->trans('DefaultSpecHelp').'
'.$langs->trans('AvailableSpecs').''; + print '
'.$langs->trans('AvailableSpecsHelp').'
'.$langs->trans('Color').''; + print '
'.$langs->trans('Position').'
'.$langs->trans('Status').'
'.$langs->trans('Description').'
'; + + print '
'; + print ''; + print ' '.$langs->trans('Cancel').''; + print '
'; + + print '
'; + +} else { + // List view + + // Button to add + print '
'; + print ''.$langs->trans('AddMediumType').''; + print '
'; + + // Filter by category + $filterCategory = GETPOST('filter_category', 'alphanohtml'); + print '
'; + print ''; + print ''; + print '
'; + + // List + $allTypes = $mediumType->fetchAllBySystem(0, 0); + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + if (empty($allTypes)) { + print ''; + } else { + $i = 0; + foreach ($allTypes as $t) { + // Filter + if ($filterCategory && $t->category != $filterCategory) continue; + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + $i++; + } + } + + print '
'.$langs->trans('Ref').''.$langs->trans('Label').''.$langs->trans('Category').''.$langs->trans('System').''.$langs->trans('DefaultSpec').''.$langs->trans('Color').''.$langs->trans('Position').''.$langs->trans('Status').''.$langs->trans('Actions').'
'.$langs->trans('NoRecords').'
'.dol_escape_htmltag($t->ref).''.dol_escape_htmltag($t->label); + if ($t->label_short) print ' ('.dol_escape_htmltag($t->label_short).')'; + print ''.dol_escape_htmltag($t->getCategoryLabel()).''; + if ($t->fk_system > 0 && $t->system_label) { + print dol_escape_htmltag($t->system_label); + } else { + print ''.$langs->trans('AllSystems').''; + } + print ''.dol_escape_htmltag($t->default_spec).''.$t->position.''; + print $t->active ? ''.$langs->trans('Enabled').'' : ''.$langs->trans('Disabled').''; + print ''; + print ''; + if (!$t->is_system) { + print ' '; + } + 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 ''; + +// Source +print ''; +print ''; + +// Target +print ''; +print ''; + +// Medium type +print ''; +print ''; + +// Medium type text (free text) +print ''; +print ''; + +// Medium spec +print ''; +print ''; + +// Length +print ''; +print ''; + +// Color +print ''; +print ''; + +// Label +print ''; +print ''; + +// Route description +print ''; +print ''; + +// Installation date +print ''; +print ''; + +print '
'.$langs->trans('Von (Quelle)').'
'.$langs->trans('Nach (Ziel)').'
'.$langs->trans('Kabeltyp').'
'.$langs->trans('Kabeltyp (Freitext)').'
'.$langs->trans('Querschnitt/Typ').'
'.$langs->trans('Länge').'
'.$langs->trans('Farbe').'
'.$langs->trans('Bezeichnung').'
'.$langs->trans('Verlegungsweg').'
'.$langs->trans('Installationsdatum').'
'; + +print '
'; +print ''; +print ' '.$langs->trans('Cancel').''; + +if ($id > 0 && $user->hasRight('kundenkarte', 'write')) { + print ' '.$langs->trans('Delete').''; +} +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 += '

' + escapeHtml(title || 'Fehler') + '

'; + 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 += '

Stückliste (BOM)

'; + 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 += ''; + html += ''; + html += ''; + html += ''; + html += ''; + 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 += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + + totalQuantity += item.quantity; + if (hasProduct && item.total) { + totalWithPrice += item.total; + } + }); + + // Totals row + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + + html += '
ReferenzBezeichnungMengeEinzelpreisGesamt
' + self.escapeHtml(item.product_ref || '-') + '' + self.escapeHtml(item.product_label || '-'); + if (!hasProduct) { + html += ' (kein Produkt verknüpft)'; + } + html += '' + item.quantity + '' + priceCell + '' + totalCell + '
Summe' + totalQuantity + ' Stück' + self.formatPrice(totalWithPrice) + ' €
'; + + // Detailed list (collapsible) + if (data.items && data.items.length > 0) { + html += '
'; + html += ' Detailliste (' + data.items.length + ' Einträge)'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + + data.items.forEach(function(item, index) { + var rowClass = index % 2 === 0 ? 'oddeven' : 'oddeven'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + + html += '
EquipmentTypFeld/HutschieneBreiteProdukt
' + self.escapeHtml(item.equipment_label || '-') + '' + self.escapeHtml(item.type_ref || '') + ' ' + self.escapeHtml(item.type_label || '-') + '' + self.escapeHtml(item.panel_label || '-') + ' / ' + self.escapeHtml(item.carrier_label || '-') + '' + (item.width_te || 1) + ' TE' + (item.product_ref ? self.escapeHtml(item.product_ref) : '-') + '
'; + html += '
'; + } + + // Export buttons + html += '
'; + html += ''; + 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 += '

Änderungsprotokoll

'; + 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 += '' + self.escapeHtml(log.action_label) + ''; + html += '' + self.escapeHtml(log.date_action) + ''; + 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 += ''; html += '
'; - // Kabelgröße + // Spezifikation (Querschnitt) - dynamic based on selected cable type html += '
'; - html += ''; - html += ''; + html += ''; html += '
'; - // Adernzahl + Phasentyp in einer Zeile - html += '
'; - html += '
'; - html += '
'; - html += '
'; + // Länge + html += '
'; + html += ''; + html += ''; + html += '
'; + + // Phase type + html += '
'; + html += ''; 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(''); + }); + } 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 += ''; + html += '' + this.escapeHtml(data.source || '') + ' → ' + this.escapeHtml(data.target || '') + ''; + 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 += '

' + title + '

'; + html += '×
'; + html += '
'; + + // Source + html += '
'; + html += ''; + html += '
'; + + // Target + html += '
'; + html += ''; + html += '
'; + + // Medium type + html += '
'; + html += ''; + html += ''; + html += ''; + html += '
'; + + // Spec + html += '
'; + html += ''; + html += ''; + html += ''; + html += '
'; + + // Length + html += '
'; + html += ''; + html += ''; + html += '
'; + + // Label + html += '
'; + html += ''; + html += ''; + html += '
'; + + // Route description + html += '
'; + html += ''; + html += ''; + 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(''); + } + }); + + // 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 ''; + // BOM (Stückliste) button + print ''; + // Audit Log button + 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 */