kundenkarte/class/equipmentcarrier.class.php
data 71272fa425 fix(schematic): Terminal-Farbpropagierung, Auto-Naming, PWA-Abgänge
- buildTerminalPhaseMap: Schritt 1b - Leitungen mit expliziter Farbe als
  Startpunkte (nur Gerät→Gerät, keine Abgänge)
- buildTerminalPhaseMap: Block-Durchreichung (Top↔Bottom) entfernt
- buildTerminalPhaseMap: Junction-Verbindungen (Terminal→Leitung)
  bidirektional verarbeitet via _connectionById Index
- PWA: Abgangs-Rendering mit Index-Fallback wenn source_terminal_id fehlt
- PWA: Abgangs-Labels max-height 130px, min-height 30px
- Auto-Naming: EquipmentCarrier create/update → 'R' + count
- Auto-Naming: EquipmentPanel update → 'Feld ' + count
- pwa_api.php: Hardcoded Fallbacks 'Feld'/'Hutschiene' entfernt
- pwa.js: Hutschiene Auto-Naming dynamisch aus Panel-Carrier-Anzahl
- kundenkarte.js: Carrier-Dialog Placeholder 'z.B. R1 (automatisch)'
- SW Cache auf v12.5 hochgezählt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:57:58 +01:00

465 lines
12 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.
*/
/**
* Class EquipmentCarrier
* Manages equipment carriers (Hutschienen/Traeger)
*/
class EquipmentCarrier extends CommonObject
{
public $element = 'equipmentcarrier';
public $table_element = 'kundenkarte_equipment_carrier';
public $fk_anlage;
public $fk_panel;
public $label;
public $total_te = 12;
public $position;
public $note_private;
public $status;
public $date_creation;
public $fk_user_creat;
public $fk_user_modif;
// Loaded objects
public $equipment = array();
public $anlage_label;
public $panel_label;
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Create object in database
*
* @param User $user User that creates
* @return int Return integer <0 if KO, Id of created object if OK
*/
public function create($user)
{
global $conf;
$error = 0;
$now = dol_now();
if (empty($this->fk_anlage)) {
$this->error = 'ErrorMissingParameters';
return -1;
}
// Get next position
if (empty($this->position)) {
$sql = "SELECT MAX(position) as maxpos FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE fk_anlage = ".((int) $this->fk_anlage);
$resql = $this->db->query($sql);
if ($resql) {
$obj = $this->db->fetch_object($resql);
$this->position = ($obj->maxpos !== null) ? $obj->maxpos + 1 : 0;
}
}
// Auto-naming wenn kein Label angegeben
if (empty($this->label)) {
$sqlCnt = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$this->table_element;
$sqlCnt .= " WHERE fk_panel = ".((int) $this->fk_panel);
$resCnt = $this->db->query($sqlCnt);
$cnt = ($resCnt && ($objCnt = $this->db->fetch_object($resCnt))) ? (int) $objCnt->cnt : 0;
$this->label = 'R'.($cnt + 1);
}
$this->db->begin();
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
$sql .= "entity, fk_anlage, fk_panel, label, total_te, position, note_private, status,";
$sql .= " date_creation, fk_user_creat";
$sql .= ") VALUES (";
$sql .= ((int) $conf->entity);
$sql .= ", ".((int) $this->fk_anlage);
$sql .= ", ".($this->fk_panel > 0 ? ((int) $this->fk_panel) : "NULL");
$sql .= ", '".$this->db->escape($this->label)."'";
$sql .= ", ".((int) ($this->total_te > 0 ? $this->total_te : 12));
$sql .= ", ".((int) $this->position);
$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 c.*, a.label as anlage_label, p.label as panel_label";
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as c";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_anlage as a ON c.fk_anlage = a.rowid";
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."kundenkarte_equipment_panel as p ON c.fk_panel = p.rowid";
$sql .= " WHERE c.rowid = ".((int) $id);
$resql = $this->db->query($sql);
if ($resql) {
if ($this->db->num_rows($resql)) {
$obj = $this->db->fetch_object($resql);
$this->id = $obj->rowid;
$this->entity = $obj->entity;
$this->fk_anlage = $obj->fk_anlage;
$this->fk_panel = $obj->fk_panel;
$this->label = $obj->label;
$this->total_te = $obj->total_te;
$this->position = $obj->position;
$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->anlage_label = $obj->anlage_label;
$this->panel_label = $obj->panel_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;
// Auto-naming wenn kein Label angegeben
if (empty($this->label)) {
$sqlCnt = "SELECT COUNT(*) as cnt FROM ".MAIN_DB_PREFIX.$this->table_element;
$sqlCnt .= " WHERE fk_panel = ".((int) $this->fk_panel);
$resCnt = $this->db->query($sqlCnt);
$cnt = ($resCnt && ($objCnt = $this->db->fetch_object($resCnt))) ? (int) $objCnt->cnt : 1;
$this->label = 'R'.$cnt;
}
$this->db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX.$this->table_element." SET";
$sql .= " label = '".$this->db->escape($this->label)."'";
$sql .= ", fk_panel = ".($this->fk_panel > 0 ? ((int) $this->fk_panel) : "NULL");
$sql .= ", total_te = ".((int) ($this->total_te > 0 ? $this->total_te : 12));
$sql .= ", position = ".((int) $this->position);
$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();
// Equipment is deleted via CASCADE
$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 carriers for a Panel
*
* @param int $panelId Panel ID
* @param int $activeOnly Only active carriers
* @return array Array of EquipmentCarrier objects
*/
public function fetchByPanel($panelId, $activeOnly = 1)
{
$results = array();
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE fk_panel = ".((int) $panelId);
if ($activeOnly) {
$sql .= " AND status = 1";
}
$sql .= " ORDER BY position ASC";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$carrier = new EquipmentCarrier($this->db);
$carrier->id = $obj->rowid;
$carrier->entity = $obj->entity;
$carrier->fk_anlage = $obj->fk_anlage;
$carrier->fk_panel = $obj->fk_panel;
$carrier->label = $obj->label;
$carrier->total_te = $obj->total_te;
$carrier->position = $obj->position;
$carrier->note_private = $obj->note_private;
$carrier->status = $obj->status;
$results[] = $carrier;
}
$this->db->free($resql);
}
return $results;
}
/**
* Fetch all carriers for an Anlage
*
* @param int $anlageId Anlage ID
* @param int $activeOnly Only active carriers
* @return array Array of EquipmentCarrier objects
*/
public function fetchByAnlage($anlageId, $activeOnly = 1)
{
$results = array();
$sql = "SELECT * FROM ".MAIN_DB_PREFIX.$this->table_element;
$sql .= " WHERE fk_anlage = ".((int) $anlageId);
if ($activeOnly) {
$sql .= " AND status = 1";
}
$sql .= " ORDER BY position ASC";
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
$carrier = new EquipmentCarrier($this->db);
$carrier->id = $obj->rowid;
$carrier->entity = $obj->entity;
$carrier->fk_anlage = $obj->fk_anlage;
$carrier->fk_panel = $obj->fk_panel;
$carrier->label = $obj->label;
$carrier->total_te = $obj->total_te;
$carrier->position = $obj->position;
$carrier->note_private = $obj->note_private;
$carrier->status = $obj->status;
$results[] = $carrier;
}
$this->db->free($resql);
}
return $results;
}
/**
* Fetch all equipment on this carrier
*
* @return array Array of Equipment objects
*/
public function fetchEquipment()
{
require_once DOL_DOCUMENT_ROOT.'/custom/kundenkarte/class/equipment.class.php';
$equipment = new Equipment($this->db);
$this->equipment = $equipment->fetchByCarrier($this->id);
return $this->equipment;
}
/**
* Belegte TE-Ranges zurückgeben (für Dezimal-Breiten)
*
* @return array Array von [start, end] Ranges
*/
public function getOccupiedRanges()
{
$ranges = array();
if (empty($this->equipment)) {
$this->fetchEquipment();
}
foreach ($this->equipment as $eq) {
$ranges[] = array(
'start' => floatval($eq->position_te),
'end' => floatval($eq->position_te) + floatval($eq->width_te)
);
}
usort($ranges, function($a, $b) {
return $a['start'] <=> $b['start'];
});
return $ranges;
}
/**
* Belegte TE-Summe
*
* @return float Belegte TE (kann Dezimal sein, z.B. 10.5)
*/
public function getUsedTE()
{
if (empty($this->equipment)) {
$this->fetchEquipment();
}
$used = 0;
foreach ($this->equipment as $eq) {
$used += floatval($eq->width_te);
}
return $used;
}
/**
* Freie TE
*
* @return float Freie TE
*/
public function getFreeTE()
{
return $this->total_te - $this->getUsedTE();
}
/**
* Nächste freie Position finden (unterstützt Dezimal-Breiten)
*
* @param float $width Benötigte Breite in TE
* @return float Position (1-basiert) oder -1 wenn kein Platz
*/
public function getNextFreePosition($width = 1)
{
$width = floatval($width);
$ranges = $this->getOccupiedRanges();
$maxEnd = floatval($this->total_te) + 1; // Position 1 + total_te = Ende der Schiene
$pos = 1.0;
foreach ($ranges as $range) {
// Passt in die Lücke vor diesem Element?
if ($pos + $width <= $range['start'] + 0.001) {
return $pos;
}
// Hinter dieses Element springen
if ($range['end'] > $pos) {
$pos = $range['end'];
}
}
// Platz nach dem letzten Element?
if ($pos + $width <= $maxEnd + 0.001) {
return $pos;
}
return -1;
}
/**
* Prüfen ob Position verfügbar ist (unterstützt Dezimal-Breiten)
*
* @param float $position Startposition (1-basiert)
* @param float $width Breite in TE
* @param int $excludeEquipmentId Equipment-ID zum Ausschließen (für Updates)
* @return bool True wenn Position frei
*/
public function isPositionAvailable($position, $width, $excludeEquipmentId = 0)
{
$position = floatval($position);
$width = floatval($width);
// Grenzen prüfen (Position 1 = erstes TE)
if ($position < 1 || $position + $width > floatval($this->total_te) + 1 + 0.001) {
return false;
}
if (empty($this->equipment)) {
$this->fetchEquipment();
}
$newEnd = $position + $width;
foreach ($this->equipment as $eq) {
if ($excludeEquipmentId > 0 && $eq->id == $excludeEquipmentId) {
continue;
}
// Overlap-Prüfung mit Half-Open-Ranges: [start, end)
$eqStart = floatval($eq->position_te);
$eqEnd = $eqStart + floatval($eq->width_te);
if ($position < $eqEnd - 0.001 && $eqStart < $newEnd - 0.001) {
return false;
}
}
return true;
}
}