PWA Mobile App für Schaltschrank-Dokumentation vor Ort: - Token-basierte Authentifizierung (15 Tage gültig) - Kundensuche mit Offline-Cache - Anlagen-Auswahl und Offline-Laden - Felder/Hutschienen/Automaten erfassen - Automatische Synchronisierung wenn wieder online - Installierbar auf dem Smartphone Home Screen - Touch-optimiertes Dark Mode Design - Quick-Select für Automaten-Werte (B16, C32, etc.) Schaltplan-Editor Verbesserungen: - Block Hover-Tooltip mit show_in_hover Feldern - Produktinfo mit Icon im Tooltip - Position und Breite in TE Neue Dateien: - pwa.php, pwa_auth.php - PWA Einstieg & Auth - ajax/pwa_api.php - PWA AJAX API - js/pwa.js, css/pwa.css - PWA App & Styles - sw.js, manifest.json - Service Worker & Manifest - img/pwa-icon-192.png, img/pwa-icon-512.png Version: 5.2.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
514 lines
16 KiB
PHP
Executable file
514 lines
16 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 .= ", ".((int) $this->position_te);
|
|
$sql .= ", ".((int) $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 = ".((int) $this->position_te);
|
|
$sql .= ", width_te = ".((int) $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->label = $this->label;
|
|
$newEquipment->width_te = $this->width_te;
|
|
$newEquipment->field_values = $this->field_values;
|
|
$newEquipment->fk_product = $this->fk_product;
|
|
$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] !== '') {
|
|
$parts[] = $values[$field->field_code];
|
|
}
|
|
}
|
|
|
|
if (empty($parts)) {
|
|
return $this->type_label_short ?: '';
|
|
}
|
|
|
|
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';
|
|
}
|
|
}
|