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:
parent
dcd00fe844
commit
20fb9d3b05
12 changed files with 414 additions and 187 deletions
|
|
@ -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>';
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
77
js/pwa.js
77
js/pwa.js
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue