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 '
';
+
+// 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 '';
+
+ // JavaScript for preset selection and preview
+ print '';
+
+} else {
+ // List of busbar types
+ $types = $busbarType->fetchAllBySystem($systemFilter, 0);
+
+ print '';
+ print '
';
+ print '';
+ print '| '.$langs->trans('Ref').' | ';
+ print ''.$langs->trans('Label').' | ';
+ print ''.$langs->trans('Phases').' | ';
+ print ''.$langs->trans('Lines').' | ';
+ print ''.$langs->trans('Colors').' | ';
+ print ''.$langs->trans('System').' | ';
+ print ''.$langs->trans('Position').' | ';
+ print ''.$langs->trans('Status').' | ';
+ print ''.$langs->trans('Actions').' | ';
+ print '
';
+
+ if (empty($types)) {
+ print '| '.$langs->trans('NoRecordFound').' |
';
+ } else {
+ foreach ($types as $type) {
+ print '';
+ print '| '.dol_escape_htmltag($type->ref).' | ';
+ print ''.dol_escape_htmltag($type->label);
+ if ($type->label_short) {
+ print ' ('.dol_escape_htmltag($type->label_short).')';
+ }
+ print ' | ';
+ print ''.dol_escape_htmltag($type->phases).' | ';
+ print ''.$type->num_lines.' | ';
+
+ // Color preview
+ print '';
+ $colors = $type->color ? explode(',', $type->color) : array($type->default_color ?: '#e74c3c');
+ foreach ($colors as $c) {
+ print '';
+ }
+ print ' | ';
+
+ print ''.dol_escape_htmltag($type->system_label).' | ';
+ print ''.$type->position.' | ';
+
+ // Status
+ print '';
+ if ($type->active) {
+ print ''.$langs->trans('Enabled').'';
+ } else {
+ print ''.$langs->trans('Disabled').'';
+ }
+ print ' | ';
+
+ // Actions
+ 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 '
';
+ 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 '  ';
+ } 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 = \'
\';
+
+ // 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 = \'
\';
+
+ // 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('