diff --git a/admin/history.php b/admin/history.php
new file mode 100644
index 0000000..58c3675
--- /dev/null
+++ b/admin/history.php
@@ -0,0 +1,230 @@
+
+ *
+ * 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 = ''.$langs->trans("BackToModuleList").'';
+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 '
';
+print '
'.$langs->trans("PreisBotCronRuns").'
';
+
+$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 '
';
+ print '';
+ print '| '.$langs->trans("Date").' | ';
+ print ''.$langs->trans("Status").' | ';
+ print ''.$langs->trans("PreisBotUpdated").' | ';
+ print ''.$langs->trans("PreisBotSkipped").' | ';
+ print ''.$langs->trans("Errors").' | ';
+ print ''.$langs->trans("PreisBotLastOutput").' | ';
+ print '
';
+
+ 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 = ''.$langs->trans("Running").'';
+ } elseif (is_null($obj->datelastresult)) {
+ $statusLabel = ''.$langs->trans("Error").'';
+ } elseif ($obj->lastresult == 0) {
+ $statusLabel = ''.$langs->trans("OK").'';
+ } else {
+ $statusLabel = ''.$langs->trans("Error").'';
+ }
+
+ // 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 = ''.nl2br(dol_htmlentities(implode("\n", $preview))).'';
+ }
+
+ $errorClass = ($errors !== '-' && $errors > 0) ? ' class="error"' : '';
+
+ print '';
+ print '| '.dol_print_date($db->jdate($obj->datelastrun), 'dayhour').' | ';
+ print ''.$statusLabel.' | ';
+ print ''.($updated !== '-' ? $updated : '-').' | ';
+ print ''.$skipped.' | ';
+ print ''.$errors.' | ';
+ print ''.$shortOutput.' | ';
+ print '
';
+ }
+
+ print '
';
+} else {
+ print '
'.$langs->trans("PreisBotNoCronRuns").'
';
+}
+print '
';
+
+print '
';
+
+// ─── Sektion 2: Preisänderungen ───────────────────────────────────────────────
+print '';
+print '
'.$langs->trans("PreisBotPriceChanges").'
';
+
+// Filter-Auswahl
+print '
';
+
+$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 '
';
+ print '';
+ print '| '.$langs->trans("Date").' | ';
+ print ''.$langs->trans("Ref").' | ';
+ print ''.$langs->trans("Label").' | ';
+ print ''.$langs->trans("PreviousPrice").' | ';
+ print ''.$langs->trans("NewPrice").' | ';
+ print ''.$langs->trans("Diff").' | ';
+ print ''.$langs->trans("VAT").' | ';
+ print '
';
+
+ 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 = ''.$obj->ref.'';
+
+ print '';
+ print '| '.dol_print_date($db->jdate($obj->date_price), 'dayhour').' | ';
+ print ''.$productLink.' | ';
+ print ''.dol_trunc($obj->product_label, 40).' | ';
+ print ''.(!is_null($oldPrice) ? number_format($oldPrice, 2, ',', '.').' €' : '-').' | ';
+ print ''.number_format($newPrice, 2, ',', '.').' € | ';
+ print ''.$diffStr.' | ';
+ print ''.number_format((float)$obj->tva_tx, 0).' % | ';
+ print '
';
+ }
+
+ print '
';
+} else {
+ print '
'.$langs->trans("PreisBotNoPriceChanges").'
';
+}
+print '
';
+
+print dol_get_fiche_end();
+llxFooter();
+$db->close();
diff --git a/core/modules/modPreisBot.class.php b/core/modules/modPreisBot.class.php
index 83c7de1..16f2346 100755
--- a/core/modules/modPreisBot.class.php
+++ b/core/modules/modPreisBot.class.php
@@ -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';
diff --git a/langs/de_DE/preisbot.lang b/langs/de_DE/preisbot.lang
index e49c867..539cd32 100755
--- a/langs/de_DE/preisbot.lang
+++ b/langs/de_DE/preisbot.lang
@@ -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
#
diff --git a/langs/en_US/preisbot.lang b/langs/en_US/preisbot.lang
index 6ca85ac..2e0b857 100755
--- a/langs/en_US/preisbot.lang
+++ b/langs/en_US/preisbot.lang
@@ -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
#
diff --git a/lib/preisbot.lib.php b/lib/preisbot.lib.php
index a2bae15..942dd7d 100755
--- a/lib/preisbot.lib.php
+++ b/lib/preisbot.lib.php
@@ -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';