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 '
'.$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 '';
+
+ // 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 '';
+
+ // Add new field form
+ print '';
+ print '
';
+ print ' ';
+ print ' ';
+ print ' ';
+ print ' ';
+ print '';
+ print ' ';
+ print '
';
+
+ // Help box
+ print '';
+ print '
Hilfe: Equipment-Felder
';
+ print '
';
+ print 'Show in Hover Feld wird im Tooltip angezeigt ';
+ print 'Show on Block Feld wird direkt auf dem SVG-Block angezeigt (z.B. "B16") ';
+ print 'Dropdown-Auswahl Optionen mit | trennen, z.B.: B|C|D oder 6|10|16|20|25|32 ';
+ print '
';
+ print '
';
+ }
+
+} else {
+ // System filter
+ print '';
+ print $langs->trans('FilterBySystem').': ';
+ print '';
+ print ''.$langs->trans('All').' ';
+ foreach ($systems as $sys) {
+ $sel = ($systemFilter == $sys->rowid) ? ' selected' : '';
+ print ''.dol_escape_htmltag($sys->label).' ';
+ }
+ print ' ';
+ print ' ';
+
+ // Add button
+ print '';
+
+ // List
+ $types = $equipmentType->fetchAllBySystem($systemFilter, 0);
+
+ print '';
+ print '';
+ print ''.$langs->trans('TypeRef').' ';
+ print ''.$langs->trans('TypeLabel').' ';
+ print ''.$langs->trans('System').' ';
+ print ''.$langs->trans('WidthTE').' ';
+ print ''.$langs->trans('Color').' ';
+ print ''.$langs->trans('Position').' ';
+ print ''.$langs->trans('Status').' ';
+ print ''.$langs->trans('Actions').' ';
+ print ' ';
+
+ foreach ($types as $type) {
+ print '';
+
+ print '';
+ if ($type->picto) {
+ print kundenkarte_render_icon($type->picto).' ';
+ }
+ print dol_escape_htmltag($type->ref).' ';
+
+ print ''.dol_escape_htmltag($type->label);
+ if ($type->label_short) {
+ print ' ('.$type->label_short.') ';
+ }
+ print ' ';
+
+ print ''.dol_escape_htmltag($type->system_label).' ';
+ print ''.$type->width_te.' TE ';
+ print ' ';
+ print ''.$type->position.' ';
+
+ print '';
+ if ($type->active) {
+ print ''.img_picto($langs->trans('Enabled'), 'switch_on').' ';
+ } else {
+ print ''.img_picto($langs->trans('Disabled'), 'switch_off').' ';
+ }
+ print ' ';
+
+ print '';
+ print '';
+ print '
';
+ print '
';
+ if (!$type->is_system) {
+ print '
';
+ }
+ print '
';
+ print ' ';
+
+ print ' ';
+ }
+
+ if (empty($types)) {
+ print ''.$langs->trans('NoRecords').' ';
+ }
+
+ print '
';
+}
+
+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 += '';
+
+ // 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 = '';
+
+ // Block rectangle with rounded corners
+ 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 += '';
+ html += '
';
+ html += '
';
+ 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 += '';
+ html += '
';
+ html += '
';
+ html += '
';
+ 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 += '';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ 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('' + self.escapeHtml(type.label) + ' ');
+ });
+
+ // 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('' + self.escapeHtml(device.display_label) + ' ');
+ });
+ }
+ }
+ });
+ },
+
+ 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 += '