12 private const ACTION_TOKEN_SCOPE =
'dbxShop_admin.actions';
18 private bool $maintenanceMode =
false;
27 private string $postedFormError =
'';
28 private $catalogTexts =
null;
37 private function catalogTexts() {
38 if ($this->catalogTexts) {
39 return $this->catalogTexts;
41 dbx()->get_system_obj(
'dbxForm',
'use');
43 $texts->init(
'shop-catalog-texts');
44 $texts->_fd =
'dbxShop_admin|shop-catalog';
45 $texts->load_fd_messages();
46 $texts->set_form_help_enabled(
false);
47 $this->catalogTexts =
$texts;
48 return $this->catalogTexts;
51 private function tpl() {
52 return dbx()->get_system_obj(
'dbxTPL');
55 private function db() {
56 return dbx()->get_system_obj(
'dbxDB');
59 private function h(
$value):
string {
60 return htmlspecialchars((
string)
$value, ENT_QUOTES,
'UTF-8');
69 private function actionUrl(
string $url):
string {
70 $securedUrl =
dbx()->action_url(
$url);
71 if ($securedUrl !==
$url) {
75 $separator = strpos(
$url,
'?') ===
false ?
'?' :
'&';
76 return $url . $separator .
'dbx_token=' . rawurlencode(
dbx()->
action_token(self::ACTION_TOKEN_SCOPE));
82 private function checkActionToken(
string $action):
bool {
83 $token = (string)
dbx()->get_modul_var(
'dbx_token',
'',
'varchar|max=128');
88 $this->postedFormError = $this->catalogTexts()->get_fd_message(
'security_token_error');
93 'Shop-Admin-Aktion ohne gueltigen Token abgewiesen',
94 'ip=' . (
string)(
$_SERVER[
'REMOTE_ADDR'] ??
'')
99 private function openWinButton(
string $url,
string $title,
string $content,
string $class =
'btn btn-outline-primary',
string $width =
'88%',
string $height =
'88%'):
string {
100 $escUrl = $this->h(
$url);
101 $escTitle = $this->h($title);
103 if (strpos(
' ' .
$class .
' ',
' openWin ') ===
false) {
106 if (strpos(
' ' .
$class .
' ',
' dbx-win ') ===
false) {
109 if (strpos(
$content,
'bi-question-circle') !==
false && strpos(
' ' .
$class .
' ',
' dbx-help-action ') ===
false) {
110 $class .=
' dbx-help-action';
112 return '<a class="' . $this->h(
$class) .
'" href="' . $escUrl .
'" data-url="' . $escUrl .
'" data-title="' . $escTitle .
'" data-width="' . $this->h($width) .
'" data-height="' . $this->h($height) .
'" title="' . $escTitle .
'" role="button">' .
$content .
'</a>';
115 private function helpButton(
int $helpId,
string $title,
string $class =
'btn btn-outline-secondary btn-sm me-1',
string $width =
'72%',
string $height =
'82%'):
string {
119 return $this->openWinButton(
120 '?dbx_modul=dbxContent&dbx_run1=content&cid=' . $helpId,
122 '<i class="bi bi-question-circle"></i><span class="visually-hidden"> Hilfe</span>',
129 private function money(
$value):
string {
130 return number_format((
float)
$value, 2,
',',
'.') .
' EUR';
133 private function mediaUrl(
string $path):
string {
134 $path = trim(str_replace(
'\\',
'/', $path));
135 if ($path ===
'')
return '';
136 if (preg_match(
'~^https?://~i', $path) || substr($path, 0, 1) ===
'/')
return $path;
137 return dbx()->get_base_url() . ltrim($path,
'/');
140 private function mediaItemUrl(array $image,
bool $thumb =
true):
string {
141 $mediaId = (int)($image[
'media_id'] ?? 0);
143 $url =
'index.php?dbx_modul=dbxContent&dbx_run1=media&dbx_mid=' .
$mediaId;
145 $url .=
'&dbx_thumb=1';
149 return $this->mediaUrl((
string)($image[
'image_path'] ??
''));
152 private function repo() {
153 return dbx()->get_include_obj(
'dbxShopRepository',
'dbxShop');
156 private function ensureSeed():
void {
159 $this->repo()->install();
162 private function shopMediaUsageContentId():
int {
163 $configured = (int)
dbx()->get_config(
'dbxShop',
'media_usage_content_id');
164 if ($configured > 0 && $this->contentPageExists($configured, $this->shopMediaUsageContentDd())) {
168 return $this->ensureShopMediaUsagePage();
171 private function contentDd():
string {
172 return function_exists(
'dbx_lng_name') ?
dbx_lng_name(
'content') :
'content_de';
175 private function shopMediaUsageLng():
string {
176 return dbxContentMediaUsageScope::language(dbxContentLngSync::masterLng());
179 private function shopMediaUsageContentDd():
string {
180 return dbxContentLng::ddContent($this->shopMediaUsageLng());
183 private function shopMediaUsageFolderDd():
string {
184 return dbxContentLng::ddFolder($this->shopMediaUsageLng());
187 private function folderDd():
string {
188 return function_exists(
'dbx_lng_name') ?
dbx_lng_name(
'content_folder') :
'content_folder_de';
191 private function contentPageExists(
int $contentId,
string $dd =
''):
bool {
192 if ($contentId <= 0) {
196 $row = $this->db()->select1(
$dd !==
'' ?
$dd : $this->contentDd(), $contentId,
'id', 0);
197 return is_array($row) && (int)($row[
'id'] ?? 0) === $contentId;
198 }
catch (\Throwable $e) {
203 private function ensureShopHelpFolder():
int {
206 $row =
$db->select1(
$folderDd, array(
'name' =>
'shop',
'parent_id' => 0),
'id', 0);
207 if (is_array($row) && (
int)($row[
'id'] ?? 0) > 0) {
208 return (
int)$row[
'id'];
216 'group_read' =>
'admin',
217 'template' =>
'c-content',
218 'hero_template' =>
'image-hero',
219 'hero_image_id' =>
'',
220 'hero_margin_top' =>
'',
222 'hero_variant' =>
'',
224 'hero_scroll_layer' =>
'',
226 if ($folderOk === 1) {
227 $folderId = (int)
$db->get_insert_id();
236 private function channelGroupsHelpHtml():
string {
237 return '<section class="dbx-shop-help">'
238 .
'<h2>Channel-Gruppen</h2>'
239 .
'<p>Channel-Gruppen buendeln die Verkaufskanaele, auf denen ein Shop-Artikel angeboten werden soll. Sie werden spaeter Artikeln zugeordnet und steuern damit, ob ein Artikel nur im Shop, auf Marktplatzkanaelen oder in mehreren Kanaelen gefuehrt wird.</p>'
240 .
'<h3>Eingabefelder</h3>'
242 .
'<dt>Key</dt><dd>Eindeutiger technischer Schluessel der Channel-Gruppe. Verwenden Sie kurze, stabile Werte wie <code>software-shop</code> oder <code>merch-marketplaces</code>. Der Key wird beim Neuanlegen gespeichert und danach nicht mehr direkt in der Liste geaendert.</dd>'
243 .
'<dt>Channel-Gruppe</dt><dd>Sichtbarer Name fuer die Verwaltung. Dieser Name sollte knapp beschreiben, fuer welche Artikel oder Verkaufssituation die Gruppe gedacht ist.</dd>'
244 .
'<dt>Beschreibung</dt><dd>Interne Erklaerung der Gruppe. Tragen Sie ein, welche Artikel in diese Gruppe gehoeren und warum die ausgewaehlten Channels passen.</dd>'
245 .
'<dt>Channels</dt><dd>Auswahl der aktiven Verkaufskanaele innerhalb dieser Gruppe. Aktivierte Checkboxen bedeuten: Artikel mit dieser Gruppe duerfen auf diesen Kanaelen gefuehrt oder exportiert werden.</dd>'
246 .
'<dt>Sort</dt><dd>Reihenfolge in Auswahllisten und Tabellen. Kleine Zahlen stehen weiter oben. Nutzen Sie Abstaende wie 10, 20, 30, damit spaeter Gruppen dazwischen einsortiert werden koennen.</dd>'
247 .
'<dt>Aktiv</dt><dd>Schaltet die Channel-Gruppe fuer die Nutzung ein oder aus. Inaktive Gruppen bleiben erhalten, werden aber nicht als aktive Option behandelt.</dd>'
249 .
'<h3>Aktionen</h3>'
251 .
'<li><strong>Plus in der Bar:</strong> fuegt oben eine leere Eingabezeile fuer eine neue Channel-Gruppe ein.</li>'
252 .
'<li><strong>Speichern:</strong> uebernimmt Name, Beschreibung, Channel-Auswahl, Sortierung und Aktiv-Status.</li>'
253 .
'<li><strong>Loeschen:</strong> verschiebt die Channel-Gruppe in den Papierkorb und deaktiviert ihre Channel-Zuordnungen. Bestehende Produktdaten bleiben dadurch nachvollziehbar, die Gruppe erscheint aber nicht mehr in der aktiven Liste.</li>'
255 .
'<p><strong>Praxis:</strong> Legen Sie Gruppen nach Vertriebsszenarien an, nicht nach einzelnen Artikeln. Beispiele sind <code>software-shop</code>, <code>digital-marketplaces</code> oder <code>service-local</code>.</p>'
259 private function channelsHelpHtml():
string {
260 return '<section class="dbx-shop-help">'
261 .
'<h2>Channels</h2>'
262 .
'<p>Channels beschreiben einzelne Verkaufskanaele wie Shop, eBay, Amazon, Kleinanzeigen oder eigene Marktplatz-Anbindungen. Ein aktiver Channel kann fuer Artikel-Export, Listing-Pflege und spaeter fuer Bestellrueckmeldungen genutzt werden.</p>'
263 .
'<h3>Eingabefelder</h3>'
265 .
'<dt>Key</dt><dd>Eindeutiger technischer Schluessel des Channels, zum Beispiel <code>ebay</code> oder <code>amazon</code>. Der Key wird beim Neuanlegen vergeben und von Artikelzuordnungen und Bestellungen referenziert.</dd>'
266 .
'<dt>Channel</dt><dd>Sichtbarer Name in Verwaltung, Artikelliste und Channel-Gruppen.</dd>'
267 .
'<dt>Beschreibung</dt><dd>Interne Notiz, wofuer der Channel genutzt wird und welche Besonderheiten bei Angeboten gelten.</dd>'
268 .
'<dt>Plattform</dt><dd>Typ der Zielplattform. Vordefinierte Werte helfen spaeter beim Anschluss spezifischer API-Adapter; <code>custom</code> ist fuer eigene Integrationen gedacht.</dd>'
269 .
'<dt>Verbindung</dt><dd>Legt fest, ob der Channel intern, manuell, per API oder per Feed/Webhook betrieben wird.</dd>'
270 .
'<dt>Export</dt><dd>Erlaubt, Artikel aus dem Shop fuer diesen Channel vorzubereiten oder spaeter automatisiert als Listing zu uebertragen.</dd>'
271 .
'<dt>Order-Import</dt><dd>Erlaubt Bestellrueckmeldungen vom Channel in den Shop. Externe Plattformen benoetigen dafuer API-Zugriff oder einen Webhook.</dd>'
272 .
'<dt>API-Basis-URL</dt><dd>Basisadresse der Channel-API. Der Verbindungstest nutzt je nach Plattform den passenden Connector, z.B. eBay OAuth/Sell API, Amazon LWA/SP-API oder mobile.de Basic Auth.</dd>'
273 .
'<dt>Client-ID / Benutzer</dt><dd>Oeffentliche Kennung oder Login-Name der Integration.</dd>'
274 .
'<dt>Secret / Token / Passwort</dt><dd>Zugangsdaten fuer API-Aufrufe. Gespeicherte Werte werden im Feld angezeigt und koennen direkt ueberschrieben werden.</dd>'
275 .
'<dt>Marketplace-ID</dt><dd>Marktplatzkennung der Plattform, zum Beispiel <code>EBAY_DE</code> fuer eBay Deutschland oder <code>A1PA6795UKMFR9</code> fuer Amazon.de.</dd>'
276 .
'<dt>Seller-ID / Account-ID</dt><dd>Verkaeufer- oder Konto-ID der Plattform. Bei mobile.de ist hier insbesondere die <code>mobileSellerId</code> relevant.</dd>'
277 .
'<dt>Location-Key</dt><dd>Lager-, Standort- oder Versandortkennung. eBay benoetigt einen Merchant Location Key, bevor Angebote veroeffentlicht werden koennen.</dd>'
278 .
'<dt>Kategorie-ID</dt><dd>Standard-Kategorie fuer neue Listings, wenn der Adapter keine automatische Kategorieermittlung nutzt.</dd>'
279 .
'<dt>Payment-, Fulfillment- und Return-Policy</dt><dd>Policy-IDs der Plattform. Bei eBay sind das die Business Policies des Verkaeuferkontos: <strong>Payment Policy</strong> beschreibt Zahlungsarten und Zahlungsbedingungen, <strong>Fulfillment Policy</strong> beschreibt Versandservice, Bearbeitungszeit, Versandkosten und Versandregionen, <strong>Return Policy</strong> beschreibt Ruecknahmefrist, Kostentraeger und Rueckgabebedingungen. eBay-Angebote koennen ohne passende Policy-IDs oft nicht veroeffentlicht werden.</dd>'
280 .
'<dt>Notification-Ziel / Topic</dt><dd>Ziel fuer Rueckmeldungen, zum Beispiel eBay Notification Destination oder Amazon SQS ARN, plus Ereignis wie <code>ORDER_CHANGE</code>.</dd>'
281 .
'<dt>API-Scopes / Rollen</dt><dd>Dokumentation der erforderlichen Berechtigungen, etwa eBay OAuth Scopes oder Amazon SP-API Rollen. In der Maske am besten einen Wert pro Zeile eintragen. Wenn ein Anbieter technisch Leerzeichen erwartet, verbindet der spaetere Adapter diese Zeilen beim API-Aufruf passend.</dd>'
282 .
'<dt>Webhook-Secret</dt><dd>Geheimer Wert zur Pruefung eingehender Rueckmeldungen, zum Beispiel wenn ein Kauf auf eBay oder Amazon im Shop als Bestellung angelegt werden soll.</dd>'
283 .
'<dt>Sort / Aktiv</dt><dd>Sortierung und grundsaetzliche Freigabe des Channels.</dd>'
285 .
'<h3>Aktionen</h3>'
287 .
'<li><strong>Plus in der Bar:</strong> oeffnet ein Formular fuer einen neuen Channel.</li>'
288 .
'<li><strong>Speichern:</strong> uebernimmt Stammdaten, Integrationsmodus und Zugangsdaten.</li>'
289 .
'<li><strong>Test:</strong> prueft provider-spezifisch die Pflichtfelder und, soweit Zugangsdaten vorhanden sind, einen echten API-Zugriff ohne Daten zu veraendern.</li>'
290 .
'<li><strong>Loeschen:</strong> deaktiviert und verschiebt den Channel in den Papierkorb. Artikelzuordnungen werden deaktiviert.</li>'
292 .
'<p><strong>Praxis:</strong> Erst Channel konfigurieren, dann in Channel-Gruppen auswaehlen und anschliessend Artikel diesen Gruppen oder einzelnen Channels zuordnen.</p>'
296 private function channelProviderHelpHtml(
string $platform):
string {
297 $common =
'<h3>Gemeinsame Eingabefelder</h3>'
299 .
'<dt>Key</dt><dd>Technischer Schluessel des Channels. Er wird in Artikelzuordnungen, Channel-Gruppen, Webhook-URLs und Bestellungen gespeichert. Nach dem Anlegen nicht mehr aendern.</dd>'
300 .
'<dt>Channel</dt><dd>Sichtbarer Name in der Verwaltung.</dd>'
301 .
'<dt>Plattform</dt><dd>Legt fest, welcher Adapter oder welche Arbeitsweise spaeter verwendet wird.</dd>'
302 .
'<dt>Verbindung</dt><dd><code>Intern</code> fuer den Shop selbst, <code>Manuell</code> fuer Pflege ohne API, <code>API</code> fuer direkte Schnittstellen, <code>Feed</code> fuer Dateiuebergaben und <code>Webhook</code> fuer Rueckmeldungen.</dd>'
303 .
'<dt>Aktiv</dt><dd>Nur aktive Channels koennen Artikeln zugeordnet und fuer Importe verwendet werden.</dd>'
304 .
'<dt>Export</dt><dd>Erlaubt, Artikel fuer diesen Channel vorzubereiten oder spaeter automatisiert zur Plattform zu uebertragen.</dd>'
305 .
'<dt>Order-Import</dt><dd>Erlaubt, Bestellungen oder Kauf-/Lead-Rueckmeldungen vom Channel in dbxShop zu uebernehmen.</dd>'
306 .
'<dt>API-Basis-URL</dt><dd>Basisadresse der Anbieter-API. Der Verbindungstest nutzt den passenden Connector und macht nur lesende Pruefungen.</dd>'
307 .
'<dt>Client-ID</dt><dd>Oeffentliche App- oder Client-Kennung der Plattform.</dd>'
308 .
'<dt>Client-Secret</dt><dd>Geheimer App-Schluessel. Der gespeicherte Wert wird angezeigt und kann direkt ersetzt werden.</dd>'
309 .
'<dt>Access-Token</dt><dd>Kurzlebiger Zugriffstoken. Wird oft automatisch aus dem Refresh-Token erneuert.</dd>'
310 .
'<dt>Refresh-Token</dt><dd>Langfristiger Token, mit dem neue Access-Tokens erzeugt werden.</dd>'
311 .
'<dt>Benutzer / Passwort</dt><dd>Nur fuer Plattformen mit Basic Auth oder klassischem API-Login verwenden.</dd>'
312 .
'<dt>Marketplace-ID</dt><dd>Marktplatz- oder Laenderkennung, zum Beispiel <code>EBAY_DE</code> oder <code>A1PA6795UKMFR9</code>.</dd>'
313 .
'<dt>Seller-ID</dt><dd>Verkaeuferkennung der Plattform.</dd>'
314 .
'<dt>Account-ID / mobileSellerId</dt><dd>Konto-, Haendler- oder mobile.de Seller-ID.</dd>'
315 .
'<dt>Location-Key</dt><dd>Standort- oder Lagerkennung, relevant fuer eBay-Angebote.</dd>'
316 .
'<dt>Kategorie-ID</dt><dd>Plattformkategorie fuer Listings, falls keine automatische Zuordnung genutzt wird.</dd>'
317 .
'<dt>Payment-, Fulfillment-, Return-Policy</dt><dd>Policy-IDs fuer Zahlungs-, Versand- und Rueckgaberegeln. Fuer eBay sind diese Felder besonders wichtig; Details stehen im eBay-Hilfetext.</dd>'
318 .
'<dt>Notification-Ziel / SQS ARN</dt><dd>Zieladresse fuer Ereignisse: HTTPS-Destination bei eBay oder Amazon SQS ARN bei Amazon.</dd>'
319 .
'<dt>Notification-Topic</dt><dd>Ereignistyp, der abonniert wird, zum Beispiel <code>ORDER_CHANGE</code>.</dd>'
320 .
'<dt>API-Scopes / Rollen</dt><dd>Dokumentation der genehmigten Berechtigungen. Empfehlung fuer die Eingabe: ein Scope oder eine Rolle pro Zeile. eBay verlangt die Scopes im OAuth-Request technisch als leerzeichengetrennte Liste; beim Senden wird das dann URL-encodiert. Amazon verwendet hier eher Rollen/Berechtigungen, keine eBay-artige Scope-Liste.</dd>'
321 .
'<dt>Webhook-Secret</dt><dd>Geheimer Wert, mit dem dbxShop eingehende Channel-Webhooks pruefen kann.</dd>'
322 .
'<dt>dbxShop Webhook-URL</dt><dd>URL, die eine Plattform oder Middleware fuer Rueckmeldungen aufrufen kann.</dd>'
325 $linksTitle =
'<h3>Beantragen / Bearbeiten</h3>';
328 return '<section class="dbx-shop-help">'
329 .
'<h2>Channel-Hilfe: eBay</h2>'
330 .
'<p>eBay ist ein API-faehiger Marktplatz-Channel. Fuer automatische Listings braucht dbxShop OAuth-Zugangsdaten, Marketplace, Location und Business-Policies. Fuer Bestellungen werden Fulfillment API oder Notification API genutzt.</p>'
332 .
'<h3>eBay-spezifisch</h3><ol>'
333 .
'<li>Developer-Konto anlegen und Application Keyset erstellen. Daraus kommen Client-ID und Client-Secret.</li>'
334 .
'<li>OAuth User Consent fuer den eBay-Verkaeufer durchlaufen. Daraus kommt der Refresh-Token.</li>'
335 .
'<li>Scopes fuer Inventory, Fulfillment und Notifications eintragen.</li>'
336 .
'<li>Marketplace-ID setzen, fuer Deutschland meist <code>EBAY_DE</code>.</li>'
337 .
'<li>Merchant Location Key anlegen oder aus der Inventory API uebernehmen.</li>'
338 .
'<li>Payment-, Fulfillment- und Return-Policy in eBay Business Policies pflegen. Die IDs kommen aus dem eBay Seller-Konto oder aus der Account API. Ohne diese IDs kann ein Offer zwar vorbereitet sein, aber haeufig nicht erfolgreich published werden.</li>'
339 .
'<li><strong>Payment Policy ID:</strong> ID der Zahlungsregel, z.B. welche Zahlungsarten erlaubt sind und wann bezahlt werden muss.</li>'
340 .
'<li><strong>Fulfillment Policy ID:</strong> ID der Versandregel, z.B. Versanddienst, Bearbeitungszeit, Kosten, kostenlose Lieferung, internationale Versandzonen.</li>'
341 .
'<li><strong>Return Policy ID:</strong> ID der Rueckgaberegel, z.B. ob Rueckgabe akzeptiert wird, Frist und wer Ruecksendekosten traegt.</li>'
342 .
'<li>Notification Destination auf die dbxShop Webhook-URL setzen und Topic/Subscription fuer Order-Ereignisse konfigurieren.</li>'
344 .
'<h3>eBay Scopes richtig eintragen</h3>'
345 .
'<p>eBay OAuth erwartet Scopes technisch im Parameter <code>scope</code> als durch Leerzeichen getrennte Liste. In diesem Formular ist es uebersichtlicher, jeden Scope in eine eigene Zeile zu schreiben. Ein spaeterer eBay-Adapter kann daraus automatisch die leerzeichengetrennte und URL-encodierte OAuth-Liste bauen.</p>'
347 .
'<pre><code>https://api.ebay.com/oauth/api_scope/sell.inventory https://api.ebay.com/oauth/api_scope/sell.fulfillment https://api.ebay.com/oauth/api_scope/commerce.notification.subscription</code></pre>'
350 .
'<li><a href="https://developer.ebay.com/develop/guides-v2/get-started-with-ebay-apis" target="_blank" rel="noopener">eBay APIs starten und Application Keys</a></li>'
351 .
'<li><a href="https://developer.ebay.com/develop/guides-v2/authorization" target="_blank" rel="noopener">eBay OAuth und Scopes</a></li>'
352 .
'<li><a href="https://developer.ebay.com/api-docs/sell/inventory/overview.html" target="_blank" rel="noopener">eBay Inventory API</a></li>'
353 .
'<li><a href="https://developer.ebay.com/api-docs/sell/fulfillment/overview.html" target="_blank" rel="noopener">eBay Fulfillment API / Orders</a></li>'
354 .
'<li><a href="https://developer.ebay.com/api-docs/commerce/notification/overview.html" target="_blank" rel="noopener">eBay Notification API</a></li>'
355 .
'<li><a href="https://www.ebay.com/help/selling/business-policies/business-policies?id=4212" target="_blank" rel="noopener">eBay Business Policies</a></li>'
359 return '<section class="dbx-shop-help">'
360 .
'<h2>Channel-Hilfe: Amazon</h2>'
361 .
'<p>Amazon nutzt die Selling Partner API. Fuer Listings und Orders braucht dbxShop eine registrierte SP-API App, LWA-Zugangsdaten, Seller-Autorisierung, Seller-ID, Marketplace-ID und passende Rollen.</p>'
363 .
'<h3>Amazon-spezifisch</h3><ol>'
364 .
'<li>Professionelles Seller-Central-Konto und Entwicklerzugang vorbereiten.</li>'
365 .
'<li>SP-API Anwendung im Solution Provider Portal registrieren.</li>'
366 .
'<li>LWA Client-ID und Client-Secret aus der App-Registrierung uebernehmen.</li>'
367 .
'<li>Seller-Autorisierung durchlaufen und Refresh-Token speichern.</li>'
368 .
'<li>Marketplace-ID setzen, fuer Amazon.de <code>A1PA6795UKMFR9</code>.</li>'
369 .
'<li>Seller-ID aus Seller Central oder aus der Autorisierung uebernehmen.</li>'
370 .
'<li>Rollen fuer Product Listing, Orders und Notifications beantragen und im Feld API-Scopes/Rollen dokumentieren.</li>'
371 .
'<li>Fuer Rueckmeldungen Amazon Notifications API mit SQS Destination einrichten; die SQS ARN im Notification-Ziel speichern.</li>'
373 .
'<h3>Amazon-Felder verstaendlich erklaert</h3><dl>'
374 .
'<dt>Client-ID und Client-Secret</dt><dd>Das sind die LWA-Zugangsdaten der registrierten SP-API Anwendung. Sie identifizieren die App, nicht den einzelnen Artikel.</dd>'
375 .
'<dt>Refresh-Token</dt><dd>Dieser Token entsteht, wenn der Seller die App autorisiert. Er ist der wichtigste gespeicherte Wert, weil daraus neue Access-Tokens erzeugt werden.</dd>'
376 .
'<dt>Marketplace-ID</dt><dd>Legt fest, auf welchem Amazon-Marktplatz gearbeitet wird. Fuer Deutschland ist das typischerweise <code>A1PA6795UKMFR9</code>.</dd>'
377 .
'<dt>Seller-ID</dt><dd>Identifiziert das Seller-Konto, fuer das Listings und Orders verarbeitet werden.</dd>'
378 .
'<dt>Kategorie-ID / Product Type</dt><dd>Amazon arbeitet nicht nur mit Kategorien, sondern stark mit Product Types und Pflichtattributen. Fuer Listings muss der Adapter wissen, welches Produktschema verwendet wird.</dd>'
379 .
'<dt>Notification-Ziel / SQS ARN</dt><dd>Amazon sendet Rueckmeldungen nicht direkt an irgendeine URL, sondern typischerweise an eine AWS SQS Queue. Die ARN dieser Queue gehoert in dieses Feld.</dd>'
380 .
'<dt>Notification-Topic</dt><dd>Fuer Bestellungen ist <code>ORDER_CHANGE</code> relevant. Damit koennen Statusaenderungen oder neue Bestellungen in dbxShop ankommen.</dd>'
381 .
'<dt>API-Scopes / Rollen</dt><dd>Hier dokumentieren, welche SP-API Rollen genehmigt wurden, zum Beispiel Listings, Orders und Notifications. Das sind keine eBay-OAuth-Scopes. Tragen Sie am besten eine Rolle pro Zeile ein, damit klar bleibt, welche Berechtigung vorhanden ist.</dd>'
382 .
'<dt>Payment/Fulfillment/Return Policies</dt><dd>Diese eBay-Felder sind fuer Amazon normalerweise nicht relevant. Amazon verwaltet Zahlungs- und Versandlogik anders ueber Seller Central, Fulfillment by Merchant oder FBA.</dd>'
386 .
'<li><a href="https://developer-docs.amazon.com/sp-api/docs/registering-your-application" target="_blank" rel="noopener">Amazon SP-API App registrieren</a></li>'
387 .
'<li><a href="https://developer-docs.amazon.com/sp-api/docs/authorization-overview" target="_blank" rel="noopener">Amazon SP-API Autorisierung</a></li>'
388 .
'<li><a href="https://developer-docs.amazon.com/sp-api/docs/marketplace-ids" target="_blank" rel="noopener">Amazon Marketplace-IDs</a></li>'
389 .
'<li><a href="https://developer-docs.amazon.com/sp-api/docs/listings-items-api" target="_blank" rel="noopener">Amazon Listings Items API</a></li>'
390 .
'<li><a href="https://developer-docs.amazon.com/sp-api/docs/orders-api-v0-reference" target="_blank" rel="noopener">Amazon Orders API</a></li>'
391 .
'<li><a href="https://developer-docs.amazon.com/sp-api/docs/notifications-api" target="_blank" rel="noopener">Amazon Notifications API</a></li>'
395 return '<section class="dbx-shop-help">'
396 .
'<h2>Channel-Hilfe: mobile.de</h2>'
397 .
'<p>mobile.de ist kein klassischer Warenkorb-Marktplatz, sondern vor allem ein Fahrzeuganzeigen- und Lead-Channel. Relevant sind Seller API fuer Anzeigen und Lead API fuer Rueckmeldungen.</p>'
399 .
'<h3>mobile.de-spezifisch</h3><ol>'
400 .
'<li>Dealer- oder API-Account bei mobile.de verwenden.</li>'
401 .
'<li>API-Zugang im Dealer-Bereich oder ueber mobile.de Support aktivieren lassen.</li>'
402 .
'<li>Benutzer und Passwort fuer Basic Auth eintragen.</li>'
403 .
'<li>API-Basis-URL setzen, produktiv normalerweise <code>https://services.mobile.de</code> bzw. Seller-API-Pfad.</li>'
404 .
'<li>Account-ID/mobileSellerId oder Customer Number aus Dealer Area/API-Daten uebernehmen.</li>'
405 .
'<li>Fuer Rueckmeldungen Lead API nutzen; passende Lead-Events im Notification-Topic dokumentieren.</li>'
407 .
'<h3>mobile.de-Felder verstaendlich erklaert</h3><dl>'
408 .
'<dt>API-Basis-URL</dt><dd>Adresse der mobile.de Seller API, zum Beispiel <code>https://services.mobile.de/seller-api</code>. Sandbox und Produktion koennen unterschiedliche URLs haben.</dd>'
409 .
'<dt>Benutzer / Passwort</dt><dd>mobile.de nutzt fuer viele API-Zugriffe Basic Auth. Deshalb sind API-Benutzer und API-Passwort wichtiger als Client-ID oder OAuth-Tokens.</dd>'
410 .
'<dt>Account-ID / mobileSellerId</dt><dd>Die mobileSellerId identifiziert den Haendlerbestand. Ohne diese Zuordnung kann ein System oft nicht wissen, zu welchem Haendler ein Inserat gehoert.</dd>'
411 .
'<dt>Seller-ID</dt><dd>Kann als Kundennummer oder interne Haendlerkennung dokumentiert werden.</dd>'
412 .
'<dt>Kategorie-ID</dt><dd>Beschreibt die Art des Inserats, zum Beispiel Auto, Motorrad oder Nutzfahrzeug. mobile.de ist stark fahrzeugorientiert, nicht allgemeiner Warenkorb-Shop.</dd>'
413 .
'<dt>Location-Key</dt><dd>Optionaler Standort oder Filialbezug, wenn ein Haendler mehrere Standorte hat.</dd>'
414 .
'<dt>Notification-Ziel / Topic</dt><dd>Rueckmeldungen sind eher Leads, Anfragen oder Direct Offers. Dokumentieren Sie hier, welche Middleware oder Lead-API angebunden ist.</dd>'
415 .
'<dt>Payment/Fulfillment/Return Policies</dt><dd>Diese eBay-Felder sind fuer mobile.de normalerweise nicht relevant, weil mobile.de keine klassischen Shop-Checkout-Regeln fuer diese Produkte verwaltet.</dd>'
416 .
'<dt>API-Scopes / Rollen</dt><dd>mobile.de nutzt in der Regel Basic Auth und API-Freischaltungen statt OAuth-Scopes. Das Feld kann zur Dokumentation genutzt werden, zum Beispiel <code>seller-api</code> und <code>lead-api</code> jeweils in einer eigenen Zeile.</dd>'
420 .
'<li><a href="https://services.mobile.de/" target="_blank" rel="noopener">mobile.de API Uebersicht und Accounts</a></li>'
421 .
'<li><a href="https://services.mobile.de/manual/seller-api.html" target="_blank" rel="noopener">mobile.de Seller API</a></li>'
422 .
'<li><a href="https://services.sandbox.mobile.de/manual/lead-api.html" target="_blank" rel="noopener">mobile.de Lead API Sandbox</a></li>'
423 .
'<li><a href="https://services.mobile.de/manual/search-api.html" target="_blank" rel="noopener">mobile.de Search API / Customer IDs</a></li>'
426 case 'kleinanzeigen':
427 return '<section class="dbx-shop-help">'
428 .
'<h2>Channel-Hilfe: Kleinanzeigen</h2>'
429 .
'<p>Kleinanzeigen ist hier als manueller oder vertraglicher Channel vorbereitet. Eine allgemein frei nutzbare offizielle Anzeigen-/Order-API fuer normale Shop-Artikel ist nicht hinterlegt. Keine Scraper-Zugangsdaten als offizielle API behandeln.</p>'
431 .
'<h3>Kleinanzeigen-spezifisch</h3><ol>'
432 .
'<li>Wenn keine vertragliche Schnittstelle vorhanden ist, Verbindung auf <code>Manuell</code> lassen.</li>'
433 .
'<li>Export nur aktivieren, wenn eine rechtlich freigegebene Middleware oder Schnittstelle vorhanden ist.</li>'
434 .
'<li>API-Basis-URL, Benutzer, Token und Kategorie-ID nur aus dieser freigegebenen Schnittstelle uebernehmen.</li>'
435 .
'<li>Order-Import nur aktivieren, wenn Kauf-/Kontakt-Rueckmeldungen offiziell oder ueber eine eigene Middleware geliefert werden.</li>'
436 .
'<li>Fuer Immobilien gibt es offizielle Importschnittstellen; diese sind nicht automatisch eine allgemeine Artikel-API.</li>'
438 .
'<h3>Kleinanzeigen-Felder verstaendlich erklaert</h3><dl>'
439 .
'<dt>Verbindung</dt><dd>Ohne freigegebene Schnittstelle sollte dieser Channel auf <code>Manuell</code> stehen. Das bedeutet: dbxShop dokumentiert den Channel, uebertraegt aber nicht automatisch.</dd>'
440 .
'<dt>API-Basis-URL / Client-ID / Secret / Token</dt><dd>Nur ausfuellen, wenn ein Vertrag, eine offizielle Importschnittstelle oder eine eigene Middleware diese Werte bereitstellt. Keine Login-Daten fuer Scraping eintragen.</dd>'
441 .
'<dt>Kategorie-ID</dt><dd>Falls eine freigegebene Schnittstelle Kategorien verwendet, kann hier die Zielkategorie dokumentiert werden.</dd>'
442 .
'<dt>Location-Key</dt><dd>Kann Ort, PLZ oder Standort-ID enthalten, wenn die Schnittstelle das erwartet.</dd>'
443 .
'<dt>Notification-Ziel / Topic</dt><dd>Bei Kleinanzeigen sind Rueckmeldungen eher Nachrichten, Leads oder Kontaktanfragen. Ein echter Kauf-Webhook ist nur sinnvoll, wenn eine Middleware solche Ereignisse liefert.</dd>'
444 .
'<dt>Order-Import</dt><dd>Nur aktivieren, wenn wirklich strukturierte Kauf-/Kontakt-Rueckmeldungen an dbxShop uebergeben werden koennen. Sonst bleibt die Pflege manuell.</dd>'
445 .
'<dt>Payment/Fulfillment/Return Policies</dt><dd>Diese eBay-Felder sind fuer Kleinanzeigen normalerweise nicht relevant.</dd>'
446 .
'<dt>API-Scopes / Rollen</dt><dd>Nur ausfuellen, wenn eine vertragliche Schnittstelle oder Middleware konkrete Berechtigungen nennt. Auch hier ist ein Wert pro Zeile am verstaendlichsten.</dd>'
450 .
'<li><a href="https://hilfe-gewerblich.kleinanzeigen.de/artikel/schnittstellen" target="_blank" rel="noopener">Kleinanzeigen gewerbliche Schnittstellen</a></li>'
451 .
'<li><a href="https://business.kleinanzeigen.de/" target="_blank" rel="noopener">Kleinanzeigen fuer Gewerbliche</a></li>'
455 return '<section class="dbx-shop-help">'
456 .
'<h2>Channel-Hilfe: Shop</h2>'
457 .
'<p>Der Shop-Channel ist der interne Verkaufskanal. Er braucht keine externen API-Zugangsdaten.</p>'
459 .
'<h3>Shop-spezifisch</h3><ol>'
460 .
'<li>Verbindung auf <code>Intern</code> lassen.</li>'
461 .
'<li>Export aktiv lassen, wenn Artikel im eigenen Shop sichtbar sein duerfen.</li>'
462 .
'<li>Order-Import bleibt aus, weil Bestellungen direkt im Shop entstehen.</li>'
463 .
'<li>API-Felder, Token, Marketplace-ID und Policies sind fuer den internen Shop nicht benoetigt.</li>'
467 return '<section class="dbx-shop-help">'
468 .
'<h2>Channel-Hilfe: Eigener Channel</h2>'
469 .
'<p>Eigene Channels koennen an individuelle APIs, Feed-Exporte oder Middleware angebunden werden. Die konkreten Pflichtwerte haengen vom Adapter ab.</p>'
471 .
'<h3>Vorgehen</h3><ol>'
472 .
'<li>Beim Anbieter API-Zugang, Dokumentation und Testumgebung beantragen.</li>'
473 .
'<li>API-Basis-URL, Authentifizierung und erforderliche IDs in die Felder uebernehmen.</li>'
474 .
'<li>Webhook-Secret setzen und den dbxShop Webhook in der Middleware hinterlegen.</li>'
475 .
'<li>Verbindung testen und erst danach Export oder Order-Import produktiv nutzen.</li>'
480 private function productGroupsHelpHtml():
string {
481 return '<section class="dbx-shop-help">'
482 .
'<h2>Artikelgruppen</h2>'
483 .
'<p>Artikelgruppen beschreiben fachliche Produktfamilien. Sie steuern Standardwerte wie Mehrwertsteuer, Darstellung, Detailansicht, Galerieverhalten und Hinweise zu Artikelattributen.</p>'
484 .
'<h3>Eingabefelder</h3>'
486 .
'<dt>Key</dt><dd>Eindeutiger technischer Schluessel der Artikelgruppe. Nutzen Sie kurze stabile Werte wie <code>software</code>, <code>merch</code> oder <code>service</code>. Der Key wird beim Neuanlegen vergeben und bleibt danach stabil.</dd>'
487 .
'<dt>Obergruppe</dt><dd>Optionaler Platz in der Gruppenstruktur. Ohne Obergruppe erscheint die Gruppe als oberste Kachel im Katalog. Mit Obergruppe wird sie erst angezeigt, wenn der Kunde die uebergeordnete Gruppe anklickt, zum Beispiel <code>Kleidung / T-Shirts / male</code>.</dd>'
488 .
'<dt>Gruppe</dt><dd>Sichtbarer Name der Artikelgruppe in Verwaltung und Auswahllisten.</dd>'
489 .
'<dt>Beschreibung</dt><dd>Interne Beschreibung, welche Artikel in diese Gruppe gehoeren und wie sie im Shop verwendet wird.</dd>'
490 .
'<dt>Gruppenbild</dt><dd>Bild fuer die Gruppenkachel im Katalog. Pro Artikelgruppe gibt es genau ein aktives Gruppenbild. Eine neue Auswahl ersetzt das bisherige Gruppenbild; die Datei im CMS-Medienbrowser bleibt erhalten.</dd>'
491 .
'<dt>MwSt.</dt><dd>Standard-Mehrwertsteuersatz fuer Artikel dieser Gruppe. Einzelne Artikel koennen abweichen, wenn sie eigene Steuerwerte verwenden.</dd>'
492 .
'<dt>Karte</dt><dd>Template fuer Produktkarten in Listen, Katalogen oder Teasern.</dd>'
493 .
'<dt>Detail</dt><dd>Template fuer die Detailseite eines Artikels aus dieser Gruppe.</dd>'
494 .
'<dt>Gallery</dt><dd>Template fuer die Bild- oder Mediengalerie der Gruppe.</dd>'
495 .
'<dt>Bilder</dt><dd>Anzahl der initial sichtbaren Galeriebilder. Weitere Bilder koennen je nach Overflow-Einstellung sichtbar, scrollbar oder per Galerie erreichbar sein.</dd>'
496 .
'<dt>Bildgroesse</dt><dd>Steuert, wie Bilder in der Galerie eingepasst werden: Original, Cover oder Contain.</dd>'
497 .
'<dt>Lightbox</dt><dd>Breite der Lightbox-Ansicht, zum Beispiel <code>100vw</code> fuer volle Viewport-Breite.</dd>'
498 .
'<dt>Overflow</dt><dd>Verhalten bei vielen Bildern, zum Beispiel Grid, Slider, Scroll, Laufband oder Tutorial.</dd>'
499 .
'<dt>Klick</dt><dd>Definiert, was beim Klick auf ein Bild passiert: Lightbox, kein Klick, neuer Tab, ViewerJS oder PhotoSwipe.</dd>'
500 .
'<dt>Artikelattribute</dt><dd>Interne Hinweise fuer Attribute dieser Gruppe, zum Beispiel welche technischen Daten, Varianten oder Filter erwartet werden.</dd>'
501 .
'<dt>Channel-Vorgaben</dt><dd>Optionale Standardwerte fuer externe Verkaufskanaele. eBay Kategorie, Amazon Product Type, Kleinanzeigen Kategorie und mobile.de Kategorie werden bei Artikeln dieser Gruppe als Vorschlag im Channel-Mapping verwendet. Pro Artikel kann der Wert dort ueberschrieben werden. Wenn Channels in der Shop-Konfiguration deaktiviert sind, werden diese Eingaben ausgeblendet.</dd>'
502 .
'<dt>Sort</dt><dd>Reihenfolge in Listen und Auswahlfeldern. Kleine Zahlen stehen weiter oben; Schritte von 10 lassen Platz fuer spaetere Gruppen.</dd>'
503 .
'<dt>Aktiv</dt><dd>Schaltet die Artikelgruppe fuer die Verwendung ein oder aus. Inaktive Gruppen bleiben erhalten, werden aber nicht als aktive Option behandelt.</dd>'
505 .
'<h3>Aktionen</h3>'
507 .
'<li><strong>Plus in der Bar:</strong> legt oben eine neue Eingabezeile fuer eine Artikelgruppe an.</li>'
508 .
'<li><strong>Speichern:</strong> uebernimmt alle sichtbaren Einstellungen der Zeile.</li>'
509 .
'<li><strong>Loeschen:</strong> verschiebt die Artikelgruppe in den Papierkorb und deaktiviert sie. Bestehende Produktzuordnungen bleiben technisch nachvollziehbar, die Gruppe erscheint aber nicht mehr in der aktiven Liste.</li>'
511 .
'<p><strong>Praxis:</strong> Verwenden Sie Artikelgruppen fuer wiederkehrende Produktlogik, nicht fuer einzelne Produkte. Beispiele sind Software, Merchandise, Service oder Zubehoer.</p>'
512 .
'<p><strong>Katalog:</strong> Kunden sehen zunaechst die obersten Gruppenbilder. Ein Klick oeffnet die Untergruppen und die direkten Artikel dieser Gruppe. Ohne Gruppenauswahl zeigt der Katalog alle aktiven Artikel und kann weiterhin per Suche und Attributen gefiltert werden.</p>'
516 private function shippingGroupsHelpHtml():
string {
517 return '<section class="dbx-shop-help">'
518 .
'<h2>Versandgruppen</h2>'
519 .
'<p>Versandgruppen definieren Versandweg, Lieferzeit, Standardkosten und Freigrenzen. Artikel koennen diese Werte aus der Gruppe uebernehmen oder eigene Versandwerte verwenden.</p>'
520 .
'<h3>Eingabefelder</h3>'
522 .
'<dt>Key</dt><dd>Eindeutiger technischer Schluessel der Versandgruppe. Beispiele sind <code>digital-free</code>, <code>service-remote</code> oder <code>merch-package</code>. Der Key wird beim Neuanlegen vergeben und bleibt danach stabil.</dd>'
523 .
'<dt>Versandgruppe</dt><dd>Sichtbarer Name fuer Verwaltung und Auswahlfelder.</dd>'
524 .
'<dt>Beschreibung</dt><dd>Interne Erklaerung, fuer welche Artikel und Liefersituationen diese Versandgruppe gedacht ist.</dd>'
525 .
'<dt>Versandweg</dt><dd>Bezeichnung des Versand- oder Bereitstellungswegs, zum Beispiel Download, Remote-Termin, DHL Paket oder Spedition.</dd>'
526 .
'<dt>Lieferzeit</dt><dd>Text fuer die erwartete Bereitstellung oder Lieferzeit, zum Beispiel <code>Sofort nach Freischaltung</code> oder <code>2-4 Werktage</code>.</dd>'
527 .
'<dt>Kosten</dt><dd>Standard-Versandkosten brutto. Verwenden Sie <code>0</code> fuer kostenfreie Lieferung oder digitale Bereitstellung.</dd>'
528 .
'<dt>Frei ab</dt><dd>Brutto-Warenwert, ab dem die Versandkosten entfallen. Der Wert <code>-1</code> bedeutet: keine Freigrenze.</dd>'
529 .
'<dt>Sort</dt><dd>Reihenfolge in Listen und Auswahlfeldern. Kleine Zahlen stehen weiter oben.</dd>'
530 .
'<dt>Aktiv</dt><dd>Schaltet die Versandgruppe fuer die Nutzung ein oder aus. Inaktive Gruppen bleiben erhalten, werden aber nicht als aktive Option behandelt.</dd>'
532 .
'<h3>Aktionen</h3>'
534 .
'<li><strong>Plus in der Bar:</strong> legt oben eine neue Eingabezeile fuer eine Versandgruppe an.</li>'
535 .
'<li><strong>Speichern:</strong> uebernimmt Versandweg, Lieferzeit, Kosten, Freigrenze, Sortierung und Aktiv-Status.</li>'
536 .
'<li><strong>Loeschen:</strong> verschiebt die Versandgruppe in den Papierkorb und deaktiviert sie. Bestehende Produktdaten bleiben nachvollziehbar, die Gruppe erscheint aber nicht mehr in der aktiven Liste.</li>'
538 .
'<p><strong>Praxis:</strong> Trennen Sie digitale Bereitstellung, Remote-Leistungen und physische Paketlieferungen in eigene Versandgruppen.</p>'
542 private function settingsHelpHtml():
string {
543 return '<section class="dbx-shop-help">'
544 .
'<h2>Shop-Einstellungen</h2>'
545 .
'<p>Diese Seite verwaltet globale dbxShop-Einstellungen. Artikel, Artikelgruppen, Versandgruppen und Channels nutzen diese Werte als gemeinsame Grundlage. Besonders wichtig ist die MwSt.-Logik: Artikel speichern keine eigenen Prozentwerte mehr, sondern Artikelgruppen waehlen eine MwSt.-Klasse wie <code>mwst1</code> oder <code>mwst2</code>. Der tatsaechliche Prozentwert wird hier gepflegt.</p>'
546 .
'<h3>Shop</h3><dl>'
547 .
'<dt>Shop aktiv</dt><dd>Schaltet den Shop grundsaetzlich ein oder aus. Wenn der Shop spaeter gesperrt werden soll, kann diese Einstellung fuer Frontend-Pruefungen genutzt werden.</dd>'
548 .
'<dt>Standard-Channel</dt><dd>Technischer Standard-Verkaufskanal fuer normale Shop-Bestellungen. Fuer den eigenen Shop ist typischerweise <code>web</code> oder <code>shop</code> sinnvoll. Externe Plattformen wie eBay oder Amazon werden als eigene Channels gepflegt.</dd>'
549 .
'<dt>Standard-Waehrung</dt><dd>ISO-Waehrungscode fuer Preise und Bestellungen, z.B. <code>EUR</code>. Der Wert sollte aus drei Buchstaben bestehen.</dd>'
550 .
'<dt>Preisanzeige</dt><dd>Legt fest, ob Preise als Brutto- oder Netto-Anzeige interpretiert werden sollen. Fuer einen deutschen Verbraucher-Shop ist Brutto in der Regel die passende Anzeige. B2B-Szenarien koennen Netto verwenden, benoetigen aber klare Steuer- und Rechtstexte.</dd>'
551 .
'<dt>B2B-Modus</dt><dd>Vorbereitung fuer Geschaeftskundenlogik. Diese Einstellung dokumentiert die Absicht, z.B. spaeter Netto-Preise, USt-ID-Pruefung oder andere Checkout-Regeln zu aktivieren.</dd>'
552 .
'<dt>Lagerbestand nutzen</dt><dd>Aktiviert die Beruecksichtigung des Lagerbestands. Wenn aus, koennen Artikel unabhaengig vom Feld Lagerbestand angezeigt werden.</dd>'
553 .
'<dt>Channels nutzen</dt><dd>Globaler Schalter fuer externe Verkaufskanaele wie eBay, Amazon, Kleinanzeigen oder mobile.de. Wenn deaktiviert, werden Channel-Funktionen im Shop-Admin ausgeblendet bzw. nicht verwendet. Welche externen Channels konkret aktiv sind, wird in der Channel-Verwaltung gepflegt und in den Einstellungen als Statusuebersicht angezeigt.</dd>'
555 .
'<h3>MwSt.</h3><dl>'
556 .
'<dt>Standard-MwSt.-Klasse</dt><dd>Standardklasse fuer neue Artikelgruppen oder Faelle ohne eindeutige Zuordnung. Ueblich ist <code>mwst1</code> fuer den regulaeren Satz.</dd>'
557 .
'<dt>MwSt.-Anzeige</dt><dd>Legt fest, ob bei Preisen im Shop der Hinweis mit dem MwSt.-Satz angezeigt wird. Wenn deaktiviert, bleibt die steuerliche Berechnung intern erhalten, der sichtbare Preistext zeigt aber nur noch die Versandinformation. Fuer Sonderfaelle wie Kleinunternehmerregelung muessen passende Rechtstexte gepflegt werden.</dd>'
558 .
'<dt>mwst1 - Name</dt><dd>Bezeichnung der ersten Steuerklasse, z.B. <code>MwSt. normal</code>. Diese Klasse wird normalerweise fuer den regulaeren deutschen Umsatzsteuersatz verwendet.</dd>'
559 .
'<dt>mwst1 - Prozent</dt><dd>Prozentwert der ersten Steuerklasse, aktuell typischerweise <code>19</code>. Wenn sich der gesetzliche Satz aendert, wird dieser Wert hier zentral angepasst.</dd>'
560 .
'<dt>mwst2 - Name</dt><dd>Bezeichnung der zweiten Steuerklasse, z.B. <code>MwSt. ermaessigt</code>. Diese Klasse ist fuer Artikel mit ermaessigtem Steuersatz gedacht.</dd>'
561 .
'<dt>mwst2 - Prozent</dt><dd>Prozentwert der zweiten Steuerklasse, aktuell typischerweise <code>7</code>.</dd>'
562 .
'<dt>mwst3 - Name</dt><dd>Dritte vorbereitete Steuerklasse. Sie kann fuer kuenftige Steuersaetze, Sonderfaelle oder Uebergangsregeln genutzt werden.</dd>'
563 .
'<dt>mwst3 - Prozent</dt><dd>Prozentwert der dritten Steuerklasse, z.B. vorbereitet mit <code>22</code>. Solange Artikelgruppen diese Klasse nicht verwenden, wirkt sie sich nicht auf Artikelpreise aus.</dd>'
564 .
'<dd><strong>Workflow:</strong> Prozentwerte hier pflegen, danach in <strong>Artikelgruppen</strong> nur die passende Klasse auswaehlen. Artikel erben den Steuersatz aus ihrer primaeren Artikelgruppe.</dd>'
566 .
'<h3>Checkout und Rechtstexte</h3><dl>'
567 .
'<dt>Gastbestellung erlauben</dt><dd>Erlaubt Bestellungen ohne vorheriges Kundenkonto. Wenn deaktiviert, verlangt der Checkout ein angemeldetes Benutzerkonto.</dd>'
568 .
'<dt>Rechtstext-Snapshot speichern</dt><dd>Speichert bei Bestellungen den Stand von Rechtstexten und Widerrufsbelehrung als Snapshot. So bleibt nachvollziehbar, welche Texte zum Kaufzeitpunkt galten.</dd>'
569 .
'<dt>Widerruf anzeigen</dt><dd>Steuert, ob der Widerrufsbereich im Shop sichtbar bzw. verwendbar sein soll. Der eigentliche Widerrufstext kommt aus der CMS-Seite <code>/shop-widerruf</code>.</dd>'
570 .
'<dt>Kunden-Mail senden</dt><dd>Sendet nach Bestellung und Widerruf eine kurze Bestaetigung an die beim Checkout bzw. Widerruf angegebene E-Mail-Adresse.</dd>'
571 .
'<dt>Admin-Mail senden</dt><dd>Sendet bei neuen Bestellungen und Widerrufen eine interne Benachrichtigung an die Admin-E-Mail-Adresse.</dd>'
572 .
'<dt>Shop-Absender</dt><dd>Eigene From-Adresse fuer Bestellungen, Statusmeldungen und Widerrufe. Verwenden Sie eine echte Domain-Adresse, z.B. <code>shop@example.org</code>. Eine stabile Shop-Adresse erleichtert automatische Mailprozesse beim Empfaenger.</dd>'
573 .
'<dt>Admin-E-Mail</dt><dd>Empfaengeradresse fuer interne Shop-Benachrichtigungen, z.B. <code>admin@example.org</code>.</dd>'
575 .
'<h3>Zahlungsarten</h3><dl>'
576 .
'<dt>Vorkasse aktiv</dt><dd>Aktiviert Vorkasse per Bankueberweisung. Die Bestellung wird gespeichert, die Zahlung bleibt offen, bis der Zahlungseingang im Admin bestaetigt wird.</dd>'
577 .
'<dt>Kontoinhaber</dt><dd>Name des Zahlungsempfaengers fuer Vorkasse, z.B. Firmenname oder Shop-Betreiber.</dd>'
578 .
'<dt>IBAN</dt><dd>Bankverbindung fuer Vorkasse. Dieser Wert wird Kunden nach der Bestellung und in der Bestellmail als Zahlungsinformation angezeigt.</dd>'
579 .
'<dt>BIC</dt><dd>BIC/SWIFT der Bank. Innerhalb Deutschlands oft nicht zwingend, fuer internationale Kunden aber hilfreich.</dd>'
580 .
'<dt>Bank</dt><dd>Name der Bank fuer die Vorkasse-Zahlungsinformation.</dd>'
581 .
'<dt>Vorkasse-Hinweis</dt><dd>Text, der Kunden erklaert, wann und wie sie ueberweisen sollen. Die Bestellnummer wird zusaetzlich als Verwendungszweck angezeigt.</dd>'
582 .
'<dt>Rechnung aktiv</dt><dd>Aktiviert Rechnung als Zahlungsart. Das ist meist nur fuer bestimmte Kunden oder B2B sinnvoll und sollte bei Bedarf mit Freigabelogik verbunden werden.</dd>'
583 .
'<dt>Rechnungs-Hinweis</dt><dd>Text fuer Zahlungsziel oder interne Hinweise, z.B. <code>Bitte zahlen Sie innerhalb von 14 Tagen nach Rechnungserhalt.</code></dd>'
584 .
'<dt>PayPal aktiv</dt><dd>Schaltet PayPal im Checkout frei. PayPal funktioniert erst, wenn Modus, Client-ID und Client-Secret korrekt eingetragen sind.</dd>'
585 .
'<dt>PayPal Modus</dt><dd><code>Sandbox</code> nutzt die PayPal-Testumgebung. <code>Live</code> nutzt echte Zahlungen. Vor Livebetrieb immer zuerst Sandbox testen.</dd>'
586 .
'<dt>PayPal Brand Name</dt><dd>Name, der bei PayPal im Zahlungsfenster erscheinen soll, z.B. der Shop- oder Firmenname.</dd>'
587 .
'<dt>PayPal Client-ID</dt><dd>Oeffentliche App-Kennung aus dem PayPal Developer Dashboard. Fuer Sandbox und Live gibt es getrennte Zugangsdaten.</dd>'
588 .
'<dt>PayPal Client-Secret</dt><dd>Geheimer App-Schluessel aus dem PayPal Developer Dashboard. Dieser Wert darf nicht oeffentlich angezeigt oder weitergegeben werden.</dd>'
589 .
'<dd>PayPal-Zugangsdaten werden im PayPal Developer Dashboard unter <a href="https://developer.paypal.com/dashboard/applications/" target="_blank" rel="noopener">developer.paypal.com/dashboard/applications</a> erstellt und verwaltet. Sandbox-Testkonten und Testkaeufer finden Sie unter <a href="https://developer.paypal.com/tools/sandbox/" target="_blank" rel="noopener">developer.paypal.com/tools/sandbox</a>.</dd>'
590 .
'<dt>Payment testen</dt><dd>Der Stecker-Button in der Bar prueft PayPal per OAuth-Token gegen Sandbox oder Live. Amazon Pay prueft vollstaendige Zugangsdaten und die lokale RSA-PSS-Signatur. Eine echte Amazon-Pay-Autorisierung wird beim Checkout mit einer echten Checkout Session ausgefuehrt.</dd>'
591 .
'<dt>Amazon Pay aktiv</dt><dd>Schaltet Amazon Pay als auswaehlbare Zahlungsart frei. Beim Checkout wird eine Amazon-Pay-Checkout-Session erzeugt, der Kunde zu Amazon Pay weitergeleitet und die Zahlung nach Rueckkehr ueber Complete Checkout Session abgeschlossen.</dd>'
592 .
'<dt>Amazon Pay Modus</dt><dd><code>Sandbox</code> fuer Tests, <code>Live</code> fuer echte Zahlungen. Die Zugangsdaten muessen zur jeweiligen Umgebung passen.</dd>'
593 .
'<dt>Amazon Pay Region</dt><dd>Region des Amazon-Pay-Kontos. Fuer Deutschland typischerweise <code>EU</code>.</dd>'
594 .
'<dt>Amazon Merchant-ID</dt><dd>Haendlerkennung aus der Amazon-Pay-/Seller-Central-Konfiguration.</dd>'
595 .
'<dt>Amazon Store-ID</dt><dd>Store-/Checkout-Konfiguration aus Amazon Pay. Sie identifiziert, welche Store-Konfiguration fuer den Checkout genutzt werden soll.</dd>'
596 .
'<dt>Public-Key-ID</dt><dd>Kennung des oeffentlichen Schluessels, der bei Amazon Pay hinterlegt wurde.</dd>'
597 .
'<dt>Private Key</dt><dd>Privater Signaturschluessel fuer Amazon Pay. Dieser Wert ist geheim und darf nicht oeffentlich angezeigt werden.</dd>'
598 .
'<dt>Sandbox Simulation</dt><dd>Optionaler Amazon-Pay-Simulationscode fuer Sandbox-Tests, z.B. <code>AmazonCanceled</code>. Leer lassen fuer normale Sandbox-Tests ohne erzwungene Fehlersimulation.</dd>'
599 .
'<dd>Amazon-Pay-Zugangsdaten werden in Seller Central bzw. Amazon Pay Integration Central verwaltet: <a href="https://sellercentral.amazon.de/" target="_blank" rel="noopener">sellercentral.amazon.de</a>. Die technische API-v2-Dokumentation steht unter <a href="https://developer.amazon.com/docs/amazon-pay-api-v2/introduction.html" target="_blank" rel="noopener">developer.amazon.com/docs/amazon-pay-api-v2</a>.</dd>'
601 .
'<h3>Channels</h3><dl>'
602 .
'<dt>Aktive externe Channels</dt><dd>Zeigt alle aktiven Channels ausser dem internen <code>shop</code>-Channel. Aktiv, Export, Order-Import und Teststatus kommen aus der Channel-Verwaltung.</dd>'
603 .
'<dt>Channels bearbeiten</dt><dd>Oeffnet die Channel-Verwaltung. Dort werden API-Zugangsdaten, Marketplace-, Seller-, Location- und Policy-Werte gepflegt.</dd>'
605 .
'<h3>Versand</h3><dl>'
606 .
'<dt>Digitale Downloads aktiv</dt><dd>Erlaubt digitale Bereitstellung fuer passende Produkte. Digitale Produkte koennen andere Widerrufsregeln haben; die Rechtstexte muessen dazu passen.</dd>'
607 .
'<dt>Pauschalversand aktiv</dt><dd>Aktiviert einen einfachen globalen Versandkostenwert als Fallback. Detailliertere Versandlogik wird weiterhin ueber Versandgruppen gepflegt.</dd>'
608 .
'<dt>Pauschalversand brutto</dt><dd>Globaler Bruttobetrag fuer Versandkosten, wenn keine spezifischere Versandgruppe greift. Beispiel: <code>5.90</code>.</dd>'
609 .
'<dt>CMS-Media Slot</dt><dd>Technischer Slotname fuer Shop-Medien im CMS-Medienbrowser. Normalerweise <code>shop</code>. Nur aendern, wenn Shop-Bilder bewusst in einem anderen CMS-Slot verwaltet werden sollen.</dd>'
611 .
'<h3>Speichern</h3>'
612 .
'<p>Nach dem Speichern werden die Werte in der Modul-Konfiguration <code>dbxShop</code> abgelegt. Aenderungen an MwSt.-Prozentwerten wirken sofort auf Artikel, die ihre MwSt. aus Artikelgruppen erben.</p>'
616 private function ordersHelpHtml():
string {
617 return '<section class="dbx-shop-help">'
618 .
'<h2>Bestellungen</h2>'
619 .
'<p>Die Bestellverwaltung zeigt Shop-, PayPal- und Channel-Bestellungen als Snapshots. Eine Bestellung soll nicht aus aktuellen Artikeldaten rekonstruiert werden, sondern behält Artikel, Preise, MwSt., Versand und Zahlungsdaten aus dem Zeitpunkt der Bestellung.</p>'
620 .
'<h3>Liste</h3><dl>'
621 .
'<dt>Bestellung</dt><dd>Bestellnummer, Datum und Herkunft. Die Bestellnummer bleibt stabil und dient fuer Kundensuche, Zahlungsabgleich und Channel-Zuordnung.</dd>'
622 .
'<dt>Kunde</dt><dd>Name und E-Mail-Adresse, soweit beim Checkout oder beim Channel-Import vorhanden. Bei importierten Marktplatzbestellungen kann die Plattform je nach Datenschutz nur eingeschraenkte Kundendaten liefern.</dd>'
623 .
'<dt>Status</dt><dd>Interner Bearbeitungsstatus: neu, Zahlung offen, bezahlt, in Bearbeitung, versendet, abgeschlossen oder storniert. Dieser Status steuert die Arbeit im Shop-Admin.</dd>'
624 .
'<dt>Zahlung</dt><dd>Zahlungsanbieter, Zahlungsstatus und Referenz, z.B. PayPal-ID oder externe Channel-Bestellnummer.</dd>'
625 .
'<dt>Positionen</dt><dd>Artikelpositionen als Snapshot mit SKU, Titel und Menge.</dd>'
626 .
'<dt>Summe</dt><dd>Bruttosumme der Bestellung in der gespeicherten Waehrung.</dd>'
628 .
'<h3>Filter</h3><dl>'
629 .
'<dt>Suche</dt><dd>Sucht nach Bestellnummer, Kunde, E-Mail, Channel und Zahlungsreferenz.</dd>'
630 .
'<dt>Status</dt><dd>Filtert nach internem Bearbeitungsstatus.</dd>'
631 .
'<dt>Zahlung</dt><dd>Filtert nach Zahlungsstatus.</dd>'
632 .
'<dt>Versand</dt><dd>Filtert nach operativem Versandstatus, zum Beispiel offen, versendet oder zugestellt.</dd>'
633 .
'<dt>Channel</dt><dd>Filtert nach Herkunft, z.B. Shop, eBay, Amazon oder mobile.de.</dd>'
635 .
'<h3>Detail bearbeiten</h3><dl>'
636 .
'<dt>Schnellaktionen</dt><dd>Setzen haeufige Arbeitsschritte mit einem Klick: bezahlt, in Bearbeitung, versandbereit, versendet, zugestellt/abgeschlossen, storniert oder erstattet. Jede Aktion schreibt die passenden Statusfelder und einen Historieneintrag.</dd>'
637 .
'<dt>Kundenmail senden</dt><dd>Der Umschlag-Button in der Bar sendet den aktuell gespeicherten Status an die Kunden-E-Mail-Adresse. Speichern Sie geaenderte Felder zuerst. Voraussetzung ist ein Mail-Absender in den Shop-Einstellungen.</dd>'
638 .
'<dt>Status</dt><dd>Setzt den internen Bearbeitungsstand. Typischer Ablauf: neu -> bezahlt -> in Bearbeitung -> versendet -> abgeschlossen.</dd>'
639 .
'<dt>Zahlungsstatus</dt><dd>Dokumentiert den Stand der Zahlung. PayPal und Channel-Import koennen diesen Wert automatisch setzen; manuelle Korrektur ist fuer Abgleich und Sonderfaelle moeglich.</dd>'
640 .
'<dt>Zahlungsreferenz</dt><dd>Externe Referenz wie PayPal Order ID, eBay Order ID, Amazon Order ID oder interne Buchungsnummer.</dd>'
641 .
'<dt>Rechnungsnummer / Rechnungsdatum</dt><dd>Werden manuell gepflegt oder beim Wechsel in einen bezahlten Bearbeitungsstatus automatisch vorgeschlagen. Fuer echten Livebetrieb muss ein rechtssicherer Nummernkreis definiert werden.</dd>'
642 .
'<dt>Versandstatus</dt><dd>Operativer Lieferstatus: offen, bereit, versendet, zugestellt oder Retoure.</dd>'
643 .
'<dt>Versanddienstleister</dt><dd>Name des Dienstleisters oder der Versandart, z.B. DHL, UPS, Deutsche Post oder Download/Freischaltung.</dd>'
644 .
'<dt>Trackingnummer / Tracking-URL</dt><dd>Trackingdaten fuer physische Sendungen. Wenn DHL, UPS, DPD oder Hermes als Versanddienstleister eingetragen ist und die Tracking-URL leer bleibt, wird beim Speichern eine passende Tracking-URL erzeugt.</dd>'
645 .
'<dt>Versendet am</dt><dd>Zeitpunkt des Versands. Wird beim Wechsel auf <code>Versendet</code> automatisch gesetzt, wenn noch leer.</dd>'
646 .
'<dt>Admin-Notiz</dt><dd>Interne Notizen fuer Rueckfragen, Versandhinweise, Sonderabsprachen oder Channel-Probleme.</dd>'
647 .
'<dt>Rechnung anzeigen / PDF</dt><dd>Der Beleg-Button zeigt eine Rechnungsvorschau. Der PDF-Button erzeugt bei Bedarf Rechnungsnummer, Datum und PDF-Datei und archiviert den Dateipfad an der Bestellung.</dd>'
648 .
'<dt>Positionen</dt><dd>Nicht direkt editierbare Snapshot-Positionen. Aenderungen an Artikeln sollen alte Bestellungen nicht veraendern.</dd>'
649 .
'<dt>Payload</dt><dd>Rohdaten aus Zahlung oder Channel-Import. Diese helfen bei technischer Pruefung und Supportfaellen.</dd>'
650 .
'<dt>Historie</dt><dd>Protokolliert wichtige Status-, Zahlungs-, Versand-, Rechnungs- und Widerrufsereignisse.</dd>'
651 .
'<dt>Widerrufe</dt><dd>Zeigt eingegangene Widerrufe, die anhand der Bestellnummer zugeordnet wurden.</dd>'
653 .
'<h3>Loeschen</h3>'
654 .
'<p>Loeschen verschiebt Bestellungen in den Papierkorb. Es loescht keine Artikel und keine Channel-Daten. Fuer echte kaufmaennische Loesch-/Aufbewahrungsregeln muss vor Livebetrieb entschieden werden, was archiviert werden muss.</p>'
658 private function productsHelpHtml():
string {
659 return '<section class="dbx-shop-help">'
660 .
'<h2>Produkte und Artikel-Formular</h2>'
661 .
'<p>Das Produktformular pflegt die Stammdaten eines Artikels. Gruppen, Attribute, Bilder und Channel-Zuordnungen sind getrennte Bereiche, damit ein Artikel sauber im Shop angezeigt, verkauft und optional zu externen Plattformen exportiert werden kann.</p>'
662 .
'<h3>Produktliste</h3><dl>'
663 .
'<dt>Suche / Filter</dt><dd>Sucht nach Artikelnummer, Titel, Beschreibung, Gruppe, Channel und weiteren Produktwerten. Die Anzahl pro Seite steuert die Tabellenlaenge; <code>Alle</code> zeigt alle passenden Artikel.</dd>'
664 .
'<dt>Bearbeiten</dt><dd>Oeffnet das Artikel-Formular im Fenster.</dd>'
665 .
'<dt>Ansehen</dt><dd>Oeffnet die Shop-Ansicht des Artikels.</dd>'
666 .
'<dt>Loeschen</dt><dd>Verschiebt den Artikel nach Confirm in den Papierkorb.</dd>'
667 .
'<dt>Mehrfachaktion</dt><dd>Fuer markierte Artikel koennen Channel hinzugefuegt, entfernt, exportiert oder Produktgruppen gesetzt werden.</dd>'
669 .
'<h3>Grunddaten</h3><dl>'
670 .
'<dt>Artikelnummer</dt><dd>Interne und externe Artikelnummer. Sie entspricht praktisch der SKU und sollte eindeutig, stabil und kanalgeeignet sein.</dd>'
671 .
'<dt>URL-Name</dt><dd>Technischer URL-Teil fuer die Shop-Detailseite. Verwenden Sie kurze, sprechende Kleinbuchstaben ohne Sonderzeichen.</dd>'
672 .
'<dt>Titel</dt><dd>Sichtbarer Produktname im Shop, in Listen und beim Export.</dd>'
673 .
'<dt>Kategorie</dt><dd>Freie Kategoriebezeichnung fuer Darstellung und Verwaltung.</dd>'
674 .
'<dt>Produkttyp</dt><dd>Unterscheidet digitale Produkte, physische Produkte und Dienstleistungen. Das wirkt auf Versand, Checkout und spaetere Channel-Logik.</dd>'
675 .
'<dt>Aktiv</dt><dd>Nur aktive Produkte werden regulaer angeboten.</dd>'
676 .
'<dt>Sortierung</dt><dd>Reihenfolge in Listen und Katalogen. Kleine Werte stehen weiter vorne.</dd>'
678 .
'<h3>Beschreibung</h3><dl>'
679 .
'<dt>Kurzbeschreibung</dt><dd>Kompakter Text fuer Karten, Listen und Suchergebnisse.</dd>'
680 .
'<dt>Beschreibung</dt><dd>Ausfuehrlicher Produkttext fuer Detailseite und Channel-Export.</dd>'
682 .
'<h3>Preis, Versand und Darstellung</h3><dl>'
683 .
'<dt>Bruttopreis</dt><dd>Artikelpreis inklusive MwSt. Die MwSt.-Prozentwerte kommen aus den Shop-Einstellungen und der Artikelgruppe.</dd>'
684 .
'<dt>Versand-Quelle</dt><dd><code>Aus Gruppe</code> nutzt die Versandgruppe. Individuelle Werte ueberschreiben die Gruppe fuer diesen Artikel.</dd>'
685 .
'<dt>Versand brutto individuell</dt><dd>Artikelbezogener Versandwert. <code>-1</code> bedeutet: aus Gruppe oder globalem Fallback berechnen.</dd>'
686 .
'<dt>Lagerbestand</dt><dd>Interner Bestand. Fuer digitale Produkte kann 0 trotzdem verkaufbar sein, wenn der Ablauf das erlaubt.</dd>'
687 .
'<dt>Badge</dt><dd>Kurzer Hervorhebungs-Text auf Karten, z.B. <code>Neu</code> oder <code>Business</code>.</dd>'
688 .
'<dt>Icon</dt><dd>Bootstrap-Icon-Klasse fuer fallbackartige Darstellung ohne Bild.</dd>'
689 .
'<dt>Logo-Variante</dt><dd>Optionaler Darstellungswert fuer Templates.</dd>'
690 .
'<dt>MwSt.-Quelle</dt><dd>Normalfall ist <code>Aus Gruppe</code>. Die Gruppe referenziert <code>mwst1</code>, <code>mwst2</code> oder <code>mwst3</code>; die echten Prozentwerte stehen in den Shop-Einstellungen.</dd>'
692 .
'<h3>Bilder</h3><dl>'
693 .
'<dt>Auswahl</dt><dd>Oeffnet den CMS-Medienbrowser fuer Shop-Bilder. Dateien bleiben im Medienbrowser; im Shop wird nur die Zuordnung gespeichert.</dd>'
694 .
'<dt>Primaerbild</dt><dd>Markiert das neu zugeordnete Bild als Hauptbild.</dd>'
695 .
'<dt>Papierkorb am Bild</dt><dd>Hebt nur die Shop-Zuordnung auf. Die Mediendatei wird nicht geloescht.</dd>'
697 .
'<h3>Channels</h3><dl>'
698 .
'<dt>Checkbox</dt><dd>Setzt eine direkte Artikel-Channel-Zuordnung. Diese hat Vorrang vor Channel-Gruppen.</dd>'
699 .
'<dt>Status</dt><dd>Zeigt, ob der Channel direkt aktiv, direkt deaktiviert, aus einer Gruppe geerbt oder nicht gesetzt ist.</dd>'
700 .
'<dt>Mapping</dt><dd>Oeffnet das produktbezogene Channel-Mapping fuer Kategorie, Pflichtattribute, externe IDs und provider-spezifische Werte.</dd>'
701 .
'<dt>Export</dt><dd>Startet den Export fuer diesen Artikel und Channel, wenn der Channel Export erlaubt und ausreichend konfiguriert ist.</dd>'
703 .
'<h3>Bar-Aktionen</h3><dl>'
704 .
'<dt>Speichern</dt><dd>Speichert Stammdaten und direkte Channel-Auswahl.</dd>'
705 .
'<dt>Artikel ansehen</dt><dd>Oeffnet die Shop-Vorschau.</dd>'
706 .
'<dt>Artikelattribute</dt><dd>Oeffnet die Werte der gruppenspezifischen Attribute fuer diesen Artikel.</dd>'
707 .
'<dt>Loeschen</dt><dd>Loescht nach Confirm den Artikel.</dd>'
708 .
'<dt>Hilfe</dt><dd>Oeffnet diese CMS-Hilfeseite.</dd>'
709 .
'<dt>Produktliste</dt><dd>Zurueck zur Produktuebersicht.</dd>'
714 private function productChannelMappingHelpHtml():
string {
715 return '<section class="dbx-shop-help">'
716 .
'<h2>Channel-Mapping je Artikel</h2>'
717 .
'<p>Das Channel-Mapping speichert produktbezogene Werte fuer genau einen Artikel und genau einen Channel. Globale Zugangsdaten bleiben im Channel-Formular; hier stehen die Werte, die sich pro Artikel unterscheiden.</p>'
718 .
'<h3>Channel-Werte</h3><dl>'
719 .
'<dt>Aktiv</dt><dd>Schaltet die direkte Artikelzuordnung fuer diesen Channel ein. Diese Einstellung hat Vorrang vor Channel-Gruppen.</dd>'
720 .
'<dt>Channel-SKU</dt><dd>Artikelnummer, die fuer diesen Channel verwendet wird. Meist identisch mit der Shop-Artikelnummer; bei Plattformen kann sie aber abweichen.</dd>'
721 .
'<dt>Channel-Preis</dt><dd>Brutto-Preis, der fuer diesen Channel verwendet wird. Wenn kein eigener Channel-Preis gespeichert ist, zeigt das Feld den aktuellen Shop-Artikelpreis an. Wird dieser Wert unveraendert gespeichert, bleibt intern die Vererbung aktiv. Aendern Sie den Wert nur, wenn dieser Channel bewusst einen abweichenden Preis bekommen soll.</dd>'
722 .
'<dt>Channel-Versand</dt><dd>Versandwert, der fuer diesen Channel verwendet wird. Ohne eigenen Channel-Wert wird der wirksame Artikel-/Gruppenversand angezeigt und intern weiter geerbt. Ein abweichender Wert wird nur gespeichert, wenn Sie ihn hier bewusst aendern.</dd>'
723 .
'<dt>Listing-ID</dt><dd>Externe ID des sichtbaren Angebots auf der Plattform. Bei eBay entsteht sie normalerweise erst nach dem Publish des Offers. Dieses Feld wird im Normalfall vom Connector nach erfolgreichem Export/Rueckmeldung gefuellt. Manuell eintragen sollten Sie sie nur, wenn ein bereits extern vorhandenes Angebot mit diesem Shop-Artikel verknuepft oder eine Zuordnung repariert werden muss.</dd>'
724 .
'<dt>Offer-ID</dt><dd>Externe Angebots-/Offer-ID der Plattform. Bei eBay ist das Offer der Zwischenschritt zwischen Inventory Item/SKU und dem veroeffentlichten Listing. Die Offer-ID braucht der Connector spaeter zum Aktualisieren, Publishen, Beenden oder Pruefen genau dieses Offers. Bei neuen Artikeln bleibt das Feld leer; nach erfolgreicher Erstellung speichert der Connector die ID automatisch.</dd>'
726 .
'<h3>Provider-Mapping</h3><dl>'
727 .
'<dt>Shop / Custom Aktion</dt><dd>Interner oder Middleware-Endpunkt, an den der Export uebergeben werden soll.</dd>'
728 .
'<dt>eBay Kategorie-ID</dt><dd>eBay Kategorie fuer das Angebot. Sie muss zum Produkt passen, sonst lehnt eBay Pflichtattribute oder das Listing ab.</dd>'
729 .
'<dt>eBay Location-Key</dt><dd>Merchant Location Key des eBay-Verkaeuferkontos. Dieser Wert kommt aus der eBay-Channel-Konfiguration und wird nicht pro Artikel gepflegt. Wenn sich Lager- oder Versandort aendert, wird der Wert in der Channel-Verwaltung geaendert und gilt dann fuer alle eBay-Mappings ohne eigene Sonderlogik.</dd>'
730 .
'<dt>eBay Zustand</dt><dd>Condition-Code oder Zustand des Artikels, z.B. neu oder gebraucht.</dd>'
731 .
'<dt>eBay Policies</dt><dd>Payment-, Fulfillment- und Return-Policy-IDs. Sie bestimmen Zahlung, Versand und Rueckgabe fuer genau dieses Listing.</dd>'
732 .
'<dt>eBay Aspekte</dt><dd>Pflicht- und Zusatzattribute als <code>key=value</code>, z.B. <code>Marke=dbxApp</code>. Ein Wert pro Zeile.</dd>'
733 .
'<dt>Amazon Product Type</dt><dd>Amazon Produkttyp fuer die Listings Items API, z.B. <code>PRODUCT</code> oder eine spezifische Kategorie.</dd>'
734 .
'<dt>Amazon Requirements</dt><dd>Steuert, welche Anforderungen Amazon beim Listing erwartet, z.B. <code>LISTING</code> oder <code>LISTING_PRODUCT_ONLY</code>.</dd>'
735 .
'<dt>Amazon Brand / Attribute</dt><dd>Marke und weitere Attribute als <code>key=value</code>. Welche Werte Pflicht sind, haengt vom Product Type ab.</dd>'
736 .
'<dt>mobile.de Fahrzeugdaten</dt><dd>mobile.de ist fuer Fahrzeuge gedacht. Marke, Modell, Erstzulassung, Kilometer, Kraftstoff und Leistung muessen zum Fahrzeug passen.</dd>'
737 .
'<dt>Kleinanzeigen</dt><dd>Offiziell nur ueber Partner-/Middleware-Schnittstelle sinnvoll. Kategorie, Ort, Kontakt und Attribute werden an die Middleware gegeben.</dd>'
739 .
'<h3>Exportstatus</h3><dl>'
740 .
'<dt>Status</dt><dd>Letzter technischer Exportzustand, z.B. veroeffentlicht, bereit, manuell bereit oder Fehler.</dd>'
741 .
'<dt>Meldung</dt><dd>Antwort oder Fehlermeldung des Connectors.</dd>'
742 .
'<dt>Payload</dt><dd>Gespeicherte technische Antwortdaten zur Fehlersuche.</dd>'
744 .
'<h3>Bar-Aktionen</h3><dl>'
745 .
'<dt>Speichern</dt><dd>Speichert Mapping und Channel-Werte.</dd>'
746 .
'<dt>Exportieren</dt><dd>Startet den Export dieses Artikels fuer diesen Channel.</dd>'
747 .
'<dt>Hilfe</dt><dd>Oeffnet diese CMS-Hilfeseite.</dd>'
752 private function productAttributesHelpHtml():
string {
753 return '<section class="dbx-shop-help">'
754 .
'<h2>Artikelattribute</h2>'
755 .
'<p>Artikelattribute beschreiben gruppenspezifische Merkmale wie Groesse, Farbe, Material oder technische Daten. Die Definition entsteht bei der Artikelgruppe; im Artikel werden nur die Werte gepflegt.</p>'
756 .
'<h3>Attribut-Definition</h3><dl>'
757 .
'<dt>Gruppe</dt><dd>Artikelgruppe, fuer die das Attribut gilt.</dd>'
758 .
'<dt>Key</dt><dd>Technischer Schluessel, z.B. <code>farbe</code>. Er sollte stabil bleiben.</dd>'
759 .
'<dt>Titel</dt><dd>Sichtbarer Name des Attributs.</dd>'
760 .
'<dt>Typ</dt><dd>Text, Zahl oder Auswahlliste.</dd>'
761 .
'<dt>Einheit</dt><dd>Optionale Einheit, z.B. cm, kg, Watt.</dd>'
762 .
'<dt>Optionen</dt><dd>Werte fuer Auswahllisten, getrennt durch <code>|</code>.</dd>'
763 .
'<dt>Pflicht</dt><dd>Markiert Werte, die fuer vollstaendige Produktdaten erwartet werden.</dd>'
764 .
'<dt>Filter</dt><dd>Kann spaeter fuer Katalogfilter verwendet werden.</dd>'
765 .
'<dt>Vergleich</dt><dd>Kann spaeter fuer Produktvergleiche verwendet werden.</dd>'
766 .
'<dt>Sort / Aktiv</dt><dd>Reihenfolge und Freigabe des Attributs.</dd>'
768 .
'<h3>Artikelwert</h3><dl>'
769 .
'<dt>Wert</dt><dd>Konkreter Wert dieses Artikels fuer das Attribut.</dd>'
770 .
'<dt>Einheit</dt><dd>Wird aus der Definition angezeigt.</dd>'
771 .
'<dt>Eigenschaft</dt><dd>Zeigt Pflicht-/Filter-Hinweise aus der Definition.</dd>'
773 .
'<h3>Aktionen</h3><dl>'
774 .
'<dt>Speichern</dt><dd>Speichert die Attributdefinition oder die Artikelwerte.</dd>'
775 .
'<dt>Zurueck</dt><dd>Fuehrt zur Produktliste zurueck.</dd>'
776 .
'<dt>Hilfe</dt><dd>Oeffnet diese CMS-Hilfeseite.</dd>'
781 private function shopMediaHelpHtml():
string {
782 return '<section class="dbx-shop-help">'
783 .
'<h2>Shop-Medien</h2>'
784 .
'<p>Shop-Medien ordnen vorhandene CMS-Medien Artikeln oder Artikelgruppen zu. Die Datei bleibt im CMS-Medienbrowser; dbxShop speichert nur die Verwendung im Shop.</p>'
785 .
'<h3>Felder</h3><dl>'
786 .
'<dt>Artikel</dt><dd>Optionaler einzelner Artikel, dem neue Bildzuordnungen hinzugefuegt werden.</dd>'
787 .
'<dt>Artikelgruppe</dt><dd>Optionale Gruppe. Gruppenbilder koennen bei Artikeln dieser Gruppe als Fallback erscheinen.</dd>'
788 .
'<dt>Sortierung</dt><dd>Reihenfolge der Bilder.</dd>'
789 .
'<dt>Primaerbild</dt><dd>Markiert die neue Zuordnung als Hauptbild.</dd>'
791 .
'<h3>Aktionen</h3><dl>'
792 .
'<dt>Auswahl laden</dt><dd>Laedt die aktuell gewaehlt Artikel-/Gruppenauswahl neu.</dd>'
793 .
'<dt>Auswahl</dt><dd>Oeffnet den CMS-Medienbrowser fuer Upload, Auswahl, Zuschneiden, Resize, Batch und Wartung.</dd>'
794 .
'<dt>Zugeordnete Shop-Bilder</dt><dd>Zeigt vorhandene Artikel- und Gruppenbild-Zuordnungen.</dd>'
795 .
'<dt>Hilfe</dt><dd>Oeffnet diese CMS-Hilfeseite.</dd>'
800 private function loadContentCacheSupport():
void {
801 $bootstrap = dirname(__DIR__, 2) .
'/dbxContent/include/dbxContent_bootstrap.php';
802 if (is_file($bootstrap)) {
803 require_once $bootstrap;
805 $cacheFile = dirname(__DIR__, 2) .
'/dbxContent/include/dbxContentPageCache.class.php';
806 if (is_file($cacheFile)) {
807 require_once $cacheFile;
809 $indexFile = dirname(__DIR__, 2) .
'/dbxContent/include/dbxContentPermalinkIndex.class.php';
810 if (is_file($indexFile)) {
811 require_once $indexFile;
815 private function invalidateShopHelpCache(
int $cid):
void {
819 $this->loadContentCacheSupport();
820 if (class_exists(
'\\dbx\\dbxContent\\dbxContentPageCache')) {
825 private function syncShopHelpPermalinkIndex(
int $cid,
string $permalink,
string $rights =
'admin'):
void {
826 if ($cid <= 0 || $permalink ===
'') {
829 $this->loadContentCacheSupport();
830 if (class_exists(
'\\dbx\\dbxContent\\dbxContentPermalinkIndex')) {
835 private function removeShopHelpPermalinkIndex(
string $permalink):
void {
836 $permalink = trim($permalink);
837 if ($permalink ===
'') {
840 $this->loadContentCacheSupport();
841 if (class_exists(
'\\dbx\\dbxContent\\dbxContentPermalinkIndex')) {
846 private function ensureShopChannelHelpPage():
int {
847 return $this->ensureShopAdminHelpPage(
'help-shop-channel-gruppen',
'Hilfe: Channel-Gruppen',
'Hilfe zur Pflege von Shop Channel-Gruppen.',
'shop,channel,channel-gruppen,hilfe', $this->channelGroupsHelpHtml(),
'9010', array(
'shop/help-channel-groups',
'shop/channel'));
850 private function ensureShopChannelsHelpPage():
int {
851 return $this->ensureShopAdminHelpPage(
'help-shop-channels',
'Hilfe: Channels',
'Hilfe zur Pflege von Shop Channels und Marktplatz-Anbindungen.',
'shop,channels,api,marktplatz,hilfe', $this->channelsHelpHtml(),
'9015', array(
'shop/help-channels',
'shop/channels'));
854 private function ensureShopChannelProviderHelpPage(
string $platform):
int {
855 $allowed = array(
'shop',
'amazon',
'ebay',
'kleinanzeigen',
'mobile',
'custom');
856 if (!in_array($platform,
$allowed,
true)) {
857 $platform =
'custom';
860 'shop' =>
'Hilfe: Channel Shop',
861 'amazon' =>
'Hilfe: Channel Amazon',
862 'ebay' =>
'Hilfe: Channel eBay',
863 'kleinanzeigen' =>
'Hilfe: Channel Kleinanzeigen',
864 'mobile' =>
'Hilfe: Channel mobile.de',
865 'custom' =>
'Hilfe: Eigener Channel',
867 $sort = array(
'shop' =>
'9016',
'amazon' =>
'9017',
'ebay' =>
'9018',
'kleinanzeigen' =>
'9019',
'mobile' =>
'9024',
'custom' =>
'9025');
868 return $this->ensureShopAdminHelpPage(
869 'help-shop-channel-' . $platform,
871 'Hilfe zu API-Daten und Rueckmeldungen fuer ' .
$titles[$platform] .
'.',
872 'shop,channels,' . $platform .
',api,hilfe',
873 $this->channelProviderHelpHtml($platform),
875 array(
'shop/help-channel-' . $platform,
'shop/channels/' . $platform)
879 private function ensureShopProductGroupsHelpPage():
int {
880 return $this->ensureShopAdminHelpPage(
'help-shop-artikelgruppen',
'Hilfe: Artikelgruppen',
'Hilfe zur Pflege von Shop Artikelgruppen.',
'shop,artikelgruppen,produktgruppen,hilfe', $this->productGroupsHelpHtml(),
'9020', array(
'shop/help-groups',
'shop/groups'));
883 private function ensureShopShippingGroupsHelpPage():
int {
884 return $this->ensureShopAdminHelpPage(
'help-shop-versandgruppen',
'Hilfe: Versandgruppen',
'Hilfe zur Pflege von Shop Versandgruppen.',
'shop,versandgruppen,shipping,hilfe', $this->shippingGroupsHelpHtml(),
'9030', array(
'shop/help-shipping-groups',
'shop/shipping-groups'));
887 private function ensureShopSettingsHelpPage():
int {
888 return $this->ensureShopAdminHelpPage(
'help-shop-einstellungen',
'Hilfe: Shop-Einstellungen',
'Hilfe zur Pflege der globalen Shop-Einstellungen.',
'shop,einstellungen,mwst,zahlung,versand,hilfe', $this->settingsHelpHtml(),
'9040', array(
'shop/help-settings',
'shop/settings'));
891 private function ensureShopOrdersHelpPage():
int {
892 return $this->ensureShopAdminHelpPage(
'help-shop-bestellungen',
'Hilfe: Bestellungen',
'Hilfe zur Bearbeitung von Shop- und Channel-Bestellungen.',
'shop,bestellungen,orders,zahlung,channel,hilfe', $this->ordersHelpHtml(),
'9050', array(
'shop/help-orders',
'shop/orders'));
895 private function ensureShopProductsHelpPage():
int {
896 return $this->ensureShopAdminHelpPage(
'help-shop-produkte',
'Hilfe: Produkte',
'Hilfe zur Produktliste und zum Artikel-Formular.',
'shop,produkte,artikel,formular,hilfe', $this->productsHelpHtml(),
'9005', array(
'shop/help-products',
'shop/products'));
899 private function ensureShopProductChannelMappingHelpPage():
int {
900 return $this->ensureShopAdminHelpPage(
'help-shop-produkt-channel-mapping',
'Hilfe: Channel-Mapping je Artikel',
'Hilfe zur produktbezogenen Channel-Mapping-Maske.',
'shop,produkte,channels,mapping,hilfe', $this->productChannelMappingHelpHtml(),
'9014', array(
'shop/help-product-channel-mapping',
'shop/product-channel-mapping'));
903 private function ensureShopProductAttributesHelpPage():
int {
904 return $this->ensureShopAdminHelpPage(
'help-shop-artikelattribute',
'Hilfe: Artikelattribute',
'Hilfe zu Attributdefinitionen und Artikelwerten.',
'shop,produkte,attribute,hilfe', $this->productAttributesHelpHtml(),
'9028', array(
'shop/help-product-attributes',
'shop/product-attributes'));
907 private function ensureShopMediaHelpPage():
int {
908 return $this->ensureShopAdminHelpPage(
'help-shop-medien',
'Hilfe: Shop-Medien',
'Hilfe zur Medienzuordnung fuer Shop-Artikel und Artikelgruppen.',
'shop,medien,bilder,hilfe', $this->shopMediaHelpHtml(),
'9035', array(
'shop/help-media',
'shop/media-help'));
911 private function ensureShopAdminHelpPage(
string $permalink,
string $title,
string $description,
string $keywords,
string $content,
string $sorter, array $oldPermalinks = array()):
int {
915 $permalinks = array_values(array_unique(array_merge(array($permalink), $oldPermalinks)));
916 foreach ($permalinks as $candidatePermalink) {
917 $candidatePermalink = trim((
string)$candidatePermalink);
918 if ($candidatePermalink ===
'') {
921 $row =
$db->select1(
$contentDd, array(
'permalink' => $candidatePermalink),
'id,content,permalink,title,description,keywords,sorter', 0);
922 if (is_array($row) && (
int)($row[
'id'] ?? 0) > 0) {
923 $id = (int)$row[
'id'];
924 if (!$this->maintenanceMode) {
927 $needsUpdate = trim((
string)($row[
'content'] ??
'')) !== trim(
$content)
928 || trim((
string)($row[
'permalink'] ??
'')) !== $permalink
929 || trim((
string)($row[
'title'] ??
'')) !== $title
930 || trim((
string)($row[
'description'] ??
'')) !== $description
931 || trim((
string)($row[
'keywords'] ??
'')) !== $keywords
932 || trim((
string)($row[
'sorter'] ??
'')) !== $sorter
933 || $candidatePermalink !== $permalink;
935 $this->invalidateShopHelpCache($id);
937 'permalink' => $permalink,
939 'description' => $description,
940 'keywords' => $keywords,
943 'group_read' =>
'admin',
946 'meta_robots' =>
'noindex,nofollow',
949 foreach ($oldPermalinks as $oldPermalink) {
950 $this->removeShopHelpPermalinkIndex((
string)$oldPermalink);
952 $this->syncShopHelpPermalinkIndex($id, $permalink,
'admin');
957 if (!$this->maintenanceMode) {
961 $folderId = $this->ensureShopHelpFolder();
965 'folder' => $folderId,
966 'group_read' =>
'admin',
969 'permalink' => $permalink,
970 'description' => $description,
971 'keywords' => $keywords,
972 'template' =>
'c-body1-footer',
974 'meta_robots' =>
'noindex,nofollow',
978 $id = (int)
$db->get_insert_id();
979 $this->invalidateShopHelpCache($id);
980 $this->syncShopHelpPermalinkIndex($id, $permalink,
'admin');
983 }
catch (\Throwable $e) {
984 if (function_exists(
'dbx')) {
985 dbx()->debug(
'dbxShop help page failed', $e->getMessage());
992 private function ensureShopMediaUsagePage():
int {
994 $contentDd = $this->shopMediaUsageContentDd();
996 $row =
$db->select1(
$contentDd, array(
'permalink' =>
'shop-medienverwendung'),
'id', 0);
997 if (!is_array($row)) {
998 $row =
$db->select1(
$contentDd, array(
'permalink' =>
'outside/shop-media-usage'),
'id', 0);
999 if ($this->maintenanceMode && is_array($row) && (
int)($row[
'id'] ?? 0) > 0) {
1000 $db->update(
$contentDd, array(
'permalink' =>
'shop-medienverwendung'), (
int)$row[
'id']);
1003 if (is_array($row) && (
int)($row[
'id'] ?? 0) > 0) {
1004 return (
int)$row[
'id'];
1007 if (!$this->maintenanceMode) {
1012 $folder =
$db->select1($this->shopMediaUsageFolderDd(), array(
'name' =>
'outside'),
'id', 0);
1013 if (is_array($folder)) {
1014 $folderId = (int)($folder[
'id'] ?? 0);
1020 'folder' => $folderId,
1021 'group_read' =>
'admin',
1023 'title' =>
'Shop Medienverwendung',
1024 'permalink' =>
'shop-medienverwendung',
1025 'description' =>
'Interne Seite fuer Shop-Medienverwendung.',
1027 'template' =>
'c-body1-footer',
1028 'content' =>
'<p>Interne Seite fuer Shop-Medienverwendung.</p>',
1029 'meta_robots' =>
'noindex,nofollow',
1033 $id = (int)
$db->get_insert_id();
1036 }
catch (\Throwable $e) {
1037 if (function_exists(
'dbx')) {
1038 dbx()->debug(
'dbxShop media_usage page failed', $e->getMessage());
1045 private function shopMediaUsageSlot():
string {
1046 $slot = strtolower(trim((
string)
dbx()->get_config(
'dbxShop',
'media_usage_slot')));
1047 $allowed = array(
'shop',
'hero',
'gallery',
'inline',
'header',
'teaser',
'footer');
1048 return in_array($slot,
$allowed,
true) ? $slot :
'shop';
1051 private function shopMediaUsageSorter(
$db,
int $contentId,
string $slot):
string {
1052 $where = dbxContentMediaUsageScope::withLanguage(
1053 'content_id = ' . $contentId .
" AND slot = '" . str_replace(
"'",
"''", $slot) .
"' AND active = 1",
1054 $this->shopMediaUsageLng()
1056 $rows =
$db->select(
'dbxMediaUsage', $where,
'sorter,id',
'sorter,id',
'DESC',
'', 1, 0, 0);
1059 $max = (int)(
$rows[0][
'sorter'] ?? 0);
1061 return sprintf(
'%04d', $max + 10);
1064 private function shopMediaFolderPath():
string {
1066 return rtrim(
$base,
'/\\') .
'/media/img/shop';
1069 private function normalizeShopSourceImagePath(
string $path):
string {
1070 $path = trim(str_replace(
'\\',
'/', $path));
1071 $path = preg_replace(
'~^https?://[^/]+/~i',
'', $path) ?: $path;
1072 $path = preg_replace(
'~^/?dbxapp/~i',
'', $path) ?: $path;
1073 return ltrim($path,
'/');
1076 private function filePathForShopImage(
string $path):
string {
1077 $path = $this->normalizeShopSourceImagePath($path);
1078 if ($path ===
'' || strpos($path,
'..') !==
false || preg_match(
'~(^|/)\.~', $path)) {
1081 if (preg_match(
'~^files/(.+)$~i', $path, $m)) {
1084 if (preg_match(
'~^(media|shop)/~i', $path)) {
1090 private function mediaMime(
string $file):
string {
1091 $mime = function_exists(
'mime_content_type') ? (string)@mime_content_type(
$file) :
'';
1096 $ext = strtolower(pathinfo(
$file, PATHINFO_EXTENSION));
1098 'jpg' =>
'image/jpeg',
1099 'jpeg' =>
'image/jpeg',
1100 'png' =>
'image/png',
1101 'gif' =>
'image/gif',
1102 'webp' =>
'image/webp',
1103 'svg' =>
'image/svg+xml',
1105 return $map[$ext] ??
'application/octet-stream';
1108 private function ensureMediaRecordForShopImage(array $image):
int {
1109 $mediaId = (int)($image[
'media_id'] ?? 0);
1114 $sourcePath = $this->normalizeShopSourceImagePath((
string)($image[
'image_path'] ??
''));
1115 if ($sourcePath ===
'' || stripos($sourcePath,
'dbxmedia:') === 0) {
1119 $sourceFile = $this->filePathForShopImage($sourcePath);
1125 $name = preg_replace(
'/[^A-Za-z0-9_.-]+/',
'-', $name) ?: (
'shop-image-' . (int)($image[
'id'] ?? 0));
1126 $targetDir = $this->shopMediaFolderPath();
1127 if (!is_dir($targetDir)) {
1128 @mkdir($targetDir, 0775,
true);
1130 $targetFile = rtrim($targetDir,
'/\\') .
'/' . $name;
1131 if (!is_file($targetFile)) {
1134 if (!is_file($targetFile)) {
1138 $rel =
'media/img/shop/' . $name;
1140 $existing =
$db->select1(
'dbxMedia', array(
'file_path' => $rel),
'id,active', 0);
1141 if (is_array($existing) && (
int)($existing[
'id'] ?? 0) > 0) {
1143 if ((
int)($existing[
'active'] ?? 0) !== 1) {
1144 $db->update(
'dbxMedia', array(
'active' => 1),
$mediaId);
1146 $this->repo()->updateImageMediaReference((
int)($image[
'id'] ?? 0),
$mediaId,
'dbxmedia:' .
$mediaId);
1152 $size = @getimagesize($targetFile);
1153 if (is_array($size)) {
1154 $width = (int)($size[0] ?? 0);
1155 $height = (int)($size[1] ?? 0);
1157 $title = trim((
string)($image[
'title'] ??
''));
1158 if ($title ===
'') {
1159 $title = pathinfo($name, PATHINFO_FILENAME);
1161 $mime = $this->mediaMime($targetFile);
1171 'alt' => (
string)($image[
'alt'] ?? $title),
1173 'file_name' => $name,
1174 'file_path' => $rel,
1176 'size' => (
int)@filesize($targetFile),
1178 'height' => $height,
1180 'media_type' =>
'image',
1181 'storage_type' =>
'local',
1182 'media_folder' =>
'img/shop',
1184 $ok = (int)
$db->insert(
'dbxMedia', $insert);
1193 $this->repo()->updateImageMediaReference((
int)($image[
'id'] ?? 0),
$mediaId,
'dbxmedia:' .
$mediaId);
1198 private function migrateExistingShopImagesToMedia():
void {
1199 foreach ($this->repo()->allImages() as $image) {
1200 if ((
int)($image[
'active'] ?? 0) !== 1) {
1203 $this->ensureMediaRecordForShopImage($image);
1207 private function syncShopMediaUsage():
void {
1209 $contentId = $this->shopMediaUsageContentId();
1210 if ($contentId <= 0) {
1214 $slot = $this->shopMediaUsageSlot();
1215 $sourceNeedle =
'%"source":"dbxShop"%';
1217 $this->migrateExistingShopImagesToMedia();
1224 "slot = 'shop' OR settings LIKE '" . str_replace(
"'",
"''", $sourceNeedle) .
"'",
1230 foreach ($this->repo()->allImages() as $image) {
1231 if ((
int)($image[
'active'] ?? 0) !== 1) {
1234 $mediaId = (int)($image[
'media_id'] ?? 0);
1241 'title' => (
string)($image[
'title'] ??
''),
1242 'product_ids' => array(),
1243 'group_ids' => array(),
1246 $productId = (int)($image[
'product_id'] ?? 0);
1247 $groupId = (int)($image[
'group_id'] ?? 0);
1248 if ($productId > 0) {
1249 $byMedia[
$mediaId][
'product_ids'][$productId] = $productId;
1252 $byMedia[
$mediaId][
'group_ids'][$groupId] = $groupId;
1256 foreach ($byMedia as
$mediaId => $info) {
1257 $settings = json_encode(array(
1258 'source' =>
'dbxShop',
1259 'product_ids' => array_values($info[
'product_ids']),
1260 'group_ids' => array_values($info[
'group_ids']),
1261 ), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
1262 $db->insert(
'dbxMediaUsage', array(
1265 'content_id' => $contentId,
1267 'content_lng' => $this->shopMediaUsageLng(),
1269 'sorter' => $this->shopMediaUsageSorter(
$db, $contentId, $slot),
1270 'template' =>
'image-gallery',
1271 'caption' => (
string)($info[
'title'] ??
''),
1272 'settings' => $settings ?:
'{"source":"dbxShop"}',
1275 }
catch (\Throwable $e) {
1276 if (function_exists(
'dbx')) {
1277 dbx()->debug(
'dbxShop media_usage sync failed', $e->getMessage());
1286 private function maintainShopAdminContent():
void {
1287 $this->ensureCmsShopMediaFolder();
1288 $this->ensureShopMediaUsagePage();
1289 $this->ensureShopChannelHelpPage();
1290 $this->ensureShopChannelsHelpPage();
1291 foreach (array(
'shop',
'amazon',
'ebay',
'kleinanzeigen',
'mobile',
'custom') as $platform) {
1292 $this->ensureShopChannelProviderHelpPage($platform);
1294 $this->ensureShopProductGroupsHelpPage();
1295 $this->ensureShopShippingGroupsHelpPage();
1296 $this->ensureShopSettingsHelpPage();
1297 $this->ensureShopOrdersHelpPage();
1298 $this->ensureShopProductsHelpPage();
1299 $this->ensureShopProductChannelMappingHelpPage();
1300 $this->ensureShopProductAttributesHelpPage();
1301 $this->ensureShopMediaHelpPage();
1302 $this->syncShopMediaUsage();
1305 private function chips(
1307 string $key =
'title',
1309 string $emptyLabel =
'keine Werte'
1312 foreach (
$items as $item) {
1313 $value = trim((
string)($item[$key] ??
''));
1314 if (
$value ===
'')
continue;
1315 $html .=
'<span class="badge text-bg-light border">' . $this->h(
$value) .
'</span>';
1318 return '<span class="text-muted small">' . $this->h($emptyLabel) .
'</span>';
1321 return '<div class="' . $this->h(
$class) .
'">' .
$html .
'</div>';
1324 private function attributeBadges(
1326 string $emptyLabel =
'keine Werte'
1329 foreach (($product[
'attributes'] ?? array()) as $attribute) {
1330 $value = trim((
string)($attribute[
'display_value'] ?? $attribute[
'value_text'] ??
''));
1331 if (
$value ===
'')
continue;
1332 $html .=
'<span class="badge text-bg-info text-start text-wrap">' . $this->h($attribute[
'title'] ??
'') .
': ' . $this->h(
$value) .
'</span>';
1335 ?
'<div class="dbx-shop-report-chip-grid dbx-shop-report-chip-grid-attributes">' .
$html .
'</div>'
1336 :
'<span class="text-muted small">' . $this->h($emptyLabel) .
'</span>';
1339 private function attributeOptions(
string $options): array {
1341 return array_values(array_filter(array_map(
'trim',
$items), fn($item) => $item !==
''));
1344 private function optionsHtml(array
$options,
string $selected):
string {
1348 $html .=
'<option value="' . $this->h(
$value) .
'"' . (
$value === $selected ?
' selected' :
'') .
'>' . $this->h($label) .
'</option>';
1353 private function shopConfig(): array {
1354 $cfg =
dbx()->get_config(
'dbxShop');
1355 return is_array(
$cfg) ?
$cfg : array();
1358 private function channelsEnabled():
bool {
1359 $value = strtolower(trim((
string)
dbx()->get_config(
'dbxShop',
'channels_enabled')));
1360 return !in_array(
$value, array(
'0',
'false',
'off',
'no',
'nein'),
true);
1363 private function taxRatesConfig(): array {
1364 $cfg = $this->shopConfig();
1365 $rates =
$cfg[
'tax_rates'] ?? array();
1366 if (!is_array($rates) || !count($rates)) {
1368 'mwst1' => array(
'title' =>
'MwSt. normal',
'rate' =>
'19'),
1369 'mwst2' => array(
'title' =>
'MwSt. ermaessigt',
'rate' =>
'7'),
1370 'mwst3' => array(
'title' =>
'MwSt. vorbereitet',
'rate' =>
'22'),
1373 foreach (array(
'mwst1' =>
'MwSt. normal',
'mwst2' =>
'MwSt. ermaessigt',
'mwst3' =>
'MwSt. vorbereitet') as $key => $title) {
1374 if (!isset($rates[$key]) || !is_array($rates[$key])) {
1375 $rates[$key] = array(
'title' => $title,
'rate' => $key ===
'mwst2' ?
'7' : ($key ===
'mwst3' ?
'22' :
'19'));
1381 private function taxClassOptions(
string $selected):
string {
1383 foreach ($this->taxRatesConfig() as $key => $rate) {
1384 if (!is_array($rate))
continue;
1385 $label = trim((
string)($rate[
'title'] ?? $key));
1386 $value = number_format((
float)($rate[
'rate'] ?? 0), 2,
',',
'.');
1389 return $this->optionsHtml(
$options, $selected !==
'' ? $selected : (
string)($this->shopConfig()[
'default_tax_class'] ??
'mwst1'));
1392 private function normalizedText(
string $value):
string {
1394 $value = strtr(
$value, array(
'ä' =>
'ae',
'ö' =>
'oe',
'ü' =>
'ue',
'ß' =>
'ss'));
1395 $value = preg_replace(
'~[^a-z0-9]+~',
' ',
$value) ?:
'';
1396 return preg_replace(
'~\\s+~',
' ', trim(
$value)) ?:
'';
1399 private function searchTerms(
string $query): array {
1400 $terms = preg_split(
'~\\s+~', $this->normalizedText($query)) ?: array();
1401 $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'));
1403 foreach ($terms as $term) {
1404 $term = trim($term);
1405 if ($term ===
'' || isset($stopWords[$term])) {
1408 if (strlen($term) < 2 && !ctype_digit($term)) {
1413 return array_keys(
$out);
1416 private function productAttributeText(array $product):
string {
1418 foreach (($product[
'attributes'] ?? array()) as $attribute) {
1419 $value = trim((
string)($attribute[
'display_value'] ?? $attribute[
'value_text'] ??
''));
1420 $parts[] = (string)($attribute[
'title'] ??
'');
1421 $parts[] = (string)($attribute[
'attr_key'] ??
'');
1426 return implode(
' ', $parts);
1429 private function productGroupText(array $product):
string {
1431 foreach (($product[
'groups'] ?? array()) as $group) {
1432 $parts[] = (string)($group[
'title'] ??
'');
1433 $parts[] = (string)($group[
'group_key'] ??
'');
1434 $parts[] = (string)($group[
'description'] ??
'');
1435 $parts[] = (string)($group[
'attribute_notes'] ??
'');
1437 foreach (($product[
'shipping_groups'] ?? array()) as $group) {
1438 $parts[] = (string)($group[
'title'] ??
'');
1439 $parts[] = (string)($group[
'group_key'] ??
'');
1441 foreach (($product[
'channel_groups'] ?? array()) as $group) {
1442 $parts[] = (string)($group[
'title'] ??
'');
1443 $parts[] = (string)($group[
'group_key'] ??
'');
1445 foreach (($product[
'channels'] ?? array()) as $channel) {
1446 $parts[] = (string)($channel[
'title'] ??
'');
1447 $parts[] = (string)($channel[
'channel_key'] ??
'');
1449 return implode(
' ', $parts);
1452 private function searchFieldScore(
string $text,
string $term,
int $weight):
int {
1453 if ($text ===
'' || $term ===
'') {
1456 if ($text === $term) {
1459 $termLength = strlen($term);
1460 $compactText = str_replace(
' ',
'', $text);
1461 $compactTerm = str_replace(
' ',
'', $term);
1462 if (strpos($text, $term) !==
false || strpos($compactText, $compactTerm) !==
false) {
1466 foreach (preg_split(
'~\\s+~', $text) ?: array() as
$token) {
1472 $best = max($best, $weight * 6);
1475 if ($termLength < 3) {
1478 if (strlen(
$token) >= $termLength && strpos(
$token, $term) === 0) {
1479 $best = max($best, $weight * 4);
1485 && substr(
$token, 0, 3) === substr($term, 0, 3)
1486 && abs(strlen(
$token) - $termLength) <= ($termLength >= 7 ? 2 : 1)
1487 && levenshtein(
$token, $term) <= ($termLength >= 7 ? 2 : 1)
1489 $best = max($best, $weight * 2);
1495 private function productSearchScore(array $product,
string $query):
int {
1496 $terms = $this->searchTerms($query);
1497 if ($terms === array()) {
1501 $primary = $this->normalizedText(implode(
' ', array(
1502 (
string)($product[
'sku'] ??
''),
1503 (
string)($product[
'title'] ??
''),
1504 (
string)($product[
'category'] ??
''),
1505 (
string)($product[
'badge'] ??
''),
1506 (
string)($product[
'product_type'] ??
''),
1508 $secondary = $this->normalizedText(implode(
' ', array(
1509 (
string)($product[
'summary'] ??
''),
1510 (
string)($product[
'description'] ??
''),
1512 $attributes = $this->normalizedText($this->productAttributeText($product));
1513 $groups = $this->normalizedText($this->productGroupText($product));
1517 $firstTermPrimaryScore = 0;
1518 $termCount = count($terms);
1520 foreach ($terms as $idx => $term) {
1521 $primaryScore = $this->searchFieldScore($primary, $term, 10);
1524 $this->searchFieldScore($attributes, $term, 7),
1525 $this->searchFieldScore($secondary, $term, 4),
1526 $this->searchFieldScore(
$groups, $term, 3)
1530 $firstTermPrimaryScore = $primaryScore;
1532 if ($termScore > 0) {
1534 $score += $termScore;
1538 if ($matched === 0) {
1541 if ($termCount === 1) {
1544 if ($matched === $termCount || $firstTermPrimaryScore > 0 || $score >= 20) {
1545 return $score + ($matched * 3);
1550 private function productSortValue(array $product,
string $sort) {
1554 return $this->normalizedText((
string)($product[$sort] ??
''));
1556 case 'effective_tax_rate':
1557 case 'effective_shipping_gross':
1558 return (
float)($product[$sort] ?? 0);
1560 return (
int)($product[
'active'] ?? 0);
1563 return (
int)($product[
'sorter'] ?? 100);
1567 private function sortProductsForReport(array $products,
string $query,
string $sort,
string $direction): array {
1568 $hasQuery = $this->searchTerms($query) !== array();
1569 $direction = strtoupper($direction) ===
'DESC' ?
'DESC' :
'ASC';
1570 usort($products,
function(array $a, array $b) use ($hasQuery, $sort, $direction):
int {
1571 if ($hasQuery && (
int)($a[
'_search_score'] ?? 0) !== (
int)($b[
'_search_score'] ?? 0)) {
1572 return (
int)($b[
'_search_score'] ?? 0) <=> (
int)($a[
'_search_score'] ?? 0);
1575 $av = $this->productSortValue($a, $sort);
1576 $bv = $this->productSortValue($b, $sort);
1578 return strcasecmp((
string)($a[
'title'] ??
''), (
string)($b[
'title'] ??
''));
1580 $cmp = is_numeric($av) && is_numeric($bv) ? ($av <=> $bv) : strcasecmp((
string)$av, (
string)$bv);
1581 return $direction ===
'DESC' ? -$cmp : $cmp;
1587 if (!is_array($record)) {
1591 $sku = (string)($record[
'sku'] ??
'');
1592 $productId = (int)($record[
'id'] ?? 0);
1593 $productTitle = trim((
string)($record[
'title'] ?? $sku));
1594 $summary = trim((
string)($record[
'summary'] ??
''));
1595 $image = $record[
'images'][0] ?? array();
1596 $imgUrl = is_array($image) ? $this->mediaItemUrl($image,
true) :
'';
1597 $emptyLabel =
$report->get_fd_message(
'no_values');
1599 $record[
'image_view'] = $imgUrl !==
''
1600 ?
'<span class="dbx-shop-report-image"><img src="' . $this->h($imgUrl) .
'" alt="" loading="lazy"></span>'
1601 :
'<span class="text-muted small">' . $this->h(
$report->get_fd_message(
'no_image')) .
'</span>';
1602 $record[
'article_view'] =
'<div class="dbx-shop-report-article-scroll"><code class="dbx-shop-report-sku">' . $this->h($sku) .
'</code>'
1603 .
'<br><strong>' . $this->h($productTitle) .
'</strong>'
1604 . ($summary !==
'' ?
'<br><small class="text-muted">' . $this->h($summary) .
'</small>' :
'')
1606 $record[
'groups_view'] = $this->chips($record[
'groups'] ?? array(),
'title',
'', $emptyLabel);
1607 $record[
'attributes_view'] = $this->attributeBadges($record, $emptyLabel);
1608 $record[
'shipping_groups_view'] = $this->chips($record[
'shipping_groups'] ?? array(),
'title',
'', $emptyLabel);
1609 $record[
'channel_groups_view'] = $this->chips($record[
'channel_groups'] ?? array(),
'title',
'', $emptyLabel);
1610 $record[
'channels_view'] = $this->chips($record[
'channels'] ?? array(),
'title',
'dbx-shop-report-chip-grid-channels', $emptyLabel);
1611 $record[
'price_view'] =
'<span class="text-nowrap">' . $this->money($record[
'price_gross'] ?? 0) .
'</span>';
1612 $record[
'tax_view'] = number_format((
float)($record[
'effective_tax_rate'] ?? 0), 2,
',',
'.') .
'%';
1613 $record[
'shipping_view'] =
'<span class="text-nowrap">' . $this->money($record[
'effective_shipping_gross'] ?? 0) .
'</span>';
1614 $record[
'status_view'] = ((int)($record[
'active'] ?? 0) === 1)
1615 ?
'<span class="badge text-bg-success">' . $this->h(
$report->get_fd_message(
'status_active')) .
'</span>'
1616 :
'<span class="badge text-bg-secondary">' . $this->h(
$report->get_fd_message(
'status_inactive')) .
'</span>';
1622 if (!is_array($data) || !isset($data[
'data']) || !is_array($data[
'data'])) {
1626 $type = (string)($data[
'type'] ??
'');
1627 $record = is_array($data[
'record'] ??
null) ? $data[
'record'] : array();
1628 $rid = (int)($data[
'data'][
'rid'] ?? $record[
'id'] ?? 0);
1629 $sku = (string)($record[
'sku'] ??
'');
1630 $title = trim((
string)($record[
'title'] ?? $sku));
1632 if ($type ===
'edit') {
1633 $url =
'?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id=' . $rid;
1634 $data[
'data'][
'action'] =
$url;
1635 $data[
'data'][
'class'] =
'openWin dbx-win';
1636 $data[
'data'][
'tooltip'] =
$report->format_fd_message(
1638 array(
'title' => $title)
1640 } elseif ($type ===
'show') {
1641 $url =
'?dbx_modul=dbxShop&dbx_run1=product&sku=' . rawurlencode($sku);
1642 $data[
'data'][
'action'] =
$url;
1643 $data[
'data'][
'class'] =
'openWin dbx-win';
1644 $data[
'data'][
'tooltip'] =
$report->format_fd_message(
1646 array(
'title' => $title)
1648 } elseif ($type ===
'delete') {
1649 $data[
'data'][
'action'] =
'?dbx_modul=dbxShop_admin&dbx_run1=products';
1650 $data[
'data'][
'confirm'] =
$report->format_fd_message(
1651 'action_delete_confirm',
1652 array(
'title' => $title)
1659 private function productTreePanel(array $products,
$texts): string {
1660 $groups = $this->repo()->groups();
1661 $groupsByParent = array();
1663 $parentId = (int)($group[
'parent_id'] ?? 0);
1664 $groupsByParent[$parentId][] = $group;
1667 $productsByGroup = array();
1668 foreach ($products as $product) {
1669 $groupId = (int)($product[
'product_group_id'] ?? 0);
1670 if ($groupId <= 0 && isset($product[
'groups'][0])) {
1671 $groupId = (int)($product[
'groups'][0][
'id'] ?? 0);
1673 $productsByGroup[$groupId][] = $product;
1676 $renderProducts =
function(
int $groupId,
bool $asListItem =
false) use (&$productsByGroup,
$texts):
string {
1678 foreach (($productsByGroup[$groupId] ?? array()) as $product) {
1679 $id = (int)($product[
'id'] ?? 0);
1680 $title = trim((
string)($product[
'title'] ??
$texts->get_fd_message(
'tree_product_fallback')));
1681 $sku = trim((
string)($product[
'sku'] ??
''));
1682 if ($id <= 0)
continue;
1683 $searchText = trim($title .
' ' . $sku);
1684 $items .=
'<li class="dbx-shop-tree-product" draggable="true" data-shop-tree-node="product" data-shop-tree-product="' . $id .
'" data-shop-tree-search-text="' . $this->h($searchText) .
'">';
1685 $items .=
'<span class="dbx-shop-tree-product-main"><i class="bi bi-box-seam"></i><span><strong>' . $this->h($title) .
'</strong>' . ($sku !==
'' ?
'<small>' . $this->h($sku) .
'</small>' :
'') .
'</span></span>';
1686 $items .=
'<a class="btn btn-outline-primary btn-sm openWin dbx-win" href="?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id=' . $id .
'" title="' . $this->h(
$texts->get_fd_message(
'tree_edit_product')) .
'"><i class="bi bi-pencil"></i></a>';
1692 $html =
'<ul class="dbx-shop-tree-products">' .
$items .
'</ul>';
1693 return $asListItem ?
'<li class="dbx-shop-tree-product-list">' .
$html .
'</li>' :
$html;
1696 $renderGroup =
function(array $group) use (&$renderGroup, &$groupsByParent, $renderProducts,
$texts):
string {
1697 $id = (int)($group[
'id'] ?? 0);
1698 if ($id <= 0)
return '';
1699 $title = trim((
string)($group[
'title'] ??
$texts->get_fd_message(
'tree_group_fallback')));
1701 foreach (($groupsByParent[$id] ?? array()) as $child) {
1702 $childHtml .= $renderGroup($child);
1704 $productsHtml = $renderProducts($id,
true);
1705 $countChildren = count($groupsByParent[$id] ?? array());
1706 $countProducts = substr_count($productsHtml,
'data-shop-tree-node="product"');
1707 $hasChildren = $childHtml !==
'' || $productsHtml !==
'';
1708 $html =
'<li class="dbx-shop-tree-group" data-shop-tree-group-wrap data-shop-tree-search-text="' . $this->h($title) .
'">';
1709 $html .=
'<div class="dbx-shop-tree-group-head" draggable="true" data-shop-tree-node="group" data-shop-tree-group="' . $id .
'" data-shop-tree-drop="' . $id .
'">';
1710 $html .=
'<span class="dbx-shop-tree-group-main">';
1712 $html .=
'<button type="button" class="dbx-shop-tree-group-toggle" data-shop-tree-group-toggle title="' . $this->h(
$texts->get_fd_message(
'tree_toggle_group')) .
'" aria-label="' . $this->h(
$texts->get_fd_message(
'tree_toggle_group')) .
'" aria-expanded="true"><i class="bi bi-chevron-down"></i></button>';
1714 $html .=
'<span class="dbx-shop-tree-toggle-spacer"></span>';
1716 $html .=
'<i class="bi bi-folder2"></i><span><strong>' . $this->h($title) .
'</strong><small>' . $this->h(
$texts->format_fd_message(
'tree_counts', array(
'groups' => $countChildren,
'products' => $countProducts))) .
'</small></span></span>';
1717 $html .=
'<a class="btn btn-outline-secondary btn-sm openWin dbx-win" href="?dbx_modul=dbxShop_admin&dbx_run1=groups" data-url="?dbx_modul=dbxShop_admin&dbx_run1=groups" data-title="' . $this->h(
$texts->get_fd_message(
'tree_edit_groups')) .
'" data-width="54%" data-height="84%" title="' . $this->h(
$texts->get_fd_message(
'tree_edit_groups')) .
'"><i class="bi bi-diagram-3"></i></a>';
1720 $html .=
'<ul class="dbx-shop-tree-children">' . $childHtml . $productsHtml .
'</ul>';
1727 foreach (($groupsByParent[0] ?? array()) as $group) {
1728 $rootGroups .= $renderGroup($group);
1730 $ungrouped = $renderProducts(0,
false);
1732 'name' =>
'shop_tree_search',
1733 'placeholder' =>
$texts->get_fd_message(
'tree_search_placeholder'),
1734 'title' =>
$texts->get_fd_message(
'tree_search_title'),
1735 'wrap_class' =>
'dbx-shop-tree-search-wrap',
1736 'extra_attrs' =>
'data-shop-tree-search',
1739 $treeMoveUrl = str_replace(
'&',
'&', $this->actionUrl(
'?dbx_modul=dbxShop_admin&dbx_run1=product_tree_move'));
1740 $html =
'<section class="dbx-shop-product-tree-panel" data-shop-tree-panel data-shop-tree-moveurl="' . $treeMoveUrl .
'" aria-label="' . $this->h(
$texts->get_fd_message(
'tree_aria')) .
'">';
1741 $html .=
'<div class="dbx-shop-product-tree-head"><div><h3>' . $this->h(
$texts->get_fd_message(
'tree_title')) .
'</h3><p>' . $this->h(
$texts->get_fd_message(
'tree_subtitle')) .
'</p></div><div class="dbx-shop-product-tree-actions"><a class="btn btn-outline-primary btn-sm openWin dbx-win" href="?dbx_modul=dbxShop_admin&dbx_run1=groups" data-url="?dbx_modul=dbxShop_admin&dbx_run1=groups" data-title="' . $this->h(
$texts->get_fd_message(
'tree_edit_groups')) .
'" data-width="54%" data-height="84%"><i class="bi bi-diagram-3"></i> ' . $this->h(
$texts->get_fd_message(
'tree_edit_groups_button')) .
'</a><button type="button" class="btn btn-outline-secondary btn-sm" data-shop-tree-close title="' . $this->h(
$texts->get_fd_message(
'tree_close')) .
'" aria-label="' . $this->h(
$texts->get_fd_message(
'tree_close')) .
'"><i class="bi bi-x-lg"></i></button></div></div>';
1742 $html .=
'<div class="dbx-shop-tree-tools">' . $search .
'</div>';
1743 $html .=
'<ul class="dbx-shop-tree-list">' . $rootGroups .
'</ul>';
1744 if ($ungrouped !==
'') {
1745 $html .=
'<div class="dbx-shop-tree-ungrouped"><strong>' . $this->h(
$texts->get_fd_message(
'tree_ungrouped')) .
'</strong>' . $ungrouped .
'</div>';
1747 $html .=
'</section>';
1751 private function productTreeToggleButton(
$texts): string {
1752 $label = $this->h(
$texts->get_fd_message(
'tree_open'));
1753 return '<button type="button" class="btn btn-outline-secondary btn-sm dbx-shop-product-tree-toggle" data-dbx="lib=shopAdmin" data-shop-tree-toggle title="' . $label .
'" aria-label="' . $label .
'" aria-expanded="false"><i class="bi bi-diagram-3"></i></button>';
1756 private function selectedProductIds(
$report): array {
1758 foreach (array_keys(
$report->get_multi_selects()) as $id) {
1764 return array_values($ids);
1767 private function productReportActionControls(
string $baseAction,
$texts): string {
1768 $channels =
'<option value="">' . $this->h(
$texts->get_fd_message(
'bulk_channel_placeholder')) .
'</option>';
1769 foreach ($this->repo()->channels() as $channel) {
1770 $key = (string)($channel[
'channel_key'] ??
'');
1774 $channels .=
'<option value="' . $this->h($key) .
'">' . $this->h($channel[
'title'] ?? $key) .
'</option>';
1777 $groups =
'<option value="0">' . $this->h(
$texts->get_fd_message(
'bulk_group_placeholder')) .
'</option>';
1778 foreach ($this->repo()->groups() as $group) {
1779 $id = (int)($group[
'id'] ?? 0);
1783 $groups .=
'<option value="' . $id .
'">' . $this->h($group[
'title'] ??
'') .
'</option>';
1786 $url =
function(
string $do, array $params = array()) use ($baseAction):
string {
1787 $query = $baseAction .
'&dbx_do=' . rawurlencode($do);
1788 foreach ($params as $key =>
$value) {
1789 $query .=
'&' . rawurlencode((
string)$key) .
'=' . rawurlencode((
string)
$value);
1791 return $this->h($query);
1794 $actions =
'<option value="">' . $this->h(
$texts->get_fd_message(
'bulk_action_placeholder')) .
'</option>'
1795 .
'<option value="shop_products_delete">' . $this->h(
$texts->get_fd_message(
'bulk_delete')) .
'</option>'
1796 .
'<option value="shop_products_channel_add">' . $this->h(
$texts->get_fd_message(
'bulk_channel_add')) .
'</option>'
1797 .
'<option value="shop_products_channel_remove">' . $this->h(
$texts->get_fd_message(
'bulk_channel_remove')) .
'</option>'
1798 .
'<option value="shop_products_channel_export">' . $this->h(
$texts->get_fd_message(
'bulk_channel_export')) .
'</option>'
1799 .
'<option value="shop_products_group_set">' . $this->h(
$texts->get_fd_message(
'bulk_group_set')) .
'</option>';
1801 return '<div class="dbx-shop-products-bulk-actions">'
1802 .
'<select class="form-select form-select-sm" name="dbx_products_bulk_action" title="' . $this->h(
$texts->get_fd_message(
'bulk_title')) .
'">' .
$actions .
'</select>'
1803 .
'<select class="form-select form-select-sm" name="dbx_action_channel" title="' . $this->h(
$texts->get_fd_message(
'bulk_channel_title')) .
'">' . $channels .
'</select>'
1804 .
'<select class="form-select form-select-sm" name="dbx_action_group" title="' . $this->h(
$texts->get_fd_message(
'bulk_group_title')) .
'">' .
$groups .
'</select>'
1805 .
'<a class="btn btn-primary btn-sm dbxAjaxFormAction dbxConfirm" href="' .
$url(
'shop_products_apply') .
'" data-confirm-title="<i class=\'bi bi-lightning-fill\'></i> ' . $this->h(
$texts->get_fd_message(
'bulk_confirm_title')) .
'" data-confirm="' . $this->h(
$texts->get_fd_message(
'bulk_confirm_question')) .
'" data-confirm-hint="<small>' . $this->h(
$texts->get_fd_message(
'bulk_confirm_hint')) .
'</small>" data-confirm-buttons="yesno" role="button"><i class="bi bi-check2-circle"></i> ' . $this->h(
$texts->get_fd_message(
'bulk_execute')) .
'</a>'
1809 private function handleProductReportAction(
$report): void {
1810 $do = (string)
dbx()->get_modul_var(
'dbx_do',
'',
'parameter');
1811 $mutatingActions = array(
1813 'shop_products_apply',
1814 'shop_products_delete',
1815 'shop_products_channel_add',
1816 'shop_products_channel_remove',
1817 'shop_products_channel_export',
1818 'shop_products_group_set',
1822 if ($do !==
'row_delete'
1823 && in_array($do, $mutatingActions,
true)
1824 && !$this->checkActionToken(
'product_report_action')) {
1829 $rid = (int)
dbx()->get_modul_var(
'rid', 0,
'int');
1830 if ($do ===
'row_delete') {
1832 $report->_msg_error =
$report->get_fd_message(
'product_delete_error');
1835 $count = $this->repo()->deleteProducts(array($rid));
1836 $report->del_multi_select($rid);
1838 ?
$report->get_fd_message(
'product_delete_success')
1842 :
$report->get_fd_message(
'product_delete_error');
1846 if ($do ===
'shop_products_apply') {
1847 $do = (string)
dbx()->get_modul_var(
'dbx_products_bulk_action',
'',
'parameter');
1850 if (!in_array($do, array(
'shop_products_delete',
'shop_products_channel_add',
'shop_products_channel_remove',
'shop_products_channel_export',
'shop_products_group_set'),
true)) {
1851 if ((
string)
dbx()->get_modul_var(
'dbx_do',
'',
'parameter') ===
'shop_products_apply') {
1857 $ids = $this->selectedProductIds(
$report);
1858 if ($ids === array()) {
1859 $report->_msg_error =
$report->get_fd_message(
'select_products');
1863 if ($do ===
'shop_products_delete') {
1864 $count = $this->repo()->deleteProducts($ids);
1865 foreach ($ids as $id) {
1866 $report->del_multi_select($id);
1869 ?
$report->get_fd_message(
'multi_deleted_one')
1871 'multi_deleted_many',
1877 if ($do ===
'shop_products_channel_add') {
1878 $channel = trim((
string)
dbx()->get_modul_var(
'dbx_action_channel',
'',
'parameter'));
1879 if ($channel ===
'') {
1883 $count = $this->repo()->addChannelToProducts($ids, $channel);
1886 array(
'count' =>
$count,
'channel' => $channel)
1891 if ($do ===
'shop_products_channel_remove') {
1892 $channel = trim((
string)
dbx()->get_modul_var(
'dbx_action_channel',
'',
'parameter'));
1893 if ($channel ===
'') {
1897 $count = $this->repo()->removeChannelFromProducts($ids, $channel);
1900 array(
'count' =>
$count,
'channel' => $channel)
1905 if ($do ===
'shop_products_channel_export') {
1906 $channel = trim((
string)
dbx()->get_modul_var(
'dbx_action_channel',
'',
'parameter'));
1907 if ($channel ===
'') {
1911 $summary = $this->repo()->exportProductsToChannel($ids, $channel);
1915 'ok' => (
int)($summary[
'ok'] ?? 0),
1916 'failed' => (
int)($summary[
'failed'] ?? 0),
1919 if (!empty($summary[
'messages'])) {
1920 $report->_msg_info = implode(
'<br>', array_map(fn($msg) => $this->h($msg), array_slice((array)$summary[
'messages'], 0, 8)));
1925 if ($do ===
'shop_products_group_set') {
1926 $groupId = (int)
dbx()->get_modul_var(
'dbx_action_group', 0,
'int');
1927 if ($groupId <= 0) {
1931 $count = $this->repo()->setProductGroupForProducts($ids, $groupId);
1939 private function cardTemplateOptions(
string $selected): string {
1940 return $this->optionsHtml(array(
1941 'product-card-default' =>
'Standardkarte',
1942 'product-card-compact' =>
'Kompaktkarte',
1946 private function detailTemplateOptions(
string $selected): string {
1947 return $this->optionsHtml(array(
1948 'product-detail-default' =>
'Standarddetail',
1949 'product-detail-technical' =>
'Technische Ansicht',
1953 private function galleryTemplateOptions(
string $selected): string {
1954 return $this->optionsHtml(array(
1955 'image-gallery' =>
'Bild Gallery',
1956 'file-gallery' =>
'Datei Gallery',
1960 private function galleryOverflowOptions(
string $selected): string {
1961 return $this->optionsHtml(array(
1963 'slider' =>
'Slider',
1964 'scroll' =>
'Scroll',
1965 'laufband' =>
'Laufband',
1966 'tutorial' =>
'Tutorial',
1970 private function galleryClickOptions(
string $selected): string {
1971 return $this->optionsHtml(array(
1972 'lightbox' =>
'Lightbox',
1973 'none' =>
'Kein Klick',
1974 'newtab' =>
'Neuer Tab',
1975 'viewerjs' =>
'ViewerJS',
1976 'photoswipe' =>
'PhotoSwipe',
1980 private function shopAdminStyle(): string {
1981 $file = dirname(__DIR__) .
'/design/css/shop-admin.css';
1982 if (!is_file(
$file)) {
1985 return '<style>' . file_get_contents(
$file) .
'</style>';
1988 private function frame(
string $content,
string $title =
'Shop Administration',
string $barActions =
''): string {
1989 if ($this->postedFormError !==
'') {
1990 $content =
'<div class="alert alert-danger mx-3 mt-3 mb-0" role="alert">'
1991 . $this->h($this->postedFormError)
1994 $this->postedFormError =
'';
1997 return $this->tpl()->get_tpl(
'dbxShop_admin|admin-shell', array(
1998 'shop_admin_style' => $this->shopAdminStyle(),
1999 'bar_title' => $this->h($title),
2000 'bar_icon' =>
'bi-bag-check',
2001 'bar_subtitle' => $this->h($this->catalogTexts()->get_fd_message(
'admin_subtitle')),
2002 'bar_title_pre' =>
'',
2003 'bar_title_heading_attrs' =>
'',
2006 'bar_actions' => $barActions,
2011 private function productBarActions(
$texts =
null): string {
2012 $title =
$texts && method_exists(
$texts,
'get_fd_message')
2013 ?
$texts->get_fd_message(
'new_product_title',
'Neuen Artikel anlegen')
2014 :
'Neuen Artikel anlegen';
2015 $label =
$texts && method_exists(
$texts,
'get_fd_message')
2016 ?
$texts->get_fd_message(
'new_product',
'Neuer Artikel')
2019 ?
$texts->get_fd_message(
'products_help',
'Hilfe: Produkte')
2020 :
'Hilfe: Produkte';
2022 return '<a class="btn btn-outline-primary btn-sm me-1" href="?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id=0" title="' . $this->h($title) .
'"><i class="bi bi-plus-square"></i><span class="visually-hidden"> ' . $this->h($label) .
'</span></a>'
2023 . $this->helpButton($this->ensureShopProductsHelpPage(),
$help);
2026 private function productShellActions(
$texts =
null): string {
2027 return $this->productBarActions(
$texts);
2030 private function productFormDefaults(
int $id): array {
2031 $now = date(
'Y-m-d H:i:s');
2032 $uid = (int)
dbx()->user();
2034 'update_date' =>
$now,
2035 'update_uid' => $uid,
2038 $defaults[
'create_date'] =
$now;
2039 $defaults[
'create_uid'] = $uid;
2040 $defaults[
'owner'] = $uid;
2041 $defaults[
'trash'] = 0;
2042 $defaults[
'currency'] =
'EUR';
2047 private function newProductDefaults(): array {
2052 'category' =>
'Merchandise',
2053 'product_type' =>
'physical',
2055 'description' =>
'',
2056 'price_gross' =>
'0.00',
2057 'currency' =>
'EUR',
2058 'tax_mode' =>
'group',
2060 'shipping_mode' =>
'group',
2061 'shipping_gross' =>
'-1',
2066 'image_icon' =>
'bi-box-seam',
2067 'logo_variant' =>
'',
2071 private function applyProductPreset(array $data): array {
2072 $preset = trim((string)
dbx()->get_modul_var(
'workflow_preset',
'',
'parameter'));
2073 if ($preset ===
'shop_article_publish') {
2074 $data[
'category'] = $data[
'category'] ?:
'Merchandise';
2075 $data[
'product_type'] = $data[
'product_type'] ?:
'physical';
2076 $data[
'active'] = 1;
2080 'preset_sku' =>
'sku',
2081 'preset_slug' =>
'slug',
2082 'preset_title' =>
'title',
2083 'preset_category' =>
'category',
2084 'preset_product_type' =>
'product_type',
2085 'preset_summary' =>
'summary',
2086 'preset_price_gross' =>
'price_gross',
2087 'preset_group_id' =>
'product_group_id',
2089 foreach ($map as $param =>
$field) {
2090 $value = trim((
string)
dbx()->get_modul_var($param,
'',
'*'));
2099 private function productFormActions(
int $id,
$texts): string {
2100 $html = $this->helpButton(
2101 $this->ensureShopProductsHelpPage(),
2102 $texts->get_fd_message(
'help_edit'),
2103 'btn btn-outline-secondary btn-sm ms-1'
2105 .
'<button class="btn btn-primary btn-sm" type="submit" title="' . $this->h(
$texts->get_fd_message(
'save_title')) .
'"><i class="bi bi-save"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'save_label')) .
'</span></button>';
2108 $product = $this->repo()->productById($id);
2109 $previewUrl =
'?dbx_modul=dbxShop&dbx_run1=product&sku=' . rawurlencode((
string)($product[
'sku'] ??
''));
2110 $deleteUrl = $this->actionUrl(
'?dbx_modul=dbxShop_admin&dbx_run1=products&dbx_do=row_delete&rid=' . $id);
2111 $html .= $this->openWinButton(
2113 $texts->get_fd_message(
'view_product'),
2114 '<i class="bi bi-search"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'preview')) .
'</span>',
2115 'btn btn-outline-primary btn-sm ms-1',
2119 $html .= $this->openWinButton(
2120 '?dbx_modul=dbxShop_admin&dbx_run1=product_attributes&id=' . $id,
2121 $texts->get_fd_message(
'product_attributes'),
2122 '<i class="bi bi-sliders"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'attributes')) .
'</span>',
2123 'btn btn-outline-primary btn-sm ms-1',
2127 $html .=
'<a class="btn btn-outline-danger btn-sm ms-1 dbxConfirm" href="' . $this->h(
$deleteUrl) .
'" data-confirm-title="' . $this->h(
$texts->get_fd_message(
'delete_title')) .
'" data-confirm="' . $this->h(
$texts->get_fd_message(
'delete_confirm')) .
'" data-confirm-buttons="yesno" title="' . $this->h(
$texts->get_fd_message(
'delete_title')) .
'"><i class="bi bi-trash"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'delete_label')) .
'</span></a>';
2130 $html .=
'<a class="btn btn-outline-secondary btn-sm ms-1" href="?dbx_modul=dbxShop_admin&dbx_run1=products" title="' . $this->h(
$texts->get_fd_message(
'product_list_title')) .
'"><i class="bi bi-table"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'product_list')) .
'</span></a>';
2135 private function productGroupOptions(
int $excludeId = 0,
bool $withNone =
false,
$texts =
null): array {
2136 $groups = $this->repo()->groups();
2137 $byParent = array();
2139 $parentId = (int)($group[
'parent_id'] ?? 0);
2140 $byParent[$parentId][] = $group;
2144 $options = $withNone ? array(
'0' =>
$texts->get_fd_message(
'groups_no_parent')) : array();
2145 $walk =
function(
int $parentId,
string $prefix) use (&$walk, &
$options, $byParent, $excludeId): void {
2146 foreach (($byParent[$parentId] ?? array()) as $group) {
2147 $id = (int)($group[
'id'] ?? 0);
2148 if ($id <= 0 || $id === $excludeId) {
2151 $title = trim((
string)($group[
'title'] ??
''));
2152 if ($title ===
'') {
2153 $title = (string)($group[
'group_key'] ?? $id);
2155 $label = $prefix !==
'' ? $prefix .
' / ' . $title : $title;
2164 private function shopMediaConfig(): array {
2166 'media' => $this->cmsEndpoint(
'cms_media', array(
'images' => 1,
'media_type' =>
'image'), true),
2167 'uploadmediafolder' =>
'img/shop',
2168 'upload' => $this->cmsEndpoint(
'cms_upload', array(), true),
2169 'externalvideo' => $this->cmsEndpoint(
'cms_external_video', array(), true),
2170 'mediafolders' => $this->cmsEndpoint(
'cms_media_folders'),
2171 'mediafoldercreate' => $this->cmsEndpoint(
'cms_media_folder_create', array(), true),
2172 'mediafolderdelete' => $this->cmsEndpoint(
'cms_media_folder_delete', array(), true),
2173 'mediafolderrename' => $this->cmsEndpoint(
'cms_media_folder_rename', array(), true),
2174 'mediamove' => $this->cmsEndpoint(
'cms_media_move', array(), true),
2175 'mediaunused' => $this->cmsEndpoint(
'cms_media_unused'),
2176 'mediaprocess' => $this->cmsEndpoint(
'cms_media_process', array(), true),
2177 'deletemedia' => $this->cmsEndpoint(
'cms_delete_media', array(), true),
2178 'editmedia' => $this->cmsEndpoint(
'cms_edit_media', array(), true),
2179 'assignurl' => $this->shopEndpoint(
'assign_media', array(), true),
2183 private function shopMediaAttrs(array $mediaCfg): string {
2184 $attrs =
' data-dbx="lib=shopAdmin"';
2185 foreach ($mediaCfg as $key =>
$value) {
2186 $attrs .=
' data-shop-' . $this->h($key) .
'="' . $this->h(
$value) .
'"';
2200 private function shopMediaFormTemplates(array $mediaCfg): string {
2201 return
dbx()->get_include_obj(
'dbxContentMediaForms',
'dbxContent_admin')->renderTemplates(
2202 (string)($mediaCfg[
'upload'] ??
''),
2204 (string)($mediaCfg[
'externalvideo'] ??
''),
2205 'cms-external-video'
2209 private function productImagesPanel(array $product,
bool $isNew,
$texts): string {
2211 return '<aside class="border rounded bg-light p-3"><h6 class="mb-3">'
2212 . $this->h(
$texts->get_fd_message(
'images_title'))
2213 .
'</h6><div class="alert alert-info mb-0">'
2214 . $this->h(
$texts->get_fd_message(
'save_first_images'))
2218 $productId = (int)($product[
'id'] ?? 0);
2219 $mediaCfg = $this->shopMediaConfig();
2220 $html =
'<aside class="border rounded bg-light p-3 dbx-shop-media-manager dbx-shop-product-image-panel"' . $this->shopMediaAttrs($mediaCfg) .
'>';
2221 $html .=
'<input type="hidden" value="' . $productId .
'" data-shop-product-select>';
2222 $html .=
'<input type="hidden" value="0" data-shop-group-select>';
2223 $html .=
'<input type="hidden" value="100" data-shop-sorter>';
2224 $html .=
'<div class="d-flex justify-content-between align-items-center gap-2 mb-3">';
2225 $html .=
'<h6 class="mb-0">' . $this->h(
$texts->get_fd_message(
'images_title')) .
'</h6>';
2226 $html .=
'<button type="button" class="btn btn-outline-primary btn-sm dbx-shop-media-pick" data-shop-media-folder="img/shop" title="' . $this->h(
$texts->get_fd_message(
'select_images_title')) .
'"><i class="bi bi-images"></i><i class="bi bi-camera-video"></i><i class="bi bi-upload"></i><span>' . $this->h(
$texts->get_fd_message(
'selection')) .
'</span></button>';
2228 $html .=
'<label class="form-check mb-3"><input class="form-check-input" type="checkbox" value="1" data-shop-primary> <span class="form-check-label">' . $this->h(
$texts->get_fd_message(
'new_primary')) .
'</span></label>';
2230 $images = (array)($product[
'images'] ?? array());
2231 if ($images === array()) {
2232 $html .=
'<div class="text-muted small">' . $this->h(
$texts->get_fd_message(
'no_images')) .
'</div>';
2234 $html .=
'<div class="dbx-shop-image-list">';
2235 foreach ($images as $image) {
2236 $imageId = (int)($image[
'id'] ?? 0);
2237 $source = (int)($image[
'product_id'] ?? 0) === $productId
2238 ?
$texts->get_fd_message(
'image_source_product')
2239 :
$texts->get_fd_message(
'image_source_group');
2240 $primary = (int)($image[
'is_primary'] ?? 0) === 1
2241 ?
'<span class="badge text-bg-primary ms-1">' . $this->h(
$texts->get_fd_message(
'primary')) .
'</span>'
2243 $title = trim((
string)($image[
'title'] ??
''));
2244 if ($title ===
'') {
2245 $title = basename((
string)($image[
'image_path'] ??
'Bild'));
2247 $removeUrl = $this->actionUrl(
'?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id=' . $productId .
'&remove_image=' . $imageId);
2248 $html .=
'<figure class="dbx-shop-image-card">';
2250 $html .=
'<a class="btn btn-outline-danger btn-sm dbxAjax dbxConfirm dbx-shop-image-unassign" href="' . $this->h($removeUrl) .
'" data-confirm-title="<i class=\'bi bi-trash\'></i> ' . $this->h(
$texts->get_fd_message(
'unlink_image_title')) .
'" data-confirm="' . $this->h(
$texts->get_fd_message(
'unlink_image_question')) .
'" data-confirm-hint="<small>' . $this->h(
$texts->get_fd_message(
'unlink_image_hint')) .
'</small>" data-confirm-buttons="yesno" title="' . $this->h(
$texts->get_fd_message(
'unlink_image_title')) .
'"><i class="bi bi-trash"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'unlink_image_label')) .
'</span></a>';
2252 $html .=
'<img src="' . $this->h($this->mediaItemUrl($image,
true)) .
'" alt="' . $this->h($image[
'alt'] ?? $title) .
'">';
2253 $html .=
'<figcaption><strong>' . $this->h($title) .
'</strong><br><span class="text-muted">' . $this->h(
$source) .
'</span>' . $primary .
'</figcaption>';
2254 $html .=
'</figure>';
2259 $html .=
'<div class="form-text mt-3">' . $this->h(
$texts->get_fd_message(
'media_hint')) .
'</div>';
2260 $html .=
'</aside>';
2264 private function productGroupImagePanel(array $group,
bool $isNew,
$texts =
null): string {
2267 return '<div class="dbx-shop-group-image-panel dbx-shop-group-image-empty"><div class="form-text">' . $this->h(
$texts->get_fd_message(
'group_image_save_first')) .
'</div></div>';
2270 $groupId = (int)($group[
'id'] ?? 0);
2271 if ($groupId <= 0) {
2275 $mediaCfg = $this->shopMediaConfig();
2276 $image = $this->repo()->primaryImageForGroup($groupId);
2277 $html =
'<div class="dbx-shop-media-manager dbx-shop-group-image-panel"' . $this->shopMediaAttrs($mediaCfg) .
'>';
2278 $html .=
'<input type="hidden" value="0" data-shop-product-select>';
2279 $html .=
'<input type="hidden" value="' . $groupId .
'" data-shop-group-select>';
2280 $html .=
'<input type="hidden" value="10" data-shop-sorter>';
2281 $html .=
'<input type="hidden" value="1" data-shop-primary>';
2282 $html .=
'<div class="dbx-shop-group-image-head">';
2283 $html .=
'<span>' . $this->h(
$texts->get_fd_message(
'group_image_title')) .
'</span>';
2284 $html .=
'<button type="button" class="btn btn-outline-primary btn-sm dbx-shop-media-pick" data-shop-media-folder="img/shop" title="' . $this->h(
$texts->get_fd_message(
'group_image_select_title')) .
'"><i class="bi bi-images"></i><span>' . $this->h(
$texts->get_fd_message(
'selection')) .
'</span></button>';
2286 if (is_array($image)) {
2287 $title = trim((
string)($image[
'title'] ??
''));
2288 if ($title ===
'') {
2289 $title = basename((
string)($image[
'image_path'] ??
'Gruppenbild'));
2291 $html .=
'<figure class="dbx-shop-group-image-preview"><img src="' . $this->h($this->mediaItemUrl($image,
true)) .
'" alt="' . $this->h($image[
'alt'] ?? $title) .
'"><figcaption>' . $this->h($title) .
'</figcaption></figure>';
2293 $html .=
'<div class="dbx-shop-group-image-placeholder"><i class="bi bi-image"></i><span>' . $this->h(
$texts->get_fd_message(
'group_image_none')) .
'</span></div>';
2295 $html .=
'<div class="form-text">' . $this->h(
$texts->get_fd_message(
'group_image_hint')) .
'</div>';
2300 private function productChannelsPanel(array $product,
bool $isNew,
$texts): string {
2301 if (!$this->channelsEnabled()) {
2305 return '<aside class="border rounded bg-light p-3 mt-3"><h6 class="mb-3">'
2306 . $this->h(
$texts->get_fd_message(
'channels_title'))
2307 .
'</h6><div class="alert alert-info mb-0">'
2308 . $this->h(
$texts->get_fd_message(
'save_first_channels'))
2312 $productId = (int)($product[
'id'] ?? 0);
2313 $overrides = $this->repo()->productChannelOverrides($productId);
2314 $inherited = $this->repo()->inheritedChannelsForProduct($productId);
2315 $html =
'<aside class="border rounded bg-light p-3 mt-3 dbx-shop-product-channel-panel">';
2316 $html .=
'<h6 class="mb-2">' . $this->h(
$texts->get_fd_message(
'channels_title')) .
'</h6>';
2317 $html .=
'<p class="form-text mb-3">' . $this->h(
$texts->get_fd_message(
'channels_info')) .
'</p>';
2318 $html .=
'<input type="hidden" name="product_channel_editor" value="1">';
2320 $channels = $this->repo()->channels();
2321 if ($channels === array()) {
2322 $html .=
'<div class="text-muted small">' . $this->h(
$texts->get_fd_message(
'no_channels')) .
'</div>';
2324 $html .=
'<div class="table-responsive dbx-shop-product-channel-table-wrap">';
2325 $html .=
'<table class="table table-sm align-middle mb-0 dbx-shop-product-channel-table">';
2326 $html .=
'<thead><tr><th>' . $this->h(
$texts->get_fd_message(
'table_channel')) .
'</th><th>' . $this->h(
$texts->get_fd_message(
'table_status')) .
'</th><th>' . $this->h(
$texts->get_fd_message(
'table_export')) .
'</th><th class="text-end">' . $this->h(
$texts->get_fd_message(
'table_action')) .
'</th></tr></thead><tbody>';
2327 foreach ($channels as $channel) {
2328 $key = trim((
string)($channel[
'channel_key'] ??
''));
2333 $hasOverride = isset($overrides[$key]);
2334 $isInherited = isset($inherited[$key]);
2336 ? (int)($overrides[$key][
'active'] ?? 0) === 1
2339 ? ((int)($overrides[$key][
'active'] ?? 0) === 1
2340 ?
$texts->get_fd_message(
'channel_direct_active')
2341 :
$texts->get_fd_message(
'channel_direct_inactive'))
2343 ?
$texts->format_fd_message(
2344 'channel_from_group_title',
2345 array(
'groups' => implode(
', ', array_values($inherited[$key][
'group_titles'] ?? array())))
2347 :
$texts->get_fd_message(
'channel_not_set'));
2348 $sourceText = (!$hasOverride && $isInherited)
2349 ?
$texts->get_fd_message(
'channel_from_group')
2351 $statusClass =
$checked ?
'text-bg-success' :
'text-bg-secondary';
2352 $export = $overrides[$key] ?? array();
2353 $exportStatus = trim((
string)($export[
'export_status'] ??
''));
2354 $exportMessage = trim((
string)($export[
'export_message'] ??
''));
2355 $listingId = trim((
string)($export[
'external_listing_id'] ??
''));
2356 $exportBadgeClass = match ($exportStatus) {
2357 'published',
'exported',
'ready',
'manual_ready' =>
'text-bg-info',
2358 'failed' =>
'text-bg-danger',
2359 default =>
'text-bg-light text-dark',
2361 $exportText = $exportStatus !==
''
2363 :
$texts->get_fd_message(
'channel_not_exported');
2364 $exportUrl = $this->actionUrl(
'?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id=' . $productId .
'&export_channel=' . rawurlencode($key));
2365 $mappingUrl =
'?dbx_modul=dbxShop_admin&dbx_run1=product_channel_mapping&id=' . $productId .
'&channel=' . rawurlencode($key);
2367 $html .=
'<tr class="dbx-shop-product-channel-row">';
2368 $html .=
'<td class="dbx-shop-product-channel-name">';
2369 $html .=
'<label class="d-flex align-items-start gap-2 mb-0">';
2370 $html .=
'<input class="form-check-input" type="checkbox" name="product_channels[]" value="' . $this->h($key) .
'"' . (
$checked ?
' checked' :
'') .
'>';
2371 $html .=
'<span class="dbx-shop-product-channel-copy"><strong>' . $this->h($channel[
'title'] ?? $key) .
'</strong><code>' . $this->h($key) .
'</code></span>';
2372 $html .=
'</label>';
2374 $html .=
'<td><span class="badge ' . $statusClass .
'" title="' . $this->h(
$source) .
'">' . $this->h($sourceText) .
'</span></td>';
2375 $html .=
'<td><span class="badge ' . $exportBadgeClass .
'" title="' . $this->h($exportMessage) .
'">' . $this->h($exportText) .
'</span>';
2376 if ($listingId !==
'') {
2377 $html .=
'<code class="small d-block mt-1">' . $this->h($listingId) .
'</code>';
2380 $html .=
'<td class="text-end"><span class="dbx-shop-product-channel-actions">';
2381 $html .= $this->openWinButton(
2383 $texts->format_fd_message(
2385 array(
'channel' => (
string)($channel[
'title'] ?? $key))
2387 '<i class="bi bi-sliders2"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'mapping_label')) .
'</span>',
2388 'btn btn-outline-secondary btn-sm',
2392 if (
$checked && (
int)($channel[
'export_enabled'] ?? 0) === 1) {
2393 $html .=
'<a class="btn btn-outline-primary btn-sm dbxConfirm" href="' . $this->h($exportUrl) .
'" data-confirm-title="<i class=\'bi bi-broadcast\'></i> ' . $this->h(
$texts->get_fd_message(
'export_title')) .
'" data-confirm="' . $this->h(
$texts->format_fd_message(
'export_question', array(
'channel' => (
string)($channel[
'title'] ?? $key)))) .
'" data-confirm-hint="<small>' . $this->h(
$texts->get_fd_message(
'export_hint')) .
'</small>" data-confirm-buttons="yesno" title="' . $this->h(
$texts->get_fd_message(
'export_button_title')) .
'"><i class="bi bi-broadcast"></i></a>';
2395 $html .=
'</span></td>';
2398 $html .=
'</tbody></table></div>';
2401 $html .=
'</aside>';
2405 private function mappingLinesToMap(
string $lines): array {
2407 foreach (preg_split(
'/\R/', $lines) ?: array() as $line) {
2408 $line = trim((
string)$line);
2409 if ($line ===
'' || strpos($line,
'=') ===
false) {
2412 [$key,
$value] = array_map(
'trim', explode(
'=', $line, 2));
2413 if ($key !==
'' &&
$value !==
'') {
2420 private function mappingMapToLines(array $map): string {
2422 foreach ($map as $key =>
$value) {
2426 $lines[] = (string)$key .
'=' . (
string)
$value;
2428 return implode(
"\n", $lines);
2431 private function ebayCategoryOptions(array $mapping, array $channel, array $product,
$texts): array {
2441 $current = (string)($mapping[
'category_id'] ??
'');
2442 $channelDefault = (string)($channel[
'category_id'] ??
'');
2443 $groupDefault = (string)($this->productGroupChannelMappingDefaults(
'ebay', $product)[
'category_id'] ??
'');
2444 $add($current,
$texts->format_fd_message(
'mapping_current_selection', array(
'value' => $current)));
2445 $add($groupDefault,
$texts->format_fd_message(
'mapping_group_default', array(
'value' => $groupDefault)));
2446 $add($channelDefault,
$texts->format_fd_message(
'mapping_channel_default', array(
'value' => $channelDefault)));
2448 $configured =
dbx()->get_config(
'dbxShop',
'ebay_category_options');
2449 if (is_array($configured)) {
2450 foreach ($configured as
$value => $label) {
2451 $add((
string)
$value, (
string)$label);
2454 foreach (preg_split(
'/\R/', (
string)$configured) ?: array() as $line) {
2455 $line = trim($line);
2459 if (strpos($line,
'=') !==
false) {
2460 [
$value, $label] = array_map(
'trim', explode(
'=', $line, 2));
2468 $productCategory = strtolower((
string)($product[
'category'] ??
'') .
' ' . (
string)($product[
'product_type'] ??
''));
2469 if (strpos($productCategory,
'software') !==
false || strpos($productCategory,
'digital') !==
false) {
2470 $add(
'58058',
'58058 - ' .
$texts->get_fd_message(
'mapping_category_software'));
2473 $add(
'58058',
'58058 - ' .
$texts->get_fd_message(
'mapping_category_software'));
2474 $add(
'11450',
'11450 - ' .
$texts->get_fd_message(
'mapping_category_clothing'));
2475 $add(
'293',
'293 - ' .
$texts->get_fd_message(
'mapping_category_electronics'));
2476 $add(
'220',
'220 - ' .
$texts->get_fd_message(
'mapping_category_home'));
2477 $add(
'12576',
'12576 - ' .
$texts->get_fd_message(
'mapping_category_business'));
2482 private function ebayCategoryInput(array $mapping, array $channel, array $product,
$texts): string {
2483 $value = (string)($mapping[
'category_id'] ?? $channel[
'category_id'] ??
'');
2484 $html =
'<div class="col-md-4"><label class="form-label">' . $this->h(
$texts->get_fd_message(
'mapping_ebay_category')) .
'</label>'
2485 .
'<select class="form-select form-select-sm" name="mapping_category_id">';
2486 foreach ($this->ebayCategoryOptions($mapping, $channel, $product,
$texts) as $optionValue => $label) {
2487 $html .=
'<option value="' . $this->h($optionValue) .
'"' . ((string)$optionValue ===
$value ?
' selected' :
'') .
'>' . $this->h($label) .
'</option>';
2489 $html .=
'</select>'
2490 .
'<div class="form-text">' . $this->h(
$texts->get_fd_message(
'mapping_ebay_category_hint')) .
'</div>'
2495 private function productGroupChannelMappingDefaults(
string $platform, array $product): array {
2496 $groupId = (int)($product[
'product_group_id'] ?? 0);
2497 if ($groupId <= 0) {
2500 $group = $this->repo()->groupById($groupId);
2501 if (!is_array($group)) {
2505 if ($platform ===
'ebay') {
2506 $category = trim((
string)($group[
'ebay_category_id'] ??
''));
2507 return $category !==
'' ? array(
'category_id' => $category) : array();
2509 if ($platform ===
'amazon') {
2510 $productType = trim((
string)($group[
'amazon_product_type'] ??
''));
2511 return $productType !==
'' ? array(
'productType' => $productType) : array();
2513 if ($platform ===
'kleinanzeigen') {
2514 $category = trim((
string)($group[
'kleinanzeigen_category_id'] ??
''));
2515 return $category !==
'' ? array(
'category_id' => $category) : array();
2517 if ($platform ===
'mobile') {
2518 $category = trim((
string)($group[
'mobile_category_id'] ??
''));
2519 return $category !==
'' ? array(
'mobile_vehicle' => array(
'category' => $category)) : array();
2525 private function channelMappingInheritedDefaults(
string $platform, array $channel, array $product): array {
2526 $defaults = array();
2527 if ($platform ===
'ebay') {
2529 'category_id' => trim((
string)($channel[
'category_id'] ??
'')),
2530 'payment_policy_id' => trim((
string)($channel[
'payment_policy_id'] ??
'')),
2531 'fulfillment_policy_id' => trim((
string)($channel[
'fulfillment_policy_id'] ??
'')),
2532 'return_policy_id' => trim((
string)($channel[
'return_policy_id'] ??
'')),
2534 } elseif ($platform ===
'amazon') {
2535 $category = trim((
string)($channel[
'category_id'] ??
''));
2536 if (stripos($category,
'productType:') === 0) {
2537 $category = trim(substr($category, strlen(
'productType:')));
2539 if (strpos($category,
'/') !==
false) {
2540 $category = trim((
string)explode(
'/', $category)[0]);
2542 $defaults = array(
'productType' => strtoupper($category));
2543 } elseif ($platform ===
'kleinanzeigen') {
2545 'category_id' => trim((
string)($channel[
'category_id'] ??
'')),
2546 'location' => trim((
string)($channel[
'location_key'] ??
'')),
2548 } elseif ($platform ===
'mobile') {
2549 $category = trim((
string)($channel[
'category_id'] ??
''));
2550 $defaults = array(
'mobile_vehicle' => array(
'category' => $category));
2553 $defaults = $this->mergeMappingDefaults($defaults, $this->productGroupChannelMappingDefaults($platform, $product));
2554 return $this->cleanEmptyMappingValues($defaults);
2557 private function mergeMappingDefaults(array $defaults, array $mapping): array {
2560 $current = is_array($mapping[$key] ??
null) ? $mapping[$key] : array();
2561 $mapping[$key] = $this->mergeMappingDefaults(
$value, $current);
2564 if (!array_key_exists($key, $mapping) || trim((
string)$mapping[$key]) ===
'') {
2571 private function cleanEmptyMappingValues(array $mapping): array {
2575 if (
$value === array()) {
2576 unset($mapping[$key]);
2582 if (trim((
string)
$value) ===
'') {
2583 unset($mapping[$key]);
2589 private function removeInheritedMappingDefaults(array $mapping, array $defaults): array {
2590 foreach ($defaults as $key => $defaultValue) {
2591 if (!array_key_exists($key, $mapping)) {
2594 if (is_array($defaultValue)) {
2595 $current = is_array($mapping[$key]) ? $mapping[$key] : array();
2596 $mapping[$key] = $this->removeInheritedMappingDefaults($current, $defaultValue);
2597 if ($mapping[$key] === array()) {
2598 unset($mapping[$key]);
2602 if (trim((
string)$mapping[$key]) === trim((
string)$defaultValue)) {
2603 unset($mapping[$key]);
2606 return $this->cleanEmptyMappingValues($mapping);
2609 private function providerMappingHtml(
string $platform, array $mapping, array $channel, array $product,
$texts): string {
2610 $input = function(string $name, string $label, string
$value =
'', string $placeholder =
'', string
$class =
'col-md-4'): string {
2611 return
'<div class="' .
$class .
'"><label class="form-label">' . $this->h($label) .
'</label><input class="form-control form-control-sm" name="mapping_' . $this->h($name) .
'" value="' . $this->h(
$value) .
'" placeholder="' . $this->h($placeholder) .
'"></div>';
2613 $textarea =
function(
string $name,
string $label,
string $value =
'',
string $placeholder =
'',
int $rows = 4,
string $class =
'col-12'):
string {
2614 return '<div class="' .
$class .
'"><label class="form-label">' . $this->h($label) .
'</label><textarea class="form-control form-control-sm" rows="' .
$rows .
'" name="mapping_' . $this->h($name) .
'" placeholder="' . $this->h($placeholder) .
'">' . $this->h(
$value) .
'</textarea></div>';
2617 $html =
'<div class="row g-3">';
2618 if ($platform ===
'ebay') {
2619 $condition = (string)($mapping[
'condition'] ??
'NEW');
2620 $aspects = is_array($mapping[
'aspects'] ??
null) ? $this->mappingMapToLines($mapping[
'aspects']) :
'';
2621 $locationKey = trim((
string)($channel[
'location_key'] ??
''));
2622 $html .= $this->ebayCategoryInput($mapping, $channel, $product,
$texts);
2623 $html .=
'<div class="col-md-4"><label class="form-label">' . $this->h(
$texts->get_fd_message(
'mapping_location_key')) .
'</label>'
2624 .
'<input class="form-control form-control-sm" value="' . $this->h($locationKey) .
'" placeholder="' . $this->h(
$texts->get_fd_message(
'mapping_location_placeholder')) .
'" readonly>'
2625 .
'<div class="form-text">' . $this->h(
$texts->get_fd_message(
'mapping_location_hint')) .
'</div></div>';
2626 $html .=
'<div class="col-md-4"><label class="form-label">' . $this->h(
$texts->get_fd_message(
'mapping_condition')) .
'</label><select class="form-select form-select-sm" name="mapping_condition">'
2627 .
'<option value="NEW"' . ($condition ===
'NEW' ?
' selected' :
'') .
'>' . $this->h(
$texts->get_fd_message(
'mapping_condition_new')) .
'</option>'
2628 .
'<option value="USED_EXCELLENT"' . ($condition ===
'USED_EXCELLENT' ?
' selected' :
'') .
'>' . $this->h(
$texts->get_fd_message(
'mapping_condition_used_excellent')) .
'</option>'
2629 .
'<option value="USED_GOOD"' . ($condition ===
'USED_GOOD' ?
' selected' :
'') .
'>' . $this->h(
$texts->get_fd_message(
'mapping_condition_used_good')) .
'</option>'
2630 .
'</select></div>';
2631 $html .=
$input(
'payment_policy_id',
'Payment-Policy-ID', (
string)($mapping[
'payment_policy_id'] ?? $channel[
'payment_policy_id'] ??
''),
'policy_payment_1234567890');
2632 $html .=
$input(
'fulfillment_policy_id',
'Fulfillment-Policy-ID', (
string)($mapping[
'fulfillment_policy_id'] ?? $channel[
'fulfillment_policy_id'] ??
''),
'policy_fulfillment_1234567890');
2633 $html .=
$input(
'return_policy_id',
'Return-Policy-ID', (
string)($mapping[
'return_policy_id'] ?? $channel[
'return_policy_id'] ??
''),
'policy_return_1234567890');
2634 $html .= $textarea(
'aspects',
$texts->get_fd_message(
'mapping_ebay_aspects'), $aspects,
"brand=dbxApp\ncolor=black\nsize=L", 5);
2635 } elseif ($platform ===
'amazon') {
2636 $simple = is_array($mapping[
'simple_attributes'] ??
null) ? $this->mappingMapToLines($mapping[
'simple_attributes']) :
'';
2637 $html .=
$input(
'productType',
'Amazon Product Type', (
string)($mapping[
'productType'] ?? $mapping[
'product_type'] ??
''),
'SOFTWARE / PRODUCT / SHIRT');
2638 $html .=
'<div class="col-md-4"><label class="form-label">' . $this->h(
$texts->get_fd_message(
'mapping_requirements')) .
'</label><select class="form-select form-select-sm" name="mapping_requirements">'
2639 .
'<option value="LISTING"' . ((string)($mapping[
'requirements'] ??
'LISTING') ===
'LISTING' ?
' selected' :
'') .
'>' . $this->h(
$texts->get_fd_message(
'mapping_requirements_listing')) .
'</option>'
2640 .
'<option value="LISTING_PRODUCT_ONLY"' . ((string)($mapping[
'requirements'] ??
'') ===
'LISTING_PRODUCT_ONLY' ?
' selected' :
'') .
'>' . $this->h(
$texts->get_fd_message(
'mapping_requirements_product')) .
'</option>'
2641 .
'<option value="LISTING_OFFER_ONLY"' . ((string)($mapping[
'requirements'] ??
'') ===
'LISTING_OFFER_ONLY' ?
' selected' :
'') .
'>' . $this->h(
$texts->get_fd_message(
'mapping_requirements_offer')) .
'</option>'
2642 .
'</select></div>';
2643 $html .=
$input(
'brand',
$texts->get_fd_message(
'mapping_brand'), (
string)($mapping[
'simple_attributes'][
'brand'] ??
'dbxApp'),
'dbxApp');
2644 $html .= $textarea(
'simple_attributes',
$texts->get_fd_message(
'mapping_amazon_attributes'), $simple,
"manufacturer=dbxApp\nitem_type_keyword=software\nrecommended_browse_nodes=123456", 6);
2645 $html .=
'<div class="col-12"><div class="form-text">' . $this->h(
$texts->get_fd_message(
'mapping_amazon_hint')) .
'</div></div>';
2646 } elseif ($platform ===
'mobile') {
2647 $vehicle = is_array($mapping[
'mobile_vehicle'] ??
null) ? $mapping[
'mobile_vehicle'] : array();
2648 $html .=
$input(
'vehicle_make',
$texts->get_fd_message(
'mapping_brand'), (
string)($vehicle[
'make'] ??
''),
'Volkswagen');
2649 $html .=
$input(
'vehicle_model',
$texts->get_fd_message(
'mapping_vehicle_model'), (
string)($vehicle[
'model'] ??
''),
'Golf');
2650 $html .=
$input(
'vehicle_first_registration',
$texts->get_fd_message(
'mapping_vehicle_registration'), (
string)($vehicle[
'firstRegistration'] ??
''),
'2023-05');
2651 $html .=
$input(
'vehicle_mileage',
$texts->get_fd_message(
'mapping_vehicle_mileage'), (
string)($vehicle[
'mileage'] ??
''),
'25000');
2652 $html .=
$input(
'vehicle_fuel',
$texts->get_fd_message(
'mapping_vehicle_fuel'), (
string)($vehicle[
'fuel'] ??
''),
'PETROL');
2653 $html .=
$input(
'vehicle_power',
$texts->get_fd_message(
'mapping_vehicle_power'), (
string)($vehicle[
'power'] ??
''),
'110');
2654 $html .=
$input(
'vehicle_category',
$texts->get_fd_message(
'mapping_vehicle_category'), (
string)($vehicle[
'category'] ?? $channel[
'category_id'] ??
''),
'car');
2655 $html .= $textarea(
'vehicle_extra',
$texts->get_fd_message(
'mapping_vehicle_extra'), is_array($mapping[
'vehicle_extra'] ??
null) ? $this->mappingMapToLines($mapping[
'vehicle_extra']) :
'',
"gearbox=MANUAL\nemissionClass=EURO6", 5);
2656 $html .=
'<div class="col-12"><div class="form-text">' . $this->h(
$texts->get_fd_message(
'mapping_mobile_hint')) .
'</div></div>';
2657 } elseif ($platform ===
'kleinanzeigen') {
2658 $attrs = is_array($mapping[
'attributes'] ??
null) ? $this->mappingMapToLines($mapping[
'attributes']) :
'';
2659 $html .=
$input(
'category_id',
$texts->get_fd_message(
'mapping_classified_category'), (
string)($mapping[
'category_id'] ?? $channel[
'category_id'] ??
''),
'category_12345');
2660 $html .=
$input(
'location',
$texts->get_fd_message(
'mapping_place'), (
string)($mapping[
'location'] ??
''),
'10115 Berlin');
2661 $html .=
$input(
'contact_name',
$texts->get_fd_message(
'mapping_contact_name'), (
string)($mapping[
'contact_name'] ??
''),
'Muster GmbH');
2662 $html .=
$input(
'phone',
$texts->get_fd_message(
'mapping_phone'), (
string)($mapping[
'phone'] ??
''),
'+49...');
2663 $html .= $textarea(
'attributes',
$texts->get_fd_message(
'mapping_classified_attributes'), $attrs,
"condition=new\nshipping=yes\ncolor=black", 5);
2664 $html .=
'<div class="col-12"><div class="form-text">' . $this->h(
$texts->get_fd_message(
'mapping_classified_hint')) .
'</div></div>';
2666 $attrs = is_array($mapping[
'attributes'] ??
null) ? $this->mappingMapToLines($mapping[
'attributes']) :
'';
2667 $html .=
$input(
'endpoint_action',
$texts->get_fd_message(
'mapping_endpoint'), (
string)($mapping[
'endpoint_action'] ??
''),
'products.upsert');
2668 $html .= $textarea(
'attributes',
$texts->get_fd_message(
'mapping_middleware_attributes'), $attrs,
"external_category=123\nbrand=dbxApp", 6);
2674 private function collectProductChannelMapping(
string $platform, array $defaults = array()): array {
2675 if ($platform ===
'ebay') {
2677 'category_id' => trim((
string)(
$_POST[
'mapping_category_id'] ??
'')),
2678 'condition' => trim((
string)(
$_POST[
'mapping_condition'] ??
'NEW')),
2679 'payment_policy_id' => trim((
string)(
$_POST[
'mapping_payment_policy_id'] ??
'')),
2680 'fulfillment_policy_id' => trim((
string)(
$_POST[
'mapping_fulfillment_policy_id'] ??
'')),
2681 'return_policy_id' => trim((
string)(
$_POST[
'mapping_return_policy_id'] ??
'')),
2682 'aspects' => $this->mappingLinesToMap((
string)(
$_POST[
'mapping_aspects'] ??
'')),
2685 if ($platform ===
'amazon') {
2686 $simple = $this->mappingLinesToMap((
string)(
$_POST[
'mapping_simple_attributes'] ??
''));
2687 $brand = trim((
string)(
$_POST[
'mapping_brand'] ??
''));
2688 if ($brand !==
'') {
2689 $simple[
'brand'] = $brand;
2692 'productType' => trim((
string)(
$_POST[
'mapping_productType'] ??
'')),
2693 'requirements' => trim((
string)(
$_POST[
'mapping_requirements'] ??
'LISTING')),
2694 'simple_attributes' => $simple,
2697 if ($platform ===
'mobile') {
2699 'make' => trim((
string)(
$_POST[
'mapping_vehicle_make'] ??
'')),
2700 'model' => trim((
string)(
$_POST[
'mapping_vehicle_model'] ??
'')),
2701 'firstRegistration' => trim((
string)(
$_POST[
'mapping_vehicle_first_registration'] ??
'')),
2702 'mileage' => trim((
string)(
$_POST[
'mapping_vehicle_mileage'] ??
'')),
2703 'fuel' => trim((
string)(
$_POST[
'mapping_vehicle_fuel'] ??
'')),
2704 'power' => trim((
string)(
$_POST[
'mapping_vehicle_power'] ??
'')),
2705 'category' => trim((
string)(
$_POST[
'mapping_vehicle_category'] ??
'')),
2707 $vehicle = array_filter($vehicle, fn(
$value) => trim((
string)
$value) !==
'');
2709 'mobile_vehicle' => $vehicle,
2710 'vehicle_extra' => $this->mappingLinesToMap((
string)(
$_POST[
'mapping_vehicle_extra'] ??
'')),
2713 if ($platform ===
'kleinanzeigen') {
2715 'category_id' => trim((
string)(
$_POST[
'mapping_category_id'] ??
'')),
2716 'location' => trim((
string)(
$_POST[
'mapping_location'] ??
'')),
2717 'contact_name' => trim((
string)(
$_POST[
'mapping_contact_name'] ??
'')),
2718 'phone' => trim((
string)(
$_POST[
'mapping_phone'] ??
'')),
2719 'attributes' => $this->mappingLinesToMap((
string)(
$_POST[
'mapping_attributes'] ??
'')),
2723 'endpoint_action' => trim((
string)(
$_POST[
'mapping_endpoint_action'] ??
'')),
2724 'attributes' => $this->mappingLinesToMap((
string)(
$_POST[
'mapping_attributes'] ??
'')),
2728 private function normalizeDecimalInput(
$value): string {
2733 return number_format((
float)
$value, 2,
'.',
'');
2736 private function channelInheritedDecimalValue(
string $postedName): string {
2737 $value = $this->normalizeDecimalInput(
$_POST[$postedName] ??
'');
2738 $inherited = (string)(
$_POST[$postedName .
'_inherited'] ??
'') ===
'1';
2739 $inheritedValue = $this->normalizeDecimalInput(
$_POST[$postedName .
'_inherited_value'] ??
'');
2743 if ($inherited && $inheritedValue !==
'' &&
$value === $inheritedValue) {
2749 private function channelMappingDisplayValues(array $product, array $productChannel): array {
2750 $storedPrice = (float)($productChannel[
'price_gross'] ?? -1);
2751 $storedShipping = (float)($productChannel[
'shipping_gross'] ?? -1);
2752 $inheritedPrice = number_format((
float)($product[
'price_gross'] ?? 0), 2,
'.',
'');
2753 $inheritedShipping = number_format((
float)($product[
'effective_shipping_gross'] ?? $product[
'shipping_gross'] ?? 0), 2,
'.',
'');
2754 $priceInherited = $storedPrice < 0;
2755 $shippingInherited = $storedShipping < 0;
2758 'price_gross' => $priceInherited ? $inheritedPrice : number_format($storedPrice, 2,
'.',
''),
2759 'shipping_gross' => $shippingInherited ? $inheritedShipping : number_format($storedShipping, 2,
'.',
''),
2760 'price_gross_inherited' => $priceInherited ?
'1' :
'0',
2761 'price_gross_inherited_value' => $inheritedPrice,
2762 'shipping_gross_inherited' => $shippingInherited ?
'1' :
'0',
2763 'shipping_gross_inherited_value' => $inheritedShipping,
2767 private function productChannelMapping(): string {
2768 $this->ensureSeed();
2769 dbx()->get_system_obj(
'dbxForm',
'use');
2771 $texts->init(
'shop-product-channel-mapping-texts');
2772 $texts->_fd =
'dbxShop|shop-product-channel';
2773 $texts->load_fd_messages();
2774 $texts->set_form_help_enabled(
false);
2775 $productId = (int)
dbx()->get_modul_var(
'id', 0,
'int');
2776 $channelKey = trim((
string)
dbx()->get_modul_var(
'channel',
'',
'parameter'));
2777 if ($productId <= 0 || $channelKey ===
'') {
2778 return $this->frame(
'<div class="alert alert-warning m-3">' . $this->h(
$texts->get_fd_message(
'mapping_missing')) .
'</div>',
$texts->get_fd_message(
'mapping_title'));
2781 $state = $this->repo()->productChannelMapping($productId, $channelKey);
2783 return $this->frame(
'<div class="alert alert-warning m-3">' . $this->h(
$texts->get_fd_message(
'mapping_not_found')) .
'</div>',
$texts->get_fd_message(
'mapping_title'));
2786 $product = (array)
$state[
'product'];
2787 $channel = (array)
$state[
'channel'];
2788 $productChannel = (array)
$state[
'product_channel'];
2789 $mapping = (array)
$state[
'mapping'];
2790 $platform = (string)($channel[
'platform_type'] ??
'custom');
2791 $mappingDefaults = $this->channelMappingInheritedDefaults($platform, $channel, $product);
2792 $mapping = $this->mergeMappingDefaults($mappingDefaults, $mapping);
2795 if ($this->posted(
'save_channel_mapping')) {
2796 $saveProductId = (int)(
$_POST[
'product_id'] ?? $productId);
2797 $saveChannelKey = trim((
string)(
$_POST[
'channel_key_ref'] ?? $channelKey));
2798 if ($saveProductId === $productId && $saveChannelKey === $channelKey) {
2799 $this->repo()->saveProductChannelMapping($productId, $channelKey, array(
2800 'active' => !empty(
$_POST[
'active']) ? 1 : 0,
2801 'channel_sku' => (
string)(
$_POST[
'channel_sku'] ??
''),
2802 'price_gross' => $this->channelInheritedDecimalValue(
'price_gross'),
2803 'shipping_gross' => $this->channelInheritedDecimalValue(
'shipping_gross'),
2804 'external_listing_id' => (
string)(
$_POST[
'external_listing_id'] ??
''),
2805 'external_offer_id' => (
string)(
$_POST[
'external_offer_id'] ??
''),
2806 'mapping' => $this->removeInheritedMappingDefaults($this->collectProductChannelMapping($platform, $mappingDefaults), $mappingDefaults),
2808 $message =
$texts->get_fd_message(
'mapping_saved');
2809 $state = $this->repo()->productChannelMapping($productId, $channelKey) ?:
$state;
2810 $productChannel = (array)
$state[
'product_channel'];
2811 $mapping = (array)
$state[
'mapping'];
2812 $mappingDefaults = $this->channelMappingInheritedDefaults($platform, $channel, $product);
2813 $mapping = $this->mergeMappingDefaults($mappingDefaults, $mapping);
2817 $displayValues = $this->channelMappingDisplayValues($product, $productChannel);
2818 $form =
dbx()->get_system_obj(
'dbxForm');
2819 $form->init(
'shop-product-channel-mapping-' . $productId .
'-' . $channelKey,
'shop-product-channel-mapping');
2820 $form->_dd =
'dbxShop|shopProductChannel';
2821 $form->_fd =
'dbxShop|shop-product-channel';
2822 $form->load_fd_messages();
2823 $form->_data = array_merge($productChannel + array(
2824 'product_id' => $productId,
2825 'channel_key' => $channelKey,
2827 'channel_sku' => (
string)($product[
'sku'] ??
''),
2828 'price_gross' => -1,
2829 'shipping_gross' => -1,
2831 'price_gross' => $displayValues[
'price_gross'],
2832 'shipping_gross' => $displayValues[
'shipping_gross'],
2834 $form->_rid = (int)($productChannel[
'id'] ?? 0);
2835 $form->_action =
'?dbx_modul=dbxShop_admin&dbx_run1=product_channel_mapping&id=' . $productId .
'&channel=' . rawurlencode($channelKey);
2836 $form->set_activ_id((
int)($productChannel[
'id'] ?? 0));
2837 $form->_msg_info =
'';
2838 $form->add_rep(
'shop_admin_style', $this->shopAdminStyle());
2839 $form->add_rep(
'bar_class',
'dbx-module-bar');
2840 $form->add_rep(
'bar_title_class',
'dbx-module-bar-titleblock');
2841 $form->add_rep(
'bar_actions_class',
'dbx-module-bar-actions');
2842 $form->add_rep(
'bar_icon',
'bi-sliders2');
2843 $form->add_rep(
'bar_title', $this->h(
$texts->get_fd_message(
'mapping_title')));
2844 $form->add_rep(
'bar_subtitle', $this->h((
string)($product[
'sku'] ??
'') .
' - ' . (
string)($channel[
'title'] ?? $channelKey)));
2845 $form->add_rep(
'bar_actions', $this->helpButton($this->ensureShopProductChannelMappingHelpPage(),
$texts->get_fd_message(
'mapping_help'),
'btn btn-outline-secondary btn-sm')
2846 .
'<button class="btn btn-primary btn-sm" type="submit" title="' . $this->h(
$texts->get_fd_message(
'mapping_save')) .
'"><i class="bi bi-save"></i> ' . $this->h(
$texts->get_fd_message(
'mapping_save')) .
'</button>'
2847 .
'<a class="btn btn-outline-primary btn-sm dbxConfirm" href="' . $this->h($this->actionUrl(
'?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id=' . $productId .
'&export_channel=' . rawurlencode($channelKey))) .
'" data-confirm-title="<i class=\'bi bi-broadcast\'></i> ' . $this->h(
$texts->get_fd_message(
'mapping_export_title')) .
'" data-confirm="' . $this->h(
$texts->get_fd_message(
'mapping_export_confirm')) .
'" data-confirm-buttons="yesno" title="' . $this->h(
$texts->get_fd_message(
'mapping_export_label')) .
'"><i class="bi bi-broadcast"></i></a>');
2848 $form->add_rep(
'product_id', (
string)$productId);
2849 $form->add_rep(
'channel_key', $this->h($channelKey));
2850 $form->add_rep(
'price_gross_inherited', $displayValues[
'price_gross_inherited']);
2851 $form->add_rep(
'price_gross_inherited_value', $displayValues[
'price_gross_inherited_value']);
2852 $form->add_rep(
'shipping_gross_inherited', $displayValues[
'shipping_gross_inherited']);
2853 $form->add_rep(
'shipping_gross_inherited_value', $displayValues[
'shipping_gross_inherited_value']);
2854 $form->add_rep(
'mapping_message', $message !==
'' ?
'<div class="alert alert-success mb-3">' . $this->h($message) .
'</div>' :
'');
2855 $form->add_rep(
'mapping_intro', $this->h(
$texts->get_fd_message(
'mapping_intro')));
2856 $form->add_rep(
'mapping_values_title', $this->h(
$texts->get_fd_message(
'mapping_values_title')));
2857 $form->add_rep(
'mapping_export_status_title', $this->h(
$texts->get_fd_message(
'mapping_export_status_title')));
2858 $form->add_rep(
'provider_title', $this->h(($channel[
'title'] ?? $channelKey) .
' Mapping'));
2859 $form->add_rep(
'export_status_view', $this->productChannelExportStatusHtml($productChannel,
$texts));
2860 $form->add_fld(
'active');
2861 $form->add_fld(
'channel_sku', placeholder: (
string)($product[
'sku'] ??
''));
2862 $form->add_fld(
'price_gross', placeholder: $displayValues[
'price_gross']);
2863 $form->add_fld(
'shipping_gross', placeholder: $displayValues[
'shipping_gross']);
2864 $form->add_fld(
'external_listing_id');
2865 $form->add_fld(
'external_offer_id');
2866 $form->add_obj(
'provider_mapping',
'obj-value', $this->providerMappingHtml($platform, $mapping, $channel, $product,
$texts));
2867 return $form->run();
2870 private function productChannelExportStatusHtml(array $productChannel,
$texts): string {
2871 $status = trim((string)($productChannel[
'export_status'] ??
''));
2872 $message = trim((
string)($productChannel[
'export_message'] ??
''));
2873 $listing = trim((
string)($productChannel[
'external_listing_id'] ??
''));
2874 $offer = trim((
string)($productChannel[
'external_offer_id'] ??
''));
2875 $date = trim((
string)($productChannel[
'last_export_date'] ??
''));
2876 $html =
'<div class="d-flex flex-wrap gap-2 align-items-center">';
2877 $html .=
'<span class="badge text-bg-' . ($status ===
'failed' ?
'danger' : ($status !==
'' ?
'info' :
'secondary')) .
'">' . $this->h($status !==
'' ? $status :
$texts->get_fd_message(
'mapping_not_exported')) .
'</span>';
2878 if ($date !==
'')
$html .=
'<span class="text-muted small">' . $this->h($date) .
'</span>';
2879 if ($listing !==
'')
$html .=
'<code>Listing: ' . $this->h($listing) .
'</code>';
2880 if ($offer !==
'')
$html .=
'<code>Offer: ' . $this->h($offer) .
'</code>';
2882 if ($message !==
'') {
2883 $html .=
'<div class="alert alert-secondary py-2 mt-2 mb-0">' . $this->h($message) .
'</div>';
2898 private function posted(
string $action): bool {
2899 if ((
$_SERVER[
'REQUEST_METHOD'] ??
'') !==
'POST'
2900 || (string)(
$_POST[
'shop_action'] ??
'') !== $action) {
2904 $id = max(0, (
int)(
$_POST[
'id'] ?? 0));
2905 $suffix = $id > 0 ? (string)$id :
'new';
2909 case 'save_channel_mapping':
2910 $productId = max(0, (
int)(
$_POST[
'product_id'] ?? 0));
2911 $channelKey = trim((
string)(
$_POST[
'channel_key_ref'] ??
''));
2912 if ($productId <= 0 || !preg_match(
'/^[A-Za-z0-9_-]+$/', $channelKey)) {
2916 'fid' =>
'shop-product-channel-mapping-' . $productId .
'-' . $channelKey,
2917 'fd' =>
'dbxShop|shop-product-channel',
2918 'fields' => array(
'active',
'channel_sku',
'price_gross',
'shipping_gross',
'external_listing_id',
'external_offer_id'),
2922 case 'save_product_group':
2924 'fid' =>
'shop-product-group-' . $suffix,
2925 'fd' =>
'dbxShop|shop-product-group',
2926 'fields' => array(
'group_key',
'parent_id',
'title',
'description',
'tax_class',
'card_template',
'detail_template',
'gallery_template',
'gallery_visible_count',
'gallery_image_size',
'gallery_lightbox_width',
'gallery_overflow',
'gallery_click',
'attribute_notes',
'ebay_category_id',
'amazon_product_type',
'kleinanzeigen_category_id',
'mobile_category_id',
'sorter',
'active'),
2930 case 'delete_product_group':
2931 $contract = array(
'fid' =>
'shop-product-group-' . $suffix,
'fd' =>
'',
'fields' => array());
2934 case 'save_attribute_definition':
2936 'fid' =>
'shop-attribute-definition-' . $suffix,
2937 'fd' =>
'dbxShop|shop-attribute-definition',
2938 'fields' => array(
'group_id',
'attr_key',
'title',
'input_type',
'unit',
'options',
'required',
'filterable',
'comparable',
'sorter',
'active'),
2942 case 'save_product_attributes':
2943 $productId = max(0, (
int)(
$_POST[
'product_id'] ?? 0));
2944 if ($productId <= 0)
return false;
2945 $contract = array(
'fid' =>
'shop-product-attributes-' . $productId,
'fd' =>
'',
'fields' => array());
2948 case 'save_shipping_group':
2950 'fid' =>
'shop-shipping-group-' . $suffix,
2951 'fd' =>
'dbxShop|shop-shipping-group',
2952 'fields' => array(
'group_key',
'title',
'description',
'shipping_way',
'delivery_time',
'shipping_gross',
'free_from_gross',
'sorter',
'active'),
2956 case 'delete_shipping_group':
2957 $contract = array(
'fid' =>
'shop-shipping-group-' . $suffix,
'fd' =>
'',
'fields' => array());
2960 case 'save_channel_group':
2962 'fid' =>
'shop-channel-group-' . $suffix,
2963 'fd' =>
'dbxShop|shop-channel-group',
2964 'fields' => array(
'group_key',
'title',
'description',
'sorter',
'active'),
2968 case 'delete_channel_group':
2969 $contract = array(
'fid' =>
'shop-channel-group-' . $suffix,
'fd' =>
'',
'fields' => array());
2972 case 'save_channel':
2973 case 'test_channel':
2975 'fid' =>
'shop-channel-form-' . $suffix,
2976 'fd' =>
'dbxShop|shop-channel',
2977 'fields' => array(),
2978 'all_fields' =>
true,
2982 case 'delete_channel':
2983 $contract = array(
'fid' =>
'shop-channel-form-' . $suffix,
'fd' =>
'',
'fields' => array());
2994 dbx()->get_system_obj(
'dbxForm');
2995 $form = new \dbxForm();
2997 if ((
string)(
$contract[
'fd'] ??
'') !==
'') {
3012 if (!
$form->submit()) {
3013 $this->postedFormError =
'Die Sicherheitsprüfung des Formulars ist fehlgeschlagen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.';
3018 'Shop-Admin-Formular abgewiesen',
3019 'fid=' . (
string)
$contract[
'fid'] .
' reason=token ip=' . (
string)(
$_SERVER[
'REMOTE_ADDR'] ??
'')
3024 if (
$form->errors()) {
3025 $this->postedFormError =
'Bitte prüfen Sie die markierten beziehungsweise erforderlichen Eingaben.';
3030 'Shop-Admin-Formular abgewiesen',
3031 'fid=' . (
string)
$contract[
'fid'] .
' reason=validation ip=' . (
string)(
$_SERVER[
'REMOTE_ADDR'] ??
'')
3036 if ($action ===
'save_product_attributes') {
3037 foreach ((array)(
$_POST[
'attr_value'] ?? array()) as
$value) {
3038 if (is_array(
$value) || mb_strlen((
string)
$value) > 255) {
3039 $this->postedFormError =
'Ein Attributwert ist ungültig oder länger als 255 Zeichen.';
3048 private function cmsEndpoint(
string $run1, array $params = array(),
bool $mutating =
false): string {
3049 $url =
'?dbx_modul=dbxContent_admin&dbx_run1=' . rawurlencode($run1);
3050 foreach ($params as $key =>
$value) {
3051 $url .=
'&' . rawurlencode((
string)$key) .
'=' . rawurlencode((
string)
$value);
3059 private function shopEndpoint(
string $run1, array $params = array(),
bool $mutating =
false): string {
3060 $url =
'?dbx_modul=dbxShop_admin&dbx_run1=' . rawurlencode($run1);
3061 foreach ($params as $key =>
$value) {
3062 $url .=
'&' . rawurlencode((
string)$key) .
'=' . rawurlencode((
string)
$value);
3064 return $mutating ? $this->actionUrl(
$url) :
$url;
3067 private function readJsonPayload(): array {
3068 $raw = (string)file_get_contents(
'php://input');
3069 $data = $raw !==
'' ? json_decode($raw,
true) : null;
3070 if (is_array($data)) {
3076 private function ensureCmsShopMediaFolder(): void {
3078 if (!is_dir(
$dir)) {
3079 @mkdir(
$dir, 0775,
true);
3083 private function jsonExit(array $data): string {
3084 if (!headers_sent()) {
3085 header(
'Content-Type: application/json; charset=utf-8');
3087 echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
3091 private function assignMedia(): string {
3092 if (!$this->checkActionToken(
'assign_media')) {
3093 return $this->jsonExit(array(
'ok' => 0,
'msg' => $this->postedFormError));
3095 $payload = $this->readJsonPayload();
3096 $productId = (int)($payload[
'product_id'] ?? 0);
3097 $groupId = (int)($payload[
'group_id'] ?? 0);
3098 $mediaId = (int)($payload[
'media_id'] ?? $payload[
'id'] ?? 0);
3099 if (
$mediaId <= 0 || ($productId <= 0 && $groupId <= 0)) {
3100 return $this->jsonExit(array(
'ok' => 0,
'msg' =>
'Bitte Artikel oder Artikelgruppe und ein Medium auswaehlen.'));
3103 $row = $this->repo()->saveMediaImage(
3107 (
string)($payload[
'title'] ??
''),
3108 (
string)($payload[
'alt'] ?? $payload[
'title'] ??
''),
3109 !empty($payload[
'is_primary']) ? 1 : 0,
3110 (
int)($payload[
'sorter'] ?? 100)
3113 return $this->jsonExit(array(
'ok' => 0,
'msg' =>
'Bild konnte nicht zugeordnet werden.'));
3115 $this->syncShopMediaUsage();
3116 return $this->jsonExit(array(
3119 'url' => $this->mediaItemUrl($row,
true),
3123 private function productTreeMove(): string {
3124 if (!$this->checkActionToken(
'product_tree_move')) {
3125 return $this->jsonExit(array(
'ok' => 0,
'msg' => $this->postedFormError));
3127 $this->ensureSeed();
3128 $payload = $this->readJsonPayload();
3129 $type = (string)($payload[
'type'] ??
'');
3130 $targetGroupId = (int)($payload[
'target_group_id'] ?? 0);
3132 if ($type ===
'product') {
3133 $productId = (int)($payload[
'product_id'] ?? 0);
3134 if ($productId <= 0 || $targetGroupId <= 0) {
3135 return $this->jsonExit(array(
'ok' => 0,
'msg' =>
'Artikel und Zielgruppe sind erforderlich.'));
3137 $count = $this->repo()->setProductGroupForProducts(array($productId), $targetGroupId);
3139 return $this->jsonExit(array(
'ok' => 0,
'msg' =>
'Artikel konnte nicht verschoben werden.'));
3141 return $this->jsonExit(array(
'ok' => 1,
'msg' =>
'Artikelgruppe wurde zugeordnet.'));
3144 if ($type ===
'group') {
3145 $groupId = (int)($payload[
'group_id'] ?? 0);
3146 if ($groupId <= 0) {
3147 return $this->jsonExit(array(
'ok' => 0,
'msg' =>
'Artikelgruppe ist erforderlich.'));
3149 if (!$this->repo()->moveProductGroupParent($groupId, $targetGroupId)) {
3150 return $this->jsonExit(array(
'ok' => 0,
'msg' =>
'Artikelgruppe konnte nicht verschoben werden. Pruefen Sie, ob dadurch ein Kreis entstehen wuerde.'));
3152 return $this->jsonExit(array(
'ok' => 1,
'msg' =>
'Artikelgruppe wurde verschoben.'));
3155 return $this->jsonExit(array(
'ok' => 0,
'msg' =>
'Unbekannte Drag-Drop-Aktion.'));
3158 private function dashboard(): string {
3159 $stats = $this->repo()->dashboardStats();
3160 return $this->frame($this->tpl()->get_tpl(
'dbxShop_admin|admin-dashboard', array(
3161 'orders_open' => (
string)($stats[
'orders_open'] ?? 0),
3162 'payments_open' => (
string)($stats[
'payments_open'] ?? 0),
3163 'shipping_open' => (
string)($stats[
'shipping_open'] ?? 0),
3164 'withdrawals_open' => (
string)($stats[
'withdrawals_open'] ?? 0),
3165 'stock_low' => (
string)($stats[
'stock_low'] ?? 0),
3166 'products_active' => (
string)($stats[
'products_active'] ?? 0),
3167 'product_url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=products',
3168 'order_url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=orders',
3169 'group_url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=groups',
3170 'attribute_url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=attributes',
3171 'shipping_group_url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=shipping_groups',
3172 'channel_group_url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=channel_groups',
3173 'channel_url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=channels',
3174 'media_url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=media',
3175 'legal_url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=legal',
3176 'return_url' =>
'?dbx_modul=dbxShop_admin&dbx_run1=returns',
3177 'install_url' => $this->actionUrl(
'?dbx_modul=dbxShop_admin&dbx_run1=install'),
3181 private function placeholder(
string $title,
string $text): string {
3182 return $this->frame(
3183 '<div class="alert alert-info m-3"><strong>' . $this->h($title) .
'</strong><br>' . $this->h($text) .
'</div>',
3188 private function shopLegalCmsPage(
string $key,
string $title,
string $intro,
string $shopRun): string {
3189 $service =
dbx()->get_include_obj(
'dbxShopService',
'dbxShop');
3193 $cid = (int)(
$pages[$key] ?? 0);
3195 return $this->placeholder($title,
'Die CMS-Seite konnte nicht angelegt oder gefunden werden.');
3198 $row = $this->db()->select1($this->contentDd(), $cid,
'id,title,permalink,content,group_read,template,activ', 0);
3199 if (!is_array($row) || (
int)($row[
'id'] ?? 0) <= 0) {
3200 return $this->placeholder($title,
'Die CMS-Seite konnte nicht geladen werden.');
3203 $editUrl =
'?dbx_modul=dbxContent_admin&dbx_run1=cms&cid=' . $cid;
3204 $cmsViewUrl =
'?dbx_modul=dbxContent&dbx_run1=content&cid=' . $cid;
3205 $shopViewUrl =
'?dbx_modul=dbxShop&dbx_run1=' . rawurlencode($shopRun);
3206 $permalink = (string)($row[
'permalink'] ??
'');
3208 $actions = $this->openWinButton($editUrl, $title .
' bearbeiten',
'<i class="bi bi-pencil-square"></i><span> Bearbeiten</span>',
'btn btn-primary btn-sm me-1',
'94%',
'92%')
3209 . $this->openWinButton($cmsViewUrl, $title .
' CMS-Ansicht',
'<i class="bi bi-file-richtext"></i><span class="visually-hidden"> CMS-Ansicht</span>',
'btn btn-outline-primary btn-sm me-1',
'82%',
'86%')
3210 . $this->openWinButton($shopViewUrl, $title .
' im Shop ansehen',
'<i class="bi bi-box-arrow-up-right"></i><span class="visually-hidden"> Shop-Ansicht</span>',
'btn btn-outline-primary btn-sm me-1',
'82%',
'86%');
3212 $renderer =
dbx()->get_include_obj(
'dbxContentRenderer',
'dbxContent');
3214 ? (string)
$renderer->renderStatic($cid, array(
'template' =>
'c-body1-footer',
'skip_hits' =>
true))
3215 : trim((
string)($row[
'content'] ??
''));
3216 if (trim($preview) ===
'') {
3217 $preview =
'<div class="alert alert-warning">Die CMS-Seite ist leer.</div>';
3220 $meta =
'<dl class="dbx-shop-admin-cms-meta">'
3221 .
'<dt>CMS-ID</dt><dd>' . $cid .
'</dd>'
3222 .
'<dt>Permalink</dt><dd><code>' . $this->h($permalink) .
'</code></dd>'
3223 .
'<dt>Status</dt><dd>' . ((int)($row[
'activ'] ?? 0) === 1 ?
'Aktiv' :
'Inaktiv') .
'</dd>'
3224 .
'<dt>Leserechte</dt><dd><code>' . $this->h((
string)($row[
'group_read'] ??
'')) .
'</code></dd>'
3225 .
'<dt>Template</dt><dd><code>' . $this->h((
string)($row[
'template'] ??
'')) .
'</code></dd>'
3228 $html =
'<section class="dbx-shop-admin-cms-page">'
3229 .
'<div class="alert alert-info"><strong>' . $this->h($title) .
'</strong><br>' . $this->h($intro) .
'</div>'
3231 .
'<div class="dbx-shop-admin-cms-preview">' . $preview .
'</div>'
3237 private function settingsBool(array
$cfg,
string $key,
bool $default =
false): bool {
3238 if (!array_key_exists($key,
$cfg)) {
3243 return in_array(strtolower(trim((
string)
$value)), array(
'1',
'true',
'yes',
'on'),
true);
3246 private function saveSettings(): void {
3247 $cfg = $this->shopConfig();
3249 $cfg[
'activ'] =
$cfg[
'enabled'] ?
'1' :
'0';
3250 $cfg[
'default_channel'] = trim((
string)(
$_POST[
'default_channel'] ??
'shop')) ?:
'shop';
3251 $cfg[
'default_currency'] = strtoupper(substr(preg_replace(
'~[^A-Z]~i',
'', (
string)(
$_POST[
'default_currency'] ??
'EUR')) ?:
'EUR', 0, 3));
3252 $cfg[
'price_display'] = in_array((
string)(
$_POST[
'price_display'] ??
'gross'), array(
'gross',
'net'),
true) ? (string)
$_POST[
'price_display'] :
'gross';
3253 $cfg[
'default_tax_class'] = in_array((
string)(
$_POST[
'default_tax_class'] ??
'mwst1'), array(
'mwst1',
'mwst2',
'mwst3'),
true) ? (string)
$_POST[
'default_tax_class'] :
'mwst1';
3254 $cfg[
'tax_display_enabled'] = (string)(
$_POST[
'tax_display_enabled'] ??
'1') !==
'0';
3257 foreach (array(
'mwst1',
'mwst2',
'mwst3') as $key) {
3258 $title = trim((
string)(
$_POST[
'tax_title_' . $key] ?? $key));
3259 $rate = str_replace(
',',
'.', trim((
string)(
$_POST[
'tax_rate_' . $key] ??
'0')));
3260 $rates[$key] = array(
3261 'title' => $title !==
'' ? $title : $key,
3262 'rate' => number_format((
float)$rate, 2,
'.',
''),
3265 $cfg[
'tax_rates'] = $rates;
3271 'checkout_guest_allowed',
3272 'demo_notice_enabled',
3273 'legal_snapshot_enabled',
3274 'withdrawal_button_enabled',
3275 'mail_customer_enabled',
3276 'mail_admin_enabled',
3277 'payment_bank_transfer_enabled',
3278 'payment_invoice_enabled',
3279 'payment_paypal_enabled',
3280 'payment_amazon_pay_enabled',
3281 'delivery_digital_download_enabled',
3282 'delivery_flat_shipping_enabled',
3287 $cfg[
'payment_bank_transfer_account_owner'] = trim((
string)(
$_POST[
'payment_bank_transfer_account_owner'] ??
''));
3288 $cfg[
'payment_bank_transfer_iban'] = trim((
string)(
$_POST[
'payment_bank_transfer_iban'] ??
''));
3289 $cfg[
'payment_bank_transfer_bic'] = trim((
string)(
$_POST[
'payment_bank_transfer_bic'] ??
''));
3290 $cfg[
'payment_bank_transfer_bank_name'] = trim((
string)(
$_POST[
'payment_bank_transfer_bank_name'] ??
''));
3291 $cfg[
'payment_bank_transfer_instructions'] = trim((
string)(
$_POST[
'payment_bank_transfer_instructions'] ??
''));
3292 $cfg[
'payment_invoice_instructions'] = trim((
string)(
$_POST[
'payment_invoice_instructions'] ??
''));
3293 $cfg[
'payment_paypal_mode'] = (string)(
$_POST[
'payment_paypal_mode'] ??
'sandbox') ===
'live' ?
'live' :
'sandbox';
3294 $cfg[
'payment_paypal_client_id'] = trim((
string)(
$_POST[
'payment_paypal_client_id'] ??
''));
3295 $cfg[
'payment_paypal_client_secret'] = trim((
string)(
$_POST[
'payment_paypal_client_secret'] ??
''));
3296 $cfg[
'payment_paypal_brand_name'] = trim((
string)(
$_POST[
'payment_paypal_brand_name'] ??
'dbXapp')) ?:
'dbXapp';
3297 $cfg[
'payment_paypal_currency'] =
$cfg[
'default_currency'];
3298 $cfg[
'payment_amazon_pay_mode'] = (string)(
$_POST[
'payment_amazon_pay_mode'] ??
'sandbox') ===
'live' ?
'live' :
'sandbox';
3299 $cfg[
'payment_amazon_pay_region'] = in_array((
string)(
$_POST[
'payment_amazon_pay_region'] ??
'EU'), array(
'EU',
'UK',
'US'),
true) ? (string)
$_POST[
'payment_amazon_pay_region'] :
'EU';
3300 $cfg[
'payment_amazon_pay_merchant_id'] = trim((
string)(
$_POST[
'payment_amazon_pay_merchant_id'] ??
''));
3301 $cfg[
'payment_amazon_pay_store_id'] = trim((
string)(
$_POST[
'payment_amazon_pay_store_id'] ??
''));
3302 $cfg[
'payment_amazon_pay_public_key_id'] = trim((
string)(
$_POST[
'payment_amazon_pay_public_key_id'] ??
''));
3303 $cfg[
'payment_amazon_pay_private_key'] = trim((
string)(
$_POST[
'payment_amazon_pay_private_key'] ??
''));
3304 $cfg[
'payment_amazon_pay_sandbox_simulation_code'] = trim((
string)(
$_POST[
'payment_amazon_pay_sandbox_simulation_code'] ??
''));
3305 $cfg[
'mail_from'] = trim((
string)(
$_POST[
'mail_from'] ??
''));
3306 $cfg[
'mail_admin_to'] = trim((
string)(
$_POST[
'mail_admin_to'] ??
''));
3308 $flatShipping = str_replace(
',',
'.', trim((
string)(
$_POST[
'delivery_flat_shipping_gross_price'] ??
'0')));
3309 $cfg[
'delivery_flat_shipping_gross_price'] = number_format((
float)$flatShipping, 2,
'.',
'');
3310 $cfg[
'media_usage_slot'] = preg_replace(
'~[^a-z0-9_-]+~i',
'', (
string)(
$_POST[
'media_usage_slot'] ??
'shop')) ?:
'shop';
3313 'mail_profile' => trim((
string)(
$cfg[
'mail_profile'] ??
'dbxApp')) ?:
'dbxApp',
3314 'mail_from' =>
$cfg[
'mail_from'],
3315 'mail_from_name' => trim((
string)(
$cfg[
'mail_from_name'] ??
'dbxShop')) ?:
'dbxShop',
3316 'mail_admin_to' =>
$cfg[
'mail_admin_to'],
3318 dbx()->patch_local_config(
'dbxShop', $mailLocal);
3319 dbx()->set_config(
'dbxShop',
$cfg);
3320 $this->loadContentCacheSupport();
3321 if (class_exists(
'\\dbx\\dbxContent\\dbxContentPageCache')) {
3326 private function settingsFormData(array
$cfg): array {
3327 $rates = $this->taxRatesConfig();
3329 foreach (array(
'mwst1',
'mwst2',
'mwst3') as $key) {
3330 $rate = is_array($rates[$key] ??
null) ? $rates[$key] : array();
3331 $data[
'tax_title_' . $key] = (string)($rate[
'title'] ?? $key);
3332 $data[
'tax_rate_' . $key] = (string)($rate[
'rate'] ??
'0');
3340 'checkout_guest_allowed',
3341 'demo_notice_enabled',
3342 'legal_snapshot_enabled',
3343 'withdrawal_button_enabled',
3344 'mail_customer_enabled',
3345 'mail_admin_enabled',
3346 'payment_bank_transfer_enabled',
3347 'payment_invoice_enabled',
3348 'payment_paypal_enabled',
3349 'payment_amazon_pay_enabled',
3350 'delivery_digital_download_enabled',
3351 'delivery_flat_shipping_enabled',
3353 $data[$key] = $this->settingsBool(
$cfg, $key, in_array($key, array(
3355 'checkout_guest_allowed',
3356 'demo_notice_enabled',
3357 'legal_snapshot_enabled',
3358 'withdrawal_button_enabled',
3359 'mail_customer_enabled',
3360 'mail_admin_enabled',
3361 'payment_bank_transfer_enabled',
3362 'delivery_digital_download_enabled',
3363 'delivery_flat_shipping_enabled',
3367 $data[
'default_channel'] = (string)(
$cfg[
'default_channel'] ??
'shop');
3368 $data[
'channels_enabled'] = array_key_exists(
'channels_enabled',
$cfg) ? (int)((
bool)
$cfg[
'channels_enabled']) : 1;
3369 $data[
'default_currency'] = (string)(
$cfg[
'default_currency'] ??
'EUR');
3370 $data[
'price_display'] = (string)(
$cfg[
'price_display'] ??
'gross');
3371 $data[
'default_tax_class'] = (string)(
$cfg[
'default_tax_class'] ??
'mwst1');
3372 $data[
'tax_display_enabled'] = $this->settingsBool(
$cfg,
'tax_display_enabled',
true) ? 1 : 0;
3373 $data[
'payment_paypal_mode'] = (string)(
$cfg[
'payment_paypal_mode'] ??
'sandbox');
3374 $data[
'payment_paypal_brand_name'] = (string)(
$cfg[
'payment_paypal_brand_name'] ??
'dbXapp');
3375 $data[
'payment_paypal_client_id'] = (string)(
$cfg[
'payment_paypal_client_id'] ??
'');
3376 $data[
'payment_paypal_client_secret'] = (string)(
$cfg[
'payment_paypal_client_secret'] ??
'');
3377 $data[
'payment_bank_transfer_account_owner'] = (string)(
$cfg[
'payment_bank_transfer_account_owner'] ??
'');
3378 $data[
'payment_bank_transfer_iban'] = (string)(
$cfg[
'payment_bank_transfer_iban'] ??
'');
3379 $data[
'payment_bank_transfer_bic'] = (string)(
$cfg[
'payment_bank_transfer_bic'] ??
'');
3380 $data[
'payment_bank_transfer_bank_name'] = (string)(
$cfg[
'payment_bank_transfer_bank_name'] ??
'');
3381 $data[
'payment_bank_transfer_instructions'] = (string)(
$cfg[
'payment_bank_transfer_instructions'] ??
'Bitte ueberweisen Sie den Rechnungsbetrag unter Angabe der Bestellnummer.');
3382 $data[
'payment_invoice_instructions'] = (string)(
$cfg[
'payment_invoice_instructions'] ??
'Sie erhalten eine Rechnung. Bitte zahlen Sie innerhalb der angegebenen Frist.');
3383 $data[
'payment_amazon_pay_mode'] = (string)(
$cfg[
'payment_amazon_pay_mode'] ??
'sandbox');
3384 $data[
'payment_amazon_pay_region'] = (string)(
$cfg[
'payment_amazon_pay_region'] ??
'EU');
3385 $data[
'payment_amazon_pay_merchant_id'] = (string)(
$cfg[
'payment_amazon_pay_merchant_id'] ??
'');
3386 $data[
'payment_amazon_pay_store_id'] = (string)(
$cfg[
'payment_amazon_pay_store_id'] ??
'');
3387 $data[
'payment_amazon_pay_public_key_id'] = (string)(
$cfg[
'payment_amazon_pay_public_key_id'] ??
'');
3388 $data[
'payment_amazon_pay_private_key'] = (string)(
$cfg[
'payment_amazon_pay_private_key'] ??
'');
3389 $data[
'payment_amazon_pay_sandbox_simulation_code'] = (string)(
$cfg[
'payment_amazon_pay_sandbox_simulation_code'] ??
'');
3390 $data[
'mail_from'] = (string)(
$cfg[
'mail_from'] ??
'');
3391 $data[
'mail_admin_to'] = (string)(
$cfg[
'mail_admin_to'] ??
'');
3392 $data[
'delivery_flat_shipping_gross_price'] = (string)(
$cfg[
'delivery_flat_shipping_gross_price'] ??
'5.90');
3393 $data[
'media_usage_slot'] = (string)(
$cfg[
'media_usage_slot'] ??
'shop');
3398 private function settingsChannelsStatusHtml(array
$cfg,
$texts): string {
3399 $channelsEnabled = $this->settingsBool(
$cfg,
'channels_enabled', true);
3400 $channels = $this->repo()->channels();
3401 $external = array();
3402 foreach ($channels as $channel) {
3403 $key = strtolower(trim((
string)($channel[
'channel_key'] ??
'')));
3404 if ($key ===
'' || $key ===
'shop') {
3407 if ((
int)($channel[
'active'] ?? 0) !== 1) {
3410 $external[] = $channel;
3413 $html =
'<div class="dbx-shop-settings-channel-status">';
3414 $html .=
'<div class="dbx-shop-settings-channel-head">';
3415 $html .=
'<div><strong>' . $this->h(
$texts->get_fd_message(
'channels_external')) .
'</strong><span>' . $this->h($channelsEnabled ?
$texts->get_fd_message(
'channels_global_active') :
$texts->get_fd_message(
'channels_global_inactive')) .
'</span></div>';
3416 $html .= $this->openWinButton(
'?dbx_modul=dbxShop_admin&dbx_run1=channels',
$texts->get_fd_message(
'channels_edit'),
'<i class="bi bi-broadcast"></i> ' . $this->h(
$texts->get_fd_message(
'column_channel')),
'btn btn-outline-primary btn-sm',
'92%',
'88%');
3419 if (!$channelsEnabled) {
3420 $html .=
'<div class="alert alert-warning py-2 mb-0">' . $this->h(
$texts->get_fd_message(
'channels_disabled')) .
'</div>';
3423 if ($external === array()) {
3424 $html .=
'<div class="dbx-shop-settings-channel-empty">' . $this->h(
$texts->get_fd_message(
'channels_none')) .
'</div>';
3425 return $html .
'</div>';
3428 $html .=
'<div class="dbx-shop-settings-channel-grid">';
3429 $html .=
'<div class="dbx-shop-settings-channel-grid-head"><span>' . $this->h(
$texts->get_fd_message(
'column_channel')) .
'</span><span>' . $this->h(
$texts->get_fd_message(
'column_platform')) .
'</span><span>' . $this->h(
$texts->get_fd_message(
'column_connection')) .
'</span><span>' . $this->h(
$texts->get_fd_message(
'column_export')) .
'</span><span>' . $this->h(
$texts->get_fd_message(
'column_import')) .
'</span><span>' . $this->h(
$texts->get_fd_message(
'column_test')) .
'</span></div>';
3430 foreach ($external as $channel) {
3431 $test = trim((
string)($channel[
'test_status'] ??
''));
3432 $testClass = $test ===
'ok' ?
'success' : ($test !==
'' ?
'warning' :
'secondary');
3433 $html .=
'<div class="dbx-shop-settings-channel-grid-row">';
3434 $html .=
'<span class="dbx-shop-settings-channel-name"><strong>' . $this->h((
string)($channel[
'title'] ?? $channel[
'channel_key'] ??
'')) .
'</strong><code>' . $this->h((
string)($channel[
'channel_key'] ??
'')) .
'</code></span>';
3435 $html .=
'<span>' . $this->h((
string)($channel[
'platform_type'] ??
'')) .
'</span>';
3436 $html .=
'<span>' . $this->h((
string)($channel[
'connection_mode'] ??
'')) .
'</span>';
3437 $html .= ((int)($channel[
'export_enabled'] ?? 0) === 1 ?
'<span class="badge text-bg-success">' . $this->h(
$texts->get_fd_message(
'column_export')) .
'</span>' :
'<span class="badge text-bg-secondary">' . $this->h(
$texts->get_fd_message(
'export_off')) .
'</span>');
3438 $html .= ((int)($channel[
'order_import_enabled'] ?? 0) === 1 ?
'<span class="badge text-bg-primary">' . $this->h(
$texts->get_fd_message(
'column_import')) .
'</span>' :
'<span class="badge text-bg-secondary">' . $this->h(
$texts->get_fd_message(
'import_off')) .
'</span>');
3439 $html .=
'<span class="badge text-bg-' . $testClass .
'">' . $this->h($test !==
'' ? $test :
$texts->get_fd_message(
'not_tested')) .
'</span>';
3443 return $html .
'</div>';
3446 private function settings(): string {
3447 $form =
dbx()->get_system_obj(
'dbxForm');
3448 $form->init(
'shop-settings-form',
'shop-settings-form');
3449 $form->_fd =
'dbxShop_admin|shop-settings';
3450 $form->load_fd_messages();
3451 $helpId = $this->ensureShopSettingsHelpPage();
3452 $helpButton = $helpId > 0
3453 ? $this->openWinButton(
'?dbx_modul=dbxContent&dbx_run1=content&cid=' . $helpId,
$form->get_fd_message(
'settings_help'),
'<i class="bi bi-question-circle"></i><span class="visually-hidden"> ' . $this->h(
$form->get_fd_message(
'settings_help')) .
'</span>',
'btn btn-outline-secondary btn-sm me-1',
'72%',
'82%')
3455 $form->_action =
'?dbx_modul=dbxShop_admin&dbx_run1=settings';
3456 $form->_data = $this->settingsFormData($this->shopConfig());
3457 $form->add_rep(
'shop_admin_style', $this->shopAdminStyle());
3458 $form->add_rep(
'form_class',
'dbx-shop-settings-dbXForm');
3459 $form->add_rep(
'bar_title', $this->h(
$form->get_fd_message(
'settings_title')));
3460 $form->add_rep(
'bar_icon',
'bi-sliders');
3461 $form->add_rep(
'bar_subtitle', $this->h(
$form->get_fd_message(
'settings_subtitle')));
3462 $form->add_rep(
'bar_class',
'dbx-module-bar');
3463 $form->add_rep(
'bar_title_class',
'dbx-module-bar-titleblock');
3464 $form->add_rep(
'bar_title_pre',
'');
3465 $form->add_rep(
'bar_title_heading_attrs',
'');
3466 $form->add_rep(
'bar_middle',
'');
3467 $form->add_rep(
'bar_actions_class',
'dbx-module-bar-actions');
3468 $form->add_rep(
'bar_extra',
'');
3469 $form->add_obj(
'channels_status',
'obj-value', $this->settingsChannelsStatusHtml($this->shopConfig(),
$form));
3470 $paymentTestButton = $this->openWinButton(
'?dbx_modul=dbxShop_admin&dbx_run1=payment_test',
$form->get_fd_message(
'settings_payment_test'),
'<i class="bi bi-plug"></i><span class="visually-hidden"> ' . $this->h(
$form->get_fd_message(
'settings_payment_test')) .
'</span>',
'btn btn-outline-primary btn-sm me-1',
'64%',
'58%');
3471 $form->add_rep(
'bar_actions', $paymentTestButton .
'<button class="btn btn-primary btn-sm" type="submit" name="shop_action" value="save_settings" title="' . $this->h(
$form->get_fd_message(
'settings_save')) .
'"><i class="bi bi-save"></i><span class="visually-hidden"> ' . $this->h(
$form->get_fd_message(
'settings_save')) .
'</span></button>' . $helpButton);
3472 $form->_msg_info =
'';
3475 if (
$form->submit()) {
3476 if (!
$form->errors() && !
$form->warnings()) {
3477 $this->saveSettings();
3478 $form->_data = $this->settingsFormData($this->shopConfig());
3479 $form->add_obj(
'channels_status',
'obj-value', $this->settingsChannelsStatusHtml($this->shopConfig(),
$form));
3483 return $form->run();
3486 private function paymentTest(): string {
3487 $paypal =
dbx()->get_include_obj(
'dbxShopPayPal',
'dbxShop');
3488 $amazonPay =
dbx()->get_include_obj(
'dbxShopAmazonPay',
'dbxShop');
3491 : array(
'ok' =>
false,
'mode' =>
'',
'message' =>
'PayPal-Connector konnte nicht geladen werden.');
3492 $amazonResult = is_object($amazonPay) && method_exists($amazonPay,
'testConnection')
3493 ? $amazonPay->testConnection()
3494 : array(
'ok' =>
false,
'mode' =>
'',
'region' =>
'',
'message' =>
'Amazon-Pay-Connector konnte nicht geladen werden.');
3496 $card =
function(
string $title,
string $icon, array
$result): string {
3499 if (trim((
string)(
$result[
'mode'] ??
'')) !==
'') {
3500 $meta[] =
'Modus: ' . $this->h((
string)
$result[
'mode']);
3502 if (trim((
string)(
$result[
'region'] ??
'')) !==
'') {
3503 $meta[] =
'Region: ' . $this->h((
string)
$result[
'region']);
3505 return $this->tpl()->get_tpl(
'dbxShop_admin|payment-test-card', array(
3506 'icon' => $this->h($icon),
3507 'title' => $this->h($title),
3508 'badge_class' =>
$ok ?
'text-bg-success' :
'text-bg-warning',
3509 'badge_text' =>
$ok ?
'OK' :
'Pruefen',
3510 'meta' => $meta !== array() ?
'<p class="dbx-shop-payment-test-meta">' . implode(
' · ', $meta) .
'</p>' :
'',
3511 'message' => $this->h((
string)(
$result[
'message'] ??
'Keine Rueckmeldung.')),
3515 $body = $this->tpl()->get_tpl(
'dbxShop_admin|payment-test', array(
3519 return $this->frame(
$body,
'Payment testen');
3522 private function install(): string {
3523 if (!$this->checkActionToken(
'install')) {
3524 return $this->placeholder(
'Shop-Installation abgewiesen', $this->postedFormError);
3526 $this->maintenanceMode =
true;
3528 $this->repo()->seedDemoProducts();
3529 $this->maintainShopAdminContent();
3531 $this->maintenanceMode =
false;
3533 return $this->placeholder(
3534 'Shop-Installation ausgefuehrt',
3535 'dbxShop.db3 wurde angelegt bzw. aktualisiert. Deutsche Testartikel, Gruppen und Channel-Zuordnungen sind vorhanden.'
3539 private function products(): string {
3540 $this->ensureSeed();
3541 $report =
dbx()->get_system_obj(
'dbxReport');
3542 $report->init(
'shop-products-report');
3543 $report->_fd =
'dbxShop_admin|rpt-products-selection';
3548 $report->_action = $this->actionUrl(
'?dbx_modul=dbxShop_admin&dbx_run1=products');
3551 $report->_multi_page_select = 1;
3552 $report->_create_row_select =
true;
3553 $report->_create_row_delete =
true;
3554 $report->_create_row_edit =
true;
3555 $report->_create_row_show =
true;
3556 $report->_create_sel_flds =
true;
3559 $report->_msg_confirm_delete =
$report->get_fd_message(
'delete_confirm');
3561 $report->add_rep(
'bar_title',
$report->get_fd_message(
'bar_title'));
3562 $report->add_rep(
'bar_icon',
'bi-bag-check');
3563 $report->add_rep(
'bar_subtitle',
$report->get_fd_message(
'bar_subtitle'));
3564 $report->add_rep(
'bar_class',
'dbx-module-bar');
3565 $report->add_rep(
'bar_title_class',
'dbx-module-bar-titleblock');
3566 $report->add_rep(
'bar_actions_class',
'dbx-module-bar-actions');
3567 $report->add_rep(
'bar_title_pre', $this->productTreeToggleButton(
$report));
3568 $report->add_rep(
'bar_title_heading_attrs',
'');
3569 $report->add_rep(
'bar_middle',
'');
3570 $report->add_rep(
'bar_extra',
'');
3571 $report->add_rep(
'bar_actions', $this->productShellActions(
$report));
3572 $report->add_rep(
'shop_admin_style', $this->shopAdminStyle());
3573 $report->add_rep(
'report_form_class',
'dbx-shop-products-form is-shop-tree-collapsed');
3574 $report->add_rep(
'report_form_attrs',
'data-dbx="lib=shopAdmin" data-shop-tree-shell');
3575 $channelsEnabled = $this->channelsEnabled();
3576 $columnClasses = array(
3577 'image_view' =>
'dbx-shop-col-image',
3578 'article_view' =>
'dbx-shop-col-article',
3579 'groups_view' =>
'dbx-shop-col-groups',
3580 'attributes_view' =>
'dbx-shop-col-attributes',
3581 'shipping_groups_view' =>
'dbx-shop-col-shipping-groups',
3582 'price_view' =>
'dbx-shop-col-money',
3583 'tax_view' =>
'dbx-shop-col-tax',
3584 'shipping_view' =>
'dbx-shop-col-money',
3585 'status_view' =>
'dbx-shop-col-status',
3587 if ($channelsEnabled) {
3588 $columnClasses[
'channel_groups_view'] =
'dbx-shop-col-channel-groups';
3589 $columnClasses[
'channels_view'] =
'dbx-shop-col-channels';
3595 $report->set_callback_owner($this);
3596 $report->set_callback(
'next_record',
'product_report_next_record');
3597 $report->set_callback(
'row_action_data',
'product_report_row_action_data');
3598 $report->add_rep(
'report_products_actions', $this->productReportActionControls(
$report->_action,
$report));
3599 $report->create_selection_fields(
'dbxShop_admin|rpt-products-selection');
3600 $this->handleProductReportAction(
$report);
3602 $query = trim((
string)
$report->get_fld_val(
'dbx_rwhere',
'',
'parameter|max=100'));
3603 $requestedRowsPerPage = (int)
$report->get_fld_val(
'dbx_rrows', 30,
'int');
3604 $rowsPerPage = $requestedRowsPerPage === 0 ? 0 : max(10, min(100, $requestedRowsPerPage));
3605 $position = $rowsPerPage === 0 ? 0 : max(0, (
int)
$report->get_fld_val(
'dbx_rpos', 0,
'int'));
3606 $sort = (string)
$report->get_fld_val(
'dbx_rsort',
'sorter',
'parameter');
3607 $direction = strtoupper((
string)
$report->get_fld_val(
'dbx_rdesc',
'ASC',
'parameter')) ===
'DESC' ?
'DESC' :
'ASC';
3608 $selectedOnly = (int)
$report->get_fld_val(
'dbx_rselect', 0,
'int') === 1;
3609 if (!in_array($sort, array(
'sorter',
'sku',
'title',
'price_gross',
'effective_tax_rate',
'effective_shipping_gross',
'active'),
true)) {
3613 $allProducts = $this->repo()->products(
false);
3614 $matchedProducts = array();
3615 $selectedIds = $selectedOnly ?
$report->get_multi_selects() : array();
3616 foreach ($allProducts as $product) {
3617 if ($selectedOnly && !isset($selectedIds[(
string)(
int)($product[
'id'] ?? 0)])) {
3620 $score = $this->productSearchScore($product, $query);
3624 $product[
'_search_score'] = $score;
3625 $matchedProducts[] = $product;
3627 $matchedProducts = $this->sortProductsForReport($matchedProducts, $query, $sort, $direction);
3628 $filteredCount = count($matchedProducts);
3629 if ($rowsPerPage > 0 && $position >= $filteredCount && $filteredCount > 0) {
3630 $position = max(0, (
int)(floor(($filteredCount - 1) / $rowsPerPage) * $rowsPerPage));
3632 $visibleProducts = $rowsPerPage === 0
3634 : array_slice($matchedProducts, $position, $rowsPerPage);
3637 'image_view' =>
$report->get_fd_message(
'column_image'),
3638 'article_view' =>
$report->get_fd_message(
'column_product'),
3639 'groups_view' =>
$report->get_fd_message(
'column_product_groups'),
3640 'attributes_view' =>
$report->get_fd_message(
'column_attributes'),
3641 'shipping_groups_view' =>
$report->get_fd_message(
'column_shipping_groups'),
3642 'price_view' =>
$report->get_fd_message(
'column_price'),
3643 'tax_view' =>
$report->get_fd_message(
'column_tax'),
3644 'shipping_view' =>
$report->get_fd_message(
'column_shipping'),
3645 'status_view' =>
$report->get_fd_message(
'column_status'),
3647 if ($channelsEnabled) {
3650 'channel_groups_view' =>
$report->get_fd_message(
'column_channel_groups'),
3651 'channels_view' =>
$report->get_fd_message(
'column_channels'),
3653 + array_slice(
$report->_rflds, 5,
null,
true);
3656 'image_view' =>
'html',
3657 'article_view' =>
'html',
3658 'groups_view' =>
'html',
3659 'attributes_view' =>
'html',
3660 'shipping_groups_view' =>
'html',
3661 'price_view' =>
'html',
3662 'tax_view' =>
'html',
3663 'shipping_view' =>
'html',
3664 'status_view' =>
'html',
3666 if ($channelsEnabled) {
3667 $report->_rpt_format[
'channel_groups_view'] =
'html';
3668 $report->_rpt_format[
'channels_view'] =
'html';
3670 $report->_rrows = $rowsPerPage;
3672 $report->_count_all = count($allProducts);
3673 $report->_rcount = $filteredCount;
3674 $report->_rdata = $visibleProducts;
3676 $report->add_rep(
'product_tree_panel', $this->productTreePanel($allProducts,
$report));
3678 if ($filteredCount === 0) {
3679 $content .=
'<div class="alert alert-info mx-3">'
3680 . $this->h(
$report->get_fd_message(
'no_results'))
3687 private function productEdit(): string {
3688 $this->ensureSeed();
3689 $id = (int)
dbx()->get_modul_var(
'id', 0,
'int');
3693 $removeImageId = (int)
dbx()->get_modul_var(
'remove_image', 0,
'int');
3694 if (!$isNew && $removeImageId > 0) {
3695 if ($this->checkActionToken(
'remove_image')) {
3696 $this->repo()->removeProductImageAssociation($removeImageId, $id);
3697 $this->syncShopMediaUsage();
3700 $exportChannel = trim((
string)
dbx()->get_modul_var(
'export_channel',
'',
'parameter'));
3701 if (!$isNew && $exportChannel !==
'') {
3702 if ($this->checkActionToken(
'export_channel')) {
3703 $result = $this->repo()->exportProductToChannel($id, $exportChannel);
3704 $exportOk = !empty(
$result[
'ok']);
3705 $exportNotice = (string)(
$result[
'message'] ??
'');
3707 $exportNotice = $this->postedFormError;
3710 $data = $isNew ? $this->applyProductPreset($this->newProductDefaults()) : $this->repo()->productById($id);
3712 if (!$isNew && !is_array($data)) {
3713 return $this->frame(
'<div class="alert alert-warning m-3">Artikel nicht gefunden.</div>',
'Artikel bearbeiten', $this->productBarActions());
3716 $form =
dbx()->get_system_obj(
'dbxForm');
3717 $form->init(
'shop-product-form',
'shop-product-form');
3718 $form->_dd =
'dbxShop|shopProduct';
3719 $form->_fd =
'dbxShop|shop-product';
3720 $form->load_fd_messages();
3721 $form->_data = $data;
3722 $form->_rid = $isNew ? 0 : $id;
3723 $form->_action =
'?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id=' . ($isNew ? 0 : $id);
3724 $form->set_activ_id($isNew ? 0 : $id);
3727 $form->get_fd_message(
3728 $isNew ?
'form_new_title' :
'form_edit_title'
3731 $form->add_rep(
'bar_icon',
'bi-bag-check');
3735 ?
$form->get_fd_message(
'form_new_subtitle')
3736 : trim((
string)($data[
'sku'] ??
'') .
' - ' . (
string)($data[
'title'] ??
''))
3738 $form->add_rep(
'bar_class',
'dbx-module-bar');
3739 $form->add_rep(
'bar_title_class',
'dbx-module-bar-titleblock');
3740 $form->add_rep(
'bar_title_pre',
'');
3741 $form->add_rep(
'bar_title_heading_attrs',
'');
3742 $form->add_rep(
'bar_middle',
'');
3743 $form->add_rep(
'bar_actions_class',
'dbx-module-bar-actions');
3744 $form->add_rep(
'bar_actions', $this->productFormActions($id,
$form));
3745 $form->add_rep(
'bar_extra',
'');
3746 $form->add_rep(
'shop_admin_style', $this->shopAdminStyle());
3747 $form->_msg_info =
$form->get_fd_message(
'form_info');
3748 if ($exportNotice !==
'') {
3750 $form->_msg_success = $exportNotice;
3752 $form->_msg_error = $exportNotice;
3758 tpl:
'select-single-label',
3759 label:
$form->get_fd_message(
'field_product_group'),
3760 options: $this->productGroupOptions(0,
false),
3764 if (
$form->submit()) {
3765 if (!
$form->errors()) {
3766 $ok =
$form->save_post(
'dbxShop|shopProduct', $isNew ?
'new' : $id, $this->productFormDefaults($id));
3768 $savedId = (int)
$form->_rid;
3769 $groupId = (int)(
$_POST[
'product_group_id'] ?? 0);
3770 if ($savedId > 0 && $groupId > 0) {
3771 $this->repo()->setProductGroupForProducts(array($savedId), $groupId);
3773 if ($savedId > 0 && isset(
$_POST[
'product_channel_editor'])) {
3774 $this->repo()->saveProductChannelOverrides($savedId, (array)(
$_POST[
'product_channels'] ?? array()));
3777 'product_save_success'
3782 $data = $this->repo()->productById($savedId) ?:
$form->_data;
3783 $form->_action =
'?dbx_modul=dbxShop_admin&dbx_run1=product_edit&id=' . $savedId;
3787 'product_save_error'
3800 $this->productImagesPanel(
3801 is_array($data) ? $data : array(),
3809 $this->productChannelsPanel(
3810 is_array($data) ? $data : array(),
3820 $content .= $this->shopMediaFormTemplates($this->shopMediaConfig());
3825 private function productsHelp(): string {
3826 $helpId = $this->ensureShopProductsHelpPage();
3828 return $this->frame(
'<div class="m-3">' . $this->productsHelpHtml() .
'</div>',
'Produkte Hilfe', $this->productBarActions());
3830 return $this->frame(
'<div class="alert alert-warning m-3">Hilfe konnte nicht angelegt werden.</div>',
'Produkte Hilfe', $this->productBarActions());
3833 private function orderStatusOptions(
$texts =
null): array {
3835 'new' =>
$texts ?
$texts->get_fd_message(
'order_status_new',
'Neu') :
'Neu',
3836 'payment_pending' =>
$texts ?
$texts->get_fd_message(
'order_status_payment_pending',
'Zahlung offen') :
'Zahlung offen',
3837 'paid' =>
$texts ?
$texts->get_fd_message(
'order_status_paid',
'Bezahlt') :
'Bezahlt',
3838 'processing' =>
$texts ?
$texts->get_fd_message(
'order_status_processing',
'In Bearbeitung') :
'In Bearbeitung',
3839 'shipped' =>
$texts ?
$texts->get_fd_message(
'order_status_shipped',
'Versendet') :
'Versendet',
3840 'done' =>
$texts ?
$texts->get_fd_message(
'order_status_done',
'Abgeschlossen') :
'Abgeschlossen',
3841 'cancelled' =>
$texts ?
$texts->get_fd_message(
'order_status_cancelled',
'Storniert') :
'Storniert',
3845 private function paymentStatusOptions(
$texts =
null): array {
3847 'open' =>
$texts ?
$texts->get_fd_message(
'payment_status_open',
'Offen') :
'Offen',
3848 'created' =>
$texts ?
$texts->get_fd_message(
'payment_status_created',
'Erstellt') :
'Erstellt',
3849 'pending' =>
$texts ?
$texts->get_fd_message(
'payment_status_pending',
'In Prüfung') :
'In Prüfung',
3850 'completed' =>
$texts ?
$texts->get_fd_message(
'payment_status_completed',
'Abgeschlossen') :
'Abgeschlossen',
3851 'paid' =>
$texts ?
$texts->get_fd_message(
'payment_status_paid',
'Bezahlt') :
'Bezahlt',
3852 'failed' =>
$texts ?
$texts->get_fd_message(
'payment_status_failed',
'Fehlgeschlagen') :
'Fehlgeschlagen',
3853 'cancelled' =>
$texts ?
$texts->get_fd_message(
'payment_status_cancelled',
'Abgebrochen') :
'Abgebrochen',
3854 'refunded' =>
$texts ?
$texts->get_fd_message(
'payment_status_refunded',
'Erstattet') :
'Erstattet',
3858 private function shippingStatusOptions(
$texts =
null): array {
3860 'open' =>
$texts ?
$texts->get_fd_message(
'shipping_status_open',
'Offen') :
'Offen',
3861 'ready' =>
$texts ?
$texts->get_fd_message(
'shipping_status_ready',
'Bereit') :
'Bereit',
3862 'shipped' =>
$texts ?
$texts->get_fd_message(
'shipping_status_shipped',
'Versendet') :
'Versendet',
3863 'delivered' =>
$texts ?
$texts->get_fd_message(
'shipping_status_delivered',
'Zugestellt') :
'Zugestellt',
3864 'returned' =>
$texts ?
$texts->get_fd_message(
'shipping_status_returned',
'Retoure') :
'Retoure',
3868 private function orderStatusBadge(
string $status,
$texts =
null): string {
3869 $labels = $this->orderStatusOptions(
$texts);
3871 'new' =>
'text-bg-secondary',
3872 'payment_pending' =>
'text-bg-warning',
3873 'paid' =>
'text-bg-success',
3874 'processing' =>
'text-bg-info',
3875 'shipped' =>
'text-bg-primary',
3876 'done' =>
'text-bg-success',
3877 'cancelled' =>
'text-bg-danger',
3879 return '<span class="badge ' . $this->h($classes[$status] ??
'text-bg-secondary') .
'">' . $this->h($labels[$status] ?? $status) .
'</span>';
3882 private function paymentStatusBadge(
string $status,
$texts =
null): string {
3883 $labels = $this->paymentStatusOptions(
$texts);
3885 'open' =>
'text-bg-secondary',
3886 'created' =>
'text-bg-info',
3887 'pending' =>
'text-bg-warning',
3888 'completed' =>
'text-bg-success',
3889 'paid' =>
'text-bg-success',
3890 'failed' =>
'text-bg-danger',
3891 'cancelled' =>
'text-bg-danger',
3892 'refunded' =>
'text-bg-dark',
3894 return '<span class="badge ' . $this->h($classes[$status] ??
'text-bg-secondary') .
'">' . $this->h($labels[$status] ?? $status) .
'</span>';
3897 private function shippingStatusBadge(
string $status,
$texts =
null): string {
3898 $labels = $this->shippingStatusOptions(
$texts);
3900 'open' =>
'text-bg-secondary',
3901 'ready' =>
'text-bg-info',
3902 'shipped' =>
'text-bg-primary',
3903 'delivered' =>
'text-bg-success',
3904 'returned' =>
'text-bg-warning',
3906 return '<span class="badge ' . $this->h($classes[$status] ??
'text-bg-secondary') .
'">' . $this->h($labels[$status] ?? $status) .
'</span>';
3909 private function channelLabel(
string $channel): string {
3913 'amazon' =>
'Amazon',
3915 'kleinanzeigen' =>
'Kleinanzeigen',
3916 'mobile' =>
'mobile.de',
3918 return $labels[$channel] ?? $channel;
3921 private function paymentProviderLabel(
string $provider,
$texts =
null): string {
3923 'bank_transfer' =>
$texts ?
$texts->get_fd_message(
'payment_bank_transfer',
'Vorkasse / Überweisung') :
'Vorkasse / Überweisung',
3924 'invoice' =>
$texts ?
$texts->get_fd_message(
'payment_invoice',
'Rechnung') :
'Rechnung',
3925 'paypal' =>
'PayPal',
3926 'amazon_pay' =>
'Amazon Pay',
3928 return $labels[$provider] ?? $this->channelLabel($provider);
3931 private function channelBadge(
string $channel): string {
3932 $class = in_array($channel, array(
'shop',
'web',
''), true) ?
'text-bg-secondary' :
'text-bg-info';
3933 $text = $channel ===
'' ?
'Shop' : $this->channelLabel($channel);
3934 return '<span class="badge ' .
$class .
'">' . $this->h($text) .
'</span>';
3937 private function orderActions(
$texts =
null): string {
3938 $helpId = $this->ensureShopOrdersHelpPage();
3940 ?
$texts->get_fd_message(
'help_orders',
'Hilfe: Bestellungen')
3941 :
'Hilfe: Bestellungen';
3943 ?
$texts->get_fd_message(
'help_label',
'Hilfe')
3945 $helpButton = $helpId > 0
3946 ? $this->openWinButton(
'?dbx_modul=dbxContent&dbx_run1=content&cid=' . $helpId, $helpTitle,
'<i class="bi bi-question-circle"></i><span class="visually-hidden"> ' . $this->h($helpLabel) .
'</span>',
'btn btn-outline-secondary btn-sm me-1',
'72%',
'82%')
3951 private function handleOrderAction(
$report): void {
3952 $deleteId = (int)
dbx()->get_modul_var(
'delete_order', 0,
'int');
3953 if ($deleteId <= 0) {
3956 if (!$this->checkActionToken(
'delete_order')) {
3960 if ($this->repo()->deleteOrder($deleteId)) {
3961 $report->_msg_success =
$report->get_fd_message(
'delete_success');
3968 $id = (int)($record[
'id'] ?? 0);
3969 $orderNo = (string)($record[
'order_no'] ??
'');
3970 $created = (string)($record[
'create_date'] ??
'');
3971 $channel = (string)($record[
'channel_key'] ??
'shop');
3972 $items = (array)($record[
'items'] ?? array());
3974 foreach (
$items as $item) {
3975 $itemLines .=
'<div>' . (int)($item[
'qty'] ?? 0) .
'x <strong>' . $this->h($item[
'title'] ??
'') .
'</strong> <code>' . $this->h($item[
'sku'] ??
'') .
'</code></div>';
3977 if ($itemLines ===
'') {
3978 $itemLines =
'<span class="text-muted">'
3979 . $this->h(
$report->get_fd_message(
'no_items'))
3982 $detailUrl =
'?dbx_modul=dbxShop_admin&dbx_run1=order_detail&id=' . $id;
3983 $deleteUrl = $this->actionUrl(
'?dbx_modul=dbxShop_admin&dbx_run1=orders&delete_order=' . $id);
3985 $externalId = trim((
string)($record[
'payment_reference'] ??
''));
3986 $sourceText = in_array($channel, array(
'',
'shop',
'web'),
true)
3987 ?
$report->get_fd_message(
'source_shop_order')
3988 :
$report->get_fd_message(
'source_channel_order');
3989 $externalText = $externalId !==
''
3990 ?
'<small>' . $this->h(
$report->format_fd_message(
'external_reference', array(
'reference' => $externalId))) .
'</small>'
3992 $record[
'order_view'] =
'<div class="dbx-shop-order-main"><strong>' . $this->h($orderNo) .
'</strong><small>' . $this->h(
$created) .
'</small><span class="dbx-shop-order-source">' . $this->channelBadge($channel) .
'<small>' . $this->h($sourceText) .
'</small></span>' . $externalText .
'</div>';
3993 $record[
'customer_view'] =
'<div class="dbx-shop-order-customer"><strong>' . $this->h($record[
'customer_name'] ??
'') .
'</strong><small>' . $this->h($record[
'customer_email'] ??
'') .
'</small></div>';
3994 $record[
'items_view'] =
'<div class="dbx-shop-order-items-small">' . $itemLines .
'</div>';
3995 $record[
'status_view'] =
'<div class="dbx-shop-order-status-stack">'
3996 .
'<span><small>' . $this->h(
$report->get_fd_message(
'label_order')) .
'</small>' . $this->orderStatusBadge((
string)($record[
'status'] ??
'new'),
$report) .
'</span>'
3997 .
'<span><small>' . $this->h(
$report->get_fd_message(
'label_shipping')) .
'</small>' . $this->shippingStatusBadge((
string)($record[
'shipping_status'] ??
'open'),
$report) .
'</span>'
3999 $record[
'payment_view'] =
'<div class="dbx-shop-order-payment">' . $this->paymentStatusBadge((
string)($record[
'payment_status'] ??
'open'),
$report) .
'<small>' . $this->h($this->paymentProviderLabel((
string)($record[
'payment_provider'] ??
''),
$report)) .
'</small><small>' . $this->h($record[
'payment_reference'] ??
'') .
'</small></div>';
4000 $record[
'total_view'] = $this->money($record[
'total_gross'] ?? 0);
4001 $record[
'actions_view'] =
'<span class="dbx-shop-order-actions">'
4002 . $this->openWinButton($detailUrl,
$report->format_fd_message(
'action_edit_title', array(
'number' => $orderNo)),
'<i class="bi bi-pencil-square"></i><span class="visually-hidden"> ' . $this->h(
$report->get_fd_message(
'action_edit')) .
'</span>',
'btn btn-outline-primary btn-sm',
'86%',
'88%')
4003 .
'<a class="btn btn-outline-danger btn-sm dbxConfirm" href="' . $this->h(
$deleteUrl) .
'" data-confirm-title="<i class=\'bi bi-trash\'></i> ' . $this->h(
$report->get_fd_message(
'delete_title')) .
'" data-confirm="' . $this->h(
$report->get_fd_message(
'delete_question')) .
'" data-confirm-hint="<small>' . $this->h(
$report->get_fd_message(
'delete_hint')) .
'</small>" data-confirm-buttons="yesno" title="' . $this->h(
$report->get_fd_message(
'delete_title')) .
'"><i class="bi bi-trash"></i><span class="visually-hidden"> ' . $this->h(
$report->get_fd_message(
'delete_label')) .
'</span></a>'
4008 private function orders(): string {
4009 $this->ensureSeed();
4010 $report =
dbx()->get_system_obj(
'dbxReport');
4011 $report->init(
'shop-orders-report');
4012 $report->_fd =
'dbxShop_admin|rpt-orders-selection';
4014 $report->_action =
'?dbx_modul=dbxShop_admin&dbx_run1=orders';
4017 $report->_create_sel_flds =
true;
4021 $report->add_rep(
'bar_title',
$report->get_fd_message(
'bar_title'));
4022 $report->add_rep(
'bar_icon',
'bi-receipt');
4023 $report->add_rep(
'bar_subtitle',
$report->get_fd_message(
'bar_subtitle'));
4024 $report->add_rep(
'bar_class',
'dbx-module-bar');
4025 $report->add_rep(
'bar_title_class',
'dbx-module-bar-titleblock');
4026 $report->add_rep(
'bar_actions_class',
'dbx-module-bar-actions');
4027 $report->add_rep(
'bar_title_pre',
'');
4028 $report->add_rep(
'bar_title_heading_attrs',
'');
4029 $report->add_rep(
'bar_middle',
'');
4030 $report->add_rep(
'bar_extra',
'');
4032 $report->add_rep(
'shop_admin_style', $this->shopAdminStyle());
4034 'order_view' =>
'dbx-shop-col-order',
4035 'customer_view' =>
'dbx-shop-col-customer',
4036 'items_view' =>
'dbx-shop-col-items',
4037 'status_view' =>
'dbx-shop-col-status',
4038 'payment_view' =>
'dbx-shop-col-payment',
4039 'total_view' =>
'dbx-shop-col-total',
4040 'actions_view' =>
'dbx-shop-col-actions',
4045 $report->set_callback_owner($this);
4046 $report->set_callback(
'next_record',
'order_report_next_record');
4047 $report->create_selection_fields(
'dbxShop_admin|rpt-orders-selection');
4048 $this->handleOrderAction(
$report);
4050 $query = trim((
string)
$report->get_fld_val(
'dbx_rwhere',
'',
'parameter|max=120'));
4051 $requestedRowsPerPage = (int)
$report->get_fld_val(
'dbx_rrows', 30,
'int');
4052 $rowsPerPage = $requestedRowsPerPage === 0 ? 0 : max(10, min(100, $requestedRowsPerPage));
4053 $position = $rowsPerPage === 0 ? 0 : max(0, (
int)
$report->get_fld_val(
'dbx_rpos', 0,
'int'));
4054 $sort = (string)
$report->get_fld_val(
'dbx_rsort',
'create_date',
'parameter');
4055 $direction = strtoupper((
string)
$report->get_fld_val(
'dbx_rdesc',
'DESC',
'parameter')) ===
'ASC' ?
'ASC' :
'DESC';
4058 'status' => trim((
string)
$report->get_fld_val(
'status',
'',
'parameter')),
4059 'payment_status' => trim((
string)
$report->get_fld_val(
'payment_status',
'',
'parameter')),
4060 'shipping_status' => trim((
string)
$report->get_fld_val(
'shipping_status',
'',
'parameter')),
4061 'channel_key' => trim((
string)
$report->get_fld_val(
'channel_key',
'',
'parameter')),
4064 $filteredCount = $this->repo()->orderCount($filters);
4065 if ($rowsPerPage > 0 && $position >= $filteredCount && $filteredCount > 0) {
4066 $position = max(0, (
int)(floor(($filteredCount - 1) / $rowsPerPage) * $rowsPerPage));
4068 $orders = $this->repo()->orders($filters, $rowsPerPage, $position, $sort, $direction);
4071 'order_view' =>
$report->get_fd_message(
'column_order'),
4072 'customer_view' =>
$report->get_fd_message(
'column_customer'),
4073 'items_view' =>
$report->get_fd_message(
'column_items'),
4074 'status_view' =>
$report->get_fd_message(
'column_status'),
4075 'payment_view' =>
$report->get_fd_message(
'column_payment'),
4076 'total_view' =>
$report->get_fd_message(
'column_total'),
4077 'actions_view' =>
$report->get_fd_message(
'column_action'),
4080 'order_view' =>
'html',
4081 'customer_view' =>
'html',
4082 'items_view' =>
'html',
4083 'status_view' =>
'html',
4084 'payment_view' =>
'html',
4085 'total_view' =>
'html',
4086 'actions_view' =>
'html',
4088 $report->_rrows = $rowsPerPage;
4090 $report->_count_all = $this->repo()->orderCount(array());
4091 $report->_rcount = $filteredCount;
4095 if ($filteredCount === 0) {
4096 $content .=
'<div class="alert alert-info mx-3">'
4097 . $this->h(
$report->get_fd_message(
'no_results'))
4104 if (!is_array($record)) {
4107 $record[
'request_view'] =
'<div><strong>' . $this->h($record[
'order_no'] ??
$report->get_fd_message(
'without_order_no')) .
'</strong><br><small>' . $this->h($record[
'create_date'] ??
'') .
'</small></div>';
4108 $record[
'customer_view'] =
'<div><strong>' . $this->h($record[
'customer_name'] ??
'') .
'</strong><br><small>' . $this->h($record[
'customer_email'] ??
'') .
'</small></div>';
4109 $record[
'message_view'] =
'<div class="small">' . nl2br($this->h($record[
'reason'] ??
'')) .
'</div>';
4110 $status = (string)($record[
'status'] ??
'');
4111 $badge = in_array($status, array(
'accepted',
'refunded',
'closed'),
true) ?
'text-bg-success' : (in_array($status, array(
'rejected'),
true) ?
'text-bg-danger' :
'text-bg-warning');
4112 $statusLabel =
$report->get_fd_message(
'status_' . $status, $status);
4113 $record[
'status_view'] =
'<span class="badge ' . $badge .
'">' . $this->h($statusLabel) .
'</span>';
4114 $id = (int)($record[
'id'] ?? 0);
4115 $base =
'?dbx_modul=dbxShop_admin&dbx_run1=returns&withdrawal_id=' . $id .
'&withdrawal_status=';
4116 $record[
'actions_view'] =
4117 '<div class="btn-group btn-group-sm" role="group">'
4118 .
'<a class="btn btn-outline-secondary" href="' . $this->h($this->actionUrl(
$base .
'processing')) .
'" title="' . $this->h(
$report->get_fd_message(
'action_processing')) .
'"><i class="bi bi-hourglass-split"></i></a>'
4119 .
'<a class="btn btn-outline-success dbxConfirm" href="' . $this->h($this->actionUrl(
$base .
'accepted')) .
'" data-confirm-title="' . $this->h(
$report->get_fd_message(
'action_accept_title')) .
'" data-confirm="' . $this->h(
$report->get_fd_message(
'action_accept_question')) .
'" data-confirm-buttons="yesno" title="' . $this->h(
$report->get_fd_message(
'action_accept')) .
'"><i class="bi bi-check2"></i></a>'
4120 .
'<a class="btn btn-outline-primary dbxConfirm" href="' . $this->h($this->actionUrl(
$base .
'refunded')) .
'" data-confirm-title="' . $this->h(
$report->get_fd_message(
'action_refund_title')) .
'" data-confirm="' . $this->h(
$report->get_fd_message(
'action_refund_question')) .
'" data-confirm-buttons="yesno" title="' . $this->h(
$report->get_fd_message(
'action_refunded')) .
'"><i class="bi bi-cash-coin"></i></a>'
4121 .
'<a class="btn btn-outline-danger" href="' . $this->h($this->actionUrl(
$base .
'rejected')) .
'" title="' . $this->h(
$report->get_fd_message(
'action_reject')) .
'"><i class="bi bi-x-lg"></i></a>'
4122 .
'<a class="btn btn-outline-secondary" href="' . $this->h($this->actionUrl(
$base .
'closed')) .
'" title="' . $this->h(
$report->get_fd_message(
'action_close')) .
'"><i class="bi bi-archive"></i></a>'
4127 private function returns(): string {
4128 $this->ensureSeed();
4129 $withdrawalId = (int)
dbx()->get_modul_var(
'withdrawal_id', 0,
'int');
4130 $withdrawalStatus = (string)
dbx()->get_modul_var(
'withdrawal_status',
'',
'parameter');
4131 if ($withdrawalId > 0 && $withdrawalStatus !==
'') {
4132 if ($this->checkActionToken(
'withdrawal_status')) {
4133 $this->repo()->updateWithdrawalAdmin($withdrawalId, $withdrawalStatus);
4136 $report =
dbx()->get_system_obj(
'dbxReport');
4137 $report->init(
'shop-orders-report');
4138 $report->_fd =
'dbxShop_admin|rpt-withdrawals-selection';
4140 $report->_action =
'?dbx_modul=dbxShop_admin&dbx_run1=returns';
4143 $report->_create_sel_flds =
true;
4147 if ($this->postedFormError !==
'') {
4149 $this->postedFormError =
'';
4151 $report->add_rep(
'bar_title',
$report->get_fd_message(
'bar_title'));
4152 $report->add_rep(
'bar_icon',
'bi-arrow-counterclockwise');
4153 $report->add_rep(
'bar_subtitle',
$report->get_fd_message(
'bar_subtitle'));
4154 $report->add_rep(
'bar_class',
'dbx-module-bar');
4155 $report->add_rep(
'bar_title_class',
'dbx-module-bar-titleblock');
4156 $report->add_rep(
'bar_actions_class',
'dbx-module-bar-actions');
4157 $report->add_rep(
'bar_title_pre',
'');
4158 $report->add_rep(
'bar_title_heading_attrs',
'');
4159 $report->add_rep(
'bar_middle',
'');
4160 $report->add_rep(
'bar_extra',
'');
4163 $withdrawalCid = (int)(
$pages[
'withdrawal'] ?? 0);
4164 $cmsButton = $withdrawalCid > 0
4165 ? $this->openWinButton(
'?dbx_modul=dbxContent_admin&dbx_run1=cms&cid=' . $withdrawalCid,
$report->get_fd_message(
'edit_legal_title'),
'<i class="bi bi-pencil-square"></i><span class="visually-hidden"> ' . $this->h(
$report->get_fd_message(
'edit_cms')) .
'</span>',
'btn btn-outline-primary btn-sm me-1',
'94%',
'92%')
4167 $report->add_rep(
'bar_actions',
4169 . $this->openWinButton(
'?dbx_modul=dbxShop&dbx_run1=withdrawal',
$report->get_fd_message(
'view_page_title'),
'<i class="bi bi-box-arrow-up-right"></i><span class="visually-hidden"> ' . $this->h(
$report->get_fd_message(
'shop_view')) .
'</span>',
'btn btn-outline-primary btn-sm me-1',
'82%',
'86%')
4171 $report->add_rep(
'shop_admin_style', $this->shopAdminStyle());
4172 $report->set_callback_owner($this);
4173 $report->set_callback(
'next_record',
'withdrawal_report_next_record');
4174 $report->create_selection_fields(
'dbxShop_admin|rpt-withdrawals-selection');
4176 $query = trim((
string)
$report->get_fld_val(
'dbx_rwhere',
'',
'parameter|max=120'));
4177 $rowsPerPage = (int)
$report->get_fld_val(
'dbx_rrows', 30,
'int');
4178 $rowsPerPage = $rowsPerPage === 0 ? 0 : max(10, min(100, $rowsPerPage));
4179 $position = $rowsPerPage === 0 ? 0 : max(0, (
int)
$report->get_fld_val(
'dbx_rpos', 0,
'int'));
4182 'status' => trim((
string)
$report->get_fld_val(
'status',
'',
'parameter')),
4184 $filteredCount = $this->repo()->withdrawalCount($filters);
4185 $rows = $this->repo()->withdrawals($filters, $rowsPerPage, $position);
4188 'request_view' =>
$report->get_fd_message(
'column_withdrawal'),
4189 'customer_view' =>
$report->get_fd_message(
'column_customer'),
4190 'message_view' =>
$report->get_fd_message(
'column_message'),
4191 'status_view' =>
$report->get_fd_message(
'column_status'),
4192 'actions_view' =>
$report->get_fd_message(
'column_action'),
4195 'request_view' =>
'html',
4196 'customer_view' =>
'html',
4197 'message_view' =>
'html',
4198 'status_view' =>
'html',
4199 'actions_view' =>
'html',
4201 $report->_rrows = $rowsPerPage;
4203 $report->_count_all = $this->repo()->withdrawalCount(array());
4204 $report->_rcount = $filteredCount;
4208 if ($filteredCount === 0) {
4209 $content .=
'<div class="alert alert-info mx-3">'
4210 . $this->h(
$report->get_fd_message(
'no_results'))
4216 private function orderMetaHtml(array
$order,
$texts): string {
4217 $channel = (string)(
$order[
'channel_key'] ??
'shop');
4218 $source = $this->channelLabel($channel) .
' '
4219 . (in_array($channel, array(
'shop',
'web',
''),
true)
4220 ?
$texts->get_fd_message(
'source_order_suffix')
4221 :
$texts->get_fd_message(
'source_channel_order'));
4222 $paymentStatuses = $this->paymentStatusOptions(
$texts);
4223 $shippingStatuses = $this->shippingStatusOptions(
$texts);
4225 $texts->get_fd_message(
'meta_order_no') =>
$order[
'order_no'] ??
'',
4226 $texts->get_fd_message(
'meta_created') =>
$order[
'create_date'] ??
'',
4227 $texts->get_fd_message(
'meta_customer') => trim((
string)(
$order[
'customer_name'] ??
'') .
' <' . (
string)(
$order[
'customer_email'] ??
'') .
'>'),
4228 $texts->get_fd_message(
'meta_phone') =>
$order[
'customer_phone'] ??
'',
4229 $texts->get_fd_message(
'meta_shipping_address') =>
$order[
'shipping_address'] ??
'',
4231 $texts->get_fd_message(
'meta_external_order') =>
$order[
'payment_reference'] ??
'',
4232 $texts->get_fd_message(
'meta_payment_method') => $this->paymentProviderLabel((
string)(
$order[
'payment_provider'] ??
''),
$texts),
4233 $texts->get_fd_message(
'meta_payment_status') => $paymentStatuses[(
string)(
$order[
'payment_status'] ??
'')] ?? (
$order[
'payment_status'] ??
''),
4234 $texts->get_fd_message(
'meta_invoice_no') =>
$order[
'invoice_no'] ??
'',
4235 $texts->get_fd_message(
'meta_invoice_date') =>
$order[
'invoice_date'] ??
'',
4236 $texts->get_fd_message(
'meta_invoice_pdf') => trim((
string)(
$order[
'invoice_pdf_path'] ??
'')) !==
'' ? (
string)
$order[
'invoice_pdf_path'] :
$texts->get_fd_message(
'not_generated'),
4237 $texts->get_fd_message(
'meta_stock_reserved') => !empty(
$order[
'stock_reserved']) ?
$texts->get_fd_message(
'yes') :
$texts->get_fd_message(
'no'),
4238 $texts->get_fd_message(
'meta_stock_released') => !empty(
$order[
'stock_released']) ?
$texts->get_fd_message(
'yes') .
', ' . (
string)(
$order[
'stock_released_date'] ??
'') :
$texts->get_fd_message(
'no'),
4239 $texts->get_fd_message(
'meta_shipping_status') => $shippingStatuses[(
string)(
$order[
'shipping_status'] ??
'')] ?? (
$order[
'shipping_status'] ??
''),
4240 $texts->get_fd_message(
'meta_shipping_provider') =>
$order[
'shipping_provider'] ??
'',
4241 $texts->get_fd_message(
'meta_tracking_no') =>
$order[
'tracking_no'] ??
'',
4242 $texts->get_fd_message(
'meta_total') => $this->money(
$order[
'total_gross'] ?? 0),
4244 $html =
'<dl class="dbx-shop-order-meta">';
4246 $html .=
'<dt>' . $this->h($label) .
'</dt><dd>' . $this->h(
$value) .
'</dd>';
4252 private function orderHistoryHtml(array
$order,
$texts): string {
4254 if (
$rows === array()) {
4255 return '<div class="text-muted small">' . $this->h(
$texts->get_fd_message(
'history_empty')) .
'</div>';
4257 $html =
'<div class="dbx-shop-order-history">';
4258 foreach (
$rows as $row) {
4259 $event = (string)($row[
'event_type'] ??
'');
4260 $old = (string)($row[
'old_value'] ??
'');
4261 $new = (string)($row[
'new_value'] ??
'');
4262 $msg = (string)($row[
'message'] ??
'');
4263 $html .=
'<div class="dbx-shop-order-history-item">'
4264 .
'<strong>' . $this->h($event) .
'</strong>'
4265 .
'<small>' . $this->h($row[
'create_date'] ??
'') .
'</small>'
4266 . ($old !==
'' || $new !==
'' ?
'<code>' . $this->h($old) .
' -> ' . $this->h($new) .
'</code>' :
'')
4267 . ($msg !==
'' ?
'<span>' . $this->h($msg) .
'</span>' :
'')
4274 private function orderWithdrawalsHtml(array
$order,
$texts): string {
4275 $rows = (array)(
$order[
'withdrawals'] ?? array());
4276 if (
$rows === array()) {
4277 return '<div class="text-muted small">' . $this->h(
$texts->get_fd_message(
'withdrawals_empty')) .
'</div>';
4279 $html =
'<div class="dbx-shop-order-withdrawals">';
4280 foreach (
$rows as $row) {
4281 $html .=
'<div class="alert alert-warning py-2 mb-2">'
4282 .
'<strong>' . $this->h($row[
'customer_name'] ??
'') .
'</strong> '
4283 .
'<span class="badge text-bg-warning">' . $this->h($row[
'status'] ??
'') .
'</span>'
4284 .
'<br><small>' . $this->h($row[
'create_date'] ??
'') .
' · ' . $this->h($row[
'customer_email'] ??
'') .
'</small>'
4285 .
'<div class="mt-1">' . nl2br($this->h($row[
'reason'] ??
'')) .
'</div>'
4292 private function orderItemsHtml(array
$order,
$texts): string {
4294 if (
$items === array()) {
4295 return '<div class="text-muted small">' . $this->h(
$texts->get_fd_message(
'items_empty')) .
'</div>';
4297 $html =
'<div class="table-responsive"><table class="table table-sm table-bordered dbx-shop-order-items-table"><thead><tr>'
4298 .
'<th>' . $this->h(
$texts->get_fd_message(
'items_product')) .
'</th><th class="text-end">' . $this->h(
$texts->get_fd_message(
'items_quantity')) .
'</th><th class="text-end">' . $this->h(
$texts->get_fd_message(
'items_unit_price')) .
'</th><th class="text-end">' . $this->h(
$texts->get_fd_message(
'items_tax')) .
'</th><th class="text-end">' . $this->h(
$texts->get_fd_message(
'items_shipping')) .
'</th><th class="text-end">' . $this->h(
$texts->get_fd_message(
'items_total')) .
'</th>'
4299 .
'</tr></thead><tbody>';
4300 foreach (
$items as $item) {
4302 .
'<td><strong>' . $this->h($item[
'title'] ??
'') .
'</strong><br><code>' . $this->h($item[
'sku'] ??
'') .
'</code></td>'
4303 .
'<td class="text-end">' . (int)($item[
'qty'] ?? 0) .
'</td>'
4304 .
'<td class="text-end">' . $this->money($item[
'price_gross'] ?? 0) .
'</td>'
4305 .
'<td class="text-end">' . $this->h(number_format((
float)($item[
'tax_rate'] ?? 0), 2,
',',
'.')) .
' %</td>'
4306 .
'<td class="text-end">' . $this->money($item[
'shipping_gross'] ?? 0) .
'</td>'
4307 .
'<td class="text-end">' . $this->money($item[
'total_gross'] ?? 0) .
'</td>'
4310 $html .=
'</tbody></table></div>';
4314 private function orderPayloadHtml(array
$order,
$texts): string {
4316 $texts->get_fd_message(
'payload_payment') => trim((string)(
$order[
'payment_payload'] ??
'')),
4317 $texts->get_fd_message(
'payload_legal') => trim((string)(
$order[
'legal_snapshot'] ??
'')),
4318 $texts->get_fd_message(
'payload_withdrawal') => trim((string)(
$order[
'withdrawal_snapshot'] ??
'')),
4321 foreach ($blocks as $title => $payload) {
4322 if ($payload ===
'') {
4325 $decoded = json_decode($payload,
true);
4326 if (is_array($decoded)) {
4327 $payload = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
4329 $html .=
'<details class="mb-2"><summary>' . $this->h($title) .
'</summary><pre>' . $this->h($payload) .
'</pre></details>';
4332 return '<div class="text-muted small">' . $this->h(
$texts->get_fd_message(
'payload_empty')) .
'</div>';
4337 private function statusMailChangesHtml(array $before, array
$order): string {
4339 'Bestellstatus' => array($this->orderStatusOptions(),
'status'),
4340 'Zahlungsstatus' => array($this->paymentStatusOptions(),
'payment_status'),
4341 'Versandstatus' => array($this->shippingStatusOptions(),
'shipping_status'),
4346 $old = (string)($before[
$field] ??
'');
4348 if ($old === $new) {
4351 $html .=
'<tr><th align="left">' . $this->h($label) .
'</th><td>' . $this->h(
$options[$old] ?? $old) .
'</td><td>' . $this->h(
$options[$new] ?? $new) .
'</td></tr>';
4355 .
'<dt>Bestellstatus</dt><dd>' . $this->h($this->orderStatusOptions()[(
string)(
$order[
'status'] ??
'')] ?? (
string)(
$order[
'status'] ??
'')) .
'</dd>'
4356 .
'<dt>Zahlungsstatus</dt><dd>' . $this->h($this->paymentStatusOptions()[(
string)(
$order[
'payment_status'] ??
'')] ?? (
string)(
$order[
'payment_status'] ??
'')) .
'</dd>'
4357 .
'<dt>Versandstatus</dt><dd>' . $this->h($this->shippingStatusOptions()[(
string)(
$order[
'shipping_status'] ??
'')] ?? (
string)(
$order[
'shipping_status'] ??
'')) .
'</dd>'
4360 return '<table border="0" cellpadding="6" cellspacing="0">'
4361 .
'<thead><tr><th align="left">Feld</th><th align="left">Vorher</th><th align="left">Jetzt</th></tr></thead>'
4362 .
'<tbody>' .
$html .
'</tbody></table>';
4365 private function sendOrderStatusMail(array $before, array
$order): array {
4366 $cfg = $this->shopConfig();
4367 $from = trim((
string)(
$cfg[
'mail_from'] ??
''));
4368 $fromName = trim((
string)(
$cfg[
'mail_from_name'] ??
'dbxShop'));
4370 $to = trim((
string)(
$order[
'customer_email'] ??
''));
4371 if (filter_var($from, FILTER_VALIDATE_EMAIL) ===
false) {
4372 return array(
false,
'Kundenmail wurde nicht gesendet: Der Mail-Absender in den Shop-Einstellungen ist ungültig.');
4374 if ($to ===
'' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
4375 return array(
false,
'Kundenmail wurde nicht gesendet: Die Bestellung hat keine gueltige Kunden-E-Mail.');
4378 $orderNo = (string)(
$order[
'order_no'] ??
'');
4379 $subject =
'Aktualisierung Ihrer Bestellung ' . $orderNo;
4380 $trackingNo = trim((
string)(
$order[
'tracking_no'] ??
''));
4381 $trackingUrl = trim((
string)(
$order[
'tracking_url'] ??
''));
4382 $invoiceNo = trim((
string)(
$order[
'invoice_no'] ??
''));
4384 if ($trackingNo !==
'') {
4385 $extra .=
'<p><strong>Trackingnummer:</strong> ' . $this->h($trackingNo) .
'</p>';
4387 if ($trackingUrl !==
'') {
4388 $extra .=
'<p><a href="' . $this->h($trackingUrl) .
'">Sendung verfolgen</a></p>';
4390 if ($invoiceNo !==
'') {
4391 $extra .=
'<p><strong>Rechnung:</strong> ' . $this->h($invoiceNo) .
'</p>';
4393 $html =
'<h2>Ihre Bestellung wurde aktualisiert</h2>'
4394 .
'<p>Bestellnummer: <strong>' . $this->h($orderNo) .
'</strong></p>'
4395 . $this->statusMailChangesHtml($before,
$order)
4397 .
'<p>Viele Gruesse<br>Ihr Shop-Team</p>';
4401 $sent =
dbx()->send_mail(
4402 array(
'email' => $from,
'name' => $fromName),
4411 $mail =
dbx()->get_system_obj(
'dbxMail');
4412 $reason = is_object(
$mail) ? trim((
string)
$mail->get_error()) :
'';
4415 'Kundenmail konnte nicht gesendet werden'
4416 . ($reason !==
'' ?
': ' . $reason :
'.')
4419 $this->repo()->addOrderHistory((
int)(
$order[
'id'] ?? 0),
'customer_mail',
'', $to,
'Statusbenachrichtigung wurde an den Kunden gesendet.');
4420 return array(
true,
'Kundenmail wurde gesendet.');
4421 }
catch (\Throwable $e) {
4422 dbx()->sys_msg(
'error',
'dbxShop_admin', (
string)(
$order[
'id'] ??
''),
'order status mail failed', $e->getMessage());
4423 return array(
false,
'Kundenmail konnte nicht gesendet werden: ' . $e->getMessage());
4427 private function notifyCustomerHint(array
$order,
$texts): string {
4428 $email = trim((string)(
$order[
'customer_email'] ??
''));
4429 return $email ===
''
4430 ?
$texts->get_fd_message(
'notify_no_email')
4431 :
$texts->format_fd_message(
4433 array(
'email' => $email)
4437 private function orderQuickActionsHtml(array
$order,
$texts): string {
4438 $id = (int)(
$order[
'id'] ?? 0);
4442 $base =
'?dbx_modul=dbxShop_admin&dbx_run1=order_detail&dbx_run2=quick_action&id=' . $id .
'&order_action=';
4444 'mark_paid' => array(
'bi-cash-coin',
$texts->get_fd_message(
'quick_paid'),
$texts->get_fd_message(
'quick_paid_confirm')),
4445 'processing' => array(
'bi-tools',
$texts->get_fd_message(
'quick_processing'),
$texts->get_fd_message(
'quick_processing_confirm')),
4446 'ready' => array(
'bi-box-seam',
$texts->get_fd_message(
'quick_ready'),
$texts->get_fd_message(
'quick_ready_confirm')),
4447 'shipped' => array(
'bi-truck',
$texts->get_fd_message(
'quick_shipped'),
$texts->get_fd_message(
'quick_shipped_confirm')),
4448 'delivered' => array(
'bi-check2-circle',
$texts->get_fd_message(
'quick_delivered'),
$texts->get_fd_message(
'quick_delivered_confirm')),
4449 'cancel' => array(
'bi-x-circle',
$texts->get_fd_message(
'quick_cancel'),
$texts->get_fd_message(
'quick_cancel_confirm')),
4450 'refund' => array(
'bi-arrow-counterclockwise',
$texts->get_fd_message(
'quick_refund'),
$texts->get_fd_message(
'quick_refund_confirm')),
4452 $html =
'<div class="dbx-shop-order-quick-actions" data-dbx="lib=confirm|class=dbxConfirm|bind=link">'
4453 .
'<strong>' . $this->h(
$texts->get_fd_message(
'quick_actions')) .
'</strong><div class="dbx-shop-order-quick-action-buttons">';
4456 $btnClass = in_array($action, array(
'cancel',
'refund'),
true) ?
'btn-outline-danger' :
'btn-outline-primary';
4457 $html .=
'<a class="btn btn-sm ' . $btnClass .
' dbxConfirm" href="' . $this->h($this->actionUrl(
$base . rawurlencode($action))) .
'"'
4458 .
' data-confirm-title="<i class=\'bi ' . $this->h($icon) .
'\'></i>
' . $this->h($label) . '"'
4459 . ' data-confirm="' . $this->h($confirm) . '"'
4460 . ' data-confirm-buttons="yesno
"'
4461 . ' title="' . $this->h($label) . '">'
4462 . '<i class="bi
' . $this->h($icon) . '"></i> ' . $this->h($label) . '</a>';
4464 $html .= '</div></div>';
4468 private function orderDetailActions(int $id, $texts): string {
4469 $helpId = $this->ensureShopOrdersHelpPage();
4470 $helpButton = $helpId > 0
4471 ? $this->openWinButton('?dbx_modul=dbxContent&dbx_run1=content&cid=' . $helpId, $texts->get_fd_message('help_orders'), '<i class="bi bi-question-circle
"></i><span class="visually-hidden
"> ' . $this->h($texts->get_fd_message('help_label')) . '</span>', 'btn btn-outline-secondary btn-sm ms-1', '72%', '82%')
4473 $deleteUrl = $this->actionUrl('?dbx_modul=dbxShop_admin&dbx_run1=orders&delete_order=' . $id);
4474 $mailUrl = $this->actionUrl('?dbx_modul=dbxShop_admin&dbx_run1=order_detail&dbx_run2=send_status_mail&id=' . $id);
4475 $invoicePdfUrl = $this->actionUrl('?dbx_modul=dbxShop_admin&dbx_run1=order_invoice_pdf&id=' . $id);
4476 return '<button class="btn btn-primary btn-sm
" type="submit
" title="' . $this->h($texts->get_fd_message('save_order_title
')) . '"><i class="bi bi-save
"></i><span class="visually-hidden
"> ' . $this->h($texts->get_fd_message('save_label')) . '</span></button>'
4477 . $this->openWinButton('?dbx_modul=dbxShop_admin&dbx_run1=order_invoice&id=' . $id, $texts->get_fd_message('invoice_view'), '<i class="bi bi-file-earmark-text
"></i><span class="visually-hidden
"> ' . $this->h($texts->get_fd_message('invoice_label')) . '</span>', 'btn btn-outline-primary btn-sm ms-1', '82%', '86%')
4478 . '<a class="btn btn-outline-danger btn-sm ms-1
" href="' . $this->h($invoicePdfUrl) . '" target="_blank
" rel="noopener
" title="' . $this->h($texts->get_fd_message('invoice_pdf_title
')) . '"><i class="bi bi-file-earmark-pdf
"></i><span class="visually-hidden
"> PDF</span></a>'
4479 . '<a class="btn btn-outline-primary btn-sm ms-1 dbxConfirm
" href="' . $this->h($mailUrl) . '" data-confirm-title="<i
class=\
'bi bi-envelope\'></i> ' . $this->h(
$texts->get_fd_message(
'customer_mail_title')) .
'" data-confirm="' . $this->h(
$texts->get_fd_message(
'customer_mail_question')) .
'" data-confirm-buttons="yesno" title="' . $this->h(
$texts->get_fd_message(
'customer_mail_title')) .
'"><i class="bi bi-envelope"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'customer_mail_label')) .
'</span></a>'
4480 .
'<a class="btn btn-outline-danger btn-sm ms-1 dbxConfirm" href="' . $this->h(
$deleteUrl) .
'" data-confirm-title="<i class=\'bi bi-trash\'></i> ' . $this->h(
$texts->get_fd_message(
'delete_title')) .
'" data-confirm="' . $this->h(
$texts->get_fd_message(
'delete_question')) .
'" data-confirm-buttons="yesno" title="' . $this->h(
$texts->get_fd_message(
'delete_title')) .
'"><i class="bi bi-trash"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'delete_label')) .
'</span></a>'
4482 . $this->openWinButton(
'?dbx_modul=dbxShop_admin&dbx_run1=orders',
$texts->get_fd_message(
'order_list_title'),
'<i class="bi bi-table"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'order_list_label')) .
'</span>',
'btn btn-outline-secondary btn-sm ms-1',
'92%',
'88%');
4485 private function orderDetail(): string {
4486 $id = (int)
dbx()->get_modul_var(
'id', 0,
'int');
4487 $order = $id > 0 ? $this->repo()->orderById($id) :
null;
4489 return $this->frame(
'<div class="alert alert-warning m-3">Bestellung nicht gefunden.</div>',
'Bestellung bearbeiten', $this->orderActions());
4493 $quickAction = (string)
dbx()->get_modul_var(
'order_action',
'',
'parameter');
4494 if ((
string)
dbx()->get_modul_var(
'dbx_run2',
'',
'parameter') ===
'quick_action' && $quickAction !==
'') {
4495 if (!$this->checkActionToken(
'order_quick_action')) {
4497 $quickMsg = $this->postedFormError;
4499 [$quickOk, $quickMsg] = $this->repo()->updateOrderQuickAction($id, $quickAction);
4502 $quickMessage = $quickMsg;
4505 $quickError = $quickMsg;
4510 $sendStatusMail = (string)
dbx()->get_modul_var(
'dbx_run2',
'',
'parameter') ===
'send_status_mail'
4511 || (string)(
$_GET[
'dbx_run2'] ??
'') ===
'send_status_mail';
4512 if ($sendStatusMail) {
4513 if (!$this->checkActionToken(
'send_status_mail')) {
4515 $mailMsg = $this->postedFormError;
4517 [$mailOk, $mailMsg] = $this->sendOrderStatusMail(
$order,
$order);
4520 $mailMessage = $mailMsg;
4523 $mailError = $mailMsg;
4527 $form =
dbx()->get_system_obj(
'dbxForm');
4528 $form->init(
'shop-order-form',
'shop-order-form');
4529 $form->_dd =
'dbxShop|shopOrder';
4530 $form->_fd =
'dbxShop_admin|rpt-orders-selection';
4531 $form->load_fd_messages();
4534 $form->_action =
'?dbx_modul=dbxShop_admin&dbx_run1=order_detail&id=' . $id;
4535 $form->set_activ_id($id);
4536 $form->add_rep(
'bar_title',
$form->get_fd_message(
'detail_title'));
4537 $form->add_rep(
'bar_icon',
'bi-receipt');
4538 $form->add_rep(
'bar_subtitle', (
string)(
$order[
'order_no'] ??
''));
4539 $form->add_rep(
'bar_class',
'dbx-module-bar');
4540 $form->add_rep(
'bar_title_class',
'dbx-module-bar-titleblock');
4541 $form->add_rep(
'bar_title_pre',
'');
4542 $form->add_rep(
'bar_title_heading_attrs',
'');
4543 $form->add_rep(
'bar_middle',
'');
4544 $form->add_rep(
'bar_actions_class',
'dbx-module-bar-actions');
4545 $form->add_rep(
'bar_actions', $this->orderDetailActions($id,
$form));
4546 $form->add_rep(
'bar_extra',
'');
4547 $form->add_rep(
'shop_admin_style', $this->shopAdminStyle());
4548 $form->_msg_info =
$form->get_fd_message(
'detail_info');
4549 $form->add_fld(
'status', tpl:
'select-single-label', label:
$form->get_fd_message(
'field_order_status'), options: $this->orderStatusOptions(
$form));
4550 $form->add_fld(
'payment_status', tpl:
'select-single-label', label:
$form->get_fd_message(
'field_payment_status'), options: $this->paymentStatusOptions(
$form));
4551 $form->add_fld(
'payment_reference', tpl:
'text-label', label:
$form->get_fd_message(
'field_payment_reference'), rules:
'*|max=180', placeholder:
'PAYID-xxxx / Channel-Order-ID');
4552 $form->add_fld(
'invoice_no', tpl:
'text-label', label:
$form->get_fd_message(
'field_invoice_no'), rules:
'*|max=60', placeholder:
'R2026-00001');
4553 $form->add_fld(
'invoice_date', tpl:
'text-label', label:
$form->get_fd_message(
'field_invoice_date'), rules:
'*|date', placeholder: date(
'Y-m-d'));
4554 $form->add_fld(
'shipping_status', tpl:
'select-single-label', label:
$form->get_fd_message(
'field_shipping_status'), options: $this->shippingStatusOptions(
$form));
4555 $form->add_fld(
'shipping_provider', tpl:
'text-label', label:
$form->get_fd_message(
'field_shipping_provider'), rules:
'*|max=120', placeholder:
'DHL, UPS');
4556 $form->add_fld(
'tracking_no', tpl:
'text-label', label:
$form->get_fd_message(
'field_tracking_no'), rules:
'*|max=180', placeholder:
'00340434123456789012');
4557 $form->add_fld(
'tracking_url', tpl:
'text-label', label:
$form->get_fd_message(
'field_tracking_url'), rules:
'*|max=255', placeholder:
'https://...');
4558 $form->add_fld(
'shipped_date', tpl:
'text-label', label:
$form->get_fd_message(
'field_shipped_date'), rules:
'*|datetime', placeholder: date(
'Y-m-d H:i:s'));
4559 $form->add_fld(
'note', tpl:
'textarea-label', label:
$form->get_fd_message(
'field_note'), rules:
'*|max=5000', data:
'rows=5', placeholder:
$form->get_fd_message(
'note_placeholder'));
4560 $form->add_rep(
'order_notice',
'');
4562 if (
$form->submit()) {
4563 if (!
$form->errors()) {
4564 if ($this->repo()->updateOrderAdmin($id,
$_POST)) {
4566 'order_save_success'
4582 $actionMessage = $quickMessage !==
'' ? $quickMessage : $mailMessage;
4583 $actionError = $quickError !==
'' ? $quickError : $mailError;
4584 if ($actionMessage !==
'' || $actionError !==
'') {
4585 $form->_form_submit = 1;
4586 $form->_msg_info =
'';
4587 if ($actionError !==
'') {
4588 $form->_msg_error = $actionError;
4589 $form->add_fld_error(
'general', $actionError);
4591 $form->_msg_success = $actionMessage;
4595 $form->add_rep(
'notify_customer_hint', $this->h($this->notifyCustomerHint(
$order,
$form)));
4596 $form->add_rep(
'order_quick_actions', $this->orderQuickActionsHtml(
$order,
$form));
4597 $form->add_obj(
'order_meta',
'obj-value', $this->orderMetaHtml(
$order,
$form));
4598 $form->add_obj(
'order_items',
'obj-value', $this->orderItemsHtml(
$order,
$form));
4599 $form->add_obj(
'order_payload',
'obj-value', $this->orderPayloadHtml(
$order,
$form));
4600 $form->add_obj(
'order_history',
'obj-value', $this->orderHistoryHtml(
$order,
$form));
4601 $form->add_obj(
'order_withdrawals',
'obj-value', $this->orderWithdrawalsHtml(
$order,
$form));
4602 return $form->run();
4605 private function orderInvoice(): string {
4606 $id = (int)
dbx()->get_modul_var(
'id', 0,
'int');
4607 $order = $id > 0 ? $this->repo()->orderById($id) :
null;
4609 return $this->frame(
'<div class="alert alert-warning m-3">Bestellung nicht gefunden.</div>',
'Rechnung');
4611 $invoiceNo = trim((
string)(
$order[
'invoice_no'] ??
''));
4612 if ($invoiceNo ===
'') {
4613 $invoiceNo =
'Entwurf';
4616 foreach ((array)(
$order[
'items'] ?? array()) as $item) {
4617 $rows .= $this->tpl()->get_tpl(
'dbxShop_admin|order-invoice-row', array(
4618 'title' => $this->h($item[
'title'] ??
''),
4619 'sku' => $this->h($item[
'sku'] ??
''),
4620 'qty' => (
string)(
int)($item[
'qty'] ?? 0),
4621 'price_gross' => $this->money($item[
'price_gross'] ?? 0),
4622 'tax_rate' => $this->h(number_format((
float)($item[
'tax_rate'] ?? 0), 2,
',',
'.')),
4623 'total_gross' => $this->money($item[
'total_gross'] ?? 0),
4626 $html = $this->tpl()->get_tpl(
'dbxShop_admin|order-invoice', array(
4627 'invoice_no' => $this->h($invoiceNo),
4628 'invoice_date' => $this->h(
$order[
'invoice_date'] ?? date(
'Y-m-d')),
4629 'order_no' => $this->h(
$order[
'order_no'] ??
''),
4630 'customer_name' => $this->h(
$order[
'customer_name'] ??
''),
4631 'customer_email' => $this->h(
$order[
'customer_email'] ??
''),
4632 'shipping_address' => nl2br($this->h(
$order[
'shipping_address'] ??
'')),
4634 'total_gross' => $this->money(
$order[
'total_gross'] ?? 0),
4636 return $this->frame(
$html,
'Rechnung ' . $invoiceNo,
'');
4639 private function orderInvoicePdf(): string {
4640 $id = (int)
dbx()->get_modul_var(
'id', 0,
'int');
4641 if (!$this->checkActionToken(
'order_invoice_pdf')) {
4642 return $this->frame(
'<div class="alert alert-danger m-3">' . $this->h($this->postedFormError) .
'</div>',
'Rechnung');
4644 $order = $id > 0 ? $this->repo()->ensureOrderInvoicePdf($id) :
null;
4646 return $this->frame(
'<div class="alert alert-warning m-3">Rechnungs-PDF konnte nicht erzeugt werden.</div>',
'Rechnung');
4648 $file = $this->repo()->invoicePdfAbsolutePath(
$order);
4650 return $this->frame(
'<div class="alert alert-warning m-3">Rechnungs-PDF ist nicht verfuegbar.</div>',
'Rechnung');
4652 if (!headers_sent()) {
4653 header(
'Content-Type: application/pdf');
4654 header(
'Content-Disposition: inline; filename="' . basename(
$file) .
'"');
4655 header(
'Content-Length: ' . filesize(
$file));
4661 private function shopAdminCardForm(
string $fid,
string $dd, array $data,
int $id,
string $action,
string $shopAction,
string $saveAction,
string $titleHtml,
string $subtitle =
'',
string $cardClass =
'') {
4662 $form =
dbx()->get_system_obj(
'dbxForm');
4663 $form->init($fid,
'shop-admin-card-form');
4666 'dbxShop|shopProductGroup' =>
'dbxShop|shop-product-group',
4667 'dbxShop|shopAttributeDefinition' =>
'dbxShop|shop-attribute-definition',
4668 'dbxShop|shopProductAttributeValue' =>
'dbxShop|shop-product-attribute-value',
4669 'dbxShop|shopShippingGroup' =>
'dbxShop|shop-shipping-group',
4670 'dbxShop|shopChannelGroup' =>
'dbxShop|shop-channel-group',
4672 if (
$form->_fd !==
'') {
4673 $form->load_fd_messages();
4675 $form->set_form_help_enabled(
false);
4676 $form->_data = $data + array(
'id' => $id);
4678 $form->_action = $action;
4679 $form->set_activ_id($id);
4680 $form->add_rep(
'form_class',
'dbx-shop-admin-card-dbXForm');
4681 $form->add_rep(
'form_attrs',
'data-target="dbxForm_{i}" data-dbx="lib=confirm|class=dbxConfirm|bind=button"');
4683 $form->add_rep(
'save_action', $this->h($saveAction));
4684 $form->add_rep(
'record_id', (
string)$id);
4685 $form->add_rep(
'extra_hidden',
'');
4686 $form->add_rep(
'card_title', $titleHtml);
4687 $form->add_rep(
'card_subtitle', $this->h($subtitle));
4688 $form->add_rep(
'card_badges',
'');
4689 $form->add_rep(
'card_class', $this->h($cardClass));
4690 $form->add_rep(
'form_body',
'');
4691 $form->add_rep(
'delete_button',
'');
4692 $form->_msg_info =
'';
4696 private function shopAdminCardDeleteButton(
string $action,
string $title,
string $message): string {
4697 return
'<button class="btn btn-outline-danger btn-sm dbxConfirm" name="shop_action" value="' . $this->h($action) .
'" title="' . $this->h($title) .
'" data-confirm-title="' . $this->h($title) .
'" data-confirm="' . $this->h($message) .
'" data-confirm-buttons="yesno"><i class="bi bi-trash"></i></button>';
4700 private function activeBadge(array $row,
$texts =
null): string {
4702 return ((
int)($row[
'active'] ?? 0) === 1)
4703 ?
'<span class="badge text-bg-success">' . $this->h(
$texts->get_fd_message(
'active')) .
'</span>'
4704 :
'<span class="badge text-bg-secondary">' . $this->h(
$texts->get_fd_message(
'inactive')) .
'</span>';
4707 private function groups(): string {
4708 $this->ensureSeed();
4709 $texts = $this->catalogTexts();
4710 if ($this->posted(
'delete_product_group')) {
4711 $this->repo()->deleteProductGroup((
int)(
$_POST[
'id'] ?? 0));
4712 } elseif ($this->posted(
'save_product_group')) {
4713 $this->repo()->updateProductGroup((
int)(
$_POST[
'id'] ?? 0),
$_POST);
4716 $cardHtml =
function (array $group,
bool $isNew =
false) use (
$texts):
string {
4717 $id = (int)($group[
'id'] ?? 0);
4719 $title =
'<span>' . $this->h(
$texts->get_fd_message(
'groups_new')) .
'</span>';
4720 $subtitle =
$texts->get_fd_message(
'groups_new_subtitle');
4722 $title =
'<code>' . $this->h($group[
'group_key'] ??
'') .
'</code><span>' . $this->h($group[
'title'] ??
'') .
'</span>';
4723 $subtitle = trim((
string)($group[
'description'] ??
''));
4726 $form = $this->shopAdminCardForm(
4727 'shop-product-group-' . ($isNew ?
'new' : $id),
4728 'dbxShop|shopProductGroup',
4731 '?dbx_modul=dbxShop_admin&dbx_run1=groups' . ($isNew ?
'&new=1' :
''),
4732 'save_product_group',
4733 'save_product_group',
4736 'dbx-shop-product-group-card'
4738 $form->add_rep(
'card_badges', $isNew ?
'<span class="badge text-bg-primary">' . $this->h(
$texts->get_fd_message(
'new')) .
'</span>' : $this->activeBadge($group,
$texts));
4739 $form->add_rep(
'extra_hidden',
'<input type="hidden" name="display_variant" value="' . $this->h($group[
'display_variant'] ??
'gallery_grid') .
'">');
4741 $form->add_rep(
'delete_button', $this->shopAdminCardDeleteButton(
'delete_product_group',
$texts->get_fd_message(
'groups_delete_title'),
$texts->get_fd_message(
'groups_delete_confirm')));
4744 $form->add_fld(
'group_key', tpl:
'text-label', placeholder:
'artikelgruppe-key', rules:
'*|parameter|max=80');
4746 $form->add_fld(
'parent_id', tpl:
'select-single-label', options: $this->productGroupOptions($id,
true,
$texts), rules:
'int');
4747 $form->add_fld(
'title', tpl:
'text-label', placeholder:
$texts->get_fd_message(
'groups_title_placeholder'), rules:
'*|max=160');
4748 $form->add_fld(
'description', tpl:
'textarea-label', placeholder:
$texts->get_fd_message(
'description_placeholder'), data:
'rows=2');
4749 $form->add_fld(
'tax_class', tpl:
'select-single-label', options:
'mwst1=mwst1&mwst2=mwst2&mwst3=mwst3');
4750 $form->add_fld(
'card_template', tpl:
'select-single-label', options: array(
4751 'product-card-default' =>
$texts->get_fd_message(
'card_default'),
4752 'product-card-compact' =>
$texts->get_fd_message(
'card_compact'),
4754 $form->add_fld(
'detail_template', tpl:
'select-single-label', options: array(
4755 'product-detail-default' =>
$texts->get_fd_message(
'detail_default'),
4756 'product-detail-technical' =>
$texts->get_fd_message(
'detail_technical'),
4758 $form->add_fld(
'gallery_template', tpl:
'select-single-label', options: array(
4759 'image-gallery' =>
$texts->get_fd_message(
'gallery_images'),
4760 'file-gallery' =>
$texts->get_fd_message(
'gallery_files'),
4762 $form->add_fld(
'gallery_visible_count', tpl:
'text-label', rules:
'int');
4763 $form->add_fld(
'gallery_image_size', tpl:
'select-single-label', options:
'original=Original&cover=Cover&contain=Contain');
4764 $form->add_fld(
'gallery_lightbox_width', tpl:
'text-label', placeholder:
'100vw');
4765 $form->add_fld(
'gallery_overflow', tpl:
'select-single-label', options: array(
4767 'slider' =>
'Slider',
4768 'scroll' =>
'Scroll',
4769 'laufband' =>
$texts->get_fd_message(
'gallery_marquee'),
4770 'tutorial' =>
'Tutorial',
4772 $form->add_fld(
'gallery_click', tpl:
'select-single-label', options: array(
4773 'lightbox' =>
'Lightbox',
4774 'none' =>
$texts->get_fd_message(
'gallery_no_click'),
4775 'newtab' =>
$texts->get_fd_message(
'gallery_new_tab'),
4776 'viewerjs' =>
'ViewerJS',
4777 'photoswipe' =>
'PhotoSwipe',
4779 $form->add_fld(
'attribute_notes', tpl:
'textarea-label', placeholder:
$texts->get_fd_message(
'attribute_notes_placeholder'), data:
'rows=2');
4780 $channelDefaults =
'';
4781 if ($this->channelsEnabled()) {
4782 $form->add_fld(
'ebay_category_id', tpl:
'text-label', placeholder:
'58058');
4783 $form->add_fld(
'amazon_product_type', tpl:
'text-label', placeholder:
'SOFTWARE / PRODUCT / SHIRT');
4784 $form->add_fld(
'kleinanzeigen_category_id', tpl:
'text-label', placeholder:
'category_12345');
4785 $form->add_fld(
'mobile_category_id', tpl:
'text-label', placeholder:
'car');
4786 $channelDefaults =
'<div class="wide dbx-shop-channel-defaults">'
4787 .
'<h6>' . $this->h(
$texts->get_fd_message(
'channel_defaults_title')) .
'</h6>'
4788 .
'<p>' . $this->h(
$texts->get_fd_message(
'channel_defaults_info')) .
'</p>'
4789 .
'<div class="dbx-shop-admin-card-grid dbx-shop-channel-default-grid">'
4790 .
'<div>{obj:ebay_category_id}</div>'
4791 .
'<div>{obj:amazon_product_type}</div>'
4792 .
'<div>{obj:kleinanzeigen_category_id}</div>'
4793 .
'<div>{obj:mobile_category_id}</div>'
4797 $form->add_fld(
'sorter', tpl:
'text-label', rules:
'int');
4798 $form->add_fld(
'active', tpl:
'checkbox-label', rules:
'int');
4799 $groupImagePanel = $this->productGroupImagePanel($group, $isNew,
$texts);
4800 $form->add_rep(
'form_body',
4801 '<div class="dbx-shop-admin-card-grid">'
4802 . ($isNew ?
'<div>{obj:group_key}</div>' :
'')
4803 .
'<div>{obj:parent_id}</div>'
4804 .
'<div>{obj:title}</div>'
4805 .
'<div>{obj:tax_class}</div>'
4806 .
'<div>{obj:sorter}</div>'
4807 .
'<div>{obj:active}</div>'
4808 .
'<div class="wide">' . $groupImagePanel .
'</div>'
4809 .
'<div class="wide">{obj:description}</div>'
4810 .
'<div>{obj:card_template}</div>'
4811 .
'<div>{obj:detail_template}</div>'
4812 .
'<div>{obj:gallery_template}</div>'
4813 .
'<div>{obj:gallery_visible_count}</div>'
4814 .
'<div>{obj:gallery_image_size}</div>'
4815 .
'<div>{obj:gallery_lightbox_width}</div>'
4816 .
'<div>{obj:gallery_overflow}</div>'
4817 .
'<div>{obj:gallery_click}</div>'
4818 .
'<div class="wide">{obj:attribute_notes}</div>'
4822 return $form->run();
4825 $groups = $this->repo()->groups();
4827 if ((
int)(
$_GET[
'new'] ?? 0) === 1) {
4830 $sorter = max($sorter, (
int)($group[
'sorter'] ?? 0) + 10);
4832 $cards .= $cardHtml(array(
4833 'tax_class' => (
string)($this->shopConfig()[
'default_tax_class'] ??
'mwst1'),
4834 'default_tax_rate' => 19,
4836 'display_variant' =>
'gallery_grid',
4837 'card_template' =>
'product-card-default',
4838 'detail_template' =>
'product-detail-default',
4839 'gallery_template' =>
'image-gallery',
4840 'gallery_visible_count' => 3,
4841 'gallery_image_size' =>
'original',
4842 'gallery_lightbox_width' =>
'100vw',
4843 'gallery_overflow' =>
'grid',
4844 'gallery_click' =>
'lightbox',
4846 'sorter' => $sorter,
4850 $cards .= $cardHtml($group);
4852 $helpButton = $this->helpButton($this->ensureShopProductGroupsHelpPage(),
$texts->get_fd_message(
'groups_help'));
4853 $barActions =
'<a class="btn btn-outline-primary btn-sm me-1" href="?dbx_modul=dbxShop_admin&dbx_run1=groups&new=1" title="' . $this->h(
$texts->get_fd_message(
'groups_new_title')) .
'"><i class="bi bi-plus-square"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'groups_new')) .
'</span></a>' . $helpButton;
4854 $content =
'<div class="alert alert-info mx-3 mt-3 mb-0">' . $this->h(
$texts->get_fd_message(
'groups_intro')) .
'</div>'
4855 .
'<div class="dbx-shop-admin-card-list">' . $cards .
'</div>';
4859 $content .= $this->shopMediaFormTemplates($this->shopMediaConfig());
4861 return $this->frame(
$content,
$texts->get_fd_message(
'groups_title'), $barActions);
4864 private function attributes(): string {
4865 $this->ensureSeed();
4866 $texts = $this->catalogTexts();
4867 if ($this->posted(
'save_attribute_definition')) {
4868 $this->repo()->saveAttributeDefinition(
$_POST);
4871 $groupOptions = array();
4872 foreach ($this->repo()->groups() as $group) {
4873 $groupOptions[(string)(
int)($group[
'id'] ?? 0)] = (
string)($group[
'title'] ??
'');
4876 $cardHelpButton = $this->helpButton($this->ensureShopProductAttributesHelpPage(),
$texts->get_fd_message(
'attributes_help'),
'btn btn-outline-secondary btn-sm me-1');
4877 $cardHtml =
function (array $attribute,
bool $isNew =
false) use ($groupOptions, $cardHelpButton,
$texts):
string {
4878 $id = (int)($attribute[
'id'] ?? 0);
4879 $type = (string)($attribute[
'input_type'] ??
'text');
4881 ?
'<span>' . $this->h(
$texts->get_fd_message(
'attributes_new')) .
'</span>'
4882 :
'<code>' . $this->h($attribute[
'attr_key'] ??
'') .
'</code><span>' . $this->h($attribute[
'title'] ??
'') .
'</span>';
4883 $subtitle = $isNew ?
$texts->get_fd_message(
'attributes_new_subtitle') : (string)($attribute[
'group_title'] ??
'');
4885 $form = $this->shopAdminCardForm(
4886 'shop-attribute-definition-' . ($isNew ?
'new' : $id),
4887 'dbxShop|shopAttributeDefinition',
4890 '?dbx_modul=dbxShop_admin&dbx_run1=attributes',
4891 'save_attribute_definition',
4892 'save_attribute_definition',
4895 'dbx-shop-attribute-card'
4897 $form->add_rep(
'card_badges', $cardHelpButton . ($isNew ?
'<span class="badge text-bg-primary">' . $this->h(
$texts->get_fd_message(
'new')) .
'</span>' : $this->activeBadge($attribute,
$texts)));
4898 $form->add_fld(
'group_id', tpl:
'select-single-label', options: $groupOptions, rules:
'int');
4899 $form->add_fld(
'attr_key', tpl:
'text-label', placeholder:
$texts->get_fd_message(
'attributes_key_placeholder'), rules:
'*|parameter|max=80');
4900 $form->add_fld(
'title', tpl:
'text-label', placeholder:
$texts->get_fd_message(
'attributes_title_placeholder'), rules:
'*|max=160');
4901 $form->add_fld(
'input_type', tpl:
'select-single-label', options: array(
4902 'text' =>
$texts->get_fd_message(
'attributes_type_text'),
4903 'select' =>
$texts->get_fd_message(
'attributes_type_select'),
4904 'number' =>
$texts->get_fd_message(
'attributes_type_number'),
4906 $form->add_fld(
'unit', tpl:
'text-label', placeholder:
'cm');
4907 $form->add_fld(
'options', tpl:
'textarea-label', placeholder:
'S|M|L|XL', data:
'rows=2');
4908 $form->add_fld(
'required', tpl:
'checkbox-label', rules:
'int');
4909 $form->add_fld(
'filterable', tpl:
'checkbox-label', rules:
'int');
4910 $form->add_fld(
'comparable', tpl:
'checkbox-label', rules:
'int');
4911 $form->add_fld(
'sorter', tpl:
'text-label', rules:
'int');
4912 $form->add_fld(
'active', tpl:
'checkbox-label', rules:
'int');
4913 $form->add_rep(
'form_body',
4914 '<div class="dbx-shop-admin-card-grid">'
4915 .
'<div>{obj:group_id}</div>'
4916 .
'<div>{obj:attr_key}</div>'
4917 .
'<div>{obj:title}</div>'
4918 .
'<div>{obj:input_type}</div>'
4919 .
'<div>{obj:unit}</div>'
4920 .
'<div>{obj:sorter}</div>'
4921 .
'<div class="wide">{obj:options}</div>'
4922 .
'<div class="wide dbx-shop-admin-check-grid">{obj:required}{obj:filterable}{obj:comparable}{obj:active}</div>'
4925 return $form->run();
4928 $cards = $cardHtml(array(
4929 'group_id' => (
int)array_key_first($groupOptions),
4930 'input_type' =>
'text',
4937 foreach ($this->repo()->allAttributeDefinitions() as $attribute) {
4938 $cards .= $cardHtml($attribute);
4941 $barActions = $this->helpButton($this->ensureShopProductAttributesHelpPage(),
$texts->get_fd_message(
'attributes_help'));
4942 return $this->frame(
'<div class="alert alert-info mx-3 mt-3 mb-0">' . $this->h(
$texts->get_fd_message(
'attributes_intro')) .
'</div><div class="dbx-shop-admin-card-list">' . $cards .
'</div>',
$texts->get_fd_message(
'attributes_title'), $barActions);
4945 private function productAttributes(): string {
4946 $this->ensureSeed();
4947 $texts = $this->catalogTexts();
4948 $productId = (int)
dbx()->get_modul_var(
'id',
'0',
'int');
4949 if ($this->posted(
'save_product_attributes')) {
4950 $productId = (int)(
$_POST[
'product_id'] ?? $productId);
4951 $this->repo()->saveProductAttributeValues($productId,
$_POST[
'attr_value'] ?? array());
4953 $product = $this->repo()->productById($productId);
4955 return $this->placeholder(
$texts->get_fd_message(
'attributes_title'),
$texts->get_fd_message(
'attributes_product_not_found'));
4958 $valueMap = array();
4959 foreach (($product[
'attributes'] ?? array()) as $attribute) {
4960 $valueMap[(int)($attribute[
'id'] ?? 0)] = (string)($attribute[
'value_text'] ??
'');
4964 foreach ($this->repo()->attributeDefinitionsForProduct($productId,
true) as
$definition) {
4966 $value = $valueMap[$id] ??
'';
4967 $type = (string)(
$definition[
'input_type'] ??
'text');
4969 if ($type ===
'select') {
4970 $input =
'<select class="form-select form-select-sm" name="attr_value[' . $id .
']"><option value="">-</option>';
4971 foreach ($this->attributeOptions((
string)(
$definition[
'options'] ??
'')) as $option) {
4972 $input .=
'<option value="' . $this->h($option) .
'"' . ($option ===
$value ?
' selected' :
'') .
'>' . $this->h($option) .
'</option>';
4975 $input .=
'<option value="' . $this->h(
$value) .
'" selected>' . $this->h(
$value) .
'</option>';
4979 $inputType = $type ===
'number' ?
'number' :
'text';
4980 $step = $type ===
'number' ?
' step="0.01"' :
'';
4981 $input =
'<input class="form-control form-control-sm" type="' . $inputType .
'"' . $step .
' name="attr_value[' . $id .
']" value="' . $this->h(
$value) .
'">';
4984 $rows .=
'<td><strong>' . $this->h(
$definition[
'title'] ??
'') .
'</strong><br><small><code>' . $this->h(
$definition[
'attr_key'] ??
'') .
'</code></small></td>';
4987 $rows .=
'<td>' . ((int)(
$definition[
'required'] ?? 0) === 1 ?
'<span class="badge text-bg-warning">' . $this->h(
$texts->get_fd_message(
'required')) .
'</span>' :
'') .
' ' . ((
int)(
$definition[
'filterable'] ?? 0) === 1 ?
'<span class="badge text-bg-info">' . $this->h(
$texts->get_fd_message(
'filter')) .
'</span>' :
'') .
'</td>';
4992 $rows =
'<tr><td colspan="4" class="text-muted">' . $this->h(
$texts->get_fd_message(
'attributes_none')) .
'</td></tr>';
4995 $form = $this->shopAdminCardForm(
4996 'shop-product-attributes-' . $productId,
4997 'dbxShop|shopProductAttributeValue',
4998 array(
'id' => $productId),
5000 '?dbx_modul=dbxShop_admin&dbx_run1=product_attributes&id=' . $productId,
5001 'save_product_attributes',
5002 'save_product_attributes',
5003 '<code>' . $this->h($product[
'sku'] ??
'') .
'</code><span>' . $this->h($product[
'title'] ??
'') .
'</span>',
5004 $texts->get_fd_message(
'attributes_value_subtitle'),
5005 'dbx-shop-product-attributes-card'
5007 $form->add_rep(
'extra_hidden',
'<input type="hidden" name="product_id" value="' . $productId .
'">');
5008 $form->add_rep(
'card_badges', $this->helpButton($this->ensureShopProductAttributesHelpPage(),
$texts->get_fd_message(
'attributes_help'),
'btn btn-outline-secondary btn-sm me-1') .
'<a class="btn btn-outline-secondary btn-sm" href="?dbx_modul=dbxShop_admin&dbx_run1=products">' . $this->h(
$texts->get_fd_message(
'back')) .
'</a>');
5009 $form->add_rep(
'form_body',
'<div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>' . $this->h(
$texts->get_fd_message(
'column_attribute')) .
'</th><th>' . $this->h(
$texts->get_fd_message(
'column_value')) .
'</th><th>' . $this->h(
$texts->get_fd_message(
'column_unit')) .
'</th><th>' . $this->h(
$texts->get_fd_message(
'column_property')) .
'</th></tr></thead><tbody>' .
$rows .
'</tbody></table></div>');
5010 return $this->frame(
'<div class="dbx-shop-admin-card-list">' .
$form->run() .
'</div>',
$texts->get_fd_message(
'attributes_edit_title'));
5013 private function shippingGroups(): string {
5014 $this->ensureSeed();
5015 $texts = $this->catalogTexts();
5016 if ($this->posted(
'delete_shipping_group')) {
5017 $this->repo()->deleteShippingGroup((
int)(
$_POST[
'id'] ?? 0));
5018 } elseif ($this->posted(
'save_shipping_group')) {
5019 $this->repo()->updateShippingGroup((
int)(
$_POST[
'id'] ?? 0),
$_POST);
5022 $cardHelpButton = $this->helpButton($this->ensureShopShippingGroupsHelpPage(),
$texts->get_fd_message(
'shipping_help'),
'btn btn-outline-secondary btn-sm me-1');
5023 $cardHtml =
function (array $group,
bool $isNew =
false) use ($cardHelpButton,
$texts):
string {
5024 $id = (int)($group[
'id'] ?? 0);
5026 $title =
'<span>' . $this->h(
$texts->get_fd_message(
'shipping_new')) .
'</span>';
5027 $subtitle =
$texts->get_fd_message(
'shipping_new_subtitle');
5029 $title =
'<code>' . $this->h($group[
'group_key'] ??
'') .
'</code><span>' . $this->h($group[
'title'] ??
'') .
'</span>';
5030 $subtitle = trim((
string)($group[
'description'] ??
''));
5033 $form = $this->shopAdminCardForm(
5034 'shop-shipping-group-' . ($isNew ?
'new' : $id),
5035 'dbxShop|shopShippingGroup',
5038 '?dbx_modul=dbxShop_admin&dbx_run1=shipping_groups' . ($isNew ?
'&new=1' :
''),
5039 'save_shipping_group',
5040 'save_shipping_group',
5043 'dbx-shop-shipping-group-card'
5045 $form->add_rep(
'card_badges', $cardHelpButton . ($isNew ?
'<span class="badge text-bg-primary">' . $this->h(
$texts->get_fd_message(
'new')) .
'</span>' : $this->activeBadge($group,
$texts)));
5047 $form->add_rep(
'delete_button', $this->shopAdminCardDeleteButton(
'delete_shipping_group',
$texts->get_fd_message(
'shipping_delete_title'),
$texts->get_fd_message(
'shipping_delete_confirm')));
5050 $form->add_fld(
'group_key', tpl:
'text-label', placeholder:
'versandgruppe-key', rules:
'*|parameter|max=80');
5052 $form->add_fld(
'title', tpl:
'text-label', placeholder:
$texts->get_fd_message(
'shipping_new'), rules:
'*|max=160');
5053 $form->add_fld(
'description', tpl:
'textarea-label', placeholder:
$texts->get_fd_message(
'description_placeholder'), data:
'rows=2');
5054 $form->add_fld(
'shipping_way', tpl:
'text-label', placeholder:
$texts->get_fd_message(
'shipping_way_placeholder'));
5055 $form->add_fld(
'delivery_time', tpl:
'text-label', placeholder:
$texts->get_fd_message(
'shipping_time_placeholder'));
5056 $form->add_fld(
'shipping_gross', tpl:
'text-label', placeholder:
'5.90', rules:
'decimal');
5057 $form->add_fld(
'free_from_gross', tpl:
'text-label', placeholder:
'-1', rules:
'decimal');
5058 $form->add_fld(
'sorter', tpl:
'text-label', rules:
'int');
5059 $form->add_fld(
'active', tpl:
'checkbox-label', rules:
'int');
5060 $form->add_rep(
'form_body',
5061 '<div class="dbx-shop-admin-card-grid">'
5062 . ($isNew ?
'<div>{obj:group_key}</div>' :
'')
5063 .
'<div>{obj:title}</div>'
5064 .
'<div>{obj:shipping_way}</div>'
5065 .
'<div>{obj:delivery_time}</div>'
5066 .
'<div>{obj:shipping_gross}</div>'
5067 .
'<div>{obj:free_from_gross}</div>'
5068 .
'<div>{obj:sorter}</div>'
5069 .
'<div>{obj:active}</div>'
5070 .
'<div class="wide">{obj:description}</div>'
5073 return $form->run();
5076 $groups = $this->repo()->shippingGroups();
5078 if ((
int)(
$_GET[
'new'] ?? 0) === 1) {
5081 $sorter = max($sorter, (
int)($group[
'sorter'] ?? 0) + 10);
5083 $cards .= $cardHtml(array(
5084 'shipping_gross' => 0,
5085 'free_from_gross' => -1,
5087 'sorter' => $sorter,
5091 $cards .= $cardHtml($group);
5093 $helpButton = $this->helpButton($this->ensureShopShippingGroupsHelpPage(),
$texts->get_fd_message(
'shipping_help'));
5094 $barActions =
'<a class="btn btn-outline-primary btn-sm me-1" href="?dbx_modul=dbxShop_admin&dbx_run1=shipping_groups&new=1" title="' . $this->h(
$texts->get_fd_message(
'shipping_new_title')) .
'"><i class="bi bi-plus-square"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'shipping_new')) .
'</span></a>' . $helpButton;
5095 return $this->frame(
'<div class="alert alert-info mx-3 mt-3 mb-0">' . $this->h(
$texts->get_fd_message(
'shipping_intro')) .
'</div><div class="dbx-shop-admin-card-list">' . $cards .
'</div>',
$texts->get_fd_message(
'shipping_title'), $barActions);
5098 private function channelGroups(): string {
5099 $this->ensureSeed();
5100 $texts = $this->catalogTexts();
5101 if ($this->posted(
'delete_channel_group')) {
5102 $this->repo()->deleteChannelGroup((
int)(
$_POST[
'id'] ?? 0));
5103 } elseif ($this->posted(
'save_channel_group')) {
5104 $this->repo()->updateChannelGroup((
int)(
$_POST[
'id'] ?? 0),
$_POST, array_map(
'strval',
$_POST[
'channels'] ?? array()));
5106 $channels = $this->repo()->channels();
5107 $groups = $this->repo()->channelGroups();
5108 $cardHelpButton = $this->helpButton($this->ensureShopChannelHelpPage(),
$texts->get_fd_message(
'channel_groups_help'),
'btn btn-outline-secondary btn-sm me-1');
5109 $cardHtml =
function (array $group,
bool $isNew =
false) use ($channels, $cardHelpButton,
$texts):
string {
5110 $id = (int)($group[
'id'] ?? 0);
5112 foreach (($group[
'channels'] ?? array()) as $channel) {
5113 if ((
int)($channel[
'active'] ?? 0) === 1) {
5114 $active[] = (string)$channel[
'channel_key'];
5117 $checks =
'<div class="dbx-shop-admin-check-grid">';
5118 foreach ($channels as $channel) {
5119 $key = (string)($channel[
'channel_key'] ??
'');
5123 $checks .=
'<label><input type="checkbox" name="channels[]" value="' . $this->h($key) .
'"' . (in_array($key, $active,
true) ?
' checked' :
'') .
'> <span>' . $this->h($channel[
'title'] ?? $key) .
'</span></label>';
5128 $title =
'<span>' . $this->h(
$texts->get_fd_message(
'channel_groups_new')) .
'</span>';
5129 $subtitle =
$texts->get_fd_message(
'channel_groups_new_subtitle');
5131 $title =
'<code>' . $this->h($group[
'group_key'] ??
'') .
'</code><span>' . $this->h($group[
'title'] ??
'') .
'</span>';
5132 $subtitle = trim((
string)($group[
'description'] ??
''));
5135 $form = $this->shopAdminCardForm(
5136 'shop-channel-group-' . ($isNew ?
'new' : $id),
5137 'dbxShop|shopChannelGroup',
5140 '?dbx_modul=dbxShop_admin&dbx_run1=channel_groups' . ($isNew ?
'&new=1' :
''),
5141 'save_channel_group',
5142 'save_channel_group',
5145 'dbx-shop-channel-group-card'
5147 $form->add_rep(
'card_badges', $cardHelpButton . ($isNew ?
'<span class="badge text-bg-primary">' . $this->h(
$texts->get_fd_message(
'new')) .
'</span>' : $this->activeBadge($group,
$texts)));
5149 $form->add_rep(
'delete_button', $this->shopAdminCardDeleteButton(
'delete_channel_group',
$texts->get_fd_message(
'channel_groups_delete_title'),
$texts->get_fd_message(
'channel_groups_delete_confirm')));
5152 $form->add_fld(
'group_key', tpl:
'text-label', placeholder:
'neue-channel-gruppe', rules:
'*|parameter|max=80');
5154 $form->add_fld(
'title', tpl:
'text-label', placeholder:
$texts->get_fd_message(
'channel_groups_new'), rules:
'*|max=160');
5155 $form->add_fld(
'description', tpl:
'textarea-label', placeholder:
$texts->get_fd_message(
'description_placeholder'), data:
'rows=2');
5156 $form->add_fld(
'sorter', tpl:
'text-label', rules:
'int');
5157 $form->add_fld(
'active', tpl:
'checkbox-label', rules:
'int');
5158 $form->add_obj(
'channel_checks',
'obj-value',
$checks);
5159 $form->add_rep(
'form_body',
5160 '<div class="dbx-shop-admin-card-grid">'
5161 . ($isNew ?
'<div>{obj:group_key}</div>' :
'')
5162 .
'<div>{obj:title}</div>'
5163 .
'<div>{obj:sorter}</div>'
5164 .
'<div>{obj:active}</div>'
5165 .
'<div class="wide">{obj:description}</div>'
5166 .
'<div class="wide"><label class="form-label">' . $this->h(
$texts->get_fd_message(
'channels_label')) .
'</label>{obj:channel_checks}</div>'
5169 return $form->run();
5173 if ((
int)(
$_GET[
'new'] ?? 0) === 1) {
5176 $sorter = max($sorter, (
int)($group[
'sorter'] ?? 0) + 10);
5178 $cards .= $cardHtml(array(
5180 'description' =>
'',
5182 'sorter' => $sorter,
5183 'channels' => array(),
5187 $cards .= $cardHtml($group);
5189 $helpButton = $this->helpButton($this->ensureShopChannelHelpPage(),
$texts->get_fd_message(
'channel_groups_help'));
5190 $barActions =
'<a class="btn btn-outline-primary btn-sm me-1" href="?dbx_modul=dbxShop_admin&dbx_run1=channel_groups&new=1" title="' . $this->h(
$texts->get_fd_message(
'channel_groups_new_title')) .
'"><i class="bi bi-plus-square"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'channel_groups_new')) .
'</span></a>' . $helpButton;
5191 return $this->frame(
'<div class="alert alert-info mx-3 mt-3 mb-0">' . $this->h(
$texts->get_fd_message(
'channel_groups_intro')) .
'</div><div class="dbx-shop-admin-card-list">' . $cards .
'</div>',
$texts->get_fd_message(
'channel_groups_title'), $barActions);
5194 private function shopMediaDir(
bool $ensure =
false): string {
5195 $dir = dirname(__DIR__, 4) .
'/files/shop/img';
5196 if ($ensure && !is_dir(
$dir)) {
5197 mkdir(
$dir, 0775,
true);
5202 private function safeFileName(
string $name): string {
5203 $name = strtolower(trim($name));
5204 $name = preg_replace(
'~[^a-z0-9._-]+~',
'-', $name);
5205 $name = trim((
string)$name,
'-.');
5206 return $name !==
'' ? $name :
'shop-image';
5209 private function handleMediaUpload(): string {
5210 if (!$this->posted(
'upload_media')) {
5213 if (empty($_FILES[
'shop_image'][
'tmp_name']) || !is_uploaded_file($_FILES[
'shop_image'][
'tmp_name'])) {
5214 return '<div class="alert alert-warning m-3">Keine Datei ausgewaehlt.</div>';
5216 $original = (string)($_FILES[
'shop_image'][
'name'] ??
'shop-image');
5217 $ext = strtolower(pathinfo($original, PATHINFO_EXTENSION));
5218 $allowed = array(
'jpg',
'jpeg',
'png',
'gif',
'webp',
'svg');
5219 if (!in_array($ext,
$allowed,
true)) {
5220 return '<div class="alert alert-danger m-3">Dieser Bildtyp ist fuer Shop-Medien nicht erlaubt.</div>';
5223 $base = $this->safeFileName(pathinfo($original, PATHINFO_FILENAME));
5224 $name =
$base .
'.' . $ext;
5225 $target = $this->shopMediaDir(
true) .
'/' . $name;
5228 $name =
$base .
'-' . $i .
'.' . $ext;
5229 $target = $this->shopMediaDir() .
'/' . $name;
5232 if (!move_uploaded_file($_FILES[
'shop_image'][
'tmp_name'],
$target)) {
5233 return '<div class="alert alert-danger m-3">Upload konnte nicht gespeichert werden.</div>';
5236 $rel =
'files/shop/img/' . $name;
5237 $productId = (int)(
$_POST[
'product_id'] ?? 0);
5238 $groupId = (int)(
$_POST[
'group_id'] ?? 0);
5239 if ($productId > 0 || $groupId > 0) {
5240 $this->repo()->saveImage($productId, $groupId, $rel, (
string)(
$_POST[
'title'] ??
$base), (
string)(
$_POST[
'alt'] ??
$base), !empty(
$_POST[
'is_primary']) ? 1 : 0, (
int)(
$_POST[
'sorter'] ?? 100));
5242 return '<div class="alert alert-success m-3">Bild wurde hochgeladen: ' . $this->h($name) .
'</div>';
5245 private function media(): string {
5246 $this->ensureSeed();
5247 $texts = $this->catalogTexts();
5248 $selectedProduct = (int)
dbx()->get_modul_var(
'product_id',
'0',
'int');
5249 $selectedGroup = (int)
dbx()->get_modul_var(
'group_id',
'0',
'int');
5251 $productOptions =
'<option value="0">' . $this->h(
$texts->get_fd_message(
'media_no_product')) .
'</option>';
5252 foreach ($this->repo()->products(
false) as $product) {
5253 $sel = (int)($product[
'id'] ?? 0) === $selectedProduct ?
' selected' :
'';
5254 $productOptions .=
'<option value="' . (int)($product[
'id'] ?? 0) .
'"' . $sel .
'>' . $this->h($product[
'title'] ??
'') .
'</option>';
5256 $groupOptions =
'<option value="0">' . $this->h(
$texts->get_fd_message(
'media_no_group')) .
'</option>';
5257 foreach ($this->repo()->groups() as $group) {
5258 $sel = (int)($group[
'id'] ?? 0) === $selectedGroup ?
' selected' :
'';
5259 $groupOptions .=
'<option value="' . (int)($group[
'id'] ?? 0) .
'"' . $sel .
'>' . $this->h($group[
'title'] ??
'') .
'</option>';
5262 $mediaCfg = $this->shopMediaConfig();
5263 $attrs = $this->shopMediaAttrs($mediaCfg);
5265 $html =
'<div class="dbx-shop-media-manager m-3"' . $attrs .
'>';
5266 $targetForm =
dbx()->get_system_obj(
'dbxForm');
5267 $targetForm->init(
'shop-media-target-form',
'shop-media-target-form');
5268 $targetForm->_action =
'?dbx_modul=dbxShop_admin&dbx_run1=media';
5269 $targetForm->set_form_help_enabled(
false);
5270 $targetForm->add_rep(
'frame_skip_form_wrap',
'1');
5271 $targetForm->add_rep(
'form_body',
5272 '<div class="row g-2 align-items-end">'
5273 .
'<div class="col-md-4"><label class="form-label">' . $this->h(
$texts->get_fd_message(
'media_product')) .
'</label><select class="form-select form-select-sm" name="product_id" data-shop-product-select>' . $productOptions .
'</select></div>'
5274 .
'<div class="col-md-4"><label class="form-label">' . $this->h(
$texts->get_fd_message(
'media_group')) .
'</label><select class="form-select form-select-sm" name="group_id" data-shop-group-select>' . $groupOptions .
'</select></div>'
5275 .
'<div class="col-md-2"><label class="form-label">' . $this->h(
$texts->get_fd_message(
'media_sort')) .
'</label><input class="form-control form-control-sm" name="sorter" value="100" data-shop-sorter></div>'
5276 .
'<div class="col-md-2 form-check pb-1"><input class="form-check-input" type="checkbox" name="is_primary" value="1" id="shop_img_primary" data-shop-primary><label class="form-check-label" for="shop_img_primary">' . $this->h(
$texts->get_fd_message(
'media_primary')) .
'</label></div>'
5277 .
'<div class="col-md-8"><div class="form-text">' . $this->h(
$texts->get_fd_message(
'media_hint')) .
'</div></div>'
5278 .
'<div class="col-md-2"><button class="btn btn-outline-secondary btn-sm w-100"><i class="bi bi-filter"></i> ' . $this->h(
$texts->get_fd_message(
'media_load_selection')) .
'</button></div>'
5279 .
'<div class="col-md-2"><button type="button" class="btn btn-outline-primary btn-sm w-100 dbx-shop-media-pick" data-shop-media-folder="img/shop" title="' . $this->h(
$texts->get_fd_message(
'media_select_title')) .
'"><i class="bi bi-images"></i><i class="bi bi-camera-video"></i><i class="bi bi-upload"></i><span>' . $this->h(
$texts->get_fd_message(
'selection')) .
'</span></button></div>'
5282 $html .= $targetForm->run();
5285 foreach ($this->repo()->allImages() as $image) {
5286 $assigned .=
'<div class="col"><div class="card h-100"><img src="' . $this->h($this->mediaItemUrl($image,
true)) .
'" class="card-img-top" alt="" style="height:120px;object-fit:cover;"><div class="card-body p-2"><strong>' . $this->h($image[
'title'] ??
'') .
'</strong><br><small>' . $this->h($image[
'product_title'] ?: $image[
'group_title'] ?:
$texts->get_fd_message(
'not_assigned')) .
'</small></div></div></div>';
5288 $html .=
'<div class="m-3"><h5>' . $this->h(
$texts->get_fd_message(
'media_assigned_title')) .
'</h5><div class="row row-cols-2 row-cols-md-4 row-cols-xl-6 g-2">' . $assigned .
'</div></div>';
5290 $files = glob($this->shopMediaDir() .
'/*.{jpg,jpeg,png,gif,webp,svg}', GLOB_BRACE) ?: array();
5293 $rel =
'files/shop/img/' . basename(
$file);
5294 $fileCards .=
'<div class="col"><div class="card h-100"><img src="' . $this->h($this->mediaUrl($rel)) .
'" class="card-img-top" alt="" style="height:120px;object-fit:cover;"><div class="card-body p-2"><small>' . $this->h(basename(
$file)) .
'</small></div></div></div>';
5296 $html .=
'<div class="m-3"><h5>' . $this->h(
$texts->get_fd_message(
'media_legacy_title')) .
'</h5><div class="row row-cols-2 row-cols-md-4 row-cols-xl-6 g-2">' . $fileCards .
'</div></div>';
5297 $html .= $this->shopMediaFormTemplates($mediaCfg);
5300 $barActions = $this->helpButton($this->ensureShopMediaHelpPage(),
$texts->get_fd_message(
'media_help'));
5301 return $this->frame(
$html,
$texts->get_fd_message(
'media_title'), $barActions);
5304 private function channels(): string {
5305 $this->ensureSeed();
5306 $texts = $this->catalogTexts();
5308 if ($this->posted(
'delete_channel')) {
5309 $this->repo()->deleteChannel((
int)(
$_POST[
'id'] ?? 0));
5310 } elseif ($this->posted(
'save_channel')) {
5311 $this->repo()->updateChannel((
int)(
$_POST[
'id'] ?? 0),
$_POST);
5312 } elseif ($this->posted(
'test_channel')) {
5313 $id = (int)(
$_POST[
'id'] ?? 0);
5315 $this->repo()->updateChannel($id,
$_POST);
5316 $result = $this->repo()->testChannelConnection($id);
5317 $notice =
'<div class="alert ' . (!empty(
$result[
'ok']) ?
'alert-success' :
'alert-warning') .
' m-3">' . $this->h(
$result[
'message'] ??
'') .
'</div>';
5319 $notice =
'<div class="alert alert-warning m-3">' . $this->h(
$texts->get_fd_message(
'channels_save_first')) .
'</div>';
5323 $platformOptions = array(
5325 'amazon' =>
'Amazon',
5327 'kleinanzeigen' =>
'Kleinanzeigen',
5328 'mobile' =>
'mobile.de',
5329 'custom' =>
$texts->get_fd_message(
'channels_platform_custom'),
5331 $modeOptions = array(
5332 'internal' =>
$texts->get_fd_message(
'channels_mode_internal'),
5333 'manual' =>
$texts->get_fd_message(
'channels_mode_manual'),
5336 'webhook' =>
'Webhook',
5339 $platformHints = array(
5340 'shop' => array(
$texts->get_fd_message(
'channels_hint_shop_api'),
$texts->get_fd_message(
'channels_hint_shop_listing'),
$texts->get_fd_message(
'channels_hint_shop_feedback')),
5341 'amazon' => array(
$texts->get_fd_message(
'channels_hint_amazon_api'),
$texts->get_fd_message(
'channels_hint_amazon_listing'),
$texts->get_fd_message(
'channels_hint_amazon_feedback')),
5342 'ebay' => array(
$texts->get_fd_message(
'channels_hint_ebay_api'),
$texts->get_fd_message(
'channels_hint_ebay_listing'),
$texts->get_fd_message(
'channels_hint_ebay_feedback')),
5343 'kleinanzeigen' => array(
$texts->get_fd_message(
'channels_hint_classified_api'),
$texts->get_fd_message(
'channels_hint_classified_listing'),
$texts->get_fd_message(
'channels_hint_classified_feedback')),
5344 'mobile' => array(
$texts->get_fd_message(
'channels_hint_mobile_api'),
$texts->get_fd_message(
'channels_hint_mobile_listing'),
$texts->get_fd_message(
'channels_hint_mobile_feedback')),
5345 'custom' => array(
$texts->get_fd_message(
'channels_hint_custom_api'),
$texts->get_fd_message(
'channels_hint_custom_listing'),
$texts->get_fd_message(
'channels_hint_custom_feedback')),
5348 $rowHtml =
function (array $channel,
bool $isNew =
false) use ($platformOptions, $modeOptions, $platformHints,
$texts):
string {
5349 $id = (int)($channel[
'id'] ?? 0);
5350 $key = (string)($channel[
'channel_key'] ??
'');
5351 $platform = (string)($channel[
'platform_type'] ??
'custom');
5352 $hint = $platformHints[$platform] ?? $platformHints[
'custom'];
5353 $placeholderMap = array(
5355 'channel_key' =>
'shop',
5356 'api_base_url' =>
'nicht benoetigt',
5357 'api_client_id' =>
'nicht benoetigt',
5358 'api_username' =>
'nicht benoetigt',
5359 'api_client_secret' =>
'nicht benoetigt',
5360 'api_access_token' =>
'nicht benoetigt',
5361 'api_refresh_token' =>
'nicht benoetigt',
5362 'api_password' =>
'nicht benoetigt',
5363 'marketplace_id' =>
'nicht benoetigt',
5364 'seller_id' =>
'nicht benoetigt',
5365 'account_id' =>
'nicht benoetigt',
5366 'location_key' =>
'nicht benoetigt',
5367 'category_id' =>
'nicht benoetigt',
5368 'payment_policy_id' =>
'nicht benoetigt',
5369 'fulfillment_policy_id' =>
'nicht benoetigt',
5370 'return_policy_id' =>
'nicht benoetigt',
5371 'notification_destination' =>
'nicht benoetigt',
5372 'notification_topic' =>
'nicht benoetigt',
5373 'webhook_secret' =>
'nicht benoetigt',
5374 'webhook_url' =>
'nicht benoetigt',
5375 'api_scope' =>
'nicht benoetigt',
5378 'channel_key' =>
'amazon',
5379 'api_base_url' =>
'https://sellingpartnerapi-eu.amazon.com',
5380 'api_client_id' =>
'amzn1.application-oa2-client.keyxxxx',
5381 'api_username' =>
'nicht benoetigt bei OAuth',
5382 'api_client_secret' =>
'amzn1.oa2-cs.v1.secretxxxx',
5383 'api_access_token' =>
'Atza|access_token_xxxx',
5384 'api_refresh_token' =>
'Atzr|refresh_token_xxxx',
5385 'api_password' =>
'nicht benoetigt bei SP-API',
5386 'marketplace_id' =>
'A1PA6795UKMFR9',
5387 'seller_id' =>
'A1SELLERIDXXXX',
5388 'account_id' =>
'account_xxxx',
5389 'location_key' =>
'nicht benoetigt',
5390 'category_id' =>
'productType: SOFTWARE / PRODUCT',
5391 'payment_policy_id' =>
'nicht benoetigt',
5392 'fulfillment_policy_id' =>
'nicht benoetigt',
5393 'return_policy_id' =>
'nicht benoetigt',
5394 'notification_destination' =>
'arn:aws:sqs:eu-central-1:123456789012:amazon-orders',
5395 'notification_topic' =>
'ORDER_CHANGE',
5396 'webhook_secret' =>
'secret_64zeichen_xxxx',
5397 'webhook_url' =>
'wird von dbxShop erzeugt',
5398 'api_scope' =>
"Listings Items\nOrders\nNotifications",
5401 'channel_key' =>
'ebay',
5402 'api_base_url' =>
'https://api.ebay.com',
5403 'api_client_id' =>
'keyxxxx-appid',
5404 'api_username' =>
'nicht benoetigt bei OAuth',
5405 'api_client_secret' =>
'certid-secretxxxx',
5406 'api_access_token' =>
'v^1.1#i^1#p^3#access_token_xxxx',
5407 'api_refresh_token' =>
'v^1.1#r^1#p^3#refresh_token_xxxx',
5408 'api_password' =>
'nicht benoetigt bei OAuth',
5409 'marketplace_id' =>
'EBAY_DE',
5410 'seller_id' =>
'sellername_xxxx',
5411 'account_id' =>
'account_xxxx',
5412 'location_key' =>
'default',
5413 'category_id' =>
'58058',
5414 'payment_policy_id' =>
'policy_payment_1234567890',
5415 'fulfillment_policy_id' =>
'policy_fulfillment_1234567890',
5416 'return_policy_id' =>
'policy_return_1234567890',
5417 'notification_destination' =>
'https://domain.de/?dbx_modul=dbxShop&dbx_run1=channel_webhook&channel=ebay',
5418 'notification_topic' =>
'ORDER',
5419 'webhook_secret' =>
'verification_token_xxxx',
5420 'webhook_url' =>
'wird von dbxShop erzeugt',
5421 'api_scope' =>
"https://api.ebay.com/oauth/api_scope/sell.inventory\nhttps://api.ebay.com/oauth/api_scope/sell.fulfillment\nhttps://api.ebay.com/oauth/api_scope/commerce.notification.subscription",
5423 'kleinanzeigen' => array(
5424 'channel_key' =>
'kleinanzeigen',
5425 'api_base_url' =>
'nur bei freigegebener Schnittstelle',
5426 'api_client_id' =>
'partner_key_xxxx',
5427 'api_username' =>
'api-user@example.de',
5428 'api_client_secret' =>
'partner_secret_xxxx',
5429 'api_access_token' =>
'access_token_xxxx',
5430 'api_refresh_token' =>
'refresh_token_xxxx',
5431 'api_password' =>
'password_xxxx',
5432 'marketplace_id' =>
'DE',
5433 'seller_id' =>
'seller_xxxx',
5434 'account_id' =>
'account_xxxx',
5435 'location_key' =>
'standort_10115_berlin',
5436 'category_id' =>
'category_12345',
5437 'payment_policy_id' =>
'nicht benoetigt',
5438 'fulfillment_policy_id' =>
'nicht benoetigt',
5439 'return_policy_id' =>
'nicht benoetigt',
5440 'notification_destination' =>
'https://middleware.example.de/webhook',
5441 'notification_topic' =>
'lead / message / sale',
5442 'webhook_secret' =>
'middleware_secret_xxxx',
5443 'webhook_url' =>
'wird von dbxShop erzeugt',
5444 'api_scope' =>
"laut Vertrag\nMiddleware-Berechtigung",
5447 'channel_key' =>
'mobile',
5448 'api_base_url' =>
'https://services.mobile.de/seller-api',
5449 'api_client_id' =>
'nicht benoetigt bei Basic Auth',
5450 'api_username' =>
'dealer_api_user_xxxx',
5451 'api_client_secret' =>
'nicht benoetigt bei Basic Auth',
5452 'api_access_token' =>
'nicht benoetigt bei Basic Auth',
5453 'api_refresh_token' =>
'nicht benoetigt bei Basic Auth',
5454 'api_password' =>
'dealer_api_password_xxxx',
5455 'marketplace_id' =>
'DE',
5456 'seller_id' =>
'customer_123456',
5457 'account_id' =>
'mobileSellerId_123456',
5458 'location_key' =>
'location_123456',
5459 'category_id' =>
'car / motorbike / commercial',
5460 'payment_policy_id' =>
'nicht benoetigt',
5461 'fulfillment_policy_id' =>
'nicht benoetigt',
5462 'return_policy_id' =>
'nicht benoetigt',
5463 'notification_destination' =>
'https://middleware.example.de/mobile-leads',
5464 'notification_topic' =>
'lead-api',
5465 'webhook_secret' =>
'lead_secret_xxxx',
5466 'webhook_url' =>
'wird von dbxShop erzeugt',
5467 'api_scope' =>
"seller-api\nbasic-auth\nlead-api",
5470 'channel_key' =>
'mein-channel',
5471 'api_base_url' =>
'https://api.anbieter.de/v1',
5472 'api_client_id' =>
'client_123456',
5473 'api_username' =>
'api-user@example.de',
5474 'api_client_secret' =>
'client_secret_xxxxx',
5475 'api_access_token' =>
'access_token_xxxxx',
5476 'api_refresh_token' =>
'refresh_token_xxxxx',
5477 'api_password' =>
'API-Passwort',
5478 'marketplace_id' =>
'DE',
5479 'seller_id' =>
'seller_123456',
5480 'account_id' =>
'account_123456',
5481 'location_key' =>
'lager-1',
5482 'category_id' =>
'12345',
5483 'payment_policy_id' =>
'payment_123',
5484 'fulfillment_policy_id' =>
'shipping_123',
5485 'return_policy_id' =>
'return_123',
5486 'notification_destination' =>
'https://domain.de/webhook',
5487 'notification_topic' =>
'order.created',
5488 'webhook_secret' =>
'zufaelliges-geheimes-secret',
5489 'webhook_url' =>
'wird von dbxShop erzeugt',
5490 'api_scope' =>
"products:write\norders:read\nwebhooks:read",
5493 $placeholders = $placeholderMap[$platform] ?? $placeholderMap[
'custom'];
5494 $placeholderTranslations = array(
5495 'nicht benoetigt' =>
$texts->get_fd_message(
'channels_not_required'),
5496 'nicht benoetigt bei OAuth' =>
$texts->get_fd_message(
'channels_not_required_oauth'),
5497 'nicht benoetigt bei SP-API' =>
$texts->get_fd_message(
'channels_not_required_spapi'),
5498 'nicht benoetigt bei Basic Auth' =>
$texts->get_fd_message(
'channels_not_required_basic'),
5499 'wird von dbxShop erzeugt' =>
$texts->get_fd_message(
'channels_generated'),
5500 'nur bei freigegebener Schnittstelle' =>
$texts->get_fd_message(
'channels_approved_only'),
5502 foreach ($placeholders as $placeholderField => $placeholderValue) {
5503 if (isset($placeholderTranslations[$placeholderValue])) {
5504 $placeholders[$placeholderField] = $placeholderTranslations[$placeholderValue];
5507 $ph =
function (
string $field) use ($placeholders):
string {
5508 return $this->h($placeholders[
$field] ??
'');
5510 $secretPlaceholder =
function (
string $field) use ($placeholders):
string {
5511 return $this->h((
string)($placeholders[
$field] ??
''));
5513 $status = (string)($channel[
'test_status'] ??
'');
5514 $statusBadge = $status ===
'ok'
5515 ?
'<span class="badge text-bg-success">' . $this->h(
$texts->get_fd_message(
'channels_status_ok')) .
'</span>'
5516 : ($status ===
'error'
5517 ?
'<span class="badge text-bg-warning">' . $this->h(
$texts->get_fd_message(
'channels_status_open')) .
'</span>'
5518 :
'<span class="badge text-bg-secondary">' . $this->h(
$texts->get_fd_message(
'channels_status_none')) .
'</span>');
5519 $activeBadge = (int)($channel[
'active'] ?? 1) === 1
5520 ?
'<span class="badge text-bg-success">' . $this->h(
$texts->get_fd_message(
'active')) .
'</span>'
5521 :
'<span class="badge text-bg-secondary">' . $this->h(
$texts->get_fd_message(
'inactive')) .
'</span>';
5522 $exportBadge = (int)($channel[
'export_enabled'] ?? 0) === 1 ?
'<span class="badge text-bg-info">' . $this->h(
$texts->get_fd_message(
'channels_export')) .
'</span>' :
'';
5523 $orderBadge = (int)($channel[
'order_import_enabled'] ?? 0) === 1 ?
'<span class="badge text-bg-primary">' . $this->h(
$texts->get_fd_message(
'channels_order_import')) .
'</span>' :
'';
5524 $webhookPath = ($key !==
'' && $platform !==
'shop') ?
'?dbx_modul=dbxShop&dbx_run1=channel_webhook&channel=' . rawurlencode($key) :
'';
5525 $open = $isNew || (int)(
$_GET[
'edit'] ?? 0) === $id;
5526 $editUrl =
'?dbx_modul=dbxShop_admin&dbx_run1=channels&edit=' . $id;
5527 $helpId = $this->ensureShopChannelProviderHelpPage($platform);
5530 $helpTitle =
'Hilfe: Channel ' . ($platformOptions[$platform] ??
'Channel');
5531 $helpButton = $this->openWinButton(
5532 '?dbx_modul=dbxContent&dbx_run1=content&cid=' . $helpId,
5534 '<i class="bi bi-question-circle"></i><span class="visually-hidden"> Hilfe</span>',
5535 'btn btn-outline-secondary btn-sm dbx-shop-channel-help',
5539 $helpButton = str_replace(
'<a ',
'<a data-dbx="lib=shopAdmin" data-shop-stop-propagation ', $helpButton);
5542 $form =
dbx()->get_system_obj(
'dbxForm');
5543 $form->init(
'shop-channel-form-' . ($isNew ?
'new' : $id),
'shop-channel-form');
5544 $form->_dd =
'dbxShop|shopChannel';
5545 $form->_fd =
'dbxShop|shop-channel';
5546 $form->_data = $channel + array(
'id' => $id);
5547 $form->_rid = $isNew ? 0 : $id;
5548 $form->_action =
'?dbx_modul=dbxShop_admin&dbx_run1=channels' . ($isNew ?
'&new=1' :
'&edit=' . $id);
5549 $form->set_activ_id($isNew ? 0 : $id);
5550 $form->set_form_help_enabled(
false);
5551 $form->add_rep(
'frame_skip_form_wrap',
'0');
5552 $form->add_rep(
'form_class',
'dbx-shop-channel-dbXForm');
5553 $form->add_rep(
'form_attrs',
'data-target="dbxForm_{i}" data-dbx="lib=confirm|class=dbxConfirm|bind=button"');
5554 $form->add_rep(
'details_open', $open ?
' open' :
'');
5555 $form->add_rep(
'channel_key_view', $this->h($key !==
'' ? $key : strtolower(
$texts->get_fd_message(
'new'))));
5556 $form->add_rep(
'channel_title_view', $this->h($channel[
'title'] ??
$texts->get_fd_message(
'channels_new')));
5557 $form->add_rep(
'platform_view', $this->h($platformOptions[$platform] ?? $platform));
5558 $form->add_rep(
'connection_view', $this->h($modeOptions[(
string)($channel[
'connection_mode'] ??
'manual')] ?? ($channel[
'connection_mode'] ??
'manual')));
5559 $form->add_rep(
'active_badge', $activeBadge);
5560 $form->add_rep(
'export_badge', $exportBadge);
5561 $form->add_rep(
'order_badge', $orderBadge);
5562 $form->add_rep(
'status_badge', $statusBadge);
5563 $form->add_rep(
'last_test_date', !empty($channel[
'last_test_date']) ? $this->h($channel[
'last_test_date']) :
'');
5564 $form->add_rep(
'channel_help_button', $helpButton);
5565 $form->add_rep(
'channel_edit_button', !$isNew ?
'<a class="btn btn-outline-primary btn-sm dbx-shop-channel-edit" data-dbx="lib=shopAdmin" data-shop-stop-propagation href="' . $this->h($editUrl) .
'" title="' . $this->h(
$texts->get_fd_message(
'channels_edit_title')) .
'"><i class="bi bi-pencil-square"></i> ' . $this->h(
$texts->get_fd_message(
'channels_edit')) .
'</a>' :
'');
5566 $form->add_rep(
'hint_api', $this->h($hint[0]));
5567 $form->add_rep(
'hint_listing', $this->h($hint[1]));
5568 $form->add_rep(
'hint_feedback', $this->h($hint[2]));
5569 $form->add_rep(
'test_message', !empty($channel[
'test_message']) ?
'<div class="col-12"><div class="alert alert-secondary py-2 mb-0">' . $this->h($channel[
'test_message']) .
'</div></div>' :
'');
5570 $form->add_rep(
'test_button', !$isNew ?
'<button class="btn btn-outline-secondary btn-sm ms-1" name="shop_action" value="test_channel" title="' . $this->h(
$texts->get_fd_message(
'channels_test_title')) .
'"><i class="bi bi-plug"></i> ' . $this->h(
$texts->get_fd_message(
'channels_test_label')) .
'</button>' :
'');
5571 $form->add_rep(
'delete_button', !$isNew ?
'<button type="submit" class="btn btn-outline-danger btn-sm ms-1 dbxConfirm" name="shop_action" value="delete_channel" title="' . $this->h(
$texts->get_fd_message(
'channels_delete_title')) .
'" data-confirm-title="<i class=\'bi bi-trash\'></i> ' . $this->h(
$texts->get_fd_message(
'channels_delete_title')) .
'" data-confirm="' . $this->h(
$texts->get_fd_message(
'channels_delete_confirm')) .
'" data-confirm-hint="<small>' . $this->h(
$texts->get_fd_message(
'channels_delete_hint')) .
'</small>" data-confirm-buttons="yesno"><i class="bi bi-trash"></i></button>' :
'');
5572 $form->_msg_info =
'';
5574 $form->add_fld(
'id', tpl:
'dbx|hidden', rules:
'int', dd:
'dd::');
5575 $form->add_fld(
'channel_key', placeholder: $placeholders[
'channel_key'] ??
'', data: $isNew ?
'' :
'readonly=readonly');
5576 $form->add_fld(
'title');
5577 $form->add_fld(
'platform_type', tpl:
'select-single-label', options: $platformOptions);
5578 $form->add_fld(
'connection_mode', tpl:
'select-single-label', options: $modeOptions);
5579 $form->add_fld(
'sorter');
5580 $form->add_fld(
'active');
5581 $form->add_fld(
'export_enabled');
5582 $form->add_fld(
'order_import_enabled');
5583 $form->add_fld(
'description', data:
'rows=2');
5584 $form->add_fld(
'api_base_url', placeholder: $placeholders[
'api_base_url'] ??
'');
5585 $form->add_fld(
'api_client_id', placeholder: $placeholders[
'api_client_id'] ??
'');
5586 $form->add_fld(
'api_username', placeholder: $placeholders[
'api_username'] ??
'');
5587 $form->add_fld(
'api_client_secret', placeholder: $secretPlaceholder(
'api_client_secret'));
5588 $form->add_fld(
'api_access_token', placeholder: $secretPlaceholder(
'api_access_token'));
5589 $form->add_fld(
'api_refresh_token', placeholder: $secretPlaceholder(
'api_refresh_token'));
5590 $form->add_fld(
'api_password', placeholder: $secretPlaceholder(
'api_password'));
5591 $form->add_fld(
'marketplace_id', placeholder: $placeholders[
'marketplace_id'] ??
'');
5592 $form->add_fld(
'seller_id', placeholder: $placeholders[
'seller_id'] ??
'');
5593 $form->add_fld(
'account_id', placeholder: $placeholders[
'account_id'] ??
'');
5594 $form->add_fld(
'location_key', placeholder: $placeholders[
'location_key'] ??
'');
5595 $form->add_fld(
'category_id', placeholder: $placeholders[
'category_id'] ??
'');
5596 $form->add_fld(
'payment_policy_id', placeholder: $placeholders[
'payment_policy_id'] ??
'');
5597 $form->add_fld(
'fulfillment_policy_id', placeholder: $placeholders[
'fulfillment_policy_id'] ??
'');
5598 $form->add_fld(
'return_policy_id', placeholder: $placeholders[
'return_policy_id'] ??
'');
5599 $form->add_fld(
'notification_destination', placeholder: $placeholders[
'notification_destination'] ??
'');
5600 $form->add_fld(
'notification_topic', placeholder: $placeholders[
'notification_topic'] ??
'');
5601 $form->add_fld(
'webhook_secret', placeholder: $secretPlaceholder(
'webhook_secret'));
5602 $form->add_fld(
'api_scope', placeholder: $placeholders[
'api_scope'] ??
'', data:
'rows=2');
5603 $form->add_obj(
'webhook_url',
'obj-value',
'<label class="form-label">' . $this->h(
$texts->get_fd_message(
'channels_webhook_url')) .
'</label><input class="form-control form-control-sm" value="' . $this->h($webhookPath) .
'" placeholder="' . $ph(
'webhook_url') .
'" readonly>');
5605 return $form->run();
5608 $channels = $this->repo()->channels();
5609 $content = $notice .
'<div class="m-3 dbx-shop-channel-list">';
5610 if ((
int)(
$_GET[
'new'] ?? 0) === 1) {
5612 foreach ($channels as $channel) {
5613 $sorter = max($sorter, (
int)($channel[
'sorter'] ?? 0) + 10);
5617 'platform_type' =>
'custom',
5618 'connection_mode' =>
'api',
5619 'export_enabled' => 1,
5620 'order_import_enabled' => 1,
5622 'sorter' => $sorter,
5625 foreach ($channels as $channel) {
5630 $helpId = $this->ensureShopChannelsHelpPage();
5631 $helpButton = $helpId > 0
5632 ? $this->openWinButton(
'?dbx_modul=dbxContent&dbx_run1=content&cid=' . $helpId,
$texts->get_fd_message(
'channels_help'),
'<i class="bi bi-question-circle"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'help')) .
'</span>',
'btn btn-outline-secondary btn-sm me-1',
'72%',
'82%')
5634 $barActions =
'<a class="btn btn-outline-primary btn-sm me-1" href="?dbx_modul=dbxShop_admin&dbx_run1=channels&new=1" title="' . $this->h(
$texts->get_fd_message(
'channels_new_title')) .
'"><i class="bi bi-plus-square"></i><span class="visually-hidden"> ' . $this->h(
$texts->get_fd_message(
'channels_new')) .
'</span></a>' . $helpButton;
5635 return $this->frame(
$content,
$texts->get_fd_message(
'channels_title'), $barActions);
5638 public function run(): string {
5639 $run =
dbx()->get_modul_var(
'dbx_run1',
'dashboard',
'parameter');
5645 return $this->dashboard();
5648 return $this->install();
5651 return $this->products();
5653 case 'product_edit':
5654 return $this->productEdit();
5656 case 'product_tree_move':
5657 return $this->productTreeMove();
5659 case 'product_channel_mapping':
5660 return $this->productChannelMapping();
5662 case 'products_help':
5663 return $this->productsHelp();
5666 return $this->groups();
5669 return $this->attributes();
5671 case 'product_attributes':
5672 return $this->productAttributes();
5674 case 'shipping_groups':
5675 return $this->shippingGroups();
5677 case 'channel_groups':
5678 return $this->channelGroups();
5681 return $this->channels();
5684 return $this->media();
5686 case 'assign_media':
5687 return $this->assignMedia();
5690 return $this->orders();
5692 case 'order_detail':
5693 return $this->orderDetail();
5695 case 'order_invoice':
5696 return $this->orderInvoice();
5698 case 'order_invoice_pdf':
5699 return $this->orderInvoicePdf();
5702 return $this->shopLegalCmsPage(
5705 'Diese Seite kommt aus dem CMS über den stabilen Permalink /shop-rechtstexte. Inhalte wie Anbieterkennzeichnung, AGB, Zahlung, Versand und Datenschutz-Hinweise werden dort gepflegt.',
5710 return $this->returns();
5713 return $this->settings();
5715 case 'payment_test':
5716 return $this->paymentTest();
5719 return $this->placeholder(
'Unbekannter Shop-Aufruf',
'dbx_run1=' .
$run .
' ist im Shop-Admin-Skeleton noch nicht definiert.');