feat: Dezimal-TE, Equipment-Kategorien, Schaltplan-Fixes

- 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 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-26 14:04:17 +01:00
parent dcd00fe844
commit 20fb9d3b05
12 changed files with 414 additions and 187 deletions

View file

@ -69,10 +69,19 @@ if ($action == 'add') {
$equipmentType->label_short = GETPOST('label_short', 'alphanohtml'); $equipmentType->label_short = GETPOST('label_short', 'alphanohtml');
$equipmentType->description = GETPOST('description', 'restricthtml'); $equipmentType->description = GETPOST('description', 'restricthtml');
$equipmentType->fk_system = GETPOSTINT('fk_system'); $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'); $equipmentType->color = GETPOST('color', 'alphanohtml');
// fk_product removed - products are selected per equipment in editor // 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->flow_direction = GETPOST('flow_direction', 'alphanohtml');
$equipmentType->terminal_position = GETPOST('terminal_position', 'alphanohtml') ?: 'both'; $equipmentType->terminal_position = GETPOST('terminal_position', 'alphanohtml') ?: 'both';
$equipmentType->picto = GETPOST('picto', 'alphanohtml'); $equipmentType->picto = GETPOST('picto', 'alphanohtml');
@ -118,10 +127,19 @@ if ($action == 'update') {
$equipmentType->label_short = GETPOST('label_short', 'alphanohtml'); $equipmentType->label_short = GETPOST('label_short', 'alphanohtml');
$equipmentType->description = GETPOST('description', 'restricthtml'); $equipmentType->description = GETPOST('description', 'restricthtml');
$equipmentType->fk_system = GETPOSTINT('fk_system'); $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'); $equipmentType->color = GETPOST('color', 'alphanohtml');
// fk_product removed - products are selected per equipment in editor // 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->flow_direction = GETPOST('flow_direction', 'alphanohtml');
$equipmentType->terminal_position = GETPOST('terminal_position', 'alphanohtml') ?: 'both'; $equipmentType->terminal_position = GETPOST('terminal_position', 'alphanohtml') ?: 'both';
$equipmentType->picto = GETPOST('picto', 'alphanohtml'); $equipmentType->picto = GETPOST('picto', 'alphanohtml');
@ -171,6 +189,7 @@ if ($action == 'copy' && $typeId > 0) {
$newType->label_short = $sourceType->label_short; $newType->label_short = $sourceType->label_short;
$newType->description = $sourceType->description; $newType->description = $sourceType->description;
$newType->fk_system = $sourceType->fk_system; $newType->fk_system = $sourceType->fk_system;
$newType->category = $sourceType->category;
$newType->width_te = $sourceType->width_te; $newType->width_te = $sourceType->width_te;
$newType->color = $sourceType->color; $newType->color = $sourceType->color;
// fk_product not copied - products are selected per equipment in editor // fk_product not copied - products are selected per equipment in editor
@ -350,6 +369,23 @@ if (in_array($action, array('create', 'edit'))) {
} }
print '</select></td></tr>'; print '</select></td></tr>';
// Category
$categories = array(
'automat' => 'Leitungsschutz',
'schutz' => 'Schutzgeräte',
'steuerung' => 'Steuerung & Sonstiges',
'klemme' => 'Klemmen',
);
print '<tr><td>'.$langs->trans('Category').'</td>';
print '<td><select name="category" class="flat minwidth200">';
foreach ($categories as $catKey => $catLabel) {
$sel = ($equipmentType->category == $catKey) ? ' selected' : '';
print '<option value="'.$catKey.'"'.$sel.'>'.dol_escape_htmltag($catLabel).'</option>';
}
print '</select>';
print ' <span class="opacitymedium">(Gruppierung im Typ-Auswahl-Dialog)</span>';
print '</td></tr>';
// Reference // Reference
print '<tr><td class="fieldrequired">'.$langs->trans('TypeRef').'</td>'; print '<tr><td class="fieldrequired">'.$langs->trans('TypeRef').'</td>';
print '<td><input type="text" name="ref" class="flat minwidth200" value="'.dol_escape_htmltag($equipmentType->ref).'" maxlength="64" required>'; print '<td><input type="text" name="ref" class="flat minwidth200" value="'.dol_escape_htmltag($equipmentType->ref).'" maxlength="64" required>';
@ -370,7 +406,7 @@ if (in_array($action, array('create', 'edit'))) {
// Width in TE // Width in TE
print '<tr><td class="fieldrequired">'.$langs->trans('WidthTE').'</td>'; print '<tr><td class="fieldrequired">'.$langs->trans('WidthTE').'</td>';
print '<td><input type="number" name="width_te" class="flat" value="'.($equipmentType->width_te ?: 1).'" min="1" max="12" required>'; print '<td><input type="number" name="width_te" class="flat" value="'.($equipmentType->width_te ?: 1).'" min="0.1" max="24" step="0.1" required>';
print ' <span class="opacitymedium">'.$langs->trans('WidthTEHelp').'</span></td></tr>'; print ' <span class="opacitymedium">'.$langs->trans('WidthTEHelp').'</span></td></tr>';
// Color // Color
@ -922,6 +958,7 @@ if (in_array($action, array('create', 'edit'))) {
print '<tr class="liste_titre">'; print '<tr class="liste_titre">';
print '<th>'.$langs->trans('TypeRef').'</th>'; print '<th>'.$langs->trans('TypeRef').'</th>';
print '<th>'.$langs->trans('TypeLabel').'</th>'; print '<th>'.$langs->trans('TypeLabel').'</th>';
print '<th>'.$langs->trans('Category').'</th>';
print '<th>'.$langs->trans('System').'</th>'; print '<th>'.$langs->trans('System').'</th>';
print '<th class="center">'.$langs->trans('WidthTE').'</th>'; print '<th class="center">'.$langs->trans('WidthTE').'</th>';
print '<th class="center">'.$langs->trans('Color').'</th>'; print '<th class="center">'.$langs->trans('Color').'</th>';
@ -930,6 +967,13 @@ if (in_array($action, array('create', 'edit'))) {
print '<th class="center">'.$langs->trans('Actions').'</th>'; print '<th class="center">'.$langs->trans('Actions').'</th>';
print '</tr>'; print '</tr>';
$categoryLabels = array(
'automat' => 'Leitungsschutz',
'schutz' => 'Schutzgeräte',
'steuerung' => 'Steuerung & Sonstiges',
'klemme' => 'Klemmen',
);
foreach ($types as $type) { foreach ($types as $type) {
print '<tr class="oddeven">'; print '<tr class="oddeven">';
@ -945,6 +989,7 @@ if (in_array($action, array('create', 'edit'))) {
} }
print '</td>'; print '</td>';
print '<td>'.dol_escape_htmltag($categoryLabels[$type->category] ?? $type->category).'</td>';
print '<td>'.dol_escape_htmltag($type->system_label).'</td>'; print '<td>'.dol_escape_htmltag($type->system_label).'</td>';
print '<td class="center">'.$type->width_te.' TE</td>'; print '<td class="center">'.$type->width_te.' TE</td>';
print '<td class="center"><span style="display:inline-block;width:24px;height:24px;background:'.($type->color ?: '#3498db').';border-radius:3px;"></span></td>'; print '<td class="center"><span style="display:inline-block;width:24px;height:24px;background:'.($type->color ?: '#3498db').';border-radius:3px;"></span></td>';
@ -972,7 +1017,7 @@ if (in_array($action, array('create', 'edit'))) {
} }
if (empty($types)) { if (empty($types)) {
print '<tr class="oddeven"><td colspan="8" class="opacitymedium">'.$langs->trans('NoRecords').'</td></tr>'; print '<tr class="oddeven"><td colspan="9" class="opacitymedium">'.$langs->trans('NoRecords').'</td></tr>';
} }
print '</table>'; print '</table>';

View file

@ -107,6 +107,7 @@ switch ($action) {
'ref' => $t->ref, 'ref' => $t->ref,
'label' => $t->label, 'label' => $t->label,
'label_short' => $t->label_short, 'label_short' => $t->label_short,
'category' => $t->category ?: 'steuerung',
'width_te' => $t->width_te, 'width_te' => $t->width_te,
'color' => $t->color, 'color' => $t->color,
'picto' => $t->picto 'picto' => $t->picto
@ -286,8 +287,8 @@ switch ($action) {
$equipment->fk_carrier = $carrierId; $equipment->fk_carrier = $carrierId;
$equipment->fk_equipment_type = GETPOSTINT('type_id'); $equipment->fk_equipment_type = GETPOSTINT('type_id');
$equipment->label = GETPOST('label', 'alphanohtml'); $equipment->label = GETPOST('label', 'alphanohtml');
$equipment->position_te = GETPOSTINT('position_te'); $equipment->position_te = floatval(GETPOST('position_te', 'alpha'));
$equipment->width_te = GETPOSTINT('width_te'); $equipment->width_te = floatval(GETPOST('width_te', 'alpha'));
$equipment->fk_product = GETPOSTINT('fk_product'); $equipment->fk_product = GETPOSTINT('fk_product');
$equipment->fk_protection = GETPOSTINT('fk_protection'); $equipment->fk_protection = GETPOSTINT('fk_protection');
$equipment->protection_label = GETPOST('protection_label', 'alphanohtml'); $equipment->protection_label = GETPOST('protection_label', 'alphanohtml');
@ -372,8 +373,8 @@ switch ($action) {
break; break;
} }
if ($equipment->fetch($equipmentId) > 0) { if ($equipment->fetch($equipmentId) > 0) {
$newPosition = GETPOSTINT('position_te'); $newPosition = floatval(GETPOST('position_te', 'alpha'));
$newWidth = GETPOSTINT('width_te') ?: $equipment->width_te; $newWidth = floatval(GETPOST('width_te', 'alpha')) ?: $equipment->width_te;
// Check if new position is available (excluding current equipment) // Check if new position is available (excluding current equipment)
if ($newPosition != $equipment->position_te || $newWidth != $equipment->width_te) { if ($newPosition != $equipment->position_te || $newWidth != $equipment->width_te) {
@ -451,7 +452,7 @@ switch ($action) {
break; break;
} }
if ($equipment->fetch($equipmentId) > 0) { if ($equipment->fetch($equipmentId) > 0) {
$newPosition = GETPOSTINT('position_te'); $newPosition = floatval(GETPOST('position_te', 'alpha'));
// Check if new position is available // Check if new position is available
$carrier = new EquipmentCarrier($db); $carrier = new EquipmentCarrier($db);
@ -500,7 +501,7 @@ switch ($action) {
} }
if ($equipment->fetch($equipmentId) > 0) { if ($equipment->fetch($equipmentId) > 0) {
$newCarrierId = GETPOSTINT('carrier_id'); $newCarrierId = GETPOSTINT('carrier_id');
$newPosition = GETPOSTINT('position_te') ?: 1; $newPosition = floatval(GETPOST('position_te', 'alpha')) ?: 1;
// Get old carrier for label pattern check // Get old carrier for label pattern check
$oldCarrier = new EquipmentCarrier($db); $oldCarrier = new EquipmentCarrier($db);
@ -608,6 +609,30 @@ switch ($action) {
// Fetch the new equipment to return its data // Fetch the new equipment to return its data
$newEquipment = new Equipment($db); $newEquipment = new Equipment($db);
if ($newEquipment->fetch($newId) > 0) { 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( $response['equipment'] = array(
'id' => $newEquipment->id, 'id' => $newEquipment->id,
'fk_carrier' => $newEquipment->fk_carrier, 'fk_carrier' => $newEquipment->fk_carrier,
@ -617,14 +642,22 @@ switch ($action) {
'type_color' => $newEquipment->type_color, 'type_color' => $newEquipment->type_color,
'type_ref' => $newEquipment->type_ref, 'type_ref' => $newEquipment->type_ref,
'type_icon_file' => $newEquipment->type_icon_file, '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, 'terminals_config' => $newEquipment->terminals_config,
'type_fields' => $typeFields,
'label' => $newEquipment->label, 'label' => $newEquipment->label,
'position_te' => $newEquipment->position_te, 'position_te' => $newEquipment->position_te,
'width_te' => $newEquipment->width_te, 'width_te' => $newEquipment->width_te,
'block_label' => $newEquipment->getBlockLabel(), 'block_label' => $newEquipment->getBlockLabel(),
'block_color' => $newEquipment->getBlockColor(), 'block_color' => $newEquipment->getBlockColor(),
'field_values' => $newEquipment->getFieldValues(), '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 // Audit log
@ -658,7 +691,7 @@ switch ($action) {
break; break;
} }
if ($equipment->fetch($equipmentId) > 0) { if ($equipment->fetch($equipmentId) > 0) {
$newPosition = GETPOSTINT('position_te'); $newPosition = floatval(GETPOST('position_te', 'alpha'));
$carrier = new EquipmentCarrier($db); $carrier = new EquipmentCarrier($db);
if ($carrier->fetch($equipment->fk_carrier) > 0) { if ($carrier->fetch($equipment->fk_carrier) > 0) {

View file

@ -68,6 +68,12 @@ switch ($action) {
'type_id' => $eq->fk_equipment_type, 'type_id' => $eq->fk_equipment_type,
'type_label' => $eq->type_label, 'type_label' => $eq->type_label,
'type_label_short' => $eq->type_label_short, '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, 'type_color' => $eq->type_color,
'label' => $eq->label, 'label' => $eq->label,
'position_te' => $eq->position_te, 'position_te' => $eq->position_te,

View file

@ -99,8 +99,8 @@ class Equipment extends CommonObject
$sql .= ", ".((int) $this->fk_carrier); $sql .= ", ".((int) $this->fk_carrier);
$sql .= ", ".((int) $this->fk_equipment_type); $sql .= ", ".((int) $this->fk_equipment_type);
$sql .= ", ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL"); $sql .= ", ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
$sql .= ", ".((int) $this->position_te); $sql .= ", ".floatval($this->position_te);
$sql .= ", ".((int) $this->width_te); $sql .= ", ".floatval($this->width_te);
$sql .= ", ".($this->field_values ? "'".$this->db->escape($this->field_values)."'" : "NULL"); $sql .= ", ".($this->field_values ? "'".$this->db->escape($this->field_values)."'" : "NULL");
$sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL"); $sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", ".($this->fk_protection > 0 ? ((int) $this->fk_protection) : "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_carrier = ".((int) $this->fk_carrier);
$sql .= ", fk_equipment_type = ".((int) $this->fk_equipment_type); $sql .= ", fk_equipment_type = ".((int) $this->fk_equipment_type);
$sql .= ", label = ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL"); $sql .= ", label = ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
$sql .= ", position_te = ".((int) $this->position_te); $sql .= ", position_te = ".floatval($this->position_te);
$sql .= ", width_te = ".((int) $this->width_te); $sql .= ", width_te = ".floatval($this->width_te);
$sql .= ", field_values = ".($this->field_values ? "'".$this->db->escape($this->field_values)."'" : "NULL"); $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_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", fk_protection = ".($this->fk_protection > 0 ? ((int) $this->fk_protection) : "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 = new Equipment($this->db);
$newEquipment->fk_carrier = $carrierId > 0 ? $carrierId : $this->fk_carrier; $newEquipment->fk_carrier = $carrierId > 0 ? $carrierId : $this->fk_carrier;
$newEquipment->fk_equipment_type = $this->fk_equipment_type; $newEquipment->fk_equipment_type = $this->fk_equipment_type;
$newEquipment->label = $this->label;
$newEquipment->width_te = $this->width_te; $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->field_values = $this->field_values;
$newEquipment->fk_product = $this->fk_product; $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->note_private = $this->note_private;
$newEquipment->status = 1; $newEquipment->status = 1;

View file

@ -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)) { if (empty($this->equipment)) {
$this->fetchEquipment(); $this->fetchEquipment();
} }
foreach ($this->equipment as $eq) { foreach ($this->equipment as $eq) {
for ($i = $eq->position_te; $i < $eq->position_te + $eq->width_te; $i++) { $ranges[] = array(
$occupied[] = $i; '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() 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() 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 * @param float $width Benötigte Breite in TE
* @return int Position (1-based) or -1 if no space * @return float Position (1-basiert) oder -1 wenn kein Platz
*/ */
public function getNextFreePosition($width = 1) 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) $pos = 1.0;
for ($pos = 1; $pos <= $this->total_te - $width + 1; $pos++) { foreach ($ranges as $range) {
$fits = true; // Passt in die Lücke vor diesem Element?
for ($i = $pos; $i < $pos + $width; $i++) { if ($pos + $width <= $range['start'] + 0.001) {
if (in_array($i, $occupied)) {
$fits = false;
break;
}
}
if ($fits) {
return $pos; 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 float $position Startposition (1-basiert)
* @param int $width Width in TE * @param float $width Breite in TE
* @param int $excludeEquipmentId Equipment ID to exclude (for updates) * @param int $excludeEquipmentId Equipment-ID zum Ausschließen (für Updates)
* @return bool True if position is available * @return bool True wenn Position frei
*/ */
public function isPositionAvailable($position, $width, $excludeEquipmentId = 0) public function isPositionAvailable($position, $width, $excludeEquipmentId = 0)
{ {
// Check bounds (positions are 1-based) $position = floatval($position);
if ($position < 1 || $position + $width - 1 > $this->total_te) { $width = floatval($width);
// Grenzen prüfen (Position 1 = erstes TE)
if ($position < 1 || $position + $width > floatval($this->total_te) + 1 + 0.001) {
return false; return false;
} }
@ -405,19 +426,19 @@ class EquipmentCarrier extends CommonObject
$this->fetchEquipment(); $this->fetchEquipment();
} }
$newEnd = $position + $width;
foreach ($this->equipment as $eq) { foreach ($this->equipment as $eq) {
if ($excludeEquipmentId > 0 && $eq->id == $excludeEquipmentId) { if ($excludeEquipmentId > 0 && $eq->id == $excludeEquipmentId) {
continue; continue;
} }
// Check for overlap // Overlap-Prüfung mit Half-Open-Ranges: [start, end)
$eqStart = $eq->position_te; $eqStart = floatval($eq->position_te);
$eqEnd = $eq->position_te + $eq->width_te - 1; $eqEnd = $eqStart + floatval($eq->width_te);
$newStart = $position;
$newEnd = $position + $width - 1;
if ($newStart <= $eqEnd && $newEnd >= $eqStart) { if ($position < $eqEnd - 0.001 && $eqStart < $newEnd - 0.001) {
return false; // Overlap return false;
} }
} }

View file

@ -21,6 +21,7 @@ class EquipmentType extends CommonObject
public $label_short; public $label_short;
public $description; public $description;
public $fk_system; public $fk_system;
public $category = 'steuerung'; // automat, schutz, steuerung, klemme
// Equipment-spezifische Felder // Equipment-spezifische Felder
public $width_te = 1; public $width_te = 1;
@ -78,7 +79,7 @@ class EquipmentType extends CommonObject
$this->db->begin(); $this->db->begin();
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; $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 .= " width_te, color, fk_product, terminals_config, flow_direction, terminal_position,";
$sql .= " picto, icon_file, block_image, is_system, position, active,"; $sql .= " picto, icon_file, block_image, is_system, position, active,";
$sql .= " date_creation, fk_user_creat"; $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->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
$sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL"); $sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
$sql .= ", ".((int) $this->fk_system); $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->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL"); $sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", ".($this->terminals_config ? "'".$this->db->escape($this->terminals_config)."'" : "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->label_short = $obj->label_short;
$this->description = $obj->description; $this->description = $obj->description;
$this->fk_system = $obj->fk_system; $this->fk_system = $obj->fk_system;
$this->category = $obj->category ?: 'steuerung';
$this->width_te = $obj->width_te; $this->width_te = $obj->width_te;
$this->color = $obj->color; $this->color = $obj->color;
$this->fk_product = $obj->fk_product; $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 .= ", label_short = ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
$sql .= ", description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL"); $sql .= ", description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
$sql .= ", fk_system = ".((int) $this->fk_system); $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 .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "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 .= ", 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 = $obj->label;
$type->label_short = $obj->label_short; $type->label_short = $obj->label_short;
$type->fk_system = $obj->fk_system; $type->fk_system = $obj->fk_system;
$type->category = $obj->category ?: 'steuerung';
$type->width_te = $obj->width_te; $type->width_te = $obj->width_te;
$type->color = $obj->color; $type->color = $obj->color;
$type->fk_product = $obj->fk_product; $type->fk_product = $obj->fk_product;

View file

@ -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' $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' // 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 // Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt'; //$this->url_last_version = 'http://www.example.com/versionmodule.txt';
@ -615,6 +615,12 @@ class modKundenKarte extends DolibarrModules
// v4.1.0: Graph-Positionen speichern // v4.1.0: Graph-Positionen speichern
$this->migrate_v410_graph_positions(); $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"); $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. * Function called when module is disabled.
* Remove from database constants, boxes and permissions from Dolibarr database. * Remove from database constants, boxes and permissions from Dolibarr database.

View file

@ -2260,8 +2260,8 @@ body.kundenkarte-drag-active * {
/* Messages - Fixed height status bar */ /* Messages - Fixed height status bar */
.schematic-message { .schematic-message {
padding: 6px 15px !important; padding: 6px 15px !important;
margin-bottom: 10px !important; margin-bottom: 0 !important;
border-radius: 4px !important; border-radius: 0 !important;
font-size: 12px !important; font-size: 12px !important;
height: 28px !important; height: 28px !important;
min-height: 28px !important; min-height: 28px !important;
@ -2269,30 +2269,40 @@ body.kundenkarte-drag-active * {
line-height: 16px !important; line-height: 16px !important;
overflow: hidden !important; overflow: hidden !important;
box-sizing: border-box !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 { .schematic-message.info {
background: #2d4a5e !important; background: #2d4a5e !important;
color: #7ec8e3 !important; color: #7ec8e3 !important;
border: 1px solid #3498db !important; border-left: 1px solid #3498db !important;
border-right: 1px solid #3498db !important;
} }
.schematic-message.success { .schematic-message.success {
background: #2d5a3d !important; background: #2d5a3d !important;
color: #7ee8a0 !important; color: #7ee8a0 !important;
border: 1px solid #27ae60 !important; border-left: 1px solid #27ae60 !important;
border-right: 1px solid #27ae60 !important;
} }
.schematic-message.warning { .schematic-message.warning {
background: #5a5a2d !important; background: #5a5a2d !important;
color: #e8e87e !important; color: #e8e87e !important;
border: 1px solid #f1c40f !important; border-left: 1px solid #f1c40f !important;
border-right: 1px solid #f1c40f !important;
} }
.schematic-message.error { .schematic-message.error {
background: #5a2d2d !important; background: #5a2d2d !important;
color: #e87e7e !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 */ /* Terminal color by phase */

View file

@ -1756,10 +1756,14 @@
html += '<div class="kundenkarte-carrier-svg-container" style="width:' + totalWidth + 'px;">'; html += '<div class="kundenkarte-carrier-svg-container" style="width:' + totalWidth + 'px;">';
html += '<svg class="kundenkarte-carrier-svg" width="' + totalWidth + '" height="' + this.BLOCK_HEIGHT + '" viewBox="0 0 ' + totalWidth + ' ' + this.BLOCK_HEIGHT + '">'; html += '<svg class="kundenkarte-carrier-svg" width="' + totalWidth + '" height="' + this.BLOCK_HEIGHT + '" viewBox="0 0 ' + totalWidth + ' ' + this.BLOCK_HEIGHT + '">';
// Background grid (TE markers) // TE-Raster (ganzzahlige TEs deutlich, 0.5er subtil)
for (var i = 0; i <= carrier.total_te; i++) { for (var i = 0; i <= carrier.total_te; i++) {
var x = i * this.TE_WIDTH; var x = i * this.TE_WIDTH;
html += '<line x1="' + x + '" y1="0" x2="' + x + '" y2="' + this.BLOCK_HEIGHT + '" stroke="#ddd" stroke-width="0.5"/>'; html += '<line x1="' + x + '" y1="0" x2="' + x + '" y2="' + this.BLOCK_HEIGHT + '" stroke="#ddd" stroke-width="1.5"/>';
if (i < carrier.total_te) {
var halfX = x + this.TE_WIDTH / 2;
html += '<line x1="' + halfX + '" y1="5" x2="' + halfX + '" y2="' + (this.BLOCK_HEIGHT - 5) + '" stroke="#ddd" stroke-width="0.3" opacity="0.4"/>';
}
} }
// Render equipment blocks // Render equipment blocks
@ -1824,11 +1828,16 @@
}, },
getOccupiedSlots: function(equipment) { getOccupiedSlots: function(equipment) {
// Range-basierte Belegung (unterstützt Dezimal-TE wie 4.5)
var slots = {}; var slots = {};
if (equipment) { if (equipment) {
equipment.forEach(function(eq) { equipment.forEach(function(eq) {
for (var i = 0; i < eq.width_te; i++) { var start = parseFloat(eq.position_te) || 1;
slots[eq.position_te + i] = true; 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 // Clear all connections
$(document).off('click.clearConns').on('click.clearConns', '.schematic-clear-connections', function(e) { $(document).off('click.clearConns').on('click.clearConns', '.schematic-clear-connections', function(e) {
e.preventDefault(); 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(); self.clearAllConnections();
}); });
}); });
@ -5408,7 +5417,7 @@
var newEndTE = newStartTE + data.busbarWidthTE - 1; var newEndTE = newStartTE + data.busbarWidthTE - 1;
// Clamp to carrier bounds (shift if needed to fit) // 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) { if (newStartTE < 1) {
newStartTE = 1; newStartTE = 1;
newEndTE = newStartTE + data.busbarWidthTE - 1; newEndTE = newStartTE + data.busbarWidthTE - 1;
@ -5931,7 +5940,7 @@
var self = this; var self = this;
var baseUrl = $('body').data('base-url') || ''; 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({ $.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php',
method: 'POST', method: 'POST',
@ -5962,7 +5971,7 @@
if (!conn) return; if (!conn) return;
var carrier = this.carriers.find(function(c) { return String(c.id) === String(conn.fk_carrier); }); 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 = '<div class="schematic-dialog-overlay" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:100000;"></div>'; var html = '<div class="schematic-dialog-overlay" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:100000;"></div>';
html += '<div class="schematic-dialog" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#2d2d44;border:1px solid #555;border-radius:8px;padding:20px;z-index:100001;min-width:350px;">'; html += '<div class="schematic-dialog" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#2d2d44;border:1px solid #555;border-radius:8px;padding:20px;z-index:100001;min-width:350px;">';
@ -6474,10 +6483,16 @@
html += self.escapeHtml(carrier.label || 'H' + (carrierIdx + 1)); html += self.escapeHtml(carrier.label || 'H' + (carrierIdx + 1));
html += '</text>'; html += '</text>';
// TE markers on the rail // TE-Markierungen auf der Schiene (ganzzahlige TEs deutlich, 0.5er-Zwischenmarken subtil)
for (var te = 0; te <= (carrier.total_te || 12); te++) { var totalTECount = carrier.total_te || 12;
for (var te = 0; te <= totalTECount; te++) {
var teX = x + te * self.TE_WIDTH; var teX = x + te * self.TE_WIDTH;
html += '<line x1="' + teX + '" y1="' + railY + '" x2="' + teX + '" y2="' + (railY + self.RAIL_HEIGHT) + '" stroke="#555" stroke-width="0.5"/>'; html += '<line x1="' + teX + '" y1="' + railY + '" x2="' + teX + '" y2="' + (railY + self.RAIL_HEIGHT) + '" stroke="#999" stroke-width="1.5"/>';
// 0.5-TE Zwischenmarkierung
if (te < totalTECount) {
var halfX = teX + self.TE_WIDTH / 2;
html += '<line x1="' + halfX + '" y1="' + (railY + 3) + '" x2="' + halfX + '" y2="' + (railY + self.RAIL_HEIGHT - 3) + '" stroke="#555" stroke-width="0.5"/>';
}
} }
}); });
}); });
@ -6506,9 +6521,15 @@
html += self.escapeHtml(carrier.label || 'Hutschiene ' + (idx + 1)); html += self.escapeHtml(carrier.label || 'Hutschiene ' + (idx + 1));
html += '</text>'; html += '</text>';
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; var teX = x + te * self.TE_WIDTH;
html += '<line x1="' + teX + '" y1="' + railY + '" x2="' + teX + '" y2="' + (railY + self.RAIL_HEIGHT) + '" stroke="#555" stroke-width="0.5"/>'; html += '<line x1="' + teX + '" y1="' + railY + '" x2="' + teX + '" y2="' + (railY + self.RAIL_HEIGHT) + '" stroke="#999" stroke-width="1.5"/>';
if (te < totalTECount) {
var halfX = teX + self.TE_WIDTH / 2;
html += '<line x1="' + halfX + '" y1="' + (railY + 3) + '" x2="' + halfX + '" y2="' + (railY + self.RAIL_HEIGHT - 3) + '" stroke="#555" stroke-width="0.5"/>';
}
} }
}); });
} }
@ -6538,7 +6559,7 @@
var blockWidth = (eq.width_te || 1) * self.TE_WIDTH - 4; var blockWidth = (eq.width_te || 1) * self.TE_WIDTH - 4;
var blockHeight = self.BLOCK_HEIGHT; 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) // Position blocks centered on the rail (Hutschiene)
// Rail is at carrier._y, block center should align with rail center // Rail is at carrier._y, block center should align with rail center
var railCenterY = carrier._y + self.RAIL_HEIGHT / 2; var railCenterY = carrier._y + self.RAIL_HEIGHT / 2;
@ -6666,7 +6687,7 @@
var topTerminals = terminals.filter(function(t) { return t.pos === 'top'; }); var topTerminals = terminals.filter(function(t) { return t.pos === 'top'; });
var bottomTerminals = terminals.filter(function(t) { return t.pos === 'bottom'; }); 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 }) // Check if this equipment is covered by a busbar (returns { top: bool, bottom: bool })
var busbarCoverage = self.isEquipmentCoveredByBusbar(eq); var busbarCoverage = self.isEquipmentCoveredByBusbar(eq);
@ -7406,35 +7427,34 @@
// Check if carrier has position data (use typeof to allow 0 values) // Check if carrier has position data (use typeof to allow 0 values)
if (typeof carrier._x !== 'undefined' && typeof carrier._y !== 'undefined') { if (typeof carrier._x !== 'undefined' && typeof carrier._y !== 'undefined') {
// Belegte Slots ermitteln (1-basiert) // Belegte Ranges ermitteln (Dezimal-TE-Unterstützung)
var totalTE = parseInt(carrier.total_te) || 12; var totalTE = parseFloat(carrier.total_te) || 12;
var occupied = {};
var lastEquipment = null; var lastEquipment = null;
var lastEndPos = 0; var lastEndPos = 0;
var ranges = [];
carrierEquipment.forEach(function(eq) { carrierEquipment.forEach(function(eq) {
var pos = parseInt(eq.position_te) || 1; var pos = parseFloat(eq.position_te) || 1;
var w = parseInt(eq.width_te) || 1; var w = parseFloat(eq.width_te) || 1;
var endPos = pos + w - 1; var endPos = pos + w;
for (var s = pos; s <= endPos; s++) { ranges.push({ start: pos, end: endPos });
occupied[s] = true;
}
if (endPos > lastEndPos) { if (endPos > lastEndPos) {
lastEndPos = endPos; lastEndPos = endPos;
lastEquipment = eq; lastEquipment = eq;
} }
}); });
ranges.sort(function(a, b) { return a.start - b.start; });
// Maximale zusammenhängende Lücke berechnen // Maximale zusammenhängende Lücke berechnen
var maxGap = 0; var maxGap = 0;
var currentGap = 0; var gapPos = 1;
for (var s = 1; s <= totalTE; s++) { var railEnd = totalTE + 1;
if (!occupied[s]) { ranges.forEach(function(r) {
currentGap++; var gap = r.start - gapPos;
if (currentGap > maxGap) maxGap = currentGap; if (gap > maxGap) maxGap = gap;
} else { if (r.end > gapPos) gapPos = r.end;
currentGap = 0; });
} var endGap = railEnd - gapPos;
} if (endGap > maxGap) maxGap = endGap;
// Carrier-Objekt merkt sich maximale Lücke für Typ-Filter // Carrier-Objekt merkt sich maximale Lücke für Typ-Filter
carrier._maxGap = maxGap; carrier._maxGap = maxGap;
@ -7868,7 +7888,7 @@
showAddBusbarDialog: function(carrierId) { showAddBusbarDialog: function(carrierId) {
var self = this; var self = this;
var carrier = this.carriers.find(function(c) { return String(c.id) === String(carrierId); }); 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 = '<div class="schematic-dialog-overlay" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:100000;"></div>'; var html = '<div class="schematic-dialog-overlay" style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.6);z-index:100000;"></div>';
html += '<div class="schematic-dialog" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#2d2d44;border:1px solid #555;border-radius:8px;padding:20px;z-index:100001;min-width:350px;">'; html += '<div class="schematic-dialog" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#2d2d44;border:1px solid #555;border-radius:8px;padding:20px;z-index:100001;min-width:350px;">';
@ -8051,7 +8071,7 @@
showEquipmentTypeSelector: function(carrierId, types, maxGap) { showEquipmentTypeSelector: function(carrierId, types, maxGap) {
var self = this; var self = this;
// Kategorisiere Equipment-Typen // Kategorisiere Equipment-Typen (category kommt aus DB)
var categories = { var categories = {
'schutz': { label: 'Schutzgeräte', icon: 'fa-shield', items: [] }, 'schutz': { label: 'Schutzgeräte', icon: 'fa-shield', items: [] },
'automat': { label: 'Leitungsschutz', icon: 'fa-bolt', 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) // Sortiere Typen in Kategorien (nur wenn Breite in verfügbare Lücke passt)
maxGap = maxGap || 99; maxGap = maxGap || 99;
types.forEach(function(t) { 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 if (typeWidth > maxGap) return; // Passt nicht in verfügbare Lücke
var ref = (t.ref || '').toUpperCase(); var cat = t.category || 'steuerung';
var label = (t.label || '').toLowerCase(); if (categories[cat]) {
categories[cat].items.push(t);
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);
} else { } else {
categories.steuerung.items.push(t); categories.steuerung.items.push(t);
} }
@ -8365,9 +8379,9 @@
// Returns: { top: boolean, bottom: boolean } indicating which terminals are covered // Returns: { top: boolean, bottom: boolean } indicating which terminals are covered
isEquipmentCoveredByBusbar: function(eq) { isEquipmentCoveredByBusbar: function(eq) {
var eqCarrierId = eq.carrier_id || eq.fk_carrier; var eqCarrierId = eq.carrier_id || eq.fk_carrier;
var eqPosTE = parseInt(eq.position_te) || 1; var eqPosTE = parseFloat(eq.position_te) || 1;
var eqWidthTE = parseInt(eq.width_te) || 1; var eqWidthTE = parseFloat(eq.width_te) || 1;
var eqEndTE = eqPosTE + eqWidthTE - 1; var eqEndTE = eqPosTE + eqWidthTE; // Half-open range: [eqPosTE, eqEndTE)
var result = { top: false, bottom: false }; var result = { top: false, bottom: false };
@ -8383,8 +8397,8 @@
var railEnd = parseInt(conn.rail_end_te) || railStart; var railEnd = parseInt(conn.rail_end_te) || railStart;
var positionY = parseInt(conn.position_y) || 0; var positionY = parseInt(conn.position_y) || 0;
// Check if equipment overlaps with busbar range // Overlap: [eqPosTE, eqEndTE) vs [railStart, railEnd+1)
var overlaps = !(eqEndTE < railStart || eqPosTE > railEnd); var overlaps = eqPosTE < railEnd + 1 && railStart < eqEndTE;
if (!overlaps) return; if (!overlaps) return;
// Determine if busbar is above (position_y = 0) or below (position_y > 0) // 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 // Try to parse terminals_config from equipment type
if (eq.terminals_config) { if (eq.terminals_config) {
try { 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) // Handle both old format (inputs/outputs) and new format (terminals)
if (config.terminals) { if (config.terminals) {
return config.terminals; return config.terminals;
@ -8501,7 +8517,7 @@
} }
var isTop = terminal.pos === 'top'; 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 // Terminal im festen TE-Raster platzieren
// Jeder Terminal belegt 1 TE - Index bestimmt welches TE // Jeder Terminal belegt 1 TE - Index bestimmt welches TE
@ -8577,7 +8593,7 @@
this.equipment.forEach(function(eq) { this.equipment.forEach(function(eq) {
if (eq._x === undefined || eq._y === undefined) return; 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 eqHeight = eq._height || self.BLOCK_HEIGHT;
var eqX1 = Math.floor((eq._x - minX) / GRID_SIZE); 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 carrier = this.carriers.find(function(c) { return parseInt(c.id) === parseInt(carrierId); });
var carrierLabel = carrier ? (carrier.label || 'Hutschiene') : 'Hutschiene'; 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({ $.ajax({
url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php', url: baseUrl + '/custom/kundenkarte/ajax/equipment_carrier.php',
method: 'POST', method: 'POST',
data: { data: {
action: 'delete', action: 'delete',
carrier_id: carrierId carrier_id: carrierId,
token: KundenKarte.token
}, },
dataType: 'json', dataType: 'json',
success: function(response) { success: function(response) {
if (response.success) { if (response.success) {
self.loadData(); // Reload all data self.loadData();
self.showMessage('Hutschiene gelöscht', 'success');
} else { } 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 // Position
dialogHtml += '<div style="margin-bottom:12px;">'; dialogHtml += '<div style="margin-bottom:12px;">';
dialogHtml += '<label style="display:block;color:#aaa;font-size:12px;margin-bottom:4px;">Position (TE):</label>'; dialogHtml += '<label style="display:block;color:#aaa;font-size:12px;margin-bottom:4px;">Position (TE):</label>';
dialogHtml += '<input type="number" class="edit-equipment-position" value="' + (eq.position_te || 1) + '" min="1" ' + dialogHtml += '<input type="number" class="edit-equipment-position" value="' + (eq.position_te || 1) + '" min="0.1" step="0.1" ' +
'style="width:100%;padding:8px;border:1px solid #555;border-radius:4px;background:#1e1e1e;color:#fff;box-sizing:border-box;"/></div>'; 'style="width:100%;padding:8px;border:1px solid #555;border-radius:4px;background:#1e1e1e;color:#fff;box-sizing:border-box;"/></div>';
// Produktauswahl mit Autocomplete // Produktauswahl mit Autocomplete
@ -10700,8 +10721,8 @@
element: $block, element: $block,
startX: e.pageX, startX: e.pageX,
startY: e.pageY, startY: e.pageY,
originalTE: parseInt(eq.position_te) || 1, originalTE: parseFloat(eq.position_te) || 1,
originalX: parseFloat(carrier._x) + ((parseInt(eq.position_te) || 1) - 1) * this.TE_WIDTH + 2, originalX: parseFloat(carrier._x) + ((parseFloat(eq.position_te) || 1) - 1) * this.TE_WIDTH + 2,
originalY: eq._y originalY: eq._y
}; };
@ -10724,13 +10745,14 @@
var targetCarrier = this.findClosestCarrier(mouseY, mouseX); var targetCarrier = this.findClosestCarrier(mouseY, mouseX);
if (!targetCarrier) targetCarrier = this.dragState.carrier; 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 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 // Auf gültigen Bereich auf ZIEL-Carrier begrenzen
var maxTE = (parseInt(targetCarrier.total_te) || 12) - (parseInt(this.dragState.equipment.width_te) || 1) + 1; var maxTE = (parseFloat(targetCarrier.total_te) || 12) - (parseFloat(this.dragState.equipment.width_te) || 1) + 1;
newTE = Math.max(1, Math.min(newTE, maxTE)); newTE = Math.max(1, Math.min(newTE, Math.round(maxTE * 10) / 10));
// Calculate visual position // Calculate visual position
var newX, newY; var newX, newY;
@ -10962,11 +10984,7 @@
showMessage: function(text, type) { showMessage: function(text, type) {
var $msg = $('.schematic-message'); var $msg = $('.schematic-message');
if (!$msg.length) { if (!$msg.length) return; // Statuszeile wird in PHP erstellt
// Create permanent status bar with fixed height
$msg = $('<div class="schematic-message" style="min-height:28px;display:block !important;visibility:visible;"></div>');
$('.schematic-editor-canvas').prepend($msg);
}
// Clear any pending hide timeout // Clear any pending hide timeout
if (this.messageTimeout) { if (this.messageTimeout) {
@ -10974,20 +10992,19 @@
this.messageTimeout = null; 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') { if (type === 'success' || type === 'error') {
this.messageTimeout = setTimeout(function() { this.messageTimeout = setTimeout(function() {
// Don't hide, just clear the message text but keep the bar $msg.attr('class', 'schematic-message info').text('Bereit');
$msg.removeClass('success error warning info').addClass('info').text('Bereit');
}, 2500); }, 2500);
} }
}, },
hideMessage: function() { hideMessage: function() {
var $msg = $('.schematic-message'); var $msg = $('.schematic-message');
// Don't hide, just reset to default state $msg.attr('class', 'schematic-message info').text('Bereit');
$msg.removeClass('success error warning info').addClass('info').text('Bereit');
}, },
escapeHtml: function(text) { escapeHtml: function(text) {

View file

@ -709,7 +709,7 @@
carrierEquipment.sort((a, b) => (a.position_te || 0) - (b.position_te || 0)); carrierEquipment.sort((a, b) => (a.position_te || 0) - (b.position_te || 0));
const totalTe = parseInt(carrier.total_te) || 12; 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; const isFull = usedTe >= totalTe;
html += ` html += `
@ -723,8 +723,8 @@
// === Zeile 1: Terminals oben (Inputs + Top-Outputs) === // === Zeile 1: Terminals oben (Inputs + Top-Outputs) ===
carrierEquipment.forEach(eq => { carrierEquipment.forEach(eq => {
const widthTe = parseInt(eq.width_te) || 1; const widthTe = parseFloat(eq.width_te) || 1;
const posTe = parseInt(eq.position_te) || 0; const posTe = parseFloat(eq.position_te) || 0;
const eqInputs = App.inputs ? App.inputs.filter(i => i.fk_target == eq.id) : []; 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) : []; 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 === // === Zeile 2: Equipment-Blöcke ===
carrierEquipment.forEach(eq => { carrierEquipment.forEach(eq => {
const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type); const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type);
const widthTe = parseInt(eq.width_te) || 1; const widthTe = parseFloat(eq.width_te) || 1;
const posTe = parseInt(eq.position_te) || 0; const posTe = parseFloat(eq.position_te) || 0;
const typeLabel = type?.label_short || type?.ref || ''; const typeLabel = type?.label_short || type?.ref || '';
const blockColor = eq.block_color || type?.color || '#3498db'; const blockColor = eq.block_color || type?.color || '#3498db';
@ -788,8 +788,8 @@
// === Zeile 3: Output-Terminals unten (Standard-Abgänge) === // === Zeile 3: Output-Terminals unten (Standard-Abgänge) ===
carrierEquipment.forEach(eq => { carrierEquipment.forEach(eq => {
const widthTe = parseInt(eq.width_te) || 1; const widthTe = parseFloat(eq.width_te) || 1;
const posTe = parseInt(eq.position_te) || 0; const posTe = parseFloat(eq.position_te) || 0;
const eqBottomOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && !o.is_top) : []; const eqBottomOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && !o.is_top) : [];
for (let t = 0; t < widthTe; t++) { for (let t = 0; t < widthTe; t++) {
@ -1074,26 +1074,24 @@
const totalTe = parseInt(carrier.total_te) || 12; const totalTe = parseInt(carrier.total_te) || 12;
const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrierId); const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrierId);
// Belegte Slots ermitteln (1-basiert) // Belegte Ranges ermitteln (1-basiert, Dezimal-Breiten)
const occupied = {}; const ranges = carrierEquipment.map(eq => ({
carrierEquipment.forEach(eq => { start: parseFloat(eq.position_te) || 1,
const pos = parseInt(eq.position_te) || 1; end: (parseFloat(eq.position_te) || 1) + (parseFloat(eq.width_te) || 1)
const w = parseInt(eq.width_te) || 1; })).sort((a, b) => a.start - b.start);
for (let s = pos; s < pos + w; s++) {
occupied[s] = true;
}
});
// Maximale zusammenhängende Lücke // Maximale zusammenhängende Lücke
let maxGap = 0, currentGap = 0; let maxGap = 0;
for (let s = 1; s <= totalTe; s++) { let pos = 1;
if (!occupied[s]) { const railEnd = totalTe + 1;
currentGap++; for (const range of ranges) {
if (currentGap > maxGap) maxGap = currentGap; const gap = range.start - pos;
} else { if (gap > maxGap) maxGap = gap;
currentGap = 0; if (range.end > pos) pos = range.end;
}
} }
// Lücke nach letztem Element
const endGap = railEnd - pos;
if (endGap > maxGap) maxGap = endGap;
return maxGap; return maxGap;
} }
@ -1304,26 +1302,27 @@
const carrierEquipment = App.equipment.filter(e => e.fk_carrier == App.currentCarrierId); const carrierEquipment = App.equipment.filter(e => e.fk_carrier == App.currentCarrierId);
const carrier = App.carriers.find(c => c.id == App.currentCarrierId); const carrier = App.carriers.find(c => c.id == App.currentCarrierId);
const totalTe = parseInt(carrier?.total_te) || 12; const totalTe = parseInt(carrier?.total_te) || 12;
const eqWidth = parseInt(type?.width_te) || 1; const eqWidth = parseFloat(type?.width_te) || 1;
// Belegungsarray erstellen // Belegte Ranges ermitteln (Dezimal-TE-Unterstützung)
const occupied = new Array(totalTe + 1).fill(false); const ranges = carrierEquipment.map(e => ({
carrierEquipment.forEach(e => { start: parseFloat(e.position_te) || 1,
const pos = parseInt(e.position_te) || 1; end: (parseFloat(e.position_te) || 1) + (parseFloat(e.width_te) || 1)
const w = parseInt(e.width_te) || 1; })).sort((a, b) => a.start - b.start);
for (let i = pos; i < pos + w && i <= totalTe; i++) {
occupied[i] = true;
}
});
// Erste Lücke finden die breit genug ist // Erste Lücke finden die breit genug ist
let nextPos = 0; let nextPos = 0;
for (let i = 1; i <= totalTe - eqWidth + 1; i++) { let pos = 1;
let fits = true; const railEnd = totalTe + 1;
for (let j = 0; j < eqWidth; j++) { for (const range of ranges) {
if (occupied[i + j]) { fits = false; break; } 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) { if (nextPos === 0) {

View file

@ -745,8 +745,8 @@ if (empty($customerSystems)) {
print '</a>'; print '</a>';
print '</div>'; print '</div>';
print '</div>'; print '</div>';
print '<div class="schematic-message info">Bereit</div>';
print '<div class="schematic-editor-canvas expanded" style="display:block;background:#1a1a1a;border:1px solid #333;border-top:none;border-radius:0 0 4px 4px;padding:15px;overflow:auto;">'; print '<div class="schematic-editor-canvas expanded" style="display:block;background:#1a1a1a;border:1px solid #333;border-top:none;border-radius:0 0 4px 4px;padding:15px;overflow:auto;">';
print '<div class="schematic-message" style="display:none;padding:8px 15px;margin-bottom:10px;border-radius:4px;font-size:12px;"></div>';
print '</div>'; print '</div>';
print '</div>'; print '</div>';

View file

@ -743,8 +743,8 @@ if (empty($customerSystems)) {
print '</a>'; print '</a>';
print '</div>'; print '</div>';
print '</div>'; print '</div>';
print '<div class="schematic-message info">Bereit</div>';
print '<div class="schematic-editor-canvas expanded" style="display:block;background:#1a1a1a;border:1px solid #333;border-top:none;border-radius:0 0 4px 4px;padding:15px;overflow:auto;">'; print '<div class="schematic-editor-canvas expanded" style="display:block;background:#1a1a1a;border:1px solid #333;border-top:none;border-radius:0 0 4px 4px;padding:15px;overflow:auto;">';
print '<div class="schematic-message" style="display:none;padding:8px 15px;margin-bottom:10px;border-radius:4px;font-size:12px;"></div>';
print '</div>'; print '</div>';
print '</div>'; print '</div>';