feat: Firmen-Werkzeuge, Zubehör-System und Produkt-Zuordnung

- Neue Seite werkzeuge.php mit Baumansicht für Firmen-Maschinen/Werkzeuge
- Menüpunkt "Firmen-Werkzeuge" unter Start-Menü
- Neue Klasse AnlageAccessory für Zubehör/Ersatzteile pro Anlage
- AJAX-Endpunkt ajax/anlage_accessory.php (CRUD + Lieferantenbestellung)
- DB: fk_product auf Anlage, has_accessories auf AnlageType, Zubehör-Tabelle
- Neues System WERKZEUG in Systemkategorien
- Admin: Checkbox "Hat Zubehör" im Typ-Editor
- Produkt-Autocomplete, Zubehör-Liste mit Bestellfunktion (CommandeFournisseur)
- Produkt-JOIN in fetchChildren für product_ref im Baum
- Übersetzungen de_DE + en_US

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-03-03 20:23:29 +01:00
parent 66abaa088d
commit a14b33b7c7
9 changed files with 1520 additions and 5 deletions

8
admin/anlage_types.php Executable file → Normal file
View file

@ -63,6 +63,7 @@ if ($action == 'add') {
$anlageType->can_be_nested = GETPOSTINT('can_be_nested'); $anlageType->can_be_nested = GETPOSTINT('can_be_nested');
$anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml')); $anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml'));
$anlageType->can_have_equipment = GETPOSTINT('can_have_equipment'); $anlageType->can_have_equipment = GETPOSTINT('can_have_equipment');
$anlageType->has_accessories = GETPOSTINT('has_accessories');
$anlageType->picto = GETPOST('picto', 'alphanohtml'); $anlageType->picto = GETPOST('picto', 'alphanohtml');
$anlageType->color = GETPOST('color', 'alphanohtml'); $anlageType->color = GETPOST('color', 'alphanohtml');
$anlageType->position = GETPOSTINT('position'); $anlageType->position = GETPOSTINT('position');
@ -113,6 +114,7 @@ if ($action == 'update') {
$anlageType->can_be_nested = GETPOSTINT('can_be_nested'); $anlageType->can_be_nested = GETPOSTINT('can_be_nested');
$anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml')); $anlageType->allowed_parent_types = preg_replace('/[^A-Z0-9_,]/i', '', GETPOST('allowed_parent_types', 'nohtml'));
$anlageType->can_have_equipment = GETPOSTINT('can_have_equipment'); $anlageType->can_have_equipment = GETPOSTINT('can_have_equipment');
$anlageType->has_accessories = GETPOSTINT('has_accessories');
$anlageType->picto = GETPOST('picto', 'alphanohtml'); $anlageType->picto = GETPOST('picto', 'alphanohtml');
$anlageType->color = GETPOST('color', 'alphanohtml'); $anlageType->color = GETPOST('color', 'alphanohtml');
$anlageType->position = GETPOSTINT('position'); $anlageType->position = GETPOSTINT('position');
@ -166,6 +168,7 @@ if ($action == 'copy' && $typeId > 0) {
$newType->can_be_nested = $sourceType->can_be_nested; $newType->can_be_nested = $sourceType->can_be_nested;
$newType->allowed_parent_types = $sourceType->allowed_parent_types; $newType->allowed_parent_types = $sourceType->allowed_parent_types;
$newType->can_have_equipment = $sourceType->can_have_equipment; $newType->can_have_equipment = $sourceType->can_have_equipment;
$newType->has_accessories = $sourceType->has_accessories;
$newType->picto = $sourceType->picto; $newType->picto = $sourceType->picto;
$newType->color = $sourceType->color; $newType->color = $sourceType->color;
$newType->position = $sourceType->position + 1; $newType->position = $sourceType->position + 1;
@ -402,6 +405,11 @@ if (in_array($action, array('create', 'edit'))) {
print '<td><input type="checkbox" name="can_have_equipment" value="1"'.($anlageType->can_have_equipment ? ' checked' : '').'>'; print '<td><input type="checkbox" name="can_have_equipment" value="1"'.($anlageType->can_have_equipment ? ' checked' : '').'>';
print ' <span class="opacitymedium">('.$langs->trans('CanHaveEquipmentHelp').')</span></td></tr>'; print ' <span class="opacitymedium">('.$langs->trans('CanHaveEquipmentHelp').')</span></td></tr>';
// Hat Zubehör (Zubehör/Ersatzteile zuordnen)
print '<tr><td>'.$langs->trans('HasAccessories').'</td>';
print '<td><input type="checkbox" name="has_accessories" value="1"'.($anlageType->has_accessories ? ' checked' : '').'>';
print ' <span class="opacitymedium">('.$langs->trans('HasAccessoriesHelp').')</span></td></tr>';
// Allowed parent types - with multi-select UI // Allowed parent types - with multi-select UI
print '<tr><td>'.$langs->trans('AllowedParentTypes').'</td>'; print '<tr><td>'.$langs->trans('AllowedParentTypes').'</td>';
print '<td>'; print '<td>';

154
ajax/anlage_accessory.php Normal file
View file

@ -0,0 +1,154 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* AJAX-Endpunkt für Anlagen-Zubehör Operationen
*/
if (!defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL', '1');
if (!defined('NOREQUIREMENU')) define('NOREQUIREMENU', '1');
if (!defined('NOREQUIREHTML')) define('NOREQUIREHTML', '1');
if (!defined('NOREQUIREAJAX')) define('NOREQUIREAJAX', '1');
$res = 0;
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res && file_exists("../../../main.inc.php")) $res = @include "../../../main.inc.php";
if (!$res) die("Include of main fails");
dol_include_once('/kundenkarte/class/anlageaccessory.class.php');
header('Content-Type: application/json; charset=UTF-8');
$langs->loadLangs(array('kundenkarte@kundenkarte'));
$action = GETPOST('action', 'aZ09');
$response = array('success' => false, 'error' => '');
// Berechtigungsprüfung
if (!$user->hasRight('kundenkarte', 'read')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
echo json_encode($response);
exit;
}
$accessory = new AnlageAccessory($db);
switch ($action) {
case 'list':
// Alle Zubehörteile einer Anlage laden
$anlageId = GETPOSTINT('fk_anlage');
if ($anlageId > 0) {
$accessories = $accessory->fetchAllByAnlage($anlageId);
$result = array();
foreach ($accessories as $acc) {
$result[] = array(
'id' => $acc->id,
'fk_product' => $acc->fk_product,
'product_ref' => $acc->product_ref,
'product_label' => $acc->product_label,
'product_price' => $acc->product_price,
'qty' => $acc->qty,
'note' => $acc->note,
);
}
$response['success'] = true;
$response['accessories'] = $result;
} else {
$response['error'] = 'Missing fk_anlage';
}
break;
case 'add':
// Zubehör hinzufügen
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
$accessory->fk_anlage = GETPOSTINT('fk_anlage');
$accessory->fk_product = GETPOSTINT('fk_product');
$accessory->qty = GETPOSTINT('qty') > 0 ? GETPOSTINT('qty') : 1;
$accessory->note = GETPOST('note', 'alphanohtml');
$result = $accessory->create($user);
if ($result > 0) {
$response['success'] = true;
$response['id'] = $result;
} else {
$response['error'] = $accessory->error ?: 'Fehler beim Speichern';
}
break;
case 'update':
// Zubehör aktualisieren (Menge, Notiz)
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
$id = GETPOSTINT('id');
if ($id > 0 && $accessory->fetch($id) > 0) {
$accessory->qty = GETPOSTINT('qty') > 0 ? GETPOSTINT('qty') : $accessory->qty;
$accessory->note = GETPOST('note', 'alphanohtml');
$result = $accessory->update($user);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Speichern';
}
} else {
$response['error'] = $langs->trans('ErrorRecordNotFound');
}
break;
case 'delete':
// Zubehör löschen
if (!$user->hasRight('kundenkarte', 'delete')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
$id = GETPOSTINT('id');
if ($id > 0 && $accessory->fetch($id) > 0) {
$result = $accessory->delete($user);
if ($result > 0) {
$response['success'] = true;
} else {
$response['error'] = 'Fehler beim Löschen';
}
} else {
$response['error'] = $langs->trans('ErrorRecordNotFound');
}
break;
case 'order':
// Lieferantenbestellung aus Zubehör erstellen
if (!$user->hasRight('kundenkarte', 'write')) {
$response['error'] = $langs->trans('ErrorPermissionDenied');
break;
}
$anlageId = GETPOSTINT('fk_anlage');
$supplierId = GETPOSTINT('supplier_id');
$idsRaw = GETPOST('ids', 'array');
if ($anlageId > 0 && $supplierId > 0 && !empty($idsRaw)) {
$ids = array_map('intval', $idsRaw);
$result = $accessory->generateSupplierOrder($user, $supplierId, $anlageId, $ids);
if ($result > 0) {
$response['success'] = true;
$response['order_id'] = $result;
} else {
$response['error'] = $accessory->error ?: 'Fehler beim Erstellen der Bestellung';
}
} else {
$response['error'] = 'Fehlende Parameter (Anlage, Lieferant, IDs)';
}
break;
default:
$response['error'] = 'Unknown action';
}
echo json_encode($response);

View file

@ -24,6 +24,7 @@ class Anlage extends CommonObject
public $fk_parent; public $fk_parent;
public $fk_system; public $fk_system;
public $fk_building_node; public $fk_building_node;
public $fk_product;
public $manufacturer; public $manufacturer;
public $model; public $model;
@ -96,7 +97,7 @@ class Anlage extends CommonObject
$this->db->begin(); $this->db->begin();
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, ref, label, fk_soc, fk_contact, fk_anlage_type, fk_parent, fk_system,"; $sql .= "entity, ref, label, fk_soc, fk_contact, fk_anlage_type, fk_parent, fk_system, fk_product,";
$sql .= " manufacturer, model, serial_number, power_rating, field_values,"; $sql .= " manufacturer, model, serial_number, power_rating, field_values,";
$sql .= " location, installation_date, warranty_until,"; $sql .= " location, installation_date, warranty_until,";
$sql .= " rang, level, note_private, note_public, status,"; $sql .= " rang, level, note_private, note_public, status,";
@ -110,6 +111,7 @@ class Anlage extends CommonObject
$sql .= ", ".((int) $this->fk_anlage_type); $sql .= ", ".((int) $this->fk_anlage_type);
$sql .= ", ".((int) ($this->fk_parent > 0 ? $this->fk_parent : 0)); $sql .= ", ".((int) ($this->fk_parent > 0 ? $this->fk_parent : 0));
$sql .= ", ".((int) $this->fk_system); $sql .= ", ".((int) $this->fk_system);
$sql .= ", ".($this->fk_product > 0 ? (int) $this->fk_product : "NULL");
$sql .= ", ".($this->manufacturer ? "'".$this->db->escape($this->manufacturer)."'" : "NULL"); $sql .= ", ".($this->manufacturer ? "'".$this->db->escape($this->manufacturer)."'" : "NULL");
$sql .= ", ".($this->model ? "'".$this->db->escape($this->model)."'" : "NULL"); $sql .= ", ".($this->model ? "'".$this->db->escape($this->model)."'" : "NULL");
$sql .= ", ".($this->serial_number ? "'".$this->db->escape($this->serial_number)."'" : "NULL"); $sql .= ", ".($this->serial_number ? "'".$this->db->escape($this->serial_number)."'" : "NULL");
@ -203,6 +205,7 @@ class Anlage extends CommonObject
$this->fk_parent = $obj->fk_parent; $this->fk_parent = $obj->fk_parent;
$this->fk_system = $obj->fk_system; $this->fk_system = $obj->fk_system;
$this->fk_building_node = isset($obj->fk_building_node) ? (int) $obj->fk_building_node : 0; $this->fk_building_node = isset($obj->fk_building_node) ? (int) $obj->fk_building_node : 0;
$this->fk_product = isset($obj->fk_product) ? (int) $obj->fk_product : null;
$this->manufacturer = $obj->manufacturer; $this->manufacturer = $obj->manufacturer;
$this->model = $obj->model; $this->model = $obj->model;
@ -231,8 +234,14 @@ class Anlage extends CommonObject
$this->type_label = $obj->type_label; $this->type_label = $obj->type_label;
$this->type_short = $obj->type_short; $this->type_short = $obj->type_short;
$this->type_picto = $obj->type_picto; $this->type_picto = $obj->type_picto;
$this->type_color = isset($obj->type_color) ? $obj->type_color : '';
$this->type_can_have_children = isset($obj->type_can_have_children) ? (int) $obj->type_can_have_children : 0; $this->type_can_have_children = isset($obj->type_can_have_children) ? (int) $obj->type_can_have_children : 0;
$this->type_can_have_equipment = isset($obj->type_can_have_equipment) ? (int) $obj->type_can_have_equipment : 0; $this->type_can_have_equipment = isset($obj->type_can_have_equipment) ? (int) $obj->type_can_have_equipment : 0;
$this->type_has_accessories = isset($obj->type_has_accessories) ? (int) $obj->type_has_accessories : 0;
// Produkt-Info (aus JOIN)
$this->product_ref = isset($obj->product_ref) ? $obj->product_ref : '';
$this->product_label = isset($obj->product_label) ? $obj->product_label : '';
// System info // System info
$this->system_label = $obj->system_label; $this->system_label = $obj->system_label;
@ -291,6 +300,7 @@ class Anlage extends CommonObject
$sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL"); $sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
$sql .= ", note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL"); $sql .= ", note_public = ".($this->note_public ? "'".$this->db->escape($this->note_public)."'" : "NULL");
$sql .= ", status = ".((int) $this->status); $sql .= ", status = ".((int) $this->status);
$sql .= ", fk_product = ".($this->fk_product > 0 ? (int) $this->fk_product : "NULL");
$sql .= ", decommissioned = ".((int) $this->decommissioned); $sql .= ", decommissioned = ".((int) $this->decommissioned);
$sql .= ", date_decommissioned = ".($this->date_decommissioned ? "'".$this->db->escape($this->date_decommissioned)."'" : "NULL"); $sql .= ", date_decommissioned = ".($this->date_decommissioned ? "'".$this->db->escape($this->date_decommissioned)."'" : "NULL");
$sql .= ", fk_user_modif = ".((int) $user->id); $sql .= ", fk_user_modif = ".((int) $user->id);
@ -407,9 +417,11 @@ class Anlage extends CommonObject
$results = array(); $results = array();
$sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto,"; $sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto, t.color as type_color,";
$sql .= " t.can_have_children as type_can_have_children, t.can_have_equipment as type_can_have_equipment,"; $sql .= " t.can_have_children as type_can_have_children, t.can_have_equipment as type_can_have_equipment,";
$sql .= " t.has_accessories as type_has_accessories,";
$sql .= " s.label as system_label, s.code as system_code,"; $sql .= " s.label as system_label, s.code as system_code,";
$sql .= " p.ref as product_ref, p.label as product_label,";
// Count images // Count images
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type = 'image') as image_count,"; $sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type = 'image') as image_count,";
// Count documents (pdf + document) // Count documents (pdf + document)
@ -417,6 +429,7 @@ class Anlage extends CommonObject
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a"; $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage_type as t ON a.fk_anlage_type = t.rowid"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage_type as t ON a.fk_anlage_type = t.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON a.fk_system = s.rowid"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON a.fk_system = s.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON a.fk_product = p.rowid";
$sql .= " WHERE a.fk_parent = ".((int) $parentId); $sql .= " WHERE a.fk_parent = ".((int) $parentId);
$sql .= " AND a.entity = ".((int) $conf->entity); $sql .= " AND a.entity = ".((int) $conf->entity);
$sql .= " AND a.status = 1"; $sql .= " AND a.status = 1";
@ -717,9 +730,11 @@ class Anlage extends CommonObject
$results = array(); $results = array();
$sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto,"; $sql = "SELECT a.*, t.label as type_label, t.label_short as type_short, t.picto as type_picto, t.color as type_color,";
$sql .= " t.can_have_children as type_can_have_children, t.can_have_equipment as type_can_have_equipment,"; $sql .= " t.can_have_children as type_can_have_children, t.can_have_equipment as type_can_have_equipment,";
$sql .= " t.has_accessories as type_has_accessories,";
$sql .= " s.label as system_label, s.code as system_code,"; $sql .= " s.label as system_label, s.code as system_code,";
$sql .= " p.ref as product_ref, p.label as product_label,";
// Count images // Count images
$sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type = 'image') as image_count,"; $sql .= " (SELECT COUNT(*) FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_files f WHERE f.fk_anlage = a.rowid AND f.file_type = 'image') as image_count,";
// Count documents (pdf + document) // Count documents (pdf + document)
@ -727,6 +742,7 @@ class Anlage extends CommonObject
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a"; $sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage_type as t ON a.fk_anlage_type = t.rowid"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage_type as t ON a.fk_anlage_type = t.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON a.fk_system = s.rowid"; $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system as s ON a.fk_system = s.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON a.fk_product = p.rowid";
$sql .= " WHERE a.fk_parent = ".((int) $parentId); $sql .= " WHERE a.fk_parent = ".((int) $parentId);
$sql .= " AND a.entity = ".((int) $conf->entity); $sql .= " AND a.entity = ".((int) $conf->entity);
$sql .= " AND a.status = 1"; $sql .= " AND a.status = 1";

View file

@ -0,0 +1,391 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*/
/**
* Class AnlageAccessory
* Verwaltet Zubehör/Ersatzteile für Anlagen-Elemente
*/
class AnlageAccessory extends CommonObject
{
public $element = 'anlageaccessory';
public $table_element = 'kundenkarte_anlage_accessory';
public $fk_anlage;
public $fk_product;
public $qty;
public $rang;
public $note;
public $date_creation;
public $fk_user_creat;
// Geladene Objekte (aus JOIN)
public $product_ref;
public $product_label;
public $product_price;
public $product_fk_unit;
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Zubehör erstellen
*
* @param User $user Benutzer
* @return int <0 bei Fehler, ID bei Erfolg
*/
public function create($user)
{
$error = 0;
$now = dol_now();
if (empty($this->fk_anlage) || empty($this->fk_product)) {
$this->error = 'ErrorMissingParameters';
return -1;
}
// Prüfen ob bereits vorhanden
if ($this->alreadyExists($this->fk_anlage, $this->fk_product)) {
$this->error = 'ErrorAccessoryAlreadyExists';
return -2;
}
$this->db->begin();
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "fk_anlage, fk_product, qty, rang, note,";
$sql .= " date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= ((int) $this->fk_anlage);
$sql .= ", ".((int) $this->fk_product);
$sql .= ", ".((float) ($this->qty > 0 ? $this->qty : 1));
$sql .= ", ".((int) $this->rang);
$sql .= ", ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL");
$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;
}
}
/**
* Zubehör laden
*
* @param int $id ID
* @return int <0 bei Fehler, 0 nicht gefunden, >0 OK
*/
public function fetch($id)
{
$sql = "SELECT a.*, p.ref as product_ref, p.label as product_label, p.price as product_price, p.fk_unit as product_fk_unit";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON a.fk_product = p.rowid";
$sql .= " WHERE a.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->fk_anlage = $obj->fk_anlage;
$this->fk_product = $obj->fk_product;
$this->qty = $obj->qty;
$this->rang = $obj->rang;
$this->note = $obj->note;
$this->date_creation = $this->db->jdate($obj->date_creation);
$this->fk_user_creat = $obj->fk_user_creat;
$this->product_ref = $obj->product_ref;
$this->product_label = $obj->product_label;
$this->product_price = $obj->product_price;
$this->product_fk_unit = $obj->product_fk_unit;
$this->db->free($resql);
return 1;
} else {
$this->db->free($resql);
return 0;
}
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Zubehör aktualisieren
*
* @param User $user Benutzer
* @return int <0 bei Fehler, >0 OK
*/
public function update($user)
{
$error = 0;
$this->db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
$sql .= " qty = ".((float) $this->qty);
$sql .= ", rang = ".((int) $this->rang);
$sql .= ", note = ".($this->note ? "'".$this->db->escape($this->note)."'" : "NULL");
$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;
}
}
/**
* Zubehör löschen
*
* @param User $user Benutzer
* @return int <0 bei Fehler, >0 OK
*/
public function delete($user)
{
$this->db->begin();
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE rowid = ".((int) $this->id);
$resql = $this->db->query($sql);
if (!$resql) {
$this->db->rollback();
$this->error = $this->db->lasterror();
return -1;
}
$this->db->commit();
return 1;
}
/**
* Alle Zubehörteile einer Anlage laden
*
* @param int $anlageId Anlage-ID
* @return array Array von AnlageAccessory-Objekten
*/
public function fetchAllByAnlage($anlageId)
{
$results = array();
$sql = "SELECT a.*, p.ref as product_ref, p.label as product_label, p.price as product_price, p.fk_unit as product_fk_unit";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON a.fk_product = p.rowid";
$sql .= " WHERE a.fk_anlage = ".((int) $anlageId);
$sql .= " ORDER BY a.rang ASC, a.rowid ASC";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$acc = new AnlageAccessory($this->db);
$acc->id = $obj->rowid;
$acc->fk_anlage = $obj->fk_anlage;
$acc->fk_product = $obj->fk_product;
$acc->qty = $obj->qty;
$acc->rang = $obj->rang;
$acc->note = $obj->note;
$acc->date_creation = $this->db->jdate($obj->date_creation);
$acc->fk_user_creat = $obj->fk_user_creat;
$acc->product_ref = $obj->product_ref;
$acc->product_label = $obj->product_label;
$acc->product_price = $obj->product_price;
$acc->product_fk_unit = $obj->product_fk_unit;
$results[] = $acc;
}
$this->db->free($resql);
}
return $results;
}
/**
* Prüfen ob Produkt bereits als Zubehör zugeordnet ist
*
* @param int $anlageId Anlage-ID
* @param int $productId Produkt-ID
* @return bool true wenn bereits vorhanden
*/
public function alreadyExists($anlageId, $productId)
{
$sql = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE fk_anlage = ".((int) $anlageId);
$sql .= " AND fk_product = ".((int) $productId);
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
return ($obj->cnt > 0);
}
return false;
}
/**
* Lieferantenbestellung aus ausgewählten Zubehörteilen erstellen
*
* @param User $user Benutzer
* @param int $supplierId Lieferanten-ID (fournisseur)
* @param int $anlageId Anlage-ID
* @param array $selectedIds Array von Accessory-IDs
* @param array $quantities Optional: ID => Menge
* @return int Bestell-ID bei Erfolg, <0 bei Fehler
*/
public function generateSupplierOrder($user, $supplierId, $anlageId, $selectedIds, $quantities = array())
{
global $conf, $langs, $mysoc;
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
if (empty($selectedIds)) {
$this->error = 'NoProductsSelected';
return -1;
}
// Lieferant laden
$supplier = new Societe($this->db);
if ($supplier->fetch($supplierId) <= 0) {
$this->error = 'ErrorLoadingSupplier';
return -1;
}
// Zubehör der Anlage laden
$accessories = $this->fetchAllByAnlage($anlageId);
if (!is_array($accessories) || empty($accessories)) {
$this->error = 'NoAccessoriesFound';
return -2;
}
// Ausgewählte filtern
$toAdd = array();
foreach ($accessories as $acc) {
if (in_array($acc->id, $selectedIds)) {
$qty = isset($quantities[$acc->id]) ? (float) $quantities[$acc->id] : $acc->qty;
if ($qty > 0) {
$toAdd[] = array(
'product_id' => $acc->fk_product,
'qty' => $qty
);
}
}
}
if (empty($toAdd)) {
$this->error = 'NoValidProductsToAdd';
return -2;
}
// Lieferantenbestellung erstellen
$order = new CommandeFournisseur($this->db);
$order->socid = $supplierId;
$order->date = dol_now();
$order->note_private = $langs->trans('OrderGeneratedFromAccessories');
$this->db->begin();
$result = $order->create($user);
if ($result <= 0) {
$this->error = $order->error;
$this->errors = $order->errors;
$this->db->rollback();
return -3;
}
// Produkte hinzufügen
foreach ($toAdd as $item) {
$product = new Product($this->db);
$product->fetch($item['product_id']);
// MwSt-Satz ermitteln (Lieferant = Verkäufer, eigene Firma = Käufer)
$tva_tx = get_default_tva($supplier, $mysoc, $product->id);
$localtax1_tx = get_default_localtax($supplier, $mysoc, 1, $product->id);
$localtax2_tx = get_default_localtax($supplier, $mysoc, 2, $product->id);
// Lieferantenpreis ermitteln
$fournPrice = $product->price;
$fournPriceId = 0;
$fournRef = '';
$sqlFourn = "SELECT rowid, price as fourn_price, ref_fourn";
$sqlFourn .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
$sqlFourn .= " WHERE fk_product = ".((int) $product->id);
$sqlFourn .= " AND fk_soc = ".((int) $supplierId);
$sqlFourn .= " ORDER BY price ASC LIMIT 1";
$resFourn = $this->db->query($sqlFourn);
if ($resFourn && $this->db->num_rows($resFourn) > 0) {
$objFourn = $this->db->fetch_object($resFourn);
$fournPrice = $objFourn->fourn_price;
$fournPriceId = $objFourn->rowid;
$fournRef = $objFourn->ref_fourn;
}
$lineResult = $order->addline(
$product->label, // Beschreibung
$fournPrice, // Preis HT
$item['qty'], // Menge
$tva_tx, // MwSt
$localtax1_tx, // Lokale Steuer 1
$localtax2_tx, // Lokale Steuer 2
$product->id, // Produkt-ID
$fournPriceId, // Lieferantenpreis-ID
$fournRef, // Lieferanten-Referenz
0, // Rabatt
'HT', // Preis-Basis
0, // Preis TTC
0, // Typ (0=Produkt)
0, // Info bits
false, // notrigger
null, // Startdatum
null, // Enddatum
array(), // Optionen
$product->fk_unit // Einheit
);
if ($lineResult < 0) {
$this->error = $order->error;
$this->errors = $order->errors;
$this->db->rollback();
return -4;
}
}
$this->db->commit();
return $order->id;
}
}

7
class/anlagetype.class.php Executable file → Normal file
View file

@ -26,6 +26,7 @@ class AnlageType extends CommonObject
public $can_be_nested; public $can_be_nested;
public $allowed_parent_types; public $allowed_parent_types;
public $can_have_equipment; public $can_have_equipment;
public $has_accessories;
public $picto; public $picto;
public $color; public $color;
@ -73,7 +74,7 @@ class AnlageType extends CommonObject
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." ("; $sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, ref, label, label_short, description, fk_system,"; $sql .= "entity, ref, label, label_short, description, fk_system,";
$sql .= " can_have_children, can_be_nested, allowed_parent_types, can_have_equipment,"; $sql .= " can_have_children, can_be_nested, allowed_parent_types, can_have_equipment, has_accessories,";
$sql .= " picto, color, is_system, position, active,"; $sql .= " picto, color, is_system, position, active,";
$sql .= " date_creation, fk_user_creat"; $sql .= " date_creation, fk_user_creat";
$sql .= ") VALUES ("; $sql .= ") VALUES (";
@ -87,6 +88,7 @@ class AnlageType extends CommonObject
$sql .= ", ".((int) $this->can_be_nested); $sql .= ", ".((int) $this->can_be_nested);
$sql .= ", ".($this->allowed_parent_types ? "'".$this->db->escape($this->allowed_parent_types)."'" : "NULL"); $sql .= ", ".($this->allowed_parent_types ? "'".$this->db->escape($this->allowed_parent_types)."'" : "NULL");
$sql .= ", ".((int) $this->can_have_equipment); $sql .= ", ".((int) $this->can_have_equipment);
$sql .= ", ".((int) $this->has_accessories);
$sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL"); $sql .= ", ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL"); $sql .= ", ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", 0"; // is_system = 0 for user-created $sql .= ", 0"; // is_system = 0 for user-created
@ -144,6 +146,7 @@ class AnlageType extends CommonObject
$this->can_be_nested = $obj->can_be_nested; $this->can_be_nested = $obj->can_be_nested;
$this->allowed_parent_types = $obj->allowed_parent_types; $this->allowed_parent_types = $obj->allowed_parent_types;
$this->can_have_equipment = $obj->can_have_equipment ?? 0; $this->can_have_equipment = $obj->can_have_equipment ?? 0;
$this->has_accessories = $obj->has_accessories ?? 0;
$this->picto = $obj->picto; $this->picto = $obj->picto;
$this->color = $obj->color; $this->color = $obj->color;
$this->is_system = $obj->is_system; $this->is_system = $obj->is_system;
@ -190,6 +193,7 @@ class AnlageType extends CommonObject
$sql .= ", can_be_nested = ".((int) $this->can_be_nested); $sql .= ", can_be_nested = ".((int) $this->can_be_nested);
$sql .= ", allowed_parent_types = ".($this->allowed_parent_types ? "'".$this->db->escape($this->allowed_parent_types)."'" : "NULL"); $sql .= ", allowed_parent_types = ".($this->allowed_parent_types ? "'".$this->db->escape($this->allowed_parent_types)."'" : "NULL");
$sql .= ", can_have_equipment = ".((int) $this->can_have_equipment); $sql .= ", can_have_equipment = ".((int) $this->can_have_equipment);
$sql .= ", has_accessories = ".((int) $this->has_accessories);
$sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL"); $sql .= ", picto = ".($this->picto ? "'".$this->db->escape($this->picto)."'" : "NULL");
$sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL"); $sql .= ", color = ".($this->color ? "'".$this->db->escape($this->color)."'" : "NULL");
$sql .= ", position = ".((int) $this->position); $sql .= ", position = ".((int) $this->position);
@ -312,6 +316,7 @@ class AnlageType extends CommonObject
$type->can_be_nested = $obj->can_be_nested; $type->can_be_nested = $obj->can_be_nested;
$type->allowed_parent_types = $obj->allowed_parent_types; $type->allowed_parent_types = $obj->allowed_parent_types;
$type->can_have_equipment = $obj->can_have_equipment ?? 0; $type->can_have_equipment = $obj->can_have_equipment ?? 0;
$type->has_accessories = $obj->has_accessories ?? 0;
$type->picto = $obj->picto; $type->picto = $obj->picto;
$type->is_system = $obj->is_system; $type->is_system = $obj->is_system;
$type->position = $obj->position; $type->position = $obj->position;

View file

@ -76,7 +76,7 @@ class modKundenKarte extends DolibarrModules
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@kundenkarte' $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@kundenkarte'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '8.4'; $this->version = '8.5';
// Url to the file with your last numberversion of this module // Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt'; //$this->url_last_version = 'http://www.example.com/versionmodule.txt';
@ -412,6 +412,23 @@ class modKundenKarte extends DolibarrModules
'target' => '', 'target' => '',
'user' => 0, 'user' => 0,
); );
// Werkzeuge-Seite unter Start-Menü
$this->menu[$r++] = array(
'fk_menu' => 'fk_mainmenu=home',
'type' => 'left',
'titre' => 'CompanyTools',
'prefix' => img_picto('', 'fa-wrench', 'class="pictofixedwidth valignmiddle paddingright"'),
'mainmenu' => 'home',
'leftmenu' => 'kundenkarte_werkzeuge',
'url' => '/kundenkarte/werkzeuge.php',
'langs' => 'kundenkarte@kundenkarte',
'position' => 100,
'enabled' => 'isModEnabled("kundenkarte")',
'perms' => '$user->hasRight("kundenkarte", "read")',
'target' => '',
'user' => 0,
);
/* END MODULEBUILDER LEFTMENU */ /* END MODULEBUILDER LEFTMENU */
@ -636,6 +653,9 @@ class modKundenKarte extends DolibarrModules
// v8.0.0: Ausgebaut-Status für Anlagen // v8.0.0: Ausgebaut-Status für Anlagen
$this->migrate_v800_decommissioned(); $this->migrate_v800_decommissioned();
// v8.1.0: Werkzeuge & Zubehör
$this->migrate_v810_werkzeuge();
} }
/** /**
@ -950,6 +970,58 @@ class modKundenKarte extends DolibarrModules
} }
} }
/**
* Migration v8.1.0: Werkzeuge & Zubehör
* - fk_product auf Anlage (Produkt-Zuordnung)
* - has_accessories auf Anlage-Typ
* - Zubehör-Tabelle
* - WERKZEUG System-Kategorie
*/
private function migrate_v810_werkzeuge()
{
// 1. fk_product auf Anlage
$table = MAIN_DB_PREFIX."kundenkarte_anlage";
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'fk_product'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("ALTER TABLE ".$table." ADD COLUMN fk_product integer NULL AFTER fk_building_node");
$this->db->query("ALTER TABLE ".$table." ADD INDEX idx_anlage_fk_product (fk_product)");
}
// 2. has_accessories auf Anlage-Typ
$table = MAIN_DB_PREFIX."kundenkarte_anlage_type";
$resql = $this->db->query("SHOW COLUMNS FROM ".$table." LIKE 'has_accessories'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("ALTER TABLE ".$table." ADD COLUMN has_accessories tinyint DEFAULT 0 NOT NULL AFTER can_have_equipment");
}
// 3. Zubehör-Tabelle
$table = MAIN_DB_PREFIX."kundenkarte_anlage_accessory";
$resql = $this->db->query("SHOW TABLES LIKE '".$this->db->escape($table)."'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$sql = "CREATE TABLE ".$table." (";
$sql .= " rowid integer AUTO_INCREMENT PRIMARY KEY,";
$sql .= " fk_anlage integer NOT NULL,";
$sql .= " fk_product integer NOT NULL,";
$sql .= " qty double DEFAULT 1,";
$sql .= " rang integer DEFAULT 0,";
$sql .= " note varchar(255),";
$sql .= " date_creation datetime,";
$sql .= " fk_user_creat integer,";
$sql .= " UNIQUE KEY uk_anlage_accessory (fk_anlage, fk_product),";
$sql .= " INDEX idx_accessory_anlage (fk_anlage),";
$sql .= " CONSTRAINT fk_accessory_anlage FOREIGN KEY (fk_anlage) REFERENCES ".MAIN_DB_PREFIX."kundenkarte_anlage(rowid) ON DELETE CASCADE";
$sql .= ") ENGINE=InnoDB";
$this->db->query($sql);
}
// 4. WERKZEUG System-Kategorie (falls nicht vorhanden)
$sysTable = MAIN_DB_PREFIX."c_kundenkarte_anlage_system";
$resql = $this->db->query("SELECT rowid FROM ".$sysTable." WHERE code = 'WERKZEUG'");
if (!$resql || $this->db->num_rows($resql) == 0) {
$this->db->query("INSERT INTO ".$sysTable." (code, label, active, position) VALUES ('WERKZEUG', 'Werkzeuge & Maschinen', 1, 90)");
}
}
/** /**
* Function called when module is disabled. * Function called when module is disabled.
* Remove from database constants, boxes and permissions from Dolibarr database. * Remove from database constants, boxes and permissions from Dolibarr database.

View file

@ -561,3 +561,19 @@ Decommissioned = Ausgebaut
Decommission = Ausbauen Decommission = Ausbauen
Recommission = Wieder einbauen Recommission = Wieder einbauen
ShowDecommissioned = Ausgebaute Elemente anzeigen ShowDecommissioned = Ausgebaute Elemente anzeigen
# Werkzeuge & Zubehör
CompanyTools = Firmen-Werkzeuge
HasAccessories = Hat Zubehör
HasAccessoriesHelp = Ermöglicht die Zuordnung von Zubehör und Ersatzteilen
Accessories = Zubehör / Ersatzteile
NoAccessories = Kein Zubehör zugeordnet
SearchProduct = Produkt suchen
SelectSupplier = Lieferant auswählen
CreateSupplierOrder = Lieferantenbestellung erstellen
OrderAccessories = Zubehör bestellen
OrderGeneratedFromAccessories = Bestellung aus Anlagen-Zubehör generiert
ConfirmDeleteAccessory = Dieses Zubehör wirklich entfernen?
NoToolsYet = Noch keine Werkzeuge erfasst
AddFirstTool = Erstes Werkzeug hinzufügen
GoToTypeAdmin = Zur Typverwaltung

View file

@ -309,3 +309,19 @@ Decommissioned = Decommissioned
Decommission = Decommission Decommission = Decommission
Recommission = Recommission Recommission = Recommission
ShowDecommissioned = Show decommissioned elements ShowDecommissioned = Show decommissioned elements
# Tools & Accessories
CompanyTools = Company Tools
HasAccessories = Has Accessories
HasAccessoriesHelp = Allows assigning accessories and spare parts
Accessories = Accessories / Spare Parts
NoAccessories = No accessories assigned
SearchProduct = Search product
SelectSupplier = Select supplier
CreateSupplierOrder = Create supplier order
OrderAccessories = Order accessories
OrderGeneratedFromAccessories = Order generated from installation accessories
ConfirmDeleteAccessory = Really remove this accessory?
NoToolsYet = No tools registered yet
AddFirstTool = Add first tool
GoToTypeAdmin = Go to type administration

837
werkzeuge.php Normal file
View file

@ -0,0 +1,837 @@
<?php
/* Copyright (C) 2026 Alles Watt lauft
*
* Firmen-Werkzeuge: Baumansicht für eigene Maschinen und Werkzeuge
* System-Filter fix auf WERKZEUG
*/
// Load Dolibarr environment
$res = 0;
if (!$res && file_exists("../main.inc.php")) $res = @include "../main.inc.php";
if (!$res && file_exists("../../main.inc.php")) $res = @include "../../main.inc.php";
if (!$res) die("Include of main fails");
require_once DOL_DOCUMENT_ROOT.'/core/lib/company.lib.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/html.form.class.php';
require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php';
dol_include_once('/kundenkarte/class/anlage.class.php');
dol_include_once('/kundenkarte/class/anlagetype.class.php');
dol_include_once('/kundenkarte/class/anlagefile.class.php');
dol_include_once('/kundenkarte/class/anlageaccessory.class.php');
dol_include_once('/kundenkarte/lib/kundenkarte.lib.php');
// Übersetzungen
$langs->loadLangs(array('companies', 'kundenkarte@kundenkarte'));
// Berechtigungen
if (!$user->hasRight('kundenkarte', 'read')) {
accessforbidden();
}
$permissiontoread = $user->hasRight('kundenkarte', 'read');
$permissiontoadd = $user->hasRight('kundenkarte', 'write');
$permissiontodelete = $user->hasRight('kundenkarte', 'delete');
$action = GETPOST('action', 'aZ09');
$confirm = GETPOST('confirm', 'alpha');
$anlageId = GETPOSTINT('anlage_id');
$parentId = GETPOSTINT('parent_id');
// Eigene Firma als Kontext
$socId = $mysoc->id;
if ($socId <= 0) {
setEventMessages('Eigene Firma nicht konfiguriert. Bitte unter Einrichtung → Firma konfigurieren.', null, 'errors');
llxHeader('', 'Firmen-Werkzeuge');
llxFooter();
exit;
}
// WERKZEUG-System ermitteln
$werkzeugSystemId = 0;
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."c_kundenkarte_anlage_system WHERE code = 'WERKZEUG' AND active = 1";
$resql = $db->query($sql);
if ($resql && $db->num_rows($resql) > 0) {
$obj = $db->fetch_object($resql);
$werkzeugSystemId = $obj->rowid;
}
if ($werkzeugSystemId <= 0) {
setEventMessages('System WERKZEUG nicht gefunden. Bitte Modul deaktivieren und wieder aktivieren.', null, 'errors');
llxHeader('', 'Firmen-Werkzeuge');
llxFooter();
exit;
}
$systemId = $werkzeugSystemId;
// Sicherstellen dass WERKZEUG für eigene Firma aktiviert ist
$sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."kundenkarte_societe_system";
$sql .= " WHERE fk_soc = ".((int) $socId)." AND fk_system = ".((int) $systemId);
$sql .= " AND (fk_contact IS NULL OR fk_contact = 0) AND active = 1";
$resql = $db->query($sql);
if (!$resql || $db->num_rows($resql) == 0) {
// Automatisch aktivieren
$sql = "INSERT INTO ".MAIN_DB_PREFIX."kundenkarte_societe_system";
$sql .= " (entity, fk_soc, fk_contact, fk_system, date_creation, fk_user_creat, active)";
$sql .= " VALUES (".$conf->entity.", ".((int) $socId).", 0, ".((int) $systemId).", NOW(), ".((int) $user->id).", 1)";
$db->query($sql);
}
// Objekte initialisieren
$form = new Form($db);
$anlage = new Anlage($db);
$anlageType = new AnlageType($db);
/*
* Actions
*/
if ($action == 'add' && $permissiontoadd) {
$anlage->label = GETPOST('label', 'alphanohtml');
$anlage->fk_soc = $socId;
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_system = $systemId;
$anlage->fk_product = GETPOSTINT('fk_product') > 0 ? GETPOSTINT('fk_product') : null;
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
$anlage->status = 1;
// Dynamische Felder
$type = new AnlageType($db);
if ($type->fetch($anlage->fk_anlage_type) > 0) {
$fieldValues = array();
$fields = $type->fetchFields();
foreach ($fields as $field) {
if ($field->field_type === 'header') continue;
$value = GETPOST('field_'.$field->field_code, 'alphanohtml');
if ($value !== '') {
$fieldValues[$field->field_code] = $value;
}
}
$anlage->setFieldValues($fieldValues);
}
$result = $anlage->create($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
} else {
setEventMessages($anlage->error, $anlage->errors, 'errors');
$action = 'create';
}
}
if ($action == 'update' && $permissiontoadd) {
$anlage->fetch($anlageId);
$anlage->label = GETPOST('label', 'alphanohtml');
$anlage->fk_anlage_type = GETPOSTINT('fk_anlage_type');
$anlage->fk_parent = GETPOSTINT('fk_parent');
$anlage->fk_product = GETPOSTINT('fk_product') > 0 ? GETPOSTINT('fk_product') : null;
$anlage->note_private = isset($_POST['note_private']) ? $_POST['note_private'] : '';
// Dynamische Felder
$type = new AnlageType($db);
if ($type->fetch($anlage->fk_anlage_type) > 0) {
$fieldValues = array();
$fields = $type->fetchFields();
foreach ($fields as $field) {
if ($field->field_type === 'header') continue;
$value = GETPOST('field_'.$field->field_code, 'alphanohtml');
if ($value !== '') {
$fieldValues[$field->field_code] = $value;
}
}
$anlage->setFieldValues($fieldValues);
}
$result = $anlage->update($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
header('Location: '.$_SERVER['PHP_SELF']);
exit;
} else {
setEventMessages($anlage->error, $anlage->errors, 'errors');
$action = 'edit';
}
}
if ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) {
$anlage->fetch($anlageId);
$result = $anlage->delete($user);
if ($result > 0) {
setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs');
} else {
setEventMessages($anlage->error, $anlage->errors, 'errors');
}
header('Location: '.$_SERVER['PHP_SELF']);
exit;
}
/*
* View
*/
$title = $langs->trans('CompanyTools');
$jsFiles = array('/kundenkarte/js/kundenkarte.js?v='.time());
$cssFiles = array('/kundenkarte/css/kundenkarte.css?v='.time());
llxHeader('', $title, '', '', 0, 0, $jsFiles, $cssFiles);
print load_fiche_titre($title, '', 'fa-wrench');
print '<div class="fichecenter">';
// Bestätigungsdialog
if ($action == 'delete') {
print $form->formconfirm(
$_SERVER['PHP_SELF'].'?anlage_id='.$anlageId,
$langs->trans('DeleteElement'),
$langs->trans('ConfirmDeleteElement'),
'confirm_delete',
'',
'yes',
1
);
}
// Typen für WERKZEUG-System laden
$types = $anlageType->fetchAllBySystem($systemId, 1, 1); // excludeGlobal=1, nur WERKZEUG-Typen
if (in_array($action, array('create', 'edit', 'view'))) {
// Formular oder Detail-Ansicht
if ($action != 'create' && $anlageId > 0) {
$anlage->fetch($anlageId);
$type = new AnlageType($db);
$type->fetch($anlage->fk_anlage_type);
$type->fetchFields();
}
print '<div class="kundenkarte-element-form">';
if ($action == 'view') {
// Detail-Ansicht
print '<h3>'.dol_escape_htmltag($anlage->label).'</h3>';
print '<table class="border centpercent">';
print '<tr><td class="titlefield">'.$langs->trans('Type').'</td>';
print '<td>'.dol_escape_htmltag($anlage->type_label).'</td></tr>';
// Zugeordnetes Produkt
if ($anlage->fk_product > 0) {
$product = new Product($db);
if ($product->fetch($anlage->fk_product) > 0) {
print '<tr><td>'.$langs->trans('Product').'</td>';
print '<td><a href="'.DOL_URL_ROOT.'/product/card.php?id='.$product->id.'">'.dol_escape_htmltag($product->ref).'</a>';
print ' - '.dol_escape_htmltag($product->label);
if ($product->price > 0) {
print ' <span class="opacitymedium">('.price($product->price).' €)</span>';
}
print '</td></tr>';
}
}
// Dynamische Felder
$fieldValues = $anlage->getFieldValues();
$typeFieldsList = $type->fetchFields();
foreach ($typeFieldsList as $field) {
if ($field->field_type === 'header') {
print '<tr class="liste_titre"><th colspan="2" style="background:#f0f0f0;padding:8px;">'.dol_escape_htmltag($field->field_label).'</th></tr>';
} else {
$value = isset($fieldValues[$field->field_code]) ? $fieldValues[$field->field_code] : '';
if ($value !== '') {
print '<tr><td>'.dol_escape_htmltag($field->field_label).'</td>';
if ($field->field_type === 'date' && $value) {
print '<td>'.dol_print_date(strtotime($value), 'day').'</td></tr>';
} elseif ($field->field_type === 'checkbox') {
print '<td>'.($value ? $langs->trans('Yes') : $langs->trans('No')).'</td></tr>';
} else {
print '<td>'.dol_escape_htmltag($value).'</td></tr>';
}
}
}
}
if ($anlage->note_private) {
print '<tr><td>'.$langs->trans('FieldNotes').'</td>';
print '<td>'.dol_htmlentitiesbr($anlage->note_private).'</td></tr>';
}
// Ausgebaut-Status
if (!empty($anlage->decommissioned)) {
print '<tr><td>'.$langs->trans('Decommissioned').'</td>';
print '<td><span class="badge-decommissioned"><i class="fa fa-power-off"></i> '.$langs->trans('Decommissioned').'</span>';
if (!empty($anlage->date_decommissioned)) {
print ' '.dol_print_date(strtotime($anlage->date_decommissioned), 'day');
}
print '</td></tr>';
}
print '</table>';
// Zubehör-Bereich (nur wenn Typ has_accessories hat)
if ($type->has_accessories) {
$accessoryObj = new AnlageAccessory($db);
$accessories = $accessoryObj->fetchAllByAnlage($anlageId);
print '<br><h4><i class="fa fa-puzzle-piece"></i> '.$langs->trans('Accessories').'</h4>';
if (!empty($accessories)) {
print '<div class="div-table-responsive">';
print '<table class="tagtable liste">';
print '<tr class="liste_titre">';
print '<th>'.$langs->trans('ProductRef').'</th>';
print '<th>'.$langs->trans('Label').'</th>';
print '<th class="right">'.$langs->trans('Qty').'</th>';
print '<th>'.$langs->trans('Note').'</th>';
if ($permissiontodelete) {
print '<th class="right">'.$langs->trans('Action').'</th>';
}
print '</tr>';
foreach ($accessories as $acc) {
print '<tr>';
print '<td><a href="'.DOL_URL_ROOT.'/product/card.php?id='.$acc->fk_product.'">'.dol_escape_htmltag($acc->product_ref).'</a></td>';
print '<td>'.dol_escape_htmltag($acc->product_label).'</td>';
print '<td class="right">'.$acc->qty.'</td>';
print '<td>'.dol_escape_htmltag($acc->note).'</td>';
if ($permissiontodelete) {
print '<td class="right"><a href="#" class="btn-delete-accessory" data-id="'.$acc->id.'" title="'.$langs->trans('Delete').'"><i class="fa fa-trash"></i></a></td>';
}
print '</tr>';
}
print '</table>';
print '</div>';
} else {
print '<p class="opacitymedium">'.$langs->trans('NoAccessories').'</p>';
}
// Zubehör hinzufügen
if ($permissiontoadd) {
print '<div id="add-accessory-form" style="margin-top:10px;">';
print '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">';
print '<input type="text" id="accessory_product_search" class="flat minwidth300" placeholder="'.$langs->trans('SearchProduct').'...">';
print '<input type="hidden" id="accessory_product_id" value="">';
print '<input type="number" id="accessory_qty" class="flat" value="1" min="1" style="width:80px;">';
print '<button type="button" id="btn-add-accessory" class="button small" disabled>'.$langs->trans('Add').'</button>';
print '</div>';
print '</div>';
}
// Bestellfunktion
if ($permissiontoadd && !empty($accessories)) {
print '<div style="margin-top:15px;padding:10px;border:1px solid #444;border-radius:4px;">';
print '<h5 style="margin:0 0 10px 0;"><i class="fa fa-shopping-cart"></i> '.$langs->trans('OrderAccessories').'</h5>';
print '<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">';
print '<select id="supplier_select" class="flat minwidth200">';
print '<option value="">'.$langs->trans('SelectSupplier').'</option>';
// Lieferanten laden
$sqlSupp = "SELECT s.rowid, s.nom FROM ".MAIN_DB_PREFIX."societe s WHERE s.fournisseur = 1 AND s.status = 1 ORDER BY s.nom";
$resSupp = $db->query($sqlSupp);
if ($resSupp) {
while ($objSupp = $db->fetch_object($resSupp)) {
print '<option value="'.$objSupp->rowid.'">'.dol_escape_htmltag($objSupp->nom).'</option>';
}
}
print '</select>';
print '<button type="button" id="btn-order-accessories" class="button small">'.$langs->trans('CreateSupplierOrder').'</button>';
print '</div>';
print '</div>';
}
}
// Aktions-Buttons
print '<div class="tabsAction">';
if ($permissiontoadd) {
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?action=edit&anlage_id='.$anlageId.'">'.$langs->trans('Modify').'</a>';
}
if ($permissiontodelete) {
print '<a class="butActionDelete" href="'.$_SERVER['PHP_SELF'].'?action=delete&anlage_id='.$anlageId.'">'.$langs->trans('Delete').'</a>';
}
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'">'.$langs->trans('Back').'</a>';
print '</div>';
} else {
// Erstellen/Bearbeiten-Formular
$isEdit = ($action == 'edit');
$formAction = $isEdit ? 'update' : 'add';
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="'.$formAction.'">';
if ($isEdit) {
print '<input type="hidden" name="anlage_id" value="'.$anlageId.'">';
}
print '<table class="border centpercent" id="element_form_table">';
// Label
$labelValue = $isEdit ? $anlage->label : GETPOST('label');
print '<tr><td class="titlefield fieldrequired">'.$langs->trans('Label').'</td>';
print '<td><input type="text" name="label" class="flat minwidth300" value="'.dol_escape_htmltag($labelValue).'" required></td></tr>';
// Typ
print '<tr><td class="fieldrequired">'.$langs->trans('Type').'</td>';
print '<td><select name="fk_anlage_type" class="flat minwidth200" id="select_type" required>';
print '<option value="">'.$langs->trans('SelectType').'</option>';
foreach ($types as $t) {
$selected = ($isEdit && $anlage->fk_anlage_type == $t->id) ? ' selected' : '';
print '<option value="'.$t->id.'"'.$selected.'>'.dol_escape_htmltag($t->label).'</option>';
}
print '</select>';
if (empty($types)) {
print '<br><span class="warning">'.$langs->trans('NoTypesDefinedForSystem').' <a href="'.dol_buildpath('/kundenkarte/admin/anlage_types.php', 1).'">'.$langs->trans('GoToTypeAdmin').'</a></span>';
}
print '</td></tr>';
// Übergeordnetes Element
$tree = $anlage->fetchTree($socId, $systemId);
$selectedParent = $isEdit ? $anlage->fk_parent : $parentId;
$excludeId = $isEdit ? $anlageId : 0;
print '<tr><td>'.$langs->trans('SelectParent').'</td>';
print '<td><select name="fk_parent" class="flat minwidth200">';
print '<option value="0">('.$langs->trans('Root').')</option>';
werkzeuge_printTreeOptions($tree, $selectedParent, $excludeId);
print '</select></td></tr>';
// Produkt-Zuordnung
$productValue = '';
$productId = 0;
if ($isEdit && $anlage->fk_product > 0) {
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
$product = new Product($db);
if ($product->fetch($anlage->fk_product) > 0) {
$productValue = $product->ref.' - '.$product->label;
$productId = $product->id;
}
}
print '<tr><td>'.$langs->trans('Product').'</td>';
print '<td>';
print '<input type="text" id="product_search" class="flat minwidth300" placeholder="'.$langs->trans('SearchProduct').'..." value="'.dol_escape_htmltag($productValue).'">';
print '<input type="hidden" name="fk_product" id="fk_product" value="'.$productId.'">';
print '</td></tr>';
// Dynamische Felder werden per JS geladen
print '<tbody id="dynamic_fields"></tbody>';
// Notizen
print '<tr><td>'.$langs->trans('FieldNotes').'</td>';
$noteValue = $isEdit ? $anlage->note_private : (isset($_POST['note_private']) ? $_POST['note_private'] : '');
print '<td><textarea name="note_private" class="flat minwidth300" rows="3">'.htmlspecialchars($noteValue, ENT_QUOTES, 'UTF-8').'</textarea></td></tr>';
print '</table>';
print '<div class="center" style="margin-top:20px;">';
print '<button type="submit" class="button button-save">'.$langs->trans('Save').'</button>';
print ' <a class="button button-cancel" href="'.$_SERVER['PHP_SELF'].'">'.$langs->trans('Cancel').'</a>';
print '</div>';
print '</form>';
}
print '</div>';
} else {
// Baumansicht
if ($permissiontoadd) {
print '<div style="margin-bottom:15px;">';
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?action=create">';
print '<i class="fa fa-plus"></i> '.$langs->trans('AddElement');
print '</a>';
print '</div>';
}
// Steuerungs-Buttons
print '<div class="kundenkarte-tree-controls" style="margin-bottom:10px;">';
print '<button type="button" class="kundenkarte-view-toggle" id="btn-compact-mode" title="Kompakte Ansicht">';
print '<i class="fa fa-compress"></i> <span>Kompakt</span>';
print '</button>';
print '<button type="button" class="button small" id="btn-expand-all" title="'.$langs->trans('ExpandAll').'">';
print '<i class="fa fa-expand"></i> '.$langs->trans('ExpandAll');
print '</button>';
print '<button type="button" class="button small" id="btn-collapse-all" title="'.$langs->trans('CollapseAll').'">';
print '<i class="fa fa-compress"></i> '.$langs->trans('CollapseAll');
print '</button>';
print '<button type="button" class="button small" id="btn-toggle-decommissioned" title="'.$langs->trans('ShowDecommissioned').'">';
print '<i class="fa fa-eye-slash"></i> <span>'.$langs->trans('Decommissioned').'</span>';
print '</button>';
print '</div>';
// Baum laden
$tree = $anlage->fetchTree($socId, $systemId);
// Feld-Metadaten laden
$typeFieldsMap = array();
$sql = "SELECT f.*, f.fk_anlage_type FROM ".MAIN_DB_PREFIX."kundenkarte_anlage_type_field f WHERE f.active = 1 ORDER BY f.position ASC";
$resql = $db->query($sql);
if ($resql) {
while ($obj = $db->fetch_object($resql)) {
if (!isset($typeFieldsMap[$obj->fk_anlage_type])) {
$typeFieldsMap[$obj->fk_anlage_type] = array();
}
$typeFieldsMap[$obj->fk_anlage_type][] = $obj;
}
$db->free($resql);
}
if (!empty($tree)) {
print '<div class="kundenkarte-tree" data-system="'.$systemId.'" data-socid="'.$socId.'">';
werkzeuge_printTree($tree, $socId, $systemId, $permissiontoadd, $permissiontodelete, $langs, 0, $typeFieldsMap);
print '</div>';
} else {
print '<div class="opacitymedium" style="padding:20px;text-align:center;">';
print '<i class="fa fa-wrench" style="font-size:48px;margin-bottom:15px;color:#666;"></i><br>';
print $langs->trans('NoToolsYet').'<br><br>';
if ($permissiontoadd) {
print '<a class="butAction" href="'.$_SERVER['PHP_SELF'].'?action=create"><i class="fa fa-plus"></i> '.$langs->trans('AddFirstTool').'</a>';
}
print '</div>';
}
}
print '</div>'; // fichecenter
// Tooltip Container
print '<div id="kundenkarte-tooltip" class="kundenkarte-tooltip"></div>';
// JavaScript: Produkt-Autocomplete + Zubehör-AJAX
print '<script>
$(document).ready(function() {
var baseUrl = "'.dol_escape_js(dol_buildpath('/kundenkarte', 1)).'";
// Produkt-Autocomplete
function initProductAutocomplete(inputSelector, hiddenSelector) {
var $input = $(inputSelector);
var $hidden = $(hiddenSelector);
if (!$input.length) return;
var searchTimeout;
$input.on("input", function() {
clearTimeout(searchTimeout);
var term = $(this).val();
if (term.length < 2) {
$hidden.val("");
$(".product-autocomplete-dropdown").remove();
return;
}
searchTimeout = setTimeout(function() {
$.get(baseUrl + "/ajax/equipment.php", {
action: "get_products",
term: term,
token: $("input[name=token]").val() || ""
}, function(data) {
$(".product-autocomplete-dropdown").remove();
if (data.success && data.products && data.products.length > 0) {
var $dropdown = $("<div class=\"product-autocomplete-dropdown\"></div>");
$.each(data.products, function(i, p) {
var label = p.ref + " - " + p.label;
if (p.price > 0) label += " (" + p.price + " €)";
$dropdown.append(
$("<div class=\"product-autocomplete-item\"></div>")
.text(label)
.data("id", p.id)
.data("ref", p.ref)
.data("label", p.label)
);
});
$input.after($dropdown);
$dropdown.on("click", ".product-autocomplete-item", function() {
var id = $(this).data("id");
var ref = $(this).data("ref");
var label = $(this).data("label");
$input.val(ref + " - " + label);
$hidden.val(id);
$dropdown.remove();
// Zubehör: Button aktivieren
if (inputSelector === "#accessory_product_search") {
$("#btn-add-accessory").prop("disabled", false);
}
});
}
});
}, 300);
});
// Dropdown schließen bei Klick außerhalb
$(document).on("click", function(e) {
if (!$(e.target).closest(inputSelector + ", .product-autocomplete-dropdown").length) {
$(".product-autocomplete-dropdown").remove();
}
});
// Bei manuellem Leeren auch Hidden zurücksetzen
$input.on("change", function() {
if (!$(this).val()) {
$hidden.val("");
if (inputSelector === "#accessory_product_search") {
$("#btn-add-accessory").prop("disabled", true);
}
}
});
}
initProductAutocomplete("#product_search", "#fk_product");
initProductAutocomplete("#accessory_product_search", "#accessory_product_id");
// Zubehör hinzufügen
$("#btn-add-accessory").on("click", function() {
var productId = $("#accessory_product_id").val();
var qty = $("#accessory_qty").val() || 1;
var anlageId = '.((int) $anlageId).';
if (!productId || !anlageId) return;
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "add",
fk_anlage: anlageId,
fk_product: productId,
qty: qty,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success) {
location.reload();
} else {
alert(data.error || "Fehler");
}
}).fail(function() {
alert("Server-Fehler");
});
});
// Zubehör löschen
$(".btn-delete-accessory").on("click", function(e) {
e.preventDefault();
if (!confirm("'.$langs->trans('ConfirmDeleteAccessory').'")) return;
var accId = $(this).data("id");
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "delete",
id: accId,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success) {
location.reload();
} else {
alert(data.error || "Fehler");
}
});
});
// Lieferantenbestellung
$("#btn-order-accessories").on("click", function() {
var supplierId = $("#supplier_select").val();
var anlageId = '.((int) $anlageId).';
if (!supplierId) {
alert("Bitte Lieferant auswählen");
return;
}
// Alle Zubehör-IDs sammeln
var ids = [];
$(".btn-delete-accessory").each(function() {
ids.push($(this).data("id"));
});
if (ids.length === 0) return;
$.post(baseUrl + "/ajax/anlage_accessory.php", {
action: "order",
fk_anlage: anlageId,
supplier_id: supplierId,
"ids[]": ids,
token: $("input[name=token]").val() || ""
}).done(function(data) {
if (data.success && data.order_id) {
window.location.href = "'.DOL_URL_ROOT.'/fourn/commande/card.php?id=" + data.order_id;
} else {
alert(data.error || "Fehler beim Erstellen der Bestellung");
}
}).fail(function() {
alert("Server-Fehler");
});
});
});
</script>';
// CSS für Autocomplete
print '<style>
.product-autocomplete-dropdown {
position: absolute;
z-index: 1000;
background: var(--colorbackbody, #fff);
border: 1px solid #555;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
min-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.product-autocomplete-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #333;
}
.product-autocomplete-item:hover {
background: var(--colorbacklinepairhover, #333);
}
.product-autocomplete-item:last-child {
border-bottom: none;
}
</style>';
llxFooter();
$db->close();
/**
* Baum rekursiv ausgeben (vereinfachte Version für Werkzeuge-Seite)
*/
function werkzeuge_printTree($nodes, $socid, $systemId, $canEdit, $canDelete, $langs, $level = 0, $typeFieldsMap = array())
{
foreach ($nodes as $node) {
$hasChildren = !empty($node->children);
$fieldValues = $node->getFieldValues();
// Badges sammeln
$treeInfoBadges = array();
$treeInfoParentheses = array();
if (!empty($typeFieldsMap[$node->fk_anlage_type])) {
foreach ($typeFieldsMap[$node->fk_anlage_type] as $fieldDef) {
if ($fieldDef->field_type === 'header') continue;
$value = isset($fieldValues[$fieldDef->field_code]) ? $fieldValues[$fieldDef->field_code] : '';
if ($fieldDef->show_in_tree && $value !== '') {
$displayVal = $value;
if ($fieldDef->field_type === 'date' && $value) {
$displayVal = dol_print_date(strtotime($value), 'day');
}
$fieldInfo = array(
'label' => $fieldDef->field_label,
'value' => $displayVal,
'code' => $fieldDef->field_code,
'type' => $fieldDef->field_type,
'color' => $fieldDef->badge_color ?? ''
);
$displayMode = $fieldDef->tree_display_mode ?? 'badge';
if ($displayMode === 'parentheses') {
$treeInfoParentheses[] = $fieldInfo;
} else {
$treeInfoBadges[] = $fieldInfo;
}
}
}
}
$nodeClass = 'kundenkarte-tree-node';
if (!empty($node->decommissioned)) {
$nodeClass .= ' decommissioned';
}
if ($node->type_can_have_children) {
$nodeClass .= ' node-structure';
} else {
$nodeClass .= ' node-leaf';
}
print '<div class="'.$nodeClass.'">';
print '<div class="kundenkarte-tree-item" data-anlage-id="'.$node->id.'">';
// Toggle
if ($hasChildren) {
print '<span class="kundenkarte-tree-toggle"><i class="fa fa-chevron-down"></i></span>';
} else {
print '<span class="kundenkarte-tree-toggle" style="visibility:hidden;"><i class="fa fa-chevron-down"></i></span>';
}
// Icon
$picto = $node->type_picto ? $node->type_picto : 'fa-wrench';
print '<span class="kundenkarte-tree-icon">'.kundenkarte_render_icon($picto).'</span>';
// Label
$viewUrl = $_SERVER['PHP_SELF'].'?action=view&anlage_id='.$node->id;
print '<span class="kundenkarte-tree-label">'.dol_escape_htmltag($node->label);
if (!empty($treeInfoParentheses)) {
$infoValues = array();
foreach ($treeInfoParentheses as $info) {
$infoValues[] = dol_escape_htmltag($info['value']);
}
print ' <span class="kundenkarte-tree-label-info">('.implode(', ', $infoValues).')</span>';
}
print '</span>';
// Ausgebaut-Badge
if (!empty($node->decommissioned)) {
$decommDate = !empty($node->date_decommissioned) ? dol_print_date(strtotime($node->date_decommissioned), 'day') : '';
$decommText = $langs->trans('Decommissioned');
if ($decommDate) $decommText .= ' '.$decommDate;
print ' <span class="badge-decommissioned"><i class="fa fa-power-off"></i> '.$decommText.'</span>';
}
// Produkt-Badge
if (!empty($node->fk_product) && !empty($node->product_ref)) {
print ' <span class="badge badge-secondary"><i class="fa fa-cube"></i> '.dol_escape_htmltag($node->product_ref).'</span>';
}
// Spacer
print '<span class="kundenkarte-tree-spacer"></span>';
// Badges
$defaultBadgeColor = getDolGlobalString('KUNDENKARTE_TREE_BADGE_COLOR', '#2a4a5e');
if (!empty($treeInfoBadges)) {
print '<span class="kundenkarte-tree-badges">';
foreach ($treeInfoBadges as $info) {
$badgeIcon = kundenkarte_get_field_icon($info['code'], $info['type']);
$fieldBadgeColor = !empty($info['color']) ? $info['color'] : $defaultBadgeColor;
print '<span class="kundenkarte-tree-badge" title="'.dol_escape_htmltag($info['label']).'" style="background:linear-gradient(135deg, '.$fieldBadgeColor.' 0%, '.kundenkarte_adjust_color($fieldBadgeColor, -20).' 100%);">';
print '<i class="fa '.$badgeIcon.'"></i> '.dol_escape_htmltag($info['value']);
print '</span>';
}
print '</span>';
}
// Typ-Badge
if ($node->type_short || $node->type_label) {
$typeDisplay = $node->type_short ? $node->type_short : $node->type_label;
print '<span class="kundenkarte-tree-type badge badge-secondary">'.dol_escape_htmltag($typeDisplay).'</span>';
}
// Aktionen
print '<span class="kundenkarte-tree-actions">';
print '<a href="'.$viewUrl.'" title="'.$langs->trans('View').'"><i class="fa fa-eye"></i></a>';
if ($canEdit) {
print '<a href="'.$_SERVER['PHP_SELF'].'?action=create&parent_id='.$node->id.'" title="'.$langs->trans('AddChild').'"><i class="fa fa-plus"></i></a>';
print '<a href="'.$_SERVER['PHP_SELF'].'?action=edit&anlage_id='.$node->id.'" title="'.$langs->trans('Edit').'"><i class="fa fa-edit"></i></a>';
$decommLabel = $node->decommissioned ? $langs->trans('Recommission') : $langs->trans('Decommission');
$decommIcon = $node->decommissioned ? 'fa-plug' : 'fa-power-off';
print '<a href="#" class="btn-toggle-decommissioned" data-anlage-id="'.$node->id.'" title="'.$decommLabel.'"><i class="fa '.$decommIcon.'"></i></a>';
}
if ($canDelete) {
print '<a href="'.$_SERVER['PHP_SELF'].'?action=delete&anlage_id='.$node->id.'" title="'.$langs->trans('Delete').'" class="deletelink"><i class="fa fa-trash"></i></a>';
}
print '</span>';
print '</div>';
// Kinder
if ($hasChildren) {
print '<div class="kundenkarte-tree-children">';
werkzeuge_printTree($node->children, $socid, $systemId, $canEdit, $canDelete, $langs, $level + 1, $typeFieldsMap);
print '</div>';
}
print '</div>';
}
}
/**
* Baum-Options für Select (vereinfacht)
*/
function werkzeuge_printTreeOptions($nodes, $selected = 0, $excludeId = 0, $prefix = '', $level = 0)
{
foreach ($nodes as $node) {
if ($node->id == $excludeId) continue;
$sel = ($node->id == $selected) ? ' selected' : '';
print '<option value="'.$node->id.'"'.$sel.'>'.$prefix.dol_escape_htmltag($node->label).'</option>';
if (!empty($node->children)) {
werkzeuge_printTreeOptions($node->children, $selected, $excludeId, $prefix.'── ', $level + 1);
}
}
}