feat: Add history/log tab to admin area
- New admin/history.php with two sections: - Cronjob run history with parsed output (updated/skipped/errors) - Price changes from llx_product_price (price_label='Preisbot') - Filter by 30/90/365 days or all - Direct link to product price page - Add "Verlauf" tab to preisbotAdminPrepareHead() - Add translation keys for history page (de_DE + en_US) - Bump version to 1.3 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5cd7e48041
commit
91c1ddd919
5 changed files with 272 additions and 1 deletions
230
admin/history.php
Normal file
230
admin/history.php
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
<?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 preisbot/admin/history.php
|
||||
* \ingroup preisbot
|
||||
* \brief History page: cronjob runs and price changes
|
||||
*/
|
||||
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["CONTEXT_DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["CONTEXT_DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
$tmp = empty($_SERVER['SCRIPT_FILENAME']) ? '' : $_SERVER['SCRIPT_FILENAME'];
|
||||
$tmp2 = realpath(__FILE__);
|
||||
$i = strlen($tmp) - 1;
|
||||
$j = strlen($tmp2) - 1;
|
||||
while ($i > 0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) {
|
||||
$i--;
|
||||
$j--;
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) {
|
||||
$res = @include substr($tmp, 0, ($i + 1))."/main.inc.php";
|
||||
}
|
||||
if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) {
|
||||
$res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../main.inc.php")) {
|
||||
$res = @include "../../main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) {
|
||||
die("Include of main fails");
|
||||
}
|
||||
|
||||
require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php';
|
||||
require_once '../lib/preisbot.lib.php';
|
||||
|
||||
$langs->loadLangs(array("admin", "preisbot@preisbot"));
|
||||
|
||||
if (!$user->admin) {
|
||||
accessforbidden();
|
||||
}
|
||||
|
||||
$filter = GETPOST('filter', 'aZ09');
|
||||
if (empty($filter)) {
|
||||
$filter = '30';
|
||||
}
|
||||
|
||||
/*
|
||||
* View
|
||||
*/
|
||||
|
||||
$form = new Form($db);
|
||||
$title = $langs->trans("PreisBotSetup");
|
||||
|
||||
llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-preisbot page-admin_history');
|
||||
|
||||
$linkback = '<a href="'.DOL_URL_ROOT.'/admin/modules.php?restore_lastsearch_values=1">'.$langs->trans("BackToModuleList").'</a>';
|
||||
print load_fiche_titre($title, $linkback, 'title_setup');
|
||||
|
||||
$head = preisbotAdminPrepareHead();
|
||||
print dol_get_fiche_head($head, 'history', $title, -1, 'preisbot@preisbot');
|
||||
|
||||
// ─── Sektion 1: Cronjob-Läufe ────────────────────────────────────────────────
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<h3 class="opacitymedium">'.$langs->trans("PreisBotCronRuns").'</h3>';
|
||||
|
||||
$sql = "SELECT rowid, label, status, processing, datelastrun, datelastresult, lastresult, lastoutput, datenextrun";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."cronjob";
|
||||
$sql .= " WHERE module_name = 'preisbot'";
|
||||
$sql .= " ORDER BY datelastrun DESC";
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if ($resql && $db->num_rows($resql) > 0) {
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("Date").'</th>';
|
||||
print '<th>'.$langs->trans("Status").'</th>';
|
||||
print '<th class="right">'.$langs->trans("PreisBotUpdated").'</th>';
|
||||
print '<th class="right">'.$langs->trans("PreisBotSkipped").'</th>';
|
||||
print '<th class="right">'.$langs->trans("Errors").'</th>';
|
||||
print '<th>'.$langs->trans("PreisBotLastOutput").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
// Parse Zusammenfassung aus lastoutput
|
||||
$updated = '-';
|
||||
$skipped = '-';
|
||||
$errors = '-';
|
||||
if (!empty($obj->lastoutput)) {
|
||||
if (preg_match('/Aktualisiert:\s*(\d+)/u', $obj->lastoutput, $m)) {
|
||||
$updated = $m[1];
|
||||
}
|
||||
if (preg_match('/Übersprungen:\s*(\d+)/u', $obj->lastoutput, $m)) {
|
||||
$skipped = $m[1];
|
||||
}
|
||||
if (preg_match('/Fehler:\s*(\d+)/u', $obj->lastoutput, $m)) {
|
||||
$errors = $m[1];
|
||||
}
|
||||
}
|
||||
|
||||
// Status-Badge
|
||||
if (!empty($obj->processing)) {
|
||||
$statusLabel = '<span class="badge badge-status4">'.$langs->trans("Running").'</span>';
|
||||
} elseif (is_null($obj->datelastresult)) {
|
||||
$statusLabel = '<span class="badge badge-status9">'.$langs->trans("Error").'</span>';
|
||||
} elseif ($obj->lastresult == 0) {
|
||||
$statusLabel = '<span class="badge badge-status1">'.$langs->trans("OK").'</span>';
|
||||
} else {
|
||||
$statusLabel = '<span class="badge badge-status8">'.$langs->trans("Error").'</span>';
|
||||
}
|
||||
|
||||
// Kurzausgabe (erste 3 Zeilen)
|
||||
$shortOutput = '';
|
||||
if (!empty($obj->lastoutput)) {
|
||||
$lines = array_filter(explode("\n", trim($obj->lastoutput)));
|
||||
$preview = array_slice(array_values($lines), 0, 3);
|
||||
$shortOutput = '<small class="opacitymedium">'.nl2br(dol_htmlentities(implode("\n", $preview))).'</small>';
|
||||
}
|
||||
|
||||
$errorClass = ($errors !== '-' && $errors > 0) ? ' class="error"' : '';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.dol_print_date($db->jdate($obj->datelastrun), 'dayhour').'</td>';
|
||||
print '<td>'.$statusLabel.'</td>';
|
||||
print '<td class="right"><strong>'.($updated !== '-' ? $updated : '-').'</strong></td>';
|
||||
print '<td class="right opacitymedium">'.$skipped.'</td>';
|
||||
print '<td class="right'.(($errors !== '-' && (int)$errors > 0) ? ' error' : '').'">'.$errors.'</td>';
|
||||
print '<td>'.$shortOutput.'</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
} else {
|
||||
print '<div class="opacitymedium">'.$langs->trans("PreisBotNoCronRuns").'</div>';
|
||||
}
|
||||
print '</div>';
|
||||
|
||||
print '<br>';
|
||||
|
||||
// ─── Sektion 2: Preisänderungen ───────────────────────────────────────────────
|
||||
print '<div class="div-table-responsive-no-min">';
|
||||
print '<h3 class="opacitymedium">'.$langs->trans("PreisBotPriceChanges").'</h3>';
|
||||
|
||||
// Filter-Auswahl
|
||||
print '<form method="GET" action="'.$_SERVER['PHP_SELF'].'">';
|
||||
print '<div class="divsearchfield">';
|
||||
print $langs->trans("Period").': ';
|
||||
print '<select name="filter" class="flat" onchange="this.form.submit()">';
|
||||
foreach (array('30' => $langs->trans("Last30Days"), '90' => $langs->trans("Last90Days"), '365' => $langs->trans("LastYear"), '0' => $langs->trans("All")) as $val => $label) {
|
||||
print '<option value="'.$val.'"'.($filter == $val ? ' selected' : '').'>'.$label.'</option>';
|
||||
}
|
||||
print '</select>';
|
||||
print '</div>';
|
||||
print '</form>';
|
||||
|
||||
$sqlFilter = '';
|
||||
if ($filter > 0) {
|
||||
$sqlFilter = " AND pp.date_price > DATE_SUB(NOW(), INTERVAL ".(int)$filter." DAY)";
|
||||
}
|
||||
|
||||
$sql = "SELECT pp.rowid, pp.fk_product, pp.date_price, pp.price, pp.price_ttc, pp.tva_tx, pp.price_base_type,";
|
||||
$sql .= " p.ref, p.label as product_label,";
|
||||
$sql .= " (SELECT pp2.price FROM ".MAIN_DB_PREFIX."product_price pp2";
|
||||
$sql .= " WHERE pp2.fk_product = pp.fk_product AND pp2.date_price < pp.date_price";
|
||||
$sql .= " ORDER BY pp2.date_price DESC LIMIT 1) as old_price";
|
||||
$sql .= " FROM ".MAIN_DB_PREFIX."product_price pp";
|
||||
$sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = pp.fk_product";
|
||||
$sql .= " WHERE pp.price_label = 'Preisbot'";
|
||||
$sql .= $sqlFilter;
|
||||
$sql .= " ORDER BY pp.date_price DESC";
|
||||
$sql .= " LIMIT 200";
|
||||
|
||||
$resql = $db->query($sql);
|
||||
if ($resql && $db->num_rows($resql) > 0) {
|
||||
print '<table class="noborder centpercent">';
|
||||
print '<tr class="liste_titre">';
|
||||
print '<th>'.$langs->trans("Date").'</th>';
|
||||
print '<th>'.$langs->trans("Ref").'</th>';
|
||||
print '<th>'.$langs->trans("Label").'</th>';
|
||||
print '<th class="right">'.$langs->trans("PreviousPrice").'</th>';
|
||||
print '<th class="right">'.$langs->trans("NewPrice").'</th>';
|
||||
print '<th class="right">'.$langs->trans("Diff").'</th>';
|
||||
print '<th class="right">'.$langs->trans("VAT").'</th>';
|
||||
print '</tr>';
|
||||
|
||||
while ($obj = $db->fetch_object($resql)) {
|
||||
$newPrice = (float) $obj->price;
|
||||
$oldPrice = !is_null($obj->old_price) ? (float) $obj->old_price : null;
|
||||
$diff = !is_null($oldPrice) ? $newPrice - $oldPrice : null;
|
||||
|
||||
if (!is_null($diff)) {
|
||||
$diffStr = ($diff >= 0 ? '+' : '').number_format($diff, 2, ',', '.').' €';
|
||||
$diffClass = $diff > 0 ? ' style="color:green"' : ($diff < 0 ? ' style="color:red"' : '');
|
||||
} else {
|
||||
$diffStr = '-';
|
||||
$diffClass = '';
|
||||
}
|
||||
|
||||
$productLink = '<a href="'.DOL_URL_ROOT.'/product/price.php?id='.$obj->fk_product.'">'.$obj->ref.'</a>';
|
||||
|
||||
print '<tr class="oddeven">';
|
||||
print '<td>'.dol_print_date($db->jdate($obj->date_price), 'dayhour').'</td>';
|
||||
print '<td>'.$productLink.'</td>';
|
||||
print '<td>'.dol_trunc($obj->product_label, 40).'</td>';
|
||||
print '<td class="right opacitymedium">'.(!is_null($oldPrice) ? number_format($oldPrice, 2, ',', '.').' €' : '-').'</td>';
|
||||
print '<td class="right"><strong>'.number_format($newPrice, 2, ',', '.').' €</strong></td>';
|
||||
print '<td class="right"'.$diffClass.'>'.$diffStr.'</td>';
|
||||
print '<td class="right">'.number_format((float)$obj->tva_tx, 0).' %</td>';
|
||||
print '</tr>';
|
||||
}
|
||||
|
||||
print '</table>';
|
||||
} else {
|
||||
print '<div class="opacitymedium">'.$langs->trans("PreisBotNoPriceChanges").'</div>';
|
||||
}
|
||||
print '</div>';
|
||||
|
||||
print dol_get_fiche_end();
|
||||
llxFooter();
|
||||
$db->close();
|
||||
|
|
@ -76,7 +76,7 @@ class modPreisBot extends DolibarrModules
|
|||
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@preisbot'
|
||||
|
||||
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
|
||||
$this->version = '1.2';
|
||||
$this->version = '1.3';
|
||||
// Url to the file with your last numberversion of this module
|
||||
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,24 @@ PreisBotExtrafieldDesc = Tragen Sie bei einem Produkt im Feld "Gewinnaufschlag %
|
|||
#
|
||||
PreisBotUpdatePrices = PreisBot - Verkaufspreise aktualisieren
|
||||
|
||||
#
|
||||
# History / Verlauf
|
||||
#
|
||||
PreisBotHistory = Verlauf
|
||||
PreisBotCronRuns = Cronjob-Läufe
|
||||
PreisBotPriceChanges = Preisänderungen durch PreisBot
|
||||
PreisBotNoCronRuns = Noch keine Läufe protokolliert.
|
||||
PreisBotNoPriceChanges = Noch keine Preisänderungen vorhanden.
|
||||
PreisBotLastOutput = Letzte Ausgabe
|
||||
PreisBotUpdated = Aktualisiert
|
||||
PreisBotSkipped = Übersprungen
|
||||
Last30Days = Letzte 30 Tage
|
||||
Last90Days = Letzte 90 Tage
|
||||
LastYear = Letztes Jahr
|
||||
PreviousPrice = Alter Preis
|
||||
NewPrice = Neuer Preis
|
||||
Period = Zeitraum
|
||||
|
||||
#
|
||||
# About Seite
|
||||
#
|
||||
|
|
|
|||
|
|
@ -52,6 +52,24 @@ PreisBotExtrafieldDesc = Enter a value of 20%% or higher in the "Profit Margin %
|
|||
#
|
||||
PreisBotUpdatePrices = PreisBot - Update Sales Prices
|
||||
|
||||
#
|
||||
# History
|
||||
#
|
||||
PreisBotHistory = History
|
||||
PreisBotCronRuns = Cronjob Runs
|
||||
PreisBotPriceChanges = Price Changes by PreisBot
|
||||
PreisBotNoCronRuns = No runs recorded yet.
|
||||
PreisBotNoPriceChanges = No price changes found.
|
||||
PreisBotLastOutput = Last Output
|
||||
PreisBotUpdated = Updated
|
||||
PreisBotSkipped = Skipped
|
||||
Last30Days = Last 30 Days
|
||||
Last90Days = Last 90 Days
|
||||
LastYear = Last Year
|
||||
PreviousPrice = Previous Price
|
||||
NewPrice = New Price
|
||||
Period = Period
|
||||
|
||||
#
|
||||
# About page
|
||||
#
|
||||
|
|
|
|||
|
|
@ -64,6 +64,11 @@ function preisbotAdminPrepareHead()
|
|||
$h++;
|
||||
*/
|
||||
|
||||
$head[$h][0] = dol_buildpath("/preisbot/admin/history.php", 1);
|
||||
$head[$h][1] = $langs->trans("PreisBotHistory");
|
||||
$head[$h][2] = 'history';
|
||||
$h++;
|
||||
|
||||
$head[$h][0] = dol_buildpath("/preisbot/admin/about.php", 1);
|
||||
$head[$h][1] = $langs->trans("About");
|
||||
$head[$h][2] = 'about';
|
||||
|
|
|
|||
Loading…
Reference in a new issue