kundenkarte/class/auditlog.class.php
data 844e6060c6 feat(pwa): Offline-fähige Progressive Web App für Elektriker
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>
2026-02-23 15:27:06 +01:00

455 lines
13 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 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);
}
}