diff --git a/README.md b/README.md index 6cdf4c9..70b2019 100755 --- a/README.md +++ b/README.md @@ -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 diff --git a/admin/busbar_types.php b/admin/busbar_types.php new file mode 100644 index 0000000..1cb5bcf --- /dev/null +++ b/admin/busbar_types.php @@ -0,0 +1,438 @@ +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 '
'; +print '
'; +print $langs->trans('System').': '; +print ''; +print ' '; +print ' '.$langs->trans('NewBusbarType').''; +print '
'; +print '
'; + +// 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 '
'; + print ''; + print ''; + if ($action == 'edit') { + print ''; + } + + print ''; + + // Ref + print ''; + print ''; + + // Label + print ''; + print ''; + + // Label Short + print ''; + print ''; + + // Description + print ''; + print ''; + + // System + print ''; + print ''; + + // Phase configuration + print ''; + print ''; + + // Number of lines + print ''; + print ''; + + // Colors + print ''; + print ''; + + // Default color + print ''; + print ''; + + // Line height + print ''; + print ''; + + // Line spacing + print ''; + print ''; + + // Default position + print ''; + print ''; + + // Product link + print ''; + print ''; + + // Position + print ''; + print ''; + + // Preview + print ''; + print ''; + + print '
'.$langs->trans('Ref').'
'.$langs->trans('Label').'
'.$langs->trans('LabelShort').'
'.$langs->trans('Description').'
'.$langs->trans('System').'
'.$langs->trans('Phases').''; + print '
'; + print 'Schnellauswahl:
'; + 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 ''; + print dol_escape_htmltag($preset['label']); + print ''; + } + print '
'; + print ''; + print '
Phasen-Konfiguration: L1, L2, L3, N, PE oder Kombinationen wie L1N, 3P, 3P+N, 3P+N+PE
'; + print '
'.$langs->trans('NumLines').'
'.$langs->trans('Colors').''; + print ''; + print '
Kommagetrennte Farbcodes fuer jede Linie (z.B. #e74c3c,#2ecc71,#9b59b6)
'; + print '
'.$langs->trans('DefaultColor').'
'.$langs->trans('LineHeight').' px
'.$langs->trans('LineSpacing').' px
'.$langs->trans('DefaultPosition').'
'.$langs->trans('LinkedProduct').'
'.$langs->trans('Position').'
'.$langs->trans('Preview').''; + print '
'; + print ''; + print '
'; + print '
'; + + print '
'; + print ''; + print ' '.$langs->trans('Cancel').''; + print '
'; + + print '
'; + + // JavaScript for preset selection and preview + print ''; + +} else { + // List of busbar types + $types = $busbarType->fetchAllBySystem($systemFilter, 0); + + print '
'; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + print ''; + + if (empty($types)) { + print ''; + } else { + foreach ($types as $type) { + print ''; + print ''; + print ''; + print ''; + print ''; + + // Color preview + print ''; + + print ''; + print ''; + + // Status + print ''; + + // Actions + print ''; + print ''; + } + } + + print '
'.$langs->trans('Ref').''.$langs->trans('Label').''.$langs->trans('Phases').''.$langs->trans('Lines').''.$langs->trans('Colors').''.$langs->trans('System').''.$langs->trans('Position').''.$langs->trans('Status').''.$langs->trans('Actions').'
'.$langs->trans('NoRecordFound').'
'.dol_escape_htmltag($type->ref).''.dol_escape_htmltag($type->label); + if ($type->label_short) { + print ' ('.dol_escape_htmltag($type->label_short).')'; + } + print ''.dol_escape_htmltag($type->phases).''.$type->num_lines.''; + $colors = $type->color ? explode(',', $type->color) : array($type->default_color ?: '#e74c3c'); + foreach ($colors as $c) { + print ''; + } + print ''.dol_escape_htmltag($type->system_label).''.$type->position.''; + if ($type->active) { + print ''.$langs->trans('Enabled').''; + } else { + print ''.$langs->trans('Disabled').''; + } + print ''; + print ''; + print img_edit(); + print ' '; + + if ($type->active) { + print ''; + print img_picto($langs->trans('Disable'), 'switch_on'); + print ' '; + } else { + print ''; + print img_picto($langs->trans('Enable'), 'switch_off'); + print ' '; + } + + if (!$type->is_system) { + print ''; + print img_delete(); + print ''; + } + print '
'; + print '
'; +} + +print dol_get_fiche_end(); + +llxFooter(); +$db->close(); diff --git a/admin/equipment_types.php b/admin/equipment_types.php index 52fbd40..0238b17 100644 --- a/admin/equipment_types.php +++ b/admin/equipment_types.php @@ -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 ''; print ''; + // Block Image Upload (for SchematicEditor display) + print ''.$langs->trans('BlockImage').''; + print ''; + print '
'; + + // Preview area + print '
'; + if ($action == 'edit' && $equipmentType->block_image) { + $blockImageUrl = DOL_URL_ROOT.'/document.php?modulepart=kundenkarte&file=block_images/'.urlencode($equipmentType->block_image); + print 'Block Image'; + } else { + print 'Kein
Bild
'; + } + print '
'; + + // Upload controls + print '
'; + print ''; + print ''; + if ($action == 'edit' && $equipmentType->block_image) { + print ' '; + } + + // 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 '
'; + print ''; + print ' '; + print '
'; + } + + print '
Bild wird im SchematicEditor als Block-Hintergrund angezeigt
'; + print '
'; + + print '
'; + print ''; + // Position print ''.$langs->trans('Position').''; print ''; @@ -440,6 +502,28 @@ if (in_array($action, array('create', 'edit'))) { print ''; print ''; + // Terminal Position + print 'Anschlusspunkt-Position'; + print ''; + print ''; + print '
Wo sollen die Anschlusspunkte angezeigt werden?
'; + print ''; + + // Flow Direction + print 'Richtung (Pfeil)'; + print ''; + print ''; + print '
Zeigt einen Richtungspfeil im Block an (z.B. für Typ B FI-Schalter)
'; + print ''; + print ''; // 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 = \'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 = \' 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 = \'Kein
Bild
\'; + 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 = \'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 = \' Löschen\'; + btn.onclick = deleteBlockImage; + document.getElementById("block-image-upload-btn").after(btn); + } + } else { + alert("Fehler: " + data.error); + } + }) + .catch(function(err) { + alert("Fehler: " + err); + }); + }; + } '; print '
'; diff --git a/ajax/equipment.php b/ajax/equipment.php index d58ec97..4860023 100644 --- a/ajax/equipment.php +++ b/ajax/equipment.php @@ -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'; diff --git a/ajax/equipment_connection.php b/ajax/equipment_connection.php index 7617900..4b4332f 100644 --- a/ajax/equipment_connection.php +++ b/ajax/equipment_connection.php @@ -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'; } diff --git a/ajax/equipment_type_block_image.php b/ajax/equipment_type_block_image.php new file mode 100644 index 0000000..24db1a6 --- /dev/null +++ b/ajax/equipment_type_block_image.php @@ -0,0 +1,187 @@ +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('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(); diff --git a/class/busbartype.class.php b/class/busbartype.class.php new file mode 100644 index 0000000..cd97430 --- /dev/null +++ b/class/busbartype.class.php @@ -0,0 +1,390 @@ +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); + } + } +} diff --git a/class/equipment.class.php b/class/equipment.class.php index 55b9866..7427c8d 100644 --- a/class/equipment.class.php +++ b/class/equipment.class.php @@ -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; diff --git a/class/equipmenttype.class.php b/class/equipmenttype.class.php index 2ca16d1..0c491cb 100644 --- a/class/equipmenttype.class.php +++ b/class/equipmenttype.class.php @@ -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; diff --git a/class/terminalbridge.class.php b/class/terminalbridge.class.php new file mode 100644 index 0000000..125f1d7 --- /dev/null +++ b/class/terminalbridge.class.php @@ -0,0 +1,255 @@ +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; + } +} diff --git a/css/kundenkarte.css b/css/kundenkarte.css index d8a170a..3a66628 100755 --- a/css/kundenkarte.css +++ b/css/kundenkarte.css @@ -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; } diff --git a/js/kundenkarte.js b/js/kundenkarte.js index 95c686b..43a6ed9 100755 --- a/js/kundenkarte.js +++ b/js/kundenkarte.js @@ -4439,6 +4439,7 @@ carriers: [], equipment: [], connections: [], + bridges: [], // Terminal bridges (Brücken zwischen Klemmen) selectedTerminal: null, dragState: null, isInitialized: false, @@ -4580,14 +4581,159 @@ self.zoomToFit(); }); - // Busbar click - show edit/delete popup + // Bridge click - show delete popup + $(document).off('click.bridge').on('click.bridge', '.schematic-bridge', function(e) { + e.preventDefault(); + e.stopPropagation(); + var bridgeId = $(this).data('bridge-id'); + self.showBridgePopup(bridgeId, e.clientX, e.clientY); + }); + + // Busbar click - show edit/delete popup (only if not dragging) $(document).off('click.busbar').on('click.busbar', '.schematic-busbar', function(e) { + if (self.busbarDragging) return; // Don't show popup if we just finished dragging e.preventDefault(); e.stopPropagation(); var connectionId = $(this).data('connection-id'); self.showBusbarPopup(connectionId, e.clientX, e.clientY); }); + // Busbar drag - allow repositioning by dragging (keeps width, can move to other carriers) + $(document).off('mousedown.busbarDrag').on('mousedown.busbarDrag', '.schematic-busbar', function(e) { + if (e.button !== 0) return; // Only left mouse button + e.preventDefault(); + e.stopPropagation(); + + var $busbar = $(this); + var connectionId = $busbar.data('connection-id'); + var conn = self.connections.find(function(c) { return String(c.id) === String(connectionId); }); + if (!conn) return; + + // Get carrier for this busbar + var carrier = self.carriers.find(function(c) { return String(c.id) === String(conn.fk_carrier); }); + if (!carrier) return; + + // Calculate busbar width in TE (this should stay constant) + var originalStartTE = parseInt(conn.rail_start_te) || 1; + var originalEndTE = parseInt(conn.rail_end_te) || 1; + var busbarWidthTE = originalEndTE - originalStartTE + 1; + + self.busbarDragData = { + connectionId: connectionId, + conn: conn, + originalCarrier: carrier, + currentCarrier: carrier, + startX: e.clientX, + startY: e.clientY, + originalStartTE: originalStartTE, + originalEndTE: originalEndTE, + busbarWidthTE: busbarWidthTE, // Keep width constant + moved: false + }; + + $busbar.css('cursor', 'grabbing'); + }); + + $(document).off('mousemove.busbarDrag').on('mousemove.busbarDrag', function(e) { + if (!self.busbarDragData) return; + + var data = self.busbarDragData; + var dx = e.clientX - data.startX; + var dy = e.clientY - data.startY; + + // Only start dragging after 5px movement + if (!data.moved && Math.abs(dx) < 5 && Math.abs(dy) < 5) return; + data.moved = true; + self.busbarDragging = true; + + // Find which carrier the mouse is over (based on Y position) + var svgRect = $(self.svgElement)[0].getBoundingClientRect(); + var mouseY = e.clientY - svgRect.top; + var targetCarrier = data.originalCarrier; + + // Check all carriers to find which one we're over + self.carriers.forEach(function(carrier) { + if (typeof carrier._y !== 'undefined') { + var carrierTop = carrier._y - self.BLOCK_HEIGHT / 2 - 50; + var carrierBottom = carrier._y + self.BLOCK_HEIGHT / 2 + 50; + if (mouseY >= carrierTop && mouseY <= carrierBottom) { + targetCarrier = carrier; + } + } + }); + + data.currentCarrier = targetCarrier; + + // Calculate new start TE based on horizontal movement relative to target carrier + var mouseX = e.clientX - svgRect.left; + var relativeX = mouseX - targetCarrier._x; + var newStartTE = Math.round(relativeX / self.TE_WIDTH) + 1; + + // Keep width constant - calculate new end based on fixed width + var newEndTE = newStartTE + data.busbarWidthTE - 1; + + // Clamp to carrier bounds (shift if needed to fit) + var totalTE = parseInt(targetCarrier.total_te) || 12; + if (newStartTE < 1) { + newStartTE = 1; + newEndTE = newStartTE + data.busbarWidthTE - 1; + } + if (newEndTE > totalTE) { + newEndTE = totalTE; + newStartTE = Math.max(1, newEndTE - data.busbarWidthTE + 1); + } + + // Update visual preview + var $busbar = $('.schematic-busbar[data-connection-id="' + data.connectionId + '"]'); + var startX = targetCarrier._x + (newStartTE - 1) * self.TE_WIDTH; + var width = data.busbarWidthTE * self.TE_WIDTH; + + // Calculate new Y position based on target carrier + var posY = parseInt(data.conn.position_y) || 0; + var railCenterY = targetCarrier._y + self.RAIL_HEIGHT / 2; + var blockTop = railCenterY - self.BLOCK_HEIGHT / 2; + var blockBottom = railCenterY + self.BLOCK_HEIGHT / 2; + var busbarHeight = 24; + var newBusbarY; + if (posY === 0) { + newBusbarY = blockTop - busbarHeight - 25; + } else { + newBusbarY = blockBottom + 25; + } + + $busbar.find('rect').each(function(idx) { + $(this).attr('x', startX); + $(this).attr('width', width); + // Update Y position (with shadow offset for second rect) + var yOffset = idx === 0 ? 2 : 0; // Shadow has +2 offset + $(this).attr('y', newBusbarY + yOffset); + }); + + // Store new positions for drop + data.newStartTE = newStartTE; + data.newEndTE = newEndTE; + data.newCarrierId = targetCarrier.id; + }); + + $(document).off('mouseup.busbarDrag').on('mouseup.busbarDrag', function(e) { + if (!self.busbarDragData) return; + + var data = self.busbarDragData; + self.busbarDragData = null; + + $('.schematic-busbar').css('cursor', 'pointer'); + + if (data.moved && data.newStartTE && data.newEndTE) { + // Save new position (and possibly new carrier) + self.updateBusbarPosition(data.connectionId, data.newStartTE, data.newEndTE, data.newCarrierId); + } + + // Clear drag flag after a short delay (to prevent click popup) + setTimeout(function() { + self.busbarDragging = false; + }, 100); + }); + // Rail (Hutschiene) click - show popup for editing $(document).off('click.rail').on('click.rail', '.schematic-rail', function(e) { e.preventDefault(); @@ -4718,6 +4864,160 @@ $('.schematic-busbar-popup').remove(); }, + showBridgePopup: function(bridgeId, x, y) { + var self = this; + this.hideBridgePopup(); + + var bridge = this.bridges.find(function(b) { return String(b.id) === String(bridgeId); }); + if (!bridge) return; + + var html = '
'; + html += '
Brücke
'; + html += '
TE ' + bridge.start_te + ' - ' + bridge.end_te + ' (' + bridge.terminal_side + ')
'; + html += '
'; + html += ''; + html += '
'; + + $('body').append(html); + + $('.bridge-delete-btn').on('click', function() { + var id = $(this).data('id'); + self.hideBridgePopup(); + self.deleteBridge(id); + }); + + // Close on click outside + setTimeout(function() { + $(document).one('click', function() { + self.hideBridgePopup(); + }); + }, 100); + }, + + hideBridgePopup: function() { + $('.schematic-bridge-popup').remove(); + }, + + deleteBridge: function(bridgeId) { + var self = this; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'delete_bridge', + bridge_id: bridgeId, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Brücke gelöscht', 'success'); + // Remove from local data + self.bridges = self.bridges.filter(function(b) { return String(b.id) !== String(bridgeId); }); + self.render(); + } else { + self.showMessage(response.error || 'Fehler beim Löschen', 'error'); + } + } + }); + }, + + createBridge: function(carrierId, startTE, endTE, terminalSide, terminalRow, color) { + var self = this; + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: { + action: 'create_bridge', + anlage_id: this.anlageId, + carrier_id: carrierId, + start_te: startTE, + end_te: endTE, + terminal_side: terminalSide || 'top', + terminal_row: terminalRow || 0, + color: color || '#e74c3c', + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.showMessage('Brücke erstellt', 'success'); + // Add to local data + self.bridges.push(response.bridge); + self.render(); + } else { + self.showMessage(response.error || 'Fehler beim Erstellen', 'error'); + } + } + }); + }, + + showCreateBridgeDialog: function(carrierId, defaultStartTE, defaultEndTE) { + var self = this; + var carrier = this.carriers.find(function(c) { return String(c.id) === String(carrierId); }); + if (!carrier) return; + + var html = '
'; + html += '
'; + html += '

Brücke erstellen

'; + + // Start TE + html += '
'; + html += '
'; + + // End TE + html += '
'; + html += '
'; + + // Terminal Side + html += '
'; + html += '
'; + + // Terminal Row + html += '
'; + html += '
'; + + // Color + html += '
'; + html += '
'; + + // Buttons + html += '
'; + html += ''; + html += ''; + html += '
'; + + $('body').append(html); + + $('.dialog-cancel').on('click', function() { + $('.schematic-dialog-overlay, .schematic-dialog').remove(); + }); + + $('.dialog-save').on('click', function() { + var startTE = parseInt($('.dialog-bridge-start').val()) || 1; + var endTE = parseInt($('.dialog-bridge-end').val()) || 2; + var side = $('.dialog-bridge-side').val(); + var row = parseInt($('.dialog-bridge-row').val()) || 0; + var color = $('.dialog-bridge-color').val(); + + $('.schematic-dialog-overlay, .schematic-dialog').remove(); + self.createBridge(carrierId, startTE, endTE, side, row, color); + }); + + // Close on Escape + $(document).on('keydown.dialog', function(e) { + if (e.key === 'Escape') { + $('.schematic-dialog-overlay, .schematic-dialog').remove(); + $(document).off('keydown.dialog'); + } + }); + }, + showCarrierPopup: function(carrierId, x, y) { var self = this; this.hideCarrierPopup(); @@ -4728,9 +5028,12 @@ var html = '
'; html += '
Hutschiene
'; html += '
' + this.escapeHtml(carrier.label || 'Ohne Name') + ' (' + carrier.total_te + ' TE)
'; + html += '
'; html += '
'; html += ''; html += ''; + html += '
'; + html += ''; html += '
'; $('body').append(html); @@ -4747,6 +5050,12 @@ self.deleteCarrier(id); }); + $('.carrier-add-bridge-btn').on('click', function() { + var id = $(this).data('id'); + self.hideCarrierPopup(); + self.showCreateBridgeDialog(id, 1, 2); + }); + // Close on click outside setTimeout(function() { $(document).one('click', function() { @@ -5063,6 +5372,24 @@ loadConnections: function() { var self = this; + // Load connections and bridges in parallel + var connectionsLoaded = false; + var bridgesLoaded = false; + + var checkComplete = function() { + if (connectionsLoaded && bridgesLoaded) { + // Initialize canvas now that all data is loaded + if (!self.isInitialized) { + self.initCanvas(); + } else { + self.render(); + } + // Reset loading flag + self.isLoading = false; + } + }; + + // Load connections $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', data: { action: 'list_all', anlage_id: this.anlageId }, @@ -5079,19 +5406,31 @@ } }); } - } - // Initialize canvas now that all data is loaded - if (!self.isInitialized) { - self.initCanvas(); - } else { - self.render(); - } - // Reset loading flag - self.isLoading = false; + connectionsLoaded = true; + checkComplete(); }, error: function() { - self.isLoading = false; + connectionsLoaded = true; + checkComplete(); + } + }); + + // Load bridges + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + data: { action: 'list_bridges', anlage_id: this.anlageId }, + dataType: 'json', + success: function(response) { + if (response.success) { + self.bridges = response.bridges || []; + } + bridgesLoaded = true; + checkComplete(); + }, + error: function() { + bridgesLoaded = true; + checkComplete(); } }); }, @@ -5216,6 +5555,7 @@ this.renderRails(); this.renderBlocks(); + this.renderBridges(); this.renderBusbars(); this.renderConnections(); this.renderControls(); @@ -5361,21 +5701,54 @@ // Block group blockHtml += ''; - // Block background with gradient - blockHtml += ''; + // Check if we have a block image + var hasBlockImage = eq.type_block_image && eq.type_block_image_url; - // Label - var labelY = blockHeight / 2; - blockHtml += ''; - blockHtml += self.escapeHtml(eq.type_label_short || eq.label || ''); - blockHtml += ''; + if (hasBlockImage) { + // Render image instead of colored rectangle + // Use xMidYMid slice to fill the block and crop if needed + blockHtml += ''; + // Border around the image block + blockHtml += ''; - // Additional info line - if (eq.label && eq.type_label_short) { - blockHtml += ''; - blockHtml += self.escapeHtml(eq.label); + // Label - zentriert im Block + var labelY = blockHeight / 2; + blockHtml += ''; + blockHtml += self.escapeHtml(eq.type_label_short || eq.label || ''); blockHtml += ''; + + // Additional info line + if (eq.label && eq.type_label_short) { + blockHtml += ''; + blockHtml += self.escapeHtml(eq.label); + blockHtml += ''; + } + + // Richtungspfeile (flow_direction) + var flowDir = eq.type_flow_direction; + if (flowDir) { + var arrowX = blockWidth - 12; + var arrowY1, arrowY2, arrowY3; + if (flowDir === 'top_to_bottom') { + // Pfeil nach unten ↓ + arrowY1 = 10; + arrowY2 = blockHeight - 10; + blockHtml += ''; + blockHtml += ''; + } else if (flowDir === 'bottom_to_top') { + // Pfeil nach oben ↑ + arrowY1 = blockHeight - 10; + arrowY2 = 10; + blockHtml += ''; + blockHtml += ''; + } + } } blockHtml += ''; @@ -5390,51 +5763,197 @@ var widthTE = parseInt(eq.width_te) || 1; - // Check if this equipment is covered by a busbar (hide top terminals if so) - var coveredByBusbar = self.isEquipmentCoveredByBusbar(eq); + // Check if this equipment is covered by a busbar (returns { top: bool, bottom: bool }) + var busbarCoverage = self.isEquipmentCoveredByBusbar(eq); - // Top terminals - im TE-Raster platziert (hide if busbar covers this equipment) + // Terminal-Position aus Equipment-Typ (both, top_only, bottom_only) + var terminalPos = eq.type_terminal_position || 'both'; + var showTopTerminals = (terminalPos === 'both' || terminalPos === 'top_only') && !busbarCoverage.top; + var showBottomTerminals = (terminalPos === 'both' || terminalPos === 'bottom_only') && !busbarCoverage.bottom; + + // Top terminals - im TE-Raster platziert (hide if busbar covers this equipment or terminal_position) + // Now supports stacked terminals with 'row' property (for Reihenklemmen) var terminalColor = '#666'; // Grau wie die Hutschienen - if (!coveredByBusbar) { + var terminalSpacing = 14; // Vertical spacing between stacked terminals + + if (showTopTerminals) { + // Group terminals by column (teIndex) for stacking + var topTerminalsByCol = {}; topTerminals.forEach(function(term, idx) { - // Berechne welches TE dieser Terminal belegt (0-basiert) - var teIndex = idx % widthTE; - // Terminal ist in der Mitte des jeweiligen TE + var teIndex = term.col !== undefined ? term.col : (idx % widthTE); + if (!topTerminalsByCol[teIndex]) topTerminalsByCol[teIndex] = []; + topTerminalsByCol[teIndex].push(term); + }); + + Object.keys(topTerminalsByCol).forEach(function(colKey) { + var colTerminals = topTerminalsByCol[colKey]; + var teIndex = parseInt(colKey); var tx = x + (teIndex * self.TE_WIDTH) + (self.TE_WIDTH / 2); - var ty = y - 7; - terminalHtml += ''; + colTerminals.forEach(function(term, rowIdx) { + var row = term.row !== undefined ? term.row : rowIdx; + var ty = y - 7 - (row * terminalSpacing); - terminalHtml += ''; - terminalHtml += '' + self.escapeHtml(term.label) + ''; - terminalHtml += ''; + terminalHtml += ''; + + terminalHtml += ''; + + // Label - position depends on whether there are stacked terminals + var labelText = self.escapeHtml(term.label); + var labelWidth = labelText.length * 6 + 6; + + if (colTerminals.length > 1) { + // Stacked: place label to the left of the terminal + terminalHtml += ''; + terminalHtml += '' + labelText + ''; + } else { + // Single: place label below in block + terminalHtml += ''; + terminalHtml += '' + labelText + ''; + } + terminalHtml += ''; + }); }); } - // Bottom terminals - im TE-Raster platziert - bottomTerminals.forEach(function(term, idx) { - // Berechne welches TE dieser Terminal belegt (0-basiert) - var teIndex = idx % widthTE; - // Terminal ist in der Mitte des jeweiligen TE - var tx = x + (teIndex * self.TE_WIDTH) + (self.TE_WIDTH / 2); - var ty = y + blockHeight + 7; + // Bottom terminals - im TE-Raster platziert (hide if busbar covers bottom) + // Now supports stacked terminals with 'row' property + if (showBottomTerminals) { + // Group terminals by column (teIndex) for stacking + var bottomTerminalsByCol = {}; + bottomTerminals.forEach(function(term, idx) { + var teIndex = term.col !== undefined ? term.col : (idx % widthTE); + if (!bottomTerminalsByCol[teIndex]) bottomTerminalsByCol[teIndex] = []; + bottomTerminalsByCol[teIndex].push(term); + }); - terminalHtml += ''; + Object.keys(bottomTerminalsByCol).forEach(function(colKey) { + var colTerminals = bottomTerminalsByCol[colKey]; + var teIndex = parseInt(colKey); + var tx = x + (teIndex * self.TE_WIDTH) + (self.TE_WIDTH / 2); - terminalHtml += ''; - terminalHtml += '' + self.escapeHtml(term.label) + ''; - terminalHtml += ''; - }); + colTerminals.forEach(function(term, rowIdx) { + var row = term.row !== undefined ? term.row : rowIdx; + var ty = y + blockHeight + 7 + (row * terminalSpacing); + + terminalHtml += ''; + + terminalHtml += ''; + + // Label - position depends on whether there are stacked terminals + var labelText = self.escapeHtml(term.label); + var labelWidth = labelText.length * 6 + 6; + + if (colTerminals.length > 1) { + // Stacked: place label to the right of the terminal + terminalHtml += ''; + terminalHtml += '' + labelText + ''; + } else { + // Single: place label above in block + terminalHtml += ''; + terminalHtml += '' + labelText + ''; + } + terminalHtml += ''; + }); + }); + } }); $layer.html(blockHtml); $terminalLayer.html(terminalHtml); }, + renderBridges: function() { + // Render terminal bridges (Brücken zwischen Reihenklemmen) + var self = this; + + // Create or get bridge layer (between terminals and busbars) + var $bridgeLayer = $(this.svgElement).find('.schematic-bridges-layer'); + if ($bridgeLayer.length === 0) { + // Insert after terminals layer + var $terminalLayer = $(this.svgElement).find('.schematic-terminals-layer'); + $terminalLayer.after(''); + $bridgeLayer = $(this.svgElement).find('.schematic-bridges-layer'); + } + $bridgeLayer.empty(); + + if (!this.bridges || this.bridges.length === 0) { + return; + } + + var html = ''; + + this.bridges.forEach(function(bridge) { + // Find carrier for this bridge + var carrier = self.carriers.find(function(c) { + return String(c.id) === String(bridge.fk_carrier); + }); + + if (!carrier || typeof carrier._x === 'undefined') { + return; + } + + // Calculate bridge positions + var startTE = parseInt(bridge.start_te) || 1; + var endTE = parseInt(bridge.end_te) || startTE; + var terminalSide = bridge.terminal_side || 'top'; + var terminalRow = parseInt(bridge.terminal_row) || 0; + var color = bridge.color || '#e74c3c'; + + // X positions for bridge endpoints (center of each TE) + var x1 = carrier._x + (startTE - 1) * self.TE_WIDTH + self.TE_WIDTH / 2; + var x2 = carrier._x + (endTE - 1) * self.TE_WIDTH + self.TE_WIDTH / 2; + + // Y position based on carrier and terminal side + var railY = carrier._y; + var blockTop = railY + self.RAIL_HEIGHT / 2 - self.BLOCK_HEIGHT / 2; + var blockBottom = blockTop + self.BLOCK_HEIGHT; + + var terminalSpacing = 14; + var y; + + if (terminalSide === 'top') { + // Bridge above block, at terminal level + y = blockTop - 7 - (terminalRow * terminalSpacing); + } else { + // Bridge below block, at terminal level + y = blockBottom + 7 + (terminalRow * terminalSpacing); + } + + // Bridge is a horizontal bar connecting terminals + var bridgeHeight = 6; + + html += ''; + + // Bridge bar (horizontal rectangle) + html += ''; + html += ''; + + // Optional label + if (bridge.label) { + var labelX = (x1 + x2) / 2; + html += '+'); + // Add Equipment button - positioned left of carrier + var $addEquipment = $(''); $controls.append($addEquipment); - // Copy Equipment button (next to last equipment) + // Copy Equipment button (below the + button) if (lastEquipment) { - // Position copy button right after the + button - var copyBtnX = nextX + 30; - var $copyEquipment = $(''); + var copyBtnY = btnY + 30; + var $copyEquipment = $(''); $controls.append($copyEquipment); } } - // Add Busbar button (at the left side of carrier, above the rail) - var busbarBtnX = carrier._x - 25; - var busbarBtnY = carrier._y - self.BLOCK_HEIGHT / 2 - 35; - var busbarBtnStyle = 'pointer-events:auto;width:24px;height:24px;border-radius:4px;border:2px solid #e74c3c;' + - 'background:#2d2d44;color:#e74c3c;font-size:11px;font-weight:bold;cursor:pointer;' + - 'display:flex;align-items:center;justify-content:center;transition:all 0.2s;'; - var $addBusbar = $(''); + // Add Busbar button (below copy button or + button) - now blue and round like other buttons + var busbarBtnX = btnX; + var busbarBtnY = btnY + (lastEquipment ? 60 : 30); + var $addBusbar = $(''); $controls.append($addBusbar); } else { console.log('Carrier ' + carrier.id + ' (' + carrier.label + '): Missing _x or _y position'); @@ -6356,6 +6872,50 @@ }); }, + updateBusbarPosition: function(connectionId, newStartTE, newEndTE, newCarrierId) { + var self = this; + var data = { + action: 'update_rail_position', + connection_id: connectionId, + rail_start_te: newStartTE, + rail_end_te: newEndTE, + token: $('input[name="token"]').val() + }; + + // Add carrier_id if provided (for moving to different carrier/panel) + if (newCarrierId) { + data.carrier_id = newCarrierId; + } + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment_connection.php', + method: 'POST', + data: data, + dataType: 'json', + success: function(response) { + if (response.success) { + // Update local connection data + var conn = self.connections.find(function(c) { return String(c.id) === String(connectionId); }); + if (conn) { + conn.rail_start_te = newStartTE; + conn.rail_end_te = newEndTE; + if (newCarrierId) { + conn.fk_carrier = newCarrierId; + } + } + self.render(); + } else { + self.showMessage(response.error || 'Fehler beim Verschieben', 'error'); + self.render(); // Re-render to reset visual position + } + }, + error: function() { + self.showMessage('Fehler beim Verschieben', 'error'); + self.render(); + } + }); + }, + showAddEquipmentDialog: function(carrierId) { var self = this; @@ -6470,34 +7030,46 @@ }, // Check if an equipment position is covered by a busbar + // Returns: { top: boolean, bottom: boolean } indicating which terminals are covered isEquipmentCoveredByBusbar: function(eq) { var eqCarrierId = eq.carrier_id || eq.fk_carrier; var eqPosTE = parseInt(eq.position_te) || 1; var eqWidthTE = parseInt(eq.width_te) || 1; var eqEndTE = eqPosTE + eqWidthTE - 1; - // Check all busbars (is_rail connections) that are ABOVE the blocks (position_y=0) - var covered = this.connections.some(function(conn) { - // Must be a rail/busbar - if (!conn.is_rail || conn.is_rail === '0' || conn.is_rail === 0) return false; - if (parseInt(conn.is_rail) !== 1) return false; + var result = { top: false, bottom: false }; - // Must be above the blocks (position_y = 0) - if (parseInt(conn.position_y) !== 0) return false; + this.connections.forEach(function(conn) { + // Must be a rail/busbar + if (!conn.is_rail || conn.is_rail === '0' || conn.is_rail === 0) return; + if (parseInt(conn.is_rail) !== 1) return; // Busbar must be on same carrier - if (String(conn.fk_carrier) !== String(eqCarrierId)) return false; + if (String(conn.fk_carrier) !== String(eqCarrierId)) return; var railStart = parseInt(conn.rail_start_te) || 1; var railEnd = parseInt(conn.rail_end_te) || railStart; + var positionY = parseInt(conn.position_y) || 0; // Check if equipment overlaps with busbar range - // Equipment is covered if its start OR end is within the busbar range var overlaps = !(eqEndTE < railStart || eqPosTE > railEnd); - return overlaps; + if (!overlaps) return; + + // Determine if busbar is above (position_y = 0) or below (position_y > 0) + if (positionY === 0) { + result.top = true; + } else { + result.bottom = true; + } }); - return covered; + return result; + }, + + // Legacy compatibility - returns true if top terminals are covered + isEquipmentCoveredByBusbarLegacy: function(eq) { + var result = this.isEquipmentCoveredByBusbar(eq); + return result.top; }, getTerminals: function(eq) { @@ -8593,21 +9165,91 @@ if (!this.dragState || this.dragState.type !== 'block') return; var dx = e.pageX - this.dragState.startX; - var newTE = this.dragState.originalTE + Math.round(dx / this.TE_WIDTH); + var dy = e.pageY - this.dragState.startY; - // Clamp to valid range - var maxTE = (parseInt(this.dragState.carrier.total_te) || 12) - (parseInt(this.dragState.equipment.width_te) || 1) + 1; + // Find target carrier based on mouse position (supports cross-panel drag) + var $svg = $(this.svgElement); + var svgOffset = $svg.offset(); + var mouseY = (e.pageY - svgOffset.top) / this.scale; + var mouseX = (e.pageX - svgOffset.left) / this.scale; + + // Find closest carrier (checks both Y and X for cross-panel support) + var targetCarrier = this.findClosestCarrier(mouseY, mouseX); + if (!targetCarrier) targetCarrier = this.dragState.carrier; + + // Calculate TE position on target carrier based on absolute X position + var relativeX = mouseX - targetCarrier._x; + var newTE = Math.round(relativeX / this.TE_WIDTH) + 1; + + // Clamp to valid range on TARGET carrier + var maxTE = (parseInt(targetCarrier.total_te) || 12) - (parseInt(this.dragState.equipment.width_te) || 1) + 1; newTE = Math.max(1, Math.min(newTE, maxTE)); - // Preview position - use stored originalX and calculate offset from originalTE - var teOffset = newTE - this.dragState.originalTE; - var newX = this.dragState.originalX + teOffset * this.TE_WIDTH; - var currentY = this.dragState.originalY; - - if (!isNaN(newX) && !isNaN(currentY)) { - this.dragState.element.attr('transform', 'translate(' + newX + ',' + currentY + ')'); + // Calculate visual position + var newX, newY; + if (targetCarrier.id === this.dragState.carrier.id) { + // Same carrier - use offset from original + var teOffset = newTE - this.dragState.originalTE; + newX = this.dragState.originalX + teOffset * this.TE_WIDTH; + newY = this.dragState.originalY; + } else { + // Different carrier - calculate new position + newX = parseFloat(targetCarrier._x) + (newTE - 1) * this.TE_WIDTH + 2; + // Block Y position: carrier Y - half block height (block sits on rail) + newY = parseFloat(targetCarrier._y) - this.BLOCK_HEIGHT / 2; } + + if (!isNaN(newX) && !isNaN(newY)) { + this.dragState.element.attr('transform', 'translate(' + newX + ',' + newY + ')'); + } + + // Store target info this.dragState.newTE = newTE; + this.dragState.targetCarrier = targetCarrier; + + // Highlight target carrier + this.highlightTargetCarrier(targetCarrier.id); + }, + + findClosestCarrier: function(mouseY, mouseX) { + var self = this; + var closest = null; + var closestDist = Infinity; + + this.carriers.forEach(function(carrier) { + if (typeof carrier._y === 'undefined' || typeof carrier._x === 'undefined') return; + var carrierY = parseFloat(carrier._y); + var carrierX = parseFloat(carrier._x); + var carrierWidth = (carrier.total_te || 12) * self.TE_WIDTH; + + // Calculate distance - prioritize Y but also check X is within carrier bounds + var distY = Math.abs(mouseY - carrierY); + + // Check if mouseX is within carrier X range (with some tolerance) + var tolerance = 50; + var inXRange = mouseX >= (carrierX - tolerance) && mouseX <= (carrierX + carrierWidth + tolerance); + + // Use combined distance for carriers in range, otherwise penalize + var dist = inXRange ? distY : distY + 1000; + + if (dist < closestDist) { + closestDist = dist; + closest = carrier; + } + }); + + // Only return if within reasonable distance (half rail spacing) + if (closestDist < this.RAIL_SPACING / 2) { + return closest; + } + return null; + }, + + highlightTargetCarrier: function(carrierId) { + // Remove previous highlights + $('.schematic-rail').removeClass('drop-target'); + // Add highlight to target + $('.schematic-rail[data-carrier-id="' + carrierId + '"]').addClass('drop-target'); }, endDragBlock: function(e) { @@ -8615,11 +9257,29 @@ var newTE = this.dragState.newTE || this.dragState.originalTE; var element = this.dragState.element; + var targetCarrier = this.dragState.targetCarrier || this.dragState.carrier; + var originalCarrier = this.dragState.carrier; - if (newTE !== this.dragState.originalTE) { + // Remove highlight + $('.schematic-rail').removeClass('drop-target'); + + // Check if carrier changed or position changed + var carrierChanged = String(targetCarrier.id) !== String(originalCarrier.id); + var positionChanged = newTE !== this.dragState.originalTE; + + console.log('endDragBlock: carrierChanged=', carrierChanged, 'positionChanged=', positionChanged, + 'newTE=', newTE, 'originalTE=', this.dragState.originalTE, + 'targetCarrier=', targetCarrier.id, 'originalCarrier=', originalCarrier.id); + + if (carrierChanged) { + // Move to different carrier + this.moveEquipmentToCarrier(this.dragState.equipment.id, targetCarrier.id, newTE); + } else if (positionChanged) { + // Same carrier, different position this.updateEquipmentPosition(this.dragState.equipment.id, newTE); } else { - // Position didn't change, reset visual position immediately + // No change, reset visual position + console.log('No change detected, resetting position'); element.attr('transform', 'translate(' + this.dragState.originalX + ',' + this.dragState.originalY + ')'); } @@ -8627,9 +9287,45 @@ this.dragState = null; }, + moveEquipmentToCarrier: function(eqId, newCarrierId, newTE) { + var self = this; + + console.log('moveEquipmentToCarrier:', eqId, 'to carrier', newCarrierId, 'position', newTE); + + $.ajax({ + url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', + method: 'POST', + data: { + action: 'move_to_carrier', + equipment_id: eqId, + carrier_id: newCarrierId, + position_te: newTE, + token: $('input[name="token"]').val() + }, + dataType: 'json', + success: function(response) { + console.log('moveEquipmentToCarrier response:', response); + if (response.success) { + self.showMessage('Equipment verschoben', 'success'); + self.loadData(); // Reload to update all positions + } else { + self.showMessage(response.error || 'Fehler beim Verschieben', 'error'); + self.render(); + } + }, + error: function(xhr, status, error) { + console.log('moveEquipmentToCarrier error:', status, error, xhr.responseText); + self.showMessage('Netzwerkfehler', 'error'); + self.render(); + } + }); + }, + updateEquipmentPosition: function(eqId, newTE) { var self = this; + console.log('updateEquipmentPosition:', eqId, newTE); + $.ajax({ url: baseUrl + '/custom/kundenkarte/ajax/equipment.php', method: 'POST', diff --git a/langs/de_DE/kundenkarte.lang b/langs/de_DE/kundenkarte.lang index 185d58a..9354e7c 100755 --- a/langs/de_DE/kundenkarte.lang +++ b/langs/de_DE/kundenkarte.lang @@ -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 diff --git a/lib/kundenkarte.lib.php b/lib/kundenkarte.lib.php index 80b43ba..0b2c08a 100755 --- a/lib/kundenkarte.lib.php +++ b/lib/kundenkarte.lib.php @@ -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"); diff --git a/sql/data_busbar_types.sql b/sql/data_busbar_types.sql new file mode 100644 index 0000000..cd42ca2 --- /dev/null +++ b/sql/data_busbar_types.sql @@ -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()); diff --git a/sql/data_terminal_types.sql b/sql/data_terminal_types.sql new file mode 100644 index 0000000..c12f00c --- /dev/null +++ b/sql/data_terminal_types.sql @@ -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; diff --git a/sql/dolibarr_allversions.sql b/sql/dolibarr_allversions.sql index 4c99eda..793a957 100755 --- a/sql/dolibarr_allversions.sql +++ b/sql/dolibarr_allversions.sql @@ -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; diff --git a/sql/llx_kundenkarte_busbar_type.key.sql b/sql/llx_kundenkarte_busbar_type.key.sql new file mode 100644 index 0000000..97f1975 --- /dev/null +++ b/sql/llx_kundenkarte_busbar_type.key.sql @@ -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; diff --git a/sql/llx_kundenkarte_busbar_type.sql b/sql/llx_kundenkarte_busbar_type.sql new file mode 100644 index 0000000..2bcfe89 --- /dev/null +++ b/sql/llx_kundenkarte_busbar_type.sql @@ -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; diff --git a/sql/llx_kundenkarte_equipment_type.sql b/sql/llx_kundenkarte_equipment_type.sql index 98139cd..9e06e8f 100644 --- a/sql/llx_kundenkarte_equipment_type.sql +++ b/sql/llx_kundenkarte_equipment_type.sql @@ -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, diff --git a/sql/llx_kundenkarte_terminal_bridge.key.sql b/sql/llx_kundenkarte_terminal_bridge.key.sql new file mode 100644 index 0000000..f4197d0 --- /dev/null +++ b/sql/llx_kundenkarte_terminal_bridge.key.sql @@ -0,0 +1,2 @@ +-- Copyright (C) 2026 Alles Watt lauft +-- Keys for llx_kundenkarte_terminal_bridge are defined in the main table file diff --git a/sql/llx_kundenkarte_terminal_bridge.sql b/sql/llx_kundenkarte_terminal_bridge.sql new file mode 100644 index 0000000..3db1aa9 --- /dev/null +++ b/sql/llx_kundenkarte_terminal_bridge.sql @@ -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;