Neue Features: - Kabelverbindungen zwischen Anlagen-Elementen dokumentieren - Visuelle Baum-Darstellung mit parallelen vertikalen Linien - Jedes Element mit eigenem Kabel bekommt eigene Linie - Horizontale Verbindungslinien zum Element - Automatische Abstände zwischen Kabel-Gruppen - Kabeltypen (Medium Types) verwalten - Gebäude-Typen für Anlagen-Struktur - Tree-Display-Konfiguration pro System - Audit-Log für Änderungsverfolgung Verbesserungen: - Erste Kabel-Linie rechts, letzte links (korrekte Reihenfolge) - Horizontale Linien enden am Element-Rand - Spacer-Zeilen für bessere Übersichtlichkeit - BOM-Generator für Stücklisten (Prototyp) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
455 lines
13 KiB
PHP
455 lines
13 KiB
PHP
<?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 AuditLog
|
|
* Manages audit logging for KundenKarte module
|
|
*/
|
|
class AuditLog extends CommonObject
|
|
{
|
|
public $element = 'auditlog';
|
|
public $table_element = 'kundenkarte_audit_log';
|
|
|
|
public $object_type;
|
|
public $object_id;
|
|
public $object_ref;
|
|
public $fk_societe;
|
|
public $fk_anlage;
|
|
public $action;
|
|
public $field_changed;
|
|
public $old_value;
|
|
public $new_value;
|
|
public $fk_user;
|
|
public $user_login;
|
|
public $date_action;
|
|
public $note;
|
|
public $ip_address;
|
|
|
|
// Loaded properties
|
|
public $user_name;
|
|
public $societe_name;
|
|
|
|
// Action constants
|
|
const ACTION_CREATE = 'create';
|
|
const ACTION_UPDATE = 'update';
|
|
const ACTION_DELETE = 'delete';
|
|
const ACTION_MOVE = 'move';
|
|
const ACTION_DUPLICATE = 'duplicate';
|
|
const ACTION_STATUS_CHANGE = 'status';
|
|
|
|
// Object type constants
|
|
const TYPE_EQUIPMENT = 'equipment';
|
|
const TYPE_CARRIER = 'carrier';
|
|
const TYPE_PANEL = 'panel';
|
|
const TYPE_ANLAGE = 'anlage';
|
|
const TYPE_CONNECTION = 'connection';
|
|
const TYPE_BUSBAR = 'busbar';
|
|
const TYPE_EQUIPMENT_TYPE = 'equipment_type';
|
|
const TYPE_BUSBAR_TYPE = 'busbar_type';
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param DoliDB $db Database handler
|
|
*/
|
|
public function __construct($db)
|
|
{
|
|
$this->db = $db;
|
|
}
|
|
|
|
/**
|
|
* Log an action
|
|
*
|
|
* @param User $user User performing the action
|
|
* @param string $objectType Type of object (equipment, carrier, panel, etc.)
|
|
* @param int $objectId ID of the object
|
|
* @param string $action Action performed (create, update, delete, etc.)
|
|
* @param string $objectRef Reference/label of the object (optional)
|
|
* @param string $fieldChanged Specific field changed (optional)
|
|
* @param mixed $oldValue Previous value (optional)
|
|
* @param mixed $newValue New value (optional)
|
|
* @param int $socid Customer ID (optional)
|
|
* @param int $anlageId Anlage ID (optional)
|
|
* @param string $note Additional note (optional)
|
|
* @return int Log entry ID or <0 on error
|
|
*/
|
|
public function log($user, $objectType, $objectId, $action, $objectRef = '', $fieldChanged = '', $oldValue = null, $newValue = null, $socid = 0, $anlageId = 0, $note = '')
|
|
{
|
|
global $conf;
|
|
|
|
$now = dol_now();
|
|
|
|
// Serialize complex values
|
|
if (is_array($oldValue) || is_object($oldValue)) {
|
|
$oldValue = json_encode($oldValue);
|
|
}
|
|
if (is_array($newValue) || is_object($newValue)) {
|
|
$newValue = json_encode($newValue);
|
|
}
|
|
|
|
// Get IP address
|
|
$ipAddress = '';
|
|
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
|
$ipAddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
|
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
|
|
$ipAddress = $_SERVER['REMOTE_ADDR'];
|
|
}
|
|
|
|
$sql = "INSERT INTO ".MAIN_DB_PREFIX.$this->table_element." (";
|
|
$sql .= "entity, object_type, object_id, object_ref, fk_societe, fk_anlage,";
|
|
$sql .= " action, field_changed, old_value, new_value,";
|
|
$sql .= " fk_user, user_login, date_action, note, ip_address";
|
|
$sql .= ") VALUES (";
|
|
$sql .= ((int) $conf->entity);
|
|
$sql .= ", '".$this->db->escape($objectType)."'";
|
|
$sql .= ", ".((int) $objectId);
|
|
$sql .= ", ".($objectRef ? "'".$this->db->escape($objectRef)."'" : "NULL");
|
|
$sql .= ", ".($socid > 0 ? ((int) $socid) : "NULL");
|
|
$sql .= ", ".($anlageId > 0 ? ((int) $anlageId) : "NULL");
|
|
$sql .= ", '".$this->db->escape($action)."'";
|
|
$sql .= ", ".($fieldChanged ? "'".$this->db->escape($fieldChanged)."'" : "NULL");
|
|
$sql .= ", ".($oldValue !== null ? "'".$this->db->escape($oldValue)."'" : "NULL");
|
|
$sql .= ", ".($newValue !== null ? "'".$this->db->escape($newValue)."'" : "NULL");
|
|
$sql .= ", ".((int) $user->id);
|
|
$sql .= ", '".$this->db->escape($user->login)."'";
|
|
$sql .= ", '".$this->db->idate($now)."'";
|
|
$sql .= ", ".($note ? "'".$this->db->escape($note)."'" : "NULL");
|
|
$sql .= ", ".($ipAddress ? "'".$this->db->escape($ipAddress)."'" : "NULL");
|
|
$sql .= ")";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) {
|
|
$this->error = $this->db->lasterror();
|
|
return -1;
|
|
}
|
|
|
|
return $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
|
|
}
|
|
|
|
/**
|
|
* Log object creation
|
|
*/
|
|
public function logCreate($user, $objectType, $objectId, $objectRef = '', $socid = 0, $anlageId = 0, $data = null)
|
|
{
|
|
return $this->log($user, $objectType, $objectId, self::ACTION_CREATE, $objectRef, '', null, $data, $socid, $anlageId);
|
|
}
|
|
|
|
/**
|
|
* Log object update
|
|
*/
|
|
public function logUpdate($user, $objectType, $objectId, $objectRef = '', $fieldChanged = '', $oldValue = null, $newValue = null, $socid = 0, $anlageId = 0)
|
|
{
|
|
return $this->log($user, $objectType, $objectId, self::ACTION_UPDATE, $objectRef, $fieldChanged, $oldValue, $newValue, $socid, $anlageId);
|
|
}
|
|
|
|
/**
|
|
* Log object deletion
|
|
*/
|
|
public function logDelete($user, $objectType, $objectId, $objectRef = '', $socid = 0, $anlageId = 0, $data = null)
|
|
{
|
|
return $this->log($user, $objectType, $objectId, self::ACTION_DELETE, $objectRef, '', $data, null, $socid, $anlageId);
|
|
}
|
|
|
|
/**
|
|
* Log object move (position change)
|
|
*/
|
|
public function logMove($user, $objectType, $objectId, $objectRef = '', $oldPosition = null, $newPosition = null, $socid = 0, $anlageId = 0)
|
|
{
|
|
return $this->log($user, $objectType, $objectId, self::ACTION_MOVE, $objectRef, 'position', $oldPosition, $newPosition, $socid, $anlageId);
|
|
}
|
|
|
|
/**
|
|
* Log object duplication
|
|
*/
|
|
public function logDuplicate($user, $objectType, $objectId, $objectRef = '', $sourceId = 0, $socid = 0, $anlageId = 0)
|
|
{
|
|
return $this->log($user, $objectType, $objectId, self::ACTION_DUPLICATE, $objectRef, '', $sourceId, $objectId, $socid, $anlageId, 'Kopiert von ID '.$sourceId);
|
|
}
|
|
|
|
/**
|
|
* Fetch audit log entries for an object
|
|
*
|
|
* @param string $objectType Object type
|
|
* @param int $objectId Object ID
|
|
* @param int $limit Max entries (0 = no limit)
|
|
* @return array Array of AuditLog objects
|
|
*/
|
|
public function fetchByObject($objectType, $objectId, $limit = 50)
|
|
{
|
|
$results = array();
|
|
|
|
$sql = "SELECT a.*, u.firstname, u.lastname, s.nom as societe_name";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as u ON a.fk_user = u.rowid";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON a.fk_societe = s.rowid";
|
|
$sql .= " WHERE a.object_type = '".$this->db->escape($objectType)."'";
|
|
$sql .= " AND a.object_id = ".((int) $objectId);
|
|
$sql .= " ORDER BY a.date_action DESC";
|
|
if ($limit > 0) {
|
|
$sql .= " LIMIT ".((int) $limit);
|
|
}
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$log = new AuditLog($this->db);
|
|
$log->id = $obj->rowid;
|
|
$log->object_type = $obj->object_type;
|
|
$log->object_id = $obj->object_id;
|
|
$log->object_ref = $obj->object_ref;
|
|
$log->fk_societe = $obj->fk_societe;
|
|
$log->fk_anlage = $obj->fk_anlage;
|
|
$log->action = $obj->action;
|
|
$log->field_changed = $obj->field_changed;
|
|
$log->old_value = $obj->old_value;
|
|
$log->new_value = $obj->new_value;
|
|
$log->fk_user = $obj->fk_user;
|
|
$log->user_login = $obj->user_login;
|
|
$log->date_action = $this->db->jdate($obj->date_action);
|
|
$log->note = $obj->note;
|
|
$log->ip_address = $obj->ip_address;
|
|
$log->user_name = trim($obj->firstname.' '.$obj->lastname);
|
|
$log->societe_name = $obj->societe_name;
|
|
|
|
$results[] = $log;
|
|
}
|
|
$this->db->free($resql);
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Fetch audit log entries for an Anlage (installation)
|
|
*
|
|
* @param int $anlageId Anlage ID
|
|
* @param int $limit Max entries
|
|
* @return array Array of AuditLog objects
|
|
*/
|
|
public function fetchByAnlage($anlageId, $limit = 100)
|
|
{
|
|
$results = array();
|
|
|
|
$sql = "SELECT a.*, u.firstname, u.lastname";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as u ON a.fk_user = u.rowid";
|
|
$sql .= " WHERE a.fk_anlage = ".((int) $anlageId);
|
|
$sql .= " ORDER BY a.date_action DESC";
|
|
if ($limit > 0) {
|
|
$sql .= " LIMIT ".((int) $limit);
|
|
}
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$log = new AuditLog($this->db);
|
|
$log->id = $obj->rowid;
|
|
$log->object_type = $obj->object_type;
|
|
$log->object_id = $obj->object_id;
|
|
$log->object_ref = $obj->object_ref;
|
|
$log->fk_societe = $obj->fk_societe;
|
|
$log->fk_anlage = $obj->fk_anlage;
|
|
$log->action = $obj->action;
|
|
$log->field_changed = $obj->field_changed;
|
|
$log->old_value = $obj->old_value;
|
|
$log->new_value = $obj->new_value;
|
|
$log->fk_user = $obj->fk_user;
|
|
$log->user_login = $obj->user_login;
|
|
$log->date_action = $this->db->jdate($obj->date_action);
|
|
$log->note = $obj->note;
|
|
$log->ip_address = $obj->ip_address;
|
|
$log->user_name = trim($obj->firstname.' '.$obj->lastname);
|
|
|
|
$results[] = $log;
|
|
}
|
|
$this->db->free($resql);
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Fetch audit log entries for a customer
|
|
*
|
|
* @param int $socid Societe ID
|
|
* @param int $limit Max entries
|
|
* @return array Array of AuditLog objects
|
|
*/
|
|
public function fetchBySociete($socid, $limit = 100)
|
|
{
|
|
$results = array();
|
|
|
|
$sql = "SELECT a.*, u.firstname, u.lastname";
|
|
$sql .= " FROM ".MAIN_DB_PREFIX.$this->table_element." as a";
|
|
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."user as u ON a.fk_user = u.rowid";
|
|
$sql .= " WHERE a.fk_societe = ".((int) $socid);
|
|
$sql .= " ORDER BY a.date_action DESC";
|
|
if ($limit > 0) {
|
|
$sql .= " LIMIT ".((int) $limit);
|
|
}
|
|
|
|
$resql = $this->db->query($sql);
|
|
if ($resql) {
|
|
while ($obj = $this->db->fetch_object($resql)) {
|
|
$log = new AuditLog($this->db);
|
|
$log->id = $obj->rowid;
|
|
$log->object_type = $obj->object_type;
|
|
$log->object_id = $obj->object_id;
|
|
$log->object_ref = $obj->object_ref;
|
|
$log->fk_societe = $obj->fk_societe;
|
|
$log->fk_anlage = $obj->fk_anlage;
|
|
$log->action = $obj->action;
|
|
$log->field_changed = $obj->field_changed;
|
|
$log->old_value = $obj->old_value;
|
|
$log->new_value = $obj->new_value;
|
|
$log->fk_user = $obj->fk_user;
|
|
$log->user_login = $obj->user_login;
|
|
$log->date_action = $this->db->jdate($obj->date_action);
|
|
$log->note = $obj->note;
|
|
$log->ip_address = $obj->ip_address;
|
|
$log->user_name = trim($obj->firstname.' '.$obj->lastname);
|
|
|
|
$results[] = $log;
|
|
}
|
|
$this->db->free($resql);
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* Get human-readable action label
|
|
*
|
|
* @return string Translated action label
|
|
*/
|
|
public function getActionLabel()
|
|
{
|
|
global $langs;
|
|
|
|
switch ($this->action) {
|
|
case self::ACTION_CREATE:
|
|
return $langs->trans('AuditActionCreate');
|
|
case self::ACTION_UPDATE:
|
|
return $langs->trans('AuditActionUpdate');
|
|
case self::ACTION_DELETE:
|
|
return $langs->trans('AuditActionDelete');
|
|
case self::ACTION_MOVE:
|
|
return $langs->trans('AuditActionMove');
|
|
case self::ACTION_DUPLICATE:
|
|
return $langs->trans('AuditActionDuplicate');
|
|
case self::ACTION_STATUS_CHANGE:
|
|
return $langs->trans('AuditActionStatus');
|
|
default:
|
|
return $this->action;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get human-readable object type label
|
|
*
|
|
* @return string Translated object type label
|
|
*/
|
|
public function getObjectTypeLabel()
|
|
{
|
|
global $langs;
|
|
|
|
switch ($this->object_type) {
|
|
case self::TYPE_EQUIPMENT:
|
|
return $langs->trans('Equipment');
|
|
case self::TYPE_CARRIER:
|
|
return $langs->trans('CarrierLabel');
|
|
case self::TYPE_PANEL:
|
|
return $langs->trans('PanelLabel');
|
|
case self::TYPE_ANLAGE:
|
|
return $langs->trans('Installation');
|
|
case self::TYPE_CONNECTION:
|
|
return $langs->trans('Connection');
|
|
case self::TYPE_BUSBAR:
|
|
return $langs->trans('Busbar');
|
|
case self::TYPE_EQUIPMENT_TYPE:
|
|
return $langs->trans('EquipmentType');
|
|
case self::TYPE_BUSBAR_TYPE:
|
|
return $langs->trans('BusbarType');
|
|
default:
|
|
return $this->object_type;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get action icon
|
|
*
|
|
* @return string FontAwesome icon class
|
|
*/
|
|
public function getActionIcon()
|
|
{
|
|
switch ($this->action) {
|
|
case self::ACTION_CREATE:
|
|
return 'fa-plus-circle';
|
|
case self::ACTION_UPDATE:
|
|
return 'fa-edit';
|
|
case self::ACTION_DELETE:
|
|
return 'fa-trash';
|
|
case self::ACTION_MOVE:
|
|
return 'fa-arrows';
|
|
case self::ACTION_DUPLICATE:
|
|
return 'fa-copy';
|
|
case self::ACTION_STATUS_CHANGE:
|
|
return 'fa-toggle-on';
|
|
default:
|
|
return 'fa-question';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get action color
|
|
*
|
|
* @return string CSS color
|
|
*/
|
|
public function getActionColor()
|
|
{
|
|
switch ($this->action) {
|
|
case self::ACTION_CREATE:
|
|
return '#27ae60';
|
|
case self::ACTION_UPDATE:
|
|
return '#3498db';
|
|
case self::ACTION_DELETE:
|
|
return '#e74c3c';
|
|
case self::ACTION_MOVE:
|
|
return '#9b59b6';
|
|
case self::ACTION_DUPLICATE:
|
|
return '#f39c12';
|
|
case self::ACTION_STATUS_CHANGE:
|
|
return '#1abc9c';
|
|
default:
|
|
return '#95a5a6';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean old audit log entries
|
|
*
|
|
* @param int $daysToKeep Number of days to keep (default: 365)
|
|
* @return int Number of deleted entries or -1 on error
|
|
*/
|
|
public function cleanOldEntries($daysToKeep = 365)
|
|
{
|
|
$cutoffDate = dol_now() - ($daysToKeep * 24 * 60 * 60);
|
|
|
|
$sql = "DELETE FROM ".MAIN_DB_PREFIX.$this->table_element;
|
|
$sql .= " WHERE date_action < '".$this->db->idate($cutoffDate)."'";
|
|
|
|
$resql = $this->db->query($sql);
|
|
if (!$resql) {
|
|
$this->error = $this->db->lasterror();
|
|
return -1;
|
|
}
|
|
|
|
return $this->db->affected_rows($resql);
|
|
}
|
|
}
|