buchhaltungswidget/class/actions_buchaltungswidget.class.php

336 lines
9.2 KiB
PHP
Executable file

<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
*
* 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.
*/
/**
* \file htdocs/custom/buchaltungswidget/class/actions_buchaltungswidget.class.php
* \ingroup buchaltungswidget
* \brief Hook actions for displaying payment statistics on thirdparty card
*/
require_once DOL_DOCUMENT_ROOT.'/core/class/commonhookactions.class.php';
/**
* Class ActionsBuchaltungsWidget
*/
class ActionsBuchaltungsWidget extends CommonHookActions
{
/**
* @var DoliDB Database handler
*/
public $db;
/**
* @var string Error message
*/
public $error = '';
/**
* @var array Errors array
*/
public $errors = array();
/**
* @var string HTML output to be printed
*/
public $resprints;
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
public function __construct($db)
{
$this->db = $db;
}
/**
* Hook to add payment statistics on thirdparty card view
*
* @param array $parameters Hook parameters
* @param object $object The object being processed (Societe)
* @param string $action Current action
* @return int 0 = continue, > 0 = replace
*/
public function tabContentViewThirdparty($parameters, &$object, &$action)
{
global $conf, $langs, $user;
// Check if feature is enabled in settings (default: enabled)
if (!getDolGlobalInt('BUCHALTUNGSWIDGET_SHOW_PAYMENT_STATS', 1)) {
return 0;
}
// Check if user has rights
if (!$user->hasRight('facture', 'lire')) {
return 0;
}
// Only show for customers (client = 1, 2 or 3)
if (empty($object->client)) {
return 0;
}
$langs->load("buchaltungswidget@buchaltungswidget");
// Get payment statistics
$stats = $this->getPaymentStatistics($object->id);
if ($stats['invoice_count'] == 0) {
// No invoices yet
return 0;
}
// Determine color based on payment behavior
$colorClass = $this->getPaymentColorClass($stats);
$icon = $this->getPaymentIcon($stats);
// Build HTML output
$html = '';
$html .= '<div class="fichecenter">';
$html .= '<div class="payment-stats-box payment-stats-'.$colorClass.'" style="margin-bottom: 15px; padding: 12px 15px; border-radius: 6px; border: 2px solid; display: flex; align-items: center; gap: 15px;">';
// Icon
$html .= '<div class="payment-stats-icon" style="font-size: 2em;">'.$icon.'</div>';
// Stats content
$html .= '<div class="payment-stats-content" style="flex: 1;">';
$html .= '<div style="font-weight: 600; margin-bottom: 5px;">'.$langs->trans("PaymentBehavior").'</div>';
$html .= '<div style="display: flex; gap: 20px; flex-wrap: wrap;">';
// Average payment days
$html .= '<div>';
$html .= '<span style="color: #666; font-size: 0.9em;">'.$langs->trans("AvgPaymentDays").':</span> ';
$html .= '<strong>'.round($stats['avg_payment_days'], 1).' '.$langs->trans("Days").'</strong>';
$html .= '</div>';
// Average due days (payment terms)
$html .= '<div>';
$html .= '<span style="color: #666; font-size: 0.9em;">'.$langs->trans("AvgDueDays").':</span> ';
$html .= '<strong>'.round($stats['avg_due_days'], 1).' '.$langs->trans("Days").'</strong>';
$html .= '</div>';
// Difference
$diff = $stats['avg_payment_days'] - $stats['avg_due_days'];
$diffText = $diff > 0 ? '+'.round($diff, 1) : round($diff, 1);
$html .= '<div>';
$html .= '<span style="color: #666; font-size: 0.9em;">'.$langs->trans("Difference").':</span> ';
$html .= '<strong>'.$diffText.' '.$langs->trans("Days").'</strong>';
$html .= '</div>';
// Invoice count
$html .= '<div>';
$html .= '<span style="color: #666; font-size: 0.9em;">'.$langs->trans("PaidInvoices").':</span> ';
$html .= '<strong>'.$stats['invoice_count'].'</strong>';
$html .= '</div>';
$html .= '</div>'; // End stats flex
$html .= '</div>'; // End content
// Rating badge
$html .= '<div class="payment-stats-rating" style="text-align: center;">';
$html .= '<div style="font-size: 0.8em; color: #666;">'.$langs->trans("Rating").'</div>';
$html .= '<div style="font-size: 1.5em; font-weight: bold;">'.$this->getPaymentRatingText($stats, $langs).'</div>';
$html .= '</div>';
$html .= '</div>'; // End box
$html .= '</div>'; // End fichecenter
// Add CSS
$html .= $this->getPaymentStatsCSS();
// Print directly since Dolibarr doesn't auto-print resprints for this hook
print $html;
$this->resprints = $html;
return 0;
}
/**
* Get payment statistics for a thirdparty
*
* @param int $socid Thirdparty ID
* @return array Statistics array
*/
private function getPaymentStatistics($socid)
{
global $conf;
$stats = array(
'invoice_count' => 0,
'avg_payment_days' => 0,
'avg_due_days' => 0,
'on_time_count' => 0,
'late_count' => 0,
);
// Get all paid invoices for this customer with payment dates
$sql = "SELECT f.rowid, f.datef as invoice_date, f.date_lim_reglement as due_date,";
$sql .= " (SELECT MAX(p.datep) FROM ".MAIN_DB_PREFIX."paiement_facture as pf";
$sql .= " INNER JOIN ".MAIN_DB_PREFIX."paiement as p ON p.rowid = pf.fk_paiement";
$sql .= " WHERE pf.fk_facture = f.rowid) as payment_date";
$sql .= " FROM ".MAIN_DB_PREFIX."facture as f";
$sql .= " WHERE f.fk_soc = ".((int) $socid);
$sql .= " AND f.fk_statut = 2"; // Paid invoices only
$sql .= " AND f.entity = ".((int) $conf->entity);
$resql = $this->db->query($sql);
if ($resql) {
$totalPaymentDays = 0;
$totalDueDays = 0;
$count = 0;
while ($obj = $this->db->fetch_object($resql)) {
if ($obj->payment_date && $obj->invoice_date) {
$invoiceDate = strtotime($obj->invoice_date);
$paymentDate = strtotime($obj->payment_date);
$dueDate = $obj->due_date ? strtotime($obj->due_date) : $invoiceDate;
$paymentDays = ($paymentDate - $invoiceDate) / 86400;
$dueDays = ($dueDate - $invoiceDate) / 86400;
if ($paymentDays >= 0) { // Only count valid positive payment days
$totalPaymentDays += $paymentDays;
$totalDueDays += $dueDays;
$count++;
if ($paymentDate <= $dueDate) {
$stats['on_time_count']++;
} else {
$stats['late_count']++;
}
}
}
}
$stats['invoice_count'] = $count;
if ($count > 0) {
$stats['avg_payment_days'] = $totalPaymentDays / $count;
$stats['avg_due_days'] = $totalDueDays / $count;
}
$this->db->free($resql);
}
return $stats;
}
/**
* Get color class based on payment statistics
*
* @param array $stats Payment statistics
* @return string CSS color class
*/
private function getPaymentColorClass($stats)
{
$diff = $stats['avg_payment_days'] - $stats['avg_due_days'];
if ($diff <= -5) {
return 'excellent'; // Pays well before due date
} elseif ($diff <= 0) {
return 'good'; // Pays on time or slightly early
} elseif ($diff <= 7) {
return 'warning'; // Slightly late
} elseif ($diff <= 14) {
return 'late'; // Late
} else {
return 'critical'; // Very late
}
}
/**
* Get icon based on payment statistics
*
* @param array $stats Payment statistics
* @return string Icon HTML
*/
private function getPaymentIcon($stats)
{
$diff = $stats['avg_payment_days'] - $stats['avg_due_days'];
if ($diff <= -5) {
return '<span style="color: #28a745;">&#9733;</span>'; // Star
} elseif ($diff <= 0) {
return '<span style="color: #28a745;">&#10003;</span>'; // Checkmark
} elseif ($diff <= 7) {
return '<span style="color: #ffc107;">&#9888;</span>'; // Warning
} elseif ($diff <= 14) {
return '<span style="color: #fd7e14;">&#9201;</span>'; // Clock
} else {
return '<span style="color: #dc3545;">&#10007;</span>'; // X
}
}
/**
* Get rating text based on payment statistics
*
* @param array $stats Payment statistics
* @param Translate $langs Translation object
* @return string Rating text
*/
private function getPaymentRatingText($stats, $langs)
{
$diff = $stats['avg_payment_days'] - $stats['avg_due_days'];
if ($diff <= -5) {
return $langs->trans("PaymentExcellent");
} elseif ($diff <= 0) {
return $langs->trans("PaymentGood");
} elseif ($diff <= 7) {
return $langs->trans("PaymentWarning");
} elseif ($diff <= 14) {
return $langs->trans("PaymentLate");
} else {
return $langs->trans("PaymentCritical");
}
}
/**
* Get CSS for payment statistics box
*
* @return string CSS HTML
*/
private function getPaymentStatsCSS()
{
return '
<style>
.payment-stats-excellent {
background-color: #d4edda;
border-color: #28a745 !important;
color: #155724;
}
.payment-stats-good {
background-color: #cce5ff;
border-color: #007bff !important;
color: #004085;
}
.payment-stats-warning {
background-color: #fff3cd;
border-color: #ffc107 !important;
color: #664d03;
}
.payment-stats-late {
background-color: #ffe5d0;
border-color: #fd7e14 !important;
color: #663000;
}
.payment-stats-critical {
background-color: #f8d7da;
border-color: #dc3545 !important;
color: #721c24;
}
@media (max-width: 768px) {
.payment-stats-box {
flex-direction: column !important;
text-align: center;
}
}
</style>';
}
}