kundenkarte/class/equipment.class.php
data 619d14e8d5 feat(pwa): FI-Schutzgruppen, gebündelte Terminals, Terminal-Konfiguration
- Schutzgruppen-Zuordnung: Equipment kann FI/RCD zugeordnet werden
  - Farbliche Markierung der Schutzgruppen im Schaltplan
  - Dropdown zur Auswahl des Schutzgeräts im Equipment-Dialog
- Gebündelte Terminals: Multi-Phasen-Abgänge (E-Herd, Durchlauferhitzer)
  - "Alle bündeln" Option im Abgang-Dialog
  - Zentriertes Label über alle Terminals des Equipment
- Terminal-Anzahl aus terminals_config statt TE-Breite
  - Neozed 3F zeigt korrekt 3 statt 4 Terminals
  - Neue getTerminalCount() Hilfsfunktion
- Zuletzt bearbeitete Kunden (max. 5) auf Search-Screen
- Medium-Typen dynamisch aus DB mit Spezifikationen-Dropdown
- Terminal-Labels anklickbar zum direkten Bearbeiten
- Kontextmenü für leere Terminals (Input/Output Auswahl)
- Block-Label mit Einheiten (40A 30mA statt 40A30mA)
- Online-Status-Anzeige entfernt (funktionierte nicht zuverlässig)
- Service Worker v5.2: Versionierte Assets nicht cachen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-02 14:34:54 +01:00

544 lines
17 KiB
PHP
Executable file

<?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.
*/
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmenttype.class.php';
/**
* Class Equipment
* Manages equipment instances (Sicherungsautomaten, FI-Schalter, etc.)
*/
class Equipment extends CommonObject
{
public $element = 'equipment';
public $table_element = 'kundenkarte_equipment';
public $fk_carrier;
public $fk_equipment_type;
public $label;
public $position_te;
public $width_te;
public $field_values;
public $fk_product;
public $fk_protection; // FK to protection device (FI/RCD)
public $protection_label; // Label shown above equipment when in protection group
public $note_private;
public $status;
public $date_creation;
public $fk_user_creat;
public $fk_user_modif;
// Loaded objects
public $type; // EquipmentType object
public $type_label;
public $type_label_short;
public $type_color;
public $type_picto;
public $type_icon_file; // SVG/PNG schematic symbol (PDF)
public $type_block_image; // Image for block display in SchematicEditor
public $type_flow_direction; // Richtung: NULL, top_to_bottom, bottom_to_top
public $type_terminal_position; // Terminal-Position: both, top_only, bottom_only
public $product_ref;
public $product_label;
public $protection_device_label; // Label of the protection device
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Create object in database
*
* @param User $user User that creates
* @return int Return integer <0 if KO, Id of created object if OK
*/
public function create($user)
{
global $conf;
$error = 0;
$now = dol_now();
if (empty($this->fk_carrier) || empty($this->fk_equipment_type)) {
$this->error = 'ErrorMissingParameters';
return -1;
}
// Get default width from type if not set
if (empty($this->width_te)) {
$type = new EquipmentType($this->db);
if ($type->fetch($this->fk_equipment_type) > 0) {
$this->width_te = $type->width_te;
} else {
$this->width_te = 1;
}
}
$this->db->begin();
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, fk_carrier, fk_equipment_type, label,";
$sql .= " position_te, width_te, field_values, fk_product,";
$sql .= " fk_protection, protection_label,";
$sql .= " note_private, status,";
$sql .= " date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= ((int) $conf->entity);
$sql .= ", ".((int) $this->fk_carrier);
$sql .= ", ".((int) $this->fk_equipment_type);
$sql .= ", ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
$sql .= ", ".floatval($this->position_te);
$sql .= ", ".floatval($this->width_te);
$sql .= ", ".($this->field_values ? "'".$this->db->escape($this->field_values)."'" : "NULL");
$sql .= ", ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", ".($this->fk_protection > 0 ? ((int) $this->fk_protection) : "NULL");
$sql .= ", ".($this->protection_label ? "'".$this->db->escape($this->protection_label)."'" : "NULL");
$sql .= ", ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
$sql .= ", ".((int) ($this->status !== null ? $this->status : 1));
$sql .= ", '".$this->db->idate($now)."'";
$sql .= ", ".((int) $user->id);
$sql .= ")";
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if (!$error) {
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return $this->id;
}
}
/**
* Load object from database
*
* @param int $id ID of record
* @return int Return integer <0 if KO, 0 if not found, >0 if OK
*/
public function fetch($id)
{
$sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,";
$sql .= " t.ref as type_ref, t.color as type_color, t.picto as type_picto, t.icon_file as type_icon_file,";
$sql .= " t.block_image as type_block_image,";
$sql .= " t.flow_direction as type_flow_direction, t.terminal_position as type_terminal_position,";
$sql .= " t.terminals_config as terminals_config,";
$sql .= " p.ref as product_ref, p.label as product_label,";
$sql .= " prot.label as protection_device_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_type as t ON e.fk_equipment_type = t.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON e.fk_product = p.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as prot ON e.fk_protection = prot.rowid";
$sql .= " WHERE e.rowid = ".((int) $id);
$resql = $this->db->query($sql);
if ($resql) {
if ($this->db->num_rows($resql)) {
$obj = $this->db->fetch_object($resql);
$this->id = $obj->rowid;
$this->entity = $obj->entity;
$this->fk_carrier = $obj->fk_carrier;
$this->fk_equipment_type = $obj->fk_equipment_type;
$this->label = $obj->label;
$this->position_te = $obj->position_te;
$this->width_te = $obj->width_te;
$this->field_values = $obj->field_values;
$this->fk_product = $obj->fk_product;
$this->fk_protection = $obj->fk_protection;
$this->protection_label = $obj->protection_label;
$this->note_private = $obj->note_private;
$this->status = $obj->status;
$this->date_creation = $this->db->jdate($obj->date_creation);
$this->fk_user_creat = $obj->fk_user_creat;
$this->fk_user_modif = $obj->fk_user_modif;
$this->type_label = $obj->type_label;
$this->type_label_short = $obj->type_label_short;
$this->type_ref = $obj->type_ref;
$this->type_color = $obj->type_color;
$this->type_picto = $obj->type_picto;
$this->type_icon_file = $obj->type_icon_file;
$this->type_block_image = $obj->type_block_image;
$this->type_flow_direction = $obj->type_flow_direction;
$this->type_terminal_position = $obj->type_terminal_position ?: 'both';
$this->terminals_config = $obj->terminals_config;
$this->product_ref = $obj->product_ref;
$this->product_label = $obj->product_label;
$this->protection_device_label = $obj->protection_device_label;
$this->db->free($resql);
return 1;
} else {
$this->db->free($resql);
return 0;
}
} else {
$this->error = $this->db->lasterror();
return -1;
}
}
/**
* Update object in database
*
* @param User $user User that modifies
* @return int Return integer <0 if KO, >0 if OK
*/
public function update($user)
{
$error = 0;
$this->db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
$sql .= " fk_carrier = ".((int) $this->fk_carrier);
$sql .= ", fk_equipment_type = ".((int) $this->fk_equipment_type);
$sql .= ", label = ".($this->label ? "'".$this->db->escape($this->label)."'" : "NULL");
$sql .= ", position_te = ".floatval($this->position_te);
$sql .= ", width_te = ".floatval($this->width_te);
$sql .= ", field_values = ".($this->field_values ? "'".$this->db->escape($this->field_values)."'" : "NULL");
$sql .= ", fk_product = ".($this->fk_product > 0 ? ((int) $this->fk_product) : "NULL");
$sql .= ", fk_protection = ".($this->fk_protection > 0 ? ((int) $this->fk_protection) : "NULL");
$sql .= ", protection_label = ".($this->protection_label ? "'".$this->db->escape($this->protection_label)."'" : "NULL");
$sql .= ", note_private = ".($this->note_private ? "'".$this->db->escape($this->note_private)."'" : "NULL");
$sql .= ", status = ".((int) $this->status);
$sql .= ", fk_user_modif = ".((int) $user->id);
$sql .= " WHERE rowid = ".((int) $this->id);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return 1;
}
}
/**
* Delete object in database
*
* @param User $user User that deletes
* @return int Return integer <0 if KO, >0 if OK
*/
public function delete($user)
{
$error = 0;
$this->db->begin();
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element." WHERE rowid = ".((int) $this->id);
$resql = $this->db->query($sql);
if (!$resql) {
$error++;
$this->errors[] = "Error ".$this->db->lasterror();
}
if ($error) {
$this->db->rollback();
return -1 * $error;
} else {
$this->db->commit();
return 1;
}
}
/**
* Fetch all equipment on a carrier
*
* @param int $carrierId Carrier ID
* @param int $activeOnly Only active equipment
* @return array Array of Equipment objects
*/
public function fetchByCarrier($carrierId, $activeOnly = 1)
{
$results = array();
$sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,";
$sql .= " t.ref as type_ref, t.color as type_color, t.picto as type_picto, t.icon_file as type_icon_file,";
$sql .= " t.block_image as type_block_image,";
$sql .= " t.flow_direction as type_flow_direction, t.terminal_position as type_terminal_position,";
$sql .= " t.terminals_config as terminals_config,";
$sql .= " prot.label as protection_device_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_type as t ON e.fk_equipment_type = t.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment as prot ON e.fk_protection = prot.rowid";
$sql .= " WHERE e.fk_carrier = ".((int) $carrierId);
if ($activeOnly) {
$sql .= " AND e.status = 1";
}
$sql .= " ORDER BY e.position_te ASC";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$eq = new Equipment($this->db);
$eq->id = $obj->rowid;
$eq->entity = $obj->entity;
$eq->fk_carrier = $obj->fk_carrier;
$eq->fk_equipment_type = $obj->fk_equipment_type;
$eq->label = $obj->label;
$eq->position_te = $obj->position_te;
$eq->width_te = $obj->width_te;
$eq->field_values = $obj->field_values;
$eq->fk_product = $obj->fk_product;
$eq->fk_protection = $obj->fk_protection;
$eq->protection_label = $obj->protection_label;
$eq->note_private = $obj->note_private;
$eq->status = $obj->status;
$eq->type_label = $obj->type_label;
$eq->type_label_short = $obj->type_label_short;
$eq->type_ref = $obj->type_ref;
$eq->type_color = $obj->type_color;
$eq->type_picto = $obj->type_picto;
$eq->type_icon_file = $obj->type_icon_file;
$eq->type_block_image = $obj->type_block_image;
$eq->type_flow_direction = $obj->type_flow_direction;
$eq->type_terminal_position = $obj->type_terminal_position ?: 'both';
$eq->terminals_config = $obj->terminals_config;
$eq->protection_device_label = $obj->protection_device_label;
$results[] = $eq;
}
$this->db->free($resql);
}
return $results;
}
/**
* Fetch protection devices (FI/RCD) for an Anlage
* Protection devices are equipment types that can protect other equipment
*
* @param int $anlageId Anlage ID
* @return array Array of Equipment objects that are protection devices
*/
public function fetchProtectionDevices($anlageId)
{
$results = array();
// Get all equipment for this anlage that have type with is_protection = 1
// Or equipment types that are typically protection devices (FI, RCD, etc.)
$sql = "SELECT e.*, t.label as type_label, t.label_short as type_label_short,";
$sql .= " t.color as type_color, t.ref as type_ref, c.fk_anlage";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as e";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_type as t ON e.fk_equipment_type = t.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_carrier as c ON e.fk_carrier = c.rowid";
$sql .= " WHERE c.fk_anlage = ".((int) $anlageId);
$sql .= " AND e.status = 1";
// Filter for protection device types (FI, RCD, FI_LS, etc.)
$sql .= " AND (t.ref LIKE '%FI%' OR t.ref LIKE '%RCD%' OR t.ref LIKE 'RCBO%')";
$sql .= " ORDER BY c.position ASC, e.position_te ASC";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$eq = new Equipment($this->db);
$eq->id = $obj->rowid;
$eq->fk_carrier = $obj->fk_carrier;
$eq->fk_equipment_type = $obj->fk_equipment_type;
$eq->label = $obj->label;
$eq->position_te = $obj->position_te;
$eq->width_te = $obj->width_te;
$eq->type_label = $obj->type_label;
$eq->type_label_short = $obj->type_label_short;
$eq->type_color = $obj->type_color;
$results[] = $eq;
}
$this->db->free($resql);
}
return $results;
}
/**
* Duplicate this equipment (for "+" button)
*
* @param User $user User that creates
* @param int $carrierId Target carrier (0 = same carrier)
* @param int $position Target position (-1 = auto)
* @return int Return integer <0 if KO, Id of new object if OK
*/
public function duplicate($user, $carrierId = 0, $position = -1)
{
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipmentcarrier.class.php';
$newEquipment = new Equipment($this->db);
$newEquipment->fk_carrier = $carrierId > 0 ? $carrierId : $this->fk_carrier;
$newEquipment->fk_equipment_type = $this->fk_equipment_type;
$newEquipment->width_te = $this->width_te;
// Label: Nummer weiterzählen wenn vorhanden, sonst beibehalten
if (!empty($this->label) && preg_match('/^(.*?)(\d+)$/', $this->label, $matches)) {
$prefix = $matches[1];
// Höchste Nummer mit gleichem Präfix auf dem Carrier finden
$sql = "SELECT label FROM ".$this->db->prefix()."kundenkarte_equipment";
$sql .= " WHERE fk_carrier = ".(int)$newEquipment->fk_carrier;
$sql .= " AND status = 1 AND label IS NOT NULL AND label != ''";
$resql = $this->db->query($sql);
$maxNum = 0;
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
if (preg_match('/^'.preg_quote($prefix, '/').'(\d+)$/', $obj->label, $m)) {
$maxNum = max($maxNum, (int)$m[1]);
}
}
}
$newEquipment->label = $prefix . ($maxNum + 1);
} else {
$newEquipment->label = $this->label;
}
$newEquipment->field_values = $this->field_values;
$newEquipment->fk_product = $this->fk_product;
$newEquipment->fk_protection = $this->fk_protection;
$newEquipment->protection_label = $this->protection_label;
$newEquipment->note_private = $this->note_private;
$newEquipment->status = 1;
// Find position
if ($position >= 0) {
$newEquipment->position_te = $position;
} else {
// Auto-find next position after this equipment
$carrier = new EquipmentCarrier($this->db);
$carrier->fetch($newEquipment->fk_carrier);
$carrier->fetchEquipment();
// Try position right after this element
$tryPos = $this->position_te + $this->width_te;
if ($carrier->isPositionAvailable($tryPos, $this->width_te)) {
$newEquipment->position_te = $tryPos;
} else {
// Find any free position
$newEquipment->position_te = $carrier->getNextFreePosition($this->width_te);
if ($newEquipment->position_te < 0) {
$this->error = 'ErrorNoSpaceOnCarrier';
return -1;
}
}
}
return $newEquipment->create($user);
}
/**
* Get field values as array
*
* @return array
*/
public function getFieldValues()
{
if (empty($this->field_values)) {
return array();
}
$values = json_decode($this->field_values, true);
return is_array($values) ? $values : array();
}
/**
* Set field values from array
*
* @param array $values Key-value pairs
* @return void
*/
public function setFieldValues($values)
{
$this->field_values = json_encode($values);
}
/**
* Get single field value
*
* @param string $fieldCode Field code
* @return mixed|null
*/
public function getFieldValue($fieldCode)
{
$values = $this->getFieldValues();
return isset($values[$fieldCode]) ? $values[$fieldCode] : null;
}
/**
* Get label for SVG block display
* Combines fields with show_on_block = 1
*
* @return string Label to display on block (e.g. "B16")
*/
public function getBlockLabel()
{
$type = new EquipmentType($this->db);
if ($type->fetch($this->fk_equipment_type) <= 0) {
return $this->type_label_short ?: '';
}
$blockFields = $type->getBlockFields();
if (empty($blockFields)) {
return $this->type_label_short ?: '';
}
$values = $this->getFieldValues();
$parts = array();
foreach ($blockFields as $field) {
if (isset($values[$field->field_code]) && $values[$field->field_code] !== '') {
$val = $values[$field->field_code];
// Einheit hinzufügen für bekannte Felder (wie in Website JS kundenkarte.js:6613-6617)
if ($field->field_code === 'ampere') {
$val = $val . 'A';
} elseif ($field->field_code === 'sensitivity') {
$val = $val . 'mA';
}
$parts[] = $val;
}
}
if (empty($parts)) {
return $this->type_label_short ?: '';
}
// Mit Leerzeichen verbinden für bessere Lesbarkeit (z.B. "40A 30mA" statt "40A30mA")
return implode(' ', $parts);
}
/**
* Get color for SVG block
*
* @return string Hex color code
*/
public function getBlockColor()
{
if (!empty($this->type_color)) {
return $this->type_color;
}
// Default color
return '#3498db';
}
}