Compare commits

..
10 Commits
Author SHA1 Message Date
Thomas Bartelt d5a3727884 fix(artikel): attribute_7 nur als Ziffernfolge in die SQL (Leerzeichen => HTTP 500); zieht Live-Haertung vom 25.08. (Grundpreis-Teiler) nach 2026-09-27 10:16:49 +02:00
Thomas Bartelt 5844b24db4 style(warenkorb-neu): Leer-Ansicht rechtsbuendig, Knopf raus aus der Klick-Falle des fixierten Kopfes 2026-09-27 10:08:31 +02:00
Thomas Bartelt d0b79a2820 fix(warenkorb-neu, kasse-neu): Scroll nicht wiederherstellen, wenn der Korb leer wurde (Knopf lag unter dem fixierten Kopf); sticky Kaesten unter den 152px-Kopf (top 168px) 2026-09-27 10:02:47 +02:00
Thomas Bartelt 4ef81ceec3 fix(warenkorb-neu): 'Weiter einkaufen' ueber das Formular (cart_goto=weiter), Auto-Speichern mit Wartezeit statt Wettlauf gegen den Klick, Doppel-Submit-Riegel; Anfuehrungszeichen 2026-09-27 09:55:16 +02:00
Thomas Bartelt 4c1ecb82a7 fix(kasse): Gast-/Express-Bestellung ohne Anrede verlor die Adresse -- CustomerAddress::validate() verlangt honorific; leer => Firma/Herr, Fehlschlag jetzt im Log 2026-09-27 00:36:03 +02:00
Thomas Bartelt 30c81cae9c fix(kasse-neu): schliessende Klammer stand hinter dem Kommentar -- Syntaxfehler, Bestellen-Knopf ohne Handler 2026-09-26 23:51:42 +02:00
Thomas Bartelt def156e1c2 feat(kasse, warenkorb): neue Ansicht ist Standard, Rueckfall ?ansicht=alt per Session
- Weiche in website_shoppingcart.php / website_cashdesk.php gedreht: content_*_neu.tpl
  ist Standard, ?ansicht=alt schaltet die Sitzung auf die alten Templates, ?ansicht=neu zurueck
- Parameter ansicht=neu aus Form-Actions und Links der neuen Templates entfernt
- Erfolgsseite completed.tpl und payment_result.tpl (theme2) im Stil der neuen Kasse
  (Schrittleiste 3 Fertig, Karte, Bestellnummer, Text aus cash_desk_order_completed)
- Erfolgsseite ohne Navigation (site_type full_width) wie Warenkorb/Kasse
- sticky Bestellkasten unter dem klebenden Theme-Kopf (top 105px statt 16px)
- fix: Korbsumme 0 teilte in manage_gift_certificate() durch null (HTTP 500)
2026-09-26 23:43:41 +02:00
Tom Bartelt 6cea66126a fix(kasse): HTTP 500 in der Gast-Kasse bei leerem Land
- Country::getShippingAreaById: Land/Gruppe als int, leeres Land -> false
  statt 'WHERE id = ' (SQL-Fatal, leere Seite, 16x seit 03.09.2026)
- manage_guest_order_address: Lieferadresse = Rechnungsadresse ohne Land
  -> Standardland fuer die Versandgebiet-Suche
2026-09-22 21:21:38 +02:00
Tom Bartelt 40b4774f0d fix(kundencenter): SQL-Fatal beim Adresse speichern (leerer parent_id)
- main_widget_menu_2: !empty statt isset, sonst 'parent_id in(10636,)'
- add_address: exit nach dem Location-Header
- get_this_url: leere Parameter nicht anhaengen
2026-09-22 20:45:53 +02:00
Tom Bartelt 85bc1d8c9a feat(payments): Block Zahlungsvorgang im Bestell-Editor, Storno-Felder bei automatischem Storno
- Bestell-Editor rechts: Zahlungsvorgang (Status, Grund, Transaktions-ID,
  Referenz, Betraege, Zeiten, Verlauf aus PG payment_log), anbieterneutral
  ueber payment_transactions (Payments\Overview, Log::forOrder)
- markCancelled() fuellt Stornodatum, Stornostatus, Stornogrund und
  Storno-Nachricht der Bestellung (nur leere Felder)
- neuer Stornogrund 'Zahlung beim Anbieter nicht abgeschlossen' (install.sql)
- reconcile: 404-Grund lesbar mit Alter des Vorgangs
2026-09-22 09:30:52 +02:00
18 changed files with 561 additions and 63 deletions
+8
View File
@@ -261,6 +261,14 @@ class Country extends Main {
public static function getShippingAreaById($countryId, $customer_group_id = false) {
$db = Registry::get('base')->db;
// 22.09.2026: leeres Land (Gast-Kasse per Enter abgeschickt, Land nicht gewaehlt) lief in
// "WHERE id = " und damit in einen SQL-Fatal -> HTTP 500 mit leerer Seite in der Kasse.
$countryId = (int) $countryId;
$customer_group_id = (int) $customer_group_id;
if ($countryId <= 0) {
return false;
}
$sql = "SELECT shipping_area_id
FROM countries
WHERE id = $countryId";
+25 -2
View File
@@ -275,13 +275,36 @@ abstract class AbstractProvider implements ProviderInterface
}
$this->setTransactionStatus($reference, 'cancelled', $why);
$orderId = (int) $tx->order_id;
// 6 = storniert (wie v1-PayPal im Cancel-Zweig)
$this->db->query("UPDATE orders SET order_status=6 WHERE id=$orderId AND payment_status<>2");
// 6 = storniert (wie v1-PayPal im Cancel-Zweig).
// 22.09.2026 -- dazu die Storno-Felder der Bestellung fuellen, damit im Bestell-Editor
// steht, WARUM: bisher stand da nur "Storniert", der Grund lag allein in
// payment_transactions.detail. Vorhandene Eintraege werden nicht ueberschrieben.
$msg = $this->db->real_escape_string(
$this->label() . ': ' . $why . '. Zahlung nicht abgeschlossen, kein Geld geflossen. '
. 'Automatisch storniert (' . $source . ', ' . \date('d.m.Y H:i') . ').'
);
$reason = $this->cancellationReasonId();
$this->db->query(
"UPDATE orders SET order_status=6,
cancellation_date = COALESCE(cancellation_date, CURDATE()),
cancellation_status = IF(cancellation_status IS NULL OR cancellation_status=0, 2, cancellation_status),
cancellation_reason_id = " . ($reason > 0 ? "COALESCE(NULLIF(cancellation_reason_id,0), $reason)" : 'cancellation_reason_id') . ",
cancellation_info = IF(cancellation_info IS NULL OR cancellation_info='', '$msg', CONCAT(cancellation_info, '\n', '$msg'))
WHERE id=$orderId AND payment_status<>2"
);
$this->pendingClear();
$this->event('cancelled', $orderId, (string) $tx->order_number, (float) $tx->expected_amount, $reference, null, (int) $tx->customer_id, $why . ' (' . $source . ')');
Log::write($this->module(), 'cancel', $orderId, (string) $tx->order_number, $reference, 'cancelled', null, null, $why);
}
/** Stornogrund "Zahlung beim Anbieter nicht abgeschlossen" (install.sql), 0 wenn nicht angelegt. */
private function cancellationReasonId(): int
{
$res = $this->db->query("SELECT id FROM cancellation_reasons WHERE name='Zahlung beim Anbieter nicht abgeschlossen' LIMIT 1");
$r = $res ? $res->fetch_object() : null;
return $r ? (int) $r->id : 0;
}
protected function amountMatches(string $expected, string $paid, string $expCur, string $paidCur): bool
{
return \number_format((float) $expected, 2, '.', '') === \number_format((float) $paid, 2, '.', '')
+28
View File
@@ -56,6 +56,34 @@ final class Log
return true;
}
/**
* Verlauf einer Bestellung fuer den Bestell-Editor, aelteste zuerst.
* Payload bleibt draussen (Roh-JSON gehoert nicht in die Maske).
*
* @return list<array{zeit:string,action:string,status:?string,http_status:?string,detail:?string,reference:?string}>
*/
public static function forOrder(int $orderId, int $limit = 40): array
{
$c = self::conn();
if (!$c || $orderId <= 0) {
return [];
}
$host = (string) ($_SERVER['SERVER_NAME'] ?? \gethostname());
$res = @\pg_query_params(
$c,
"SELECT to_char(zeitpunkt, 'DD.MM.YYYY HH24:MI:SS') AS zeit, action, status, http_status, detail, reference
FROM payment_log
WHERE order_id = $1 AND host = $2
ORDER BY id
LIMIT $3",
[$orderId, $host, $limit]
);
if (!$res) {
return [];
}
return \pg_fetch_all($res) ?: [];
}
/** @return resource|\PgSql\Connection|false */
private static function conn()
{
+121
View File
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Payments;
/**
* Lesesicht auf einen Zahlungsvorgang fuers Backend (Bestell-Editor, Block "Zahlungsvorgang").
*
* Anbieterneutral: liest payment_transactions (MySQL) und den Verlauf aus payment_log (PG).
* Kein Provider-Aufruf, keine API -- nur Anzeige. Faellt still auf null zurueck, wenn die
* Bestellung keinen Vorgang hat (alte Zahlarten, Vorkasse, Rechnung ...).
*/
final class Overview
{
private const STATUS = [
'open' => 'offen -- Kunde ist beim Anbieter',
'pending' => 'in Pruefung beim Anbieter',
'paid' => 'bezahlt',
'cancelled' => 'abgebrochen / storniert',
'mismatch' => 'BETRAG WEICHT AB -- pruefen!',
];
private const SOURCE = [
'return' => 'Rueckkehr des Kunden vom Anbieter',
'express' => 'Express-Kauf aus dem Warenkorb',
'webhook' => 'Meldung des Anbieters (Webhook)',
'reconcile' => 'Abgleicher (Cron)',
];
private const ACTION = [
'create' => 'Vorgang beim Anbieter angelegt',
'start' => 'Kunde zum Anbieter weitergeleitet',
'capture' => 'Einzug (Capture)',
'webhook' => 'Meldung vom Anbieter',
'mail' => 'Bestellbestaetigung an Kunden',
'cancel' => 'Storno',
'mismatch' => 'Betragsabweichung',
'express_create' => 'Express-Kauf gestartet',
'express_get' => 'Express: Freigabe abgeholt',
'express_patch' => 'Express: Betrag angepasst',
'express_reapprove' => 'Express: zweite Freigabe noetig',
];
/**
* @return array<string,mixed>|null Felder fuers Template oder null ohne Vorgang
*/
public static function forOrder(\mysqli $db, int $orderId, ?object $base = null): ?array
{
if ($orderId <= 0) {
return null;
}
$stmt = $db->prepare('SELECT * FROM payment_transactions WHERE order_id=? ORDER BY id DESC LIMIT 1');
if (!$stmt) {
return null;
}
$stmt->bind_param('i', $orderId);
$stmt->execute();
$tx = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$tx) {
return null;
}
$module = (string) $tx['provider'];
$label = $module;
if ($base !== null && Registry::has($module)) {
$p = Registry::get($module, $base);
if ($p) {
$label = $p->label();
}
}
$status = (string) $tx['status'];
$detail = (string) ($tx['detail'] ?? '');
if ($status === 'paid') {
// markPaid() legt in detail nur die Quelle ab (return/express/webhook/reconcile).
$detail = 'bezahlt, bestaetigt ueber: ' . (self::SOURCE[$detail] ?? $detail);
}
return [
'provider' => $module,
'provider_label' => $label,
'status' => $status,
'status_label' => self::STATUS[$status] ?? $status,
'reference' => (string) $tx['reference'],
'capture_id' => (string) ($tx['capture_id'] ?? ''),
'expected_amount' => \number_format((float) $tx['expected_amount'], 2, ',', '.') . ' ' . $tx['currency'],
'paid_amount' => $tx['paid_amount'] !== null ? \number_format((float) $tx['paid_amount'], 2, ',', '.') . ' ' . $tx['currency'] : '',
'created_at' => self::de((string) $tx['created_at']),
'paid_at' => self::de((string) ($tx['paid_at'] ?? '')),
'updated_at' => self::de((string) $tx['updated_at']),
'detail' => $detail,
'history' => self::history($orderId),
];
}
/** @return list<array{zeit:string,aktion:string,status:string,http:string,detail:string}> */
private static function history(int $orderId): array
{
$out = [];
foreach (Log::forOrder($orderId, 40) as $row) {
$a = (string) $row['action'];
$out[] = [
'zeit' => (string) $row['zeit'],
'aktion' => self::ACTION[$a] ?? $a,
'status' => (string) ($row['status'] ?? ''),
'http' => $row['http_status'] !== null && $row['http_status'] !== '' ? 'HTTP ' . $row['http_status'] : '',
'detail' => (string) ($row['detail'] ?? ''),
];
}
return $out;
}
private static function de(string $dt): string
{
if ($dt === '' || \str_starts_with($dt, '0000')) {
return '';
}
$t = \strtotime($dt);
return $t ? \date('d.m.Y H:i:s', $t) : $dt;
}
}
+1 -1
View File
@@ -60,7 +60,7 @@ final class Registry
public static function requireCore(): void
{
foreach (['ProviderInterface', 'Result', 'Secrets', 'Log', 'AbstractProvider'] as $c) {
foreach (['ProviderInterface', 'Result', 'Secrets', 'Log', 'Overview', 'AbstractProvider'] as $c) {
require_once __DIR__ . '/' . $c . '.php';
}
}
+6
View File
@@ -47,6 +47,12 @@ INSERT IGNORE INTO `configuration` (`name`, `text`, `group_id`, `setting`, `type
('paypal_v2_express', '', 'paypal_v2', '1', 1, '1 = Button "Direkt mit PayPal bezahlen" im Warenkorb, 0 = aus'),
('paypal_v2_express_countries', '', 'paypal_v2', 'DE', 1, 'Laender (ISO-2, kommagetrennt), deren PayPal-Adresse im Express-Kauf angenommen wird');
-- 2a2) Stornogrund fuer automatische Stornos (Kunde hat beim Anbieter nicht bezahlt).
-- type 0 = Kunden Storno. markCancelled() sucht ihn ueber den Namen.
INSERT INTO `cancellation_reasons` (`name`, `type`, `active`)
SELECT 'Zahlung beim Anbieter nicht abgeschlossen', 0, 1
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM `cancellation_reasons` WHERE `name` = 'Zahlung beim Anbieter nicht abgeschlossen');
-- 2b) Zahlart anlegen. active=1, aber noch KEINER Kundengruppe zugeordnet => im Shop unsichtbar.
-- helperjoinkey 'pp' = derselbe PayPal-Aufschlag wie die alte Zahlart (shipping_area.add_pp).
INSERT INTO `payment_methods`
@@ -540,7 +540,10 @@ final class Provider extends AbstractProvider
if ($status === 404) {
$out[] = "$ref (Order {$tx->order_id}): bei PayPal unbekannt -> storniert" . ($dryRun ? ' [dry-run]' : '');
if (!$dryRun) {
$this->markCancelled($ref, 'PayPal kennt den Vorgang nicht (404)', 'reconcile');
// Beobachtet auf Live 19.-22.09.2026: nie freigegebene Orders (CREATED) liefern
// nach ~6 h 404 -- der Kunde ist bei PayPal nicht bis zur Freigabe gekommen.
$age = \round((\time() - \strtotime((string) $tx->created_at)) / 3600, 1);
$this->markCancelled($ref, "Kunde hat den Vorgang bei PayPal nicht abgeschlossen (nach $age h bei PayPal unbekannt, 404)", 'reconcile');
}
continue;
}
+4 -2
View File
@@ -155,10 +155,12 @@ class website {
function main_widget_menu_2() {
$structure_object = new Structure($this->base_object);
$selected_ids = array();
if (isset($_GET['menu_id'])) {
// 22.09.2026: !empty statt isset -- das Kundencenter postet auf "...&parent_id=" (leer),
// isset war dann wahr und get_all_tree() baute "parent_id in(10636,)" -> SQL-Fatal.
if (!empty($_GET['menu_id'])) {
$selected_ids[] = $_GET['menu_id'];
}
if (isset($_GET['parent_id'])) {
if (!empty($_GET['parent_id'])) {
$selected_ids[] = $_GET['parent_id'];
}
$data = $structure_object->get_all_tree(5, $selected_ids, $this->base_object->customer_group->id);
+5
View File
@@ -152,6 +152,11 @@ class admin_order_editor {
$this->layout_object->assign('order', $order_data);
$this->layout_object->assign('order_items', $order_items);
$this->layout_object->assign('footer', $order_footer);
// 22.09.2026 -- Zahlungsvorgang aus core/payments (PayPal v2, Mollie ...):
// Status, Referenz, Transaktions-ID, Grund und Verlauf. Null bei alten Zahlarten.
include_once './core/payments/Registry.php';
$this->layout_object->assign('payment_tx', \Payments\Overview::forOrder($this->base_object->db, (int)$id, $this->base_object));
} catch (Exception $e) {
$this->layout_object->assign('error_message', $e->getMessage());
}
+51 -6
View File
@@ -67,6 +67,10 @@ class website_cashdesk {
if (isset($_GET['order'])) {
$this->layout_object->assign('order_number', $_GET['order']);
}
// 27.09.2026: Erfolgsseite ohne Navigation, wie Warenkorb und Kasse der neuen Ansicht
if (!$this->alte_ansicht()) {
$this->layout_object->assign('site_type', 'full_width');
}
// Success-Template anzeigen - auch bei Reload!
return $this->layout_object->_fetch('content_cash_desk/completed.tpl');
}
@@ -157,6 +161,19 @@ class website_cashdesk {
if (count($this->errors) == 0) {
if (isset($_GET['guest_order']) && $_GET['guest_order'] == 1) {
$this->setup_customer_data();
// 27.09.2026 -- Express ohne Anrede: die Anrede ist im Express-Kauf keine Pflicht,
// CustomerAddress::validate() verlangt sie aber. Leere Anrede liess die Adress-Anlage
// still scheitern, die Bestellung wurde OHNE Adresse gespeichert (312153, 312217).
// Leer => Firma (2) bei Firmenname, sonst Herr (0) -- wie customers.honorific es
// ohnehin speichert. Neutrale Anrede bleibt eigenes Thema.
foreach (array('billing', 'shipping') as $blk) {
if (isset($_POST[$blk]) && is_array($_POST[$blk])
&& (!isset($_POST[$blk]['honorific']) || (string)$_POST[$blk]['honorific'] === '')) {
$_POST[$blk]['honorific'] = !empty($_POST[$blk]['company']) ? 2 : 0;
}
}
// check customer already exists by email
$customer_id = 0;//$this->base_object->customer->check_email($_POST['billing']['email']);
@@ -194,6 +211,11 @@ class website_cashdesk {
'customer_id' => $customer_id
);
$res = $customer_address_object->create($billing_address);
if (!$res) {
// bisher stumm -> Bestellung ohne Adresse; jetzt wenigstens sichtbar im PHP-Log
error_log('Checkout: Gast-Rechnungsadresse nicht angelegt (Kunde '.$customer_id.'): '
. json_encode($customer_address_object->get_validation_errors()));
}
$this->billing_address['address'] = $res;
$shipping_address = array (
'honorific' => $_POST['shipping']['honorific'],
@@ -209,6 +231,10 @@ class website_cashdesk {
);
$res = $customer_address_object->create($shipping_address);
if (!$res) {
error_log('Checkout: Gast-Lieferadresse nicht angelegt (Kunde '.$customer_id.'): '
. json_encode($customer_address_object->get_validation_errors()));
}
$this->shipping_address['address'] = $res;
}
@@ -362,12 +388,13 @@ class website_cashdesk {
// "Bestellen" als denselben Vorgang erkennbar (siehe save_order()).
$this->layout_object->assign('checkout_nonce', $this->checkout_nonce_holen());
// 18.09.2026 -- Entwurf der neuen Kassenansicht zum Anschauen: /Kasse/?ansicht=neu
if (isset($_GET['ansicht']) && $_GET['ansicht'] === 'neu') {
$this->layout_object->assign('site_type', 'full_width'); // ohne Navigation, mehr Breite (Tom, 18.09.)
return $this->layout_object->_fetch('content_cash_desk_neu.tpl');
// 27.09.2026 -- die neue Kasse (content_cash_desk_neu.tpl) ist Standard.
// Rueckfall auf das alte Template: /Kasse/?ansicht=alt (siehe alte_ansicht()).
if ($this->alte_ansicht()) {
return $this->layout_object->_fetch('content_cash_desk.tpl');
}
return $this->layout_object->_fetch('content_cash_desk.tpl');
$this->layout_object->assign('site_type', 'full_width'); // ohne Navigation, mehr Breite (Tom, 18.09.)
return $this->layout_object->_fetch('content_cash_desk_neu.tpl');
} else if (isset($_GET['token']) && $_GET['token']) {
return $this->change_order('paypal');
@@ -382,6 +409,18 @@ class website_cashdesk {
// end run
/**
* Rueckfall auf die alte Kassen-/Warenkorbansicht (27.09.2026, Go-Live der neuen Ansicht):
* ?ansicht=alt schaltet fuer die Sitzung auf die alten Templates, ?ansicht=neu hebt das auf.
* Dieselbe Weiche steht in website_shoppingcart.php.
*/
private function alte_ansicht() {
if (isset($_GET['ansicht'])) {
$_SESSION['easyshop']['ansicht_alt'] = ($_GET['ansicht'] === 'alt') ? 1 : 0;
}
return !empty($_SESSION['easyshop']['ansicht_alt']);
}
// ------------------------------------------------------------ Express
/** Start aus dem Warenkorb: /Kasse/?payment_express_start=<modul> */
@@ -694,6 +733,10 @@ class website_cashdesk {
if (isset($_POST['shipping']) && $_POST['shipping']['address'] == -1) {
$this->shipping_address = $_POST['billing'];
if (empty($this->shipping_address['country'])) {
// 22.09.2026: Land nicht gewaehlt -> Standardland fuer die Versandgebiet-Suche, sonst SQL-Fatal
$this->shipping_address['country'] = $this->customer_data->default_address_country_id;
}
$_POST['shipping'] = $_POST['billing'];
} else if (isset($_POST['shipping'])) {
if (!$this->shipping_address['country']) {
@@ -882,8 +925,10 @@ class website_cashdesk {
$this->has_gift_certificate = false;
}
// 27.09.2026: Korbsumme 0 (Artikel ohne Preis) teilte hier durch null -> HTTP 500 in der Kasse
$sum_gross = (float)$this->shoppingcart_data['price_sum_gross'];
foreach ($this->shoppingcart_data['tax_values'] as $key => $value) {
$this->shoppingcart_data['tax_values'][$key]['gift']['percent'] = $percent = round(($value['gross'] / $this->shoppingcart_data['price_sum_gross'] * 100), 2);
$this->shoppingcart_data['tax_values'][$key]['gift']['percent'] = $percent = $sum_gross > 0 ? round(($value['gross'] / $sum_gross * 100), 2) : 0;
$this->shoppingcart_data['tax_values'][$key]['gift']['gross'] = $new_gross = $this->shoppingcart_data['price_sum_gross'] / 100 * $percent;
$this->shoppingcart_data['tax_values'][$key]['gift']['net'] = $new_net = $new_gross / (100 + $key) * 100;
$this->shoppingcart_data['tax_values'][$key]['gift']['vat'] = $new_gross - $new_net;
+8 -1
View File
@@ -250,6 +250,8 @@ class website_customercenter {
}
header('location: ' . $_SERVER["HTTP_REFERER"]);
// 22.09.2026: nach dem Redirect nicht weiterrendern (lief sonst in die Widgets und den SQL-Fatal).
exit();
}
private function edit_base_data($customer_id) {
@@ -332,7 +334,12 @@ class website_customercenter {
$base_url .= ':' . $_SERVER['SERVER_PORT'];
}
return $base_url . '/index.php?menu_id=' . $_GET['menu_id'] . '&parent_id=' . $_GET['parent_id'];
// 22.09.2026: leere Parameter nicht anhaengen (Seite kommt per short_uri /Kundencenter/ ohne parent_id).
$url = $base_url . '/index.php?menu_id=' . (int) ($_GET['menu_id'] ?? 0);
if (!empty($_GET['parent_id'])) {
$url .= '&parent_id=' . (int) $_GET['parent_id'];
}
return $url;
}
private function get_invoice_pdf() {
+28 -7
View File
@@ -59,6 +59,12 @@ class website_shoppingcart {
header('Location: /Kasse/');
exit;
}
// 27.09.2026: "Weiter einkaufen" geht ueber das Formular (Mengen werden mitgespeichert),
// festes Ziel Startseite -- kein offener Redirect.
if ($goto === 'weiter') {
header('Location: /');
exit;
}
if (preg_match('/^express:([a-z0-9_]{1,40})$/', $goto, $m)) {
header('Location: /Kasse/?payment_express_start='.$m[1]);
exit;
@@ -66,8 +72,8 @@ class website_shoppingcart {
}
// Post-Redirect-Get fuer die neue Ansicht: nach Mengen-/Anmerkungsaenderung per GET neu
// laden, sonst fragt ein Reload "Formular erneut senden" und schickt die Mengen doppelt.
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['cart_goto']) && isset($_GET['ansicht']) && $_GET['ansicht'] === 'neu') {
header('Location: /Warenkorb/?ansicht=neu');
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['cart_goto']) && !$this->alte_ansicht()) {
header('Location: /Warenkorb/');
exit;
}
@@ -84,15 +90,30 @@ class website_shoppingcart {
// Angezeigt, wenn der Anbieter Express kann und seine Zahlart aktiv ist.
$this->layout_object->assign('express_providers', $this->express_providers());
// 18.09.2026 -- Entwurf der neuen Warenkorb-Ansicht zum Anschauen: /Warenkorb/?ansicht=neu
if (isset($_GET['ansicht']) && $_GET['ansicht'] === 'neu') {
$this->layout_object->assign('site_type', 'full_width'); // ohne Navigation, mehr Breite (Tom, 18.09.)
return $this->layout_object->_fetch('content_shoppingcart_neu.tpl');
// 27.09.2026 -- die neue Ansicht (content_shoppingcart_neu.tpl) ist Standard.
// Rueckfall auf das alte Template: /Warenkorb/?ansicht=alt (siehe alte_ansicht()).
if ($this->alte_ansicht()) {
return $this->layout_object->_fetch('content_shoppingcart.tpl');
}
return $this->layout_object->_fetch('content_shoppingcart.tpl');
$this->layout_object->assign('site_type', 'full_width'); // ohne Navigation, mehr Breite (Tom, 18.09.)
return $this->layout_object->_fetch('content_shoppingcart_neu.tpl');
} // end show_shoppingcart
/**
* Rueckfall auf die alte Warenkorb-/Kassenansicht (27.09.2026, Go-Live der neuen Ansicht):
* ?ansicht=alt schaltet fuer die Sitzung auf die alten Templates, ?ansicht=neu hebt das auf.
* Haelt ueber die Formular-POSTs hinweg, weil die alten Templates keinen Parameter mitschicken.
* Dieselbe Weiche steht in website_cashdesk.php.
*/
private function alte_ansicht() {
if (isset($_GET['ansicht'])) {
$_SESSION['easyshop']['ansicht_alt'] = ($_GET['ansicht'] === 'alt') ? 1 : 0;
}
return !empty($_SESSION['easyshop']['ansicht_alt']);
}
/** @return array<int,array{module:string,label:string}> */
private function express_providers() {
$out = array();
@@ -483,6 +483,56 @@
<input style="width:300px;" type="text" readonly="readonly" value="{$order->paymorrow_response_result_code}" />
{/if}
</div>
{if $payment_tx}
{* 22.09.2026 -- Zahlungsvorgang aus core/payments (anbieterneutral: PayPal v2, spaeter Mollie) *}
<div class="info_block" id="payment_tx_block">
<div class="edit_title">Zahlungsvorgang {$payment_tx.provider_label|escape}</div>
<label>Status</label>
<input style="width:300px;{if $payment_tx.status == 'paid'}color:green;{elseif $payment_tx.status == 'mismatch'}color:red;font-weight:bold;{elseif $payment_tx.status == 'cancelled'}color:#990000;{/if}" type="text" readonly="readonly" value="{$payment_tx.status_label|escape}" />
{if $payment_tx.detail}
<label>Grund / Quelle</label>
<textarea style="width:300px;" rows="3" readonly="readonly">{$payment_tx.detail|escape}</textarea>
{/if}
<label>Transaktions-ID (Anbieter)</label>
<input style="width:300px;" type="text" readonly="readonly" value="{$payment_tx.capture_id|escape}" onclick="this.select();" />
<label>Vorgangs-Referenz (Anbieter)</label>
<input style="width:300px;" type="text" readonly="readonly" value="{$payment_tx.reference|escape}" onclick="this.select();" />
<label>Sollbetrag / gezahlt</label>
<input style="width:300px;" type="text" readonly="readonly" value="{$payment_tx.expected_amount|escape}{if $payment_tx.paid_amount} / {$payment_tx.paid_amount|escape}{/if}" />
<label>Gestartet</label>
<input style="width:300px;" type="text" readonly="readonly" value="{$payment_tx.created_at|escape}" />
{if $payment_tx.paid_at}
<label>Bezahlt am</label>
<input style="width:300px;" type="text" readonly="readonly" value="{$payment_tx.paid_at|escape}" />
{else}
<label>Letzte &Auml;nderung</label>
<input style="width:300px;" type="text" readonly="readonly" value="{$payment_tx.updated_at|escape}" />
{/if}
{if $payment_tx.history}
<label>Verlauf</label>
<div style="width:300px;max-height:220px;overflow-y:auto;background:#fff;border:1px solid #ddd;font-size:11px;line-height:1.35;">
{foreach $payment_tx.history as $h}
<div style="margin:0;padding:3px 5px;border-bottom:1px solid #eee;">
<span style="color:#666;margin:0;">{$h.zeit|escape}</span><br />
<span style="margin:0;">{$h.aktion|escape}</span>
{if $h.status}<span style="color:#003580;margin:0;"> &middot; {$h.status|escape}</span>{/if}
{if $h.http}<span style="color:#999;margin:0;"> &middot; {$h.http|escape}</span>{/if}
{if $h.detail}<br /><span style="color:#990000;margin:0;">{$h.detail|escape}</span>{/if}
</div>
{/foreach}
</div>
{/if}
</div>
{/if}
<div class="info_block">
<div class="edit_title">Rechnung</div>
@@ -52,8 +52,12 @@ if (strlen($obj->number) >= 8 || !empty($obj->attribute_7)) {
$ersatzteil_ids = array();
// Query 1A: Legacy Items (über attribute_7)
if (!empty($obj->attribute_7)) {
$sql1a = "SELECT id FROM krempl_passendwie WHERE navisionid = ".$this->db->real_escape_string($obj->attribute_7);
// 27.09.2026: navisionid ist bigint -- nur eine echte Ziffernfolge in die SQL. Artikel 832247
// hatte ein einzelnes Leerzeichen in attribute_7 (truthy, aber "navisionid = " => SQL-Fehler,
// Artikelseite HTTP 500, 14x im fatal.log seit 22.09.).
$attr7 = trim((string)$obj->attribute_7);
if ($attr7 !== '' && ctype_digit($attr7)) {
$sql1a = "SELECT id FROM krempl_passendwie WHERE navisionid = ".$attr7;
$result1a = $this->db->query($sql1a);
if ($result1a) {
while ($row = $result1a->fetch_assoc()) {
@@ -229,11 +233,32 @@ $int = $this->createIntWithTableData($html);
// base price calc
if ($obj->attribute_11 && $obj->attribute_15) {
$obj->sample_unit = $obj->attribute_15;
if (strstr($obj->sample_unit, '100')) {
$obj->sample_price = $obj->shop_price_1 / ($obj->attribute_11 * 10);
//
// 25.08.2026 gehaertet. Vorher wurde nur auf "truthy" geprueft -- und zwei Sorten
// gepflegter Werte sind truthy, ohne rechenbar zu sein:
//
// " " (Artikel 831966, beide Felder ein Leerzeichen)
// PHP 8: float / " " => TypeError => Artikelseite mit HTTP 500 tot.
// Unter PHP 7 lief das als Warnung durch -- ein Migrationsueberbleibsel.
//
// "2,5" (24 aktive Artikel, Komma statt Punkt)
// PHP 8 nimmt den fuehrenden Teil und rechnet mit 2 statt 2,5. Kein Fehler,
// aber ein um 25 % zu hoher Grundpreis -- und wegen error_reporting(0) in
// index.php sieht das niemand.
//
// Deshalb: Komma normalisieren, numerisch pruefen, und erst dann rechnen. Ist der
// Teiler unbrauchbar, bleibt sample_price schlicht ungesetzt -- besser kein Grundpreis
// als ein falscher (Preisangabenverordnung).
$grundpreis_teiler = str_replace(',', '.', trim((string)$obj->attribute_11));
$grundpreis_einheit = trim((string)$obj->attribute_15);
if (is_numeric($grundpreis_teiler) && (float)$grundpreis_teiler > 0 && $grundpreis_einheit !== '') {
$grundpreis_teiler = (float)$grundpreis_teiler;
$obj->sample_unit = $obj->attribute_15;
if (strstr($grundpreis_einheit, '100')) {
$obj->sample_price = $obj->shop_price_1 / ($grundpreis_teiler * 10);
} else {
$obj->sample_price = $obj->shop_price_1 / $obj->attribute_11;
$obj->sample_price = $obj->shop_price_1 / $grundpreis_teiler;
}
}
@@ -1,21 +1,59 @@
<!-- content_cash_desk/completed.tpl -->
<div class="full_box">
<div class="title_wrapper">
<div class="title"><h2>Bestellung erfolgreich &uuml;bermittelt</h2></div>
<div class="clear"></div>
{* =====================================================================================
content_cash_desk/completed.tpl (theme2) -- Erfolgsseite "Bestellung uebermittelt".
Seit 27.09.2026 im Stil der neuen Kasse (Schrittleiste, Karte). Ziel des Redirects
/Kasse/?success=1&order=<nr> aus website_cashdesk.php (Vorkasse/Rechnung direkt,
PayPal v2 nach dem Capture). Der Text kommt aus dem Backend
(Konfiguration cash_desk_order_completed, Platzhalter [#order_number]).
===================================================================================== *}
{$ok_nr = (isset($order_number) && $order_number) ? $order_number : ''}
<div class="ok">
<div class="ok-steps">
<span class="ok-step ok-step-done">1&nbsp;Warenkorb</span>
<span class="ok-step ok-step-done">2&nbsp;Kasse</span>
<span class="ok-step ok-step-now">3&nbsp;Fertig</span>
</div>
<div class="box_content">
<div class="completed_box">
{$system_configuration.cash_desk_order_completed|replace:"[#order_number]":$order_number}
<div class="ok-card">
<div class="ok-icon" aria-hidden="true">&#10003;</div>
<h1 class="ok-title">Vielen Dank f&uuml;r Ihre Bestellung</h1>
{if $ok_nr}
<div class="ok-nr">Bestellnummer <strong>{$ok_nr|escape}</strong></div>
{/if}
<div class="ok-text completed_box">
{$system_configuration.cash_desk_order_completed|replace:"[#order_number]":$ok_nr}
</div>
<br><br>
{if $system_configuration.trusted_shop_buyer_protection == 1}
<div class="protection_box">
{include file='content_cash_desk/trusted_shop_buyer_protection.tpl'}
</div>
{/if}
<div class="ok-actions">
<a href="/" class="ok-btn ok-btn-primary">Weiter einkaufen</a>
</div>
</div>
</div>
{literal}
<style>
.ok { font-family: Arial, Helvetica, sans-serif; color: #222; font-size: 14px; line-height: 1.45; padding-bottom: 150px; max-width: 720px; }
.ok *, .ok *:before, .ok *:after { box-sizing: border-box; }
.ok-steps { display: flex; gap: 6px; margin-bottom: 14px; font-size: 12px; }
.ok-step { padding: 4px 10px; border-radius: 12px; background: #eee; color: #777; }
.ok-step-done { background: #e8f3e0; color: #3a6b1f; }
.ok-step-now { background: #FF6700; color: #fff; font-weight: 700; }
.ok-card { border: 1px solid #e3e3e3; border-radius: 8px; padding: 28px 24px; background: #fff; text-align: center; }
.ok-icon { width: 56px; height: 56px; margin: 0 auto 12px; border-radius: 50%; background: #e8f3e0; color: #3a6b1f; font-size: 30px; line-height: 56px; font-weight: 700; }
.ok-title { font-size: 24px; font-weight: 700; margin: 0 0 6px; }
.ok-nr { font-size: 16px; color: #555; margin-bottom: 18px; }
.ok-nr strong { color: #222; }
.ok-text { text-align: left; border-top: 1px solid #eee; padding-top: 16px; }
.ok-text p { margin: 0 0 10px; }
.ok-actions { margin-top: 20px; }
.ok-btn { display: inline-flex; align-items: center; justify-content: center; height: 44px; padding: 0 26px; border-radius: 6px; font-size: 15px; font-weight: 700; text-decoration: none; border: 1px solid transparent; }
.ok-btn-primary { background: #FF6700; color: #fff; }
.ok-btn-primary:hover { background: #e45a00; color: #fff; }
</style>
{/literal}
@@ -0,0 +1,81 @@
{* =====================================================================================
content_cash_desk/payment_result.tpl (theme2) -- Rueckmeldung der Zahlungsanbieter
(core/payments): Weiterleitung zum Anbieter, Abbruch, ausstehend, Fehler.
Seit 27.09.2026 im Stil der neuen Kasse; ueberlagert die Fassung in easyshop_advanced.
Variablen wie dort: call, provider_label, payment_link, message, order_number.
===================================================================================== *}
{$pr_label = (isset($provider_label) && $provider_label) ? $provider_label : 'Zahlung'}
<div class="ok">
<div class="ok-steps">
<span class="ok-step ok-step-done">1&nbsp;Warenkorb</span>
<span class="ok-step ok-step-now">2&nbsp;Kasse</span>
<span class="ok-step">3&nbsp;Fertig</span>
</div>
<div class="ok-card cashdesk_payment_info">
{if $call == 'payment_open'}
<div class="ok-icon ok-icon-wait" aria-hidden="true">&rarr;</div>
<h1 class="ok-title">Weiter zu {$pr_label|escape}</h1>
<p>Vielen Dank f&uuml;r Ihre Bestellung. Sie werden jetzt zur Bezahlung weitergeleitet.<br />
Falls das nicht automatisch geschieht, klicken Sie bitte auf den Knopf.</p>
<meta http-equiv="refresh" content="0;url={$payment_link|escape}">
<div class="ok-actions">
<a href="{$payment_link|escape}" class="ok-btn ok-btn-primary">Weiter zu {$pr_label|escape}</a>
</div>
{elseif $call == 'payment_cancelled'}
<div class="ok-icon ok-icon-warn" aria-hidden="true">&times;</div>
<h1 class="ok-title">Bezahlung abgebrochen</h1>
<p>Ihr Warenkorb ist unver&auml;ndert. Sie k&ouml;nnen die Bestellung jederzeit
mit einer anderen Zahlart abschlie&szlig;en.</p>
<div class="ok-actions">
<a href="/Kasse/" class="ok-btn ok-btn-primary">Zur&uuml;ck zur Kasse</a>
<a href="/Warenkorb/" class="ok-btn ok-btn-ghost">Zum Warenkorb</a>
</div>
{elseif $call == 'payment_pending'}
<div class="ok-icon ok-icon-wait" aria-hidden="true">&hellip;</div>
<h1 class="ok-title">Zahlung wird gepr&uuml;ft</h1>
<p>{$message}</p>
{if $order_number}<div class="ok-nr">Ihre Bestellnummer: <strong>{$order_number|escape}</strong></div>{/if}
<div class="ok-actions">
<a href="/" class="ok-btn ok-btn-ghost">Weiter einkaufen</a>
</div>
{else}
<div class="ok-icon ok-icon-warn" aria-hidden="true">!</div>
<h1 class="ok-title">Das hat leider nicht geklappt</h1>
<p>{$message}</p>
{if $order_number}<div class="ok-nr">Bestellnummer: <strong>{$order_number|escape}</strong></div>{/if}
<div class="ok-actions">
<a href="/Kasse/" class="ok-btn ok-btn-primary">Zur&uuml;ck zur Kasse</a>
<a href="/Warenkorb/" class="ok-btn ok-btn-ghost">Zum Warenkorb</a>
</div>
{/if}
</div>
</div>
{literal}
<style>
.ok { font-family: Arial, Helvetica, sans-serif; color: #222; font-size: 14px; line-height: 1.45; padding-bottom: 150px; max-width: 720px; }
.ok *, .ok *:before, .ok *:after { box-sizing: border-box; }
.ok-steps { display: flex; gap: 6px; margin-bottom: 14px; font-size: 12px; }
.ok-step { padding: 4px 10px; border-radius: 12px; background: #eee; color: #777; }
.ok-step-done { background: #e8f3e0; color: #3a6b1f; }
.ok-step-now { background: #FF6700; color: #fff; font-weight: 700; }
.ok-card { border: 1px solid #e3e3e3; border-radius: 8px; padding: 28px 24px; background: #fff; text-align: center; }
.ok-card p { margin: 0 0 10px; }
.ok-icon { width: 56px; height: 56px; margin: 0 auto 12px; border-radius: 50%; background: #e8f3e0; color: #3a6b1f; font-size: 30px; line-height: 56px; font-weight: 700; }
.ok-icon-wait { background: #fff3e8; color: #a44a00; }
.ok-icon-warn { background: #fdecec; color: #8a1c1c; }
.ok-title { font-size: 24px; font-weight: 700; margin: 0 0 10px; }
.ok-nr { font-size: 16px; color: #555; margin: 8px 0 14px; }
.ok-nr strong { color: #222; }
.ok-actions { margin-top: 20px; display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.ok-btn { display: inline-flex; align-items: center; justify-content: center; height: 44px; padding: 0 26px; border-radius: 6px; font-size: 15px; font-weight: 700; text-decoration: none; border: 1px solid transparent; }
.ok-btn-primary { background: #FF6700; color: #fff; }
.ok-btn-primary:hover { background: #e45a00; color: #fff; }
.ok-btn-ghost { background: #fff; color: #555; border-color: #cfcfcf; font-weight: 400; }
.ok-btn-ghost:hover { border-color: #999; color: #222; }
</style>
{/literal}
@@ -1,7 +1,6 @@
{* =====================================================================================
Kasse NEU (theme2) -- Entwurf 18.09.2026, liegt neben content_cash_desk.tpl.
Ansehen: /Kasse/?guest_order=1&ansicht=neu (Gast) bzw. /Kasse/?ansicht=neu (eingeloggt).
Weiche in website_cashdesk.php.
Kasse NEU (theme2) -- seit 27.09.2026 die Standardansicht von /Kasse/ (Gast: ?guest_order=1).
Rueckfall auf content_cash_desk.tpl: /Kasse/?ansicht=alt (Weiche in website_cashdesk.php).
Nur Darstellung ist neu. Alle Feldnamen, IDs und Klassen, an denen Modul und
content_cash_desk.js haengen, sind unveraendert: order_form, cashdesk_action,
@@ -19,14 +18,14 @@
{$ck_guest = (isset($guest_order) && $guest_order == 1)}
{$ck_express = (isset($express_active) && $express_active)}
{$ck_merchant = (isset($customer) && $customer->group_id == 101)}
{if $ck_guest}{$ck_url = "/Kasse/?guest_order=1&ansicht=neu"}{else}{$ck_url = "/Kasse/?ansicht=neu"}{/if}
{if $ck_guest}{$ck_url = "/Kasse/?guest_order=1"}{else}{$ck_url = "/Kasse/"}{/if}
<div class="ck">
<div class="ck-steps">
<a href="/Warenkorb/?ansicht=neu" class="ck-step ck-step-done">1&nbsp;Warenkorb</a>
<a href="/Warenkorb/" class="ck-step ck-step-done">1&nbsp;Warenkorb</a>
<span class="ck-step ck-step-now">2&nbsp;Kasse</span>
<span class="ck-step">3&nbsp;Fertig</span>
<a href="/Warenkorb/?ansicht=neu" class="ck-back">&larr; Zur&uuml;ck zum Warenkorb</a>
<a href="/Warenkorb/" class="ck-back">&larr; Zur&uuml;ck zum Warenkorb</a>
</div>
<h1 class="ck-title">Kasse{if $ck_guest} <span class="ck-muted">als Gast</span>{/if}</h1>
@@ -119,7 +118,7 @@
<input type="text" id="billing_surname" class="formatted obligatory" name="billing[surname]" autocomplete="family-name" value="{if isset($personal_data.billing.surname)}{$personal_data.billing.surname|escape}{/if}" data-req="person" data-label="Nachname" />
</div>
<div class="ck-f ck-f-1">
<label for="billing_company" class="address_company">Firma <span class="ck-muted ck-company-hint">(bei Anrede „Firma" Pflicht)</span></label>
<label for="billing_company" class="address_company">Firma <span class="ck-muted ck-company-hint">(bei Anrede „Firma“ Pflicht)</span></label>
<input type="text" id="billing_company" class="formatted" name="billing[company]" autocomplete="organization" value="{if isset($personal_data.billing.company)}{$personal_data.billing.company|escape}{/if}" data-req="company" data-label="Firma" />
</div>
<div class="ck-f ck-f-2w">
@@ -198,7 +197,7 @@
<input type="text" id="shipping_surname" class="formatted obligatory" name="shipping[surname]" value="{if isset($personal_data.shipping.surname)}{$personal_data.shipping.surname|escape}{/if}" data-req="person" data-label="Nachname (Lieferadresse)" />
</div>
<div class="ck-f ck-f-1">
<label for="shipping_company" class="address_company">Firma <span class="ck-muted ck-company-hint">(bei Anrede „Firma" Pflicht)</span></label>
<label for="shipping_company" class="address_company">Firma <span class="ck-muted ck-company-hint">(bei Anrede „Firma“ Pflicht)</span></label>
<input type="text" id="shipping_company" class="formatted" name="shipping[company]" value="{if isset($personal_data.shipping.company)}{$personal_data.shipping.company|escape}{/if}" data-req="company" data-label="Firma (Lieferadresse)" />
</div>
<div class="ck-f ck-f-2w">
@@ -424,7 +423,9 @@
.ck-grid { display: flex; gap: 20px; align-items: flex-start; }
.ck-main { flex: 1 1 auto; min-width: 0; }
.ck-side { flex: 0 0 280px; position: sticky; top: 16px; }
/* top = Hoehe des fixierten Theme-Kopfes (#header_sticky.fixed, 152px) + Luft. Der Kopf ist unten
ausgeblendet, faengt dort aber Klicks ab -- darunter darf nichts Klickbares liegen (Tom, 18./27.09.) */
.ck-side { flex: 0 0 280px; position: sticky; top: 168px; }
.ck-sec { border: 1px solid #e3e3e3; border-radius: 8px; padding: 16px 18px; margin-bottom: 14px; background: #fff; }
.ck-h { display: flex; align-items: center; gap: 10px; font-size: 17px; font-weight: 700; margin: 0 0 12px; }
@@ -546,7 +547,10 @@ $(document).ready(function() {
if (fehler.length) {
$box.html('<strong>Bitte noch ausfüllen:</strong> ' + fehler.join(', ')).removeAttr('hidden').show();
var $first = $('.ck .ck-invalid').first();
if ($first.length) { $('html, body').animate({ scrollTop: $first.offset().top - 90 }, 250); $first.find('input,select').add($first.filter('input,select')).first().focus(); // kein .addBack() in jQuery 1.6 }
if ($first.length) {
$('html, body').animate({ scrollTop: $first.offset().top - 90 }, 250);
$first.find('input,select').add($first.filter('input,select')).first().focus(); // kein .addBack() in jQuery 1.6
}
return false;
}
$box.attr('hidden', 'hidden').hide();
@@ -1,6 +1,6 @@
{* =====================================================================================
Warenkorb NEU (theme2) -- Entwurf 18.09.2026, liegt neben content_shoppingcart.tpl.
Aufruf zum Anschauen: /Warenkorb/?ansicht=neu (Weiche in website_shoppingcart.php).
Warenkorb NEU (theme2) -- seit 27.09.2026 die Standardansicht von /Warenkorb/.
Rueckfall auf content_shoppingcart.tpl: /Warenkorb/?ansicht=alt (Weiche in website_shoppingcart.php).
Kein Tabellen-Markup mehr: jede Position ist eine Zeile (Bild, Name + Art.-Nr., Menge
mit Stepper, Preis, Entfernen), rechts ein Kasten mit Zwischensumme, MwSt, Gesamt und
@@ -22,7 +22,7 @@
<h1 class="wk-title">Warenkorb{if $shoppingcart.items} <span class="wk-count">{$shoppingcart.items|@count} {if $shoppingcart.items|@count == 1}Artikel{else}Artikel{/if}</span>{/if}</h1>
{if $shoppingcart.items}
<form method="post" action="/Warenkorb/?ansicht=neu" id="cart_form" class="wk-grid">
<form method="post" action="/Warenkorb/" id="cart_form" class="wk-grid">
<input type="hidden" name="cart_goto" id="cart_goto" value="" />
<div class="wk-list">
@@ -130,7 +130,9 @@
.wk { container-type: inline-size; }
.wk-grid { display: flex; gap: 20px; align-items: flex-start; }
.wk-list { flex: 1 1 auto; min-width: 0; }
.wk-side { flex: 0 0 260px; position: sticky; top: 16px; }
/* top = Hoehe des fixierten Theme-Kopfes (#header_sticky.fixed, 152px) + Luft. Der Kopf ist unten
ausgeblendet, faengt dort aber Klicks ab -- darunter darf nichts Klickbares liegen (Tom, 18./27.09.) */
.wk-side { flex: 0 0 260px; position: sticky; top: 168px; }
.wk-box { background: #fafafa; border: 1px solid #e3e3e3; border-radius: 8px; padding: 16px; }
/* Gewaehrleistungs-Label ueber dem Kasten: volle Breite der Spalte, Hoehe folgt dem Seitenverhaeltnis
(Tom, 18.09.), Klick oeffnet das Label gross in neuem Tab */
@@ -187,8 +189,11 @@
.wk-btn-ghost:hover { border-color: #999; }
.wk-trust { text-align: center; font-size: 12px; margin-top: 12px; }
.wk-empty { padding: 30px 0; }
.wk-empty p { font-size: 16px; margin-bottom: 14px; }
/* Rechtsbuendig: links reicht der fixierte Theme-Kopf (Zahlarten-Fenster) tiefer und schluckt Klicks,
rechts ist der Bereich frei (Tom, 27.09.). Rand rechts 0 = Kante der Inhaltsspalte. */
.wk-empty { padding: 30px 0; text-align: right; }
.wk-empty p { font-size: 16px; margin: 0 0 14px; }
.wk-empty .wk-btn-inline { margin-right: 0; }
.wk-quick { margin: 0 0 22px; padding-bottom: 16px; border-bottom: 1px solid #e6e6e6; }
.wk-quick-title { font-weight: 700; margin-bottom: 6px; }
@@ -230,21 +235,47 @@ $(document).ready(function() {
}
// Scrollposition ueber den Submit retten: ein POST rendert die Seite neu und der
// Browser faengt oben an. Vor dem Absenden merken, nach dem Laden wiederherstellen.
var gesendet = false;
function absenden(ziel) {
if (!mengenOk()) { return; }
if (gesendet) { return; } // Formular ist schon unterwegs, kein zweiter Submit
if (!mengenOk()) {
if (ziel === 'weiter') { window.location.href = '/'; } // Mengenfehler haelt niemanden im Laden fest
return;
}
if (ziel === '') { try { sessionStorage.setItem('wk_scroll', String(window.scrollY || window.pageYOffset || 0)); } catch (e) {} }
gesendet = true;
$('#cart_goto').val(ziel);
$('#cart_form').submit();
}
try {
var y = sessionStorage.getItem('wk_scroll');
if (y !== null) { sessionStorage.removeItem('wk_scroll'); window.scrollTo(0, parseInt(y) || 0); }
if (y !== null) {
sessionStorage.removeItem('wk_scroll');
// Nur wiederherstellen, solange der Korb noch Positionen hat. Wird die letzte entfernt, ist die
// Seite kurz: die alte Scrollposition klemmt dann am Seitenende und schiebt den Knopf
// "Weiter einkaufen" unter den fixierten Theme-Kopf, dessen unterer Rand durchsichtig ist,
// aber Klicks schluckt (Tom, 27.09.).
if ($('#cart_form').length) { window.scrollTo(0, parseInt(y) || 0); } else { window.scrollTo(0, 0); }
}
} catch (e) {}
// Getippte Menge: beim Verlassen des Feldes speichern (kein extra Knopf mehr)
$('.input_quantity').change(function() { absenden(''); });
$('#cashdesk_button').click(function(e) { e.preventDefault(); absenden('kasse'); });
$('.express_start').click(function(e) { e.preventDefault(); absenden('express:' + $(this).data('module')); });
$('.item_delete').click(function(e) { e.preventDefault(); $('.input_quantity', $(this).closest('.wk-row')).val(0); absenden(''); });
// Getippte Menge: beim Verlassen des Feldes speichern -- aber erst nach einer kurzen Wartezeit.
// Sonst gewinnt das Auto-Speichern gegen den Klick, der das Feld verlassen hat ("Weiter einkaufen",
// "Zur Kasse"): das Formular geht ab, der Klick verpufft, die Seite laedt sich nur neu (Tom, 27.09.).
var autoTimer = null;
function autoSpeichern() {
if (autoTimer) { clearTimeout(autoTimer); }
autoTimer = setTimeout(function() { autoTimer = null; absenden(''); }, 300);
}
// Eine Aktion bricht ein wartendes Auto-Speichern ab; ihr eigener Submit traegt die Mengen mit.
function aktion(ziel) {
if (autoTimer) { clearTimeout(autoTimer); autoTimer = null; }
absenden(ziel);
}
$('.input_quantity').change(function() { autoSpeichern(); });
$('#cashdesk_button').click(function(e) { e.preventDefault(); aktion('kasse'); });
$('.express_start').click(function(e) { e.preventDefault(); aktion('express:' + $(this).data('module')); });
$('.wk-continue a').click(function(e) { e.preventDefault(); aktion('weiter'); });
$('.item_delete').click(function(e) { e.preventDefault(); $('.input_quantity', $(this).closest('.wk-row')).val(0); aktion(''); });
// Stepper: Menge in Staffelschritten aendern und sofort speichern
$('.wk-step').click(function(e) {
@@ -258,10 +289,10 @@ $(document).ready(function() {
q = q + step * buy;
if (q < min) { q = (step < 0) ? 0 : min; } // unter Mindestmenge = entfernen
input.val(q);
absenden('');
aktion('');
});
// Enter im Mengenfeld = aktualisieren, nicht das Formular blind abschicken
$('.input_quantity').keydown(function(e) { if (e.keyCode == 13) { e.preventDefault(); absenden(''); } });
$('.input_quantity').keydown(function(e) { if (e.keyCode == 13) { e.preventDefault(); aktion(''); } });
});
</script>
{/literal}