Version 3.2 - Phasenschienen-Typen, Block-Bilder & Reihenklemmen

Neue Features:
- Phasenschienen-Typen Admin-Seite (Sammelschienen konfigurierbar)
- 12 vordefinierte Phasenschienen-Typen (L1, L2, L3, N, PE, 3P, 3P+N etc.)
- Block-Bilder fuer Equipment-Typen (individuelle Darstellung)
- Reihenklemmen mit gestapelten Terminals (Mehrstockklemmen)
- Bruecken-System zwischen Reihenklemmen
- Phasenschienen per Drag & Drop verschiebbar (auch zwischen Hutschienen)
- Terminal-Labels mit dunklem Badge-Hintergrund

Technische Aenderungen:
- Neue Tabelle: llx_kundenkarte_busbar_type (Phasenschienen-Typen)
- Neue Tabelle: llx_kundenkarte_terminal_bridge (Klemmen-Bruecken)
- Neue PHP-Klassen: BusbarType, TerminalBridge
- Equipment-Type block_image Upload
- Terminals mit row/col Eigenschaften fuer Stapelung
- CSS-Optimierungen fuer Layout-Stabilitaet

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-12 14:03:34 +01:00
parent 2995b8fa4e
commit 5d05ac9f68
22 changed files with 2872 additions and 104 deletions

View file

@ -21,11 +21,15 @@ Das KundenKarte-Modul erweitert Dolibarr um zwei wichtige Funktionen fuer Kunden
- Interaktiver SVG-basierter Schaltplan-Editor
- Felder (Panels) und Hutschienen visuell verwalten
- Equipment-Bloecke per Drag & Drop positionieren
- Sammelschienen (Busbars) fuer Phasenverteilung
- Verbindungen zwischen Geraeten zeichnen
- Sammelschienen (Busbars) fuer Phasenverteilung mit konfigurierbaren Typen
- Phasenschienen per Drag & Drop verschiebbar (auch zwischen Hutschienen)
- Verbindungen zwischen Geraeten zeichnen (automatisch oder manuell)
- Abgaenge und Anschlusspunkte dokumentieren
- Klickbare Hutschienen zum Bearbeiten
- Zoom und Pan fuer grosse Schaltplaene
- Block-Bilder fuer Equipment-Typen (individuelle Darstellung)
- Reihenklemmen mit gestapelten Terminals (Mehrstockklemmen)
- Bruecken zwischen Reihenklemmen
### PDF Export
- Export der Anlagenstruktur als PDF
@ -57,6 +61,7 @@ Im Admin-Bereich (Home > Setup > Module > KundenKarte) koennen Sie:
- **Element-Typen**: Geraetetypen definieren (z.B. Zaehler, Router, Wallbox)
- **Typ-Felder**: Individuelle Felder pro Geraetetyp konfigurieren
- **Equipment-Typen**: Schaltplan-Komponenten (z.B. Sicherungsautomaten, FI-Schalter) mit Breite (TE), Farbe und Terminal-Konfiguration
- **Phasenschienen-Typen**: Sammelschienen/Phasenschienen-Vorlagen (L1, L2, L3, N, PE, 3P+N etc.) mit Farben und Linien-Konfiguration
## Berechtigungen

438
admin/busbar_types.php Normal file
View file

@ -0,0 +1,438 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* Admin page to manage busbar/phase rail types (Phasenschienen-Typen)
*/
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
dol_include_once('/kundenkarte/class/busbartype.class.php');
$langs->loadLangs(array('admin', 'kundenkarte@kundenkarte', 'products'));
// Security check
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
accessforbidden();
}
$action = GETPOST('action', 'aZ09');
$confirm = GETPOST('confirm', 'alpha');
$typeId = GETPOSTINT('typeid');
$systemFilter = GETPOSTINT('system');
$form = new Form($db);
$busbarType = new BusbarType($db);
// Load systems
$systems = array();
$sql = "SELECT rowid, code, label FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE active = 1 ORDER BY position ASC";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$systems[$obj->rowid] = $obj;
}
}
// Load products for dropdown
$products = array();
$sql = "SELECT rowid, ref, label FROM ".MAIN_DB_PREFIX."product WHERE tosell = 1 ORDER BY ref ASC";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
$products[$obj->rowid] = $obj;
}
}
// Predefined phase configurations
$phasePresets = array(
'L1' => array('label' => 'L1 (Phase 1)', 'num_lines' => 1, 'colors' => '#e74c3c'),
'L2' => array('label' => 'L2 (Phase 2)', 'num_lines' => 1, 'colors' => '#2ecc71'),
'L3' => array('label' => 'L3 (Phase 3)', 'num_lines' => 1, 'colors' => '#9b59b6'),
'N' => array('label' => 'N (Neutralleiter)', 'num_lines' => 1, 'colors' => '#3498db'),
'PE' => array('label' => 'PE (Schutzleiter)', 'num_lines' => 1, 'colors' => '#f1c40f'),
'L1N' => array('label' => 'L1+N (Wechselstrom)', 'num_lines' => 2, 'colors' => '#e74c3c,#3498db'),
'3P' => array('label' => '3P (Drehstrom)', 'num_lines' => 3, 'colors' => '#e74c3c,#2ecc71,#9b59b6'),
'3P+N' => array('label' => '3P+N (Drehstrom+N)', 'num_lines' => 4, 'colors' => '#e74c3c,#2ecc71,#9b59b6,#3498db'),
'3P+N+PE' => array('label' => '3P+N+PE (Vollausstattung)', 'num_lines' => 5, 'colors' => '#e74c3c,#2ecc71,#9b59b6,#3498db,#f1c40f'),
);
/*
* Actions
*/
if ($action == 'add') {
$busbarType->ref = GETPOST('ref', 'aZ09');
$busbarType->label = GETPOST('label', 'alphanohtml');
$busbarType->label_short = GETPOST('label_short', 'alphanohtml');
$busbarType->description = GETPOST('description', 'restricthtml');
$busbarType->fk_system = GETPOSTINT('fk_system');
$busbarType->phases = GETPOST('phases', 'alphanohtml');
$busbarType->num_lines = GETPOSTINT('num_lines');
$busbarType->color = GETPOST('color', 'alphanohtml');
$busbarType->default_color = GETPOST('default_color', 'alphanohtml');
$busbarType->line_height = GETPOSTINT('line_height') ?: 3;
$busbarType->line_spacing = GETPOSTINT('line_spacing') ?: 4;
$busbarType->position_default = GETPOST('position_default', 'alphanohtml') ?: 'below';
$busbarType->fk_product = GETPOSTINT('fk_product');
$busbarType->picto = GETPOST('picto', 'alphanohtml');
$busbarType->position = GETPOSTINT('position');
$busbarType->active = 1;
if (empty($busbarType->ref) || empty($busbarType->label) || empty($busbarType->fk_system) || empty($busbarType->phases)) {
setEventMessages($langs->trans('ErrorFieldRequired'), null, 'errors');
$action = 'create';
} else {
$result = $busbarType->create($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF'].'?system='.$busbarType->fk_system);
exit;
} else {
setEventMessages($busbarType->error, $busbarType->errors, 'errors');
$action = 'create';
}
}
}
if ($action == 'update') {
$busbarType->fetch($typeId);
$busbarType->ref = GETPOST('ref', 'aZ09');
$busbarType->label = GETPOST('label', 'alphanohtml');
$busbarType->label_short = GETPOST('label_short', 'alphanohtml');
$busbarType->description = GETPOST('description', 'restricthtml');
$busbarType->fk_system = GETPOSTINT('fk_system');
$busbarType->phases = GETPOST('phases', 'alphanohtml');
$busbarType->num_lines = GETPOSTINT('num_lines');
$busbarType->color = GETPOST('color', 'alphanohtml');
$busbarType->default_color = GETPOST('default_color', 'alphanohtml');
$busbarType->line_height = GETPOSTINT('line_height') ?: 3;
$busbarType->line_spacing = GETPOSTINT('line_spacing') ?: 4;
$busbarType->position_default = GETPOST('position_default', 'alphanohtml') ?: 'below';
$busbarType->fk_product = GETPOSTINT('fk_product');
$busbarType->picto = GETPOST('picto', 'alphanohtml');
$busbarType->position = GETPOSTINT('position');
$result = $busbarType->update($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF'].'?system='.$busbarType->fk_system);
exit;
} else {
setEventMessages($busbarType->error, $busbarType->errors, 'errors');
$action = 'edit';
}
}
if ($action == 'confirm_delete' && $confirm == 'yes') {
$busbarType->fetch($typeId);
$result = $busbarType->delete($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
} else {
setEventMessages($busbarType->error, $busbarType->errors, 'errors');
}
$action = '';
}
if ($action == 'activate') {
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_busbar_type SET active = 1 WHERE rowid = ".((int) $typeId);
$db->query($sql);
$action = '';
}
if ($action == 'deactivate') {
$sql = "UPDATE ".MAIN_DB_PREFIX."kundenkarte_busbar_type SET active = 0 WHERE rowid = ".((int) $typeId);
$db->query($sql);
$action = '';
}
/*
* View
*/
llxHeader('', $langs->trans('BusbarTypes'));
$head = kundenkarteAdminPrepareHead();
print dol_get_fiche_head($head, 'busbar_types', $langs->trans('KundenkarteSetup'), -1, 'kundenkarte@kundenkarte');
// System filter
print '<div class="tabBar" style="margin-bottom:15px;">';
print '<form method="get" action="'.$_SERVER['PHP_SELF'].'">';
print $langs->trans('System').': ';
print '<select name="system" class="flat" onchange="this.form.submit()">';
print '<option value="0">'.$langs->trans('All').'</option>';
foreach ($systems as $sys) {
$selected = ($systemFilter == $sys->rowid) ? ' selected' : '';
print '<option value="'.$sys->rowid.'"'.$selected.'>'.dol_escape_htmltag($sys->label).'</option>';
}
print '</select>';
print ' <button type="submit" class="button">'.$langs->trans('Filter').'</button>';
print ' <a class="button" href="'.$_SERVER['PHP_SELF'].'?action=create&system='.$systemFilter.'">'.$langs->trans('NewBusbarType').'</a>';
print '</form>';
print '</div>';
// Delete confirmation
if ($action == 'delete') {
$busbarType->fetch($typeId);
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?typeid='.$typeId.'&system='.$systemFilter,
$langs->trans('DeleteBusbarType'),
$langs->trans('ConfirmDeleteBusbarType', $busbarType->label),
'confirm_delete',
'',
0,
1
);
}
// Create/Edit form
if ($action == 'create' || $action == 'edit') {
if ($action == 'edit') {
$busbarType->fetch($typeId);
}
print '<form method="post" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="'.($action == 'edit' ? 'update' : 'add').'">';
if ($action == 'edit') {
print '<input type="hidden" name="typeid" value="'.$busbarType->id.'">';
}
print '<table class="border centpercent">';
// Ref
print '<tr><td class="fieldrequired">'.$langs->trans('Ref').'</td>';
print '<td><input type="text" name="ref" class="flat minwidth200" value="'.dol_escape_htmltag($busbarType->ref).'" required></td></tr>';
// Label
print '<tr><td class="fieldrequired">'.$langs->trans('Label').'</td>';
print '<td><input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($busbarType->label).'" required></td></tr>';
// Label Short
print '<tr><td>'.$langs->trans('LabelShort').'</td>';
print '<td><input type="text" name="label_short" class="flat minwidth100" value="'.dol_escape_htmltag($busbarType->label_short).'" maxlength="32"></td></tr>';
// Description
print '<tr><td>'.$langs->trans('Description').'</td>';
print '<td><textarea name="description" class="flat" rows="3" cols="60">'.dol_escape_htmltag($busbarType->description).'</textarea></td></tr>';
// System
print '<tr><td class="fieldrequired">'.$langs->trans('System').'</td>';
print '<td><select name="fk_system" class="flat" required>';
foreach ($systems as $sys) {
$selected = ($busbarType->fk_system == $sys->rowid || ($action == 'create' && $systemFilter == $sys->rowid)) ? ' selected' : '';
print '<option value="'.$sys->rowid.'"'.$selected.'>'.dol_escape_htmltag($sys->label).'</option>';
}
print '</select></td></tr>';
// Phase configuration
print '<tr><td class="fieldrequired">'.$langs->trans('Phases').'</td>';
print '<td>';
print '<div style="margin-bottom:10px;">';
print '<strong>Schnellauswahl:</strong><br>';
foreach ($phasePresets as $code => $preset) {
$style = 'display:inline-block;margin:3px;padding:5px 10px;border:1px solid #ccc;border-radius:4px;cursor:pointer;background:#f8f8f8;';
print '<span class="phase-preset" data-phases="'.$code.'" data-numlines="'.$preset['num_lines'].'" data-colors="'.$preset['colors'].'" style="'.$style.'">';
print dol_escape_htmltag($preset['label']);
print '</span>';
}
print '</div>';
print '<input type="text" name="phases" id="phases-input" class="flat minwidth150" value="'.dol_escape_htmltag($busbarType->phases).'" placeholder="z.B. L1N, 3P, 3P+N" required>';
print '<div class="opacitymedium small">Phasen-Konfiguration: L1, L2, L3, N, PE oder Kombinationen wie L1N, 3P, 3P+N, 3P+N+PE</div>';
print '</td></tr>';
// Number of lines
print '<tr><td>'.$langs->trans('NumLines').'</td>';
print '<td><input type="number" name="num_lines" id="numlines-input" class="flat" value="'.($busbarType->num_lines ?: 1).'" min="1" max="10"></td></tr>';
// Colors
print '<tr><td>'.$langs->trans('Colors').'</td>';
print '<td>';
print '<input type="text" name="color" id="colors-input" class="flat minwidth300" value="'.dol_escape_htmltag($busbarType->color).'" placeholder="Kommagetrennt: #e74c3c,#3498db">';
print '<div class="opacitymedium small">Kommagetrennte Farbcodes fuer jede Linie (z.B. #e74c3c,#2ecc71,#9b59b6)</div>';
print '</td></tr>';
// Default color
print '<tr><td>'.$langs->trans('DefaultColor').'</td>';
print '<td><input type="color" name="default_color" class="flat" value="'.($busbarType->default_color ?: '#e74c3c').'" style="width:60px;height:30px;"></td></tr>';
// Line height
print '<tr><td>'.$langs->trans('LineHeight').'</td>';
print '<td><input type="number" name="line_height" class="flat" value="'.($busbarType->line_height ?: 3).'" min="1" max="10"> px</td></tr>';
// Line spacing
print '<tr><td>'.$langs->trans('LineSpacing').'</td>';
print '<td><input type="number" name="line_spacing" class="flat" value="'.($busbarType->line_spacing ?: 4).'" min="1" max="20"> px</td></tr>';
// Default position
print '<tr><td>'.$langs->trans('DefaultPosition').'</td>';
print '<td><select name="position_default" class="flat">';
print '<option value="below"'.($busbarType->position_default == 'below' ? ' selected' : '').'>Unterhalb (below)</option>';
print '<option value="above"'.($busbarType->position_default == 'above' ? ' selected' : '').'>Oberhalb (above)</option>';
print '</select></td></tr>';
// Product link
print '<tr><td>'.$langs->trans('LinkedProduct').'</td>';
print '<td><select name="fk_product" class="flat minwidth300">';
print '<option value="0">-- '.$langs->trans('None').' --</option>';
foreach ($products as $prod) {
$selected = ($busbarType->fk_product == $prod->rowid) ? ' selected' : '';
print '<option value="'.$prod->rowid.'"'.$selected.'>'.dol_escape_htmltag($prod->ref.' - '.$prod->label).'</option>';
}
print '</select></td></tr>';
// Position
print '<tr><td>'.$langs->trans('Position').'</td>';
print '<td><input type="number" name="position" class="flat" value="'.($busbarType->position ?: 0).'" min="0"></td></tr>';
// Preview
print '<tr><td>'.$langs->trans('Preview').'</td>';
print '<td>';
print '<div id="busbar-preview" style="background:#1a1a2e;padding:20px;border-radius:8px;min-height:80px;">';
print '<svg id="preview-svg" width="200" height="60"></svg>';
print '</div>';
print '</td></tr>';
print '</table>';
print '<div class="center" style="margin-top:20px;">';
print '<button type="submit" class="button">'.$langs->trans('Save').'</button>';
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'?system='.$systemFilter.'">'.$langs->trans('Cancel').'</a>';
print '</div>';
print '</form>';
// JavaScript for preset selection and preview
print '<script>
document.querySelectorAll(".phase-preset").forEach(function(el) {
el.addEventListener("click", function() {
document.getElementById("phases-input").value = this.dataset.phases;
document.getElementById("numlines-input").value = this.dataset.numlines;
document.getElementById("colors-input").value = this.dataset.colors;
updatePreview();
});
});
function updatePreview() {
var numLines = parseInt(document.getElementById("numlines-input").value) || 1;
var colorsStr = document.getElementById("colors-input").value;
var colors = colorsStr ? colorsStr.split(",") : ["#e74c3c"];
var lineHeight = parseInt(document.querySelector("input[name=line_height]").value) || 3;
var lineSpacing = parseInt(document.querySelector("input[name=line_spacing]").value) || 4;
var svg = document.getElementById("preview-svg");
var html = "";
var y = 10;
for (var i = 0; i < numLines; i++) {
var color = colors[i % colors.length] || "#e74c3c";
html += "<rect x=\"10\" y=\"" + y + "\" width=\"180\" height=\"" + lineHeight + "\" fill=\"" + color + "\" rx=\"1\"/>";
y += lineHeight + lineSpacing;
}
svg.innerHTML = html;
svg.setAttribute("height", y + 10);
}
// Update preview on input change
document.getElementById("numlines-input").addEventListener("change", updatePreview);
document.getElementById("colors-input").addEventListener("input", updatePreview);
document.querySelector("input[name=line_height]").addEventListener("change", updatePreview);
document.querySelector("input[name=line_spacing]").addEventListener("change", updatePreview);
// Initial preview
updatePreview();
</script>';
} else {
// List of busbar types
$types = $busbarType->fetchAllBySystem($systemFilter, 0);
print '<div class="div-table-responsive">';
print '<table class="tagtable nobordernopadding liste centpercent">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('Ref').'</th>';
print '<th>'.$langs->trans('Label').'</th>';
print '<th>'.$langs->trans('Phases').'</th>';
print '<th class="center">'.$langs->trans('Lines').'</th>';
print '<th>'.$langs->trans('Colors').'</th>';
print '<th>'.$langs->trans('System').'</th>';
print '<th class="center">'.$langs->trans('Position').'</th>';
print '<th class="center">'.$langs->trans('Status').'</th>';
print '<th class="center">'.$langs->trans('Actions').'</th>';
print '</tr>';
if (empty($types)) {
print '<tr><td colspan="9" class="opacitymedium">'.$langs->trans('NoRecordFound').'</td></tr>';
} else {
foreach ($types as $type) {
print '<tr class="oddeven">';
print '<td><a href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$type->id.'&system='.$systemFilter.'">'.dol_escape_htmltag($type->ref).'</a></td>';
print '<td>'.dol_escape_htmltag($type->label);
if ($type->label_short) {
print ' <span class="opacitymedium">('.dol_escape_htmltag($type->label_short).')</span>';
}
print '</td>';
print '<td>'.dol_escape_htmltag($type->phases).'</td>';
print '<td class="center">'.$type->num_lines.'</td>';
// Color preview
print '<td>';
$colors = $type->color ? explode(',', $type->color) : array($type->default_color ?: '#e74c3c');
foreach ($colors as $c) {
print '<span style="display:inline-block;width:16px;height:16px;background:'.trim($c).';border-radius:2px;margin-right:2px;border:1px solid #555;"></span>';
}
print '</td>';
print '<td>'.dol_escape_htmltag($type->system_label).'</td>';
print '<td class="center">'.$type->position.'</td>';
// Status
print '<td class="center">';
if ($type->active) {
print '<span class="badge badge-status4">'.$langs->trans('Enabled').'</span>';
} else {
print '<span class="badge badge-status5">'.$langs->trans('Disabled').'</span>';
}
print '</td>';
// Actions
print '<td class="center nowraponall">';
print '<a href="'.$_SERVER['PHP_SELF'].'?action=edit&typeid='.$type->id.'&system='.$systemFilter.'" title="'.$langs->trans('Edit').'">';
print img_edit();
print '</a> ';
if ($type->active) {
print '<a href="'.$_SERVER['PHP_SELF'].'?action=deactivate&typeid='.$type->id.'&system='.$systemFilter.'&token='.newToken().'" title="'.$langs->trans('Disable').'">';
print img_picto($langs->trans('Disable'), 'switch_on');
print '</a> ';
} else {
print '<a href="'.$_SERVER['PHP_SELF'].'?action=activate&typeid='.$type->id.'&system='.$systemFilter.'&token='.newToken().'" title="'.$langs->trans('Enable').'">';
print img_picto($langs->trans('Enable'), 'switch_off');
print '</a> ';
}
if (!$type->is_system) {
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&typeid='.$type->id.'&system='.$systemFilter.'" title="'.$langs->trans('Delete').'">';
print img_delete();
print '</a>';
}
print '</td>';
print '</tr>';
}
}
print '</table>';
print '</div>';
}
print dol_get_fiche_end();
llxFooter();
$db->close();

View file

@ -63,6 +63,8 @@ if ($action == 'add') {
$equipmentType->color = GETPOST('color', 'alphanohtml');
$equipmentType->fk_product = GETPOSTINT('fk_product');
$equipmentType->terminals_config = GETPOST('terminals_config', 'nohtml');
$equipmentType->flow_direction = GETPOST('flow_direction', 'alphanohtml');
$equipmentType->terminal_position = GETPOST('terminal_position', 'alphanohtml') ?: 'both';
$equipmentType->picto = GETPOST('picto', 'alphanohtml');
$equipmentType->position = GETPOSTINT('position');
$equipmentType->active = 1;
@ -110,6 +112,8 @@ if ($action == 'update') {
$equipmentType->color = GETPOST('color', 'alphanohtml');
$equipmentType->fk_product = GETPOSTINT('fk_product');
$equipmentType->terminals_config = GETPOST('terminals_config', 'nohtml');
$equipmentType->flow_direction = GETPOST('flow_direction', 'alphanohtml');
$equipmentType->terminal_position = GETPOST('terminal_position', 'alphanohtml') ?: 'both';
$equipmentType->picto = GETPOST('picto', 'alphanohtml');
$equipmentType->position = GETPOSTINT('position');
@ -418,6 +422,64 @@ if (in_array($action, array('create', 'edit'))) {
print '</div>';
print '</td></tr>';
// Block Image Upload (for SchematicEditor display)
print '<tr><td>'.$langs->trans('BlockImage').'</td>';
print '<td>';
print '<div id="block-image-upload-area" style="display:flex;align-items:center;gap:15px;">';
// Preview area
print '<div id="block-image-preview" style="width:80px;height:80px;border:2px dashed #ccc;border-radius:8px;display:flex;align-items:center;justify-content:center;background:#f8f8f8;">';
if ($action == 'edit' && $equipmentType->block_image) {
$blockImageUrl = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.urlencode($equipmentType->block_image);
print '<img src="'.$blockImageUrl.'" style="max-width:70px;max-height:70px;" alt="Block Image">';
} else {
print '<span style="color:#999;font-size:11px;text-align:center;">Kein<br>Bild</span>';
}
print '</div>';
// Upload controls
print '<div>';
print '<input type="file" id="block-image-file-input" accept=".svg,.png,.jpg,.jpeg,.gif,.webp" style="display:none;">';
print '<button type="button" id="block-image-upload-btn" class="button" onclick="document.getElementById(\'block-image-file-input\').click();">';
print '<i class="fa fa-upload"></i> Bild hochladen</button>';
if ($action == 'edit' && $equipmentType->block_image) {
print ' <button type="button" id="block-image-delete-btn" class="button" style="background:#e74c3c;border-color:#c0392b;color:#fff;">';
print '<i class="fa fa-trash"></i> Löschen</button>';
}
// Dropdown to select existing images
$blockImagesDir = DOL_DATA_ROOT.'/kundenkarte/block_images/';
$existingImages = array();
if (is_dir($blockImagesDir)) {
$files = scandir($blockImagesDir);
foreach ($files as $file) {
if ($file != '.' && $file != '..' && preg_match('/\.(png|jpg|jpeg|gif|svg|webp)$/i', $file)) {
$existingImages[] = $file;
}
}
sort($existingImages);
}
if (!empty($existingImages)) {
print '<div style="margin-top:10px;">';
print '<select id="block-image-select" class="flat minwidth200">';
print '<option value="">-- Vorhandenes Bild wählen --</option>';
foreach ($existingImages as $img) {
$sel = ($equipmentType->block_image == $img) ? ' selected' : '';
print '<option value="'.dol_escape_htmltag($img).'"'.$sel.'>'.dol_escape_htmltag($img).'</option>';
}
print '</select>';
print ' <button type="button" id="block-image-select-btn" class="button">';
print '<i class="fa fa-check"></i> Übernehmen</button>';
print '</div>';
}
print '<div class="opacitymedium small" style="margin-top:5px;">Bild wird im SchematicEditor als Block-Hintergrund angezeigt</div>';
print '</div>';
print '</div>';
print '</td></tr>';
// Position
print '<tr><td>'.$langs->trans('Position').'</td>';
print '<td><input type="number" name="position" class="flat" value="'.($equipmentType->position ?: 0).'" min="0"></td></tr>';
@ -440,6 +502,28 @@ if (in_array($action, array('create', 'edit'))) {
print '</div>';
print '</td></tr>';
// Terminal Position
print '<tr><td>Anschlusspunkt-Position</td>';
print '<td>';
print '<select name="terminal_position" class="flat minwidth200">';
print '<option value="both"'.($equipmentType->terminal_position == 'both' ? ' selected' : '').'>Beidseitig (oben + unten)</option>';
print '<option value="top_only"'.($equipmentType->terminal_position == 'top_only' ? ' selected' : '').'>Nur oben</option>';
print '<option value="bottom_only"'.($equipmentType->terminal_position == 'bottom_only' ? ' selected' : '').'>Nur unten</option>';
print '</select>';
print '<div class="opacitymedium small" style="margin-top:5px;">Wo sollen die Anschlusspunkte angezeigt werden?</div>';
print '</td></tr>';
// Flow Direction
print '<tr><td>Richtung (Pfeil)</td>';
print '<td>';
print '<select name="flow_direction" class="flat minwidth200">';
print '<option value=""'.(!$equipmentType->flow_direction ? ' selected' : '').'>Keine</option>';
print '<option value="top_to_bottom"'.($equipmentType->flow_direction == 'top_to_bottom' ? ' selected' : '').'>Von oben nach unten ↓</option>';
print '<option value="bottom_to_top"'.($equipmentType->flow_direction == 'bottom_to_top' ? ' selected' : '').'>Von unten nach oben ↑</option>';
print '</select>';
print '<div class="opacitymedium small" style="margin-top:5px;">Zeigt einen Richtungspfeil im Block an (z.B. für Typ B FI-Schalter)</div>';
print '</td></tr>';
print '</table>';
// JavaScript for terminal presets and icon upload
@ -530,6 +614,126 @@ if (in_array($action, array('create', 'edit'))) {
var delBtn = document.getElementById("icon-delete-btn");
if (delBtn) delBtn.onclick = deleteIcon;
// Block Image upload handling
document.getElementById("block-image-file-input").addEventListener("change", function(e) {
if (!typeId || typeId == 0) {
alert("Bitte speichern Sie zuerst den Equipment-Typ bevor Sie ein Bild hochladen.");
return;
}
var file = e.target.files[0];
if (!file) return;
var formData = new FormData();
formData.append("action", "upload");
formData.append("type_id", typeId);
formData.append("block_image", file);
formData.append("token", "'.newToken().'");
fetch("'.DOL_URL_ROOT.'/custom/kundenkarte/ajax/equipment_type_block_image.php", {
method: "POST",
body: formData
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
var preview = document.getElementById("block-image-preview");
preview.innerHTML = \'<img src="\' + data.block_image_url + \'&t=\' + Date.now() + \'" style="max-width:70px;max-height:70px;" alt="Block Image">\';
// Add delete button if not present
if (!document.getElementById("block-image-delete-btn")) {
var btn = document.createElement("button");
btn.type = "button";
btn.id = "block-image-delete-btn";
btn.className = "button";
btn.style.cssText = "background:#e74c3c;border-color:#c0392b;color:#fff;margin-left:5px;";
btn.innerHTML = \'<i class="fa fa-trash"></i> Löschen\';
btn.onclick = deleteBlockImage;
document.getElementById("block-image-upload-btn").after(btn);
}
} else {
alert("Fehler: " + data.error);
}
})
.catch(function(err) {
alert("Upload fehlgeschlagen: " + err);
});
e.target.value = "";
});
function deleteBlockImage() {
if (!confirm("Bild wirklich löschen?")) return;
fetch("'.DOL_URL_ROOT.'/custom/kundenkarte/ajax/equipment_type_block_image.php", {
method: "POST",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: "action=delete&type_id=" + typeId + "&token='.newToken().'"
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
var preview = document.getElementById("block-image-preview");
preview.innerHTML = \'<span style="color:#999;font-size:11px;text-align:center;">Kein<br>Bild</span>\';
var delBtn = document.getElementById("block-image-delete-btn");
if (delBtn) delBtn.remove();
} else {
alert("Fehler: " + data.error);
}
});
}
var blockImgDelBtn = document.getElementById("block-image-delete-btn");
if (blockImgDelBtn) blockImgDelBtn.onclick = deleteBlockImage;
// Select existing image
var selectBtn = document.getElementById("block-image-select-btn");
if (selectBtn) {
selectBtn.onclick = function() {
if (!typeId || typeId == 0) {
alert("Bitte speichern Sie zuerst den Equipment-Typ.");
return;
}
var select = document.getElementById("block-image-select");
var selectedImage = select.value;
if (!selectedImage) {
alert("Bitte wählen Sie ein Bild aus.");
return;
}
fetch("'.DOL_URL_ROOT.'/custom/kundenkarte/ajax/equipment_type_block_image.php", {
method: "POST",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: "action=select&type_id=" + typeId + "&image=" + encodeURIComponent(selectedImage) + "&token='.newToken().'"
})
.then(function(response) { return response.json(); })
.then(function(data) {
if (data.success) {
var preview = document.getElementById("block-image-preview");
preview.innerHTML = \'<img src="\' + data.block_image_url + \'&t=\' + Date.now() + \'" style="max-width:70px;max-height:70px;" alt="Block Image">\';
// Add delete button if not present
if (!document.getElementById("block-image-delete-btn")) {
var btn = document.createElement("button");
btn.type = "button";
btn.id = "block-image-delete-btn";
btn.className = "button";
btn.style.cssText = "background:#e74c3c;border-color:#c0392b;color:#fff;margin-left:5px;";
btn.innerHTML = \'<i class="fa fa-trash"></i> Löschen\';
btn.onclick = deleteBlockImage;
document.getElementById("block-image-upload-btn").after(btn);
}
} else {
alert("Fehler: " + data.error);
}
})
.catch(function(err) {
alert("Fehler: " + err);
});
};
}
</script>';
print '<div class="center" style="margin-top:20px;">';

View file

@ -123,6 +123,10 @@ switch ($action) {
'type_color' => $eq->type_color,
'type_icon_file' => $eq->type_icon_file,
'type_icon_url' => $iconUrl,
'type_block_image' => $eq->type_block_image,
'type_block_image_url' => !empty($eq->type_block_image) ? DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.urlencode($eq->type_block_image) : '',
'type_flow_direction' => $eq->type_flow_direction,
'type_terminal_position' => $eq->type_terminal_position ?: 'both',
'terminals_config' => $eq->terminals_config,
'label' => $eq->label,
'position_te' => $eq->position_te,
@ -277,6 +281,44 @@ switch ($action) {
}
break;
case 'move_to_carrier':
// Move equipment to different carrier (drag-drop between carriers)
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = 'Permission denied';
break;
}
if ($equipment->fetch($equipmentId) > 0) {
$newCarrierId = GETPOSTINT('carrier_id');
$newPosition = GETPOSTINT('position_te') ?: 1;
// Check if target carrier exists
$targetCarrier = new EquipmentCarrier($db);
if ($targetCarrier->fetch($newCarrierId) <= 0) {
$response['error'] = 'Target carrier not found';
break;
}
// Check if position is available on target carrier
if (!$targetCarrier->isPositionAvailable($newPosition, $equipment->width_te, 0)) {
$response['error'] = 'Position auf Ziel-Hutschiene nicht verfügbar';
break;
}
// Update equipment
$equipment->fk_carrier = $newCarrierId;
$equipment->position_te = $newPosition;
$result = $equipment->update($user);
if ($result > 0) {
$response['success'] = true;
$response['message'] = 'Equipment verschoben';
} else {
$response['error'] = $equipment->error;
}
} else {
$response['error'] = 'Equipment not found';
}
break;
case 'delete':
if (!$user->hasRight('kundenkarte', 'delete')) {
$response['error'] = 'Permission denied';

View file

@ -243,6 +243,39 @@ switch ($action) {
}
break;
case 'update_rail_position':
// Update rail/busbar start and end position (for drag & drop)
// Also supports moving to a different carrier (different panel/hutschiene)
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = 'Permission denied';
break;
}
if ($connection->fetch($connectionId) > 0) {
// Only allow updating rail connections
if (!$connection->is_rail) {
$response['error'] = 'Not a rail connection';
break;
}
$connection->rail_start_te = GETPOSTINT('rail_start_te');
$connection->rail_end_te = GETPOSTINT('rail_end_te');
// Update carrier if provided (for moving between panels)
if (GETPOSTISSET('carrier_id') && GETPOSTINT('carrier_id') > 0) {
$connection->fk_carrier = GETPOSTINT('carrier_id');
}
$result = $connection->update($user);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = $connection->error ?: 'Update failed';
}
} else {
$response['error'] = 'Connection not found';
}
break;
case 'create_output':
// Create an output connection
if (!$user->hasRight('kundenkarte', 'write')) {
@ -372,6 +405,149 @@ switch ($action) {
}
break;
// ============================================
// Bridge Actions (Brücken zwischen Klemmen)
// ============================================
case 'list_bridges':
// List all bridges for an anlage
$anlageId = GETPOSTINT('anlage_id');
if ($anlageId > 0) {
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/terminalbridge.class.php';
$bridgeObj = new TerminalBridge($db);
$bridges = $bridgeObj->fetchAllByAnlage($anlageId);
$bridgeList = array();
foreach ($bridges as $bridge) {
$bridgeList[] = array(
'id' => $bridge->id,
'fk_carrier' => $bridge->fk_carrier,
'start_te' => $bridge->start_te,
'end_te' => $bridge->end_te,
'terminal_side' => $bridge->terminal_side,
'terminal_row' => $bridge->terminal_row,
'color' => $bridge->color,
'bridge_type' => $bridge->bridge_type,
'label' => $bridge->label
);
}
$response['success'] = true;
$response['bridges'] = $bridgeList;
} else {
$response['error'] = 'Missing anlage_id';
}
break;
case 'create_bridge':
// Create a new terminal bridge
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = 'Permission denied';
break;
}
$anlageId = GETPOSTINT('anlage_id');
$carrierId = GETPOSTINT('carrier_id');
$startTE = GETPOSTINT('start_te');
$endTE = GETPOSTINT('end_te');
if (empty($anlageId) || empty($carrierId) || empty($startTE) || empty($endTE)) {
$response['error'] = 'Missing required parameters';
break;
}
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/terminalbridge.class.php';
$bridge = new TerminalBridge($db);
$bridge->fk_anlage = $anlageId;
$bridge->fk_carrier = $carrierId;
$bridge->start_te = min($startTE, $endTE);
$bridge->end_te = max($startTE, $endTE);
$bridge->terminal_side = GETPOST('terminal_side', 'alpha') ?: 'top';
$bridge->terminal_row = GETPOSTINT('terminal_row');
$bridge->color = GETPOST('color', 'alphanohtml') ?: '#e74c3c';
$bridge->bridge_type = GETPOST('bridge_type', 'alpha') ?: 'standard';
$bridge->label = GETPOST('label', 'alphanohtml');
$result = $bridge->create($user);
if ($result > 0) {
$response['success'] = true;
$response['bridge_id'] = $result;
$response['bridge'] = array(
'id' => $bridge->id,
'fk_carrier' => $bridge->fk_carrier,
'start_te' => $bridge->start_te,
'end_te' => $bridge->end_te,
'terminal_side' => $bridge->terminal_side,
'terminal_row' => $bridge->terminal_row,
'color' => $bridge->color,
'bridge_type' => $bridge->bridge_type,
'label' => $bridge->label
);
} else {
$response['error'] = $bridge->error ?: 'Create failed';
}
break;
case 'update_bridge':
// Update an existing bridge
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = 'Permission denied';
break;
}
$bridgeId = GETPOSTINT('bridge_id');
if ($bridgeId > 0) {
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/terminalbridge.class.php';
$bridge = new TerminalBridge($db);
if ($bridge->fetch($bridgeId) > 0) {
if (GETPOSTISSET('start_te')) $bridge->start_te = GETPOSTINT('start_te');
if (GETPOSTISSET('end_te')) $bridge->end_te = GETPOSTINT('end_te');
if (GETPOSTISSET('terminal_side')) $bridge->terminal_side = GETPOST('terminal_side', 'alpha');
if (GETPOSTISSET('terminal_row')) $bridge->terminal_row = GETPOSTINT('terminal_row');
if (GETPOSTISSET('color')) $bridge->color = GETPOST('color', 'alphanohtml');
if (GETPOSTISSET('bridge_type')) $bridge->bridge_type = GETPOST('bridge_type', 'alpha');
if (GETPOSTISSET('label')) $bridge->label = GETPOST('label', 'alphanohtml');
$result = $bridge->update($user);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = $bridge->error ?: 'Update failed';
}
} else {
$response['error'] = 'Bridge not found';
}
} else {
$response['error'] = 'Missing bridge_id';
}
break;
case 'delete_bridge':
// Delete a bridge
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = 'Permission denied';
break;
}
$bridgeId = GETPOSTINT('bridge_id');
if ($bridgeId > 0) {
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/terminalbridge.class.php';
$bridge = new TerminalBridge($db);
if ($bridge->fetch($bridgeId) > 0) {
$result = $bridge->delete($user);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = $bridge->error ?: 'Delete failed';
}
} else {
$response['error'] = 'Bridge not found';
}
} else {
$response['error'] = 'Missing bridge_id';
}
break;
default:
$response['error'] = 'Unknown action';
}

View file

@ -0,0 +1,187 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX handler for equipment type block image upload
* Accepts image files for SchematicEditor block display
*/
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res && file_exists("../../../../main.inc.php")) $res = @include "../../../../main.inc.php";
if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
dol_include_once('/kundenkarte/class/equipmenttype.class.php');
header('Content-Type: application/json');
// Security check
if (!$user->admin && !$user->hasRight('kundenkarte', 'admin')) {
echo json_encode(array('success' => false, 'error' => 'Access denied'));
exit;
}
$action = GETPOST('action', 'aZ09');
$typeId = GETPOSTINT('type_id');
$response = array('success' => false);
// Directory for block images
$uploadDir = DOL_DATA_ROOT.'/kundenkarte/block_images/';
// Create directory if not exists
if (!is_dir($uploadDir)) {
dol_mkdir($uploadDir);
}
switch ($action) {
case 'upload':
if (empty($_FILES['block_image']) || $_FILES['block_image']['error'] !== UPLOAD_ERR_OK) {
$response['error'] = 'No file uploaded or upload error';
break;
}
$file = $_FILES['block_image'];
$fileName = dol_sanitizeFileName($file['name']);
$fileExt = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
// Validate file type
$allowedExtensions = array('svg', 'png', 'jpg', 'jpeg', 'gif', 'webp');
if (!in_array($fileExt, $allowedExtensions)) {
$response['error'] = 'Invalid file type. Only SVG, PNG, JPG, GIF, WEBP are allowed.';
break;
}
// Validate MIME type
$mimeType = mime_content_type($file['tmp_name']);
$allowedMimes = array('image/svg+xml', 'image/png', 'image/jpeg', 'image/gif', 'image/webp', 'text/plain', 'text/xml', 'application/xml');
if (!in_array($mimeType, $allowedMimes)) {
$response['error'] = 'Invalid MIME type: '.$mimeType;
break;
}
// For SVG files, do basic security check
if ($fileExt === 'svg') {
$content = file_get_contents($file['tmp_name']);
// Check for potentially dangerous content
$dangerous = array('<script', 'javascript:', 'onload=', 'onerror=', 'onclick=', 'onmouseover=');
foreach ($dangerous as $pattern) {
if (stripos($content, $pattern) !== false) {
$response['error'] = 'SVG contains potentially dangerous content';
break 2;
}
}
}
// Generate unique filename
$newFileName = 'block_'.$typeId.'_'.dol_now().'.'.$fileExt;
$destPath = $uploadDir.$newFileName;
// Move uploaded file
if (move_uploaded_file($file['tmp_name'], $destPath)) {
// Update database
$equipmentType = new EquipmentType($db);
if ($equipmentType->fetch($typeId) > 0) {
// Delete old block image file if exists
if ($equipmentType->block_image && file_exists($uploadDir.$equipmentType->block_image)) {
unlink($uploadDir.$equipmentType->block_image);
}
$equipmentType->block_image = $newFileName;
$result = $equipmentType->update($user);
if ($result > 0) {
$response['success'] = true;
$response['block_image'] = $newFileName;
$response['block_image_url'] = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.$newFileName;
} else {
$response['error'] = 'Database update failed';
// Remove uploaded file on DB error
unlink($destPath);
}
} else {
$response['error'] = 'Equipment type not found';
unlink($destPath);
}
} else {
$response['error'] = 'Failed to move uploaded file';
}
break;
case 'delete':
$equipmentType = new EquipmentType($db);
if ($equipmentType->fetch($typeId) > 0) {
if ($equipmentType->block_image && file_exists($uploadDir.$equipmentType->block_image)) {
unlink($uploadDir.$equipmentType->block_image);
}
$equipmentType->block_image = '';
$result = $equipmentType->update($user);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Database update failed';
}
} else {
$response['error'] = 'Equipment type not found';
}
break;
case 'get':
$equipmentType = new EquipmentType($db);
if ($equipmentType->fetch($typeId) > 0) {
$response['success'] = true;
$response['block_image'] = $equipmentType->block_image;
if ($equipmentType->block_image) {
$response['block_image_url'] = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.$equipmentType->block_image;
}
} else {
$response['error'] = 'Equipment type not found';
}
break;
case 'select':
// Select an existing image from the block_images folder
$selectedImage = GETPOST('image', 'alphanohtml');
if (empty($selectedImage)) {
$response['error'] = 'No image selected';
break;
}
// Validate that the image exists
$imagePath = $uploadDir . $selectedImage;
if (!file_exists($imagePath)) {
$response['error'] = 'Image file not found';
break;
}
// Validate file extension
$fileExt = strtolower(pathinfo($selectedImage, PATHINFO_EXTENSION));
$allowedExtensions = array('svg', 'png', 'jpg', 'jpeg', 'gif', 'webp');
if (!in_array($fileExt, $allowedExtensions)) {
$response['error'] = 'Invalid file type';
break;
}
$equipmentType = new EquipmentType($db);
if ($equipmentType->fetch($typeId) > 0) {
$equipmentType->block_image = $selectedImage;
$result = $equipmentType->update($user);
if ($result > 0) {
$response['success'] = true;
$response['block_image'] = $selectedImage;
$response['block_image_url'] = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.urlencode($selectedImage);
} else {
$response['error'] = 'Database update failed';
}
} else {
$response['error'] = 'Equipment type not found';
}
break;
default:
$response['error'] = 'Unknown action';
}
echo json_encode($response);
$db->close();

390
class/busbartype.class.php Normal file
View file

@ -0,0 +1,390 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*/
/**
* Class BusbarType
* Manages busbar/phase rail type templates (Phasenschienen-Typen)
*/
class BusbarType extends CommonObject
{
public $element = 'busbartype';
public $table_element = 'kundenkarte_busbar_type';
public $ref;
public $label;
public $label_short;
public $description;
public $fk_system;
// Busbar-spezifische Felder
public $phases; // L1, L2, L3, N, PE, L1N, 3P, 3P+N, etc.
public $num_lines = 1; // Anzahl der Linien
public $color; // Kommagetrennte Farben
public $default_color; // Standard-Einzelfarbe
public $line_height = 3;
public $line_spacing = 4;
public $position_default = 'below';
public $fk_product;
public $picto;
public $icon_file;
public $is_system;
public $position;
public $active;
public $date_creation;
public $fk_user_creat;
public $fk_user_modif;
// Loaded objects
public $system_label;
public $system_code;
public $product_ref;
public $product_label;
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Create object in database
*
* @param User $user User that creates
* @return int Return integer <0 if KO, Id of created object if OK
*/
public function create($user)
{
global $conf;
$error = 0;
$now = dol_now();
if (empty($this->ref) || empty($this->label) || empty($this->fk_system) || empty($this->phases)) {
$this->error = 'ErrorMissingParameters';
return -1;
}
$this->db->begin();
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, ref, label, label_short, description, fk_system,";
$sql .= " phases, num_lines, color, default_color, line_height, line_spacing, position_default,";
$sql .= " fk_product, picto, icon_file, is_system, position, active,";
$sql .= " date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= "0"; // entity 0 = global
$sql .= ", '".$this->db->escape($this->ref)."'";
$sql .= ", '".$this->db->escape($this->label)."'";
$sql .= ", ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
$sql .= ", ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
$sql .= ", ".((int) $this->fk_system);
$sql .= ", '".$this->db->escape($this->phases)."'";
$sql .= ", ".((int) ($this->num_lines > 0 ? $this->num_lines : 1));
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", ".($this->default_color ? "'".$this->db->escape($this->default_color)."'" : "NULL");
$sql .= ", ".((int) ($this->line_height > 0 ? $this->line_height : 3));
$sql .= ", ".((int) ($this->line_spacing > 0 ? $this->line_spacing : 4));
$sql .= ", '".$this->db->escape($this->position_default ?: 'below')."'";
$sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", ".($this->icon_file ? "'".$this->db->escape($this->icon_file)."'" : "NULL");
$sql .= ", 0"; // is_system = 0 for user-created
$sql .= ", ".((int) $this->position);
$sql .= ", ".((int) ($this->active !== null ? $this->active : 1));
$sql .= ", '".$this->db->idate($now)."'";
$sql .= ", ".((int) $user->id);
$sql .= ")";
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if (!$error) {
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return $this->id;
}
}
/**
* Load object from database
*
* @param int $id ID of record
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
*/
public function fetch($id)
{
$sql = "SELECT t.*, s.label as system_label, s.code as system_code,";
$sql .= " p.ref as product_ref, p.label as product_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON t.fk_product = p.rowid";
$sql .= " WHERE t.rowid = ".((int) $id);
$resql = $this->db->query($sql);
if ($resql) {
if ($this->db->num_rows($resql)) {
$obj = $this->db->fetch_object($resql);
$this->id = $obj->rowid;
$this->entity = $obj->entity;
$this->ref = $obj->ref;
$this->label = $obj->label;
$this->label_short = $obj->label_short;
$this->description = $obj->description;
$this->fk_system = $obj->fk_system;
$this->phases = $obj->phases;
$this->num_lines = $obj->num_lines;
$this->color = $obj->color;
$this->default_color = $obj->default_color;
$this->line_height = $obj->line_height;
$this->line_spacing = $obj->line_spacing;
$this->position_default = $obj->position_default ?: 'below';
$this->fk_product = $obj->fk_product;
$this->picto = $obj->picto;
$this->icon_file = $obj->icon_file;
$this->is_system = $obj->is_system;
$this->position = $obj->position;
$this->active = $obj->active;
$this->date_creation = $this->db->jdate($obj->date_creation);
$this->fk_user_creat = $obj->fk_user_creat;
$this->fk_user_modif = $obj->fk_user_modif;
$this->system_label = $obj->system_label;
$this->system_code = $obj->system_code;
$this->product_ref = $obj->product_ref;
$this->product_label = $obj->product_label;
$this->db->free($resql);
return 1;
} else {
$this->db->free($resql);
return 0;
}
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Update object in database
*
* @param User $user User that modifies
* @return int Return integer <0 if KO, >0 if OK
*/
public function update($user)
{
$error = 0;
$this->db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
$sql .= " ref = '".$this->db->escape($this->ref)."'";
$sql .= ", label = '".$this->db->escape($this->label)."'";
$sql .= ", label_short = ".($this->label_short ? "'".$this->db->escape($this->label_short)."'" : "NULL");
$sql .= ", description = ".($this->description ? "'".$this->db->escape($this->description)."'" : "NULL");
$sql .= ", fk_system = ".((int) $this->fk_system);
$sql .= ", phases = '".$this->db->escape($this->phases)."'";
$sql .= ", num_lines = ".((int) ($this->num_lines > 0 ? $this->num_lines : 1));
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", default_color = ".($this->default_color ? "'".$this->db->escape($this->default_color)."'" : "NULL");
$sql .= ", line_height = ".((int) ($this->line_height > 0 ? $this->line_height : 3));
$sql .= ", line_spacing = ".((int) ($this->line_spacing > 0 ? $this->line_spacing : 4));
$sql .= ", position_default = '".$this->db->escape($this->position_default ?: 'below')."'";
$sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", icon_file = ".($this->icon_file ? "'".$this->db->escape($this->icon_file)."'" : "NULL");
$sql .= ", position = ".((int) $this->position);
$sql .= ", active = ".((int) $this->active);
$sql .= ", fk_user_modif = ".((int) $user->id);
$sql .= " WHERE rowid = ".((int) $this->id);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return 1;
}
}
/**
* Delete object in database
*
* @param User $user User that deletes
* @return int Return integer <0 if KO, >0 if OK
*/
public function delete($user)
{
global $conf;
// Check if type is in use (connections referencing this type)
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX."kundenkarte_equipment_connection";
$sql .= " WHERE fk_busbar_type = ".((int) $this->id);
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
if ($obj->cnt > 0) {
$this->error = 'ErrorTypeInUse';
return -1;
}
}
// Cannot delete system types
if ($this->is_system) {
$this->error = 'ErrorCannotDeleteSystemType';
return -2;
}
$error = 0;
$this->db->begin();
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return 1;
}
}
/**
* Fetch all busbar types for a system
*
* @param int $systemId System ID (0 = all)
* @param int $activeOnly Only active types
* @return array Array of BusbarType objects
*/
public function fetchAllBySystem($systemId = 0, $activeOnly = 1)
{
$results = array();
$sql = "SELECT t.*, s.label as system_label, s.code as system_code";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as t";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON t.fk_system = s.rowid";
$sql .= " WHERE 1 = 1";
if ($systemId > 0) {
$sql .= " AND t.fk_system = ".((int) $systemId);
}
if ($activeOnly) {
$sql .= " AND t.active = 1";
}
$sql .= " ORDER BY t.fk_system ASC, t.position ASC, t.label ASC";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$type = new BusbarType($this->db);
$type->id = $obj->rowid;
$type->ref = $obj->ref;
$type->label = $obj->label;
$type->label_short = $obj->label_short;
$type->fk_system = $obj->fk_system;
$type->phases = $obj->phases;
$type->num_lines = $obj->num_lines;
$type->color = $obj->color;
$type->default_color = $obj->default_color;
$type->line_height = $obj->line_height;
$type->line_spacing = $obj->line_spacing;
$type->position_default = $obj->position_default;
$type->fk_product = $obj->fk_product;
$type->picto = $obj->picto;
$type->icon_file = $obj->icon_file;
$type->is_system = $obj->is_system;
$type->position = $obj->position;
$type->active = $obj->active;
$type->system_label = $obj->system_label;
$type->system_code = $obj->system_code;
$results[] = $type;
}
$this->db->free($resql);
}
return $results;
}
/**
* Get color array from comma-separated string
*
* @return array Array of color codes
*/
public function getColors()
{
if (empty($this->color)) {
return array($this->default_color ?: '#e74c3c');
}
return explode(',', $this->color);
}
/**
* Get phase labels array from phases string
*
* @return array Array of phase labels
*/
public function getPhaseLabels()
{
$phases = $this->phases;
// Parse common phase configurations
switch (strtoupper($phases)) {
case 'L1':
return array('L1');
case 'L2':
return array('L2');
case 'L3':
return array('L3');
case 'N':
return array('N');
case 'PE':
return array('PE');
case 'L1N':
return array('L1', 'N');
case '3P':
return array('L1', 'L2', 'L3');
case '3P+N':
case '3PN':
return array('L1', 'L2', 'L3', 'N');
case '3P+N+PE':
case '3PNPE':
return array('L1', 'L2', 'L3', 'N', 'PE');
default:
// Try to split by comma or +
return preg_split('/[,+]/', $phases);
}
}
}

View file

@ -40,7 +40,10 @@ class Equipment extends CommonObject
public $type_label_short;
public $type_color;
public $type_picto;
public $type_icon_file; // SVG/PNG schematic symbol
public $type_icon_file; // SVG/PNG schematic symbol (PDF)
public $type_block_image; // Image for block display in SchematicEditor
public $type_flow_direction; // Richtung: NULL, top_to_bottom, bottom_to_top
public $type_terminal_position; // Terminal-Position: both, top_only, bottom_only
public $product_ref;
public $product_label;
public $protection_device_label; // Label of the protection device
@ -137,6 +140,8 @@ class Equipment extends CommonObject
{
$sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,";
$sql .= " t.ref as type_ref, t.color as type_color, t.picto as type_picto, t.icon_file as type_icon_file,";
$sql .= " t.block_image as type_block_image,";
$sql .= " t.flow_direction as type_flow_direction, t.terminal_position as type_terminal_position,";
$sql .= " t.terminals_config as terminals_config,";
$sql .= " p.ref as product_ref, p.label as product_label,";
$sql .= " prot.label as protection_device_label";
@ -174,6 +179,9 @@ class Equipment extends CommonObject
$this->type_color = $obj->type_color;
$this->type_picto = $obj->type_picto;
$this->type_icon_file = $obj->type_icon_file;
$this->type_block_image = $obj->type_block_image;
$this->type_flow_direction = $obj->type_flow_direction;
$this->type_terminal_position = $obj->type_terminal_position ?: 'both';
$this->terminals_config = $obj->terminals_config;
$this->product_ref = $obj->product_ref;
$this->product_label = $obj->product_label;
@ -204,7 +212,8 @@ class Equipment extends CommonObject
$this->db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
$sql .= " fk_equipment_type = ".((int) $this->fk_equipment_type);
$sql .= " fk_carrier = ".((int) $this->fk_carrier);
$sql .= ", fk_equipment_type = ".((int) $this->fk_equipment_type);
$sql .= ", label = ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
$sql .= ", position_te = ".((int) $this->position_te);
$sql .= ", width_te = ".((int) $this->width_te);
@ -272,6 +281,8 @@ class Equipment extends CommonObject
$sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,";
$sql .= " t.ref as type_ref, t.color as type_color, t.picto as type_picto, t.icon_file as type_icon_file,";
$sql .= " t.block_image as type_block_image,";
$sql .= " t.flow_direction as type_flow_direction, t.terminal_position as type_terminal_position,";
$sql .= " t.terminals_config as terminals_config,";
$sql .= " prot.label as protection_device_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e";
@ -307,6 +318,9 @@ class Equipment extends CommonObject
$eq->type_color = $obj->type_color;
$eq->type_picto = $obj->type_picto;
$eq->type_icon_file = $obj->type_icon_file;
$eq->type_block_image = $obj->type_block_image;
$eq->type_flow_direction = $obj->type_flow_direction;
$eq->type_terminal_position = $obj->type_terminal_position ?: 'both';
$eq->terminals_config = $obj->terminals_config;
$eq->protection_device_label = $obj->protection_device_label;

View file

@ -27,9 +27,12 @@ class EquipmentType extends CommonObject
public $color;
public $fk_product;
public $terminals_config; // JSON config for terminals
public $flow_direction; // Flussrichtung: NULL=keine, top_to_bottom, bottom_to_top
public $terminal_position = 'both'; // Terminal-Position: both, top_only, bottom_only
public $picto;
public $icon_file; // Uploaded SVG/PNG file for schematic symbol
public $icon_file; // Uploaded SVG/PNG file for schematic symbol (PDF)
public $block_image; // Uploaded image for block display in SchematicEditor
public $is_system;
public $position;
public $active;
@ -76,8 +79,8 @@ class EquipmentType extends CommonObject
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, ref, label, label_short, description, fk_system,";
$sql .= " width_te, color, fk_product, terminals_config,";
$sql .= " picto, icon_file, is_system, position, active,";
$sql .= " width_te, color, fk_product, terminals_config, flow_direction, terminal_position,";
$sql .= " picto, icon_file, block_image, is_system, position, active,";
$sql .= " date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= "0"; // entity 0 = global
@ -90,8 +93,11 @@ class EquipmentType extends CommonObject
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", ".($this->terminals_config ? "'".$this->db->escape($this->terminals_config)."'" : "NULL");
$sql .= ", ".($this->flow_direction ? "'".$this->db->escape($this->flow_direction)."'" : "NULL");
$sql .= ", '".$this->db->escape($this->terminal_position ?: 'both')."'";
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", ".($this->icon_file ? "'".$this->db->escape($this->icon_file)."'" : "NULL");
$sql .= ", ".($this->block_image ? "'".$this->db->escape($this->block_image)."'" : "NULL");
$sql .= ", 0"; // is_system = 0 for user-created
$sql .= ", ".((int) $this->position);
$sql .= ", ".((int) ($this->active !== null ? $this->active : 1));
@ -149,8 +155,11 @@ class EquipmentType extends CommonObject
$this->color = $obj->color;
$this->fk_product = $obj->fk_product;
$this->terminals_config = $obj->terminals_config;
$this->flow_direction = $obj->flow_direction;
$this->terminal_position = $obj->terminal_position ?: 'both';
$this->picto = $obj->picto;
$this->icon_file = $obj->icon_file;
$this->block_image = $obj->block_image;
$this->is_system = $obj->is_system;
$this->position = $obj->position;
$this->active = $obj->active;
@ -197,8 +206,11 @@ class EquipmentType extends CommonObject
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", terminals_config = ".($this->terminals_config ? "'".$this->db->escape($this->terminals_config)."'" : "NULL");
$sql .= ", flow_direction = ".($this->flow_direction ? "'".$this->db->escape($this->flow_direction)."'" : "NULL");
$sql .= ", terminal_position = '".$this->db->escape($this->terminal_position ?: 'both')."'";
$sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", icon_file = ".($this->icon_file ? "'".$this->db->escape($this->icon_file)."'" : "NULL");
$sql .= ", block_image = ".($this->block_image ? "'".$this->db->escape($this->block_image)."'" : "NULL");
$sql .= ", position = ".((int) $this->position);
$sql .= ", active = ".((int) $this->active);
$sql .= ", fk_user_modif = ".((int) $user->id);
@ -306,8 +318,11 @@ class EquipmentType extends CommonObject
$type->width_te = $obj->width_te;
$type->color = $obj->color;
$type->fk_product = $obj->fk_product;
$type->flow_direction = $obj->flow_direction;
$type->terminal_position = $obj->terminal_position ?: 'both';
$type->picto = $obj->picto;
$type->icon_file = $obj->icon_file;
$type->block_image = $obj->block_image;
$type->is_system = $obj->is_system;
$type->position = $obj->position;
$type->active = $obj->active;

View file

@ -0,0 +1,255 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*/
/**
* Class TerminalBridge
* Manages bridges between terminal blocks (Brücken zwischen Reihenklemmen)
*/
class TerminalBridge extends CommonObject
{
public $element = 'terminalbridge';
public $table_element = 'kundenkarte_terminal_bridge';
public $fk_anlage;
public $fk_carrier;
public $start_te;
public $end_te;
public $terminal_side = 'top';
public $terminal_row = 0;
public $color = '#e74c3c';
public $bridge_type = 'standard';
public $label;
public $status = 1;
public $date_creation;
public $fk_user_creat;
public $fk_user_modif;
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Create object in database
*
* @param User $user User that creates
* @return int Return integer <0 if KO, Id of created object if OK
*/
public function create($user)
{
$error = 0;
$now = dol_now();
if (empty($this->fk_anlage) || empty($this->fk_carrier) || empty($this->start_te) || empty($this->end_te)) {
$this->error = 'ErrorMissingParameters';
return -1;
}
$this->db->begin();
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, fk_anlage, fk_carrier, start_te, end_te,";
$sql .= " terminal_side, terminal_row, color, bridge_type, label,";
$sql .= " status, date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= ((int) $this->entity ?: 1);
$sql .= ", ".((int) $this->fk_anlage);
$sql .= ", ".((int) $this->fk_carrier);
$sql .= ", ".((int) $this->start_te);
$sql .= ", ".((int) $this->end_te);
$sql .= ", '".$this->db->escape($this->terminal_side ?: 'top')."'";
$sql .= ", ".((int) $this->terminal_row);
$sql .= ", '".$this->db->escape($this->color ?: '#e74c3c')."'";
$sql .= ", '".$this->db->escape($this->bridge_type ?: 'standard')."'";
$sql .= ", ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
$sql .= ", ".((int) ($this->status !== null ? $this->status : 1));
$sql .= ", '".$this->db->idate($now)."'";
$sql .= ", ".((int) $user->id);
$sql .= ")";
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if (!$error) {
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return $this->id;
}
}
/**
* Load object from database
*
* @param int $id ID of record
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
*/
public function fetch($id)
{
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE rowid = ".((int) $id);
$resql = $this->db->query($sql);
if ($resql) {
if ($this->db->num_rows($resql)) {
$obj = $this->db->fetch_object($resql);
$this->id = $obj->rowid;
$this->entity = $obj->entity;
$this->fk_anlage = $obj->fk_anlage;
$this->fk_carrier = $obj->fk_carrier;
$this->start_te = $obj->start_te;
$this->end_te = $obj->end_te;
$this->terminal_side = $obj->terminal_side;
$this->terminal_row = $obj->terminal_row;
$this->color = $obj->color;
$this->bridge_type = $obj->bridge_type;
$this->label = $obj->label;
$this->status = $obj->status;
$this->date_creation = $this->db->jdate($obj->date_creation);
$this->fk_user_creat = $obj->fk_user_creat;
$this->fk_user_modif = $obj->fk_user_modif;
$this->db->free($resql);
return 1;
} else {
$this->db->free($resql);
return 0;
}
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Update object in database
*
* @param User $user User that modifies
* @return int Return integer <0 if KO, >0 if OK
*/
public function update($user)
{
$error = 0;
$this->db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
$sql .= " fk_anlage = ".((int) $this->fk_anlage);
$sql .= ", fk_carrier = ".((int) $this->fk_carrier);
$sql .= ", start_te = ".((int) $this->start_te);
$sql .= ", end_te = ".((int) $this->end_te);
$sql .= ", terminal_side = '".$this->db->escape($this->terminal_side ?: 'top')."'";
$sql .= ", terminal_row = ".((int) $this->terminal_row);
$sql .= ", color = '".$this->db->escape($this->color ?: '#e74c3c')."'";
$sql .= ", bridge_type = '".$this->db->escape($this->bridge_type ?: 'standard')."'";
$sql .= ", label = ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
$sql .= ", status = ".((int) $this->status);
$sql .= ", fk_user_modif = ".((int) $user->id);
$sql .= " WHERE rowid = ".((int) $this->id);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return 1;
}
}
/**
* Delete object in database
*
* @param User $user User that deletes
* @return int Return integer <0 if KO, >0 if OK
*/
public function delete($user)
{
$error = 0;
$this->db->begin();
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return 1;
}
}
/**
* Fetch all bridges for an installation
*
* @param int $anlageId Installation ID
* @param int $carrierId Optional carrier ID filter
* @return array Array of TerminalBridge objects
*/
public function fetchAllByAnlage($anlageId, $carrierId = 0)
{
$results = array();
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE fk_anlage = ".((int) $anlageId);
$sql .= " AND status = 1";
if ($carrierId > 0) {
$sql .= " AND fk_carrier = ".((int) $carrierId);
}
$sql .= " ORDER BY fk_carrier ASC, terminal_side ASC, start_te ASC";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$bridge = new TerminalBridge($this->db);
$bridge->id = $obj->rowid;
$bridge->entity = $obj->entity;
$bridge->fk_anlage = $obj->fk_anlage;
$bridge->fk_carrier = $obj->fk_carrier;
$bridge->start_te = $obj->start_te;
$bridge->end_te = $obj->end_te;
$bridge->terminal_side = $obj->terminal_side;
$bridge->terminal_row = $obj->terminal_row;
$bridge->color = $obj->color;
$bridge->bridge_type = $obj->bridge_type;
$bridge->label = $obj->label;
$bridge->status = $obj->status;
$results[] = $bridge;
}
$this->db->free($resql);
}
return $results;
}
}

View file

@ -821,8 +821,10 @@
border: 1px solid #444 !important;
border-radius: 6px !important;
overflow: visible !important;
display: inline-block !important;
min-width: 100% !important;
display: block !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.kundenkarte-carrier-header {
@ -876,10 +878,13 @@
position: relative !important;
padding: 15px !important;
background: #1e1e1e !important;
overflow: visible !important;
overflow-x: auto !important;
overflow-y: visible !important;
border-radius: 0 0 6px 6px !important;
display: inline-block !important;
min-width: calc(100% - 30px) !important;
display: block !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.kundenkarte-carrier-svg {
@ -1414,7 +1419,7 @@
.kundenkarte-connection-svg {
display: block !important;
min-width: 100% !important;
max-width: 100% !important;
border-radius: 4px !important;
}
@ -1602,6 +1607,14 @@
.schematic-editor-wrapper {
margin-top: 20px !important;
max-width: 100% !important;
overflow-x: auto !important;
}
/* Prevent Schematic Editor from breaking Dolibarr layout */
.kundenkarte-equipment-container {
max-width: 100% !important;
overflow-x: auto !important;
}
.schematic-editor-header {
@ -1638,6 +1651,8 @@
padding: 15px !important;
overflow: auto !important;
min-height: 300px !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.schematic-editor-canvas.expanded {
@ -1660,6 +1675,12 @@
filter: brightness(1.3) !important;
}
.schematic-rail.drop-target {
filter: brightness(1.5) !important;
stroke: #27ae60 !important;
stroke-width: 3px !important;
}
.schematic-rail-bg {
opacity: 0.8 !important;
}

File diff suppressed because it is too large Load diff

View file

@ -230,6 +230,18 @@ AddOutput = Abgang hinzufuegen
AddRail = Sammelschiene hinzufuegen
AddBusbar = Sammelschiene hinzufuegen
Busbar = Sammelschiene
BusbarTypes = Phasenschienen-Typen
NewBusbarType = Neuer Phasenschienen-Typ
DeleteBusbarType = Phasenschienen-Typ loeschen
ConfirmDeleteBusbarType = Moechten Sie den Phasenschienen-Typ "%s" wirklich loeschen?
Phases = Phasen
NumLines = Anzahl Linien
Colors = Farben
DefaultColor = Standardfarbe
LineHeight = Linienhoehe
LineSpacing = Linienabstand
DefaultPosition = Standard-Position
BlockImage = Block-Bild
ConnectionEditor = Verbindungseditor
ConnectionType = Verbindungstyp
Color = Farbe

View file

@ -59,6 +59,11 @@ function kundenkarteAdminPrepareHead()
$head[$h][2] = 'equipment_types';
$h++;
$head[$h][0] = dol_buildpath("/kundenkarte/admin/busbar_types.php", 1);
$head[$h][1] = $langs->trans("BusbarTypes");
$head[$h][2] = 'busbar_types';
$h++;
/*
$head[$h][0] = dol_buildpath("/kundenkarte/admin/myobject_extrafields.php", 1);
$head[$h][1] = $langs->trans("ExtraFields");

49
sql/data_busbar_types.sql Normal file
View file

@ -0,0 +1,49 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
-- Standard busbar types for electrical installations
-- ============================================================================
-- Get the electrical system ID (assuming it's 1, but using subquery to be safe)
-- Note: These are inserted for system_id = 1 (Elektroinstallation)
-- 1-phasige Phasenschienen
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'PS_L1', 'Phasenschiene L1', 'L1', 1, 'L1', 1, '#e74c3c', '#e74c3c', 3, 4, 'below', 1, 10, 1, NOW());
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'PS_L2', 'Phasenschiene L2', 'L2', 1, 'L2', 1, '#2ecc71', '#2ecc71', 3, 4, 'below', 1, 20, 1, NOW());
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'PS_L3', 'Phasenschiene L3', 'L3', 1, 'L3', 1, '#9b59b6', '#9b59b6', 3, 4, 'below', 1, 30, 1, NOW());
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'PS_N', 'Neutralleiter-Schiene', 'N', 1, 'N', 1, '#3498db', '#3498db', 3, 4, 'below', 1, 40, 1, NOW());
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'PS_PE', 'Schutzleiter-Schiene', 'PE', 1, 'PE', 1, '#f1c40f', '#f1c40f', 3, 4, 'below', 1, 50, 1, NOW());
-- 1-phasig + N (Wechselstrom)
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'PS_L1N', 'Phasenschiene L1+N', 'L1N', 1, 'L1N', 2, '#e74c3c,#3498db', '#e74c3c', 3, 4, 'below', 1, 100, 1, NOW());
-- 3-phasig (Drehstrom)
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'PS_3P', 'Phasenschiene 3-phasig', '3P', 1, '3P', 3, '#e74c3c,#2ecc71,#9b59b6', '#e74c3c', 3, 4, 'below', 1, 200, 1, NOW());
-- 3-phasig + N
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'PS_3PN', 'Phasenschiene 3P+N', '3P+N', 1, '3P+N', 4, '#e74c3c,#2ecc71,#9b59b6,#3498db', '#e74c3c', 3, 4, 'below', 1, 300, 1, NOW());
-- 3-phasig + N + PE (Vollausstattung)
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'PS_3PNPE', 'Phasenschiene 3P+N+PE', '3P+N+PE', 1, '3P+N+PE', 5, '#e74c3c,#2ecc71,#9b59b6,#3498db,#f1c40f', '#e74c3c', 3, 4, 'below', 1, 400, 1, NOW());
-- Kammschiene (Gabelverschienung)
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'KS_1P', 'Kammschiene 1-polig', 'KS1', 1, 'L1', 1, '#e74c3c', '#e74c3c', 4, 4, 'above', 1, 500, 1, NOW());
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'KS_3P', 'Kammschiene 3-polig', 'KS3', 1, '3P', 3, '#e74c3c,#2ecc71,#9b59b6', '#e74c3c', 4, 4, 'above', 1, 510, 1, NOW());
INSERT INTO llx_kundenkarte_busbar_type (entity, ref, label, label_short, fk_system, phases, num_lines, color, default_color, line_height, line_spacing, position_default, is_system, position, active, date_creation)
VALUES (0, 'KS_3PN', 'Kammschiene 3P+N', 'KS3N', 1, '3P+N', 4, '#e74c3c,#2ecc71,#9b59b6,#3498db', '#e74c3c', 4, 4, 'above', 1, 520, 1, NOW());

110
sql/data_terminal_types.sql Normal file
View file

@ -0,0 +1,110 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Standard terminal block types (Reihenklemmen-Typen)
-- These are narrow components (0.5 TE) with stacked terminals
-- ============================================================================
-- Note: Run this after the module is installed and you have created a system
-- with code 'STROM' for electrical installations.
-- Adjust fk_system values to match your actual system IDs.
-- Get the electrical system ID (adjust WHERE clause if needed)
-- SET @strom_system = (SELECT rowid FROM llx_c_kundenkarte_anlage_system WHERE code = 'STROM' LIMIT 1);
-- ============================================================================
-- Reihenklemmen / Terminal Blocks (0.5 TE breit)
-- ============================================================================
-- Durchgangsklemme (Feed-through terminal) - 1 input, 1 output
-- Standard 2-point terminal block
INSERT INTO llx_kundenkarte_equipment_type
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
SELECT 0, 'RK_DURCH', 'Durchgangsklemme', 'DK', 'Standard-Durchgangsklemme für Leiterverbindung',
s.rowid, 1, '#7f8c8d',
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0}]}',
'top_to_bottom', 'both', 'fa-square', 0, 100, 1, NOW()
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
-- Dreistockklemme (3-level terminal) - 3 top, 2 bottom
-- Used for phase distribution with multiple connection points
INSERT INTO llx_kundenkarte_equipment_type
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
SELECT 0, 'RK_3STOCK', 'Dreistockklemme', '3ST', 'Dreistöckige Klemme für mehrfache Verbindungen',
s.rowid, 1, '#27ae60',
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t3","label":"3","pos":"top","col":0,"row":1},{"id":"t5","label":"5","pos":"top","col":0,"row":2},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0},{"id":"t4","label":"4","pos":"bottom","col":0,"row":1}]}',
NULL, 'both', 'fa-th-large', 0, 110, 1, NOW()
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
-- Doppelstockklemme (2-level terminal) - 2 top, 2 bottom
INSERT INTO llx_kundenkarte_equipment_type
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
SELECT 0, 'RK_2STOCK', 'Doppelstockklemme', '2ST', 'Zweistöckige Klemme für doppelte Verbindungen',
s.rowid, 1, '#2980b9',
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t3","label":"3","pos":"top","col":0,"row":1},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0},{"id":"t4","label":"4","pos":"bottom","col":0,"row":1}]}',
NULL, 'both', 'fa-th', 0, 105, 1, NOW()
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
-- N-Klemme (Neutral terminal) - blaue Farbe
INSERT INTO llx_kundenkarte_equipment_type
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
SELECT 0, 'RK_N', 'N-Klemme', 'N', 'Neutralleiter-Klemme (blau)',
s.rowid, 1, '#3498db',
'{"terminals":[{"id":"t1","label":"N","pos":"top","col":0,"row":0},{"id":"t2","label":"N","pos":"bottom","col":0,"row":0}]}',
'top_to_bottom', 'both', 'fa-square', 0, 120, 1, NOW()
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
-- PE-Klemme (Protective Earth terminal) - grün-gelb
INSERT INTO llx_kundenkarte_equipment_type
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
SELECT 0, 'RK_PE', 'PE-Klemme', 'PE', 'Schutzleiter-Klemme (grün-gelb)',
s.rowid, 1, '#27ae60',
'{"terminals":[{"id":"t1","label":"PE","pos":"top","col":0,"row":0},{"id":"t2","label":"PE","pos":"bottom","col":0,"row":0}]}',
'top_to_bottom', 'both', 'fa-square', 0, 125, 1, NOW()
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
-- Hauptabzweigklemme (Main junction terminal) - 4 Anschlusspunkte
-- Large terminal for main distribution with 2 inputs and 2 outputs
INSERT INTO llx_kundenkarte_equipment_type
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
SELECT 0, 'RK_HAK', 'Hauptabzweigklemme', 'HAK', 'Hauptabzweigklemme für große Querschnitte mit 4 Anschlüssen',
s.rowid, 2, '#e74c3c',
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t3","label":"3","pos":"top","col":1,"row":0},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0},{"id":"t4","label":"4","pos":"bottom","col":1,"row":0}]}',
NULL, 'both', 'fa-th-large', 0, 130, 1, NOW()
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
-- Trennklemme (Disconnect terminal) - mit Trennmöglichkeit
INSERT INTO llx_kundenkarte_equipment_type
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
SELECT 0, 'RK_TRENN', 'Trennklemme', 'TK', 'Reihenklemme mit Trennmöglichkeit',
s.rowid, 1, '#f39c12',
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0}]}',
'top_to_bottom', 'both', 'fa-minus-square', 0, 115, 1, NOW()
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
-- Sicherungsklemme (Fuse terminal)
INSERT INTO llx_kundenkarte_equipment_type
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
SELECT 0, 'RK_SI', 'Sicherungsklemme', 'SI', 'Reihenklemme mit Sicherungseinsatz',
s.rowid, 1, '#c0392b',
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0}]}',
'top_to_bottom', 'both', 'fa-bolt', 0, 135, 1, NOW()
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
-- LED-Klemme (Terminal with LED indicator)
INSERT INTO llx_kundenkarte_equipment_type
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
SELECT 0, 'RK_LED', 'LED-Klemme', 'LED', 'Reihenklemme mit LED-Anzeige',
s.rowid, 1, '#8e44ad',
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0}]}',
'top_to_bottom', 'both', 'fa-lightbulb-o', 0, 140, 1, NOW()
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;
-- Vierfachklemme (4-level terminal) - für dichte Verdrahtung
INSERT INTO llx_kundenkarte_equipment_type
(entity, ref, label, label_short, description, fk_system, width_te, color, terminals_config, flow_direction, terminal_position, picto, is_system, position, active, date_creation)
SELECT 0, 'RK_4STOCK', 'Vierfachklemme', '4ST', 'Vierstöckige Klemme für sehr dichte Verdrahtung',
s.rowid, 1, '#1abc9c',
'{"terminals":[{"id":"t1","label":"1","pos":"top","col":0,"row":0},{"id":"t3","label":"3","pos":"top","col":0,"row":1},{"id":"t5","label":"5","pos":"top","col":0,"row":2},{"id":"t7","label":"7","pos":"top","col":0,"row":3},{"id":"t2","label":"2","pos":"bottom","col":0,"row":0},{"id":"t4","label":"4","pos":"bottom","col":0,"row":1},{"id":"t6","label":"6","pos":"bottom","col":0,"row":2}]}',
NULL, 'both', 'fa-th', 0, 112, 1, NOW()
FROM llx_c_kundenkarte_anlage_system s WHERE s.code = 'STROM' LIMIT 1;

View file

@ -10,3 +10,43 @@ ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS icon_file VA
-- Add terminals_config if not exists
ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS terminals_config TEXT AFTER fk_product;
-- Add flow_direction and terminal_position for equipment types
ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS flow_direction VARCHAR(16) DEFAULT NULL AFTER terminals_config;
ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS terminal_position VARCHAR(16) DEFAULT 'both' AFTER flow_direction;
-- Add block_image for SchematicEditor display
ALTER TABLE llx_kundenkarte_equipment_type ADD COLUMN IF NOT EXISTS block_image VARCHAR(255) AFTER icon_file;
-- Add busbar type reference to connections
ALTER TABLE llx_kundenkarte_equipment_connection ADD COLUMN IF NOT EXISTS fk_busbar_type INTEGER DEFAULT NULL AFTER is_rail;
-- Create busbar_type table if not exists
CREATE TABLE IF NOT EXISTS llx_kundenkarte_busbar_type
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
entity integer DEFAULT 0 NOT NULL,
ref varchar(64) NOT NULL,
label varchar(255) NOT NULL,
label_short varchar(32),
description text,
fk_system integer NOT NULL,
phases varchar(20) NOT NULL,
num_lines integer DEFAULT 1 NOT NULL,
color varchar(64),
default_color varchar(8),
line_height integer DEFAULT 3,
line_spacing integer DEFAULT 4,
position_default varchar(16) DEFAULT 'below',
fk_product integer DEFAULT NULL,
picto varchar(64),
icon_file varchar(255),
is_system tinyint DEFAULT 0 NOT NULL,
position integer DEFAULT 0,
active tinyint DEFAULT 1 NOT NULL,
date_creation datetime,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
fk_user_creat integer,
fk_user_modif integer,
import_key varchar(14)
) ENGINE=innodb;

View file

@ -0,0 +1,14 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
-- Keys for llx_kundenkarte_busbar_type
-- ============================================================================
ALTER TABLE llx_kundenkarte_busbar_type ADD INDEX idx_busbar_type_ref (ref);
ALTER TABLE llx_kundenkarte_busbar_type ADD INDEX idx_busbar_type_system (fk_system);
ALTER TABLE llx_kundenkarte_busbar_type ADD INDEX idx_busbar_type_active (active);
ALTER TABLE llx_kundenkarte_busbar_type ADD UNIQUE INDEX uk_busbar_type_ref_system (ref, fk_system);
ALTER TABLE llx_kundenkarte_busbar_type ADD CONSTRAINT fk_busbar_type_system
FOREIGN KEY (fk_system) REFERENCES llx_c_kundenkarte_anlage_system(rowid);
ALTER TABLE llx_kundenkarte_busbar_type ADD CONSTRAINT fk_busbar_type_product
FOREIGN KEY (fk_product) REFERENCES llx_product(rowid) ON DELETE SET NULL;

View file

@ -0,0 +1,46 @@
-- ============================================================================
-- Copyright (C) 2026 Alles Watt lauft
--
-- Table for busbar/phase rail types (Phasenschienen/Sammelschienen-Typen)
-- Examples: 3-phasig, 1-phasig, PE, N, etc.
-- ============================================================================
CREATE TABLE llx_kundenkarte_busbar_type
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
entity integer DEFAULT 0 NOT NULL,
ref varchar(64) NOT NULL,
label varchar(255) NOT NULL,
label_short varchar(32),
description text,
fk_system integer NOT NULL,
-- Phasenschienen-spezifische Felder
phases varchar(20) NOT NULL COMMENT 'Phasen-Konfiguration: L1, L2, L3, N, PE, L1N, 3P, 3P+N, etc.',
num_lines integer DEFAULT 1 NOT NULL COMMENT 'Anzahl der Linien/Schienen',
color varchar(64) COMMENT 'Farbcode(s) fuer Darstellung (kommagetrennt fuer mehrere Phasen)',
default_color varchar(8) COMMENT 'Standard-Einzelfarbe fuer die Anzeige',
-- Darstellung
line_height integer DEFAULT 3 COMMENT 'Linienhoehe in Pixel',
line_spacing integer DEFAULT 4 COMMENT 'Abstand zwischen Linien in Pixel',
position_default varchar(16) DEFAULT 'below' COMMENT 'Standard-Position: above, below',
-- Optionale Produkt-Verknuepfung
fk_product integer DEFAULT NULL COMMENT 'Optionales Standard-Dolibarr-Produkt',
picto varchar(64),
icon_file varchar(255) COMMENT 'Uploaded SVG/PNG file for display',
is_system tinyint DEFAULT 0 NOT NULL,
position integer DEFAULT 0,
active tinyint DEFAULT 1 NOT NULL,
date_creation datetime,
tms timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
fk_user_creat integer,
fk_user_modif integer,
import_key varchar(14)
) ENGINE=innodb;

View file

@ -27,8 +27,13 @@ CREATE TABLE llx_kundenkarte_equipment_type
-- Terminal-Konfiguration (JSON)
terminals_config text COMMENT 'JSON config for terminals',
-- Stromrichtung und Terminal-Position
flow_direction varchar(16) DEFAULT NULL COMMENT 'Stromrichtung: NULL=keine, top_to_bottom, bottom_to_top',
terminal_position varchar(16) DEFAULT 'both' COMMENT 'Terminal-Position: both, top_only, bottom_only',
picto varchar(64),
icon_file varchar(255) COMMENT 'Uploaded SVG/PNG file for schematic symbol',
icon_file varchar(255) COMMENT 'Uploaded SVG/PNG file for schematic symbol (PDF)',
block_image varchar(255) COMMENT 'Uploaded image for block display in SchematicEditor',
is_system tinyint DEFAULT 0 NOT NULL,
position integer DEFAULT 0,

View file

@ -0,0 +1,2 @@
-- Copyright (C) 2026 Alles Watt lauft
-- Keys for llx_kundenkarte_terminal_bridge are defined in the main table file

View file

@ -0,0 +1,42 @@
-- Copyright (C) 2026 Alles Watt lauft
--
-- Terminal bridges (Brücken zwischen Reihenklemmen)
-- Connects terminals of adjacent terminal blocks horizontally
CREATE TABLE llx_kundenkarte_terminal_bridge (
rowid INTEGER AUTO_INCREMENT PRIMARY KEY,
entity INTEGER DEFAULT 1 NOT NULL,
fk_anlage INTEGER NOT NULL, -- Installation reference
fk_carrier INTEGER NOT NULL, -- Carrier where bridge is placed
-- Bridge spans from start_te to end_te, connecting terminals horizontally
start_te INTEGER NOT NULL, -- Start position (TE)
end_te INTEGER NOT NULL, -- End position (TE)
-- Terminal position
terminal_side VARCHAR(10) DEFAULT 'top', -- 'top' or 'bottom' side of blocks
terminal_row INTEGER DEFAULT 0, -- Row index for stacked terminals
-- Visual properties
color VARCHAR(20) DEFAULT '#e74c3c', -- Bridge color (red default)
bridge_type VARCHAR(20) DEFAULT 'standard', -- 'standard', 'phase', 'pe', 'n'
label VARCHAR(50), -- Optional label
status INTEGER DEFAULT 1,
date_creation DATETIME,
tms TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
fk_user_creat INTEGER,
fk_user_modif INTEGER
) ENGINE=InnoDB;
-- Indexes
ALTER TABLE llx_kundenkarte_terminal_bridge ADD INDEX idx_bridge_anlage (fk_anlage);
ALTER TABLE llx_kundenkarte_terminal_bridge ADD INDEX idx_bridge_carrier (fk_carrier);
-- Foreign keys
ALTER TABLE llx_kundenkarte_terminal_bridge ADD CONSTRAINT fk_bridge_anlage
FOREIGN KEY (fk_anlage) REFERENCES llx_kundenkarte_anlage(rowid) ON DELETE CASCADE;
ALTER TABLE llx_kundenkarte_terminal_bridge ADD CONSTRAINT fk_bridge_carrier
FOREIGN KEY (fk_carrier) REFERENCES llx_kundenkarte_equipment_carrier(rowid) ON DELETE CASCADE;