From 3de514308b9317542b99e987d619a5da82bff838 Mon Sep 17 00:00:00 2001 From: data Date: Wed, 11 Feb 2026 06:50:23 +0100 Subject: [PATCH] Neuer Editor neue Idee --- admin/anlage_types.php | 8 + admin/equipment_types.php | 670 +++ ajax/equipment.php | 355 ++ ajax/equipment_carrier.php | 188 + ajax/equipment_connection.php | 348 ++ ajax/equipment_panel.php | 201 + ajax/equipment_type_fields.php | 74 + class/anlagetype.class.php | 7 +- class/equipment.class.php | 494 ++ class/equipmentcarrier.class.php | 426 ++ class/equipmentconnection.class.php | 419 ++ class/equipmentpanel.class.php | 285 ++ class/equipmenttype.class.php | 374 ++ core/modules/modKundenKarte.class.php | 19 +- css/kundenkarte.css | 1180 ++++- js/kundenkarte.js | 4329 +++++++++++++++++ langs/de_DE/kundenkarte.lang | 77 + lib/kundenkarte.lib.php | 5 + sql/llx_kundenkarte_equipment.key.sql | 10 + sql/llx_kundenkarte_equipment.sql | 36 + sql/llx_kundenkarte_equipment_carrier.key.sql | 8 + sql/llx_kundenkarte_equipment_carrier.sql | 26 + ...x_kundenkarte_equipment_connection.key.sql | 5 + sql/llx_kundenkarte_equipment_connection.sql | 61 + sql/llx_kundenkarte_equipment_panel.sql | 28 + sql/llx_kundenkarte_equipment_type.key.sql | 10 + sql/llx_kundenkarte_equipment_type.sql | 38 + ...x_kundenkarte_equipment_type_field.key.sql | 8 + sql/llx_kundenkarte_equipment_type_field.sql | 33 + sql/update_3.0.0.sql | 12 + sql/update_3.1.0.sql | 41 + sql/update_3.2.0.sql | 78 + sql/update_3.3.0.sql | 54 + tabs/anlagen.php | 1419 +++++- 34 files changed, 11320 insertions(+), 6 deletions(-) create mode 100644 admin/equipment_types.php create mode 100644 ajax/equipment.php create mode 100644 ajax/equipment_carrier.php create mode 100644 ajax/equipment_connection.php create mode 100644 ajax/equipment_panel.php create mode 100644 ajax/equipment_type_fields.php create mode 100644 class/equipment.class.php create mode 100644 class/equipmentcarrier.class.php create mode 100644 class/equipmentconnection.class.php create mode 100644 class/equipmentpanel.class.php create mode 100644 class/equipmenttype.class.php create mode 100644 sql/llx_kundenkarte_equipment.key.sql create mode 100644 sql/llx_kundenkarte_equipment.sql create mode 100644 sql/llx_kundenkarte_equipment_carrier.key.sql create mode 100644 sql/llx_kundenkarte_equipment_carrier.sql create mode 100644 sql/llx_kundenkarte_equipment_connection.key.sql create mode 100644 sql/llx_kundenkarte_equipment_connection.sql create mode 100644 sql/llx_kundenkarte_equipment_panel.sql create mode 100644 sql/llx_kundenkarte_equipment_type.key.sql create mode 100644 sql/llx_kundenkarte_equipment_type.sql create mode 100644 sql/llx_kundenkarte_equipment_type_field.key.sql create mode 100644 sql/llx_kundenkarte_equipment_type_field.sql create mode 100644 sql/update_3.0.0.sql create mode 100644 sql/update_3.1.0.sql create mode 100644 sql/update_3.2.0.sql create mode 100644 sql/update_3.3.0.sql diff --git a/admin/anlage_types.php b/admin/anlage_types.php index 175c1d1..b54fe3f 100755 --- a/admin/anlage_types.php +++ b/admin/anlage_types.php @@ -52,6 +52,7 @@ if ($action == 'add') { $anlageType->can_have_children = GETPOSTINT('can_have_children'); $anlageType->can_be_nested = GETPOSTINT('can_be_nested'); $anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml')); + $anlageType->can_have_equipment = GETPOSTINT('can_have_equipment'); $anlageType->picto = GETPOST('picto', 'alphanohtml'); $anlageType->color = GETPOST('color', 'alphanohtml'); $anlageType->position = GETPOSTINT('position'); @@ -101,6 +102,7 @@ if ($action == 'update') { $anlageType->can_have_children = GETPOSTINT('can_have_children'); $anlageType->can_be_nested = GETPOSTINT('can_be_nested'); $anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml')); + $anlageType->can_have_equipment = GETPOSTINT('can_have_equipment'); $anlageType->picto = GETPOST('picto', 'alphanohtml'); $anlageType->color = GETPOST('color', 'alphanohtml'); $anlageType->position = GETPOSTINT('position'); @@ -153,6 +155,7 @@ if ($action == 'copy' && $typeId > 0) { $newType->can_have_children = $sourceType->can_have_children; $newType->can_be_nested = $sourceType->can_be_nested; $newType->allowed_parent_types = $sourceType->allowed_parent_types; + $newType->can_have_equipment = $sourceType->can_have_equipment; $newType->picto = $sourceType->picto; $newType->color = $sourceType->color; $newType->position = $sourceType->position + 1; @@ -361,6 +364,11 @@ if (in_array($action, array('create', 'edit'))) { print 'can_be_nested ? ' checked' : '').'>'; print ' ('.$langs->trans('SameTypeUnderItself').')'; + // Can have equipment (Hutschienen-Komponenten) + print ''.$langs->trans('CanHaveEquipment').''; + print 'can_have_equipment ? ' checked' : '').'>'; + print ' ('.$langs->trans('CanHaveEquipmentHelp').')'; + // Allowed parent types - with multi-select UI print ''.$langs->trans('AllowedParentTypes').''; print ''; diff --git a/admin/equipment_types.php b/admin/equipment_types.php new file mode 100644 index 0000000..a343733 --- /dev/null +++ b/admin/equipment_types.php @@ -0,0 +1,670 @@ +loadLangs(array('admin', 'kundenkarte@kundenkarte', 'products')); + +// Security check +if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) { + accessforbidden(); +} + +$action = GETPOST('action', 'aZ09'); +$confirm = GETPOST('confirm', 'alpha'); +$typeId = GETPOSTINT('typeid'); +$systemFilter = GETPOSTINT('system'); + +$form = new Form($db); +$equipmentType = new EquipmentType($db); + +// Load systems +$systems = array(); +$sql = "SELECT rowid, code, label FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position ASC"; +$resql = $db->query($sql); +if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $systems[$obj->rowid] = $obj; + } +} + +// Load products for dropdown +$products = array(); +$sql = "SELECT rowid, ref, label FROM ".MAIN_DB_PREFIX."product WHERE tosell = 1 ORDER BY ref ASC"; +$resql = $db->query($sql); +if ($resql) { + while ($obj = $db->fetch_object($resql)) { + $products[$obj->rowid] = $obj; + } +} + +/* + * Actions + */ + +if ($action == 'add') { + $equipmentType->ref = GETPOST('ref', 'aZ09'); + $equipmentType->label = GETPOST('label', 'alphanohtml'); + $equipmentType->label_short = GETPOST('label_short', 'alphanohtml'); + $equipmentType->description = GETPOST('description', 'restricthtml'); + $equipmentType->fk_system = GETPOSTINT('fk_system'); + $equipmentType->width_te = GETPOSTINT('width_te'); + $equipmentType->color = GETPOST('color', 'alphanohtml'); + $equipmentType->fk_product = GETPOSTINT('fk_product'); + $equipmentType->terminals_config = GETPOST('terminals_config', 'nohtml'); + $equipmentType->picto = GETPOST('picto', 'alphanohtml'); + $equipmentType->position = GETPOSTINT('position'); + $equipmentType->active = 1; + + if (empty($equipmentType->ref) || empty($equipmentType->label) || empty($equipmentType->fk_system)) { + setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors'); + $action = 'create'; + } else { + $result = $equipmentType->create($user); + if ($result > 0) { + // Create default fields for common equipment + $defaultFields = array( + array('code' => 'characteristic', 'label' => 'Charakteristik', 'type' => 'select', 'options' => 'B|C|D|K|Z', 'position' => 10, 'show_in_hover' => 1, 'show_on_block' => 1), + array('code' => 'ampere', 'label' => 'Nennstrom (A)', 'type' => 'select', 'options' => '6|10|13|16|20|25|32|40|50|63', 'position' => 20, 'show_in_hover' => 1, 'show_on_block' => 1), + array('code' => 'pole', 'label' => 'Polzahl', 'type' => 'select', 'options' => '1|2|3|3+N', 'position' => 30, 'show_in_hover' => 1, 'show_on_block' => 0), + array('code' => 'circuit', 'label' => 'Stromkreis', 'type' => 'text', 'options' => '', 'position' => 40, 'show_in_hover' => 1, 'show_on_block' => 0), + ); + + foreach ($defaultFields as $field) { + $sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field"; + $sql .= " (fk_equipment_type, field_code, field_label, field_type, field_options, show_in_hover, show_on_block, required, position, active)"; + $sql .= " VALUES (".((int) $equipmentType->id).", '".$db->escape($field['code'])."', '".$db->escape($field['label'])."',"; + $sql .= " '".$db->escape($field['type'])."', '".$db->escape($field['options'])."', ".((int) $field['show_in_hover']).", ".((int) $field['show_on_block']).", 0, ".((int) $field['position']).", 1)"; + $db->query($sql); + } + + setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); + header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$equipmentType->id.'&system='.$equipmentType->fk_system); + exit; + } else { + setEventMessages($equipmentType->error, $equipmentType->errors, 'errors'); + $action = 'create'; + } + } +} + +if ($action == 'update') { + $equipmentType->fetch($typeId); + $equipmentType->ref = GETPOST('ref', 'aZ09'); + $equipmentType->label = GETPOST('label', 'alphanohtml'); + $equipmentType->label_short = GETPOST('label_short', 'alphanohtml'); + $equipmentType->description = GETPOST('description', 'restricthtml'); + $equipmentType->fk_system = GETPOSTINT('fk_system'); + $equipmentType->width_te = GETPOSTINT('width_te'); + $equipmentType->color = GETPOST('color', 'alphanohtml'); + $equipmentType->fk_product = GETPOSTINT('fk_product'); + $equipmentType->terminals_config = GETPOST('terminals_config', 'nohtml'); + $equipmentType->picto = GETPOST('picto', 'alphanohtml'); + $equipmentType->position = GETPOSTINT('position'); + + $result = $equipmentType->update($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); + header('Location: '.$_SERVER['PHP_SELF'].'?system='.$equipmentType->fk_system); + exit; + } else { + setEventMessages($equipmentType->error, $equipmentType->errors, 'errors'); + $action = 'edit'; + } +} + +if ($action == 'confirm_delete' && $confirm == 'yes') { + $equipmentType->fetch($typeId); + $result = $equipmentType->delete($user); + if ($result > 0) { + setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs'); + } else { + setEventMessages($equipmentType->error, $equipmentType->errors, 'errors'); + } + $action = ''; +} + +if ($action == 'activate') { + $sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_equipment_type SET active = 1 WHERE rowid = ".((int) $typeId); + $db->query($sql); + $action = ''; +} + +if ($action == 'deactivate') { + $sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_equipment_type SET active = 0 WHERE rowid = ".((int) $typeId); + $db->query($sql); + $action = ''; +} + +// Copy type with all fields +if ($action == 'copy' && $typeId > 0) { + $sourceType = new EquipmentType($db); + if ($sourceType->fetch($typeId) > 0) { + $newType = new EquipmentType($db); + $newType->ref = $sourceType->ref.'_COPY'; + $newType->label = $sourceType->label.' (Kopie)'; + $newType->label_short = $sourceType->label_short; + $newType->description = $sourceType->description; + $newType->fk_system = $sourceType->fk_system; + $newType->width_te = $sourceType->width_te; + $newType->color = $sourceType->color; + $newType->fk_product = $sourceType->fk_product; + $newType->picto = $sourceType->picto; + $newType->position = $sourceType->position + 1; + $newType->active = 1; + + $result = $newType->create($user); + if ($result > 0) { + // Copy all fields from source type + $sourceFields = $sourceType->fetchFields(0); + foreach ($sourceFields as $field) { + $sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field"; + $sql .= " (fk_equipment_type, field_code, field_label, field_type, field_options, show_in_hover, show_on_block, required, position, active)"; + $sql .= " VALUES (".((int) $newType->id).", '".$db->escape($field->field_code)."', '".$db->escape($field->field_label)."',"; + $sql .= " '".$db->escape($field->field_type)."', '".$db->escape($field->field_options)."', ".((int) $field->show_in_hover).","; + $sql .= " ".((int) $field->show_on_block).", ".((int) $field->required).", ".((int) $field->position).", ".((int) $field->active).")"; + $db->query($sql); + } + + setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); + header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$newType->id.'&system='.$newType->fk_system); + exit; + } else { + setEventMessages($newType->error, $newType->errors, 'errors'); + } + } + $action = ''; +} + +// Field actions +$fieldId = GETPOSTINT('fieldid'); + +if ($action == 'add_field') { + $fieldCode = GETPOST('field_code', 'aZ09'); + $fieldLabel = GETPOST('field_label', 'alphanohtml'); + $fieldType = GETPOST('field_type', 'aZ09'); + $fieldOptions = GETPOST('field_options', 'nohtml'); + $showInHover = GETPOSTINT('show_in_hover'); + $showOnBlock = GETPOSTINT('show_on_block'); + $isRequired = GETPOSTINT('is_required'); + $fieldPosition = GETPOSTINT('field_position'); + + if (empty($fieldCode) || empty($fieldLabel) || empty($fieldType)) { + setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors'); + } else { + $sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field"; + $sql .= " (fk_equipment_type, field_code, field_label, field_type, field_options, show_in_hover, show_on_block, required, position, active)"; + $sql .= " VALUES (".((int) $typeId).", '".$db->escape($fieldCode)."', '".$db->escape($fieldLabel)."',"; + $sql .= " '".$db->escape($fieldType)."', '".$db->escape($fieldOptions)."',"; + $sql .= " ".((int) $showInHover).", ".((int) $showOnBlock).", ".((int) $isRequired).", ".((int) $fieldPosition).", 1)"; + + if ($db->query($sql)) { + setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); + } else { + setEventMessages($db->lasterror(), null, 'errors'); + } + } + $action = 'edit'; +} + +if ($action == 'update_field') { + $fieldCode = GETPOST('field_code', 'aZ09'); + $fieldLabel = GETPOST('field_label', 'alphanohtml'); + $fieldType = GETPOST('field_type', 'aZ09'); + $fieldOptions = GETPOST('field_options', 'nohtml'); + $showInHover = GETPOSTINT('show_in_hover'); + $showOnBlock = GETPOSTINT('show_on_block'); + $isRequired = GETPOSTINT('is_required'); + $fieldPosition = GETPOSTINT('field_position'); + + $sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field SET"; + $sql .= " field_code = '".$db->escape($fieldCode)."',"; + $sql .= " field_label = '".$db->escape($fieldLabel)."',"; + $sql .= " field_type = '".$db->escape($fieldType)."',"; + $sql .= " field_options = '".$db->escape($fieldOptions)."',"; + $sql .= " show_in_hover = ".((int) $showInHover).","; + $sql .= " show_on_block = ".((int) $showOnBlock).","; + $sql .= " required = ".((int) $isRequired).","; + $sql .= " position = ".((int) $fieldPosition); + $sql .= " WHERE rowid = ".((int) $fieldId); + + if ($db->query($sql)) { + setEventMessages($langs->trans('RecordSaved'), null, 'mesgs'); + } else { + setEventMessages($db->lasterror(), null, 'errors'); + } + $action = 'edit'; +} + +if ($action == 'confirm_delete_field' && $confirm == 'yes') { + $sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field WHERE rowid = ".((int) $fieldId); + if ($db->query($sql)) { + setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs'); + } else { + setEventMessages($db->lasterror(), null, 'errors'); + } + header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$typeId.'&system='.$systemFilter); + exit; +} + +if ($action == 'activate_field') { + $sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field SET active = 1 WHERE rowid = ".((int) $fieldId); + $db->query($sql); + $action = 'edit'; +} + +if ($action == 'deactivate_field') { + $sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field SET active = 0 WHERE rowid = ".((int) $fieldId); + $db->query($sql); + $action = 'edit'; +} + +/* + * View + */ + +$title = $langs->trans('EquipmentTypes'); + +$morejs = array('/kundenkarte/js/kundenkarte.js?v='.time()); +$morecss = array('/kundenkarte/css/kundenkarte.css?v='.time()); + +llxHeader('', $title, '', '', 0, 0, $morejs, $morecss); + +$head = kundenkarteAdminPrepareHead(); +print dol_get_fiche_head($head, 'equipment_types', $langs->trans('ModuleKundenKarteName'), -1, 'fa-file'); + +// Confirmation for type deletion +if ($action == 'delete') { + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?typeid='.$typeId.'&system='.$systemFilter, + $langs->trans('Delete'), + $langs->trans('ConfirmDeleteType'), + 'confirm_delete', + '', + 'yes', + 1 + ); +} + +// Confirmation for field deletion +if ($action == 'delete_field') { + print $form->formconfirm( + $_SERVER['PHP_SELF'].'?typeid='.$typeId.'&fieldid='.$fieldId.'&system='.$systemFilter, + $langs->trans('Delete'), + $langs->trans('ConfirmDeleteField'), + 'confirm_delete_field', + '', + 'yes', + 1 + ); + $action = 'edit'; +} + +// Add/Edit form +if (in_array($action, array('create', 'edit'))) { + if ($action == 'edit' && $typeId > 0) { + $equipmentType->fetch($typeId); + } + + print '
'; + print ''; + print ''; + if ($action == 'edit') { + print ''; + } + + print ''; + + // System + print ''; + print ''; + + // Reference + print ''; + print ''; + + // Label + print ''; + print ''; + + // Short label + print ''; + print ''; + + // Description + print ''; + print ''; + + // Width in TE + print ''; + print ''; + + // Color + print ''; + print ''; + + // Product + print ''; + print ''; + + // Icon + print ''; + print ''; + + // Position + print ''; + print ''; + + // Terminal configuration + print ''; + print ''; + + print '
'.$langs->trans('System').'
'.$langs->trans('TypeRef').''; + print ' (UPPERCASE, no spaces)
'.$langs->trans('TypeLabel').'
'.$langs->trans('TypeShortLabel').''; + print ' (z.B. LS, FI, FI/LS)
'.$langs->trans('Description').'
'.$langs->trans('WidthTE').''; + print ' '.$langs->trans('WidthTEHelp').'
'.$langs->trans('Color').''; + print ' '.$langs->trans('ColorForSVG').'
'.$langs->trans('LinkedProduct').'
'.$langs->trans('SystemPicto').'
'; + print ''; + if ($equipmentType->picto) { + print kundenkarte_render_icon($equipmentType->picto); + } + print ''; + print ''; + print ''; + print '
'.$langs->trans('Position').'
'.$langs->trans('TerminalConfig').''; + print '
'; + print ''; + print ' '; + print ' '; + print ' '; + print ''; + print '
'; + print ''; + print '
'; + print 'JSON-Format: {"terminals":[{"id":"t1","label":"L","pos":"top"},...]} - pos: "top" oder "bottom"'; + print '
'; + print '
'; + + // JavaScript for terminal presets + print ''; + + print '
'; + print ''; + print ' '.$langs->trans('Cancel').''; + print '
'; + + print '
'; + + // Fields management for existing type + if ($action == 'edit' && $typeId > 0) { + $editFieldId = GETPOSTINT('editfield'); + + print '

'; + print '

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

'; + + $fields = $equipmentType->fetchFields(0); + + // Field types available + $fieldTypes = array( + 'text' => 'Textfeld (einzeilig)', + 'textarea' => 'Textfeld (mehrzeilig)', + 'number' => 'Zahlenfeld', + 'select' => 'Dropdown-Auswahl', + 'date' => 'Datumsfeld', + 'checkbox' => 'Checkbox (Ja/Nein)', + ); + + // Output edit forms BEFORE the table + foreach ($fields as $field) { + if ($editFieldId == $field->rowid) { + $formId = 'editfield_'.$field->rowid; + print '
'; + print ''; + print ''; + print ''; + print ''; + print ''; + print '
'; + } + } + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + foreach ($fields as $field) { + if ($editFieldId == $field->rowid) { + $formId = 'editfield_'.$field->rowid; + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + } else { + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + } + } + + if (empty($fields)) { + print ''; + } + + print '
'.$langs->trans('FieldCode').''.$langs->trans('FieldLabel').''.$langs->trans('FieldType').''.$langs->trans('FieldOptions').''.$langs->trans('ShowInHover').''.$langs->trans('ShowOnBlock').''.$langs->trans('IsRequired').''.$langs->trans('Position').''.$langs->trans('Status').''.$langs->trans('Actions').'
show_in_hover ? ' checked' : '').'>show_on_block ? ' checked' : '').'>required ? ' checked' : '').'>'; + print '
'; + print ''; + print ''; + print '
'; + print '
'.dol_escape_htmltag($field->field_code).''.dol_escape_htmltag($field->field_label).''.dol_escape_htmltag($fieldTypes[$field->field_type] ?? $field->field_type).''.dol_escape_htmltag(dol_trunc($field->field_options, 20)).''.($field->show_in_hover ? img_picto('', 'tick') : '').''.($field->show_on_block ? img_picto('', 'tick') : '').''.($field->required ? img_picto('', 'tick') : '').''.$field->position.''; + if ($field->active) { + print ''.img_picto($langs->trans('Enabled'), 'switch_on').''; + } else { + print ''.img_picto($langs->trans('Disabled'), 'switch_off').''; + } + print ''; + print '
'; + print ''; + print ''; + print '
'; + print '
'.$langs->trans('NoFieldsDefined').'
'; + + // Add new field form + print '
'; + print '
'; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print '
'.$langs->trans('Add').' '.$langs->trans('Field').'
'; + print '
'; + print '
'; + + // Help box + print '
'; + print '

Hilfe: Equipment-Felder

'; + print ''; + print ''; + print ''; + print ''; + print '
Show in HoverFeld wird im Tooltip angezeigt
Show on BlockFeld wird direkt auf dem SVG-Block angezeigt (z.B. "B16")
Dropdown-AuswahlOptionen mit | trennen, z.B.: B|C|D oder 6|10|16|20|25|32
'; + print '
'; + } + +} else { + // System filter + print '
'; + print $langs->trans('FilterBySystem').': '; + print ''; + print '
'; + + // Add button + print '
'; + print ''; + print ' '.$langs->trans('AddEquipmentType'); + print ''; + print '
'; + + // List + $types = $equipmentType->fetchAllBySystem($systemFilter, 0); + + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + foreach ($types as $type) { + print ''; + + print ''; + + print ''; + + print ''; + print ''; + print ''; + print ''; + + print ''; + + print ''; + + print ''; + } + + if (empty($types)) { + print ''; + } + + print '
'.$langs->trans('TypeRef').''.$langs->trans('TypeLabel').''.$langs->trans('System').''.$langs->trans('WidthTE').''.$langs->trans('Color').''.$langs->trans('Position').''.$langs->trans('Status').''.$langs->trans('Actions').'
'; + if ($type->picto) { + print kundenkarte_render_icon($type->picto).' '; + } + print dol_escape_htmltag($type->ref).''.dol_escape_htmltag($type->label); + if ($type->label_short) { + print ' ('.$type->label_short.')'; + } + print ''.dol_escape_htmltag($type->system_label).''.$type->width_te.' TE'.$type->position.''; + if ($type->active) { + print ''.img_picto($langs->trans('Enabled'), 'switch_on').''; + } else { + print ''.img_picto($langs->trans('Disabled'), 'switch_off').''; + } + print ''; + print '
'; + print ''; + print ''; + if (!$type->is_system) { + print ''; + } + print '
'; + print '
'.$langs->trans('NoRecords').'
'; +} + +print dol_get_fiche_end(); + +llxFooter(); +$db->close(); diff --git a/ajax/equipment.php b/ajax/equipment.php new file mode 100644 index 0000000..e93860b --- /dev/null +++ b/ajax/equipment.php @@ -0,0 +1,355 @@ + false, 'error' => ''); + +// Security check +if (!$user->hasRight('kundenkarte', 'read')) { + $response['error'] = 'Permission denied'; + echo json_encode($response); + exit; +} + +switch ($action) { + case 'get_types': + // Get all equipment types for dropdown + require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmenttype.class.php'; + $eqType = new EquipmentType($db); + $systemId = GETPOSTINT('system_id'); + $types = $eqType->fetchAllBySystem($systemId, 1); // Filter by system if provided, only active + + $result = array(); + foreach ($types as $t) { + $result[] = array( + 'id' => $t->id, + 'label' => $t->label, + 'label_short' => $t->label_short, + 'width_te' => $t->width_te, + 'color' => $t->color + ); + } + $response['success'] = true; + $response['types'] = $result; + break; + + case 'get': + // Get single equipment data + if ($equipmentId > 0 && $equipment->fetch($equipmentId) > 0) { + $response['success'] = true; + $response['equipment'] = array( + 'id' => $equipment->id, + 'fk_carrier' => $equipment->fk_carrier, + 'type_id' => $equipment->fk_equipment_type, + 'type_label' => $equipment->type_label, + 'label' => $equipment->label, + 'position_te' => $equipment->position_te, + 'width_te' => $equipment->width_te, + 'field_values' => $equipment->getFieldValues(), + 'fk_product' => $equipment->fk_product, + 'fk_protection' => $equipment->fk_protection, + 'protection_label' => $equipment->protection_label + ); + } else { + $response['error'] = 'Equipment not found'; + } + break; + + case 'get_protection_devices': + // Get all protection devices (FI/RCD) for an Anlage + $anlageId = GETPOSTINT('anlage_id'); + if ($anlageId > 0) { + $devices = $equipment->fetchProtectionDevices($anlageId); + $result = array(); + foreach ($devices as $d) { + $result[] = array( + 'id' => $d->id, + 'label' => $d->label ?: $d->type_label, + 'type_label' => $d->type_label, + 'type_label_short' => $d->type_label_short, + 'display_label' => ($d->label ?: $d->type_label_short ?: $d->type_label).' (Pos. '.$d->position_te.')' + ); + } + $response['success'] = true; + $response['devices'] = $result; + } else { + $response['error'] = 'Missing anlage_id'; + } + break; + + case 'list': + // List all equipment on a carrier + if ($carrierId > 0) { + $items = $equipment->fetchByCarrier($carrierId); + $result = array(); + foreach ($items as $eq) { + $result[] = array( + 'id' => $eq->id, + 'type_id' => $eq->fk_equipment_type, + 'type_label' => $eq->type_label, + 'type_label_short' => $eq->type_label_short, + 'type_ref' => $eq->type_ref, + 'type_color' => $eq->type_color, + 'terminals_config' => $eq->terminals_config, + 'label' => $eq->label, + 'position_te' => $eq->position_te, + 'width_te' => $eq->width_te, + 'block_label' => $eq->getBlockLabel(), + 'block_color' => $eq->getBlockColor(), + 'field_values' => $eq->getFieldValues(), + 'fk_product' => $eq->fk_product + ); + } + $response['success'] = true; + $response['equipment'] = $result; + } else { + $response['error'] = 'Missing carrier_id'; + } + break; + + case 'create': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + + $equipment->fk_carrier = $carrierId; + $equipment->fk_equipment_type = GETPOSTINT('type_id'); + $equipment->label = GETPOST('label', 'alphanohtml'); + $equipment->position_te = GETPOSTINT('position_te'); + $equipment->width_te = GETPOSTINT('width_te'); + $equipment->fk_product = GETPOSTINT('fk_product'); + $equipment->fk_protection = GETPOSTINT('fk_protection'); + $equipment->protection_label = GETPOST('protection_label', 'alphanohtml'); + + // Field values + $fieldValues = GETPOST('field_values', 'nohtml'); + if ($fieldValues) { + $equipment->field_values = $fieldValues; + } + + // If no width specified, get from type + if (empty($equipment->width_te)) { + $type = new EquipmentType($db); + if ($type->fetch($equipment->fk_equipment_type) > 0) { + $equipment->width_te = $type->width_te; + } else { + $equipment->width_te = 1; + } + } + + // Check carrier and position + $carrier = new EquipmentCarrier($db); + if ($carrier->fetch($carrierId) > 0) { + // If no position specified, find next free position (1-based) + if (empty($equipment->position_te)) { + $equipment->position_te = $carrier->getNextFreePosition($equipment->width_te); + if ($equipment->position_te < 0) { + $response['error'] = 'No free position available on carrier'; + break; + } + } + + // Check if position is available + if (!$carrier->isPositionAvailable($equipment->position_te, $equipment->width_te)) { + $response['error'] = 'Position not available'; + break; + } + } + + $result = $equipment->create($user); + if ($result > 0) { + $response['success'] = true; + $response['equipment_id'] = $result; + $response['block_label'] = $equipment->getBlockLabel(); + } else { + $response['error'] = $equipment->error; + } + break; + + case 'update': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if ($equipment->fetch($equipmentId) > 0) { + $newPosition = GETPOSTINT('position_te'); + $newWidth = GETPOSTINT('width_te') ?: $equipment->width_te; + + // Check if new position is available (excluding current equipment) + if ($newPosition != $equipment->position_te || $newWidth != $equipment->width_te) { + $carrier = new EquipmentCarrier($db); + if ($carrier->fetch($equipment->fk_carrier) > 0) { + if (!$carrier->isPositionAvailable($newPosition, $newWidth, $equipmentId)) { + $response['error'] = 'Position not available'; + break; + } + } + } + + $equipment->fk_equipment_type = GETPOSTINT('type_id') ?: $equipment->fk_equipment_type; + $equipment->label = GETPOST('label', 'alphanohtml'); + $equipment->position_te = $newPosition; + $equipment->width_te = $newWidth; + $equipment->fk_product = GETPOSTINT('fk_product'); + $equipment->fk_protection = GETPOSTINT('fk_protection'); + $equipment->protection_label = GETPOST('protection_label', 'alphanohtml'); + + $fieldValues = GETPOST('field_values', 'nohtml'); + if ($fieldValues) { + $equipment->field_values = $fieldValues; + } + + $result = $equipment->update($user); + if ($result > 0) { + $response['success'] = true; + $response['block_label'] = $equipment->getBlockLabel(); + } else { + $response['error'] = $equipment->error; + } + } else { + $response['error'] = 'Equipment not found'; + } + break; + + case 'update_position': + // Quick position update for drag-drop + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if ($equipment->fetch($equipmentId) > 0) { + $newPosition = GETPOSTINT('position_te'); + + // Check if new position is available + if ($newPosition != $equipment->position_te) { + $carrier = new EquipmentCarrier($db); + if ($carrier->fetch($equipment->fk_carrier) > 0) { + if (!$carrier->isPositionAvailable($newPosition, $equipment->width_te, $equipmentId)) { + $response['error'] = 'Position not available'; + break; + } + } + } + + $equipment->position_te = $newPosition; + $result = $equipment->update($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $equipment->error; + } + } else { + $response['error'] = 'Equipment not found'; + } + break; + + case 'delete': + if (!$user->hasRight('kundenkarte', 'delete')) { + $response['error'] = 'Permission denied'; + break; + } + if ($equipment->fetch($equipmentId) > 0) { + $result = $equipment->delete($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $equipment->error; + } + } else { + $response['error'] = 'Equipment not found'; + } + break; + + case 'duplicate': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if ($equipment->fetch($equipmentId) > 0) { + $newId = $equipment->duplicate($user); + if ($newId > 0) { + $response['success'] = true; + $response['equipment_id'] = $newId; + + // Fetch the new equipment to return its data + $newEquipment = new Equipment($db); + if ($newEquipment->fetch($newId) > 0) { + $response['equipment'] = array( + 'id' => $newEquipment->id, + 'type_id' => $newEquipment->fk_equipment_type, + 'type_label' => $newEquipment->type_label, + 'type_color' => $newEquipment->type_color, + 'label' => $newEquipment->label, + 'position_te' => $newEquipment->position_te, + 'width_te' => $newEquipment->width_te, + 'block_label' => $newEquipment->getBlockLabel(), + 'block_color' => $newEquipment->getBlockColor() + ); + } + } else { + $response['error'] = $equipment->error ?: 'Duplication failed'; + } + } else { + $response['error'] = 'Equipment not found'; + } + break; + + case 'move': + // Move equipment to new position (for drag & drop) + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if ($equipment->fetch($equipmentId) > 0) { + $newPosition = GETPOSTINT('position_te'); + + $carrier = new EquipmentCarrier($db); + if ($carrier->fetch($equipment->fk_carrier) > 0) { + if ($carrier->isPositionAvailable($newPosition, $equipment->width_te, $equipmentId)) { + $equipment->position_te = $newPosition; + $result = $equipment->update($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $equipment->error; + } + } else { + $response['error'] = 'Position not available'; + } + } + } else { + $response['error'] = 'Equipment not found'; + } + break; + + default: + $response['error'] = 'Unknown action'; +} + +echo json_encode($response); +$db->close(); diff --git a/ajax/equipment_carrier.php b/ajax/equipment_carrier.php new file mode 100644 index 0000000..14ba690 --- /dev/null +++ b/ajax/equipment_carrier.php @@ -0,0 +1,188 @@ + false, 'error' => ''); + +// Security check +if (!$user->hasRight('kundenkarte', 'read')) { + $response['error'] = 'Permission denied'; + echo json_encode($response); + exit; +} + +switch ($action) { + case 'get': + // Get single carrier data + if ($carrierId > 0 && $carrier->fetch($carrierId) > 0) { + $response['success'] = true; + $response['carrier'] = array( + 'id' => $carrier->id, + 'fk_anlage' => $carrier->fk_anlage, + 'fk_panel' => $carrier->fk_panel, + 'label' => $carrier->label, + 'total_te' => $carrier->total_te, + 'position' => $carrier->position, + 'panel_label' => $carrier->panel_label + ); + } else { + $response['error'] = 'Carrier not found'; + } + break; + + case 'list': + // List all carriers for an Anlage + if ($anlageId > 0) { + $carriers = $carrier->fetchByAnlage($anlageId); + $result = array(); + foreach ($carriers as $c) { + $c->fetchEquipment(); + $equipment = array(); + foreach ($c->equipment as $eq) { + $equipment[] = array( + 'id' => $eq->id, + 'type_id' => $eq->fk_equipment_type, + 'type_label' => $eq->type_label, + 'type_label_short' => $eq->type_label_short, + 'type_color' => $eq->type_color, + 'label' => $eq->label, + 'position_te' => $eq->position_te, + 'width_te' => $eq->width_te, + 'block_label' => $eq->getBlockLabel(), + 'block_color' => $eq->getBlockColor(), + 'field_values' => $eq->getFieldValues() + ); + } + $result[] = array( + 'id' => $c->id, + 'label' => $c->label, + 'total_te' => $c->total_te, + 'used_te' => $c->getUsedTE(), + 'free_te' => $c->getFreeTE(), + 'position' => $c->position, + 'equipment' => $equipment + ); + } + $response['success'] = true; + $response['carriers'] = $result; + } else { + $response['error'] = 'Missing anlage_id'; + } + break; + + case 'create': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + $carrier->fk_anlage = $anlageId; + $carrier->fk_panel = $panelId > 0 ? $panelId : null; + $carrier->label = GETPOST('label', 'alphanohtml'); + $carrier->total_te = GETPOSTINT('total_te') ?: 12; + + $result = $carrier->create($user); + if ($result > 0) { + $response['success'] = true; + $response['carrier_id'] = $result; + } else { + $response['error'] = $carrier->error; + } + break; + + case 'update': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if ($carrier->fetch($carrierId) > 0) { + $carrier->label = GETPOST('label', 'alphanohtml'); + $carrier->total_te = GETPOSTINT('total_te') ?: $carrier->total_te; + $carrier->position = GETPOSTINT('position'); + // Allow changing panel (0 or empty = no panel) + $newPanelId = GETPOSTINT('panel_id'); + $carrier->fk_panel = $newPanelId > 0 ? $newPanelId : null; + + $result = $carrier->update($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $carrier->error; + } + } else { + $response['error'] = 'Carrier not found'; + } + break; + + case 'delete': + if (!$user->hasRight('kundenkarte', 'delete')) { + $response['error'] = 'Permission denied'; + break; + } + if ($carrier->fetch($carrierId) > 0) { + $result = $carrier->delete($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $carrier->error; + } + } else { + $response['error'] = 'Carrier not found'; + } + break; + + case 'duplicate': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if ($carrier->fetch($carrierId) > 0) { + // Create a copy of the carrier + $newCarrier = new EquipmentCarrier($db); + $newCarrier->fk_anlage = $carrier->fk_anlage; + $newCarrier->fk_panel = $carrier->fk_panel; + $newCarrier->label = $carrier->label; + $newCarrier->total_te = $carrier->total_te; + $newCarrier->note_private = $carrier->note_private; + + $result = $newCarrier->create($user); + if ($result > 0) { + $response['success'] = true; + $response['carrier_id'] = $result; + } else { + $response['error'] = $newCarrier->error; + } + } else { + $response['error'] = 'Carrier not found'; + } + break; + + default: + $response['error'] = 'Unknown action'; +} + +echo json_encode($response); +$db->close(); diff --git a/ajax/equipment_connection.php b/ajax/equipment_connection.php new file mode 100644 index 0000000..2d0173e --- /dev/null +++ b/ajax/equipment_connection.php @@ -0,0 +1,348 @@ + false, 'error' => ''); + +// Security check +if (!$user->hasRight('kundenkarte', 'read')) { + $response['error'] = 'Permission denied'; + echo json_encode($response); + exit; +} + +switch ($action) { + case 'get': + // Get single connection data + if ($connectionId > 0 && $connection->fetch($connectionId) > 0) { + $response['success'] = true; + $response['connection'] = array( + 'id' => $connection->id, + 'fk_source' => $connection->fk_source, + 'source_terminal' => $connection->source_terminal, + 'fk_target' => $connection->fk_target, + 'target_terminal' => $connection->target_terminal, + 'connection_type' => $connection->connection_type, + 'color' => $connection->color, + 'output_label' => $connection->output_label, + 'medium_type' => $connection->medium_type, + 'medium_spec' => $connection->medium_spec, + 'medium_length' => $connection->medium_length, + 'is_rail' => $connection->is_rail, + 'rail_start_te' => $connection->rail_start_te, + 'rail_end_te' => $connection->rail_end_te, + 'rail_phases' => $connection->rail_phases, + 'excluded_te' => $connection->excluded_te, + 'fk_carrier' => $connection->fk_carrier, + 'position_y' => $connection->position_y, + 'source_label' => $connection->source_label, + 'target_label' => $connection->target_label + ); + } else { + $response['error'] = 'Connection not found'; + } + break; + + case 'list': + // List all connections for a carrier + if ($carrierId > 0) { + $connections = $connection->fetchByCarrier($carrierId); + $result = array(); + foreach ($connections as $c) { + $result[] = array( + 'id' => $c->id, + 'fk_source' => $c->fk_source, + 'source_terminal' => $c->source_terminal, + 'source_label' => $c->source_label, + 'source_pos' => $c->source_pos, + 'source_width' => $c->source_width, + 'fk_target' => $c->fk_target, + 'target_terminal' => $c->target_terminal, + 'target_label' => $c->target_label, + 'target_pos' => $c->target_pos, + 'connection_type' => $c->connection_type, + 'color' => $c->getColor(), + 'output_label' => $c->output_label, + 'medium_type' => $c->medium_type, + 'medium_spec' => $c->medium_spec, + 'medium_length' => $c->medium_length, + 'is_rail' => $c->is_rail, + 'rail_start_te' => $c->rail_start_te, + 'rail_end_te' => $c->rail_end_te, + 'rail_phases' => $c->rail_phases, + 'excluded_te' => $c->excluded_te, + 'position_y' => $c->position_y, + 'display_label' => $c->getDisplayLabel() + ); + } + $response['success'] = true; + $response['connections'] = $result; + } else { + $response['error'] = 'Missing carrier_id'; + } + break; + + case 'list_outputs': + // List outputs for an equipment + if ($equipmentId > 0) { + $outputs = $connection->fetchOutputs($equipmentId); + $result = array(); + foreach ($outputs as $c) { + $result[] = array( + 'id' => $c->id, + 'connection_type' => $c->connection_type, + 'color' => $c->getColor(), + 'output_label' => $c->output_label, + 'medium_type' => $c->medium_type, + 'medium_spec' => $c->medium_spec, + 'medium_length' => $c->medium_length, + 'display_label' => $c->getDisplayLabel() + ); + } + $response['success'] = true; + $response['outputs'] = $result; + } else { + $response['error'] = 'Missing equipment_id'; + } + break; + + case 'create': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + + $connection->fk_source = GETPOSTINT('fk_source'); + $connection->source_terminal = GETPOST('source_terminal', 'alphanohtml') ?: 'output'; + $connection->source_terminal_id = GETPOST('source_terminal_id', 'alphanohtml'); + $connection->fk_target = GETPOSTINT('fk_target'); + $connection->target_terminal = GETPOST('target_terminal', 'alphanohtml') ?: 'input'; + $connection->target_terminal_id = GETPOST('target_terminal_id', 'alphanohtml'); + $connection->connection_type = GETPOST('connection_type', 'alphanohtml'); + $connection->color = GETPOST('color', 'alphanohtml'); + $connection->output_label = GETPOST('output_label', 'alphanohtml'); + $connection->medium_type = GETPOST('medium_type', 'alphanohtml'); + $connection->medium_spec = GETPOST('medium_spec', 'alphanohtml'); + $connection->medium_length = GETPOST('medium_length', 'alphanohtml'); + $connection->is_rail = GETPOSTINT('is_rail'); + $connection->rail_start_te = GETPOSTINT('rail_start_te'); + $connection->rail_end_te = GETPOSTINT('rail_end_te'); + $connection->fk_carrier = $carrierId; + $connection->position_y = GETPOSTINT('position_y'); + + $result = $connection->create($user); + if ($result > 0) { + $response['success'] = true; + $response['connection_id'] = $result; + } else { + $response['error'] = $connection->error ?: 'Create failed'; + } + break; + + case 'update': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if ($connection->fetch($connectionId) > 0) { + $connection->fk_source = GETPOSTINT('fk_source'); + $connection->source_terminal = GETPOST('source_terminal', 'alphanohtml') ?: 'output'; + $connection->fk_target = GETPOSTINT('fk_target'); + $connection->target_terminal = GETPOST('target_terminal', 'alphanohtml') ?: 'input'; + $connection->connection_type = GETPOST('connection_type', 'alphanohtml'); + $connection->color = GETPOST('color', 'alphanohtml'); + $connection->output_label = GETPOST('output_label', 'alphanohtml'); + $connection->medium_type = GETPOST('medium_type', 'alphanohtml'); + $connection->medium_spec = GETPOST('medium_spec', 'alphanohtml'); + $connection->medium_length = GETPOST('medium_length', 'alphanohtml'); + $connection->is_rail = GETPOSTINT('is_rail'); + $connection->rail_start_te = GETPOSTINT('rail_start_te'); + $connection->rail_end_te = GETPOSTINT('rail_end_te'); + $connection->rail_phases = GETPOST('rail_phases', 'alphanohtml'); + $connection->excluded_te = GETPOST('excluded_te', 'alphanohtml'); + $connection->position_y = GETPOSTINT('position_y'); + + $result = $connection->update($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $connection->error ?: 'Update failed'; + } + } else { + $response['error'] = 'Connection not found'; + } + break; + + case 'delete': + if (!$user->hasRight('kundenkarte', 'delete')) { + $response['error'] = 'Permission denied'; + break; + } + if ($connection->fetch($connectionId) > 0) { + $result = $connection->delete($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $connection->error ?: 'Delete failed'; + } + } else { + $response['error'] = 'Connection not found'; + } + break; + + case 'create_rail': + // Create a rail/bar connection spanning multiple equipment + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + + $connection->is_rail = 1; + $connection->connection_type = GETPOST('connection_type', 'alphanohtml'); + $connection->color = GETPOST('color', 'alphanohtml'); + $connection->rail_start_te = GETPOSTINT('rail_start_te'); + $connection->rail_end_te = GETPOSTINT('rail_end_te'); + $connection->rail_phases = GETPOST('rail_phases', 'alphanohtml'); + $connection->excluded_te = GETPOST('excluded_te', 'alphanohtml'); + $connection->fk_carrier = $carrierId; + $connection->position_y = GETPOSTINT('position_y'); + + $result = $connection->create($user); + if ($result > 0) { + $response['success'] = true; + $response['connection_id'] = $result; + } else { + $response['error'] = $connection->error ?: 'Create failed'; + } + break; + + case 'create_output': + // Create an output connection + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + + $connection->fk_source = $equipmentId; + $connection->source_terminal = 'output'; + $connection->fk_target = null; + $connection->connection_type = GETPOST('connection_type', 'alphanohtml'); + $connection->color = GETPOST('color', 'alphanohtml'); + $connection->output_label = GETPOST('output_label', 'alphanohtml'); + $connection->medium_type = GETPOST('medium_type', 'alphanohtml'); + $connection->medium_spec = GETPOST('medium_spec', 'alphanohtml'); + $connection->medium_length = GETPOST('medium_length', 'alphanohtml'); + $connection->fk_carrier = $carrierId; + $connection->position_y = 0; + + $result = $connection->create($user); + if ($result > 0) { + $response['success'] = true; + $response['connection_id'] = $result; + } else { + $response['error'] = $connection->error ?: 'Create failed'; + } + break; + + case 'list_all': + // List all connections for an anlage (across all carriers) + $anlageId = GETPOSTINT('anlage_id'); + if ($anlageId > 0) { + // Get all carriers for this anlage + require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php'; + $carrierObj = new EquipmentCarrier($db); + $carriers = $carrierObj->fetchByAnlage($anlageId); + + $allConnections = array(); + foreach ($carriers as $carrier) { + $connections = $connection->fetchByCarrier($carrier->id); + foreach ($connections as $c) { + $allConnections[] = array( + 'id' => $c->id, + 'fk_source' => $c->fk_source, + 'source_terminal' => $c->source_terminal, + 'source_terminal_id' => $c->source_terminal_id, + 'source_label' => $c->source_label, + 'fk_target' => $c->fk_target, + 'target_terminal' => $c->target_terminal, + 'target_terminal_id' => $c->target_terminal_id, + 'target_label' => $c->target_label, + 'connection_type' => $c->connection_type, + 'color' => $c->getColor(), + 'output_label' => $c->output_label, + 'medium_type' => $c->medium_type, + 'medium_spec' => $c->medium_spec, + 'medium_length' => $c->medium_length, + 'is_rail' => $c->is_rail, + 'fk_carrier' => $c->fk_carrier + ); + } + } + + $response['success'] = true; + $response['connections'] = $allConnections; + } else { + $response['error'] = 'Missing anlage_id'; + } + break; + + case 'clear_all': + // Delete all connections for an anlage + if (!$user->hasRight('kundenkarte', 'delete')) { + $response['error'] = 'Permission denied'; + break; + } + + $anlageId = GETPOSTINT('anlage_id'); + if ($anlageId > 0) { + // Get all carriers for this anlage + require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php'; + $carrierObj = new EquipmentCarrier($db); + $carriers = $carrierObj->fetchByAnlage($anlageId); + + $deletedCount = 0; + foreach ($carriers as $carrier) { + $sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection WHERE fk_carrier = ".((int)$carrier->id); + $resql = $db->query($sql); + if ($resql) { + $deletedCount += $db->affected_rows($resql); + } + } + + $response['success'] = true; + $response['deleted_count'] = $deletedCount; + } else { + $response['error'] = 'Missing anlage_id'; + } + break; + + default: + $response['error'] = 'Unknown action'; +} + +echo json_encode($response); +$db->close(); diff --git a/ajax/equipment_panel.php b/ajax/equipment_panel.php new file mode 100644 index 0000000..acde20a --- /dev/null +++ b/ajax/equipment_panel.php @@ -0,0 +1,201 @@ + false, 'error' => ''); + +// Security check +if (!$user->hasRight('kundenkarte', 'read')) { + $response['error'] = 'Permission denied'; + echo json_encode($response); + exit; +} + +switch ($action) { + case 'get': + // Get single panel data + if ($panelId > 0 && $panel->fetch($panelId) > 0) { + $response['success'] = true; + $response['panel'] = array( + 'id' => $panel->id, + 'fk_anlage' => $panel->fk_anlage, + 'label' => $panel->label, + 'position' => $panel->position, + 'note_private' => $panel->note_private, + 'status' => $panel->status + ); + } else { + $response['error'] = 'Panel not found'; + } + break; + + case 'list': + // List all panels for an Anlage + if ($anlageId > 0) { + $panels = $panel->fetchByAnlage($anlageId); + $result = array(); + foreach ($panels as $p) { + $result[] = array( + 'id' => $p->id, + 'fk_anlage' => $p->fk_anlage, + 'label' => $p->label, + 'position' => $p->position, + 'status' => $p->status + ); + } + $response['success'] = true; + $response['panels'] = $result; + } else { + $response['error'] = 'Missing anlage_id'; + } + break; + + case 'create': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + + $panel->fk_anlage = $anlageId; + $panel->label = GETPOST('label', 'alphanohtml'); + $panel->position = GETPOSTINT('position'); + $panel->note_private = GETPOST('note_private', 'restricthtml'); + + $result = $panel->create($user); + if ($result > 0) { + $response['success'] = true; + $response['panel_id'] = $result; + $response['label'] = $panel->label; + } else { + $response['error'] = $panel->error; + } + break; + + case 'update': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if ($panel->fetch($panelId) > 0) { + $panel->label = GETPOST('label', 'alphanohtml') ?: $panel->label; + $panel->position = GETPOSTINT('position') ?: $panel->position; + $panel->note_private = GETPOST('note_private', 'restricthtml'); + + $result = $panel->update($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $panel->error; + } + } else { + $response['error'] = 'Panel not found'; + } + break; + + case 'delete': + if (!$user->hasRight('kundenkarte', 'delete')) { + $response['error'] = 'Permission denied'; + break; + } + if ($panel->fetch($panelId) > 0) { + $result = $panel->delete($user); + if ($result > 0) { + $response['success'] = true; + } else { + $response['error'] = $panel->error; + } + } else { + $response['error'] = 'Panel not found'; + } + break; + + case 'list_with_carriers': + // List all panels with their carriers for an Anlage + if ($anlageId > 0) { + $panels = $panel->fetchByAnlage($anlageId); + $result = array(); + + foreach ($panels as $p) { + $panelData = array( + 'id' => $p->id, + 'fk_anlage' => $p->fk_anlage, + 'label' => $p->label, + 'position' => $p->position, + 'status' => $p->status, + 'carriers' => array() + ); + + // Fetch carriers for this panel + $p->fetchCarriers(); + foreach ($p->carriers as $c) { + $panelData['carriers'][] = array( + 'id' => $c->id, + 'label' => $c->label, + 'total_te' => $c->total_te, + 'position' => $c->position + ); + } + + $result[] = $panelData; + } + + $response['success'] = true; + $response['panels'] = $result; + } else { + $response['error'] = 'Missing anlage_id'; + } + break; + + case 'duplicate': + if (!$user->hasRight('kundenkarte', 'write')) { + $response['error'] = 'Permission denied'; + break; + } + if ($panel->fetch($panelId) > 0) { + // Create a copy of the panel + $newPanel = new EquipmentPanel($db); + $newPanel->fk_anlage = $panel->fk_anlage; + $newPanel->label = $panel->label.' (Kopie)'; + $newPanel->note_private = $panel->note_private; + + $result = $newPanel->create($user); + if ($result > 0) { + $response['success'] = true; + $response['panel_id'] = $result; + } else { + $response['error'] = $newPanel->error; + } + } else { + $response['error'] = 'Panel not found'; + } + break; + + default: + $response['error'] = 'Unknown action'; +} + +echo json_encode($response); +$db->close(); diff --git a/ajax/equipment_type_fields.php b/ajax/equipment_type_fields.php new file mode 100644 index 0000000..e1c2e2b --- /dev/null +++ b/ajax/equipment_type_fields.php @@ -0,0 +1,74 @@ + false, 'fields' => array()); + +if (!$user->hasRight('kundenkarte', 'read')) { + $response['error'] = 'Permission denied'; + echo json_encode($response); + exit; +} + +if ($typeId > 0) { + $type = new EquipmentType($db); + if ($type->fetch($typeId) > 0) { + $fields = $type->fetchFields(1); + + // Get existing values if editing + $existingValues = array(); + if ($equipmentId > 0) { + $equipment = new Equipment($db); + if ($equipment->fetch($equipmentId) > 0) { + $existingValues = $equipment->getFieldValues(); + } + } + + $result = array(); + foreach ($fields as $field) { + $value = isset($existingValues[$field->field_code]) ? $existingValues[$field->field_code] : $field->field_default; + $result[] = array( + 'code' => $field->field_code, + 'label' => $field->field_label, + 'type' => $field->field_type, + 'options' => $field->field_options, + 'required' => $field->required, + 'show_on_block' => $field->show_on_block, + 'value' => $value + ); + } + + $response['success'] = true; + $response['fields'] = $result; + $response['type'] = array( + 'id' => $type->id, + 'label' => $type->label, + 'label_short' => $type->label_short, + 'width_te' => $type->width_te, + 'color' => $type->color + ); + } +} + +echo json_encode($response); +$db->close(); diff --git a/class/anlagetype.class.php b/class/anlagetype.class.php index 92feaae..3a0bb47 100755 --- a/class/anlagetype.class.php +++ b/class/anlagetype.class.php @@ -25,6 +25,7 @@ class AnlageType extends CommonObject public $can_have_children; public $can_be_nested; public $allowed_parent_types; + public $can_have_equipment; public $picto; public $color; @@ -72,7 +73,7 @@ class AnlageType extends CommonObject $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; $sql .= "entity, ref, label, label_short, description, fk_system,"; - $sql .= " can_have_children, can_be_nested, allowed_parent_types,"; + $sql .= " can_have_children, can_be_nested, allowed_parent_types, can_have_equipment,"; $sql .= " picto, color, is_system, position, active,"; $sql .= " date_creation, fk_user_creat"; $sql .= ") VALUES ("; @@ -85,6 +86,7 @@ class AnlageType extends CommonObject $sql .= ", ".((int) $this->can_have_children); $sql .= ", ".((int) $this->can_be_nested); $sql .= ", ".($this->allowed_parent_types ? "'".$this->db->escape($this->allowed_parent_types)."'" : "NULL"); + $sql .= ", ".((int) $this->can_have_equipment); $sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL"); $sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL"); $sql .= ", 0"; // is_system = 0 for user-created @@ -141,6 +143,7 @@ class AnlageType extends CommonObject $this->can_have_children = $obj->can_have_children; $this->can_be_nested = $obj->can_be_nested; $this->allowed_parent_types = $obj->allowed_parent_types; + $this->can_have_equipment = $obj->can_have_equipment ?? 0; $this->picto = $obj->picto; $this->color = $obj->color; $this->is_system = $obj->is_system; @@ -186,6 +189,7 @@ class AnlageType extends CommonObject $sql .= ", can_have_children = ".((int) $this->can_have_children); $sql .= ", can_be_nested = ".((int) $this->can_be_nested); $sql .= ", allowed_parent_types = ".($this->allowed_parent_types ? "'".$this->db->escape($this->allowed_parent_types)."'" : "NULL"); + $sql .= ", can_have_equipment = ".((int) $this->can_have_equipment); $sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL"); $sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL"); $sql .= ", position = ".((int) $this->position); @@ -295,6 +299,7 @@ class AnlageType extends CommonObject $type->can_have_children = $obj->can_have_children; $type->can_be_nested = $obj->can_be_nested; $type->allowed_parent_types = $obj->allowed_parent_types; + $type->can_have_equipment = $obj->can_have_equipment ?? 0; $type->picto = $obj->picto; $type->is_system = $obj->is_system; $type->position = $obj->position; diff --git a/class/equipment.class.php b/class/equipment.class.php new file mode 100644 index 0000000..f03bf0f --- /dev/null +++ b/class/equipment.class.php @@ -0,0 +1,494 @@ +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->fk_carrier) || empty($this->fk_equipment_type)) { + $this->error = 'ErrorMissingParameters'; + return -1; + } + + // Get default width from type if not set + if (empty($this->width_te)) { + $type = new EquipmentType($this->db); + if ($type->fetch($this->fk_equipment_type) > 0) { + $this->width_te = $type->width_te; + } else { + $this->width_te = 1; + } + } + + $this->db->begin(); + + $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; + $sql .= "entity, fk_carrier, fk_equipment_type, label,"; + $sql .= " position_te, width_te, field_values, fk_product,"; + $sql .= " fk_protection, protection_label,"; + $sql .= " note_private, status,"; + $sql .= " date_creation, fk_user_creat"; + $sql .= ") VALUES ("; + $sql .= ((int) $conf->entity); + $sql .= ", ".((int) $this->fk_carrier); + $sql .= ", ".((int) $this->fk_equipment_type); + $sql .= ", ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL"); + $sql .= ", ".((int) $this->position_te); + $sql .= ", ".((int) $this->width_te); + $sql .= ", ".($this->field_values ? "'".$this->db->escape($this->field_values)."'" : "NULL"); + $sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL"); + $sql .= ", ".($this->fk_protection > 0 ? ((int) $this->fk_protection) : "NULL"); + $sql .= ", ".($this->protection_label ? "'".$this->db->escape($this->protection_label)."'" : "NULL"); + $sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL"); + $sql .= ", ".((int) ($this->status !== null ? $this->status : 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 e.*, t.label as type_label, t.label_short as type_label_short,"; + $sql .= " t.color as type_color, t.picto as type_picto,"; + $sql .= " p.ref as product_ref, p.label as product_label,"; + $sql .= " prot.label as protection_device_label"; + $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_type as t ON e.fk_equipment_type = t.rowid"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON e.fk_product = p.rowid"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as prot ON e.fk_protection = prot.rowid"; + $sql .= " WHERE e.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->fk_carrier = $obj->fk_carrier; + $this->fk_equipment_type = $obj->fk_equipment_type; + $this->label = $obj->label; + $this->position_te = $obj->position_te; + $this->width_te = $obj->width_te; + $this->field_values = $obj->field_values; + $this->fk_product = $obj->fk_product; + $this->fk_protection = $obj->fk_protection; + $this->protection_label = $obj->protection_label; + $this->note_private = $obj->note_private; + $this->status = $obj->status; + $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->type_label = $obj->type_label; + $this->type_label_short = $obj->type_label_short; + $this->type_color = $obj->type_color; + $this->type_picto = $obj->type_picto; + $this->product_ref = $obj->product_ref; + $this->product_label = $obj->product_label; + $this->protection_device_label = $obj->protection_device_label; + + $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 .= " fk_equipment_type = ".((int) $this->fk_equipment_type); + $sql .= ", label = ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL"); + $sql .= ", position_te = ".((int) $this->position_te); + $sql .= ", width_te = ".((int) $this->width_te); + $sql .= ", field_values = ".($this->field_values ? "'".$this->db->escape($this->field_values)."'" : "NULL"); + $sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL"); + $sql .= ", fk_protection = ".($this->fk_protection > 0 ? ((int) $this->fk_protection) : "NULL"); + $sql .= ", protection_label = ".($this->protection_label ? "'".$this->db->escape($this->protection_label)."'" : "NULL"); + $sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL"); + $sql .= ", status = ".((int) $this->status); + $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) + { + $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 equipment on a carrier + * + * @param int $carrierId Carrier ID + * @param int $activeOnly Only active equipment + * @return array Array of Equipment objects + */ + public function fetchByCarrier($carrierId, $activeOnly = 1) + { + $results = array(); + + $sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,"; + $sql .= " t.ref as type_ref, t.color as type_color, t.picto as type_picto,"; + $sql .= " t.terminals_config as terminals_config,"; + $sql .= " prot.label as protection_device_label"; + $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_type as t ON e.fk_equipment_type = t.rowid"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as prot ON e.fk_protection = prot.rowid"; + $sql .= " WHERE e.fk_carrier = ".((int) $carrierId); + if ($activeOnly) { + $sql .= " AND e.status = 1"; + } + $sql .= " ORDER BY e.position_te ASC"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $eq = new Equipment($this->db); + $eq->id = $obj->rowid; + $eq->entity = $obj->entity; + $eq->fk_carrier = $obj->fk_carrier; + $eq->fk_equipment_type = $obj->fk_equipment_type; + $eq->label = $obj->label; + $eq->position_te = $obj->position_te; + $eq->width_te = $obj->width_te; + $eq->field_values = $obj->field_values; + $eq->fk_product = $obj->fk_product; + $eq->fk_protection = $obj->fk_protection; + $eq->protection_label = $obj->protection_label; + $eq->note_private = $obj->note_private; + $eq->status = $obj->status; + + $eq->type_label = $obj->type_label; + $eq->type_label_short = $obj->type_label_short; + $eq->type_ref = $obj->type_ref; + $eq->type_color = $obj->type_color; + $eq->type_picto = $obj->type_picto; + $eq->terminals_config = $obj->terminals_config; + $eq->protection_device_label = $obj->protection_device_label; + + $results[] = $eq; + } + $this->db->free($resql); + } + + return $results; + } + + /** + * Fetch protection devices (FI/RCD) for an Anlage + * Protection devices are equipment types that can protect other equipment + * + * @param int $anlageId Anlage ID + * @return array Array of Equipment objects that are protection devices + */ + public function fetchProtectionDevices($anlageId) + { + $results = array(); + + // Get all equipment for this anlage that have type with is_protection = 1 + // Or equipment types that are typically protection devices (FI, RCD, etc.) + $sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,"; + $sql .= " t.color as type_color, t.ref as type_ref, c.fk_anlage"; + $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_type as t ON e.fk_equipment_type = t.rowid"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_carrier as c ON e.fk_carrier = c.rowid"; + $sql .= " WHERE c.fk_anlage = ".((int) $anlageId); + $sql .= " AND e.status = 1"; + // Filter for protection device types (FI, RCD, FI_LS, etc.) + $sql .= " AND (t.ref LIKE '%FI%' OR t.ref LIKE '%RCD%' OR t.ref LIKE 'RCBO%')"; + $sql .= " ORDER BY c.position ASC, e.position_te ASC"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $eq = new Equipment($this->db); + $eq->id = $obj->rowid; + $eq->fk_carrier = $obj->fk_carrier; + $eq->fk_equipment_type = $obj->fk_equipment_type; + $eq->label = $obj->label; + $eq->position_te = $obj->position_te; + $eq->width_te = $obj->width_te; + $eq->type_label = $obj->type_label; + $eq->type_label_short = $obj->type_label_short; + $eq->type_color = $obj->type_color; + + $results[] = $eq; + } + $this->db->free($resql); + } + + return $results; + } + + /** + * Duplicate this equipment (for "+" button) + * + * @param User $user User that creates + * @param int $carrierId Target carrier (0 = same carrier) + * @param int $position Target position (-1 = auto) + * @return int Return integer <0 if KO, Id of new object if OK + */ + public function duplicate($user, $carrierId = 0, $position = -1) + { + require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php'; + + $newEquipment = new Equipment($this->db); + $newEquipment->fk_carrier = $carrierId > 0 ? $carrierId : $this->fk_carrier; + $newEquipment->fk_equipment_type = $this->fk_equipment_type; + $newEquipment->label = $this->label; + $newEquipment->width_te = $this->width_te; + $newEquipment->field_values = $this->field_values; + $newEquipment->fk_product = $this->fk_product; + $newEquipment->note_private = $this->note_private; + $newEquipment->status = 1; + + // Find position + if ($position >= 0) { + $newEquipment->position_te = $position; + } else { + // Auto-find next position after this equipment + $carrier = new EquipmentCarrier($this->db); + $carrier->fetch($newEquipment->fk_carrier); + $carrier->fetchEquipment(); + + // Try position right after this element + $tryPos = $this->position_te + $this->width_te; + if ($carrier->isPositionAvailable($tryPos, $this->width_te)) { + $newEquipment->position_te = $tryPos; + } else { + // Find any free position + $newEquipment->position_te = $carrier->getNextFreePosition($this->width_te); + if ($newEquipment->position_te < 0) { + $this->error = 'ErrorNoSpaceOnCarrier'; + return -1; + } + } + } + + return $newEquipment->create($user); + } + + /** + * Get field values as array + * + * @return array + */ + public function getFieldValues() + { + if (empty($this->field_values)) { + return array(); + } + $values = json_decode($this->field_values, true); + return is_array($values) ? $values : array(); + } + + /** + * Set field values from array + * + * @param array $values Key-value pairs + * @return void + */ + public function setFieldValues($values) + { + $this->field_values = json_encode($values); + } + + /** + * Get single field value + * + * @param string $fieldCode Field code + * @return mixed|null + */ + public function getFieldValue($fieldCode) + { + $values = $this->getFieldValues(); + return isset($values[$fieldCode]) ? $values[$fieldCode] : null; + } + + /** + * Get label for SVG block display + * Combines fields with show_on_block = 1 + * + * @return string Label to display on block (e.g. "B16") + */ + public function getBlockLabel() + { + $type = new EquipmentType($this->db); + if ($type->fetch($this->fk_equipment_type) <= 0) { + return $this->type_label_short ?: ''; + } + + $blockFields = $type->getBlockFields(); + if (empty($blockFields)) { + return $this->type_label_short ?: ''; + } + + $values = $this->getFieldValues(); + $parts = array(); + + foreach ($blockFields as $field) { + if (isset($values[$field->field_code]) && $values[$field->field_code] !== '') { + $parts[] = $values[$field->field_code]; + } + } + + if (empty($parts)) { + return $this->type_label_short ?: ''; + } + + return implode('', $parts); + } + + /** + * Get color for SVG block + * + * @return string Hex color code + */ + public function getBlockColor() + { + if (!empty($this->type_color)) { + return $this->type_color; + } + + // Default color + return '#3498db'; + } +} diff --git a/class/equipmentcarrier.class.php b/class/equipmentcarrier.class.php new file mode 100644 index 0000000..2094417 --- /dev/null +++ b/class/equipmentcarrier.class.php @@ -0,0 +1,426 @@ +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->fk_anlage) || empty($this->label)) { + $this->error = 'ErrorMissingParameters'; + return -1; + } + + // Get next position + if (empty($this->position)) { + $sql = "SELECT MAX(position) as maxpos FROM ".MAIN_DB_PREFIX.$this->table_element; + $sql .= " WHERE fk_anlage = ".((int) $this->fk_anlage); + $resql = $this->db->query($sql); + if ($resql) { + $obj = $this->db->fetch_object($resql); + $this->position = ($obj->maxpos !== null) ? $obj->maxpos + 1 : 0; + } + } + + $this->db->begin(); + + $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; + $sql .= "entity, fk_anlage, fk_panel, label, total_te, position, note_private, status,"; + $sql .= " date_creation, fk_user_creat"; + $sql .= ") VALUES ("; + $sql .= ((int) $conf->entity); + $sql .= ", ".((int) $this->fk_anlage); + $sql .= ", ".($this->fk_panel > 0 ? ((int) $this->fk_panel) : "NULL"); + $sql .= ", '".$this->db->escape($this->label)."'"; + $sql .= ", ".((int) ($this->total_te > 0 ? $this->total_te : 12)); + $sql .= ", ".((int) $this->position); + $sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL"); + $sql .= ", ".((int) ($this->status !== null ? $this->status : 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 c.*, a.label as anlage_label, p.label as panel_label"; + $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as a ON c.fk_anlage = a.rowid"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_panel as p ON c.fk_panel = p.rowid"; + $sql .= " WHERE c.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->fk_anlage = $obj->fk_anlage; + $this->fk_panel = $obj->fk_panel; + $this->label = $obj->label; + $this->total_te = $obj->total_te; + $this->position = $obj->position; + $this->note_private = $obj->note_private; + $this->status = $obj->status; + $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->anlage_label = $obj->anlage_label; + $this->panel_label = $obj->panel_label; + + $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 .= " label = '".$this->db->escape($this->label)."'"; + $sql .= ", fk_panel = ".($this->fk_panel > 0 ? ((int) $this->fk_panel) : "NULL"); + $sql .= ", total_te = ".((int) ($this->total_te > 0 ? $this->total_te : 12)); + $sql .= ", position = ".((int) $this->position); + $sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL"); + $sql .= ", status = ".((int) $this->status); + $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) + { + $error = 0; + $this->db->begin(); + + // Equipment is deleted via CASCADE + + $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 carriers for a Panel + * + * @param int $panelId Panel ID + * @param int $activeOnly Only active carriers + * @return array Array of EquipmentCarrier objects + */ + public function fetchByPanel($panelId, $activeOnly = 1) + { + $results = array(); + + $sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element; + $sql .= " WHERE fk_panel = ".((int) $panelId); + if ($activeOnly) { + $sql .= " AND status = 1"; + } + $sql .= " ORDER BY position ASC"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $carrier = new EquipmentCarrier($this->db); + $carrier->id = $obj->rowid; + $carrier->entity = $obj->entity; + $carrier->fk_anlage = $obj->fk_anlage; + $carrier->fk_panel = $obj->fk_panel; + $carrier->label = $obj->label; + $carrier->total_te = $obj->total_te; + $carrier->position = $obj->position; + $carrier->note_private = $obj->note_private; + $carrier->status = $obj->status; + + $results[] = $carrier; + } + $this->db->free($resql); + } + + return $results; + } + + /** + * Fetch all carriers for an Anlage + * + * @param int $anlageId Anlage ID + * @param int $activeOnly Only active carriers + * @return array Array of EquipmentCarrier objects + */ + public function fetchByAnlage($anlageId, $activeOnly = 1) + { + $results = array(); + + $sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element; + $sql .= " WHERE fk_anlage = ".((int) $anlageId); + if ($activeOnly) { + $sql .= " AND status = 1"; + } + $sql .= " ORDER BY position ASC"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $carrier = new EquipmentCarrier($this->db); + $carrier->id = $obj->rowid; + $carrier->entity = $obj->entity; + $carrier->fk_anlage = $obj->fk_anlage; + $carrier->fk_panel = $obj->fk_panel; + $carrier->label = $obj->label; + $carrier->total_te = $obj->total_te; + $carrier->position = $obj->position; + $carrier->note_private = $obj->note_private; + $carrier->status = $obj->status; + + $results[] = $carrier; + } + $this->db->free($resql); + } + + return $results; + } + + /** + * Fetch all equipment on this carrier + * + * @return array Array of Equipment objects + */ + public function fetchEquipment() + { + require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.class.php'; + + $equipment = new Equipment($this->db); + $this->equipment = $equipment->fetchByCarrier($this->id); + + return $this->equipment; + } + + /** + * Get array of occupied TE slots + * + * @return array Array of occupied slot numbers (0-based) + */ + public function getOccupiedSlots() + { + $occupied = array(); + + if (empty($this->equipment)) { + $this->fetchEquipment(); + } + + foreach ($this->equipment as $eq) { + for ($i = $eq->position_te; $i < $eq->position_te + $eq->width_te; $i++) { + $occupied[] = $i; + } + } + + return $occupied; + } + + /** + * Get used TE count + * + * @return int Number of used TE + */ + public function getUsedTE() + { + return count($this->getOccupiedSlots()); + } + + /** + * Get free TE count + * + * @return int Number of free TE + */ + public function getFreeTE() + { + return $this->total_te - $this->getUsedTE(); + } + + /** + * Find next free position for given width + * + * @param int $width Width in TE needed + * @return int Position (1-based) or -1 if no space + */ + public function getNextFreePosition($width = 1) + { + $occupied = $this->getOccupiedSlots(); + + // Positions are 1-based (1 to total_te) + for ($pos = 1; $pos <= $this->total_te - $width + 1; $pos++) { + $fits = true; + for ($i = $pos; $i < $pos + $width; $i++) { + if (in_array($i, $occupied)) { + $fits = false; + break; + } + } + if ($fits) { + return $pos; + } + } + + return -1; // No space available + } + + /** + * Check if position is available for given width + * + * @param int $position Start position (1-based) + * @param int $width Width in TE + * @param int $excludeEquipmentId Equipment ID to exclude (for updates) + * @return bool True if position is available + */ + public function isPositionAvailable($position, $width, $excludeEquipmentId = 0) + { + // Check bounds (positions are 1-based) + if ($position < 1 || $position + $width - 1 > $this->total_te) { + return false; + } + + if (empty($this->equipment)) { + $this->fetchEquipment(); + } + + foreach ($this->equipment as $eq) { + if ($excludeEquipmentId > 0 && $eq->id == $excludeEquipmentId) { + continue; + } + + // Check for overlap + $eqStart = $eq->position_te; + $eqEnd = $eq->position_te + $eq->width_te - 1; + $newStart = $position; + $newEnd = $position + $width - 1; + + if ($newStart <= $eqEnd && $newEnd >= $eqStart) { + return false; // Overlap + } + } + + return true; + } +} diff --git a/class/equipmentconnection.class.php b/class/equipmentconnection.class.php new file mode 100644 index 0000000..d2e7eea --- /dev/null +++ b/class/equipmentconnection.class.php @@ -0,0 +1,419 @@ +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(); + + $this->db->begin(); + + $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; + $sql .= "entity, fk_source, source_terminal, source_terminal_id, fk_target, target_terminal, target_terminal_id,"; + $sql .= " connection_type, color, output_label,"; + $sql .= " medium_type, medium_spec, medium_length,"; + $sql .= " is_rail, rail_start_te, rail_end_te, rail_phases, excluded_te, fk_carrier, position_y,"; + $sql .= " note_private, status, date_creation, fk_user_creat"; + $sql .= ") VALUES ("; + $sql .= ((int) $conf->entity); + $sql .= ", ".($this->fk_source > 0 ? ((int) $this->fk_source) : "NULL"); + $sql .= ", '".$this->db->escape($this->source_terminal ?: 'output')."'"; + $sql .= ", ".($this->source_terminal_id ? "'".$this->db->escape($this->source_terminal_id)."'" : "NULL"); + $sql .= ", ".($this->fk_target > 0 ? ((int) $this->fk_target) : "NULL"); + $sql .= ", '".$this->db->escape($this->target_terminal ?: 'input')."'"; + $sql .= ", ".($this->target_terminal_id ? "'".$this->db->escape($this->target_terminal_id)."'" : "NULL"); + $sql .= ", ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL"); + $sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL"); + $sql .= ", ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL"); + $sql .= ", ".($this->medium_type ? "'".$this->db->escape($this->medium_type)."'" : "NULL"); + $sql .= ", ".($this->medium_spec ? "'".$this->db->escape($this->medium_spec)."'" : "NULL"); + $sql .= ", ".($this->medium_length ? "'".$this->db->escape($this->medium_length)."'" : "NULL"); + $sql .= ", ".((int) $this->is_rail); + $sql .= ", ".($this->rail_start_te > 0 ? ((int) $this->rail_start_te) : "NULL"); + $sql .= ", ".($this->rail_end_te > 0 ? ((int) $this->rail_end_te) : "NULL"); + $sql .= ", ".($this->rail_phases ? "'".$this->db->escape($this->rail_phases)."'" : "NULL"); + $sql .= ", ".($this->excluded_te ? "'".$this->db->escape($this->excluded_te)."'" : "NULL"); + $sql .= ", ".($this->fk_carrier > 0 ? ((int) $this->fk_carrier) : "NULL"); + $sql .= ", ".((int) $this->position_y); + $sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL"); + $sql .= ", ".((int) $this->status); + $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 c.*, "; + $sql .= " src.label as source_label, tgt.label as target_label, car.label as carrier_label"; + $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as src ON c.fk_source = src.rowid"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as tgt ON c.fk_target = tgt.rowid"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_carrier as car ON c.fk_carrier = car.rowid"; + $sql .= " WHERE c.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->fk_source = $obj->fk_source; + $this->source_terminal = $obj->source_terminal; + $this->source_terminal_id = $obj->source_terminal_id; + $this->fk_target = $obj->fk_target; + $this->target_terminal = $obj->target_terminal; + $this->target_terminal_id = $obj->target_terminal_id; + $this->connection_type = $obj->connection_type; + $this->color = $obj->color; + $this->output_label = $obj->output_label; + $this->medium_type = $obj->medium_type; + $this->medium_spec = $obj->medium_spec; + $this->medium_length = $obj->medium_length; + $this->is_rail = $obj->is_rail; + $this->rail_start_te = $obj->rail_start_te; + $this->rail_end_te = $obj->rail_end_te; + $this->rail_phases = $obj->rail_phases; + $this->excluded_te = $obj->excluded_te; + $this->fk_carrier = $obj->fk_carrier; + $this->position_y = $obj->position_y; + $this->note_private = $obj->note_private; + $this->status = $obj->status; + $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->target_label = $obj->target_label; + $this->carrier_label = $obj->carrier_label; + + $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 .= " fk_source = ".($this->fk_source > 0 ? ((int) $this->fk_source) : "NULL"); + $sql .= ", source_terminal = '".$this->db->escape($this->source_terminal ?: 'output')."'"; + $sql .= ", fk_target = ".($this->fk_target > 0 ? ((int) $this->fk_target) : "NULL"); + $sql .= ", target_terminal = '".$this->db->escape($this->target_terminal ?: 'input')."'"; + $sql .= ", connection_type = ".($this->connection_type ? "'".$this->db->escape($this->connection_type)."'" : "NULL"); + $sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL"); + $sql .= ", output_label = ".($this->output_label ? "'".$this->db->escape($this->output_label)."'" : "NULL"); + $sql .= ", medium_type = ".($this->medium_type ? "'".$this->db->escape($this->medium_type)."'" : "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 .= ", is_rail = ".((int) $this->is_rail); + $sql .= ", rail_start_te = ".($this->rail_start_te > 0 ? ((int) $this->rail_start_te) : "NULL"); + $sql .= ", rail_end_te = ".($this->rail_end_te > 0 ? ((int) $this->rail_end_te) : "NULL"); + $sql .= ", rail_phases = ".($this->rail_phases ? "'".$this->db->escape($this->rail_phases)."'" : "NULL"); + $sql .= ", excluded_te = ".($this->excluded_te ? "'".$this->db->escape($this->excluded_te)."'" : "NULL"); + $sql .= ", fk_carrier = ".($this->fk_carrier > 0 ? ((int) $this->fk_carrier) : "NULL"); + $sql .= ", position_y = ".((int) $this->position_y); + $sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL"); + $sql .= ", status = ".((int) $this->status); + $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) + { + $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 connections for a carrier + * + * @param int $carrierId Carrier ID + * @param int $activeOnly Only active connections + * @return array Array of EquipmentConnection objects + */ + public function fetchByCarrier($carrierId, $activeOnly = 1) + { + $results = array(); + + $sql = "SELECT c.*, "; + $sql .= " src.label as source_label, src.position_te as source_pos, src.width_te as source_width,"; + $sql .= " tgt.label as target_label, tgt.position_te as target_pos"; + $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as src ON c.fk_source = src.rowid"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as tgt ON c.fk_target = tgt.rowid"; + $sql .= " WHERE c.fk_carrier = ".((int) $carrierId); + if ($activeOnly) { + $sql .= " AND c.status = 1"; + } + $sql .= " ORDER BY c.position_y ASC, c.rowid ASC"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $conn = new EquipmentConnection($this->db); + $conn->id = $obj->rowid; + $conn->entity = $obj->entity; + $conn->fk_source = $obj->fk_source; + $conn->source_terminal = $obj->source_terminal; + $conn->source_terminal_id = $obj->source_terminal_id; + $conn->fk_target = $obj->fk_target; + $conn->target_terminal = $obj->target_terminal; + $conn->target_terminal_id = $obj->target_terminal_id; + $conn->connection_type = $obj->connection_type; + $conn->color = $obj->color; + $conn->output_label = $obj->output_label; + $conn->medium_type = $obj->medium_type; + $conn->medium_spec = $obj->medium_spec; + $conn->medium_length = $obj->medium_length; + $conn->is_rail = $obj->is_rail; + $conn->rail_start_te = $obj->rail_start_te; + $conn->rail_end_te = $obj->rail_end_te; + $conn->rail_phases = $obj->rail_phases; + $conn->excluded_te = $obj->excluded_te; + $conn->fk_carrier = $obj->fk_carrier; + $conn->position_y = $obj->position_y; + $conn->status = $obj->status; + + $conn->source_label = $obj->source_label; + $conn->source_pos = $obj->source_pos; + $conn->source_width = $obj->source_width; + $conn->target_label = $obj->target_label; + $conn->target_pos = $obj->target_pos; + + $results[] = $conn; + } + $this->db->free($resql); + } + + return $results; + } + + /** + * Fetch all outputs for an equipment + * + * @param int $equipmentId Equipment ID + * @return array Array of EquipmentConnection objects + */ + public function fetchOutputs($equipmentId) + { + $results = array(); + + $sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element; + $sql .= " WHERE fk_source = ".((int) $equipmentId); + $sql .= " AND fk_target IS NULL"; + $sql .= " AND status = 1"; + $sql .= " ORDER BY position_y ASC"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $conn = new EquipmentConnection($this->db); + $conn->id = $obj->rowid; + $conn->fk_source = $obj->fk_source; + $conn->connection_type = $obj->connection_type; + $conn->color = $obj->color; + $conn->output_label = $obj->output_label; + $conn->medium_type = $obj->medium_type; + $conn->medium_spec = $obj->medium_spec; + $conn->medium_length = $obj->medium_length; + $conn->fk_carrier = $obj->fk_carrier; + $conn->position_y = $obj->position_y; + $conn->status = $obj->status; + + $results[] = $conn; + } + $this->db->free($resql); + } + + return $results; + } + + /** + * Get display color + * + * @return string Color hex code + */ + public function getColor() + { + if (!empty($this->color)) { + return $this->color; + } + return '#888888'; // Default grey + } + + /** + * Get display label for output + * + * @return string Display label + */ + public function getDisplayLabel() + { + $parts = array(); + + if ($this->output_label) { + $parts[] = $this->output_label; + } + if ($this->medium_type) { + $mediumInfo = $this->medium_type; + if ($this->medium_spec) { + $mediumInfo .= ' '.$this->medium_spec; + } + $parts[] = $mediumInfo; + } + + return implode(' - ', $parts); + } +} diff --git a/class/equipmentpanel.class.php b/class/equipmentpanel.class.php new file mode 100644 index 0000000..1e368b7 --- /dev/null +++ b/class/equipmentpanel.class.php @@ -0,0 +1,285 @@ +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->fk_anlage)) { + $this->error = 'ErrorMissingParameters'; + return -1; + } + + // Get next position + if (empty($this->position)) { + $sql = "SELECT MAX(position) as maxpos FROM ".MAIN_DB_PREFIX.$this->table_element; + $sql .= " WHERE fk_anlage = ".((int) $this->fk_anlage); + $resql = $this->db->query($sql); + if ($resql) { + $obj = $this->db->fetch_object($resql); + $this->position = ($obj->maxpos !== null) ? $obj->maxpos + 1 : 0; + } + } + + // Default label if not set + if (empty($this->label)) { + $this->label = 'Feld '.($this->position + 1); + } + + $this->db->begin(); + + $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; + $sql .= "entity, fk_anlage, label, position, note_private, status,"; + $sql .= " date_creation, fk_user_creat"; + $sql .= ") VALUES ("; + $sql .= ((int) $conf->entity); + $sql .= ", ".((int) $this->fk_anlage); + $sql .= ", '".$this->db->escape($this->label)."'"; + $sql .= ", ".((int) $this->position); + $sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL"); + $sql .= ", ".((int) ($this->status !== null ? $this->status : 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 p.*, a.label as anlage_label"; + $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as p"; + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as a ON p.fk_anlage = a.rowid"; + $sql .= " WHERE p.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->fk_anlage = $obj->fk_anlage; + $this->label = $obj->label; + $this->position = $obj->position; + $this->note_private = $obj->note_private; + $this->status = $obj->status; + $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->anlage_label = $obj->anlage_label; + + $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 .= " label = '".$this->db->escape($this->label)."'"; + $sql .= ", position = ".((int) $this->position); + $sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL"); + $sql .= ", status = ".((int) $this->status); + $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) + { + $error = 0; + $this->db->begin(); + + // Carriers are deleted via CASCADE or need to be reassigned + + $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 panels for an Anlage + * + * @param int $anlageId Anlage ID + * @param int $activeOnly Only active panels + * @return array Array of EquipmentPanel objects + */ + public function fetchByAnlage($anlageId, $activeOnly = 1) + { + $results = array(); + + $sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element; + $sql .= " WHERE fk_anlage = ".((int) $anlageId); + if ($activeOnly) { + $sql .= " AND status = 1"; + } + $sql .= " ORDER BY position ASC"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $panel = new EquipmentPanel($this->db); + $panel->id = $obj->rowid; + $panel->entity = $obj->entity; + $panel->fk_anlage = $obj->fk_anlage; + $panel->label = $obj->label; + $panel->position = $obj->position; + $panel->note_private = $obj->note_private; + $panel->status = $obj->status; + + $results[] = $panel; + } + $this->db->free($resql); + } + + return $results; + } + + /** + * Fetch all carriers in this panel + * + * @return array Array of EquipmentCarrier objects + */ + public function fetchCarriers() + { + require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php'; + + $carrier = new EquipmentCarrier($this->db); + $this->carriers = $carrier->fetchByPanel($this->id); + + return $this->carriers; + } + + /** + * Get total carriers count + * + * @return int Number of carriers + */ + public function getCarrierCount() + { + if (empty($this->carriers)) { + $this->fetchCarriers(); + } + return count($this->carriers); + } +} diff --git a/class/equipmenttype.class.php b/class/equipmenttype.class.php new file mode 100644 index 0000000..52e0257 --- /dev/null +++ b/class/equipmenttype.class.php @@ -0,0 +1,374 @@ +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) || empty($this->fk_system)) { + $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,"; + $sql .= " width_te, color, fk_product, terminals_config,"; + $sql .= " picto, is_system, position, active,"; + $sql .= " 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 .= ", ".((int) ($this->width_te > 0 ? $this->width_te : 1)); + $sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL"); + $sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL"); + $sql .= ", ".($this->terminals_config ? "'".$this->db->escape($this->terminals_config)."'" : "NULL"); + $sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "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 .= " p.ref as product_ref, p.label as product_label"; + $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 .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON t.fk_product = p.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->width_te = $obj->width_te; + $this->color = $obj->color; + $this->fk_product = $obj->fk_product; + $this->terminals_config = $obj->terminals_config; + $this->picto = $obj->picto; + $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->product_ref = $obj->product_ref; + $this->product_label = $obj->product_label; + + $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 .= ", width_te = ".((int) ($this->width_te > 0 ? $this->width_te : 1)); + $sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL"); + $sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL"); + $sql .= ", terminals_config = ".($this->terminals_config ? "'".$this->db->escape($this->terminals_config)."'" : "NULL"); + $sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "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) + { + global $conf; + + // Check if type is in use + $sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_equipment"; + $sql .= " WHERE fk_equipment_type = ".((int) $this->id); + $resql = $this->db->query($sql); + if ($resql) { + $obj = $this->db->fetch_object($resql); + if ($obj->cnt > 0) { + $this->error = 'ErrorTypeInUse'; + return -1; + } + } + + // Cannot delete system types + if ($this->is_system) { + $this->error = 'ErrorCannotDeleteSystemType'; + return -2; + } + + $error = 0; + $this->db->begin(); + + // Delete fields first + $sql = "DELETE FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field WHERE fk_equipment_type = ".((int) $this->id); + $this->db->query($sql); + + // Delete type + $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 equipment types for a system + * + * @param int $systemId System ID (0 = all) + * @param int $activeOnly Only active types + * @return array Array of EquipmentType 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) { + $sql .= " AND t.fk_system = ".((int) $systemId); + } + if ($activeOnly) { + $sql .= " AND t.active = 1"; + } + $sql .= " ORDER BY t.fk_system ASC, t.position ASC, t.label ASC"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $type = new EquipmentType($this->db); + $type->id = $obj->rowid; + $type->ref = $obj->ref; + $type->label = $obj->label; + $type->label_short = $obj->label_short; + $type->fk_system = $obj->fk_system; + $type->width_te = $obj->width_te; + $type->color = $obj->color; + $type->fk_product = $obj->fk_product; + $type->picto = $obj->picto; + $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 fields for this equipment type + * + * @param int $activeOnly Only active fields + * @return array Array of field objects + */ + public function fetchFields($activeOnly = 1) + { + $results = array(); + + $sql = "SELECT * FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field"; + $sql .= " WHERE fk_equipment_type = ".((int) $this->id); + if ($activeOnly) { + $sql .= " AND active = 1"; + } + $sql .= " ORDER BY position ASC"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $results[] = $obj; + } + $this->db->free($resql); + } + + $this->fields = $results; + return $results; + } + + /** + * Get fields that should be shown on the SVG block + * + * @return array Array of field objects with show_on_block = 1 + */ + public function getBlockFields() + { + $results = array(); + + $sql = "SELECT * FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field"; + $sql .= " WHERE fk_equipment_type = ".((int) $this->id); + $sql .= " AND show_on_block = 1"; + $sql .= " AND active = 1"; + $sql .= " ORDER BY position ASC"; + + $resql = $this->db->query($sql); + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + $results[] = $obj; + } + $this->db->free($resql); + } + + return $results; + } +} diff --git a/core/modules/modKundenKarte.class.php b/core/modules/modKundenKarte.class.php index 7ec7015..6f7e240 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 = '2.5'; + $this->version = '3.0'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; @@ -393,6 +393,23 @@ class modKundenKarte extends DolibarrModules 'target' => '', 'user' => 0, ); + + // Admin submenu: Manage Equipment Types + $this->menu[$r++] = array( + 'fk_menu' => 'fk_mainmenu=kundenkarte', + 'type' => 'left', + 'titre' => 'EquipmentTypes', + 'prefix' => img_picto('', 'fa-microchip', 'class="pictofixedwidth valignmiddle paddingright"'), + 'mainmenu' => 'kundenkarte', + 'leftmenu' => 'kundenkarte_equipment_types', + 'url' => '/kundenkarte/admin/equipment_types.php', + 'langs' => 'kundenkarte@kundenkarte', + 'position' => 1000 + $r, + 'enabled' => 'isModEnabled("kundenkarte")', + 'perms' => '$user->hasRight("kundenkarte", "admin")', + 'target' => '', + 'user' => 0, + ); /* END MODULEBUILDER LEFTMENU */ diff --git a/css/kundenkarte.css b/css/kundenkarte.css index 5782ed9..a8bbe02 100755 --- a/css/kundenkarte.css +++ b/css/kundenkarte.css @@ -77,13 +77,15 @@ } .kundenkarte-tree-actions { - display: none !important; + display: flex !important; + gap: 5px !important; margin-left: 10px !important; + opacity: 0.6 !important; + transition: opacity 0.15s !important; } .kundenkarte-tree-item:hover .kundenkarte-tree-actions { - display: flex !important; - gap: 5px !important; + opacity: 1 !important; } .kundenkarte-tree-actions a { @@ -625,3 +627,1175 @@ white-space: nowrap !important; max-width: 70px !important; } + +/* ======================================== + EQUIPMENT / HUTSCHIENEN + ======================================== */ + +.kundenkarte-equipment-container { + margin-top: 15px !important; + padding: 15px !important; + background: #252525 !important; + border: 1px solid #444 !important; + border-radius: 6px !important; + overflow: visible !important; +} + +.kundenkarte-equipment-header { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + margin-bottom: 15px !important; + padding-bottom: 10px !important; + border-bottom: 1px solid #444 !important; +} + +.kundenkarte-equipment-header h4 { + margin: 0 !important; + color: #e0e0e0 !important; + font-size: 1em !important; +} + +.kundenkarte-add-carrier { + background: #2d6a4f !important; + color: #fff !important; + border: 1px solid #40916c !important; + padding: 6px 12px !important; + border-radius: 4px !important; + cursor: pointer !important; + font-size: 0.85em !important; + display: inline-flex !important; + align-items: center !important; + gap: 5px !important; + text-decoration: none !important; +} + +.kundenkarte-add-carrier:hover { + background: #40916c !important; +} + +/* Panels Container */ +.kundenkarte-panels-container { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + gap: 20px !important; + padding: 10px 5px !important; +} + +/* Individual Panel (Feld) */ +.kundenkarte-panel { + flex: 0 0 auto !important; + background: #1e1e1e !important; + border: 2px solid #555 !important; + border-radius: 8px !important; + padding: 10px !important; + display: inline-block !important; +} + +.kundenkarte-panel-direct { + border-style: dashed !important; + border-color: #666 !important; +} + +/* Quick-add panel button (duplicate last panel) */ +.kundenkarte-panel-quickadd { + flex: 0 0 auto !important; + min-width: 60px !important; + max-width: 80px !important; + background: transparent !important; + border: 2px dashed #555 !important; + border-radius: 8px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + cursor: pointer !important; + transition: all 0.2s ease !important; + min-height: 100px !important; +} + +.kundenkarte-panel-quickadd:hover { + border-color: #27ae60 !important; + background: rgba(39, 174, 96, 0.1) !important; +} + +.kundenkarte-panel-quickadd i { + font-size: 24px !important; + color: #555 !important; + transition: color 0.2s ease !important; +} + +.kundenkarte-panel-quickadd:hover i { + color: #27ae60 !important; +} + +/* Quick-add carrier button (duplicate last carrier) */ +.kundenkarte-carrier-quickadd { + background: transparent !important; + border: 2px dashed #555 !important; + border-radius: 6px !important; + padding: 15px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + cursor: pointer !important; + transition: all 0.2s ease !important; + margin-top: 10px !important; +} + +.kundenkarte-carrier-quickadd:hover { + border-color: #27ae60 !important; + background: rgba(39, 174, 96, 0.1) !important; +} + +.kundenkarte-carrier-quickadd i { + font-size: 18px !important; + color: #555 !important; + transition: color 0.2s ease !important; +} + +.kundenkarte-carrier-quickadd:hover i { + color: #27ae60 !important; +} + +.kundenkarte-panel-header { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + padding: 8px 12px !important; + background: #333 !important; + border-radius: 4px !important; + margin-bottom: 10px !important; +} + +.kundenkarte-panel-label { + font-weight: bold !important; + color: #e0e0e0 !important; + font-size: 0.95em !important; +} + +.kundenkarte-panel-actions { + display: flex !important; + gap: 8px !important; +} + +.kundenkarte-panel-actions a { + color: #aaa !important; + text-decoration: none !important; + padding: 4px !important; +} + +.kundenkarte-panel-actions a:hover { + color: #fff !important; +} + +/* Add Panel Button */ +.kundenkarte-add-panel { + background: #1e5f74 !important; + color: #fff !important; + border: 1px solid #2c8ba0 !important; + padding: 6px 12px !important; + border-radius: 4px !important; + cursor: pointer !important; + font-size: 0.85em !important; + display: inline-flex !important; + align-items: center !important; + gap: 5px !important; + text-decoration: none !important; +} + +.kundenkarte-add-panel:hover { + background: #2c8ba0 !important; +} + +.kundenkarte-carriers-list { + display: flex !important; + flex-direction: column !important; + gap: 20px !important; + overflow: visible !important; +} + +/* Individual Carrier (Hutschiene) */ +.kundenkarte-carrier { + background: #2d2d2d !important; + border: 1px solid #444 !important; + border-radius: 6px !important; + overflow: visible !important; + display: inline-block !important; + min-width: 100% !important; +} + +.kundenkarte-carrier-header { + display: flex !important; + align-items: center !important; + padding: 10px 15px !important; + background: #333 !important; + border-bottom: 1px solid #444 !important; +} + +.kundenkarte-carrier-label { + font-weight: 600 !important; + color: #e0e0e0 !important; + flex: 1 !important; +} + +.kundenkarte-carrier-info { + color: #888 !important; + font-size: 0.85em !important; + margin-right: 15px !important; +} + +.kundenkarte-carrier-actions { + display: flex !important; + gap: 8px !important; +} + +.kundenkarte-carrier-actions a { + color: #888 !important; + padding: 4px 8px !important; + border-radius: 3px !important; + text-decoration: none !important; +} + +.kundenkarte-carrier-actions a:hover { + background: #444 !important; + color: #fff !important; +} + +.kundenkarte-carrier-add-equipment { + color: #27ae60 !important; +} + +.kundenkarte-carrier-add-equipment:hover { + background: #27ae60 !important; + color: #fff !important; +} + +/* SVG Container */ +.kundenkarte-carrier-svg-container { + position: relative !important; + padding: 15px !important; + background: #1e1e1e !important; + overflow: visible !important; + border-radius: 0 0 6px 6px !important; + display: inline-block !important; + min-width: calc(100% - 30px) !important; +} + +.kundenkarte-carrier-svg { + display: block !important; + background: linear-gradient(to bottom, #3a3a3a 0%, #2a2a2a 100%) !important; + border-radius: 4px !important; + border: 1px solid #555 !important; + min-width: max-content !important; + flex-shrink: 0 !important; +} + +/* Clickable slot overlay */ +.kundenkarte-carrier-slots { + position: absolute !important; + top: 15px !important; + left: 15px !important; + pointer-events: none !important; +} + +.kundenkarte-slot-empty { + position: absolute !important; + top: 0 !important; + height: 100% !important; + pointer-events: all !important; + cursor: pointer !important; + opacity: 0 !important; + transition: opacity 0.15s, background 0.15s !important; +} + +.kundenkarte-slot-empty:hover { + opacity: 1 !important; + background: rgba(39, 174, 96, 0.3) !important; + border: 1px dashed #27ae60 !important; + border-radius: 3px !important; +} + +.kundenkarte-slot-drop-target { + opacity: 1 !important; + background: rgba(52, 152, 219, 0.4) !important; + border: 2px dashed #3498db !important; + border-radius: 3px !important; +} + +/* Quick-add slot (duplicate last equipment) */ +.kundenkarte-slot-quickadd { + position: absolute !important; + top: 2px !important; + height: calc(100% - 4px) !important; + pointer-events: all !important; + cursor: pointer !important; + background: rgba(39, 174, 96, 0.15) !important; + border: 2px dashed #27ae60 !important; + border-radius: 4px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + transition: all 0.2s ease !important; +} + +.kundenkarte-slot-quickadd i { + color: #27ae60 !important; + font-size: 24px !important; +} + +.kundenkarte-slot-quickadd:hover { + background: rgba(39, 174, 96, 0.4) !important; + border-color: #1e8449 !important; +} + +.kundenkarte-slot-quickadd:hover i { + color: #fff !important; +} + +/* Equipment Block (SVG elements styled via CSS) */ +.kundenkarte-equipment-block { + cursor: pointer !important; + transition: filter 0.15s !important; +} + +.kundenkarte-equipment-block:hover { + filter: brightness(1.15) !important; +} + +.kundenkarte-equipment-block.dragging { + opacity: 0.5 !important; +} + +/* Duplicate button on blocks */ +.kundenkarte-equipment-duplicate { + cursor: pointer !important; + opacity: 0 !important; + transition: opacity 0.15s !important; +} + +.kundenkarte-equipment-block:hover .kundenkarte-equipment-duplicate { + opacity: 1 !important; +} + +/* Equipment Tooltip */ +.kundenkarte-equipment-tooltip { + position: absolute !important; + z-index: 10001 !important; + background: #1e1e1e !important; + border: 1px solid #555 !important; + border-radius: 6px !important; + box-shadow: 0 4px 12px rgba(0,0,0,0.5) !important; + padding: 12px !important; + min-width: 180px !important; + max-width: 280px !important; + display: none !important; + pointer-events: none !important; +} + +.kundenkarte-equipment-tooltip.visible { + display: block !important; +} + +.kundenkarte-equipment-tooltip-header { + margin-bottom: 8px !important; + padding-bottom: 8px !important; + border-bottom: 1px solid #444 !important; + color: #e0e0e0 !important; +} + +.kundenkarte-equipment-tooltip-fields { + font-size: 0.9em !important; +} + +.kundenkarte-equipment-tooltip-fields .field-label { + color: #888 !important; +} + +.kundenkarte-equipment-tooltip-fields .field-value { + color: #e0e0e0 !important; +} + +/* Modal Footer */ +.kundenkarte-modal-footer { + padding: 15px 20px !important; + border-top: 1px solid #444 !important; + background: #2d2d2d !important; + text-align: right !important; + border-radius: 0 0 8px 8px !important; +} + +.kundenkarte-modal-footer .button { + margin-left: 10px !important; +} + +/* Equipment color swatches in admin */ +.kundenkarte-color-swatch { + display: inline-block !important; + width: 24px !important; + height: 24px !important; + border-radius: 4px !important; + border: 1px solid #666 !important; + vertical-align: middle !important; + margin-right: 8px !important; +} + +/* Equipment Type Badge */ +.kundenkarte-equipment-type-badge { + display: inline-flex !important; + align-items: center !important; + gap: 6px !important; + padding: 4px 10px !important; + border-radius: 4px !important; + font-size: 0.85em !important; + background: #333 !important; + border: 1px solid #444 !important; + color: #e0e0e0 !important; +} + +.kundenkarte-equipment-type-badge .te-width { + color: #888 !important; + font-size: 0.9em !important; +} + +/* Responsive: scroll on small screens */ +@media (max-width: 768px) { + .kundenkarte-carrier-svg-container { + overflow-x: auto !important; + -webkit-overflow-scrolling: touch !important; + } +} + +/* ======================================== + CONNECTIONS (Stromverbindungen) + ======================================== */ + +.kundenkarte-connections-container { + margin-top: 5px !important; + padding: 8px 15px !important; + background: #252525 !important; + border-top: 1px solid #333 !important; +} + +.kundenkarte-connections-svg { + display: block !important; + width: 100% !important; +} + +/* Connection lines */ +.kundenkarte-connection-line { + fill: none !important; + stroke-width: 2 !important; + stroke-linecap: round !important; +} + +.kundenkarte-connection-line.phase-L1 { stroke: #8B4513 !important; } +.kundenkarte-connection-line.phase-L2 { stroke: #000000 !important; } +.kundenkarte-connection-line.phase-L3 { stroke: #808080 !important; } +.kundenkarte-connection-line.phase-N { stroke: #0000FF !important; } +.kundenkarte-connection-line.phase-PE { stroke: #9ACD32 !important; } +.kundenkarte-connection-line.phase-L1N { stroke: #8B4513 !important; stroke-width: 3 !important; } +.kundenkarte-connection-line.phase-L1L2L3 { stroke: #8B4513 !important; stroke-width: 4 !important; } +.kundenkarte-connection-line.phase-L1L2L3N { stroke: #8B4513 !important; stroke-width: 5 !important; } +.kundenkarte-connection-line.phase-L1L2L3NPE { stroke: #8B4513 !important; stroke-width: 6 !important; } + +/* Rail/Sammelschiene (generic for all systems) */ +.kundenkarte-rail { + cursor: pointer !important; + transition: opacity 0.2s !important; +} + +.kundenkarte-rail-line { + stroke-width: 4 !important; + stroke-linecap: round !important; + transition: stroke-width 0.2s, filter 0.2s !important; +} + +.kundenkarte-rail:hover .kundenkarte-rail-line { + filter: brightness(1.3) drop-shadow(0 0 3px rgba(255,255,255,0.3)) !important; + stroke-width: 6 !important; +} + +.kundenkarte-rail:hover text { + fill: #fff !important; +} + +.kundenkarte-rail text { + font-weight: bold !important; + transition: fill 0.2s !important; +} + +/* Consumer output (Abgang) */ +.kundenkarte-output { + cursor: pointer !important; + transition: opacity 0.2s !important; +} + +.kundenkarte-output-line { + fill: none !important; + stroke-width: 2 !important; + stroke-linecap: round !important; + stroke-dasharray: 5, 3 !important; + transition: stroke-width 0.2s !important; +} + +.kundenkarte-output-label { + font-size: 10px !important; + fill: #aaa !important; + transition: fill 0.2s, font-weight 0.2s !important; +} + +.kundenkarte-output:hover .kundenkarte-output-label { + fill: #fff !important; + font-weight: bold !important; +} + +.kundenkarte-output:hover .kundenkarte-output-line { + stroke-width: 3 !important; + filter: drop-shadow(0 0 2px rgba(255,255,255,0.3)) !important; +} + +/* Add connection button */ +.kundenkarte-add-connection { + display: inline-flex !important; + align-items: center !important; + gap: 5px !important; + padding: 5px 10px !important; + margin-top: 8px !important; + background: #333 !important; + border: 1px dashed #555 !important; + border-radius: 4px !important; + color: #888 !important; + font-size: 0.8em !important; + cursor: pointer !important; + text-decoration: none !important; +} + +.kundenkarte-add-connection:hover { + background: #444 !important; + border-color: #27ae60 !important; + color: #27ae60 !important; +} + +/* Connection legend */ +.kundenkarte-connection-legend { + display: flex !important; + flex-wrap: wrap !important; + gap: 10px !important; + margin-top: 8px !important; + padding-top: 8px !important; + border-top: 1px solid #333 !important; + font-size: 0.75em !important; +} + +.kundenkarte-legend-item { + display: flex !important; + align-items: center !important; + gap: 4px !important; + color: #888 !important; +} + +.kundenkarte-legend-color { + width: 20px !important; + height: 4px !important; + border-radius: 2px !important; +} + +.kundenkarte-legend-color.phase-L1 { background: #8B4513 !important; } +.kundenkarte-legend-color.phase-L2 { background: #000000 !important; border: 1px solid #444 !important; } +.kundenkarte-legend-color.phase-L3 { background: #808080 !important; } +.kundenkarte-legend-color.phase-N { background: #0000FF !important; } +.kundenkarte-legend-color.phase-PE { background: #9ACD32 !important; } + +/* Connection dialog form */ +.kundenkarte-connection-form .form-row { + display: flex !important; + gap: 10px !important; + margin-bottom: 12px !important; +} + +.kundenkarte-connection-form .form-group { + flex: 1 !important; +} + +.kundenkarte-connection-form label { + display: block !important; + margin-bottom: 4px !important; + color: #aaa !important; + font-size: 0.85em !important; +} + +.kundenkarte-connection-form select, +.kundenkarte-connection-form input { + width: 100% !important; + padding: 8px 10px !important; + background: #2d2d2d !important; + border: 1px solid #444 !important; + border-radius: 4px !important; + color: #e0e0e0 !important; +} + +.kundenkarte-connection-form select:focus, +.kundenkarte-connection-form input:focus { + border-color: #3498db !important; + outline: none !important; +} + +/* Phase selection buttons */ +.kundenkarte-phase-buttons { + display: flex !important; + flex-wrap: wrap !important; + gap: 5px !important; +} + +.kundenkarte-phase-btn { + padding: 6px 12px !important; + border: 1px solid #444 !important; + border-radius: 4px !important; + background: #2d2d2d !important; + color: #aaa !important; + cursor: pointer !important; + font-size: 0.85em !important; +} + +.kundenkarte-phase-btn:hover { + border-color: #666 !important; + color: #fff !important; +} + +.kundenkarte-phase-btn.active { + border-color: #3498db !important; + background: #3498db !important; + color: #fff !important; +} + +.kundenkarte-phase-btn.phase-L1 { border-left: 3px solid #8B4513 !important; } +.kundenkarte-phase-btn.phase-L2 { border-left: 3px solid #333 !important; } +.kundenkarte-phase-btn.phase-L3 { border-left: 3px solid #808080 !important; } +.kundenkarte-phase-btn.phase-N { border-left: 3px solid #0000FF !important; } +.kundenkarte-phase-btn.phase-PE { border-left: 3px solid #9ACD32 !important; } + +/* ======================================== + jsPlumb Interactive Editor (Prototype) + ======================================== */ + +.kundenkarte-jsplumb-prototype { + margin-top: 20px !important; +} + +.kundenkarte-jsplumb-prototype .titre { + background: #2d2d2d !important; + padding: 10px 15px !important; + border-radius: 6px !important; + color: #ddd !important; +} + +.kundenkarte-jsplumb-prototype .titre:hover { + background: #3d3d3d !important; +} + +#jsplumb-canvas { + position: relative !important; + background: linear-gradient(135deg, #1a1a1a 0%, #252525 100%) !important; +} + +.jsplumb-node { + transition: box-shadow 0.2s ease !important; + z-index: 10 !important; +} + +.jsplumb-node:hover { + z-index: 20 !important; + transform: scale(1.02); +} + +.jsplumb-node.jsplumb-external { + background: linear-gradient(135deg, #27ae60 0%, #1e8449 100%) !important; +} + +/* jsPlumb endpoint styling */ +.jtk-endpoint { + z-index: 30 !important; +} + +.jtk-connector { + z-index: 5 !important; +} + +/* Connection hover effect */ +.jtk-connector:hover path { + stroke-width: 3px !important; +} + +/* ======================================== + CONNECTION EDITOR (Interactive SVG) + ======================================== */ + +.kundenkarte-connection-editor-wrapper { + border-top: 1px solid #333 !important; + background: #1a1a1a !important; +} + +.kundenkarte-connection-editor-header { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + padding: 8px 15px !important; + background: #252525 !important; + border-bottom: 1px solid #333 !important; +} + +.kundenkarte-connection-editor-toggle { + display: inline-flex !important; + align-items: center !important; + gap: 8px !important; + color: #888 !important; + text-decoration: none !important; + font-size: 0.85em !important; + padding: 4px 8px !important; + border-radius: 4px !important; + transition: all 0.2s ease !important; +} + +.kundenkarte-connection-editor-toggle:hover { + color: #fff !important; + background: #333 !important; +} + +.kundenkarte-connection-editor-toggle i { + transition: transform 0.3s ease !important; +} + +.kundenkarte-connection-editor-actions { + display: flex !important; + gap: 8px !important; +} + +.kundenkarte-connection-editor-actions button { + display: inline-flex !important; + align-items: center !important; + gap: 5px !important; + padding: 5px 10px !important; + background: #333 !important; + border: 1px solid #444 !important; + border-radius: 4px !important; + color: #aaa !important; + font-size: 0.8em !important; + cursor: pointer !important; + transition: all 0.2s ease !important; +} + +.kundenkarte-connection-editor-actions button:hover { + background: #444 !important; + border-color: #555 !important; + color: #fff !important; +} + +.kundenkarte-add-busbar-btn:hover { + border-color: #3498db !important; + color: #3498db !important; +} + +.kundenkarte-add-conn-btn:hover { + border-color: #27ae60 !important; + color: #27ae60 !important; +} + +.kundenkarte-connection-editor { + display: none !important; + padding: 15px !important; + background: linear-gradient(135deg, #1a1a1a 0%, #202020 100%) !important; + overflow-x: auto !important; +} + +.kundenkarte-connection-editor.expanded { + display: block !important; +} + +.kundenkarte-connection-svg { + display: block !important; + min-width: 100% !important; + border-radius: 4px !important; +} + +/* Endpoints (connection points) */ +.kundenkarte-endpoint { + cursor: crosshair !important; + transition: all 0.2s ease !important; +} + +.kundenkarte-endpoint:hover, +.kundenkarte-endpoint.hover { + r: 8 !important; + filter: drop-shadow(0 0 4px rgba(52, 152, 219, 0.8)) !important; +} + +.kundenkarte-endpoint.dragging { + fill: #3498db !important; + r: 10 !important; + filter: drop-shadow(0 0 6px rgba(52, 152, 219, 1)) !important; +} + +/* Busbar elements */ +.kundenkarte-busbar-element { + cursor: pointer !important; +} + +.kundenkarte-busbar-element:hover .kundenkarte-busbar-line { + filter: brightness(1.2) !important; +} + +.kundenkarte-busbar-line { + transition: filter 0.2s ease !important; +} + +.kundenkarte-busbar-endpoint { + opacity: 0.6 !important; + transition: opacity 0.2s ease, r 0.2s ease !important; +} + +.kundenkarte-busbar-element:hover .kundenkarte-busbar-endpoint, +.kundenkarte-busbar-endpoint:hover { + opacity: 1 !important; +} + +/* Equipment endpoints in editor */ +.kundenkarte-equipment-endpoints rect { + transition: filter 0.2s ease !important; +} + +.kundenkarte-equipment-endpoints:hover rect { + filter: brightness(1.15) !important; +} + +.kundenkarte-equipment-input, +.kundenkarte-equipment-output { + opacity: 0.7 !important; + transition: all 0.2s ease !important; +} + +.kundenkarte-equipment-endpoints:hover .kundenkarte-equipment-input, +.kundenkarte-equipment-endpoints:hover .kundenkarte-equipment-output, +.kundenkarte-equipment-input:hover, +.kundenkarte-equipment-output:hover { + opacity: 1 !important; +} + +/* Connection paths */ +.kundenkarte-connection-path { + cursor: pointer !important; + transition: stroke-width 0.2s ease, filter 0.2s ease !important; +} + +.kundenkarte-connection-path:hover { + stroke-width: 4 !important; + filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.5)) !important; +} + +.kundenkarte-connection-path-shadow { + pointer-events: none !important; +} + +.kundenkarte-connection-group text { + pointer-events: none !important; + transition: fill 0.2s ease !important; +} + +.kundenkarte-connection-group:hover text { + fill: #fff !important; +} + +/* Connection layer ordering - SVG elements rendered in order */ +.kundenkarte-connections-layer { + pointer-events: auto !important; +} + +.kundenkarte-equipment-layer { + pointer-events: auto !important; +} + +/* Make connection paths clickable with wider hit area */ +.kundenkarte-connection-group { + cursor: pointer !important; +} + +.kundenkarte-connection-group:hover .kundenkarte-connection-path { + stroke-width: 4 !important; +} + +.kundenkarte-connection-group:hover .kundenkarte-connection-path-shadow { + stroke-width: 7 !important; +} + +/* Drag preview line */ +.kundenkarte-drag-preview { + pointer-events: none !important; + animation: dashOffset 0.5s linear infinite !important; +} + +@keyframes dashOffset { + 0% { stroke-dashoffset: 0; } + 100% { stroke-dashoffset: 10; } +} + +/* Phase-specific colors for endpoints */ +.kundenkarte-endpoint[data-phase="L1"] { stroke: #8B4513 !important; } +.kundenkarte-endpoint[data-phase="L2"] { stroke: #000000 !important; } +.kundenkarte-endpoint[data-phase="L3"] { stroke: #808080 !important; } +.kundenkarte-endpoint[data-phase="N"] { stroke: #0066cc !important; } +.kundenkarte-endpoint[data-phase="PE"] { stroke: #27ae60 !important; } + +/* Busbar preset buttons */ +.busbar-preset-btn { + transition: transform 0.15s ease, box-shadow 0.15s ease !important; +} + +.busbar-preset-btn:hover { + transform: scale(1.05) !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important; +} + +.busbar-preset-btn:active { + transform: scale(0.98) !important; +} + +/* Connection editor scrollbar */ +.kundenkarte-connection-editor::-webkit-scrollbar { + height: 8px !important; +} + +.kundenkarte-connection-editor::-webkit-scrollbar-track { + background: #1a1a1a !important; + border-radius: 4px !important; +} + +.kundenkarte-connection-editor::-webkit-scrollbar-thumb { + background: #444 !important; + border-radius: 4px !important; +} + +.kundenkarte-connection-editor::-webkit-scrollbar-thumb:hover { + background: #555 !important; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .kundenkarte-connection-editor-header { + flex-direction: column !important; + gap: 10px !important; + align-items: flex-start !important; + } + + .kundenkarte-connection-editor-actions { + flex-wrap: wrap !important; + } + + .kundenkarte-connection-editor-actions button { + flex: 1 !important; + justify-content: center !important; + } +} + +/* ======================================== + SCHEMATIC EDITOR v2 (Interactive SVG) + ======================================== */ + +.schematic-editor-wrapper { + margin-top: 20px !important; +} + +.schematic-editor-header { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + padding: 10px 15px !important; + background: #252525 !important; + border: 1px solid #333 !important; + border-radius: 4px 4px 0 0 !important; +} + +.schematic-editor-toggle { + color: #3498db !important; + text-decoration: none !important; + transition: color 0.2s ease !important; +} + +.schematic-editor-toggle:hover { + color: #5dade2 !important; +} + +.schematic-editor-toggle i { + transition: transform 0.3s ease !important; + margin-right: 8px !important; +} + +.schematic-editor-canvas { + display: none !important; + background: #1a1a1a !important; + border: 1px solid #333 !important; + border-top: none !important; + border-radius: 0 0 4px 4px !important; + padding: 15px !important; + overflow: auto !important; + min-height: 300px !important; +} + +.schematic-editor-canvas.expanded { + display: block !important; +} + +/* SVG Canvas */ +.schematic-svg { + background: #1e1e1e !important; + border-radius: 4px !important; +} + +/* Rails (Hutschienen) */ +.schematic-rail { + cursor: default !important; +} + +.schematic-rail-bg { + opacity: 0.8 !important; +} + +/* Equipment Blocks */ +.schematic-block { + cursor: grab !important; + transition: filter 0.2s ease !important; +} + +.schematic-block:hover { + filter: brightness(1.1) !important; +} + +.schematic-block.dragging { + cursor: grabbing !important; + filter: brightness(1.2) drop-shadow(0 0 8px rgba(52, 152, 219, 0.6)) !important; +} + +.schematic-block-bg { + transition: filter 0.2s ease !important; +} + +/* Terminals */ +.schematic-terminal { + cursor: crosshair !important; +} + +.schematic-terminal-circle { + transition: all 0.2s ease !important; +} + +.schematic-terminal:hover .schematic-terminal-circle { + r: 8 !important; + filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.5)) !important; +} + +/* Connections */ +.schematic-connection { + cursor: pointer !important; + transition: stroke-width 0.2s ease !important; +} + +.schematic-connection:hover { + stroke-width: 4 !important; + filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.4)) !important; +} + +.schematic-connection-shadow { + pointer-events: none !important; +} + +.schematic-connection-group:hover .schematic-connection { + stroke-width: 4 !important; +} + +.schematic-connection-group:hover .schematic-connection-shadow { + stroke-width: 7 !important; +} + +/* Connection Preview */ +.schematic-connection-preview { + pointer-events: none !important; + animation: dashOffset 0.5s linear infinite !important; +} + +/* Messages */ +.schematic-message { + padding: 8px 15px !important; + margin-bottom: 10px !important; + border-radius: 4px !important; + font-size: 12px !important; +} + +.schematic-message.info { + background: #2d4a5e !important; + color: #7ec8e3 !important; + border: 1px solid #3498db !important; +} + +.schematic-message.success { + background: #2d5a3d !important; + color: #7ee8a0 !important; + border: 1px solid #27ae60 !important; +} + +.schematic-message.warning { + background: #5a5a2d !important; + color: #e8e87e !important; + border: 1px solid #f1c40f !important; +} + +.schematic-message.error { + background: #5a2d2d !important; + color: #e87e7e !important; + border: 1px solid #e74c3c !important; +} + +/* Terminal color by phase */ +.schematic-terminal[data-phase="L1"] .schematic-terminal-circle, +.schematic-terminal-circle[data-phase="L1"] { + fill: #8B4513 !important; +} + +.schematic-terminal[data-phase="L2"] .schematic-terminal-circle, +.schematic-terminal-circle[data-phase="L2"] { + fill: #1a1a1a !important; + stroke: #666 !important; +} + +.schematic-terminal[data-phase="L3"] .schematic-terminal-circle, +.schematic-terminal-circle[data-phase="L3"] { + fill: #666666 !important; +} + +.schematic-terminal[data-phase="N"] .schematic-terminal-circle, +.schematic-terminal-circle[data-phase="N"] { + fill: #0066cc !important; +} + +.schematic-terminal[data-phase="PE"] .schematic-terminal-circle, +.schematic-terminal-circle[data-phase="PE"] { + fill: #27ae60 !important; +} + +/* Scrollbar for canvas */ +.schematic-editor-canvas::-webkit-scrollbar { + width: 10px !important; + height: 10px !important; +} + +.schematic-editor-canvas::-webkit-scrollbar-track { + background: #1a1a1a !important; + border-radius: 5px !important; +} + +.schematic-editor-canvas::-webkit-scrollbar-thumb { + background: #444 !important; + border-radius: 5px !important; +} + +.schematic-editor-canvas::-webkit-scrollbar-thumb:hover { + background: #555 !important; +} diff --git a/js/kundenkarte.js b/js/kundenkarte.js index 3deb50b..1f8a41f 100755 --- a/js/kundenkarte.js +++ b/js/kundenkarte.js @@ -1044,6 +1044,4324 @@ } }; + /** + * Equipment Component + * Manages Hutschienen (DIN rail) components with SVG visualization + */ + KundenKarte.Equipment = { + TE_WIDTH: 50, // Width of one TE in pixels (wider for more space) + BLOCK_HEIGHT: 110, // Height of equipment block in pixels + currentCarrierId: null, + currentAnlageId: null, + currentSystemId: null, + draggedEquipment: null, + isSaving: false, // Prevent double-clicks + + init: function(anlageId, systemId) { + if (!anlageId) return; + this.currentAnlageId = anlageId; + this.currentSystemId = systemId || 0; + this.bindEvents(); + }, + + bindEvents: function() { + var self = this; + + // Add panel button + $(document).on('click', '.kundenkarte-add-panel', function(e) { + e.preventDefault(); + var anlageId = $(this).data('anlage-id'); + self.showPanelDialog(anlageId); + }); + + // Edit panel + $(document).on('click', '.kundenkarte-panel-edit', function(e) { + e.preventDefault(); + e.stopPropagation(); + var panelId = $(this).closest('.kundenkarte-panel').data('panel-id'); + self.showPanelDialog(self.currentAnlageId, panelId); + }); + + // Delete panel + $(document).on('click', '.kundenkarte-panel-delete', function(e) { + e.preventDefault(); + e.stopPropagation(); + var panelId = $(this).closest('.kundenkarte-panel').data('panel-id'); + self.deletePanel(panelId); + }); + + // Quick-duplicate panel (+ button next to last panel) + $(document).on('click', '.kundenkarte-panel-quickadd', function(e) { + e.preventDefault(); + e.stopPropagation(); + var panelId = $(this).data('panel-id'); + self.duplicatePanel(panelId); + }); + + // Quick-duplicate carrier (+ button below last carrier) + $(document).on('click', '.kundenkarte-carrier-quickadd', function(e) { + e.preventDefault(); + e.stopPropagation(); + var carrierId = $(this).data('carrier-id'); + self.duplicateCarrier(carrierId); + }); + + // Add carrier button + $(document).on('click', '.kundenkarte-add-carrier', function(e) { + e.preventDefault(); + var anlageId = $(this).data('anlage-id'); + var panelId = $(this).data('panel-id') || 0; + self.showCarrierDialog(anlageId, null, panelId); + }); + + // Edit carrier + $(document).on('click', '.kundenkarte-carrier-edit', function(e) { + e.preventDefault(); + e.stopPropagation(); + var carrierId = $(this).closest('.kundenkarte-carrier').data('carrier-id'); + self.showCarrierDialog(self.currentAnlageId, carrierId); + }); + + // Delete carrier + $(document).on('click', '.kundenkarte-carrier-delete', function(e) { + e.preventDefault(); + e.stopPropagation(); + var carrierId = $(this).closest('.kundenkarte-carrier').data('carrier-id'); + self.deleteCarrier(carrierId); + }); + + // Add equipment (click on empty slot or + button) + $(document).on('click', '.kundenkarte-carrier-add-equipment, .kundenkarte-slot-empty', function(e) { + e.preventDefault(); + var $carrier = $(this).closest('.kundenkarte-carrier'); + var carrierId = $carrier.data('carrier-id'); + var position = $(this).data('position') || null; + self.showEquipmentDialog(carrierId, null, position); + }); + + // Edit equipment (click on block) + $(document).on('click', '.kundenkarte-equipment-block', function(e) { + e.preventDefault(); + e.stopPropagation(); + var equipmentId = $(this).data('equipment-id'); + self.showEquipmentDialog(null, equipmentId); + }); + + // Quick-add slot (duplicate last equipment into next free position) + $(document).on('click', '.kundenkarte-slot-quickadd', function(e) { + e.preventDefault(); + e.stopPropagation(); + var equipmentId = $(this).data('equipment-id'); + self.duplicateEquipment(equipmentId); + }); + + // Delete equipment + $(document).on('click', '.kundenkarte-equipment-delete', function(e) { + e.preventDefault(); + e.stopPropagation(); + var equipmentId = $(this).closest('.kundenkarte-equipment-block').data('equipment-id'); + self.deleteEquipment(equipmentId); + }); + + // Add output connection + $(document).on('click', '.kundenkarte-add-output-btn', function(e) { + e.preventDefault(); + var carrierId = $(this).data('carrier-id'); + self.showOutputDialog(carrierId); + }); + + // Add rail connection + $(document).on('click', '.kundenkarte-add-rail-btn', function(e) { + e.preventDefault(); + var carrierId = $(this).data('carrier-id'); + self.showRailDialog(carrierId); + }); + + // Click on rail to edit + $(document).on('click', '.kundenkarte-rail', function(e) { + e.preventDefault(); + e.stopPropagation(); + var connectionId = $(this).data('connection-id'); + // Find carrier ID from connections container or carrier element + var carrierId = $(this).closest('.kundenkarte-connections-container').data('carrier-id') || + $(this).closest('.kundenkarte-carrier').data('carrier-id'); + self.showEditRailDialog(connectionId, carrierId); + }); + + // Click on output to edit + $(document).on('click', '.kundenkarte-output', function(e) { + e.preventDefault(); + e.stopPropagation(); + var connectionId = $(this).data('connection-id'); + // Find carrier ID from connections container or carrier element + var carrierId = $(this).closest('.kundenkarte-connections-container').data('carrier-id') || + $(this).closest('.kundenkarte-carrier').data('carrier-id'); + self.showEditOutputDialog(connectionId, carrierId); + }); + + // Click on connection to delete (generic connections) + $(document).on('click', '.kundenkarte-connection', function(e) { + e.preventDefault(); + var connectionId = $(this).data('connection-id'); + self.deleteConnection(connectionId); + }); + + // Drag & Drop events + $(document).on('dragstart', '.kundenkarte-equipment-block', function(e) { + self.draggedEquipment = $(this); + $(this).addClass('dragging'); + e.originalEvent.dataTransfer.effectAllowed = 'move'; + e.originalEvent.dataTransfer.setData('text/plain', $(this).data('equipment-id')); + }); + + $(document).on('dragend', '.kundenkarte-equipment-block', function() { + $(this).removeClass('dragging'); + self.draggedEquipment = null; + $('.kundenkarte-slot-drop-target').removeClass('kundenkarte-slot-drop-target'); + }); + + $(document).on('dragover', '.kundenkarte-slot-empty, .kundenkarte-carrier-slots', function(e) { + e.preventDefault(); + e.originalEvent.dataTransfer.dropEffect = 'move'; + }); + + $(document).on('dragenter', '.kundenkarte-slot-empty', function(e) { + e.preventDefault(); + $(this).addClass('kundenkarte-slot-drop-target'); + }); + + $(document).on('dragleave', '.kundenkarte-slot-empty', function() { + $(this).removeClass('kundenkarte-slot-drop-target'); + }); + + $(document).on('drop', '.kundenkarte-slot-empty', function(e) { + e.preventDefault(); + $(this).removeClass('kundenkarte-slot-drop-target'); + + if (self.draggedEquipment) { + var equipmentId = self.draggedEquipment.data('equipment-id'); + var newPosition = $(this).data('position'); + self.moveEquipment(equipmentId, newPosition); + } + }); + + // Equipment type change in dialog + $(document).on('change', '#equipment_type_id', function() { + self.loadEquipmentTypeFields($(this).val()); + }); + + // Hover tooltip for equipment + $(document).on('mouseenter', '.kundenkarte-equipment-block', function() { + var $block = $(this); + var tooltipData = $block.attr('data-tooltip'); + if (tooltipData) { + self.showEquipmentTooltip($block, JSON.parse(tooltipData)); + } + }); + + $(document).on('mouseleave', '.kundenkarte-equipment-block', function() { + self.hideEquipmentTooltip(); + }); + }, + + loadCarriers: function(anlageId) { + var self = this; + var $container = $('.kundenkarte-equipment-container[data-anlage-id="' + anlageId + '"]'); + + if (!$container.length) return; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', + data: { action: 'list', anlage_id: anlageId }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.renderCarriers($container, response.carriers); + } + } + }); + }, + + renderCarriers: function($container, carriers) { + var self = this; + var html = ''; + + if (carriers.length === 0) { + html = '
Keine Hutschienen vorhanden. Klicken Sie auf "Hutschiene hinzufügen".
'; + } else { + carriers.forEach(function(carrier) { + html += self.renderCarrier(carrier); + }); + } + + $container.find('.kundenkarte-carriers-list').html(html); + }, + + renderCarrier: function(carrier) { + var self = this; + var totalWidth = carrier.total_te * this.TE_WIDTH; + + var html = '
'; + + // Header + html += '
'; + html += '' + this.escapeHtml(carrier.label || 'Hutschiene') + ''; + html += '' + carrier.used_te + '/' + carrier.total_te + ' TE belegt'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += '
'; + + // SVG Rail + html += '
'; + html += ''; + + // Background grid (TE markers) + for (var i = 0; i <= carrier.total_te; i++) { + var x = i * this.TE_WIDTH; + html += ''; + } + + // Render equipment blocks + if (carrier.equipment) { + carrier.equipment.forEach(function(eq) { + html += self.renderEquipmentBlock(eq); + }); + } + + html += ''; + + // Clickable slots overlay (for adding equipment) + html += '
'; + var occupiedSlots = this.getOccupiedSlots(carrier.equipment); + for (var pos = 1; pos <= carrier.total_te; pos++) { + if (!occupiedSlots[pos]) { + var slotLeft = (pos - 1) * this.TE_WIDTH; + html += '
'; + } + } + html += '
'; + + html += '
'; // svg-container + html += '
'; // carrier + + return html; + }, + + renderEquipmentBlock: function(equipment) { + var x = (equipment.position_te - 1) * this.TE_WIDTH; + var width = equipment.width_te * this.TE_WIDTH; + var color = equipment.block_color || equipment.type_color || '#3498db'; + var label = equipment.block_label || equipment.type_label_short || ''; + + // Build tooltip data + var tooltipData = { + label: equipment.label, + type: equipment.type_label, + fields: equipment.field_values || {} + }; + + var html = ''; + + // Label text (centered) + var fontSize = width < 40 ? 11 : 14; + html += ''; + html += this.escapeHtml(label); + html += ''; + + // Duplicate button (+) at the right edge + html += ''; + + return html; + }, + + getOccupiedSlots: function(equipment) { + var slots = {}; + if (equipment) { + equipment.forEach(function(eq) { + for (var i = 0; i < eq.width_te; i++) { + slots[eq.position_te + i] = true; + } + }); + } + return slots; + }, + + // Panel dialog functions + showPanelDialog: function(anlageId, panelId) { + var self = this; + + if ($('#kundenkarte-panel-dialog').length) return; + + var isEdit = !!panelId; + var title = isEdit ? 'Feld bearbeiten' : 'Feld hinzufügen'; + + var panelData = { label: '' }; + + var showDialog = function(data) { + var html = '
'; + html += '
'; + html += '

' + title + '

'; + html += '×
'; + html += '
'; + html += ''; + html += ''; + html += ''; + html += '
Bezeichnung
'; + html += '
'; + html += ''; + html += '
'; + + $('body').append(html); + $('#kundenkarte-panel-dialog').addClass('visible'); + + $('#panel-save').on('click', function() { + var label = $('input[name="panel_label"]').val(); + self.savePanel(anlageId, panelId, label); + }); + + $('#panel-cancel, .kundenkarte-modal-close').on('click', function() { + $('#kundenkarte-panel-dialog').remove(); + }); + + $(document).on('keydown.panelDialog', function(e) { + if (e.key === 'Escape') { + $('#kundenkarte-panel-dialog').remove(); + $(document).off('keydown.panelDialog'); + } + }); + }; + + if (isEdit) { + var $panel = $('.kundenkarte-panel[data-panel-id="' + panelId + '"]'); + panelData.label = $panel.find('.kundenkarte-panel-label').text(); + showDialog(panelData); + } else { + showDialog(panelData); + } + }, + + savePanel: function(anlageId, panelId, label) { + var self = this; + + if (self.isSaving) return; + self.isSaving = true; + $('#panel-save').prop('disabled', true); + + var action = panelId ? 'update' : 'create'; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_panel.php', + method: 'POST', + data: { + action: action, + anlage_id: anlageId, + panel_id: panelId, + label: label, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + self.isSaving = false; + if (response.success) { + $('#kundenkarte-panel-dialog').remove(); + location.reload(); + } else { + $('#panel-save').prop('disabled', false); + alert('Fehler: ' + response.error); + } + }, + error: function() { + self.isSaving = false; + $('#panel-save').prop('disabled', false); + alert('Netzwerkfehler'); + } + }); + }, + + deletePanel: function(panelId) { + var self = this; + self.showConfirmDialog('Feld löschen', 'Möchten Sie dieses Feld wirklich löschen? Alle Hutschienen und Equipment werden ebenfalls gelöscht.', function() { + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_panel.php', + method: 'POST', + data: { + action: 'delete', + panel_id: panelId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + location.reload(); + } else { + self.showAlertDialog('Fehler', response.error); + } + }, + error: function() { + self.showAlertDialog('Fehler', 'Netzwerkfehler'); + } + }); + }); + }, + + duplicatePanel: function(panelId) { + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_panel.php', + method: 'POST', + data: { + action: 'duplicate', + panel_id: panelId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + location.reload(); + } else { + alert('Fehler: ' + response.error); + } + }, + error: function() { + alert('Netzwerkfehler'); + } + }); + }, + + duplicateCarrier: function(carrierId) { + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', + method: 'POST', + data: { + action: 'duplicate', + carrier_id: carrierId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + location.reload(); + } else { + alert('Fehler: ' + response.error); + } + }, + error: function() { + alert('Netzwerkfehler'); + } + }); + }, + + showCarrierDialog: function(anlageId, carrierId, panelId) { + var self = this; + + // Prevent opening multiple dialogs + if ($('#kundenkarte-carrier-dialog').length) return; + + var isEdit = !!carrierId; + var title = isEdit ? 'Hutschiene bearbeiten' : 'Hutschiene hinzufügen'; + panelId = panelId || 0; + + // Load carrier data if editing + var carrierData = { label: '', total_te: 12, fk_panel: panelId }; + + var showDialog = function(data, panels) { + var html = '
'; + html += '
'; + html += '

' + title + '

'; + html += '×
'; + html += '
'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + + // Panel dropdown (only show if panels exist) + if (panels && panels.length > 0) { + html += ''; + html += ''; + } + + html += '
Bezeichnung
Kapazität (TE)
Feld
'; + html += '
'; + html += ''; + html += '
'; + + $('body').append(html); + $('#kundenkarte-carrier-dialog').addClass('visible'); + + // Save button + $('#carrier-save').on('click', function() { + var label = $('input[name="carrier_label"]').val(); + var totalTe = parseInt($('input[name="carrier_total_te"]').val()) || 12; + var selectedPanelId = $('select[name="carrier_panel_id"]').val() || 0; + + self.saveCarrier(anlageId, carrierId, label, totalTe, selectedPanelId); + }); + + // Close + $('#carrier-cancel, .kundenkarte-modal-close').on('click', function() { + $('#kundenkarte-carrier-dialog').remove(); + }); + + $(document).on('keydown.carrierDialog', function(e) { + if (e.key === 'Escape') { + $('#kundenkarte-carrier-dialog').remove(); + $(document).off('keydown.carrierDialog'); + } + }); + }; + + // Load panels for the anlage + var loadPanelsAndShow = function() { + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_panel.php', + data: { action: 'list', anlage_id: self.currentAnlageId }, + dataType: 'json', + success: function(response) { + var panels = response.success ? response.panels : []; + showDialog(carrierData, panels); + }, + error: function() { + showDialog(carrierData, []); + } + }); + }; + + if (isEdit) { + // Fetch existing carrier data via AJAX + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', + data: { action: 'get', carrier_id: carrierId }, + dataType: 'json', + success: function(response) { + if (response.success && response.carrier) { + carrierData.label = response.carrier.label; + carrierData.total_te = response.carrier.total_te; + carrierData.fk_panel = response.carrier.fk_panel || 0; + } + loadPanelsAndShow(); + }, + error: function() { + // Fallback to DOM data + var $carrier = $('.kundenkarte-carrier[data-carrier-id="' + carrierId + '"]'); + carrierData.label = $carrier.find('.kundenkarte-carrier-label').text(); + var infoText = $carrier.find('.kundenkarte-carrier-info').text(); + var match = infoText.match(/\/(\d+)/); + if (match) carrierData.total_te = parseInt(match[1]); + loadPanelsAndShow(); + } + }); + } else { + carrierData.fk_panel = panelId; + loadPanelsAndShow(); + } + }, + + saveCarrier: function(anlageId, carrierId, label, totalTe, panelId) { + var self = this; + + // Prevent double-click + if (self.isSaving) return; + self.isSaving = true; + $('#carrier-save').prop('disabled', true); + + var action = carrierId ? 'update' : 'create'; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', + method: 'POST', + data: { + action: action, + anlage_id: anlageId, + carrier_id: carrierId, + panel_id: panelId || 0, + label: label, + total_te: totalTe, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + self.isSaving = false; + if (response.success) { + $('#kundenkarte-carrier-dialog').remove(); + // Reload page to get fresh PHP-rendered carriers + location.reload(); + } else { + $('#carrier-save').prop('disabled', false); + alert('Fehler: ' + response.error); + } + }, + error: function() { + self.isSaving = false; + $('#carrier-save').prop('disabled', false); + alert('Netzwerkfehler'); + } + }); + }, + + deleteCarrier: function(carrierId) { + var self = this; + + self.showConfirmDialog('Hutschiene löschen', 'Möchten Sie diese Hutschiene wirklich löschen? Alle Equipment darauf wird ebenfalls gelöscht.', function() { + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', + method: 'POST', + data: { + action: 'delete', + carrier_id: carrierId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + location.reload(); + } else { + self.showAlertDialog('Fehler', response.error); + } + } + }); + }); + }, + + showEquipmentDialog: function(carrierId, equipmentId, position) { + var self = this; + + // Prevent opening multiple dialogs + if ($('#kundenkarte-equipment-dialog').length) return; + + var isEdit = !!equipmentId; + var title = isEdit ? 'Equipment bearbeiten' : 'Equipment hinzufügen'; + + // First, load equipment types + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_type_fields.php', + data: { type_id: 0 }, // Get types list + dataType: 'json', + success: function() { + self.renderEquipmentDialog(carrierId, equipmentId, position, title, isEdit); + } + }); + }, + + renderEquipmentDialog: function(carrierId, equipmentId, position, title, isEdit) { + var self = this; + + var html = '
'; + html += '
'; + html += '

' + title + '

'; + html += '×
'; + html += '
'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += '
Typ *
Bezeichnung
'; + html += ' FI/RCD-Zuordnung
Schutzeinrichtung
Schutzbezeichnung
'; + html += '
'; + html += ''; + html += '
'; + + $('body').append(html); + $('#kundenkarte-equipment-dialog').addClass('visible'); + + // Load equipment types into select + self.loadEquipmentTypes(equipmentId); + + // Save button + $('#equipment-save').on('click', function() { + self.saveEquipment(); + }); + + // Delete button + $('#equipment-delete').on('click', function() { + $('#kundenkarte-equipment-dialog').remove(); + self.showConfirmDialog('Equipment löschen', 'Möchten Sie dieses Equipment wirklich löschen?', function() { + self.deleteEquipment(equipmentId); + }); + }); + + // Close + $('#equipment-cancel, .kundenkarte-modal-close').on('click', function() { + $('#kundenkarte-equipment-dialog').remove(); + }); + + $(document).on('keydown.equipmentDialog', function(e) { + if (e.key === 'Escape') { + $('#kundenkarte-equipment-dialog').remove(); + $(document).off('keydown.equipmentDialog'); + } + }); + }, + + loadEquipmentTypes: function(equipmentId) { + var self = this; + + // Get equipment types from admin config (stored in page data or via AJAX) + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + data: { action: 'get_types', system_id: self.currentSystemId }, + dataType: 'json', + success: function(response) { + if (response.types) { + var $select = $('#equipment_type_id'); + // Clear existing options except first placeholder + $select.find('option:not(:first)').remove(); + response.types.forEach(function(type) { + $select.append(''); + }); + + // Load protection devices + self.loadProtectionDevices(); + + // If editing, load equipment data + if (equipmentId) { + self.loadEquipmentData(equipmentId); + } + } + } + }); + }, + + loadProtectionDevices: function() { + var self = this; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + data: { action: 'get_protection_devices', anlage_id: self.currentAnlageId }, + dataType: 'json', + success: function(response) { + if (response.success && response.devices) { + var $select = $('#equipment_fk_protection'); + $select.find('option:not(:first)').remove(); + response.devices.forEach(function(device) { + $select.append(''); + }); + } + } + }); + }, + + loadEquipmentData: function(equipmentId) { + var self = this; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + data: { action: 'get', equipment_id: equipmentId }, + dataType: 'json', + success: function(response) { + if (response.equipment) { + var eq = response.equipment; + $('input[name="equipment_carrier_id"]').val(eq.fk_carrier); + $('input[name="equipment_label"]').val(eq.label); + $('input[name="equipment_position"]').val(eq.position_te); + $('#equipment_type_id').val(eq.type_id).trigger('change'); + // Protection fields + if (eq.fk_protection) { + $('#equipment_fk_protection').val(eq.fk_protection); + } + if (eq.protection_label) { + $('input[name="equipment_protection_label"]').val(eq.protection_label); + } + } + } + }); + }, + + loadEquipmentTypeFields: function(typeId) { + var self = this; + var $container = $('#equipment-dynamic-fields'); + var equipmentId = $('input[name="equipment_id"]').val(); + + if (!typeId) { + $container.html(''); + return; + } + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_type_fields.php', + data: { type_id: typeId, equipment_id: equipmentId }, + dataType: 'json', + success: function(response) { + if (response.success && response.fields) { + var html = ''; + response.fields.forEach(function(field) { + html += '' + self.escapeHtml(field.label); + if (field.required) html += ' *'; + html += ''; + html += self.renderEquipmentField(field); + html += ''; + }); + $container.html(html); + } else { + $container.html(''); + } + } + }); + }, + + renderEquipmentField: function(field) { + var name = 'eq_field_' + field.code; + var value = field.value || ''; + var required = field.required ? ' required' : ''; + + switch (field.type) { + case 'select': + var html = ''; + return html; + + case 'number': + return ''; + + default: + return ''; + } + }, + + saveEquipment: function() { + var self = this; + + // Prevent double-click + if (self.isSaving) return; + + var carrierId = $('input[name="equipment_carrier_id"]').val(); + var equipmentId = $('input[name="equipment_id"]').val(); + var typeId = $('#equipment_type_id').val(); + var position = $('input[name="equipment_position"]').val(); + var label = $('input[name="equipment_label"]').val(); + var fkProtection = $('#equipment_fk_protection').val(); + var protectionLabel = $('input[name="equipment_protection_label"]').val(); + + if (!typeId) { + alert('Bitte wählen Sie einen Typ.'); + return; + } + + self.isSaving = true; + $('#equipment-save').prop('disabled', true); + + // Collect field values + var fieldValues = {}; + $('#equipment-dynamic-fields input, #equipment-dynamic-fields select').each(function() { + var name = $(this).attr('name'); + if (name && name.startsWith('eq_field_')) { + var code = name.replace('eq_field_', ''); + fieldValues[code] = $(this).val(); + } + }); + + var action = equipmentId ? 'update' : 'create'; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + method: 'POST', + data: { + action: action, + carrier_id: carrierId, + equipment_id: equipmentId, + type_id: typeId, + label: label, + position_te: position, + fk_protection: fkProtection, + protection_label: protectionLabel, + field_values: JSON.stringify(fieldValues), + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + self.isSaving = false; + if (response.success) { + $('#kundenkarte-equipment-dialog').remove(); + // Reload page to get fresh data + location.reload(); + } else { + $('#equipment-save').prop('disabled', false); + alert('Fehler: ' + response.error); + } + }, + error: function() { + self.isSaving = false; + $('#equipment-save').prop('disabled', false); + alert('Netzwerkfehler'); + } + }); + }, + + duplicateEquipment: function(equipmentId) { + var self = this; + + if (self.isSaving) return; + self.isSaving = true; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + method: 'POST', + data: { + action: 'duplicate', + equipment_id: equipmentId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + self.isSaving = false; + if (response.success) { + location.reload(); + } else { + alert('Fehler: ' + response.error); + } + }, + error: function() { + self.isSaving = false; + alert('Netzwerkfehler'); + } + }); + }, + + deleteEquipment: function(equipmentId) { + var self = this; + + // No confirm here - handled by caller + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + method: 'POST', + data: { + action: 'delete', + equipment_id: equipmentId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + location.reload(); + } else { + self.showAlertDialog('Fehler', response.error); + } + } + }); + }, + + moveEquipment: function(equipmentId, newPosition) { + var self = this; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + method: 'POST', + data: { + action: 'move', + equipment_id: equipmentId, + position_te: newPosition, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + location.reload(); + } else { + alert('Fehler: ' + response.error); + } + } + }); + }, + + showEquipmentTooltip: function($block, data) { + var self = this; + + var html = '
'; + html += '' + this.escapeHtml(data.type || '') + ''; + if (data.label) { + html += '
' + this.escapeHtml(data.label) + ''; + } + html += '
'; + + if (data.fields && Object.keys(data.fields).length > 0) { + html += '
'; + for (var key in data.fields) { + if (data.fields[key]) { + html += '
' + this.escapeHtml(key) + ': '; + html += '' + this.escapeHtml(data.fields[key]) + '
'; + } + } + html += '
'; + } + + var $tooltip = $('#kundenkarte-equipment-tooltip'); + if (!$tooltip.length) { + $tooltip = $('
'); + $('body').append($tooltip); + } + + $tooltip.html(html); + + // Position near the block + var offset = $block.offset ? $block.offset() : $(this).offset(); + var rect = $block[0].getBoundingClientRect ? $block[0].getBoundingClientRect() : { right: 0, top: 0 }; + + $tooltip.css({ + top: rect.top + window.scrollY + 10, + left: rect.right + window.scrollX + 10 + }).addClass('visible'); + }, + + hideEquipmentTooltip: function() { + $('#kundenkarte-equipment-tooltip').removeClass('visible'); + }, + + // ========================================== + // CONNECTION METHODS (Stromverbindungen) + // ========================================== + + showOutputDialog: function(carrierId) { + var self = this; + + if ($('#kundenkarte-output-dialog').length) return; + + // Get equipment on this carrier for dropdown + var $carrier = $('.kundenkarte-carrier[data-carrier-id="' + carrierId + '"]'); + var equipmentOptions = []; + + $carrier.find('.kundenkarte-equipment-block').each(function() { + var $block = $(this); + var tooltipData = $block.data('tooltip'); + var id = $block.data('equipment-id'); + var label = tooltipData ? (tooltipData.label || tooltipData.type || 'Equipment ' + id) : 'Equipment ' + id; + equipmentOptions.push({ id: id, label: label }); + }); + + var html = '
'; + html += '
'; + html += '

Abgang hinzufügen

'; + html += '×
'; + html += '
'; + html += '
'; + + // Equipment selection + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Phase selection + html += '
'; + html += '
'; + html += ''; + html += '
'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += '
'; + html += ''; + html += '
'; + html += '
'; + + // Consumer label + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Cable info + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + html += '
'; // form + html += '
'; // body + html += ''; + html += '
'; + + $('body').append(html); + $('#kundenkarte-output-dialog').addClass('visible'); + + // Phase button handlers + $('#kundenkarte-output-dialog .kundenkarte-phase-btn').on('click', function() { + $('#kundenkarte-output-dialog .kundenkarte-phase-btn').removeClass('active'); + $(this).addClass('active'); + $('input[name="output_connection_type"]').val($(this).data('phase')); + }); + + // Save + $('#output-save').on('click', function() { + self.saveOutput(carrierId); + }); + + // Close + $('#output-cancel, .kundenkarte-modal-close').on('click', function() { + $('#kundenkarte-output-dialog').remove(); + }); + }, + + saveOutput: function(carrierId) { + var self = this; + + var data = { + action: 'create_output', + carrier_id: carrierId, + equipment_id: $('select[name="output_equipment_id"]').val(), + connection_type: $('input[name="output_connection_type"]').val(), + output_label: $('input[name="output_consumer_label"]').val(), + medium_type: $('select[name="output_cable_type"]').val(), + medium_spec: $('select[name="output_cable_section"]').val(), + token: $('input[name="token"]').val() + }; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: data, + dataType: 'json', + success: function(response) { + if (response.success) { + $('#kundenkarte-output-dialog').remove(); + location.reload(); + } else { + alert('Fehler: ' + response.error); + } + }, + error: function() { + alert('Netzwerkfehler'); + } + }); + }, + + showRailDialog: function(carrierId) { + var self = this; + + if ($('#kundenkarte-rail-dialog').length) return; + + // Get carrier info + var $carrier = $('.kundenkarte-carrier[data-carrier-id="' + carrierId + '"]'); + var totalTE = 12; + var infoText = $carrier.find('.kundenkarte-carrier-info').text(); + var match = infoText.match(/\/(\d+)/); + if (match) totalTE = parseInt(match[1]); + + var html = '
'; + html += '
'; + html += '

Sammelschiene hinzufügen

'; + html += '×
'; + html += '
'; + html += '
'; + + // Quick presets row + html += '
'; + html += ''; + html += '
'; + // Electrical presets + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += '|'; + // Network presets + html += ''; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Connection type (free text) + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Position (above/below) + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // TE range + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Multi-phase rail options + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Excluded positions (for FI switches etc.) + html += '
'; + html += '
'; + html += ''; + html += ''; + html += 'An diesen Positionen wird die Schiene unterbrochen'; + html += '
'; + html += '
'; + + html += '
'; // form + html += '
'; // body + html += ''; + html += '
'; + + $('body').append(html); + $('#kundenkarte-rail-dialog').addClass('visible'); + + // Preset button handlers + $('#kundenkarte-rail-dialog .rail-preset-btn').on('click', function() { + var type = $(this).data('type'); + $('input[name="rail_connection_type"]').val(type); + $('input[name="rail_color"]').val($(this).data('color')); + + // Auto-set rail_phases for electrical presets + var phasesSelect = $('select[name="rail_phases"]'); + if (type === 'L1') phasesSelect.val('L1'); + else if (type === 'L1N') phasesSelect.val('L1N'); + else if (type === '3P') phasesSelect.val('3P'); + else if (type === '3P+N') phasesSelect.val('3P+N'); + else phasesSelect.val(''); // Simple line for N, PE, network cables + }); + + // Save + $('#rail-save').on('click', function() { + self.saveRail(carrierId); + }); + + // Close + $('#rail-cancel, .kundenkarte-modal-close').on('click', function() { + $('#kundenkarte-rail-dialog').remove(); + }); + }, + + saveRail: function(carrierId) { + var self = this; + + // position_y: -1 = above equipment, 0+ = below equipment + var positionY = $('select[name="rail_position"]').val() === 'above' ? -1 : 0; + + var data = { + action: 'create_rail', + carrier_id: carrierId, + connection_type: $('input[name="rail_connection_type"]').val(), + color: $('input[name="rail_color"]').val(), + rail_start_te: $('input[name="rail_start_te"]').val(), + rail_end_te: $('input[name="rail_end_te"]').val(), + rail_phases: $('select[name="rail_phases"]').val(), + excluded_te: $('input[name="rail_excluded_te"]').val(), + position_y: positionY, + token: $('input[name="token"]').val() + }; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: data, + dataType: 'json', + success: function(response) { + if (response.success) { + $('#kundenkarte-rail-dialog').remove(); + location.reload(); + } else { + alert('Fehler: ' + response.error); + } + }, + error: function() { + alert('Netzwerkfehler'); + } + }); + }, + + showEditRailDialog: function(connectionId, carrierId) { + var self = this; + + if ($('#kundenkarte-rail-dialog').length) return; + + // Load connection data first + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + data: { action: 'get', connection_id: connectionId }, + dataType: 'json', + success: function(response) { + if (response.success && response.connection) { + self.renderEditRailDialog(connectionId, carrierId, response.connection); + } else { + alert('Fehler: Verbindung nicht gefunden'); + } + }, + error: function() { + alert('Netzwerkfehler'); + } + }); + }, + + renderEditRailDialog: function(connectionId, carrierId, conn) { + var self = this; + + // Get carrier info + var $carrier = $('.kundenkarte-carrier[data-carrier-id="' + carrierId + '"]'); + var totalTE = 12; + var infoText = $carrier.find('.kundenkarte-carrier-info').text(); + var match = infoText.match(/\/(\d+)/); + if (match) totalTE = parseInt(match[1]); + + var isAbove = conn.position_y < 0; + + var html = '
'; + html += '
'; + html += '

Sammelschiene bearbeiten

'; + html += '×
'; + html += '
'; + html += ''; + html += '
'; + + // Quick presets row + html += '
'; + html += ''; + html += '
'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += '|'; + html += ''; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Connection type (free text) + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Position (above/below) + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // TE range + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Multi-phase rail options + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Excluded positions (for FI switches etc.) + html += '
'; + html += '
'; + html += ''; + html += ''; + html += 'An diesen Positionen wird die Schiene unterbrochen'; + html += '
'; + html += '
'; + + html += '
'; // form + html += '
'; // body + html += ''; + html += '
'; + + $('body').append(html); + $('#kundenkarte-rail-dialog').addClass('visible'); + + // Preset button handlers + $('#kundenkarte-rail-dialog .rail-preset-btn').on('click', function() { + var type = $(this).data('type'); + $('input[name="rail_connection_type"]').val(type); + $('input[name="rail_color"]').val($(this).data('color')); + + // Auto-set rail_phases for electrical presets + var phasesSelect = $('select[name="rail_phases"]'); + if (type === 'L1') phasesSelect.val('L1'); + else if (type === 'L1N') phasesSelect.val('L1N'); + else if (type === '3P') phasesSelect.val('3P'); + else if (type === '3P+N') phasesSelect.val('3P+N'); + else phasesSelect.val(''); // Simple line for N, PE, network cables + }); + + // Save + $('#rail-save').on('click', function() { + self.updateRail(connectionId, carrierId); + }); + + // Delete + $('#rail-delete').on('click', function() { + $('#kundenkarte-rail-dialog').remove(); + self.showConfirmDialog('Sammelschiene löschen', 'Möchten Sie diese Sammelschiene wirklich löschen?', function() { + self.doDeleteConnection(connectionId); + }); + }); + + // Close + $('#rail-cancel, .kundenkarte-modal-close').on('click', function() { + $('#kundenkarte-rail-dialog').remove(); + }); + }, + + updateRail: function(connectionId, carrierId) { + var self = this; + + var positionY = $('select[name="rail_position"]').val() === 'above' ? -1 : 0; + + var data = { + action: 'update', + connection_id: connectionId, + carrier_id: carrierId, + connection_type: $('input[name="rail_connection_type"]').val(), + color: $('input[name="rail_color"]').val(), + rail_start_te: $('input[name="rail_start_te"]').val(), + rail_end_te: $('input[name="rail_end_te"]').val(), + rail_phases: $('select[name="rail_phases"]').val(), + excluded_te: $('input[name="rail_excluded_te"]').val(), + position_y: positionY, + is_rail: 1, + token: $('input[name="token"]').val() + }; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: data, + dataType: 'json', + success: function(response) { + if (response.success) { + $('#kundenkarte-rail-dialog').remove(); + location.reload(); + } else { + alert('Fehler: ' + response.error); + } + }, + error: function() { + alert('Netzwerkfehler'); + } + }); + }, + + showEditOutputDialog: function(connectionId, carrierId) { + var self = this; + + if ($('#kundenkarte-output-dialog').length) return; + + // Load connection data first + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + data: { action: 'get', connection_id: connectionId }, + dataType: 'json', + success: function(response) { + if (response.success && response.connection) { + self.renderEditOutputDialog(connectionId, carrierId, response.connection); + } else { + alert('Fehler: Verbindung nicht gefunden'); + } + }, + error: function() { + alert('Netzwerkfehler'); + } + }); + }, + + renderEditOutputDialog: function(connectionId, carrierId, conn) { + var self = this; + + var html = '
'; + html += '
'; + html += '

Abgang bearbeiten

'; + html += '×
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + + // Quick presets + html += '
'; + html += ''; + html += '
'; + html += ''; + html += ''; + html += ''; + html += '|'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Output label (consumer) + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Connection type and color + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Medium info + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + // Length + html += '
'; + html += '
'; + html += ''; + html += ''; + html += '
'; + html += '
'; + + html += '
'; // form + html += '
'; // body + html += ''; + html += '
'; + + $('body').append(html); + $('#kundenkarte-output-dialog').addClass('visible'); + + // Preset button handlers + $('#kundenkarte-output-dialog .output-preset-btn').on('click', function() { + $('input[name="output_connection_type"]').val($(this).data('type')); + $('input[name="output_color"]').val($(this).data('color')); + }); + + // Save + $('#output-save').on('click', function() { + self.updateOutput(connectionId, carrierId); + }); + + // Delete + $('#output-delete').on('click', function() { + $('#kundenkarte-output-dialog').remove(); + self.showConfirmDialog('Abgang löschen', 'Möchten Sie diesen Abgang wirklich löschen?', function() { + self.doDeleteConnection(connectionId); + }); + }); + + // Close + $('#output-cancel, .kundenkarte-modal-close').on('click', function() { + $('#kundenkarte-output-dialog').remove(); + }); + }, + + updateOutput: function(connectionId, carrierId) { + var self = this; + + var data = { + action: 'update', + connection_id: connectionId, + carrier_id: carrierId, + fk_source: $('input[name="output_fk_source"]').val(), + source_terminal: 'output', + connection_type: $('input[name="output_connection_type"]').val(), + color: $('input[name="output_color"]').val(), + output_label: $('input[name="output_label"]').val(), + medium_type: $('input[name="output_medium_type"]').val(), + medium_spec: $('input[name="output_medium_spec"]').val(), + medium_length: $('input[name="output_medium_length"]').val(), + is_rail: 0, + token: $('input[name="token"]').val() + }; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: data, + dataType: 'json', + success: function(response) { + if (response.success) { + $('#kundenkarte-output-dialog').remove(); + location.reload(); + } else { + alert('Fehler: ' + response.error); + } + }, + error: function() { + alert('Netzwerkfehler'); + } + }); + }, + + deleteConnection: function(connectionId) { + var self = this; + + self.showConfirmDialog('Verbindung löschen', 'Möchten Sie diese Verbindung wirklich löschen?', function() { + self.doDeleteConnection(connectionId); + }); + }, + + // Internal function to delete connection without confirmation + doDeleteConnection: function(connectionId) { + var self = this; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'delete', + connection_id: connectionId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + location.reload(); + } else { + self.showAlertDialog('Fehler', response.error); + } + } + }); + }, + + // Custom confirm dialog (replaces browser confirm()) + showConfirmDialog: function(title, message, onConfirm, onCancel) { + var self = this; + + // Remove any existing confirm dialog + $('#kundenkarte-confirm-dialog').remove(); + + var html = '
'; + html += '
'; + html += '

' + self.escapeHtml(title) + '

'; + html += '×
'; + html += '
'; + html += '

' + self.escapeHtml(message) + '

'; + html += '
'; + html += ''; + html += '
'; + + $('body').append(html); + $('#kundenkarte-confirm-dialog').addClass('visible'); + + // Focus yes button + $('#confirm-yes').focus(); + + // Yes button + $('#confirm-yes').on('click', function() { + $('#kundenkarte-confirm-dialog').remove(); + if (typeof onConfirm === 'function') { + onConfirm(); + } + }); + + // No button and close + $('#confirm-no, #kundenkarte-confirm-dialog .kundenkarte-modal-close').on('click', function() { + $('#kundenkarte-confirm-dialog').remove(); + if (typeof onCancel === 'function') { + onCancel(); + } + }); + + // ESC key + $(document).one('keydown.confirmDialog', function(e) { + if (e.key === 'Escape') { + $('#kundenkarte-confirm-dialog').remove(); + if (typeof onCancel === 'function') { + onCancel(); + } + } else if (e.key === 'Enter') { + $('#kundenkarte-confirm-dialog').remove(); + if (typeof onConfirm === 'function') { + onConfirm(); + } + } + }); + }, + + // Custom alert dialog (replaces browser alert()) + showAlertDialog: function(title, message, onClose) { + var self = this; + + // Remove any existing alert dialog + $('#kundenkarte-alert-dialog').remove(); + + var html = '
'; + html += '
'; + html += '

' + self.escapeHtml(title) + '

'; + html += '×
'; + html += '
'; + html += '

' + self.escapeHtml(message) + '

'; + html += '
'; + html += ''; + html += '
'; + + $('body').append(html); + $('#kundenkarte-alert-dialog').addClass('visible'); + + $('#alert-ok').focus(); + + var closeDialog = function() { + $('#kundenkarte-alert-dialog').remove(); + $(document).off('keydown.alertDialog'); + if (typeof onClose === 'function') { + onClose(); + } + }; + + $('#alert-ok, #kundenkarte-alert-dialog .kundenkarte-modal-close').on('click', closeDialog); + + $(document).on('keydown.alertDialog', function(e) { + if (e.key === 'Escape' || e.key === 'Enter') { + closeDialog(); + } + }); + }, + + escapeHtml: function(text) { + if (!text) return ''; + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + }; + + /** + * Connection Editor Component + * Interactive SVG-based connection editor with orthogonal routing + * Supports busbars, multi-phase connections, and drag-drop + */ + KundenKarte.ConnectionEditor = { + TE_WIDTH: 50, + BLOCK_HEIGHT: 110, + BUSBAR_HEIGHT: 25, + BUSBAR_SPACING: 8, + CONNECTION_AREA_HEIGHT: 120, + ENDPOINT_RADIUS: 6, + + // Phase colors (German electrical standard) + PHASE_COLORS: { + 'L1': '#8B4513', // Brown + 'L2': '#000000', // Black + 'L3': '#808080', // Gray + 'N': '#0066cc', // Blue + 'PE': '#27ae60', // Green-Yellow + 'L1N': '#3498db', // Combined + '3P': '#2c3e50', // 3-phase + '3P+N': '#34495e' // 3-phase + neutral + }, + + // State + isExpanded: false, + currentCarrierId: null, + currentAnlageId: null, + connections: [], + busbars: [], + equipment: [], + dragState: null, + selectedConnection: null, + + init: function(anlageId) { + console.log('ConnectionEditor.init called with anlageId:', anlageId); + if (!anlageId) { + console.log('ConnectionEditor: No anlageId, aborting'); + return; + } + this.currentAnlageId = anlageId; + + // Restore expanded state from localStorage + var savedState = localStorage.getItem('kundenkarte_connection_editor_expanded'); + this.isExpanded = savedState === 'true'; + console.log('ConnectionEditor: isExpanded =', this.isExpanded); + + this.bindEvents(); + console.log('ConnectionEditor: Events bound'); + + this.renderAllEditors(); + console.log('ConnectionEditor: Editors rendered'); + }, + + bindEvents: function() { + var self = this; + + // Toggle editor visibility + $(document).on('click', '.kundenkarte-connection-editor-toggle', function(e) { + e.preventDefault(); + var $editor = $(this).closest('.kundenkarte-carrier').find('.kundenkarte-connection-editor'); + var carrierId = $(this).closest('.kundenkarte-carrier').data('carrier-id'); + + $editor.toggleClass('expanded'); + $(this).find('i').toggleClass('fa-chevron-down fa-chevron-up'); + + // Save state + self.isExpanded = $editor.hasClass('expanded'); + localStorage.setItem('kundenkarte_connection_editor_expanded', self.isExpanded); + + if (self.isExpanded) { + self.loadAndRenderEditor(carrierId); + } + }); + + // Add busbar button + $(document).on('click', '.kundenkarte-add-busbar-btn', function(e) { + e.preventDefault(); + var carrierId = $(this).data('carrier-id'); + self.showBusbarDialog(carrierId); + }); + + // Add connection button + $(document).on('click', '.kundenkarte-add-conn-btn', function(e) { + e.preventDefault(); + var carrierId = $(this).data('carrier-id'); + self.showConnectionDialog(carrierId); + }); + + // SVG mouse events for drag-drop connections + $(document).on('mousedown', '.kundenkarte-endpoint', function(e) { + e.preventDefault(); + e.stopPropagation(); + var $endpoint = $(this); + var carrierId = $endpoint.closest('.kundenkarte-connection-editor').data('carrier-id'); + + self.startDragConnection(e, $endpoint, carrierId); + }); + + $(document).on('mousemove', function(e) { + if (self.dragState) { + self.updateDragConnection(e); + } + }); + + $(document).on('mouseup', function(e) { + if (self.dragState) { + self.endDragConnection(e); + } + }); + + // Click on busbar to edit + $(document).on('click', '.kundenkarte-busbar-element', function(e) { + e.preventDefault(); + e.stopPropagation(); + var connectionId = $(this).data('connection-id'); + var carrierId = $(this).closest('.kundenkarte-connection-editor').data('carrier-id'); + self.showEditBusbarDialog(connectionId, carrierId); + }); + + // Click on connection line to edit/delete + $(document).on('click', '.kundenkarte-connection-path', function(e) { + e.preventDefault(); + e.stopPropagation(); + var connectionId = $(this).data('connection-id'); + var carrierId = $(this).closest('.kundenkarte-connection-editor').data('carrier-id'); + self.showEditConnectionDialog(connectionId, carrierId); + }); + + // Hover effects + $(document).on('mouseenter', '.kundenkarte-endpoint', function() { + $(this).addClass('hover'); + }); + $(document).on('mouseleave', '.kundenkarte-endpoint', function() { + $(this).removeClass('hover'); + }); + }, + + renderAllEditors: function() { + var self = this; + $('.kundenkarte-carrier').each(function() { + var carrierId = $(this).data('carrier-id'); + var $editor = $(this).find('.kundenkarte-connection-editor'); + + // Editor is already rendered via PHP, just need to initialize state + if ($editor.length) { + if (self.isExpanded) { + $editor.addClass('expanded'); + $(this).find('.kundenkarte-connection-editor-toggle i').removeClass('fa-chevron-down').addClass('fa-chevron-up'); + self.loadAndRenderEditor(carrierId); + } + } + }); + }, + + loadAndRenderEditor: function(carrierId) { + var self = this; + + // Load equipment for this carrier + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + data: { action: 'list', carrier_id: carrierId }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.equipment = response.equipment || []; + + // Load connections + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + data: { action: 'list', carrier_id: carrierId }, + dataType: 'json', + success: function(connResponse) { + if (connResponse.success) { + self.connections = connResponse.connections || []; + self.busbars = self.connections.filter(function(c) { return c.is_rail == 1; }); + self.renderEditor(carrierId); + } + } + }); + } + } + }); + }, + + renderEditor: function(carrierId) { + var self = this; + var $editor = $('.kundenkarte-connection-editor[data-carrier-id="' + carrierId + '"]'); + var $svg = $editor.find('.kundenkarte-connection-svg'); + var totalTE = parseInt($editor.data('total-te')) || 12; + var totalWidth = totalTE * this.TE_WIDTH; + + // Reset busbar positions + this.busbarPositions = {}; + + var svgContent = ''; + + // Defs for markers and gradients + svgContent += ''; + svgContent += ''; + svgContent += ''; + svgContent += ''; + + // Phase gradients + Object.keys(this.PHASE_COLORS).forEach(function(phase) { + svgContent += ''; + svgContent += ''; + svgContent += ''; + svgContent += ''; + }); + svgContent += ''; + + // If no busbars exist, show a demo + if (this.busbars.length === 0 && this.equipment.length > 0) { + svgContent += this.renderDemoView(carrierId, totalTE, totalWidth); + $svg.html(svgContent); + $svg.attr('height', 200); + return; + } + + // Background grid + for (var i = 0; i <= totalTE; i++) { + var x = i * this.TE_WIDTH; + svgContent += ''; + } + + // Render busbars + var busbarY = 15; + var busbarsByPosition = {}; + + this.busbars.forEach(function(busbar, index) { + var posKey = busbar.position_y < 0 ? 'above' : busbar.position_y; + if (!busbarsByPosition[posKey]) busbarsByPosition[posKey] = []; + busbarsByPosition[posKey].push(busbar); + }); + + Object.keys(busbarsByPosition).forEach(function(posKey) { + busbarsByPosition[posKey].forEach(function(busbar, idx) { + svgContent += self.renderBusbar(busbar, carrierId, busbarY + (idx * (self.BUSBAR_HEIGHT + self.BUSBAR_SPACING))); + }); + busbarY += busbarsByPosition[posKey].length * (self.BUSBAR_HEIGHT + self.BUSBAR_SPACING); + }); + + // Render equipment endpoints + var equipmentY = busbarY + 20; + + // Store equipment positions for connection routing + this.equipmentPositions = {}; + this.equipment.forEach(function(eq) { + var width = eq.width_te * self.TE_WIDTH; + var centerX = (eq.position_te - 1) * self.TE_WIDTH + width / 2; + self.equipmentPositions[eq.id] = { + x: centerX, + left: (eq.position_te - 1) * self.TE_WIDTH, + right: eq.position_te * self.TE_WIDTH + (eq.width_te - 1) * self.TE_WIDTH, + inputY: equipmentY - 15, + outputY: equipmentY + 15, + centerY: equipmentY + }; + }); + + // Render non-busbar connections FIRST (behind equipment) + // But route them around blocks, not through them + var regularConnections = this.connections.filter(function(c) { return c.is_rail != 1; }); + + // Connection layer - rendered below equipment layer + svgContent += ''; + regularConnections.forEach(function(conn, index) { + svgContent += self.renderOrthogonalConnection(conn, carrierId, equipmentY, index); + }); + svgContent += ''; + + // Equipment layer - rendered on top + svgContent += ''; + this.equipment.forEach(function(eq) { + svgContent += self.renderEquipmentEndpoints(eq, equipmentY, carrierId); + }); + svgContent += ''; + + // Render drag preview line (hidden by default) - on top of everything + svgContent += '