8 private function tpl() {
9 return dbx()->get_system_obj(
'dbxTPL');
13 return dbx()->get_include_obj(
'dbxShopRepository',
'dbxShop');
17 return dbx()->get_include_obj(
'dbxShopPayPal',
'dbxShop');
21 return dbx()->get_include_obj(
'dbxShopAmazonPay',
'dbxShop');
24 private function h($value):
string {
25 return htmlspecialchars((
string) $value, ENT_QUOTES,
'UTF-8');
28 private function money($value):
string {
29 return number_format((
float) $value, 2,
',',
'.') .
' EUR';
32 private function shopConfig(): array {
33 $cfg =
dbx()->get_config(
'dbxShop');
34 return is_array(
$cfg) ?
$cfg : array();
37 private function settingsBool(array
$cfg,
string $key,
bool $default =
false):
bool {
38 if (!array_key_exists($key,
$cfg)) {
42 if (is_bool($value))
return $value;
43 return in_array(strtolower(trim((
string)$value)), array(
'1',
'true',
'yes',
'on'),
true);
46 private function shopStyle():
string {
47 $file = dirname(__DIR__) .
'/design/css/shop.css';
48 if (!is_file($file)) {
51 return '<style>' . file_get_contents($file) .
'</style>';
54 private function demoShopNoticeHtml(
string $id =
'',
string $extraClass =
''):
string {
55 $idAttribute = $id !==
'' ?
' id="' . $this->h($id) .
'"' :
'';
56 $classAttribute = $extraClass !==
'' ?
' ' . $this->h($extraClass) :
'';
57 return '<div' . $idAttribute .
' class="alert alert-danger dbx-shop-demo-alert' . $classAttribute .
'" role="alert">'
58 .
'<strong><i class="bi bi-exclamation-octagon-fill"></i> Demo-Shop – kein tatsächlicher Kauf</strong><br>'
59 .
'Dieser Shop dient ausschließlich Demonstrations- und Testzwecken. Der vollständige Bestellablauf kann mit Testdaten durchlaufen werden; dabei wird lediglich ein technischer Testvorgang verarbeitet. Es erfolgen kein tatsächlicher Kauf, keine Zahlung und keine Lieferung. Ein Kaufvertrag kommt nicht zustande.'
63 private function page(
string $title,
string $subtitle,
string $body,
string $active =
'catalog'):
string {
64 return $this->tpl()->get_tpl(
'dbxShop|start', array(
65 'shop_style' => $this->shopStyle(),
66 'title' => $this->h($title),
67 'subtitle' => $this->h($subtitle),
69 'active_catalog' => $active ===
'catalog' ?
'active' :
'',
70 'active_cart' => $active ===
'cart' ?
'active' :
'',
71 'active_checkout' => $active ===
'checkout' ?
'active' :
'',
72 'active_orders' => $active ===
'orders' ?
'active' :
'',
73 'active_legal' => $active ===
'legal' ?
'active' :
'',
74 'active_withdrawal' => $active ===
'withdrawal' ?
'active' :
'',
78 private function contentDb() {
79 $db =
dbx()->get_system_obj(
'dbxDB');
80 if (!is_object(
$db) || !
$db->connect_db_server(
'dbx|dbxContent.db3')) {
86 private function findContentFolder(
$db,
string $name,
int $parentId):
int {
88 $parentId = (int) $parentId;
92 $where =
"name = '" . str_replace(
"'",
"''", $name) .
"' AND parent_id = " . $parentId;
93 $rows =
$db->select(\
dbx\
dbxContent\dbxContentLng::ddFolder(), $where,
'id',
'id',
'ASC',
'', 1, 0, 0);
94 if (!is_array($rows) || !isset($rows[0][
'id'])) {
97 return (
int) $rows[0][
'id'];
100 private function nextFolderSorter(
$db,
int $parentId):
string {
101 $parentId = (int) $parentId;
102 $rows =
$db->select(\
dbx\
dbxContent\dbxContentLng::ddFolder(),
'parent_id = ' . $parentId,
'sorter',
'sorter,id',
'DESC',
'', 1, 0, 0);
104 if (is_array($rows) && isset($rows[0]) && is_array($rows[0])) {
105 $max = (int) ($rows[0][
'sorter'] ?? 0);
107 return sprintf(
'%04d', $max + 10);
110 private function nextContentSorter(
$db,
int $folderId):
string {
111 $folderId = (int) $folderId;
112 $rows =
$db->select(\
dbx\
dbxContent\dbxContentLng::ddContent(),
'folder = ' . $folderId,
'sorter',
'sorter,id',
'DESC',
'', 1, 0, 0);
114 if (is_array($rows) && isset($rows[0]) && is_array($rows[0])) {
115 $max = (int) ($rows[0][
'sorter'] ?? 0);
117 return sprintf(
'%04d', $max + 10);
120 private function ensureShopContentFolder(
$db):
int {
121 $folderId = $this->findContentFolder(
$db,
'shop', 0);
129 'sorter' => $this->nextFolderSorter(
$db, 0),
131 'template' =>
'c-body1-footer',
132 'hero_template' =>
'image-hero',
133 'hero_image_id' =>
'parent',
134 'hero_margin_top' =>
'parent',
135 'hero_height' =>
'parent',
136 'hero_variant' =>
'parent',
137 'hero_sticky' =>
'parent',
138 'hero_scroll_layer' =>
'parent',
140 $ok = (int)
$db->insert(\
dbx\
dbxContent\dbxContentLng::ddFolder(), $data, 0, 1, 0, 0);
144 $folderId = (int)
$db->get_insert_id();
145 if ($folderId <= 0) {
155 private function shopLegalPageData(
$db,
int $folderId,
string $title,
string $permalink,
string $content): array {
158 'folder' => $folderId,
159 'title' => substr($title, 0, 254),
160 'permalink' => substr($permalink, 0, 254),
164 'sorter' => $this->nextContentSorter(
$db, $folderId),
165 'template' =>
'c-body1-footer',
166 'hero_template' =>
'parent',
167 'hero_image_id' =>
'parent',
168 'hero_margin_top' =>
'parent',
169 'hero_height' =>
'parent',
170 'hero_variant' =>
'parent',
171 'hero_sticky' =>
'parent',
172 'hero_scroll_layer' =>
'parent',
173 'gallery_template' =>
'image-gallery',
174 'gallery_visible_count' =>
'3',
175 'gallery_image_size' =>
'original',
176 'gallery_lightbox_width' =>
'100vw',
177 'gallery_overflow' =>
'grid',
178 'gallery_click_behavior' =>
'lightbox',
179 'content' => $content,
183 private function syncContentPermalink(
int $cid,
string $permalink):
void {
184 if ($cid <= 0 || trim($permalink) ===
'') {
190 private function ensureShopLegalPage(
$db,
int $folderId,
string $title,
string $permalink,
string $content):
int {
192 $existing =
$db->select1($dd, array(
'permalink' => $permalink),
'id,content', 0);
193 if (is_array($existing) && (
int) ($existing[
'id'] ?? 0) > 0) {
194 $id = (int) $existing[
'id'];
195 $storedContent = trim((
string) ($existing[
'content'] ??
''));
196 if ($storedContent ===
'') {
197 $data = $this->shopLegalPageData(
$db, $folderId, $title, $permalink, $content);
198 unset($data[
'sorter']);
199 $db->update($dd, $data, $id, 0, 1, 1, 0);
202 $db->update($dd, array(
'template' =>
'c-body1-footer',
'group_read' =>
'*',
'activ' => 1), $id, 0, 1, 1, 0);
204 $this->syncContentPermalink($id, $permalink);
208 $data = $this->shopLegalPageData(
$db, $folderId, $title, $permalink, $content);
209 $ok = (int)
$db->insert($dd, $data, 0, 1, 0, 0);
213 $id = (int)
$db->get_insert_id();
219 $this->syncContentPermalink($id, $permalink);
225 $db = $this->contentDb();
226 if (!is_object(
$db)) {
229 $folderId = $this->ensureShopContentFolder(
$db);
230 if ($folderId <= 0) {
235 'legal' => $this->ensureShopLegalPage(
$db, $folderId,
'Rechtstexte',
'shop/rechtstexte', $this->defaultLegalContent()),
236 'withdrawal' => $this->ensureShopLegalPage(
$db, $folderId,
'Widerruf',
'shop/widerruf', $this->defaultWithdrawalContent()),
240 private function renderCmsShopPage(
string $key,
string $title,
string $subtitle,
string $active): string {
241 $pages = $this->ensureShopLegalPages();
242 $cid = (int) ($pages[$key] ?? 0);
244 return $this->page($title, $subtitle, $this->placeholder($title,
'Die CMS-Seite konnte nicht angelegt oder geladen werden.'), $active);
247 $renderer = dbx()->get_include_obj(
'dbxContentRenderer',
'dbxContent');
248 $body = is_object($renderer) ? (string) $renderer->renderStatic($cid, array(
'template' =>
'c-body1-footer')) :
'';
249 if (trim($body) ===
'') {
250 $body = $this->placeholder($title,
'Die CMS-Seite ist leer.');
252 return $this->page($title, $subtitle,
'<div class="dbx-shop-cms-page">' . $body .
'</div>', $active);
255 private function withdrawalFormHtml(): string {
256 $form =
dbx()->get_system_obj(
'dbxForm');
257 $form->init(
'shop-withdrawal-form',
'shop-withdrawal-form');
258 $form->_fd =
'dbxShop|withdrawal';
259 $form->_action =
'?dbx_modul=dbxShop&dbx_run1=withdrawal';
260 $form->_data = array(
261 'order_no' => (
string)($_POST[
'order_no'] ??
''),
262 'customer_name' => (
string)($_POST[
'customer_name'] ??
''),
263 'customer_email' => (
string)($_POST[
'customer_email'] ??
''),
264 'customer_address' => (
string)($_POST[
'customer_address'] ??
''),
265 'reason' => (
string)($_POST[
'reason'] ??
''),
267 $form->_msg_info =
'Bitte Bestellnummer und Kontaktdaten eintragen, damit der Widerruf zugeordnet werden kann.';
268 $form->_msg_success =
'Widerruf wurde gespeichert.';
269 $form->_msg_error =
'Bitte Eingaben pruefen.';
271 $form->add_rep(
'withdrawal_error',
'');
273 $submitted = (
$_SERVER[
'REQUEST_METHOD'] ??
'') ===
'POST' && isset($_POST[
'shop_withdrawal_submit']);
276 $name = trim((
string)($_POST[
'customer_name'] ??
''));
277 $email = trim((
string)($_POST[
'customer_email'] ??
''));
278 $address = trim((
string)($_POST[
'customer_address'] ??
''));
279 if ($name ===
'' || !filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($address) < 8) {
280 $form->add_rep(
'withdrawal_error',
'<div class="alert alert-warning mb-0">Bitte Name, gueltige E-Mail und Adresse eintragen.</div>');
282 $row = $this->repo()->saveWithdrawal($_POST);
283 if (is_array($row)) {
284 $this->sendWithdrawalMails($row);
285 $form->_data = array(
'order_no' =>
'',
'customer_name' =>
'',
'customer_email' =>
'',
'customer_address' =>
'',
'reason' =>
'');
286 $form->add_rep(
'withdrawal_error',
'<div class="alert alert-success mb-0">Ihr Widerruf wurde gespeichert. Wir pruefen die Zuordnung zur Bestellung.</div>');
288 $form->add_rep(
'withdrawal_error',
'<div class="alert alert-danger mb-0">Der Widerruf konnte nicht gespeichert werden.</div>');
296 private function defaultLegalContent(): string {
298<div class=
"dbx-shop-legal-text">
300 <p class=
"dbx-shop-legal-note"><strong>Hinweis:</strong> Dies ist ein Mustertext fuer einen deutschen Online-Shop. Alle Platzhalter in eckigen Klammern muessen vor der Veroeffentlichung durch echte Betreiber-, Register-, Steuer-, Zahlungs- und Versanddaten ersetzt und rechtlich geprueft werden.</p>
302 <h2>Anbieterkennzeichnung</h2>
303 <p><strong>[Name/Firma des Shop-Betreibers]</strong><br>
304 [Strasse und Hausnummer]<br>
307 <p>Vertreten durch: [vertretungsberechtigte Person]<br>
308 E-Mail: <a href=
"mailto:[E-Mail-Adresse]">[E-Mail-Adresse]</a><br>
309 Telefon: [Telefonnummer]</p>
310 <p>Registereintrag: [Registergericht und Registernummer, falls vorhanden]<br>
311 Umsatzsteuer-ID: [USt-IdNr., falls vorhanden]<br>
312 Wirtschafts-ID: [W-IdNr., falls vorhanden]</p>
313 <p>Zustaendige Aufsichtsbehoerde: [nur eintragen, wenn fuer die Taetigkeit erforderlich]</p>
315 <h2>Geltungsbereich</h2>
316 <p>Diese Rechtstexte gelten fuer Bestellungen ueber diesen Shop. Abweichende Bedingungen von Kunden gelten nur, wenn der Shop-Betreiber ihnen ausdruecklich zustimmt.</p>
318 <h2>Vertragspartner und Vertragsschluss</h2>
319 <p>Der Kaufvertrag kommt zustande mit <strong>[Name/Firma des Shop-Betreibers]</strong>. Die Darstellung der Produkte im Shop ist kein rechtlich bindendes Angebot, sondern eine Aufforderung zur Bestellung. Der Kunde gibt ein verbindliches Angebot ab, wenn er den Bestellprozess abschliesst. Die Annahme erfolgt durch Bestellbestaetigung, Zahlungsaufforderung, Versandbestaetigung oder Lieferung der Ware.</p>
321 <h2>Preise, Zahlung und Rechnung</h2>
322 <p>Alle Preise verstehen sich in Euro und enthalten die gesetzliche Umsatzsteuer, sofern diese im Shop ausgewiesen wird. Zusaetzliche Versandkosten werden vor Abgabe der Bestellung angezeigt. Verfuegbare Zahlungsarten sind: [Zahlungsarten eintragen, z. B. PayPal, Ueberweisung, Rechnung]. Die Rechnung wird elektronisch oder in Textform bereitgestellt.</p>
324 <h2>Lieferung und Versand</h2>
325 <p>Die Lieferung erfolgt an die vom Kunden angegebene Lieferadresse. Liefergebiete, Versandarten, Versandkosten und Lieferzeiten ergeben sich aus den Angaben im Bestellprozess. Bei digitalen Produkten erfolgt die Bereitstellung per Download, E-Mail, Kundenkonto oder Freischaltung.</p>
327 <h2>Eigentumsvorbehalt</h2>
328 <p>Gelieferte Waren bleiben bis zur vollstaendigen Bezahlung Eigentum des Shop-Betreibers.</p>
330 <h2>Maengelhaftung</h2>
331 <p>Es gilt das gesetzliche Maengelhaftungsrecht. Kunden werden gebeten, offensichtliche Transportschaeden moeglichst schnell beim Zusteller und beim Shop-Betreiber zu melden. Die gesetzlichen Rechte des Kunden bleiben davon unberuehrt.</p>
333 <h2>Digitale Inhalte und Dienstleistungen</h2>
334 <p>Bei digitalen Inhalten, Downloads, Software, Online-Zugaengen oder Dienstleistungen koennen besondere Hinweise zur Vertragsausfuehrung, Kompatibilitaet, Laufzeit, Kuendigung und zum Widerrufsrecht erforderlich sein. Diese Angaben muessen beim jeweiligen Produkt und im Bestellprozess klar dargestellt werden.</p>
336 <h2>Streitbeilegung</h2>
337 <p>Die Europaeische Kommission stellt eine Plattform zur Online-Streitbeilegung bereit: <a href=
"https://ec.europa.eu/consumers/odr/" target=
"_blank" rel=
"noopener">https:
340 <p>Informationen zur Verarbeitung personenbezogener Daten, zu Kontaktformularen, Zahlungsdiensten, Versanddienstleistern, Cookies, Logfiles und Rechten betroffener Personen stehen in der Datenschutzerklaerung: <a href=
"?dbx_modul=dbxContent&dbx_permalink=datenschutz">Datenschutzerklaerung</a>.</p>
345 private function defaultWithdrawalContent(): string {
347<div class=
"dbx-shop-legal-text">
349 <p class=
"dbx-shop-legal-note"><strong>Hinweis:</strong> Dies ist ein Muster fuer Verbraucherbestellungen nach deutschem Recht. Platzhalter muessen ersetzt und die Ausnahmen fuer konkrete Produkte, digitale Inhalte und Dienstleistungen geprueft werden.</p>
351 <h2>Widerrufsrecht</h2>
352 <p>Verbraucher haben grundsaetzlich das Recht, einen Vertrag binnen vierzehn Tagen ohne Angabe von Gruenden zu widerrufen. Die Widerrufsfrist betraegt vierzehn Tage ab dem Tag, an dem der Kunde oder ein benannter Dritter die Ware erhalten hat. Bei digitalen Inhalten oder Dienstleistungen kann der Fristbeginn und das Erloeschen des Widerrufsrechts abweichen.</p>
354 <h2>Ausuebung des Widerrufs</h2>
355 <p>Um das Widerrufsrecht auszuueben, muss der Kunde den Shop-Betreiber mit einer eindeutigen Erklaerung ueber den Entschluss informieren, den Vertrag zu widerrufen. Die Erklaerung kann per Brief, E-Mail oder ueber ein im Shop bereitgestelltes Formular erfolgen.</p>
356 <p><strong>Widerruf an:</strong><br>
357 [Name/Firma des Shop-Betreibers]<br>
358 [Strasse und Hausnummer]<br>
360 E-Mail: <a href=
"mailto:[E-Mail-Adresse]">[E-Mail-Adresse]</a></p>
362 <h2>Folgen des Widerrufs</h2>
363 <p>Wenn der Kunde den Vertrag widerruft, werden alle Zahlungen einschliesslich der Standard-Lieferkosten unverzueglich und spaetestens binnen vierzehn Tagen ab Eingang der Widerrufserklaerung zurueckgezahlt. Fuer die Rueckzahlung wird dasselbe Zahlungsmittel verwendet, das bei der urspruenglichen Transaktion eingesetzt wurde, sofern nichts anderes vereinbart wurde.</p>
364 <p>Bei Waren kann die Rueckzahlung verweigert werden, bis die Ware wieder eingegangen ist oder der Kunde den Nachweis erbracht hat, dass die Ware zurueckgesendet wurde. Der Kunde hat die Ware unverzueglich und spaetestens binnen vierzehn Tagen ab Widerruf zurueckzusenden.</p>
366 <h2>Ruecksendekosten und Wertersatz</h2>
367 <p>Die unmittelbaren Kosten der Ruecksendung traegt [Kunde/Shop-Betreiber - bitte passend eintragen]. Fuer einen Wertverlust der Ware muss der Kunde nur aufkommen, wenn dieser Wertverlust auf einen nicht notwendigen Umgang mit der Ware zurueckzufuehren ist.</p>
369 <h2>Ausschluss oder Erloeschen des Widerrufsrechts</h2>
370 <p>Das Widerrufsrecht kann insbesondere ausgeschlossen oder vorzeitig erloschen sein bei individuell angefertigten Waren, versiegelten Waren aus Hygiene- oder Gesundheitsschutzgruenden nach Entfernung der Versiegelung, schnell verderblichen Waren, bestimmten Dienstleistungen sowie digitalen Inhalten, wenn der Kunde ausdruecklich zugestimmt hat, dass die Ausfuehrung vor Ablauf der Widerrufsfrist beginnt, und die gesetzlich erforderlichen Bestaetigungen erteilt wurden.</p>
372 <h2>Muster-Widerrufsformular</h2>
373 <p>Wenn Sie den Vertrag widerrufen wollen, koennen Sie diesen Text verwenden und an den Shop-Betreiber senden:</p>
374 <div class=
"dbx-shop-withdrawal-form">
375 <p>An [Name/Firma, Anschrift und E-Mail-Adresse des Shop-Betreibers]</p>
376 <p>Hiermit widerrufe ich den von mir abgeschlossenen Vertrag ueber den Kauf der folgenden Waren oder die Erbringung der folgenden Dienstleistung:</p>
377 <p>[Artikel/Dienstleistung eintragen]</p>
378 <p>Bestellt am: [Datum]<br>Erhalten am: [Datum]</p>
379 <p>Name des Kunden: [Name]<br>Anschrift des Kunden: [Anschrift]</p>
380 <p>Datum: [Datum]</p>
381 <p>Unterschrift des Kunden: [nur bei Mitteilung auf Papier]</p>
387 private function mediaUrl(
string $path): string {
388 $path = trim(str_replace(
'\\',
'/', $path));
392 if (preg_match(
'~^https?://~i', $path) || substr($path, 0, 1) ===
'/') {
395 return dbx()->get_base_url() . ltrim($path,
'/');
398 private function mediaItemUrl(array $image,
bool $thumb =
false): string {
399 $mediaId = (int)($image[
'media_id'] ?? 0);
401 $url =
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' . $mediaId;
403 $url .=
'&dbx_thumb=1';
407 return $this->mediaUrl((
string)($image[
'image_path'] ??
''));
410 private function productImage(array $product): array {
411 $images = $product[
'images'] ?? array();
412 if (is_array($images) && isset($images[0]) && is_array($images[0])) {
416 'image_path' =>
'files/shop/img/software-dashboard.svg',
417 'title' => $product[
'title'] ??
'Artikel',
418 'alt' => $product[
'title'] ??
'Artikel',
422 private function primaryGroup(array $product): array {
423 return is_array($product[
'groups'] ?? null) ? (($product[
'groups'] ?? array())[0] ?? array()) : array();
426 private function templateName(
string $value,
string $fallback,
string $prefix =
''): string {
427 $value = preg_replace(
'~[^a-z0-9_-]+~i',
'', trim($value));
431 if ($prefix !==
'' && strpos($value, $prefix) !== 0) {
437 private function shopTemplateExists(
string $template): bool {
438 $template = preg_replace(
'~[^a-z0-9_-]+~i',
'', $template);
439 if ($template ===
'')
return false;
440 return is_file(dirname(__DIR__) .
'/tpl/htm/' . $template .
'.htm');
443 private function mediaTemplateExists(
string $template): bool {
444 $template = preg_replace(
'~[^a-z0-9_-]+~i',
'', strtolower($template));
445 if ($template ===
'')
return false;
446 return is_file(dirname(dirname(__DIR__)) .
'/dbxContent/tpl/htm/media-' . $template .
'.htm');
449 private function groupSetting(array $product,
string $key, $fallback) {
450 $group = $this->primaryGroup($product);
451 $value = $group[$key] ?? $fallback;
452 return $value ===
'' || $value ===
null ? $fallback : $value;
455 private function productVisual(array $product,
string $class =
''): string {
456 $image = $this->productImage($product);
457 $src = $this->mediaItemUrl($image,
true);
458 $alt = (string)($image[
'alt'] ?? $image[
'title'] ?? $product[
'title'] ??
'');
459 $count = count($product[
'images'] ?? array());
460 $html =
'<div class="dbx-shop-product-visual ' . $this->h($class) .
'">';
461 $html .=
'<img class="dbx-shop-product-img" src="' . $this->h($src) .
'" alt="' . $this->h($alt) .
'" loading="lazy">';
462 $html .=
'<span class="dbx-shop-badge">' . $this->h($product[
'badge'] ??
'Artikel') .
'</span>';
464 $html .=
'<span class="dbx-shop-image-count"><i class="bi bi-images"></i> ' . (int)
$count .
'</span>';
470 private function productGallery(array $product): string {
471 $images = $product[
'images'] ?? array();
472 if (!is_array($images) || $images === array()) {
473 return $this->productVisual($product,
'dbx-shop-product-visual-large');
475 $overflow = preg_replace(
'~[^a-z0-9_-]~i',
'', (
string)$this->groupSetting($product,
'gallery_overflow',
'grid')) ?:
'grid';
476 $click = preg_replace(
'~[^a-z0-9_-]~i',
'', (
string)$this->groupSetting($product,
'gallery_click',
'lightbox')) ?:
'lightbox';
477 $visible = max(1, (
int)$this->groupSetting($product,
'gallery_visible_count', 3));
478 $imgSize = preg_replace(
'~[^a-z0-9_-]~i',
'', (
string)$this->groupSetting($product,
'gallery_image_size',
'original')) ?:
'original';
479 $lightboxWidth = preg_replace(
'~[^a-z0-9%._-]+~i',
'', (
string)$this->groupSetting($product,
'gallery_lightbox_width',
'100vw')) ?:
'100vw';
480 $template = preg_replace(
'~[^a-z0-9_-]+~i',
'', strtolower((
string)$this->groupSetting($product,
'gallery_template',
'image-gallery'))) ?:
'image-gallery';
481 if (!$this->mediaTemplateExists($template)) {
482 $template =
'image-gallery';
484 $html =
'<div class="dbx-shop-product-gallery dbx-content-media-gallery gallery-list gallery-template-' . $this->h($template) .
'" data-dbx="lib=gallery|overflow=' . $this->h($overflow) .
'|click=' . $this->h($click) .
'|img-count=' . $visible .
'|img-size=' . $this->h($imgSize) .
'|lightbox-width=' . $this->h($lightboxWidth) .
'">';
485 foreach ($images as $image) {
486 $url = $this->mediaItemUrl($image,
false);
487 $thumbUrl = $this->mediaItemUrl($image,
true);
491 $title = (string)($image[
'title'] ?? $product[
'title'] ??
'');
492 $alt = (string)($image[
'alt'] ?? $title);
494 $html .= $this->tpl()->get_tpl(
'dbxContent|media-' . $template, array(
495 'id' => (
string)($image[
'media_id'] ??
''),
496 'url' => $this->h($url),
497 'thumb_url' => $this->h($thumbUrl),
498 'poster_url' => $this->h($thumbUrl),
499 'media_type' =>
'image',
500 'title' => $this->h($title),
501 'alt' => $this->h($alt),
502 'caption' => $this->h($caption),
511 private function placeholder(
string $headline,
string $text, array $items = array()): string {
513 foreach ($items as $item) {
514 $list .=
'<li>' . $this->h($item) .
'</li>';
517 return $this->tpl()->get_tpl(
'dbxShop|placeholder', array(
518 'headline' => $this->h($headline),
519 'text' => $this->h($text),
520 'items' => $list !==
'' ?
'<ul>' . $list .
'</ul>' :
'',
524 private function ensureSeed(): void {
525 $repo = $this->repo();
527 if (count(
$repo->products(
false)) === 0 || count(
$repo->shippingGroups()) === 0 || count(
$repo->channelGroups()) === 0 || count(
$repo->allImages()) === 0) {
528 $repo->seedDemoProducts();
532 private function activeChannel(): string {
536 private function channelNav(
string $active): string {
537 $channels = $this->repo()->channels();
538 $html =
'<div class="dbx-shop-channel-nav">';
539 foreach ($channels as $channel) {
540 $key = (string)($channel[
'channel_key'] ??
'');
544 $cls = $key === $active ?
' active' :
'';
545 $html .=
'<a class="btn btn-outline-secondary btn-sm' . $cls .
'" href="?dbx_modul=dbxShop&dbx_run1=catalog&channel=' . rawurlencode($key) .
'">';
546 $html .= $this->h($channel[
'title'] ?? $key);
553 private function productHasChannel(array $product,
string $channel): bool {
554 foreach (($product[
'channels'] ?? array()) as $ch) {
555 if ((
string)($ch[
'channel_key'] ??
'') === $channel && (
int)($ch[
'active'] ?? 0) === 1) {
562 private function groupsHtml(array $product): string {
564 foreach (($product[
'groups'] ?? array()) as $group) {
565 $groupId = (int)($group[
'id'] ?? 0);
566 $href = $groupId > 0 ?
'?dbx_modul=dbxShop&dbx_run1=catalog&group=' . $groupId :
'';
567 $label = $this->h($group[
'title'] ??
'');
568 $html .= $href !==
''
569 ?
'<a class="dbx-shop-chip" href="' . $href .
'">' . $label .
'</a>'
570 :
'<span class="dbx-shop-chip">' . $label .
'</span>';
575 private function catalogGroupId(): int {
576 return max(0, (int)
dbx()->get_modul_var(
'group', 0,
'int'));
579 private function groupImageUrl(array $group): string {
580 $image = $this->repo()->primaryImageForGroup((int)($group[
'id'] ?? 0));
581 if (is_array($image)) {
582 $url = $this->mediaItemUrl($image,
true);
587 return $this->mediaUrl(
'files/shop/img/software-dashboard.svg');
590 private function catalogGroupBreadcrumb(
int $groupId): string {
594 $path = $this->repo()->groupPath($groupId);
595 if ($path === array()) {
598 $html =
'<nav class="dbx-shop-group-breadcrumb" aria-label="Artikelgruppen">';
599 $html .=
'<a href="?dbx_modul=dbxShop&dbx_run1=catalog">Alle Artikel</a>';
600 foreach ($path as $group) {
601 $id = (int)($group[
'id'] ?? 0);
602 $title = $this->h($group[
'title'] ??
'');
603 if ($id === $groupId) {
604 $html .=
'<span>' . $title .
'</span>';
606 $html .=
'<a href="?dbx_modul=dbxShop&dbx_run1=catalog&group=' . $id .
'">' . $title .
'</a>';
613 private function catalogGroupNavigation(
int $parentId): string {
614 $groups = $this->repo()->groupsByParent($parentId, true);
615 if ($groups === array()) {
618 $html =
'<section class="dbx-shop-group-grid" aria-label="Artikelgruppen">';
619 foreach ($groups as $group) {
620 $id = (int)($group[
'id'] ?? 0);
621 if ($id <= 0)
continue;
622 $title = trim((
string)($group[
'title'] ??
'Artikelgruppe'));
623 $description = trim((
string)($group[
'description'] ??
''));
624 $html .=
'<a class="dbx-shop-group-card" href="?dbx_modul=dbxShop&dbx_run1=catalog&group=' . $id .
'">';
625 $html .=
'<span class="dbx-shop-group-card-image"><img src="' . $this->h($this->groupImageUrl($group)) .
'" alt="' . $this->h($title) .
'" loading="lazy"></span>';
626 $html .=
'<span class="dbx-shop-group-card-body"><strong>' . $this->h($title) .
'</strong>';
627 if ($description !==
'') {
628 $html .=
'<small>' . $this->h($description) .
'</small>';
630 $html .=
'</span></a>';
632 $html .=
'</section>';
636 private function productInCatalogGroup(array $product,
int $groupId): bool {
640 if ((
int)($product[
'product_group_id'] ?? 0) === $groupId) {
643 foreach (($product[
'groups'] ?? array()) as $group) {
644 if ((
int)($group[
'id'] ?? 0) === $groupId) {
651 private function channelsHtml(array $product): string {
653 foreach (($product[
'channels'] ?? array()) as $channel) {
654 if ((
int)($channel[
'active'] ?? 0) !== 1) {
657 $html .=
'<span class="dbx-shop-chip dbx-shop-chip-channel">' . $this->h($channel[
'title'] ?? $channel[
'channel_key'] ??
'') .
'</span>';
662 private function normalizedText(
string $value): string {
663 $value = strtolower($value);
664 $value = strtr($value, array(
'ä' =>
'ae',
'ö' =>
'oe',
'ü' =>
'ue',
'ß' =>
'ss'));
665 $value = preg_replace(
'~[^a-z0-9]+~',
' ', $value) ?:
'';
666 return preg_replace(
'~\\s+~',
' ', trim($value)) ?:
'';
669 private function attributeText(array $product): string {
671 foreach (($product[
'attributes'] ?? array()) as $attribute) {
672 $value = trim((
string)($attribute[
'display_value'] ?? $attribute[
'value_text'] ??
''));
673 $parts[] = (string)($attribute[
'title'] ??
'');
674 $parts[] = (string)($attribute[
'attr_key'] ??
'');
679 return implode(
' ', $parts);
682 private function groupText(array $product): string {
684 foreach (($product[
'groups'] ?? array()) as $group) {
685 $parts[] = (string)($group[
'title'] ??
'');
686 $parts[] = (string)($group[
'group_key'] ??
'');
687 $parts[] = (string)($group[
'description'] ??
'');
688 $parts[] = (string)($group[
'attribute_notes'] ??
'');
690 return implode(
' ', $parts);
693 private function searchTerms(
string $query): array {
694 $terms = preg_split(
'~\\s+~', $this->normalizedText($query)) ?: array();
695 $stopWords = array_flip(array(
'der',
'die',
'das',
'den',
'dem',
'des',
'ein',
'eine',
'einer',
'einem',
'und',
'oder',
'mit',
'ohne',
'fuer',
'fur',
'von',
'im',
'in',
'am',
'an',
'auf',
'zu'));
697 foreach ($terms as $term) {
699 if ($term ===
'' || isset($stopWords[$term])) {
702 if (strlen($term) < 2 && !ctype_digit($term)) {
707 return array_keys($out);
710 private function textMatchesSearchTerm(
string $text,
string $term): bool {
711 return $this->searchFieldScore($text, $term, 1) > 0;
714 private function searchFieldScore(
string $text,
string $term,
int $weight): int {
715 if ($text ===
'' || $term ===
'') {
718 if ($text === $term) {
721 $termLength = strlen($term);
722 $compactText = str_replace(
' ',
'', $text);
723 $compactTerm = str_replace(
' ',
'', $term);
724 if (strpos($text, $term) !==
false || strpos($compactText, $compactTerm) !==
false) {
728 foreach (preg_split(
'~\\s+~', $text) ?: array() as $token) {
729 $token = trim($token);
733 if ($token === $term) {
734 $best = max($best, $weight * 6);
737 if ($termLength < 3) {
740 if (strlen($token) >= $termLength && strpos($token, $term) === 0) {
741 $best = max($best, $weight * 4);
746 && strlen($token) >= 4
747 && substr($token, 0, 3) === substr($term, 0, 3)
748 && abs(strlen($token) - $termLength) <= ($termLength >= 7 ? 2 : 1)
749 && levenshtein($token, $term) <= ($termLength >= 7 ? 2 : 1)
751 $best = max($best, $weight * 2);
757 private function productSearchScore(array $product,
string $query): int {
758 $terms = $this->searchTerms($query);
759 if ($terms === array()) {
763 $primary = $this->normalizedText(implode(
' ', array(
764 (
string)($product[
'sku'] ??
''),
765 (
string)($product[
'title'] ??
''),
766 (
string)($product[
'category'] ??
''),
767 (
string)($product[
'badge'] ??
''),
768 (
string)($product[
'product_type'] ??
''),
770 $secondary = $this->normalizedText(implode(
' ', array(
771 (
string)($product[
'summary'] ??
''),
772 (
string)($product[
'description'] ??
''),
774 $attributes = $this->normalizedText($this->attributeText($product));
775 $groups = $this->normalizedText($this->groupText($product));
779 $firstTermPrimaryScore = 0;
780 $termCount = count($terms);
782 foreach ($terms as $idx => $term) {
783 $primaryScore = $this->searchFieldScore($primary, $term, 10);
786 $this->searchFieldScore($attributes, $term, 7),
787 $this->searchFieldScore($secondary, $term, 4),
788 $this->searchFieldScore($groups, $term, 3)
792 $firstTermPrimaryScore = $primaryScore;
794 if ($termScore > 0) {
796 $score += $termScore;
800 if ($matched === 0) {
803 if ($termCount === 1) {
807 if ($matched === $termCount || $firstTermPrimaryScore > 0 || $score >= 20) {
808 return $score + ($matched * 3);
814 private function attributesInlineHtml(array $product,
int $max = 4): string {
817 foreach (($product[
'attributes'] ?? array()) as $attribute) {
818 $value = trim((
string)($attribute[
'display_value'] ?? $attribute[
'value_text'] ??
''));
819 if ($value ===
'')
continue;
820 $html .=
'<span class="dbx-shop-attribute-chip"><span>' . $this->h($attribute[
'title'] ??
'') .
'</span><strong>' . $this->h($value) .
'</strong></span>';
822 if (
$count >= $max)
break;
824 return $html !==
'' ?
'<div class="dbx-shop-attribute-row">' . $html .
'</div>' :
'';
827 private function attributesTableHtml(array $product): string {
829 foreach (($product[
'attributes'] ?? array()) as $attribute) {
830 $value = trim((
string)($attribute[
'display_value'] ?? $attribute[
'value_text'] ??
''));
831 if ($value ===
'')
continue;
832 $rows .=
'<tr><th>' . $this->h($attribute[
'title'] ??
'') .
'</th><td>' . $this->h($value) .
'</td></tr>';
837 return '<div class="dbx-shop-attributes"><h4>Eigenschaften</h4><table><tbody>' . $rows .
'</tbody></table></div>';
840 private function selectedAttributeFilters(): array {
841 $raw = $_GET[
'attr'] ?? array();
842 if (!is_array($raw)) {
846 foreach ($raw as $id => $value) {
848 $value = trim((
string)$value);
849 if ($id > 0 && $value !==
'') {
856 private function productMatchesQuery(array $product,
string $query): bool {
857 return $this->productSearchScore($product, $query) > 0;
860 private function productMatchesAttributeFilters(array $product, array $filters): bool {
861 if ($filters === array()) {
865 foreach (($product[
'attributes'] ?? array()) as $attribute) {
866 $id = (int)($attribute[
'id'] ?? 0);
867 if ($id <= 0)
continue;
868 $values[$id] = $this->normalizedText((
string)($attribute[
'value_text'] ??
''));
870 foreach ($filters as $id => $value) {
871 if (!isset($values[$id]) || $values[$id] !== $this->normalizedText((
string)$value)) {
878 private function catalogFiltersHtml(
string $channel,
string $query, array $selected,
int $groupId = 0): string {
879 $searchField =
dbx()->search_html(
dbx()->search_defaults(array(
882 'placeholder' =>
'Suchbegriff eingeben',
884 'input_class' =>
'form-control-sm dbx-shop-search-input',
885 'wrap_class' =>
'dbx-shop-search-wrap',
886 'data_role' =>
'shop-search',
890 foreach ($this->repo()->attributeFilterDefinitions() as $definition) {
891 $id = (int)($definition[
'id'] ?? 0);
892 $values = $definition[
'values'] ?? array();
893 if ($id <= 0 || !is_array($values) || $values === array())
continue;
894 $label = trim((
string)($definition[
'title'] ??
''));
895 $group = trim((
string)($definition[
'group_title'] ??
''));
896 $filterFields .=
'<label><span>' . $this->h($group !==
'' ? $group .
': ' . $label : $label) .
'</span><select class="form-select form-select-sm" name="attr[' . $id .
']">';
897 $filterFields .=
'<option value="">Alle</option>';
898 foreach ($values as $value) {
899 $sel = isset($selected[$id]) && $this->normalizedText((
string)$selected[$id]) === $this->normalizedText((
string)$value) ?
' selected' :
'';
900 $filterFields .=
'<option value="' . $this->h($value) .
'"' . $sel .
'>' . $this->h($value) .
'</option>';
902 $filterFields .=
'</select></label>';
904 $advancedFilters =
'';
905 if ($filterFields !==
'') {
906 $open = $selected !== array() ?
' open' :
'';
907 $advancedFilters .=
'<details class="dbx-shop-filter-advanced"' . $open .
'>';
908 $advancedFilters .=
'<summary><i class="bi bi-sliders"></i> Filter verfeinern</summary>';
909 $advancedFilters .=
'<div class="dbx-shop-filter-row">' . $filterFields .
'</div>';
910 $advancedFilters .=
'</details>';
913 $form =
dbx()->get_system_obj(
'dbxForm');
914 $form->init(
'shop-catalog-filter-form',
'shop-catalog-filter-form');
915 $form->set_editor_class_file(__FILE__);
916 $form->_action =
'?dbx_modul=dbxShop&dbx_run1=catalog';
917 $form->_data = array(
'q' => $query);
918 $form->_msg_info =
'';
919 $form->_msg_success =
'';
920 $form->_msg_error =
'';
921 $form->_msg_warning =
'';
922 $form->add_rep(
'frame_skip_form_wrap',
'1');
923 $form->add_rep(
'frame_form_open',
'<form action="?dbx_modul=dbxShop&dbx_run1=catalog" method="get" class="dbx-shop-filter">');
924 $form->add_rep(
'frame_form_close',
'</form>');
925 $form->add_rep(
'search_field', $searchField);
926 $form->add_rep(
'advanced_filters', $advancedFilters);
927 $form->add_rep(
'group_hidden', $groupId > 0 ?
'<input type="hidden" name="group" value="' . $groupId .
'">' :
'');
931 private function productTemplateData(array $product,
string $channel): array {
932 $sku = (string)($product[
'sku'] ??
'');
934 'sku' => $this->h($sku),
935 'title' => $this->h($product[
'title'] ??
''),
936 'summary' => $this->h($product[
'summary'] ??
''),
937 'description' => $this->h($product[
'description'] ?? $product[
'summary'] ??
''),
938 'groups' => $this->groupsHtml($product),
940 'attributes' => $this->attributesInlineHtml($product, 4),
941 'attributes_table' => $this->attributesTableHtml($product),
942 'gallery' => $this->productGallery($product),
943 'visual' => $this->productVisual($product),
944 'price' => $this->money($product[
'price_gross'] ?? 0),
945 'tax_shipping' => $this->taxShippingHtml($product),
946 'shipping_info' => $this->shippingInfoHtml($product),
947 'stock_info' => $this->stockInfoHtml($product),
948 'buy_form' => $this->buyFormHtml($product),
949 'detail_url' =>
'?dbx_modul=dbxShop&dbx_run1=product&sku=' . rawurlencode($sku),
950 'catalog_url' =>
'?dbx_modul=dbxShop&dbx_run1=catalog',
951 'cart_url' =>
'?dbx_modul=dbxShop&dbx_run1=cart&sku=' . rawurlencode($sku),
952 'card_class' => $this->h($this->cssTemplateClass((
string)$this->groupSetting($product,
'card_template',
'product-card-default'))),
953 'detail_class' => $this->h($this->cssTemplateClass((
string)$this->groupSetting($product,
'detail_template',
'product-detail-default'))),
957 private function cssTemplateClass(
string $template): string {
958 $template = preg_replace(
'~[^a-z0-9_-]+~i',
'-', trim($template));
959 return $template !==
'' ?
'is-template-' . strtolower($template) :
'';
962 private function renderProductCard(array $product,
string $channel): string {
963 $template = $this->templateName((string)$this->groupSetting($product,
'card_template',
'product-card-default'),
'product-card-default',
'product-card-');
964 if (!$this->shopTemplateExists($template)) {
965 $template =
'product-card-default';
967 return $this->tpl()->get_tpl(
'dbxShop|' . $template, $this->productTemplateData($product, $channel));
970 private function catalogReportHtml(array $products,
string $channel,
string $query, array $attributeFilters,
int $groupId): string {
971 $report =
dbx()->get_system_obj(
'dbxReport');
972 $report->init(
'shop-catalog-report',
'dbxShop|shop-catalog-report');
973 $report->set_editor_class_file(__FILE__);
974 $report->_mode =
'tpl';
975 $report->_pages =
true;
976 $report->_create_row_select =
false;
977 $report->_create_row_edit =
false;
978 $report->_create_row_delete =
false;
979 $report->_but_pagination = 7;
980 $rowsPerPage = max(6, min(48, (
int)$report->get_fld_val(
'dbx_rrows', 12,
'int')));
981 $position = max(0, (
int)$report->get_fld_val(
'dbx_rpos', 0,
'int'));
982 $filteredCount = count($products);
983 if ($position >= $filteredCount && $filteredCount > 0) {
984 $position = max(0, (
int)(floor(($filteredCount - 1) / $rowsPerPage) * $rowsPerPage));
986 $visible = array_slice($products, $position, $rowsPerPage);
988 foreach ($visible as $product) {
990 'id' => (
int)($product[
'id'] ?? 0),
991 'card' => $this->renderProductCard($product, $channel),
996 'dbx_modul' =>
'dbxShop',
997 'dbx_run1' =>
'catalog',
1000 $queryParts[
'q'] = $query;
1003 $queryParts[
'group'] = $groupId;
1005 foreach ($attributeFilters as $id => $value) {
1006 $queryParts[
'attr[' . (int)$id .
']'] = (
string)$value;
1008 $report->_action =
'?' . http_build_query($queryParts,
'',
'&');
1009 $report->_rflds = array(
'card' =>
'Artikel');
1010 $report->_rpt_format = array(
'card' =>
'html');
1011 $report->_rrows = $rowsPerPage;
1012 $report->_rpos = $position;
1013 $report->_count_all = $filteredCount;
1014 $report->_rcount = $filteredCount;
1015 $report->_rdata = $rows;
1016 return $report->run();
1019 private function renderProductDetail(array $product,
string $channel): string {
1020 $template = $this->templateName((string)$this->groupSetting($product,
'detail_template',
'product-detail-default'),
'product-detail-default',
'product-detail-');
1021 if (!$this->shopTemplateExists($template)) {
1022 $template =
'product-detail-default';
1024 $data = $this->productTemplateData($product, $channel);
1025 $data[
'attributes'] = $data[
'attributes_table'];
1026 return $this->tpl()->get_tpl(
'dbxShop|' . $template, $data);
1029 private function taxShippingHtml(array $product): string {
1030 $tax = $this->h(number_format((float)($product[
'effective_tax_rate'] ?? 0), 2,
',',
'.'));
1031 $shipping = (float)($product[
'effective_shipping_gross'] ?? 0);
1032 $shippingText = $shipping > 0 ? $this->money($shipping) .
' Versand' :
'versandfrei';
1033 $showTax = $this->settingsBool($this->shopConfig(),
'tax_display_enabled',
true);
1036 $parts[] = $tax .
'% MwSt.';
1038 $parts[] = $this->h($shippingText);
1039 return '<small>' . implode(
', ', $parts) .
'</small>';
1042 private function shippingInfoHtml(array $product): string {
1043 $deliveryTime = trim((string)($product[
'effective_delivery_time'] ??
''));
1044 $shippingWay = trim((
string)($product[
'effective_shipping_way'] ??
''));
1045 $shipping = (float)($product[
'effective_shipping_gross'] ?? 0);
1046 $shippingText = $shipping > 0 ? $this->money($shipping) :
'versandfrei';
1049 if ($deliveryTime !==
'') {
1050 $rows .=
'<div class="dbx-shop-shipping-info-row"><i class="bi bi-clock"></i><span>Lieferzeit: ' . $this->h($deliveryTime) .
'</span></div>';
1052 if ($shippingWay !==
'') {
1053 $rows .=
'<div class="dbx-shop-shipping-info-row"><i class="bi bi-truck"></i><span>Versandweg: ' . $this->h($shippingWay) .
'</span></div>';
1055 $rows .=
'<div class="dbx-shop-shipping-info-row"><i class="bi bi-box-seam"></i><span>Versandkosten: ' . $this->h($shippingText) .
'</span></div>';
1057 return '<div class="dbx-shop-shipping-info">' . $rows .
'</div>';
1060 private function stockInfoHtml(array $product): string {
1061 $cfg = $this->shopConfig();
1062 if (!$this->settingsBool(
$cfg,
'stock_enabled',
false) || (
string)($product[
'product_type'] ??
'') !==
'physical') {
1065 $stock = (int)($product[
'stock'] ?? 0);
1067 return '<div class="alert alert-warning py-2 mb-2"><i class="bi bi-exclamation-triangle"></i> Aktuell nicht auf Lager.</div>';
1070 return '<div class="alert alert-info py-2 mb-2"><i class="bi bi-box-seam"></i> Nur noch ' . $stock .
' verfuegbar.</div>';
1072 return '<div class="dbx-shop-shipping-info-row"><i class="bi bi-box-seam"></i><span>Lagerbestand: verfuegbar</span></div>';
1075 private function buyFormHtml(array $product): string {
1076 $sku = (string)($product[
'sku'] ??
'');
1080 $cfg = $this->shopConfig();
1081 if ($this->settingsBool(
$cfg,
'stock_enabled',
false) && (
string)($product[
'product_type'] ??
'') ===
'physical' && (
int)($product[
'stock'] ?? 0) <= 0) {
1082 return '<a class="btn btn-outline-secondary" href="?dbx_modul=dbxShop&dbx_run1=catalog"><i class="bi bi-arrow-left"></i> Zurueck zum Katalog</a>';
1084 $form =
dbx()->get_system_obj(
'dbxForm');
1085 $form->init(
'shop-buy-' . preg_replace(
'~[^a-z0-9_-]+~i',
'-', strtolower($sku)),
'shop-buy-form');
1086 $form->set_editor_class_file(__FILE__);
1087 $form->_action =
'?dbx_modul=dbxShop&dbx_run1=cart&sku=' . rawurlencode($sku);
1088 $form->_data = array(
'qty' => 1);
1089 $form->_msg_info =
'';
1090 $form->_msg_success =
'';
1091 $form->_msg_error =
'';
1092 $form->_msg_warning =
'';
1093 $form->add_rep(
'frame_skip_form_wrap',
'1');
1094 $form->add_rep(
'frame_form_open',
'<form action="' . $this->h($form->_action) .
'" method="post" id="dbx_shop_buy_' . $this->h($sku) .
'" class="dbx-shop-buy-form">');
1095 $form->add_rep(
'frame_form_close',
'</form>');
1096 $form->add_rep(
'catalog_url',
'?dbx_modul=dbxShop&dbx_run1=catalog');
1097 $form->add_fld(
'qty',
'dbxShop|shop-field-qty', label:
'Anzahl', rules:
'int|min=1');
1098 return $form->run();
1101 private function startSession(): void {
1102 if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
1105 if (!isset($_SESSION[
'dbxShop_cart']) || !is_array($_SESSION[
'dbxShop_cart'])) {
1106 $_SESSION[
'dbxShop_cart'] = array();
1110 private function cartItems(): array {
1111 $this->startSession();
1112 return $_SESSION[
'dbxShop_cart'];
1115 private function requestedQuantity($value,
int $fallback = 1): int {
1117 return max(1, min(999, $qty > 0 ? $qty : $fallback));
1120 private function addToCart(
string $sku,
int $qty = 1): void {
1124 $product = $this->repo()->productBySku($sku);
1128 $this->startSession();
1129 $_SESSION[
'dbxShop_cart'][$sku] = max(0, (
int)($_SESSION[
'dbxShop_cart'][$sku] ?? 0)) + $this->requestedQuantity($qty);
1132 private function updateCartQuantities(array $quantities): void {
1133 $this->startSession();
1134 foreach ($quantities as $sku => $qty) {
1135 $sku = (string)$sku;
1136 if (!isset($_SESSION[
'dbxShop_cart'][$sku])) {
1139 $_SESSION[
'dbxShop_cart'][$sku] = $this->requestedQuantity($qty);
1143 private function removeFromCart(
string $sku): void {
1148 $this->startSession();
1149 unset($_SESSION[
'dbxShop_cart'][$sku]);
1152 private function addedToCartDialog(array $product): string {
1153 $title = trim((string)($product[
'title'] ??
'Artikel'));
1154 $qty = $this->requestedQuantity(
dbx()->get_modul_var(
'qty',
'1',
'parameter'));
1155 $body =
'<div class="dbx-shop-added-dialog" role="dialog" aria-modal="true" aria-labelledby="dbx-shop-added-title">';
1156 $body .=
'<div class="dbx-shop-added-dialog-backdrop"></div>';
1157 $body .=
'<div class="dbx-shop-added-dialog-box">';
1158 $body .=
'<div class="dbx-shop-added-dialog-icon"><i class="bi bi-check2"></i></div>';
1159 $body .=
'<h3 id="dbx-shop-added-title">Artikel wurde in den Warenkorb gelegt</h3>';
1160 $body .=
'<p>' . $this->h($title) .
' <span class="dbx-shop-added-qty">x ' . (int)$qty .
'</span></p>';
1161 $body .=
'<div class="dbx-shop-added-dialog-actions">';
1162 $body .=
'<a class="btn btn-outline-primary" href="?dbx_modul=dbxShop&dbx_run1=cart"><i class="bi bi-cart"></i> Warenkorb</a>';
1163 $body .=
'<a class="btn btn-primary" href="?dbx_modul=dbxShop&dbx_run1=checkout"><i class="bi bi-credit-card"></i> Kasse</a>';
1164 $body .=
'<a class="btn btn-outline-secondary" href="?dbx_modul=dbxShop&dbx_run1=catalog"><i class="bi bi-grid"></i> Weiter einkaufen</a>';
1171 private function cartRowsAndSum(
bool $editable =
false): array {
1174 foreach ($this->cartItems() as $sku => $qty) {
1175 $product = $this->repo()->productBySku((
string)$sku);
1179 $qty = max(1, (
int)$qty);
1180 $price = (float)($product[
'price_gross'] ?? 0);
1181 $shipping = (float)($product[
'effective_shipping_gross'] ?? 0);
1182 $line = ($price + $shipping) * $qty;
1184 $qtyHtml = $editable
1185 ?
'<input class="form-control form-control-sm dbx-shop-cart-qty" type="number" min="1" max="999" step="1" name="qty[' . $this->h($sku) .
']" value="' . (int)$qty .
'">'
1186 : (string)(int)$qty;
1188 $rows .=
'<td><strong>' . $this->h($product[
'title'] ??
'') .
'</strong><br><small>' . $this->h($sku) .
'</small></td>';
1189 $rows .=
'<td class="text-end">' . $qtyHtml .
'</td>';
1190 $rows .=
'<td class="text-end">' . $this->money($price) .
'</td>';
1191 $rows .=
'<td class="text-end">' . $this->money($shipping) .
'</td>';
1192 $rows .=
'<td class="text-end">' . $this->money($line) .
'</td>';
1195 return array($rows, $sum);
1198 private function cartReportDataAndSum(): array {
1201 foreach ($this->cartItems() as $sku => $qty) {
1202 $product = $this->repo()->productBySku((
string)$sku);
1206 $qty = max(1, (
int)$qty);
1207 $price = (float)($product[
'price_gross'] ?? 0);
1208 $shipping = (float)($product[
'effective_shipping_gross'] ?? 0);
1209 $line = ($price + $shipping) * $qty;
1211 $skuText = (string)$sku;
1214 'remove' =>
'<a class="btn btn-sm btn-outline-danger dbxAjax dbxConfirm dbx-shop-cart-remove" href="?dbx_modul=dbxShop&dbx_run1=cart&remove=' . rawurlencode($skuText) .
'" data-confirm-title="<i class=\'bi bi-trash\'></i> Artikel entfernen" data-confirm="Diesen Artikel aus dem Warenkorb entfernen?" data-confirm-hint="<small>Die Position wird aus dem Warenkorb geloescht.</small>" data-confirm-buttons="yesno" title="Artikel entfernen"><i class="bi bi-trash"></i></a>',
1215 'article' =>
'<strong>' . $this->h($product[
'title'] ??
'') .
'</strong><br><small>' . $this->h($skuText) .
'</small>',
1216 'qty' =>
'<input class="form-control form-control-sm dbx-shop-cart-qty" type="number" min="1" max="999" step="1" name="qty[' . $this->h($skuText) .
']" value="' . (
int)$qty .
'">',
1217 'price' =>
'<span class="dbx-shop-money">' . $this->money($price) .
'</span>',
1218 'shipping' =>
'<span class="dbx-shop-money">' . $this->money($shipping) .
'</span>',
1219 'line' =>
'<span class="dbx-shop-money"><strong>' . $this->money($line) .
'</strong></span>',
1222 return array($rows, $sum);
1225 private function cartReportHtml(array $rows,
float $sum): string {
1226 $report =
dbx()->get_system_obj(
'dbxReport');
1227 $report->init(
'shop-cart-report',
'dbxShop|shop-cart-report');
1228 $report->set_editor_class_file(__FILE__);
1229 $report->_action =
'?dbx_modul=dbxShop&dbx_run1=cart';
1230 $report->_mode =
'table';
1231 $report->_pages =
false;
1232 $report->_rdata = $rows;
1233 $report->_rcount = count($rows);
1234 $report->_count_all = count($rows);
1235 $report->_rrows = max(1, count($rows));
1237 $report->_create_row_select =
false;
1238 $report->_create_row_edit =
false;
1239 $report->_create_row_delete =
false;
1240 $report->_rflds = array(
1241 'remove' =>
'Aktion',
1242 'article' =>
'Artikel',
1245 'shipping' =>
'Versand',
1248 $report->_rpt_format = array(
1250 'article' =>
'html',
1253 'shipping' =>
'html',
1256 $report->add_rep(
'cart_sum', $this->money($sum));
1257 return $report->run();
1260 private function cartBodyHtml(): string {
1261 [$reportRows, $sum] = $this->cartReportDataAndSum();
1263 if ($reportRows === array()) {
1264 return $this->placeholder(
'Der Warenkorb ist leer',
'Waehlen Sie im Katalog einen Artikel und legen Sie ihn in den Warenkorb.');
1267 return $this->cartReportHtml($reportRows, $sum);
1270 private function absoluteShopUrl(
string $run, array $params = array()): string {
1271 $query = array_merge(array(
1272 'dbx_modul' =>
'dbxShop',
1275 return dbx()->get_base_url() .
'?' . http_build_query($query,
'',
'&');
1278 private function checkoutPaymentOptions(): array {
1279 $cfg = $this->shopConfig();
1281 if ($this->settingsBool(
$cfg,
'payment_bank_transfer_enabled',
true)) {
1282 $options[
'bank_transfer'] =
'Vorkasse / Ueberweisung';
1284 if ($this->settingsBool(
$cfg,
'payment_invoice_enabled',
false)) {
1285 $options[
'invoice'] =
'Rechnung';
1287 if ($this->settingsBool(
$cfg,
'payment_paypal_enabled',
false) && $this->paypal()->isConfigured()) {
1288 $options[
'paypal'] =
'PayPal';
1290 if ($this->settingsBool(
$cfg,
'payment_amazon_pay_enabled',
false) && $this->amazonPay()->isConfigured()) {
1291 $options[
'amazon_pay'] =
'Amazon Pay';
1296 private function paymentMethodLabels(): array {
1298 'bank_transfer' =>
'Vorkasse / Ueberweisung',
1299 'invoice' =>
'Rechnung',
1300 'paypal' =>
'PayPal',
1301 'amazon_pay' =>
'Amazon Pay',
1305 private function paymentProviderLabel(
string $provider): string {
1306 $labels = $this->paymentMethodLabels();
1307 $channelLabels = array(
1309 'amazon' =>
'Amazon Channel',
1310 'ebay' =>
'eBay Channel',
1311 'kleinanzeigen' =>
'Kleinanzeigen Channel',
1312 'mobile' =>
'mobile.de Channel',
1314 return $labels[$provider] ?? $channelLabels[$provider] ?? $provider;
1317 private function paymentInstructions(
string $method, array $order = array()): string {
1318 $cfg = $this->shopConfig();
1319 if ($method ===
'bank_transfer') {
1321 $intro = trim((
string)(
$cfg[
'payment_bank_transfer_instructions'] ??
''));
1322 if ($intro ===
'') {
1323 $intro =
'Bitte ueberweisen Sie den Rechnungsbetrag unter Angabe der Bestellnummer.';
1327 'Kontoinhaber' =>
'payment_bank_transfer_account_owner',
1328 'IBAN' =>
'payment_bank_transfer_iban',
1329 'BIC' =>
'payment_bank_transfer_bic',
1330 'Bank' =>
'payment_bank_transfer_bank_name',
1331 ) as $label => $key) {
1332 $value = trim((
string)(
$cfg[$key] ??
''));
1333 if ($value !==
'') {
1334 $lines[] = $label .
': ' . $value;
1337 if (trim((
string)($order[
'order_no'] ??
'')) !==
'') {
1338 $lines[] =
'Verwendungszweck: ' . (string)$order[
'order_no'];
1340 return implode(
"\n", $lines);
1342 if ($method ===
'invoice') {
1343 $text = trim((
string)(
$cfg[
'payment_invoice_instructions'] ??
''));
1344 return $text !==
'' ? $text :
'Sie erhalten eine Rechnung. Bitte zahlen Sie innerhalb der angegebenen Frist.';
1346 if ($method ===
'amazon_pay') {
1347 return 'Die Zahlung wird ueber Amazon Pay autorisiert und nach erfolgreicher Rueckmeldung automatisch aktualisiert.';
1349 if ($method ===
'paypal') {
1350 return 'Die Zahlung wird ueber PayPal autorisiert und nach erfolgreicher Rueckmeldung automatisch aktualisiert.';
1355 private function checkoutPaymentHelp(array $options): string {
1356 if ($options === array()) {
1357 return '<div class="alert alert-warning mb-0">Aktuell ist keine Zahlungsart aktiv oder vollstaendig konfiguriert.</div>';
1360 if (isset($options[
'bank_transfer'])) {
1361 $parts[] =
'<div><strong>Vorkasse / Ueberweisung</strong><span>Die Bestellung wird gespeichert. Der Kunde ueberweist danach mit Bestellnummer als Verwendungszweck.</span></div>';
1363 if (isset($options[
'invoice'])) {
1364 $parts[] =
'<div><strong>Rechnung</strong><span>Die Bestellung wird gespeichert und spaeter per Rechnung bezahlt.</span></div>';
1366 if (isset($options[
'paypal'])) {
1367 $parts[] =
'<div><strong>PayPal</strong><span>Sie werden zu PayPal weitergeleitet. Nach der Freigabe wird die Zahlung automatisch bestaetigt.</span></div>';
1369 if (isset($options[
'amazon_pay'])) {
1370 $parts[] =
'<div><strong>Amazon Pay</strong><span>Sie werden zu Amazon Pay weitergeleitet. Sandbox und Live laufen ueber die in den Shop-Einstellungen gespeicherten Zugangsdaten.</span></div>';
1372 return '<div class="dbx-shop-payment-method-help">' . implode(
'', $parts) .
'</div>';
1375 private function checkoutTableHtml(
string $rows,
float $sum): string {
1376 return
'<div class="dbx-shop-cart table-responsive">'
1377 .
'<table class="table table-sm align-middle">'
1378 .
'<thead><tr><th>Artikel</th><th class="text-end">Menge</th><th class="text-end">Preis</th><th class="text-end">Versand</th><th class="text-end">Summe</th></tr></thead>'
1379 .
'<tbody>' . $rows .
'</tbody>'
1380 .
'<tfoot><tr><th colspan="4" class="text-end">Zahlbetrag</th><th class="text-end">' . $this->money($sum) .
'</th></tr></tfoot>'
1384 private function legalSnapshotsForOrder(): array {
1385 $cfg = $this->shopConfig();
1386 if (!$this->settingsBool(
$cfg,
'legal_snapshot_enabled',
true)) {
1387 return array(
'',
'');
1389 $db = $this->contentDb();
1390 if (!is_object(
$db)) {
1391 return array(
'',
'');
1393 $pages = $this->ensureShopLegalPages();
1395 $snapshot =
function(
string $key) use (
$db, $pages, $dd):
string {
1396 $cid = (int)($pages[$key] ?? 0);
1400 $row =
$db->select1($dd, $cid,
'title,permalink,content,update_date', 0);
1401 if (!is_array($row)) {
1404 return json_encode(array(
1405 'captured_at' => date(
'Y-m-d H:i:s'),
1406 'title' => (
string)($row[
'title'] ??
''),
1407 'permalink' => (
string)($row[
'permalink'] ??
''),
1408 'update_date' => (
string)($row[
'update_date'] ??
''),
1409 'content' => (
string)($row[
'content'] ??
''),
1410 ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) ?:
'';
1412 return array($snapshot(
'legal'), $snapshot(
'withdrawal'));
1415 private function orderSuccessPage(array $order,
string $paymentMethod): string {
1416 $this->startSession();
1417 $_SESSION[
'dbxShop_last_order_no'] = (string)($order[
'order_no'] ??
'');
1418 $methodLabels = $this->paymentMethodLabels();
1419 $instructions = trim($this->paymentInstructions($paymentMethod, $order));
1420 $body =
'<section class="dbx-shop-order-success">'
1421 .
'<div class="dbx-shop-order-success-icon"><i class="bi bi-check2-circle"></i></div>'
1422 .
'<h2>Bestellung gespeichert</h2>'
1423 .
'<p>Ihre Bestellnummer lautet <strong>' . $this->h($order[
'order_no'] ??
'') .
'</strong>.</p>'
1425 .
'<dt>Zahlungsart</dt><dd>' . $this->h($methodLabels[$paymentMethod] ?? $paymentMethod) .
'</dd>'
1426 .
'<dt>Status</dt><dd>Die Bestellung ist im Shop-Admin sichtbar und wartet auf Bearbeitung.</dd>'
1427 .
'<dt>Summe</dt><dd>' . $this->money($order[
'total_gross'] ?? 0) .
'</dd>'
1429 . ($instructions !==
'' ?
'<div class="alert alert-info text-start"><strong>Zahlungshinweis</strong><br>' . nl2br($this->h($instructions)) .
'</div>' :
'')
1430 .
'<div class="dbx-shop-order-success-actions">'
1431 .
'<a class="btn btn-primary" href="?dbx_modul=dbxShop&dbx_run1=orders"><i class="bi bi-receipt"></i> Bestellungen ansehen</a>'
1432 .
'<a class="btn btn-outline-secondary" href="?dbx_modul=dbxShop&dbx_run1=catalog"><i class="bi bi-grid"></i> Weiter einkaufen</a>'
1435 return $this->page(
'Danke',
'Die Bestellung wurde als Snapshot gespeichert.', $body,
'orders');
1438 private function shopMailFrom(array
$cfg) {
1439 $from = trim((
string)(
$cfg[
'mail_from'] ??
''));
1441 $from =
'shop@dbxapp.de';
1443 return array(
'email' => $from,
'name' =>
'dbxShop');
1446 private function orderMailHtml(array $order,
string $title): string {
1448 foreach ((array)($order[
'items'] ?? array()) as $item) {
1449 $items .=
'<li>' . (int)($item[
'qty'] ?? 0) .
'x ' . $this->h($item[
'title'] ??
'') .
' - ' . $this->money($item[
'total_gross'] ?? 0) .
'</li>';
1451 $provider = (string)($order[
'payment_provider'] ??
'');
1452 $instructions = trim($this->paymentInstructions($provider, $order));
1453 return '<h1>' . $this->h($title) .
'</h1>'
1454 .
'<p>Bestellnummer: <strong>' . $this->h($order[
'order_no'] ??
'') .
'</strong></p>'
1455 .
'<ul>' . $items .
'</ul>'
1456 .
'<p>Summe: <strong>' . $this->money($order[
'total_gross'] ?? 0) .
'</strong></p>'
1457 .
'<p>Status: ' . $this->h($order[
'status'] ??
'') .
', Zahlung: ' . $this->h($this->paymentProviderLabel($provider)) .
' / ' . $this->h($order[
'payment_status'] ??
'') .
'</p>'
1458 . ($instructions !==
'' ?
'<p><strong>Zahlungshinweis</strong><br>' . nl2br($this->h($instructions)) .
'</p>' :
'');
1461 private function sendOrderMails(array $order): void {
1462 $cfg = $this->shopConfig();
1463 $from = $this->shopMailFrom(
$cfg);
1464 $subject =
'Bestellung ' . (string)($order[
'order_no'] ??
'');
1466 if ($this->settingsBool(
$cfg,
'mail_customer_enabled',
false)) {
1467 $to = trim((
string)($order[
'customer_email'] ??
''));
1469 dbx()->send_mail($from, $to, $subject, $this->orderMailHtml($order,
'Ihre Bestellung'),
'html');
1472 if ($this->settingsBool(
$cfg,
'mail_admin_enabled',
false)) {
1473 $to = trim((
string)(
$cfg[
'mail_admin_to'] ??
''));
1475 dbx()->send_mail($from, $to,
'[Shop] ' . $subject, $this->orderMailHtml($order,
'Neue Shop-Bestellung'),
'html');
1478 }
catch (\Throwable $e) {
1479 dbx()->sys_msg(
'error',
'dbxShop', (
string)($order[
'id'] ??
''),
'order mail failed', $e->getMessage());
1483 private function sendWithdrawalMails(array $withdrawal): void {
1484 $cfg = $this->shopConfig();
1485 $from = $this->shopMailFrom(
$cfg);
1486 $subject =
'Widerruf ' . (string)($withdrawal[
'order_no'] ??
'');
1487 $html =
'<h1>Widerruf</h1>'
1488 .
'<p>Bestellnummer: <strong>' . $this->h($withdrawal[
'order_no'] ??
'') .
'</strong></p>'
1489 .
'<p>Name: ' . $this->h($withdrawal[
'customer_name'] ??
'') .
'<br>E-Mail: ' . $this->h($withdrawal[
'customer_email'] ??
'') .
'</p>'
1490 .
'<p>' . nl2br($this->h($withdrawal[
'reason'] ??
'')) .
'</p>';
1492 if ($this->settingsBool(
$cfg,
'mail_customer_enabled',
false)) {
1493 $to = trim((
string)($withdrawal[
'customer_email'] ??
''));
1495 dbx()->send_mail($from, $to, $subject, $html,
'html');
1498 if ($this->settingsBool(
$cfg,
'mail_admin_enabled',
false)) {
1499 $to = trim((
string)(
$cfg[
'mail_admin_to'] ??
''));
1501 dbx()->send_mail($from, $to,
'[Shop] ' . $subject, $html,
'html');
1504 }
catch (\Throwable $e) {
1505 dbx()->sys_msg(
'error',
'dbxShop', (
string)($withdrawal[
'id'] ??
''),
'withdrawal mail failed', $e->getMessage());
1509 private function publicOrderCard(array $order): string {
1511 foreach ((array)($order[
'items'] ?? array()) as $item) {
1512 $items .=
'<li><span>' . (int)($item[
'qty'] ?? 0) .
'x ' . $this->h($item[
'title'] ??
'') .
'</span><strong>' . $this->money($item[
'total_gross'] ?? 0) .
'</strong></li>';
1514 if ($items ===
'') {
1515 $items =
'<li><span>Keine Positionen gespeichert</span><strong></strong></li>';
1517 $statusLabels = array(
1519 'payment_pending' =>
'Zahlung offen',
1520 'paid' =>
'Bezahlt',
1521 'processing' =>
'In Bearbeitung',
1522 'shipped' =>
'Versendet',
1523 'done' =>
'Abgeschlossen',
1524 'cancelled' =>
'Storniert',
1526 $shippingLabels = array(
1528 'ready' =>
'Bereit',
1529 'shipped' =>
'Versendet',
1530 'delivered' =>
'Zugestellt',
1531 'returned' =>
'Retoure',
1533 $withdrawalLabels = array(
1535 'processing' =>
'In Bearbeitung',
1536 'accepted' =>
'Angenommen',
1537 'rejected' =>
'Abgelehnt',
1538 'refunded' =>
'Erstattet',
1539 'closed' =>
'Abgeschlossen',
1541 $historyLabels = array(
1542 'created' =>
'Bestellung',
1543 'status' =>
'Bestellstatus',
1544 'payment_status' =>
'Zahlung',
1545 'shipping_status' =>
'Versand',
1546 'invoice_no' =>
'Rechnung',
1547 'tracking_no' =>
'Tracking',
1548 'payment' =>
'Zahlung',
1549 'customer_mail' =>
'Kundenmail',
1550 'withdrawal' =>
'Widerruf',
1551 'withdrawal_status' =>
'Widerrufsstatus',
1552 'stock_release' =>
'Bestand',
1553 'invoice_pdf' =>
'Rechnung',
1555 $invoice = trim((
string)($order[
'invoice_no'] ??
''));
1556 $trackingNo = trim((
string)($order[
'tracking_no'] ??
''));
1557 $trackingUrl = trim((
string)($order[
'tracking_url'] ??
''));
1558 $channel = (string)($order[
'channel_key'] ??
'shop');
1560 $extra .=
'<span>Herkunft: ' . $this->h($this->paymentProviderLabel($channel)) .
'</span>';
1561 if ($invoice !==
'') {
1562 $extra .=
'<span>Rechnung: ' . $this->h($invoice) .
'</span>';
1564 $extra .=
'<span>Versand: ' . $this->h($shippingLabels[(
string)($order[
'shipping_status'] ??
'open')] ?? (
string)($order[
'shipping_status'] ??
'open')) .
'</span>';
1565 if ($trackingNo !==
'') {
1566 $trackingText =
'Tracking: ' . $this->h($trackingNo);
1567 $extra .= $trackingUrl !==
''
1568 ?
'<span><a href="' . $this->h($trackingUrl) .
'" target="_blank" rel="noopener">' . $trackingText .
'</a></span>'
1569 :
'<span>' . $trackingText .
'</span>';
1571 $withdrawalsHtml =
'';
1572 foreach ((array)($order[
'withdrawals'] ?? array()) as $withdrawal) {
1573 $status = (string)($withdrawal[
'status'] ??
'new');
1574 $created = trim((
string)($withdrawal[
'create_date'] ??
''));
1575 $withdrawalsHtml .=
'<li><span><strong>' . $this->h($withdrawalLabels[$status] ?? $status) .
'</strong>' . (
$created !==
'' ?
'<small>' . $this->h(
$created) .
'</small>' :
'') .
'</span></li>';
1577 if ($withdrawalsHtml !==
'') {
1578 $withdrawalsHtml =
'<section class="dbx-shop-public-order-withdrawals"><h4><i class="bi bi-arrow-counterclockwise"></i> Widerrufe</h4><ul>' . $withdrawalsHtml .
'</ul></section>';
1582 foreach ((array)($order[
'history'] ?? array()) as $history) {
1583 if ($historyCount >= 6) {
1586 $type = (string)($history[
'event_type'] ??
'');
1587 $created = trim((
string)($history[
'create_date'] ??
''));
1588 $message = trim((
string)($history[
'message'] ??
''));
1589 $old = trim((
string)($history[
'old_value'] ??
''));
1590 $new = trim((
string)($history[
'new_value'] ??
''));
1591 $detail = $message !==
'' ? $message : trim($old . ($old !==
'' && $new !==
'' ?
' -> ' :
'') . $new);
1592 $historyHtml .=
'<li><span><strong>' . $this->h($historyLabels[$type] ?? $type) .
'</strong>' . ($detail !==
'' ?
'<small>' . $this->h($detail) .
'</small>' :
'') .
'</span>' . (
$created !==
'' ?
'<time>' . $this->h(
$created) .
'</time>' :
'') .
'</li>';
1595 if ($historyHtml !==
'') {
1596 $historyHtml =
'<section class="dbx-shop-public-order-history"><h4><i class="bi bi-clock-history"></i> Verlauf</h4><ol>' . $historyHtml .
'</ol></section>';
1598 $instructions = trim($this->paymentInstructions((
string)($order[
'payment_provider'] ??
''), $order));
1600 $canInvoice = $invoice !==
'' || in_array((
string)($order[
'status'] ??
''), array(
'paid',
'processing',
'shipped',
'done'),
true);
1602 $invoiceLink =
'<a class="btn btn-outline-primary btn-sm" href="?dbx_modul=dbxShop&dbx_run1=invoice_pdf&order_no=' . rawurlencode((
string)($order[
'order_no'] ??
'')) .
'" target="_blank" rel="noopener"><i class="bi bi-file-earmark-pdf"></i> Rechnung</a>';
1604 return '<article class="dbx-shop-public-order">'
1605 .
'<header><div><strong>' . $this->h($order[
'order_no'] ??
'') .
'</strong><small>' . $this->h($order[
'create_date'] ??
'') .
'</small></div><span class="badge text-bg-primary">' . $this->h($statusLabels[(
string)($order[
'status'] ??
'')] ?? ($order[
'status'] ??
'')) .
'</span></header>'
1606 .
'<ul>' . $items .
'</ul>'
1607 .
'<footer><span>Zahlung: ' . $this->h($this->paymentProviderLabel((
string)($order[
'payment_provider'] ??
''))) .
' / ' . $this->h($order[
'payment_status'] ??
'') .
'</span><strong>' . $this->money($order[
'total_gross'] ?? 0) .
'</strong></footer>'
1608 . ($extra !==
'' ?
'<div class="dbx-shop-public-order-extra">' . $extra .
'</div>' :
'')
1609 . ($instructions !==
'' && in_array((
string)($order[
'payment_status'] ??
''), array(
'open',
'created',
'pending'),
true) ?
'<div class="alert alert-info py-2 my-2"><strong>Zahlungshinweis</strong><br>' . nl2br($this->h($instructions)) .
'</div>' :
'')
1612 .
'<div class="dbx-shop-public-order-actions">' . $invoiceLink .
'<a class="btn btn-outline-secondary btn-sm" href="?dbx_modul=dbxShop&dbx_run1=withdrawal"><i class="bi bi-arrow-counterclockwise"></i> Widerruf</a></div>'
1616 private function startPayPalForOrder(array $order): string {
1617 $returnUrl = $this->absoluteShopUrl(
'paypal_return', array(
'order_no' => (string)$order[
'order_no']));
1618 $cancelUrl = $this->absoluteShopUrl(
'paypal_cancel', array(
'order_no' => (
string)$order[
'order_no']));
1619 $paypalOrder = $this->paypal()->createOrder($order, $returnUrl, $cancelUrl);
1620 $paypalId = (string)($paypalOrder[
'id'] ??
'');
1621 $this->repo()->updateOrderPayment((
int)$order[
'id'],
'paypal',
'created', $paypalId, $paypalOrder);
1622 $approvalUrl = $this->paypal()->approvalUrl($paypalOrder);
1623 if ($approvalUrl ===
'') {
1624 throw new \RuntimeException(
'PayPal hat keinen Freigabe-Link geliefert.');
1626 if (!headers_sent()) {
1627 header(
'Location: ' . $approvalUrl,
true, 302);
1630 return '<a class="btn btn-primary" href="' . $this->h($approvalUrl) .
'">Weiter zu PayPal</a>';
1633 private function startAmazonPayForOrder(array $order): string {
1634 $returnUrl = $this->absoluteShopUrl(
'amazon_pay_return', array(
'order_no' => (string)$order[
'order_no']));
1635 $cancelUrl = $this->absoluteShopUrl(
'amazon_pay_cancel', array(
'order_no' => (
string)$order[
'order_no']));
1636 $checkoutSession = $this->amazonPay()->createCheckoutSession($order, $returnUrl, $cancelUrl);
1637 $checkoutSessionId = (string)($checkoutSession[
'checkoutSessionId'] ?? $checkoutSession[
'id'] ??
'');
1638 $this->repo()->updateOrderPayment((
int)$order[
'id'],
'amazon_pay',
'created', $checkoutSessionId, $checkoutSession);
1639 $redirectUrl = $this->amazonPay()->redirectUrl($checkoutSession);
1640 if ($redirectUrl ===
'') {
1641 throw new \RuntimeException(
'Amazon Pay hat keinen Redirect-Link geliefert.');
1643 if (!headers_sent()) {
1644 header(
'Location: ' . $redirectUrl,
true, 302);
1647 return '<a class="btn btn-primary" href="' . $this->h($redirectUrl) .
'">Weiter zu Amazon Pay</a>';
1651 $this->ensureSeed();
1652 $channel = $this->activeChannel();
1653 $query = trim((
string)($_GET[
'q'] ??
''));
1654 $groupId = $this->catalogGroupId();
1655 $attributeFilters = $this->selectedAttributeFilters();
1657 $hasQuery = $this->searchTerms($query) !== array();
1658 $currentGroup = $groupId > 0 ? $this->repo()->groupById($groupId) :
null;
1659 if ($groupId > 0 && !is_array($currentGroup)) {
1663 foreach ($this->repo()->products(
true) as $product) {
1664 if (!$this->productHasChannel($product, $channel)) {
1667 if (!$this->productInCatalogGroup($product, $groupId)) {
1670 $score = $this->productSearchScore($product, $query);
1671 if ($score <= 0 || !$this->productMatchesAttributeFilters($product, $attributeFilters)) {
1675 $sku = (string)($product[
'sku'] ??
'');
1680 'product' => $product,
1682 'sorter' => (
int)($product[
'sorter'] ?? 100),
1683 'title' => (
string)($product[
'title'] ??
''),
1687 if ($hasQuery && count($matches) > 1) {
1688 usort($matches,
static function(array $a, array $b):
int {
1689 if ($a[
'score'] !== $b[
'score']) {
1690 return $b[
'score'] <=> $a[
'score'];
1692 if ($a[
'sorter'] !== $b[
'sorter']) {
1693 return $a[
'sorter'] <=> $b[
'sorter'];
1695 return strcasecmp($a[
'title'], $b[
'title']);
1699 $products = array_map(
static fn($match) => $match[
'product'], $matches);
1700 $reportHtml = $products === array()
1701 ? $this->placeholder(
'Keine Artikel gefunden',
'Fuer Ihre Suche wurden aktuell keine aktiven Shop-Artikel gefunden.')
1702 : $this->catalogReportHtml($products, $channel, $query, $attributeFilters, $groupId);
1704 $isPaginationAjax = (int)
dbx()->get_system_var(
'dbx_ajax', 0,
'int') === 1
1705 && (array_key_exists(
'dbx_rpos', $_GET) || array_key_exists(
'dbx_rrows', $_GET));
1706 if ($isPaginationAjax) {
1710 $navigation = $this->catalogGroupBreadcrumb($groupId) . $this->catalogGroupNavigation($groupId);
1711 if ($navigation ===
'') {
1712 $navigation = $this->catalogGroupNavigation(0);
1714 $title = is_array($currentGroup) ? (string)($currentGroup[
'title'] ??
'Shop') :
'Shop';
1715 $subtitle = is_array($currentGroup) ?
'Artikelgruppe und passende Artikel.' :
'Artikel, Merchandise, Dienstleistungen und digitale Pakete.';
1720 $this->demoShopNoticeHtml(
'dbx-shop-demo-catalog-notice',
'dbx-shop-demo-alert-catalog')
1722 . $this->catalogFiltersHtml($channel, $query, $attributeFilters, $groupId)
1729 $this->ensureSeed();
1730 $channel = $this->activeChannel();
1731 $sku =
dbx()->get_modul_var(
'sku',
'',
'parameter');
1732 $product = $this->repo()->productBySku((
string) $sku);
1734 if (!$product || !$this->productHasChannel($product, $channel)) {
1737 'Produkt wurde nicht gefunden oder ist fuer diesen Channel nicht aktiv.',
1738 $this->placeholder(
'Produkt nicht gefunden',
'Der angeforderte Artikel existiert nicht oder ist fuer den gewaehlten Channel nicht freigegeben.'),
1743 $body = $this->renderProductDetail($product, $channel);
1746 $product[
'title'] ??
'Produkt',
1747 $product[
'summary'] ??
'Artikel',
1754 $this->ensureSeed();
1755 $channel = $this->activeChannel();
1756 $ajax = (int)
dbx()->get_system_var(
'dbx_ajax', 0,
'int') === 1;
1757 $addSku = (string)
dbx()->get_modul_var(
'sku',
'',
'parameter');
1758 $removeSku = (string)
dbx()->get_modul_var(
'remove',
'',
'parameter');
1759 if ((
string)
dbx()->get_modul_var(
'clear',
'',
'parameter') ===
'1') {
1760 $this->startSession();
1761 $_SESSION[
'dbxShop_cart'] = array();
1762 } elseif ($removeSku !==
'') {
1763 $this->removeFromCart($removeSku);
1764 } elseif (isset($_POST[
'shop_cart_update']) && is_array($_POST[
'qty'] ??
null)) {
1765 $this->updateCartQuantities($_POST[
'qty']);
1766 } elseif ($addSku !==
'') {
1767 $qty = $this->requestedQuantity(
dbx()->get_modul_var(
'qty',
'1',
'parameter'));
1768 $this->addToCart($addSku, $qty);
1769 $product = $this->repo()->productBySku($addSku);
1773 'Der Artikel wurde hinzugefuegt.',
1774 $this->addedToCartDialog($product),
1780 $body = $this->cartBodyHtml();
1787 'Artikel, Mengen, MwSt.-Grundlage und Versandkosten.',
1794 $this->ensureSeed();
1795 [$rows, $sum] = $this->cartRowsAndSum();
1799 'Erst Artikel auswaehlen, dann bezahlen.',
1800 $this->placeholder(
'Der Warenkorb ist leer',
'Waehlen Sie im Katalog einen Artikel und legen Sie ihn in den Warenkorb.'),
1804 $cfg = $this->shopConfig();
1805 if (!$this->settingsBool(
$cfg,
'checkout_guest_allowed',
true) && (
int)
dbx()->user() <= 0) {
1808 'Anmeldung erforderlich.',
1809 '<div class="alert alert-warning m-3"><strong>Anmeldung erforderlich.</strong><br>Gastbestellungen sind aktuell deaktiviert. Bitte melden Sie sich an, bevor Sie die Bestellung abschliessen.</div>',
1815 $form =
dbx()->get_system_obj(
'dbxForm');
1816 $form->init(
'shop-checkout-form',
'shop-checkout-form');
1817 $form->_fd =
'dbxShop|checkout';
1818 $form->_action =
'?dbx_modul=dbxShop&dbx_run1=checkout';
1819 $form->_data = array(
1820 'customer_name' => (
string)($_POST[
'customer_name'] ??
''),
1821 'customer_email' => (
string)($_POST[
'customer_email'] ??
''),
1822 'customer_phone' => (
string)($_POST[
'customer_phone'] ??
''),
1823 'shipping_address' => (
string)($_POST[
'shipping_address'] ??
''),
1824 'note' => (
string)($_POST[
'note'] ??
''),
1825 'payment_method' => (
string)($_POST[
'payment_method'] ?? array_key_first(
$paymentOptions)),
1826 'accept_legal' => !empty($_POST[
'accept_legal']) ? 1 : 0,
1827 'accept_withdrawal' => !empty($_POST[
'accept_withdrawal']) ? 1 : 0,
1829 $form->_msg_info =
'Bitte Kundendaten, Zahlungsart und Rechtstext-Bestaetigung pruefen.';
1831 $form->add_obj(
'checkout_cart',
'obj-value', $this->checkoutTableHtml($rows, $sum));
1832 $form->add_rep(
'payment_help', $this->checkoutPaymentHelp(
$paymentOptions));
1833 $form->add_rep(
'demo_shop_notice', $this->demoShopNoticeHtml(
'dbx-shop-demo-notice'));
1834 $form->add_rep(
'checkout_error',
'');
1836 $submitted = (
$_SERVER[
'REQUEST_METHOD'] ??
'') ===
'POST' && isset($_POST[
'shop_checkout_submit']);
1839 $validLegal = !empty($_POST[
'accept_legal']) && !empty($_POST[
'accept_withdrawal']);
1840 $paymentMethod = (string)($_POST[
'payment_method'] ??
'');
1841 $customerName = trim((
string)($_POST[
'customer_name'] ??
''));
1842 $customerEmail = trim((
string)($_POST[
'customer_email'] ??
''));
1843 $shippingAddress = trim((
string)($_POST[
'shipping_address'] ??
''));
1844 $checkoutError =
'';
1846 $checkoutError =
'Es ist keine Zahlungsart verfuegbar.';
1847 } elseif ($customerName ===
'' || strlen($customerName) < 2) {
1848 $checkoutError =
'Bitte den Namen eintragen.';
1849 } elseif (!filter_var($customerEmail, FILTER_VALIDATE_EMAIL)) {
1850 $checkoutError =
'Bitte eine gueltige E-Mail-Adresse eintragen.';
1851 } elseif ($shippingAddress ===
'' || strlen($shippingAddress) < 8) {
1852 $checkoutError =
'Bitte die Lieferadresse eintragen.';
1854 $checkoutError =
'Bitte eine verfuegbare Zahlungsart waehlen.';
1855 } elseif (!$validLegal) {
1856 $checkoutError =
'Bitte Rechtstexte und Widerrufsbelehrung bestaetigen.';
1859 if ($checkoutError !==
'') {
1860 $form->add_rep(
'checkout_error',
'<div class="alert alert-warning mb-0">' . $this->h($checkoutError) .
'</div>');
1863 [$legalSnapshot, $withdrawalSnapshot] = $this->legalSnapshotsForOrder();
1864 $order = $this->repo()->createOrderFromItems(
1866 $this->activeChannel(),
1869 trim((
string)($_POST[
'note'] ??
'')),
1871 in_array($paymentMethod, array(
'paypal',
'amazon_pay'),
true) ?
'created' :
'open',
1873 trim((
string)($_POST[
'customer_phone'] ??
'')),
1879 $form->add_rep(
'checkout_error',
'<div class="alert alert-danger mb-0">Die Bestellung konnte nicht erstellt werden.</div>');
1880 } elseif ($paymentMethod ===
'paypal') {
1881 return $this->startPayPalForOrder($order);
1882 } elseif ($paymentMethod ===
'amazon_pay') {
1883 return $this->startAmazonPayForOrder($order);
1885 $this->sendOrderMails($order);
1886 $this->startSession();
1887 $_SESSION[
'dbxShop_cart'] = array();
1888 return $this->orderSuccessPage($order, $paymentMethod);
1890 }
catch (\Throwable $e) {
1891 $form->add_rep(
'checkout_error',
'<div class="alert alert-danger mb-0">' . $this->h($e->getMessage()) .
'</div>');
1898 'Kundendaten pruefen und den Demo-Bestellablauf testen.',
1905 $this->ensureSeed();
1910 $this->ensureSeed();
1911 $paypalOrderId = (string)
dbx()->get_modul_var(
'token',
'',
'parameter');
1912 $orderNo = (string)
dbx()->get_modul_var(
'order_no',
'',
'parameter');
1913 $order = $orderNo !==
'' ? $this->repo()->orderByNo($orderNo) :
null;
1914 if (!$order && $paypalOrderId !==
'') {
1915 $order = $this->repo()->orderByPaymentReference(
'paypal', $paypalOrderId);
1917 if (!$order || $paypalOrderId ===
'') {
1918 return $this->page(
'PayPal',
'Rueckkehr konnte nicht zugeordnet werden.',
'<div class="alert alert-danger m-3">Die PayPal-Rueckkehr konnte keiner Bestellung zugeordnet werden.</div>',
'checkout');
1922 $capture = $this->paypal()->capture($paypalOrderId);
1923 $this->repo()->updateOrderPayment((
int)$order[
'id'],
'paypal',
'completed', $paypalOrderId, $capture);
1924 $freshOrder = $this->repo()->orderById((
int)$order[
'id']) ?: $order;
1925 $this->sendOrderMails($freshOrder);
1926 $this->startSession();
1927 $_SESSION[
'dbxShop_cart'] = array();
1928 $_SESSION[
'dbxShop_last_order_no'] = (string)($order[
'order_no'] ??
'');
1929 $body =
'<div class="alert alert-success m-3"><strong>Zahlung abgeschlossen.</strong><br>Bestellung ' . $this->h($order[
'order_no'] ??
'') .
' wurde bezahlt.</div>';
1930 return $this->page(
'Danke',
'PayPal-Zahlung wurde bestaetigt.', $body,
'orders');
1931 }
catch (\Throwable $e) {
1932 $this->repo()->updateOrderPayment((
int)$order[
'id'],
'paypal',
'failed', $paypalOrderId, array(
'error' => $e->getMessage()));
1933 return $this->page(
'PayPal',
'Zahlung konnte nicht bestaetigt werden.',
'<div class="alert alert-danger m-3">' . $this->h($e->getMessage()) .
'</div>',
'checkout');
1938 $orderNo = (string)
dbx()->get_modul_var(
'order_no',
'',
'parameter');
1939 $order = $orderNo !==
'' ? $this->repo()->orderByNo($orderNo) :
null;
1941 $this->repo()->updateOrderPayment((
int)$order[
'id'],
'paypal',
'cancelled', (
string)($order[
'payment_reference'] ??
''), array(
'cancelled' =>
true));
1944 'PayPal abgebrochen',
1945 'Die Zahlung wurde nicht ausgefuehrt.',
1946 '<div class="alert alert-info m-3">Die PayPal-Zahlung wurde abgebrochen. Der Warenkorb bleibt erhalten.</div>',
1952 $this->ensureSeed();
1953 $checkoutSessionId = (string)
dbx()->get_modul_var(
'checkoutSessionId',
'',
'parameter');
1954 if ($checkoutSessionId ===
'') {
1955 $checkoutSessionId = (string)
dbx()->get_modul_var(
'amazonCheckoutSessionId',
'',
'parameter');
1957 $orderNo = (string)
dbx()->get_modul_var(
'order_no',
'',
'parameter');
1958 $order = $orderNo !==
'' ? $this->repo()->orderByNo($orderNo) :
null;
1959 if (!$order && $checkoutSessionId !==
'') {
1960 $order = $this->repo()->orderByPaymentReference(
'amazon_pay', $checkoutSessionId);
1962 if (!$order || $checkoutSessionId ===
'') {
1963 return $this->page(
'Amazon Pay',
'Rueckkehr konnte nicht zugeordnet werden.',
'<div class="alert alert-danger m-3">Die Amazon-Pay-Rueckkehr konnte keiner Bestellung zugeordnet werden.</div>',
'checkout');
1967 $result = $this->amazonPay()->completeCheckoutSession($checkoutSessionId, $order);
1968 $httpStatus = (int)(
$result[
'_http_status'] ?? 0);
1969 $state = strtolower((
string)(
$result[
'statusDetails'][
'state'] ??
''));
1970 $paymentStatus = ($httpStatus === 202 || $state ===
'authorizationinitiated') ?
'pending' :
'completed';
1971 $this->repo()->updateOrderPayment((
int)$order[
'id'],
'amazon_pay', $paymentStatus, $checkoutSessionId,
$result);
1972 $freshOrder = $this->repo()->orderById((
int)$order[
'id']) ?: $order;
1973 $this->sendOrderMails($freshOrder);
1974 $this->startSession();
1975 $_SESSION[
'dbxShop_cart'] = array();
1976 $_SESSION[
'dbxShop_last_order_no'] = (string)($order[
'order_no'] ??
'');
1977 $body =
'<div class="alert alert-success m-3"><strong>Amazon-Pay-Zahlung verarbeitet.</strong><br>Bestellung ' . $this->h($order[
'order_no'] ??
'') .
' wurde aktualisiert.</div>';
1978 return $this->page(
'Danke',
'Amazon-Pay-Zahlung wurde bestaetigt.', $body,
'orders');
1979 }
catch (\Throwable $e) {
1980 $this->repo()->updateOrderPayment((
int)$order[
'id'],
'amazon_pay',
'failed', $checkoutSessionId, array(
'error' => $e->getMessage()));
1981 return $this->page(
'Amazon Pay',
'Zahlung konnte nicht bestaetigt werden.',
'<div class="alert alert-danger m-3">' . $this->h($e->getMessage()) .
'</div>',
'checkout');
1986 $orderNo = (string)
dbx()->get_modul_var(
'order_no',
'',
'parameter');
1987 $order = $orderNo !==
'' ? $this->repo()->orderByNo($orderNo) :
null;
1989 $this->repo()->updateOrderPayment((
int)$order[
'id'],
'amazon_pay',
'cancelled', (
string)($order[
'payment_reference'] ??
''), array(
'cancelled' =>
true));
1992 'Amazon Pay abgebrochen',
1993 'Die Zahlung wurde nicht ausgefuehrt.',
1994 '<div class="alert alert-info m-3">Die Amazon-Pay-Zahlung wurde abgebrochen. Der Warenkorb bleibt erhalten.</div>',
2000 $this->ensureSeed();
2001 $this->startSession();
2004 $lastOrderNo = (string)($_SESSION[
'dbxShop_last_order_no'] ??
'');
2005 if ($lastOrderNo !==
'') {
2006 $order = $this->repo()->orderByNo($lastOrderNo);
2007 if (is_array($order)) {
2008 $cards .= $this->publicOrderCard($order);
2009 $seen[(string)($order[
'order_no'] ??
'')] =
true;
2013 $uid = function_exists(
'dbx') ? (int)
dbx()->user() : 0;
2014 foreach ($this->repo()->ordersByUid($uid, 25) as $order) {
2015 $orderNo = (string)($order[
'order_no'] ??
'');
2016 if ($orderNo !==
'' && isset($seen[$orderNo])) {
2019 $cards .= $this->publicOrderCard($order);
2022 if ($cards ===
'') {
2023 $cards = $this->placeholder(
2024 'Noch keine Bestellungen',
2025 'Nach einer Bestellung wird hier die letzte Bestellung angezeigt. Angemeldete Benutzer sehen hier ihre gespeicherten Bestellungen.'
2028 $cards =
'<section class="dbx-shop-public-orders">' . $cards .
'</section>';
2033 'Ihre gespeicherten Shop-Bestellungen.',
2039 private function orderIsPublicAccessible(array $order): bool {
2040 $this->startSession();
2041 $orderNo = (string)($order[
'order_no'] ??
'');
2042 if ($orderNo !==
'' && $orderNo === (
string)($_SESSION[
'dbxShop_last_order_no'] ??
'')) {
2045 $uid = function_exists(
'dbx') ? (int)
dbx()->user() : 0;
2046 return $uid > 0 && (int)($order[
'uid'] ?? 0) === $uid;
2050 $this->ensureSeed();
2051 $this->startSession();
2052 $orderNo = (string)
dbx()->get_modul_var(
'order_no',
'',
'parameter');
2053 $order = $orderNo !==
'' ? $this->repo()->orderByNo($orderNo) :
null;
2054 if (!is_array($order) || !$this->orderIsPublicAccessible($order)) {
2055 return $this->page(
'Rechnung',
'Zugriff nicht moeglich.',
'<div class="alert alert-warning m-3">Die Rechnung wurde nicht gefunden oder ist fuer diesen Benutzer nicht freigegeben.</div>',
'orders');
2057 $order = $this->repo()->ensureOrderInvoicePdf((
int)$order[
'id']);
2058 if (!is_array($order)) {
2059 return $this->page(
'Rechnung',
'PDF konnte nicht erzeugt werden.',
'<div class="alert alert-danger m-3">Die Rechnungsdatei konnte nicht erzeugt werden.</div>',
'orders');
2061 $file = $this->repo()->invoicePdfAbsolutePath($order);
2063 return $this->page(
'Rechnung',
'PDF konnte nicht geladen werden.',
'<div class="alert alert-danger m-3">Die Rechnungsdatei ist nicht verfuegbar.</div>',
'orders');
2065 if (!headers_sent()) {
2066 header(
'Content-Type: application/pdf');
2067 header(
'Content-Disposition: inline; filename="' . basename($file) .
'"');
2068 header(
'Content-Length: ' . filesize($file));
2074 private function jsonResponse(array $data,
int $status = 200): string {
2075 if (!headers_sent()) {
2076 http_response_code($status);
2077 header(
'Content-Type: application/json; charset=utf-8');
2079 return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?:
'{}';
2083 $channelKey = (string)
dbx()->get_modul_var(
'channel',
'',
'parameter');
2084 $raw = (string)file_get_contents(
'php://input');
2085 $payload = json_decode($raw,
true);
2086 if (!is_array($payload)) {
2091 $channel = $this->repo()->channelByKey($channelKey);
2093 return $this->jsonResponse(array(
'ok' =>
false,
'message' =>
'Channel nicht gefunden.'), 404);
2095 if ((
int)($channel[
'active'] ?? 0) !== 1 || (
int)($channel[
'order_import_enabled'] ?? 0) !== 1) {
2096 return $this->jsonResponse(array(
'ok' =>
false,
'message' =>
'Order-Import fuer diesen Channel ist nicht aktiv.'), 403);
2099 $secret = (string)($channel[
'webhook_secret'] ??
'');
2100 if ($secret !==
'') {
2101 $given = (string)(
$_SERVER[
'HTTP_X_DBX_SHOP_SECRET'] ??
$_SERVER[
'HTTP_X_CHANNEL_SECRET'] ?? $_GET[
'secret'] ?? $_POST[
'secret'] ?? $payload[
'secret'] ??
'');
2102 if ($given ===
'' || !hash_equals($secret, $given)) {
2103 return $this->jsonResponse(array(
'ok' =>
false,
'message' =>
'Webhook-Secret ungueltig.'), 403);
2107 $connector =
dbx()->get_include_obj(
'dbxShopChannelConnector',
'dbxShop');
2108 if (is_object($connector) && method_exists($connector,
'normalizeWebhookPayload')) {
2109 $payload = (array)$connector->normalizeWebhookPayload($channel, $payload);
2112 $order = $this->repo()->importChannelOrder($channelKey, $payload);
2113 return $this->jsonResponse(array(
2115 'order_no' => (
string)($order[
'order_no'] ??
''),
2116 'channel' => $channelKey,
2118 }
catch (\Throwable $e) {
2119 return $this->jsonResponse(array(
'ok' =>
false,
'message' => $e->getMessage()), 400);
2124 return $this->renderCmsShopPage(
2127 'AGB, Anbieterkennzeichnung, Zahlung, Versand und Datenschutz-Hinweise.',
2134 $cid = (int)($pages[
'withdrawal'] ?? 0);
2137 $renderer =
dbx()->get_include_obj(
'dbxContentRenderer',
'dbxContent');
2138 $body = is_object($renderer) ? (string)$renderer->renderStatic($cid, array(
'template' =>
'c-body1-footer')) :
'';
2140 if (trim($body) ===
'') {
2141 $body = $this->placeholder(
'Widerruf',
'Die CMS-Seite ist leer.');
2145 'Widerrufsbelehrung und Widerruf direkt senden.',
2146 '<div class="dbx-shop-cms-page">' . $body .
'</div>' . $this->withdrawalFormHtml(),