Compare commits
10
Commits
fe868641d2
...
d5a3727884
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5a3727884 | ||
|
|
5844b24db4 | ||
|
|
d0b79a2820 | ||
|
|
4ef81ceec3 | ||
|
|
4c1ecb82a7 | ||
|
|
30c81cae9c | ||
|
|
def156e1c2 | ||
|
|
6cea66126a | ||
|
|
40b4774f0d | ||
|
|
85bc1d8c9a |
@@ -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";
|
||||
|
||||
@@ -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, '.', '')
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 Ä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;"> · {$h.status|escape}</span>{/if}
|
||||
{if $h.http}<span style="color:#999;margin:0;"> · {$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 ü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 Warenkorb</span>
|
||||
<span class="ok-step ok-step-done">2 Kasse</span>
|
||||
<span class="ok-step ok-step-now">3 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">✓</div>
|
||||
<h1 class="ok-title">Vielen Dank fü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 Warenkorb</span>
|
||||
<span class="ok-step ok-step-now">2 Kasse</span>
|
||||
<span class="ok-step">3 Fertig</span>
|
||||
</div>
|
||||
|
||||
<div class="ok-card cashdesk_payment_info">
|
||||
{if $call == 'payment_open'}
|
||||
<div class="ok-icon ok-icon-wait" aria-hidden="true">→</div>
|
||||
<h1 class="ok-title">Weiter zu {$pr_label|escape}</h1>
|
||||
<p>Vielen Dank fü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">×</div>
|
||||
<h1 class="ok-title">Bezahlung abgebrochen</h1>
|
||||
<p>Ihr Warenkorb ist unverändert. Sie können die Bestellung jederzeit
|
||||
mit einer anderen Zahlart abschließen.</p>
|
||||
<div class="ok-actions">
|
||||
<a href="/Kasse/" class="ok-btn ok-btn-primary">Zurü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">…</div>
|
||||
<h1 class="ok-title">Zahlung wird geprü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ü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 Warenkorb</a>
|
||||
<a href="/Warenkorb/" class="ck-step ck-step-done">1 Warenkorb</a>
|
||||
<span class="ck-step ck-step-now">2 Kasse</span>
|
||||
<span class="ck-step">3 Fertig</span>
|
||||
<a href="/Warenkorb/?ansicht=neu" class="ck-back">← Zurück zum Warenkorb</a>
|
||||
<a href="/Warenkorb/" class="ck-back">← Zurü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}
|
||||
|
||||
Reference in New Issue
Block a user