336 lines
9.2 KiB
PHP
336 lines
9.2 KiB
PHP
<?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;">★</span>'; // Star
|
|
} elseif ($diff <= 0) {
|
|
return '<span style="color: #28a745;">✓</span>'; // Checkmark
|
|
} elseif ($diff <= 7) {
|
|
return '<span style="color: #ffc107;">⚠</span>'; // Warning
|
|
} elseif ($diff <= 14) {
|
|
return '<span style="color: #fd7e14;">⏱</span>'; // Clock
|
|
} else {
|
|
return '<span style="color: #dc3545;">✗</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>';
|
|
}
|
|
}
|