From 20fb9d3b059817cde4576e4d827bd0c8cdcbecff Mon Sep 17 00:00:00 2001 From: data Date: Thu, 26 Feb 2026 14:04:17 +0100 Subject: [PATCH] feat: Dezimal-TE, Equipment-Kategorien, Schaltplan-Fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Dezimal-TE (0.1 Schritte): DB DECIMAL(4,1), JS parseFloat statt parseInt, Drag&Drop mit 0.1-Snap, SVG-Markierungen (ganzzahlig deutlich, 0.5er subtil) - Equipment-Kategorien: automat/schutz/steuerung/klemme im Typ-Editor und Dialog - Hutschiene löschen Fix: showConfirmDialog → KundenKarte.showConfirm() (3 Stellen) - terminals_config JSON-Sanitizer: PHP beim Speichern + JS-Fallback (Dolibarr GETPOST konvertiert Newlines zu literal \r\n → ungültiges JSON) - Equipment duplizieren: Label-Inkrement, Feldwerte werden mitkopiert - Statusleiste Größen-Sprung behoben (min-height statt dynamisch) - Duplikat-Docblock in equipmentcarrier.class.php bereinigt Co-Authored-By: Claude Opus 4.6 --- admin/equipment_types.php | 57 ++++++++- ajax/equipment.php | 49 ++++++-- ajax/equipment_carrier.php | 6 + class/equipment.class.php | 32 ++++- class/equipmentcarrier.class.php | 105 +++++++++------- class/equipmenttype.class.php | 11 +- core/modules/modKundenKarte.class.php | 71 ++++++++++- css/kundenkarte.css | 22 +++- js/kundenkarte.js | 167 ++++++++++++++------------ js/pwa.js | 77 ++++++------ tabs/anlagen.php | 2 +- tabs/contact_anlagen.php | 2 +- 12 files changed, 414 insertions(+), 187 deletions(-) diff --git a/admin/equipment_types.php b/admin/equipment_types.php index 22ee53e..4427771 100755 --- a/admin/equipment_types.php +++ b/admin/equipment_types.php @@ -69,10 +69,19 @@ if ($action == 'add') { $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->category = GETPOST('category', 'aZ09') ?: 'steuerung'; + $equipmentType->width_te = floatval(GETPOST('width_te', 'alpha')); $equipmentType->color = GETPOST('color', 'alphanohtml'); // fk_product removed - products are selected per equipment in editor - $equipmentType->terminals_config = GETPOST('terminals_config', 'nohtml'); + // JSON bereinigen: literale \r\n entfernen, dann parsen+re-encodieren für sauberes JSON + $rawConfig = GETPOST('terminals_config', 'nohtml'); + if (!empty($rawConfig)) { + $rawConfig = str_replace(array("\\r\\n", "\\r", "\\n", "\\t"), array("\n", "", "\n", "\t"), $rawConfig); + $decoded = json_decode($rawConfig); + $equipmentType->terminals_config = ($decoded !== null) ? json_encode($decoded) : $rawConfig; + } else { + $equipmentType->terminals_config = ''; + } $equipmentType->flow_direction = GETPOST('flow_direction', 'alphanohtml'); $equipmentType->terminal_position = GETPOST('terminal_position', 'alphanohtml') ?: 'both'; $equipmentType->picto = GETPOST('picto', 'alphanohtml'); @@ -118,10 +127,19 @@ if ($action == 'update') { $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->category = GETPOST('category', 'aZ09') ?: 'steuerung'; + $equipmentType->width_te = floatval(GETPOST('width_te', 'alpha')); $equipmentType->color = GETPOST('color', 'alphanohtml'); // fk_product removed - products are selected per equipment in editor - $equipmentType->terminals_config = GETPOST('terminals_config', 'nohtml'); + // JSON bereinigen: literale \r\n entfernen, dann parsen+re-encodieren für sauberes JSON + $rawConfig = GETPOST('terminals_config', 'nohtml'); + if (!empty($rawConfig)) { + $rawConfig = str_replace(array("\\r\\n", "\\r", "\\n", "\\t"), array("\n", "", "\n", "\t"), $rawConfig); + $decoded = json_decode($rawConfig); + $equipmentType->terminals_config = ($decoded !== null) ? json_encode($decoded) : $rawConfig; + } else { + $equipmentType->terminals_config = ''; + } $equipmentType->flow_direction = GETPOST('flow_direction', 'alphanohtml'); $equipmentType->terminal_position = GETPOST('terminal_position', 'alphanohtml') ?: 'both'; $equipmentType->picto = GETPOST('picto', 'alphanohtml'); @@ -171,6 +189,7 @@ if ($action == 'copy' && $typeId > 0) { $newType->label_short = $sourceType->label_short; $newType->description = $sourceType->description; $newType->fk_system = $sourceType->fk_system; + $newType->category = $sourceType->category; $newType->width_te = $sourceType->width_te; $newType->color = $sourceType->color; // fk_product not copied - products are selected per equipment in editor @@ -350,6 +369,23 @@ if (in_array($action, array('create', 'edit'))) { } print ''; + // Category + $categories = array( + 'automat' => 'Leitungsschutz', + 'schutz' => 'Schutzgeräte', + 'steuerung' => 'Steuerung & Sonstiges', + 'klemme' => 'Klemmen', + ); + print ''.$langs->trans('Category').''; + print ''; + print ' (Gruppierung im Typ-Auswahl-Dialog)'; + print ''; + // Reference print ''.$langs->trans('TypeRef').''; print ''; @@ -370,7 +406,7 @@ if (in_array($action, array('create', 'edit'))) { // Width in TE print ''.$langs->trans('WidthTE').''; - print ''; + print ''; print ' '.$langs->trans('WidthTEHelp').''; // Color @@ -922,6 +958,7 @@ if (in_array($action, array('create', 'edit'))) { print ''; print ''.$langs->trans('TypeRef').''; print ''.$langs->trans('TypeLabel').''; + print ''.$langs->trans('Category').''; print ''.$langs->trans('System').''; print ''.$langs->trans('WidthTE').''; print ''.$langs->trans('Color').''; @@ -930,6 +967,13 @@ if (in_array($action, array('create', 'edit'))) { print ''.$langs->trans('Actions').''; print ''; + $categoryLabels = array( + 'automat' => 'Leitungsschutz', + 'schutz' => 'Schutzgeräte', + 'steuerung' => 'Steuerung & Sonstiges', + 'klemme' => 'Klemmen', + ); + foreach ($types as $type) { print ''; @@ -945,6 +989,7 @@ if (in_array($action, array('create', 'edit'))) { } print ''; + print ''.dol_escape_htmltag($categoryLabels[$type->category] ?? $type->category).''; print ''.dol_escape_htmltag($type->system_label).''; print ''.$type->width_te.' TE'; print ''; @@ -972,7 +1017,7 @@ if (in_array($action, array('create', 'edit'))) { } if (empty($types)) { - print ''.$langs->trans('NoRecords').''; + print ''.$langs->trans('NoRecords').''; } print ''; diff --git a/ajax/equipment.php b/ajax/equipment.php index 6114044..c0d0ba9 100755 --- a/ajax/equipment.php +++ b/ajax/equipment.php @@ -107,6 +107,7 @@ switch ($action) { 'ref' => $t->ref, 'label' => $t->label, 'label_short' => $t->label_short, + 'category' => $t->category ?: 'steuerung', 'width_te' => $t->width_te, 'color' => $t->color, 'picto' => $t->picto @@ -286,8 +287,8 @@ switch ($action) { $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->position_te = floatval(GETPOST('position_te', 'alpha')); + $equipment->width_te = floatval(GETPOST('width_te', 'alpha')); $equipment->fk_product = GETPOSTINT('fk_product'); $equipment->fk_protection = GETPOSTINT('fk_protection'); $equipment->protection_label = GETPOST('protection_label', 'alphanohtml'); @@ -372,8 +373,8 @@ switch ($action) { break; } if ($equipment->fetch($equipmentId) > 0) { - $newPosition = GETPOSTINT('position_te'); - $newWidth = GETPOSTINT('width_te') ?: $equipment->width_te; + $newPosition = floatval(GETPOST('position_te', 'alpha')); + $newWidth = floatval(GETPOST('width_te', 'alpha')) ?: $equipment->width_te; // Check if new position is available (excluding current equipment) if ($newPosition != $equipment->position_te || $newWidth != $equipment->width_te) { @@ -451,7 +452,7 @@ switch ($action) { break; } if ($equipment->fetch($equipmentId) > 0) { - $newPosition = GETPOSTINT('position_te'); + $newPosition = floatval(GETPOST('position_te', 'alpha')); // Check if new position is available $carrier = new EquipmentCarrier($db); @@ -500,7 +501,7 @@ switch ($action) { } if ($equipment->fetch($equipmentId) > 0) { $newCarrierId = GETPOSTINT('carrier_id'); - $newPosition = GETPOSTINT('position_te') ?: 1; + $newPosition = floatval(GETPOST('position_te', 'alpha')) ?: 1; // Get old carrier for label pattern check $oldCarrier = new EquipmentCarrier($db); @@ -608,6 +609,30 @@ switch ($action) { // Fetch the new equipment to return its data $newEquipment = new Equipment($db); if ($newEquipment->fetch($newId) > 0) { + // Icon-URL berechnen + $iconUrl = ''; + if (!empty($newEquipment->type_icon_file)) { + $iconUrl = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=equipment_icons/'.urlencode($newEquipment->type_icon_file); + } + + // Typ-Felder laden + $typeFields = array(); + $sqlTf = "SELECT field_code, field_label, show_on_block, show_in_hover"; + $sqlTf .= " FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_type_field"; + $sqlTf .= " WHERE fk_equipment_type = ".((int) $newEquipment->fk_equipment_type); + $sqlTf .= " AND active = 1 ORDER BY position ASC"; + $resTf = $db->query($sqlTf); + if ($resTf) { + while ($objTf = $db->fetch_object($resTf)) { + $typeFields[] = array( + 'field_code' => $objTf->field_code, + 'field_label' => $objTf->field_label, + 'show_on_block' => (int) $objTf->show_on_block, + 'show_in_hover' => (int) $objTf->show_in_hover + ); + } + } + $response['equipment'] = array( 'id' => $newEquipment->id, 'fk_carrier' => $newEquipment->fk_carrier, @@ -617,14 +642,22 @@ switch ($action) { 'type_color' => $newEquipment->type_color, 'type_ref' => $newEquipment->type_ref, 'type_icon_file' => $newEquipment->type_icon_file, + 'type_icon_url' => $iconUrl, + 'type_block_image' => $newEquipment->type_block_image, + 'type_block_image_url' => !empty($newEquipment->type_block_image) ? DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.urlencode($newEquipment->type_block_image) : '', + 'type_flow_direction' => $newEquipment->type_flow_direction, + 'type_terminal_position' => $newEquipment->type_terminal_position ?: 'both', 'terminals_config' => $newEquipment->terminals_config, + 'type_fields' => $typeFields, 'label' => $newEquipment->label, 'position_te' => $newEquipment->position_te, 'width_te' => $newEquipment->width_te, 'block_label' => $newEquipment->getBlockLabel(), 'block_color' => $newEquipment->getBlockColor(), 'field_values' => $newEquipment->getFieldValues(), - 'fk_product' => $newEquipment->fk_product + 'fk_product' => $newEquipment->fk_product, + 'fk_protection' => $newEquipment->fk_protection, + 'protection_label' => $newEquipment->protection_label ); // Audit log @@ -658,7 +691,7 @@ switch ($action) { break; } if ($equipment->fetch($equipmentId) > 0) { - $newPosition = GETPOSTINT('position_te'); + $newPosition = floatval(GETPOST('position_te', 'alpha')); $carrier = new EquipmentCarrier($db); if ($carrier->fetch($equipment->fk_carrier) > 0) { diff --git a/ajax/equipment_carrier.php b/ajax/equipment_carrier.php index cb11013..16496de 100755 --- a/ajax/equipment_carrier.php +++ b/ajax/equipment_carrier.php @@ -68,6 +68,12 @@ switch ($action) { 'type_id' => $eq->fk_equipment_type, 'type_label' => $eq->type_label, 'type_label_short' => $eq->type_label_short, + 'type_ref' => $eq->type_ref, + 'type_icon_file' => $eq->type_icon_file, + 'type_block_image' => $eq->type_block_image, + 'type_flow_direction' => $eq->type_flow_direction, + 'type_terminal_position' => $eq->type_terminal_position ?: 'both', + 'terminals_config' => $eq->terminals_config, 'type_color' => $eq->type_color, 'label' => $eq->label, 'position_te' => $eq->position_te, diff --git a/class/equipment.class.php b/class/equipment.class.php index 7427c8d..a761a3d 100755 --- a/class/equipment.class.php +++ b/class/equipment.class.php @@ -99,8 +99,8 @@ class Equipment extends CommonObject $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 .= ", ".floatval($this->position_te); + $sql .= ", ".floatval($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"); @@ -215,8 +215,8 @@ class Equipment extends CommonObject $sql .= " fk_carrier = ".((int) $this->fk_carrier); $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 .= ", position_te = ".floatval($this->position_te); + $sql .= ", width_te = ".floatval($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"); @@ -393,10 +393,32 @@ class Equipment extends CommonObject $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; + + // Label: Nummer weiterzählen wenn vorhanden, sonst beibehalten + if (!empty($this->label) && preg_match('/^(.*?)(\d+)$/', $this->label, $matches)) { + $prefix = $matches[1]; + // Höchste Nummer mit gleichem Präfix auf dem Carrier finden + $sql = "SELECT label FROM ".$this->db->prefix()."kundenkarte_equipment"; + $sql .= " WHERE fk_carrier = ".(int)$newEquipment->fk_carrier; + $sql .= " AND status = 1 AND label IS NOT NULL AND label != ''"; + $resql = $this->db->query($sql); + $maxNum = 0; + if ($resql) { + while ($obj = $this->db->fetch_object($resql)) { + if (preg_match('/^'.preg_quote($prefix, '/').'(\d+)$/', $obj->label, $m)) { + $maxNum = max($maxNum, (int)$m[1]); + } + } + } + $newEquipment->label = $prefix . ($maxNum + 1); + } else { + $newEquipment->label = $this->label; + } $newEquipment->field_values = $this->field_values; $newEquipment->fk_product = $this->fk_product; + $newEquipment->fk_protection = $this->fk_protection; + $newEquipment->protection_label = $this->protection_label; $newEquipment->note_private = $this->note_private; $newEquipment->status = 1; diff --git a/class/equipmentcarrier.class.php b/class/equipmentcarrier.class.php index 2094417..b248aaa 100755 --- a/class/equipmentcarrier.class.php +++ b/class/equipmentcarrier.class.php @@ -318,41 +318,54 @@ class EquipmentCarrier extends CommonObject } /** - * Get array of occupied TE slots + * Belegte TE-Ranges zurückgeben (für Dezimal-Breiten) * - * @return array Array of occupied slot numbers (0-based) + * @return array Array von [start, end] Ranges */ - public function getOccupiedSlots() + public function getOccupiedRanges() { - $occupied = array(); + $ranges = 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; - } + $ranges[] = array( + 'start' => floatval($eq->position_te), + 'end' => floatval($eq->position_te) + floatval($eq->width_te) + ); } - return $occupied; + usort($ranges, function($a, $b) { + return $a['start'] <=> $b['start']; + }); + + return $ranges; } /** - * Get used TE count + * Belegte TE-Summe * - * @return int Number of used TE + * @return float Belegte TE (kann Dezimal sein, z.B. 10.5) */ public function getUsedTE() { - return count($this->getOccupiedSlots()); + if (empty($this->equipment)) { + $this->fetchEquipment(); + } + + $used = 0; + foreach ($this->equipment as $eq) { + $used += floatval($eq->width_te); + } + return $used; } /** - * Get free TE count + * Freie TE * - * @return int Number of free TE + * @return float Freie TE */ public function getFreeTE() { @@ -360,44 +373,52 @@ class EquipmentCarrier extends CommonObject } /** - * Find next free position for given width + * Nächste freie Position finden (unterstützt Dezimal-Breiten) * - * @param int $width Width in TE needed - * @return int Position (1-based) or -1 if no space + * @param float $width Benötigte Breite in TE + * @return float Position (1-basiert) oder -1 wenn kein Platz */ public function getNextFreePosition($width = 1) { - $occupied = $this->getOccupiedSlots(); + $width = floatval($width); + $ranges = $this->getOccupiedRanges(); + $maxEnd = floatval($this->total_te) + 1; // Position 1 + total_te = Ende der Schiene - // 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) { + $pos = 1.0; + foreach ($ranges as $range) { + // Passt in die Lücke vor diesem Element? + if ($pos + $width <= $range['start'] + 0.001) { return $pos; } + // Hinter dieses Element springen + if ($range['end'] > $pos) { + $pos = $range['end']; + } } - return -1; // No space available + // Platz nach dem letzten Element? + if ($pos + $width <= $maxEnd + 0.001) { + return $pos; + } + + return -1; } /** - * Check if position is available for given width + * Prüfen ob Position verfügbar ist (unterstützt Dezimal-Breiten) * - * @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 + * @param float $position Startposition (1-basiert) + * @param float $width Breite in TE + * @param int $excludeEquipmentId Equipment-ID zum Ausschließen (für Updates) + * @return bool True wenn Position frei */ public function isPositionAvailable($position, $width, $excludeEquipmentId = 0) { - // Check bounds (positions are 1-based) - if ($position < 1 || $position + $width - 1 > $this->total_te) { + $position = floatval($position); + $width = floatval($width); + + // Grenzen prüfen (Position 1 = erstes TE) + if ($position < 1 || $position + $width > floatval($this->total_te) + 1 + 0.001) { return false; } @@ -405,19 +426,19 @@ class EquipmentCarrier extends CommonObject $this->fetchEquipment(); } + $newEnd = $position + $width; + 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; + // Overlap-Prüfung mit Half-Open-Ranges: [start, end) + $eqStart = floatval($eq->position_te); + $eqEnd = $eqStart + floatval($eq->width_te); - if ($newStart <= $eqEnd && $newEnd >= $eqStart) { - return false; // Overlap + if ($position < $eqEnd - 0.001 && $eqStart < $newEnd - 0.001) { + return false; } } diff --git a/class/equipmenttype.class.php b/class/equipmenttype.class.php index 0c491cb..817ae4e 100755 --- a/class/equipmenttype.class.php +++ b/class/equipmenttype.class.php @@ -21,6 +21,7 @@ class EquipmentType extends CommonObject public $label_short; public $description; public $fk_system; + public $category = 'steuerung'; // automat, schutz, steuerung, klemme // Equipment-spezifische Felder public $width_te = 1; @@ -78,7 +79,7 @@ class EquipmentType extends CommonObject $this->db->begin(); $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; - $sql .= "entity, ref, label, label_short, description, fk_system,"; + $sql .= "entity, ref, label, label_short, description, fk_system, category,"; $sql .= " width_te, color, fk_product, terminals_config, flow_direction, terminal_position,"; $sql .= " picto, icon_file, block_image, is_system, position, active,"; $sql .= " date_creation, fk_user_creat"; @@ -89,7 +90,8 @@ class EquipmentType extends CommonObject $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->db->escape($this->category ?: 'steuerung')."'"; + $sql .= ", ".floatval($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"); @@ -151,6 +153,7 @@ class EquipmentType extends CommonObject $this->label_short = $obj->label_short; $this->description = $obj->description; $this->fk_system = $obj->fk_system; + $this->category = $obj->category ?: 'steuerung'; $this->width_te = $obj->width_te; $this->color = $obj->color; $this->fk_product = $obj->fk_product; @@ -202,7 +205,8 @@ class EquipmentType extends CommonObject $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 .= ", category = '".$this->db->escape($this->category ?: 'steuerung')."'"; + $sql .= ", width_te = ".floatval($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"); @@ -315,6 +319,7 @@ class EquipmentType extends CommonObject $type->label = $obj->label; $type->label_short = $obj->label_short; $type->fk_system = $obj->fk_system; + $type->category = $obj->category ?: 'steuerung'; $type->width_te = $obj->width_te; $type->color = $obj->color; $type->fk_product = $obj->fk_product; diff --git a/core/modules/modKundenKarte.class.php b/core/modules/modKundenKarte.class.php index 6fa99e8..12274cd 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 = '5.2.0'; + $this->version = '6.1'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; @@ -615,6 +615,12 @@ class modKundenKarte extends DolibarrModules // v4.1.0: Graph-Positionen speichern $this->migrate_v410_graph_positions(); + + // v5.2.0: Equipment-Typ Kategorie + $this->migrate_v520_equipment_type_category(); + + // v5.2.0: Halbe TE-Breiten (4.5 TE für Neozed etc.) + $this->migrate_v520_decimal_te(); } /** @@ -760,6 +766,69 @@ class modKundenKarte extends DolibarrModules $this->db->query("ALTER TABLE ".$table." ADD COLUMN graph_y double DEFAULT NULL AFTER graph_x"); } + /** + * Migration v5.2.0: Equipment-Typ Kategorie-Spalte + * Ermöglicht konfigurierbare Kategorien im Typ-Auswahl-Dialog + */ + private function migrate_v520_equipment_type_category() + { + $table = MAIN_DB_PREFIX."kundenkarte_equipment_type"; + + $resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'"); + if (!$resql || $this->db->num_rows($resql) == 0) { + return; + } + + $resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'category'"); + if ($resql && $this->db->num_rows($resql) > 0) { + return; + } + + // Spalte hinzufügen + $this->db->query("ALTER TABLE ".$table." ADD COLUMN category varchar(32) DEFAULT 'steuerung' AFTER fk_system"); + + // Bestehende Typen kategorisieren (basierend auf bisheriger Logik) + // Leitungsschutz: LS*, HS*, NH*, MSS + $this->db->query("UPDATE ".$table." SET category = 'automat' WHERE ref LIKE 'LS%' OR ref LIKE 'HS%' OR ref LIKE 'NH%' OR ref = 'MSS'"); + // Schutzgeräte: FI*, AFDD, SPD* + $this->db->query("UPDATE ".$table." SET category = 'schutz' WHERE ref LIKE 'FI%' OR ref = 'AFDD' OR ref LIKE 'SPD%'"); + // Klemmen: RK_* + $this->db->query("UPDATE ".$table." SET category = 'klemme' WHERE ref LIKE 'RK\\_%'"); + // Neozed als Leitungsschutz + $this->db->query("UPDATE ".$table." SET category = 'automat' WHERE ref LIKE 'NEO%'"); + } + + /** + * Migration v5.2.0: width_te und position_te auf DECIMAL(4,1) + * Ermöglicht halbe TE-Breiten (z.B. 4.5 TE für Neozed-Elemente) + */ + private function migrate_v520_decimal_te() + { + // Equipment-Type: width_te INT → DECIMAL(4,1) + $typeTable = MAIN_DB_PREFIX."kundenkarte_equipment_type"; + $resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($typeTable)."'"); + if ($resql && $this->db->num_rows($resql) > 0) { + $resql = $this->db->query("SHOW COLUMNS FROM ".$typeTable." WHERE Field = 'width_te' AND Type LIKE 'int%'"); + if ($resql && $this->db->num_rows($resql) > 0) { + $this->db->query("ALTER TABLE ".$typeTable." MODIFY COLUMN width_te DECIMAL(4,1) NOT NULL DEFAULT 1"); + } + } + + // Equipment: width_te und position_te INT → DECIMAL(4,1) + $eqTable = MAIN_DB_PREFIX."kundenkarte_equipment"; + $resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($eqTable)."'"); + if ($resql && $this->db->num_rows($resql) > 0) { + $resql = $this->db->query("SHOW COLUMNS FROM ".$eqTable." WHERE Field = 'width_te' AND Type LIKE 'int%'"); + if ($resql && $this->db->num_rows($resql) > 0) { + $this->db->query("ALTER TABLE ".$eqTable." MODIFY COLUMN width_te DECIMAL(4,1) NOT NULL DEFAULT 1"); + } + $resql = $this->db->query("SHOW COLUMNS FROM ".$eqTable." WHERE Field = 'position_te' AND Type LIKE 'int%'"); + if ($resql && $this->db->num_rows($resql) > 0) { + $this->db->query("ALTER TABLE ".$eqTable." MODIFY COLUMN position_te DECIMAL(4,1) NOT NULL DEFAULT 1"); + } + } + } + /** * Function called when module is disabled. * Remove from database constants, boxes and permissions from Dolibarr database. diff --git a/css/kundenkarte.css b/css/kundenkarte.css index 837f288..3a445a2 100755 --- a/css/kundenkarte.css +++ b/css/kundenkarte.css @@ -2260,8 +2260,8 @@ body.kundenkarte-drag-active * { /* Messages - Fixed height status bar */ .schematic-message { padding: 6px 15px !important; - margin-bottom: 10px !important; - border-radius: 4px !important; + margin-bottom: 0 !important; + border-radius: 0 !important; font-size: 12px !important; height: 28px !important; min-height: 28px !important; @@ -2269,30 +2269,40 @@ body.kundenkarte-drag-active * { line-height: 16px !important; overflow: hidden !important; box-sizing: border-box !important; + transition: none !important; + background: #2d4a5e !important; + color: #7ec8e3 !important; + border: 1px solid #3498db !important; + border-top: none !important; + border-bottom: none !important; } .schematic-message.info { background: #2d4a5e !important; color: #7ec8e3 !important; - border: 1px solid #3498db !important; + border-left: 1px solid #3498db !important; + border-right: 1px solid #3498db !important; } .schematic-message.success { background: #2d5a3d !important; color: #7ee8a0 !important; - border: 1px solid #27ae60 !important; + border-left: 1px solid #27ae60 !important; + border-right: 1px solid #27ae60 !important; } .schematic-message.warning { background: #5a5a2d !important; color: #e8e87e !important; - border: 1px solid #f1c40f !important; + border-left: 1px solid #f1c40f !important; + border-right: 1px solid #f1c40f !important; } .schematic-message.error { background: #5a2d2d !important; color: #e87e7e !important; - border: 1px solid #e74c3c !important; + border-left: 1px solid #e74c3c !important; + border-right: 1px solid #e74c3c !important; } /* Terminal color by phase */ diff --git a/js/kundenkarte.js b/js/kundenkarte.js index 4f3e605..bb4bd6d 100755 --- a/js/kundenkarte.js +++ b/js/kundenkarte.js @@ -1756,10 +1756,14 @@ html += '
'; html += ''; - // Background grid (TE markers) + // TE-Raster (ganzzahlige TEs deutlich, 0.5er subtil) for (var i = 0; i <= carrier.total_te; i++) { var x = i * this.TE_WIDTH; - html += ''; + html += ''; + if (i < carrier.total_te) { + var halfX = x + this.TE_WIDTH / 2; + html += ''; + } } // Render equipment blocks @@ -1824,11 +1828,16 @@ }, getOccupiedSlots: function(equipment) { + // Range-basierte Belegung (unterstützt Dezimal-TE wie 4.5) var slots = {}; if (equipment) { equipment.forEach(function(eq) { - for (var i = 0; i < eq.width_te; i++) { - slots[eq.position_te + i] = true; + var start = parseFloat(eq.position_te) || 1; + var width = parseFloat(eq.width_te) || 1; + var end = start + width; + // Ganzzahl-Slots markieren die von diesem Equipment überlappt werden + for (var i = Math.floor(start); i < Math.ceil(end); i++) { + slots[i] = true; } }); } @@ -5188,7 +5197,7 @@ // Clear all connections $(document).off('click.clearConns').on('click.clearConns', '.schematic-clear-connections', function(e) { e.preventDefault(); - self.showConfirmDialog('Alle löschen', 'Alle Verbindungen wirklich löschen?', function() { + KundenKarte.showConfirm('Alle löschen', 'Alle Verbindungen wirklich löschen?', function() { self.clearAllConnections(); }); }); @@ -5408,7 +5417,7 @@ var newEndTE = newStartTE + data.busbarWidthTE - 1; // Clamp to carrier bounds (shift if needed to fit) - var totalTE = parseInt(targetCarrier.total_te) || 12; + var totalTE = parseFloat(targetCarrier.total_te) || 12; if (newStartTE < 1) { newStartTE = 1; newEndTE = newStartTE + data.busbarWidthTE - 1; @@ -5931,7 +5940,7 @@ var self = this; var baseUrl = $('body').data('base-url') || ''; - this.showConfirmDialog('Hutschiene löschen', 'Diese Hutschiene und alle Equipments darauf wirklich löschen?', function() { + KundenKarte.showConfirm('Hutschiene löschen', 'Diese Hutschiene und alle Equipments darauf wirklich löschen?', function() { $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', method: 'POST', @@ -5962,7 +5971,7 @@ if (!conn) return; var carrier = this.carriers.find(function(c) { return String(c.id) === String(conn.fk_carrier); }); - var totalTE = carrier ? (parseInt(carrier.total_te) || 12) : 12; + var totalTE = carrier ? (parseFloat(carrier.total_te) || 12) : 12; var html = '
'; html += '
'; @@ -6474,10 +6483,16 @@ html += self.escapeHtml(carrier.label || 'H' + (carrierIdx + 1)); html += ''; - // TE markers on the rail - for (var te = 0; te <= (carrier.total_te || 12); te++) { + // TE-Markierungen auf der Schiene (ganzzahlige TEs deutlich, 0.5er-Zwischenmarken subtil) + var totalTECount = carrier.total_te || 12; + for (var te = 0; te <= totalTECount; te++) { var teX = x + te * self.TE_WIDTH; - html += ''; + html += ''; + // 0.5-TE Zwischenmarkierung + if (te < totalTECount) { + var halfX = teX + self.TE_WIDTH / 2; + html += ''; + } } }); }); @@ -6506,9 +6521,15 @@ html += self.escapeHtml(carrier.label || 'Hutschiene ' + (idx + 1)); html += ''; - for (var te = 0; te <= (carrier.total_te || 12); te++) { + // TE-Markierungen (ganzzahlige TEs deutlich, 0.5er subtil) + var totalTECount = carrier.total_te || 12; + for (var te = 0; te <= totalTECount; te++) { var teX = x + te * self.TE_WIDTH; - html += ''; + html += ''; + if (te < totalTECount) { + var halfX = teX + self.TE_WIDTH / 2; + html += ''; + } } }); } @@ -6538,7 +6559,7 @@ var blockWidth = (eq.width_te || 1) * self.TE_WIDTH - 4; var blockHeight = self.BLOCK_HEIGHT; - var x = parseFloat(carrier._x) + ((parseInt(eq.position_te) || 1) - 1) * self.TE_WIDTH + 2; + var x = parseFloat(carrier._x) + ((parseFloat(eq.position_te) || 1) - 1) * self.TE_WIDTH + 2; // Position blocks centered on the rail (Hutschiene) // Rail is at carrier._y, block center should align with rail center var railCenterY = carrier._y + self.RAIL_HEIGHT / 2; @@ -6666,7 +6687,7 @@ var topTerminals = terminals.filter(function(t) { return t.pos === 'top'; }); var bottomTerminals = terminals.filter(function(t) { return t.pos === 'bottom'; }); - var widthTE = parseInt(eq.width_te) || 1; + var widthTE = parseFloat(eq.width_te) || 1; // Check if this equipment is covered by a busbar (returns { top: bool, bottom: bool }) var busbarCoverage = self.isEquipmentCoveredByBusbar(eq); @@ -7406,35 +7427,34 @@ // Check if carrier has position data (use typeof to allow 0 values) if (typeof carrier._x !== 'undefined' && typeof carrier._y !== 'undefined') { - // Belegte Slots ermitteln (1-basiert) - var totalTE = parseInt(carrier.total_te) || 12; - var occupied = {}; + // Belegte Ranges ermitteln (Dezimal-TE-Unterstützung) + var totalTE = parseFloat(carrier.total_te) || 12; var lastEquipment = null; var lastEndPos = 0; + var ranges = []; carrierEquipment.forEach(function(eq) { - var pos = parseInt(eq.position_te) || 1; - var w = parseInt(eq.width_te) || 1; - var endPos = pos + w - 1; - for (var s = pos; s <= endPos; s++) { - occupied[s] = true; - } + var pos = parseFloat(eq.position_te) || 1; + var w = parseFloat(eq.width_te) || 1; + var endPos = pos + w; + ranges.push({ start: pos, end: endPos }); if (endPos > lastEndPos) { lastEndPos = endPos; lastEquipment = eq; } }); + ranges.sort(function(a, b) { return a.start - b.start; }); // Maximale zusammenhängende Lücke berechnen var maxGap = 0; - var currentGap = 0; - for (var s = 1; s <= totalTE; s++) { - if (!occupied[s]) { - currentGap++; - if (currentGap > maxGap) maxGap = currentGap; - } else { - currentGap = 0; - } - } + var gapPos = 1; + var railEnd = totalTE + 1; + ranges.forEach(function(r) { + var gap = r.start - gapPos; + if (gap > maxGap) maxGap = gap; + if (r.end > gapPos) gapPos = r.end; + }); + var endGap = railEnd - gapPos; + if (endGap > maxGap) maxGap = endGap; // Carrier-Objekt merkt sich maximale Lücke für Typ-Filter carrier._maxGap = maxGap; @@ -7868,7 +7888,7 @@ showAddBusbarDialog: function(carrierId) { var self = this; var carrier = this.carriers.find(function(c) { return String(c.id) === String(carrierId); }); - var totalTE = carrier ? (parseInt(carrier.total_te) || 12) : 12; + var totalTE = carrier ? (parseFloat(carrier.total_te) || 12) : 12; var html = '
'; html += '
'; @@ -8051,7 +8071,7 @@ showEquipmentTypeSelector: function(carrierId, types, maxGap) { var self = this; - // Kategorisiere Equipment-Typen + // Kategorisiere Equipment-Typen (category kommt aus DB) var categories = { 'schutz': { label: 'Schutzgeräte', icon: 'fa-shield', items: [] }, 'automat': { label: 'Leitungsschutz', icon: 'fa-bolt', items: [] }, @@ -8062,18 +8082,12 @@ // Sortiere Typen in Kategorien (nur wenn Breite in verfügbare Lücke passt) maxGap = maxGap || 99; types.forEach(function(t) { - var typeWidth = parseInt(t.width_te) || 1; + var typeWidth = parseFloat(t.width_te) || 1; if (typeWidth > maxGap) return; // Passt nicht in verfügbare Lücke - var ref = (t.ref || '').toUpperCase(); - var label = (t.label || '').toLowerCase(); - - if (ref.indexOf('FI') === 0 || ref === 'AFDD' || ref.indexOf('SPD') === 0) { - categories.schutz.items.push(t); - } else if (ref.indexOf('LS') === 0 || ref.indexOf('HS') === 0 || ref.indexOf('NH') === 0 || ref === 'MSS') { - categories.automat.items.push(t); - } else if (ref.indexOf('RK_') === 0 || label.indexOf('klemme') !== -1) { - categories.klemme.items.push(t); + var cat = t.category || 'steuerung'; + if (categories[cat]) { + categories[cat].items.push(t); } else { categories.steuerung.items.push(t); } @@ -8365,9 +8379,9 @@ // Returns: { top: boolean, bottom: boolean } indicating which terminals are covered isEquipmentCoveredByBusbar: function(eq) { var eqCarrierId = eq.carrier_id || eq.fk_carrier; - var eqPosTE = parseInt(eq.position_te) || 1; - var eqWidthTE = parseInt(eq.width_te) || 1; - var eqEndTE = eqPosTE + eqWidthTE - 1; + var eqPosTE = parseFloat(eq.position_te) || 1; + var eqWidthTE = parseFloat(eq.width_te) || 1; + var eqEndTE = eqPosTE + eqWidthTE; // Half-open range: [eqPosTE, eqEndTE) var result = { top: false, bottom: false }; @@ -8383,8 +8397,8 @@ var railEnd = parseInt(conn.rail_end_te) || railStart; var positionY = parseInt(conn.position_y) || 0; - // Check if equipment overlaps with busbar range - var overlaps = !(eqEndTE < railStart || eqPosTE > railEnd); + // Overlap: [eqPosTE, eqEndTE) vs [railStart, railEnd+1) + var overlaps = eqPosTE < railEnd + 1 && railStart < eqEndTE; if (!overlaps) return; // Determine if busbar is above (position_y = 0) or below (position_y > 0) @@ -8408,7 +8422,9 @@ // Try to parse terminals_config from equipment type if (eq.terminals_config) { try { - var config = JSON.parse(eq.terminals_config); + // Literale \r\n bereinigen (falls DB-Daten kaputt) + var configStr = eq.terminals_config.replace(/\\r\\n|\\r|\\n/g, ' ').replace(/\\t/g, ''); + var config = JSON.parse(configStr); // Handle both old format (inputs/outputs) and new format (terminals) if (config.terminals) { return config.terminals; @@ -8501,7 +8517,7 @@ } var isTop = terminal.pos === 'top'; - var widthTE = parseInt(eq.width_te) || 1; + var widthTE = parseFloat(eq.width_te) || 1; // Terminal im festen TE-Raster platzieren // Jeder Terminal belegt 1 TE - Index bestimmt welches TE @@ -8577,7 +8593,7 @@ this.equipment.forEach(function(eq) { if (eq._x === undefined || eq._y === undefined) return; - var eqWidth = eq._width || (self.TE_WIDTH * (parseInt(eq.width_te) || 1)); + var eqWidth = eq._width || (self.TE_WIDTH * (parseFloat(eq.width_te) || 1)); var eqHeight = eq._height || self.BLOCK_HEIGHT; var eqX1 = Math.floor((eq._x - minX) / GRID_SIZE); @@ -10456,21 +10472,26 @@ var carrier = this.carriers.find(function(c) { return parseInt(c.id) === parseInt(carrierId); }); var carrierLabel = carrier ? (carrier.label || 'Hutschiene') : 'Hutschiene'; - this.showConfirmDialog('Hutschiene löschen', 'Hutschiene "' + carrierLabel + '" wirklich löschen? Alle darauf platzierten Geräte werden ebenfalls gelöscht!', function() { + KundenKarte.showConfirm('Hutschiene löschen', 'Hutschiene "' + carrierLabel + '" wirklich löschen? Alle darauf platzierten Geräte werden ebenfalls gelöscht!', function() { $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', method: 'POST', data: { action: 'delete', - carrier_id: carrierId + carrier_id: carrierId, + token: KundenKarte.token }, dataType: 'json', success: function(response) { if (response.success) { - self.loadData(); // Reload all data + self.loadData(); + self.showMessage('Hutschiene gelöscht', 'success'); } else { - alert('Fehler: ' + (response.error || 'Unbekannter Fehler')); + self.showMessage('Fehler: ' + (response.error || 'Unbekannt'), 'error'); } + }, + error: function() { + self.showMessage('Fehler beim Löschen', 'error'); } }); }); @@ -10535,7 +10556,7 @@ // Position dialogHtml += '
'; dialogHtml += ''; - dialogHtml += '
'; // Produktauswahl mit Autocomplete @@ -10700,8 +10721,8 @@ element: $block, startX: e.pageX, startY: e.pageY, - originalTE: parseInt(eq.position_te) || 1, - originalX: parseFloat(carrier._x) + ((parseInt(eq.position_te) || 1) - 1) * this.TE_WIDTH + 2, + originalTE: parseFloat(eq.position_te) || 1, + originalX: parseFloat(carrier._x) + ((parseFloat(eq.position_te) || 1) - 1) * this.TE_WIDTH + 2, originalY: eq._y }; @@ -10724,13 +10745,14 @@ var targetCarrier = this.findClosestCarrier(mouseY, mouseX); if (!targetCarrier) targetCarrier = this.dragState.carrier; - // Calculate TE position on target carrier based on absolute X position + // TE-Position auf Ziel-Carrier berechnen (0.1er-Snap für Dezimal-Breiten) var relativeX = mouseX - targetCarrier._x; - var newTE = Math.round(relativeX / this.TE_WIDTH) + 1; + var rawTE = relativeX / this.TE_WIDTH + 1; + var newTE = Math.round(rawTE * 10) / 10; - // Clamp to valid range on TARGET carrier - var maxTE = (parseInt(targetCarrier.total_te) || 12) - (parseInt(this.dragState.equipment.width_te) || 1) + 1; - newTE = Math.max(1, Math.min(newTE, maxTE)); + // Auf gültigen Bereich auf ZIEL-Carrier begrenzen + var maxTE = (parseFloat(targetCarrier.total_te) || 12) - (parseFloat(this.dragState.equipment.width_te) || 1) + 1; + newTE = Math.max(1, Math.min(newTE, Math.round(maxTE * 10) / 10)); // Calculate visual position var newX, newY; @@ -10962,11 +10984,7 @@ showMessage: function(text, type) { var $msg = $('.schematic-message'); - if (!$msg.length) { - // Create permanent status bar with fixed height - $msg = $('
'); - $('.schematic-editor-canvas').prepend($msg); - } + if (!$msg.length) return; // Statuszeile wird in PHP erstellt // Clear any pending hide timeout if (this.messageTimeout) { @@ -10974,20 +10992,19 @@ this.messageTimeout = null; } - $msg.removeClass('success error warning info').addClass(type).text(text).css('opacity', 1); + // Atomarer Klassen-Wechsel - kein Flackern durch removeClass/addClass + $msg.attr('class', 'schematic-message ' + type).text(text); if (type === 'success' || type === 'error') { this.messageTimeout = setTimeout(function() { - // Don't hide, just clear the message text but keep the bar - $msg.removeClass('success error warning info').addClass('info').text('Bereit'); + $msg.attr('class', 'schematic-message info').text('Bereit'); }, 2500); } }, hideMessage: function() { var $msg = $('.schematic-message'); - // Don't hide, just reset to default state - $msg.removeClass('success error warning info').addClass('info').text('Bereit'); + $msg.attr('class', 'schematic-message info').text('Bereit'); }, escapeHtml: function(text) { diff --git a/js/pwa.js b/js/pwa.js index 77f6883..a62233c 100644 --- a/js/pwa.js +++ b/js/pwa.js @@ -709,7 +709,7 @@ carrierEquipment.sort((a, b) => (a.position_te || 0) - (b.position_te || 0)); const totalTe = parseInt(carrier.total_te) || 12; - const usedTe = carrierEquipment.reduce((sum, eq) => sum + (parseInt(eq.width_te) || 1), 0); + const usedTe = carrierEquipment.reduce((sum, eq) => sum + (parseFloat(eq.width_te) || 1), 0); const isFull = usedTe >= totalTe; html += ` @@ -723,8 +723,8 @@ // === Zeile 1: Terminals oben (Inputs + Top-Outputs) === carrierEquipment.forEach(eq => { - const widthTe = parseInt(eq.width_te) || 1; - const posTe = parseInt(eq.position_te) || 0; + const widthTe = parseFloat(eq.width_te) || 1; + const posTe = parseFloat(eq.position_te) || 0; const eqInputs = App.inputs ? App.inputs.filter(i => i.fk_target == eq.id) : []; const eqTopOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && o.is_top) : []; @@ -757,8 +757,8 @@ // === Zeile 2: Equipment-Blöcke === carrierEquipment.forEach(eq => { const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type); - const widthTe = parseInt(eq.width_te) || 1; - const posTe = parseInt(eq.position_te) || 0; + const widthTe = parseFloat(eq.width_te) || 1; + const posTe = parseFloat(eq.position_te) || 0; const typeLabel = type?.label_short || type?.ref || ''; const blockColor = eq.block_color || type?.color || '#3498db'; @@ -788,8 +788,8 @@ // === Zeile 3: Output-Terminals unten (Standard-Abgänge) === carrierEquipment.forEach(eq => { - const widthTe = parseInt(eq.width_te) || 1; - const posTe = parseInt(eq.position_te) || 0; + const widthTe = parseFloat(eq.width_te) || 1; + const posTe = parseFloat(eq.position_te) || 0; const eqBottomOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && !o.is_top) : []; for (let t = 0; t < widthTe; t++) { @@ -1074,26 +1074,24 @@ const totalTe = parseInt(carrier.total_te) || 12; const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrierId); - // Belegte Slots ermitteln (1-basiert) - const occupied = {}; - carrierEquipment.forEach(eq => { - const pos = parseInt(eq.position_te) || 1; - const w = parseInt(eq.width_te) || 1; - for (let s = pos; s < pos + w; s++) { - occupied[s] = true; - } - }); + // Belegte Ranges ermitteln (1-basiert, Dezimal-Breiten) + const ranges = carrierEquipment.map(eq => ({ + start: parseFloat(eq.position_te) || 1, + end: (parseFloat(eq.position_te) || 1) + (parseFloat(eq.width_te) || 1) + })).sort((a, b) => a.start - b.start); // Maximale zusammenhängende Lücke - let maxGap = 0, currentGap = 0; - for (let s = 1; s <= totalTe; s++) { - if (!occupied[s]) { - currentGap++; - if (currentGap > maxGap) maxGap = currentGap; - } else { - currentGap = 0; - } + let maxGap = 0; + let pos = 1; + const railEnd = totalTe + 1; + for (const range of ranges) { + const gap = range.start - pos; + if (gap > maxGap) maxGap = gap; + if (range.end > pos) pos = range.end; } + // Lücke nach letztem Element + const endGap = railEnd - pos; + if (endGap > maxGap) maxGap = endGap; return maxGap; } @@ -1304,26 +1302,27 @@ const carrierEquipment = App.equipment.filter(e => e.fk_carrier == App.currentCarrierId); const carrier = App.carriers.find(c => c.id == App.currentCarrierId); const totalTe = parseInt(carrier?.total_te) || 12; - const eqWidth = parseInt(type?.width_te) || 1; + const eqWidth = parseFloat(type?.width_te) || 1; - // Belegungsarray erstellen - const occupied = new Array(totalTe + 1).fill(false); - carrierEquipment.forEach(e => { - const pos = parseInt(e.position_te) || 1; - const w = parseInt(e.width_te) || 1; - for (let i = pos; i < pos + w && i <= totalTe; i++) { - occupied[i] = true; - } - }); + // Belegte Ranges ermitteln (Dezimal-TE-Unterstützung) + const ranges = carrierEquipment.map(e => ({ + start: parseFloat(e.position_te) || 1, + end: (parseFloat(e.position_te) || 1) + (parseFloat(e.width_te) || 1) + })).sort((a, b) => a.start - b.start); // Erste Lücke finden die breit genug ist let nextPos = 0; - for (let i = 1; i <= totalTe - eqWidth + 1; i++) { - let fits = true; - for (let j = 0; j < eqWidth; j++) { - if (occupied[i + j]) { fits = false; break; } + let pos = 1; + const railEnd = totalTe + 1; + for (const range of ranges) { + if (pos + eqWidth <= range.start + 0.001) { + nextPos = pos; + break; } - if (fits) { nextPos = i; break; } + if (range.end > pos) pos = range.end; + } + if (nextPos === 0 && pos + eqWidth <= railEnd + 0.001) { + nextPos = pos; } if (nextPos === 0) { diff --git a/tabs/anlagen.php b/tabs/anlagen.php index fce9bda..d463f22 100755 --- a/tabs/anlagen.php +++ b/tabs/anlagen.php @@ -745,8 +745,8 @@ if (empty($customerSystems)) { print ''; print '
'; print '
'; + print '
Bereit
'; print '
'; - print ''; print '
'; print '
'; diff --git a/tabs/contact_anlagen.php b/tabs/contact_anlagen.php index 145f072..7ef06c6 100755 --- a/tabs/contact_anlagen.php +++ b/tabs/contact_anlagen.php @@ -743,8 +743,8 @@ if (empty($customerSystems)) { print ''; print ''; print ''; + print '
Bereit
'; print '
'; - print ''; print '
'; print '';