Harden admin ops: integrations UI, uploads, pick/analytics polish.
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
Move PayU and carrier credentials into Settings, raise nginx upload limits, fix analytics Query translation, and improve orders pick/channel UX plus missing i18n. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
da85370936
commit
bca7ef9dfa
36 files changed
+4508
-236
No files matched your search
@@ -4,6 +4,11 @@ server {
|
|||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
|
# Product image uploads are up to 10 MB (API RequestSizeLimit).
|
||||||
|
# Nginx default is 1m and returns 413 for larger bodies.
|
||||||
|
client_max_body_size 12m;
|
||||||
|
client_body_timeout 120s;
|
||||||
|
|
||||||
# Browser calls same-origin /api and /uploads; proxy to API container.
|
# Browser calls same-origin /api and /uploads; proxy to API container.
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://api:8080/api/;
|
proxy_pass http://api:8080/api/;
|
||||||
@@ -12,6 +17,10 @@ server {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_request_buffering on;
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 120s;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /uploads/ {
|
location /uploads/ {
|
||||||
|
|||||||
@@ -53,14 +53,40 @@ export const CS_TRANSLATIONS: TranslationMap = {
|
|||||||
'dashboard.errorLoad': 'Nepodařilo se načíst dashboard',
|
'dashboard.errorLoad': 'Nepodařilo se načíst dashboard',
|
||||||
'dashboard.alertLowStock': 'SKU s nízkým stavem',
|
'dashboard.alertLowStock': 'SKU s nízkým stavem',
|
||||||
'dashboard.alertPending': 'čeká na platbu',
|
'dashboard.alertPending': 'čeká na platbu',
|
||||||
|
'dashboard.alertReviews': 'recenzí k moderaci',
|
||||||
|
'dashboard.viewOrders': 'Zobrazit objednávky',
|
||||||
|
'dashboard.openPickList': 'Otevřít pick list',
|
||||||
|
'dashboard.pendingFulfillment': 'K vyřízení',
|
||||||
|
'dashboard.pendingFulfillmentHint': 'Vyžaduje pozornost',
|
||||||
|
'dashboard.lowStockHint': 'Práh ≤ 5',
|
||||||
|
'dashboard.needsAction': 'Vyžaduje akci — objednávky',
|
||||||
|
'dashboard.pendingReviews': 'Recenze k moderaci',
|
||||||
|
'dashboard.moderate': 'Moderovat',
|
||||||
|
'dashboard.restock': 'Doplnit',
|
||||||
|
'dashboard.noActionOrders': 'Žádné objednávky nevyžadují akci.',
|
||||||
|
'dashboard.noLowStock': 'Stavy skladu vypadají v pořádku.',
|
||||||
|
'dashboard.noPendingReviews': 'Žádné recenze nečekají.',
|
||||||
|
'orders.purpose':
|
||||||
|
'Vyřízení objednávek — vyberte více pro kompletaci, faktury nebo štítky.',
|
||||||
|
'orders.order': 'Objednávka',
|
||||||
'orders.bulkSelected': 'Vybráno {count}',
|
'orders.bulkSelected': 'Vybráno {count}',
|
||||||
'orders.bulkPick': 'Kompletovat / pick',
|
'orders.bulkPick': 'Kompletovat / pick',
|
||||||
'orders.bulkInvoices': 'Generovat faktury',
|
'orders.bulkInvoices': 'Generovat faktury',
|
||||||
'orders.bulkLabels': 'Generovat štítky',
|
'orders.bulkLabels': 'Generovat štítky',
|
||||||
'orders.bulkLabelsSoon': 'Hromadné štítky vygenerují přepravní štítky pro výběr.',
|
'orders.bulkLabelsSoon': 'Hromadné štítky vygenerují přepravní štítky pro výběr.',
|
||||||
|
'orders.bulkLabelsEmpty': 'Nejdřív vyberte objednávky',
|
||||||
|
'orders.bulkLabelsDone':
|
||||||
|
'Štítky: {{ok}} vytvořeno, {{fail}} chyb, {{skip}} přeskočeno',
|
||||||
|
'orders.bulkLabelsError': 'Hromadné generování štítků selhalo',
|
||||||
'orders.selectAll': 'Vybrat vše',
|
'orders.selectAll': 'Vybrat vše',
|
||||||
'orders.selectRow': 'Vybrat objednávku',
|
'orders.selectRow': 'Vybrat objednávku',
|
||||||
'products.title': 'Produkty',
|
'products.title': 'Produkty',
|
||||||
|
'products.purpose': 'Správa prodejního katalogu a skladů.',
|
||||||
|
'promotions.purpose': 'Kampaně a promo kódy bez zmatku.',
|
||||||
|
'newsletter.purpose':
|
||||||
|
'Komunikace s odběrateli — sestavení, náhled, bezpečné odeslání.',
|
||||||
|
'reviews.purpose':
|
||||||
|
'Fronta moderace — ochrana důvěry na obchodě.',
|
||||||
'products.add': 'Přidat produkt',
|
'products.add': 'Přidat produkt',
|
||||||
'products.edit': 'Upravit',
|
'products.edit': 'Upravit',
|
||||||
'products.editTitle': 'Upravit produkt',
|
'products.editTitle': 'Upravit produkt',
|
||||||
@@ -237,6 +263,11 @@ export const CS_TRANSLATIONS: TranslationMap = {
|
|||||||
'orders.markCollected': 'Označit jako sestavené',
|
'orders.markCollected': 'Označit jako sestavené',
|
||||||
'orders.collected': 'Sestaveno',
|
'orders.collected': 'Sestaveno',
|
||||||
'orders.showCollected': 'Zobrazit sestavené',
|
'orders.showCollected': 'Zobrazit sestavené',
|
||||||
|
'orders.pickByOrder': 'Podle objednávek',
|
||||||
|
'orders.pickByProduct': 'Podle produktů',
|
||||||
|
'orders.pickTotalQty': 'Celkem k vyzvednutí: {{count}}',
|
||||||
|
'orders.collectProduct': 'Kompletovat všechny',
|
||||||
|
'orders.productCollected': 'Všechny kompletovány',
|
||||||
'orders.pendingItems': 'Čekající',
|
'orders.pendingItems': 'Čekající',
|
||||||
'orders.collectedItems': 'Sestavené',
|
'orders.collectedItems': 'Sestavené',
|
||||||
'orders.orderWarehouse': 'Sklad pro objednávku',
|
'orders.orderWarehouse': 'Sklad pro objednávku',
|
||||||
@@ -294,6 +325,7 @@ export const CS_TRANSLATIONS: TranslationMap = {
|
|||||||
'promotions.kpiActive': 'Aktivní akce',
|
'promotions.kpiActive': 'Aktivní akce',
|
||||||
'promotions.kpiRedemptions': 'Uplatnění (stránka)',
|
'promotions.kpiRedemptions': 'Uplatnění (stránka)',
|
||||||
'promotions.kpiCodes': 'Kódy (stránka)',
|
'promotions.kpiCodes': 'Kódy (stránka)',
|
||||||
|
'promotions.kpiDiscount': 'Poskytnuté slevy (30 dní)',
|
||||||
'promotions.sectionBenefit': 'Výhoda',
|
'promotions.sectionBenefit': 'Výhoda',
|
||||||
'promotions.sectionRequirements': 'Požadavky',
|
'promotions.sectionRequirements': 'Požadavky',
|
||||||
'promotions.sectionLimits': 'Limity použití',
|
'promotions.sectionLimits': 'Limity použití',
|
||||||
@@ -330,6 +362,7 @@ export const CS_TRANSLATIONS: TranslationMap = {
|
|||||||
'newsletter.searchCampaign': 'Hledat kampaně…',
|
'newsletter.searchCampaign': 'Hledat kampaně…',
|
||||||
'newsletter.joined': 'Připojen',
|
'newsletter.joined': 'Připojen',
|
||||||
'newsletter.sent': 'Odesláno',
|
'newsletter.sent': 'Odesláno',
|
||||||
|
'newsletter.recipients': 'Příjemci',
|
||||||
'newsletter.unsubscribed': 'Odhlášen',
|
'newsletter.unsubscribed': 'Odhlášen',
|
||||||
'newsletter.status.sent': 'Odesláno',
|
'newsletter.status.sent': 'Odesláno',
|
||||||
'newsletter.status.draft': 'Koncept',
|
'newsletter.status.draft': 'Koncept',
|
||||||
@@ -429,6 +462,8 @@ export const CS_TRANSLATIONS: TranslationMap = {
|
|||||||
'settings.title': 'Nastavení',
|
'settings.title': 'Nastavení',
|
||||||
'settings.purpose':
|
'settings.purpose':
|
||||||
'Konfigurace obchodu — lokálně, dokud nebudou API nastavení.',
|
'Konfigurace obchodu — lokálně, dokud nebudou API nastavení.',
|
||||||
|
'settings.errorLoad': 'Nepodařilo se načíst nastavení',
|
||||||
|
'settings.errorSave': 'Nepodařilo se uložit nastavení',
|
||||||
'settings.save': 'Uložit změny',
|
'settings.save': 'Uložit změny',
|
||||||
'settings.navStore': 'Profil obchodu',
|
'settings.navStore': 'Profil obchodu',
|
||||||
'settings.navLocales': 'Jazyky a měna',
|
'settings.navLocales': 'Jazyky a měna',
|
||||||
@@ -437,10 +472,36 @@ export const CS_TRANSLATIONS: TranslationMap = {
|
|||||||
'settings.navIntegrations': 'Integrace',
|
'settings.navIntegrations': 'Integrace',
|
||||||
'settings.navEmail': 'E-mail',
|
'settings.navEmail': 'E-mail',
|
||||||
'settings.navAccount': 'Můj účet',
|
'settings.navAccount': 'Můj účet',
|
||||||
'settings.integrationsTitle': 'Marketplace a faktury',
|
'settings.integrationsTitle': 'Platby, dopravci a marketplace',
|
||||||
'settings.integrationsHint':
|
'settings.integrationsHint':
|
||||||
'Nastavení Allegro, Amazon a iFirma. Sync objednávek a auto-faktury použijí tato nastavení po zapnutí konektorů.',
|
'PayU, InPost, DHL, DPD, Allegro, Amazon a iFirma — uloženo v databázi. Prázdné tajné pole ponechá uloženou hodnotu.',
|
||||||
'settings.secretSaved': 'uloženo',
|
'settings.secretSaved': 'uloženo',
|
||||||
|
'settings.shippingSandbox': 'Použít sandbox / testovací API dopravy',
|
||||||
|
'settings.live': 'Produkce',
|
||||||
|
'settings.payuPosId': 'PayU POS ID',
|
||||||
|
'settings.payuClientId': 'PayU client ID',
|
||||||
|
'settings.payuClientSecret': 'PayU client secret',
|
||||||
|
'settings.payuSecondKey': 'PayU second key (MD5)',
|
||||||
|
'settings.payuOauthUrl': 'PayU OAuth URL',
|
||||||
|
'settings.payuOrdersUrl': 'PayU orders URL',
|
||||||
|
'settings.payuNotifyUrl': 'PayU notify (webhook) URL',
|
||||||
|
'settings.payuContinueUrl': 'PayU continue URL (základ)',
|
||||||
|
'settings.inPostOrgId': 'InPost organization ID',
|
||||||
|
'settings.inPostShipXToken': 'InPost ShipX token',
|
||||||
|
'settings.inPostShipXBaseUrl': 'InPost ShipX base URL',
|
||||||
|
'settings.inPostGeowidgetConfig': 'Geowidget config',
|
||||||
|
'settings.inPostGeowidgetSdk': 'Geowidget SDK base URL',
|
||||||
|
'settings.inPostGeowidgetPublic': 'Geowidget public token',
|
||||||
|
'settings.inPostGeowidgetSandbox': 'Geowidget sandbox token',
|
||||||
|
'settings.dhlApiKey': 'DHL API key',
|
||||||
|
'settings.dhlApiSecret': 'DHL API secret',
|
||||||
|
'settings.dhlApiBaseUrl': 'DHL API base URL',
|
||||||
|
'settings.dhlWidgetUrl': 'DHL mapa Parcelshop',
|
||||||
|
'settings.dpdLogin': 'DPD login',
|
||||||
|
'settings.dpdPassword': 'DPD heslo',
|
||||||
|
'settings.dpdFid': 'DPD FID',
|
||||||
|
'settings.dpdApiBaseUrl': 'DPD API base URL',
|
||||||
|
'settings.dpdWidgetUrl': 'DPD pudofinder URL',
|
||||||
'settings.allegroClientId': 'Allegro client ID',
|
'settings.allegroClientId': 'Allegro client ID',
|
||||||
'settings.allegroClientSecret': 'Allegro client secret',
|
'settings.allegroClientSecret': 'Allegro client secret',
|
||||||
'settings.allegroSellerId': 'Allegro seller ID',
|
'settings.allegroSellerId': 'Allegro seller ID',
|
||||||
@@ -514,7 +575,7 @@ export const CS_TRANSLATIONS: TranslationMap = {
|
|||||||
'settings.mode': 'Režim',
|
'settings.mode': 'Režim',
|
||||||
'settings.sandbox': 'Sandbox',
|
'settings.sandbox': 'Sandbox',
|
||||||
'settings.shippingHint':
|
'settings.shippingHint':
|
||||||
'Přihlašovací údaje zůstávají v konfiguraci serveru — zde jen flagy viditelnosti.',
|
'Dopravce zapněte zde. API tokeny a URL widgetů nastavíte v Integracích.',
|
||||||
'settings.emailTitle': 'E-mail / newsletter',
|
'settings.emailTitle': 'E-mail / newsletter',
|
||||||
'settings.fromName': 'Jméno odesílatele',
|
'settings.fromName': 'Jméno odesílatele',
|
||||||
'settings.fromAddress': 'Adresa odesílatele',
|
'settings.fromAddress': 'Adresa odesílatele',
|
||||||
|
|||||||
@@ -53,14 +53,40 @@ export const DE_TRANSLATIONS: TranslationMap = {
|
|||||||
'dashboard.errorLoad': 'Dashboard konnte nicht geladen werden',
|
'dashboard.errorLoad': 'Dashboard konnte nicht geladen werden',
|
||||||
'dashboard.alertLowStock': 'SKU mit niedrigem Bestand',
|
'dashboard.alertLowStock': 'SKU mit niedrigem Bestand',
|
||||||
'dashboard.alertPending': 'warten auf Zahlung',
|
'dashboard.alertPending': 'warten auf Zahlung',
|
||||||
|
'dashboard.alertReviews': 'Bewertungen zur Moderation',
|
||||||
|
'dashboard.viewOrders': 'Bestellungen anzeigen',
|
||||||
|
'dashboard.openPickList': 'Kommissionierung öffnen',
|
||||||
|
'dashboard.pendingFulfillment': 'Offene Erfüllung',
|
||||||
|
'dashboard.pendingFulfillmentHint': 'Braucht Aufmerksamkeit',
|
||||||
|
'dashboard.lowStockHint': 'Schwelle ≤ 5',
|
||||||
|
'dashboard.needsAction': 'Handlungsbedarf — Bestellungen',
|
||||||
|
'dashboard.pendingReviews': 'Offene Bewertungen',
|
||||||
|
'dashboard.moderate': 'Moderieren',
|
||||||
|
'dashboard.restock': 'Auffüllen',
|
||||||
|
'dashboard.noActionOrders': 'Keine Bestellungen brauchen gerade Aktion.',
|
||||||
|
'dashboard.noLowStock': 'Bestände sehen gesund aus.',
|
||||||
|
'dashboard.noPendingReviews': 'Keine Bewertungen warten.',
|
||||||
|
'orders.purpose':
|
||||||
|
'Erfüllen und klären — mehrere auswählen zum Kommissionieren, Rechnungen oder Labels.',
|
||||||
|
'orders.order': 'Bestellung',
|
||||||
'orders.bulkSelected': '{count} ausgewählt',
|
'orders.bulkSelected': '{count} ausgewählt',
|
||||||
'orders.bulkPick': 'Kommissionieren',
|
'orders.bulkPick': 'Kommissionieren',
|
||||||
'orders.bulkInvoices': 'Rechnungen erzeugen',
|
'orders.bulkInvoices': 'Rechnungen erzeugen',
|
||||||
'orders.bulkLabels': 'Labels erzeugen',
|
'orders.bulkLabels': 'Labels erzeugen',
|
||||||
'orders.bulkLabelsSoon': 'Massen-Labels erzeugen Versandetiketten für die Auswahl.',
|
'orders.bulkLabelsSoon': 'Massen-Labels erzeugen Versandetiketten für die Auswahl.',
|
||||||
|
'orders.bulkLabelsEmpty': 'Zuerst Bestellungen auswählen',
|
||||||
|
'orders.bulkLabelsDone':
|
||||||
|
'Labels: {{ok}} erstellt, {{fail}} fehlgeschlagen, {{skip}} übersprungen',
|
||||||
|
'orders.bulkLabelsError': 'Massen-Labels fehlgeschlagen',
|
||||||
'orders.selectAll': 'Alle auswählen',
|
'orders.selectAll': 'Alle auswählen',
|
||||||
'orders.selectRow': 'Bestellung auswählen',
|
'orders.selectRow': 'Bestellung auswählen',
|
||||||
'products.title': 'Produkte',
|
'products.title': 'Produkte',
|
||||||
|
'products.purpose': 'Verkaufskatalog und Lager verwalten.',
|
||||||
|
'promotions.purpose': 'Kampagnen und Codes ohne Verwirrung führen.',
|
||||||
|
'newsletter.purpose':
|
||||||
|
'Mit Abonnenten kommunizieren — verfassen, Vorschau, sicher senden.',
|
||||||
|
'reviews.purpose':
|
||||||
|
'Moderationswarteschlange — Vertrauen im Shop schützen.',
|
||||||
'products.add': 'Produkt hinzufügen',
|
'products.add': 'Produkt hinzufügen',
|
||||||
'products.edit': 'Bearbeiten',
|
'products.edit': 'Bearbeiten',
|
||||||
'products.editTitle': 'Produkt bearbeiten',
|
'products.editTitle': 'Produkt bearbeiten',
|
||||||
@@ -237,6 +263,11 @@ export const DE_TRANSLATIONS: TranslationMap = {
|
|||||||
'orders.markCollected': 'Kommissioniert',
|
'orders.markCollected': 'Kommissioniert',
|
||||||
'orders.collected': 'Kommissioniert',
|
'orders.collected': 'Kommissioniert',
|
||||||
'orders.showCollected': 'Kommissionierte anzeigen',
|
'orders.showCollected': 'Kommissionierte anzeigen',
|
||||||
|
'orders.pickByOrder': 'Nach Bestellung',
|
||||||
|
'orders.pickByProduct': 'Nach Produkt',
|
||||||
|
'orders.pickTotalQty': 'Gesamt zu holen: {{count}}',
|
||||||
|
'orders.collectProduct': 'Alle Positionen holen',
|
||||||
|
'orders.productCollected': 'Alle Positionen geholt',
|
||||||
'orders.pendingItems': 'Offen',
|
'orders.pendingItems': 'Offen',
|
||||||
'orders.collectedItems': 'Kommissioniert',
|
'orders.collectedItems': 'Kommissioniert',
|
||||||
'orders.orderWarehouse': 'Lager für Bestellung',
|
'orders.orderWarehouse': 'Lager für Bestellung',
|
||||||
@@ -294,6 +325,7 @@ export const DE_TRANSLATIONS: TranslationMap = {
|
|||||||
'promotions.kpiActive': 'Aktive Aktionen',
|
'promotions.kpiActive': 'Aktive Aktionen',
|
||||||
'promotions.kpiRedemptions': 'Einlösungen (Seite)',
|
'promotions.kpiRedemptions': 'Einlösungen (Seite)',
|
||||||
'promotions.kpiCodes': 'Codes (Seite)',
|
'promotions.kpiCodes': 'Codes (Seite)',
|
||||||
|
'promotions.kpiDiscount': 'Gewährte Rabatte (30 Tage)',
|
||||||
'promotions.sectionBenefit': 'Vorteil',
|
'promotions.sectionBenefit': 'Vorteil',
|
||||||
'promotions.sectionRequirements': 'Voraussetzungen',
|
'promotions.sectionRequirements': 'Voraussetzungen',
|
||||||
'promotions.sectionLimits': 'Nutzungslimits',
|
'promotions.sectionLimits': 'Nutzungslimits',
|
||||||
@@ -330,6 +362,7 @@ export const DE_TRANSLATIONS: TranslationMap = {
|
|||||||
'newsletter.searchCampaign': 'Kampagnen suchen…',
|
'newsletter.searchCampaign': 'Kampagnen suchen…',
|
||||||
'newsletter.joined': 'Beigetreten',
|
'newsletter.joined': 'Beigetreten',
|
||||||
'newsletter.sent': 'Gesendet',
|
'newsletter.sent': 'Gesendet',
|
||||||
|
'newsletter.recipients': 'Empfänger',
|
||||||
'newsletter.unsubscribed': 'Abgemeldet',
|
'newsletter.unsubscribed': 'Abgemeldet',
|
||||||
'newsletter.status.sent': 'Gesendet',
|
'newsletter.status.sent': 'Gesendet',
|
||||||
'newsletter.status.draft': 'Entwurf',
|
'newsletter.status.draft': 'Entwurf',
|
||||||
@@ -429,6 +462,8 @@ export const DE_TRANSLATIONS: TranslationMap = {
|
|||||||
'settings.title': 'Einstellungen',
|
'settings.title': 'Einstellungen',
|
||||||
'settings.purpose':
|
'settings.purpose':
|
||||||
'Shop-Konfiguration — lokal gespeichert bis Settings-APIs existieren.',
|
'Shop-Konfiguration — lokal gespeichert bis Settings-APIs existieren.',
|
||||||
|
'settings.errorLoad': 'Einstellungen konnten nicht geladen werden',
|
||||||
|
'settings.errorSave': 'Einstellungen konnten nicht gespeichert werden',
|
||||||
'settings.save': 'Änderungen speichern',
|
'settings.save': 'Änderungen speichern',
|
||||||
'settings.navStore': 'Shop-Profil',
|
'settings.navStore': 'Shop-Profil',
|
||||||
'settings.navLocales': 'Sprachen & Währung',
|
'settings.navLocales': 'Sprachen & Währung',
|
||||||
@@ -437,10 +472,36 @@ export const DE_TRANSLATIONS: TranslationMap = {
|
|||||||
'settings.navIntegrations': 'Integrationen',
|
'settings.navIntegrations': 'Integrationen',
|
||||||
'settings.navEmail': 'E-Mail',
|
'settings.navEmail': 'E-Mail',
|
||||||
'settings.navAccount': 'Mein Konto',
|
'settings.navAccount': 'Mein Konto',
|
||||||
'settings.integrationsTitle': 'Marktplätze & Rechnungen',
|
'settings.integrationsTitle': 'Zahlungen, Carrier & Marktplätze',
|
||||||
'settings.integrationsHint':
|
'settings.integrationsHint':
|
||||||
'Allegro-, Amazon- und iFirma-Zugangsdaten. Sync und Auto-Rechnung nutzen diese Einstellungen, sobald die Connectoren aktiv sind.',
|
'PayU, InPost, DHL, DPD, Allegro, Amazon und iFirma — in der Datenbank. Leeres Geheimnisfeld behält den gespeicherten Wert.',
|
||||||
'settings.secretSaved': 'gespeichert',
|
'settings.secretSaved': 'gespeichert',
|
||||||
|
'settings.shippingSandbox': 'Shipping-Sandbox / Test-APIs nutzen',
|
||||||
|
'settings.live': 'Live',
|
||||||
|
'settings.payuPosId': 'PayU POS-ID',
|
||||||
|
'settings.payuClientId': 'PayU Client-ID',
|
||||||
|
'settings.payuClientSecret': 'PayU Client-Secret',
|
||||||
|
'settings.payuSecondKey': 'PayU Second Key (MD5)',
|
||||||
|
'settings.payuOauthUrl': 'PayU OAuth-URL',
|
||||||
|
'settings.payuOrdersUrl': 'PayU Orders-URL',
|
||||||
|
'settings.payuNotifyUrl': 'PayU Notify-(Webhook)-URL',
|
||||||
|
'settings.payuContinueUrl': 'PayU Continue-URL (Basis)',
|
||||||
|
'settings.inPostOrgId': 'InPost Organisations-ID',
|
||||||
|
'settings.inPostShipXToken': 'InPost ShipX-Token',
|
||||||
|
'settings.inPostShipXBaseUrl': 'InPost ShipX Base-URL',
|
||||||
|
'settings.inPostGeowidgetConfig': 'Geowidget-Config',
|
||||||
|
'settings.inPostGeowidgetSdk': 'Geowidget SDK Base-URL',
|
||||||
|
'settings.inPostGeowidgetPublic': 'Geowidget Public-Token',
|
||||||
|
'settings.inPostGeowidgetSandbox': 'Geowidget Sandbox-Token',
|
||||||
|
'settings.dhlApiKey': 'DHL API-Key',
|
||||||
|
'settings.dhlApiSecret': 'DHL API-Secret',
|
||||||
|
'settings.dhlApiBaseUrl': 'DHL API Base-URL',
|
||||||
|
'settings.dhlWidgetUrl': 'DHL Parcelshop-Karten-URL',
|
||||||
|
'settings.dpdLogin': 'DPD Login',
|
||||||
|
'settings.dpdPassword': 'DPD Passwort',
|
||||||
|
'settings.dpdFid': 'DPD FID',
|
||||||
|
'settings.dpdApiBaseUrl': 'DPD API Base-URL',
|
||||||
|
'settings.dpdWidgetUrl': 'DPD Pudofinder-URL',
|
||||||
'settings.allegroClientId': 'Allegro Client-ID',
|
'settings.allegroClientId': 'Allegro Client-ID',
|
||||||
'settings.allegroClientSecret': 'Allegro Client-Secret',
|
'settings.allegroClientSecret': 'Allegro Client-Secret',
|
||||||
'settings.allegroSellerId': 'Allegro Verkäufer-ID',
|
'settings.allegroSellerId': 'Allegro Verkäufer-ID',
|
||||||
@@ -514,7 +575,7 @@ export const DE_TRANSLATIONS: TranslationMap = {
|
|||||||
'settings.mode': 'Modus',
|
'settings.mode': 'Modus',
|
||||||
'settings.sandbox': 'Sandbox',
|
'settings.sandbox': 'Sandbox',
|
||||||
'settings.shippingHint':
|
'settings.shippingHint':
|
||||||
'Zugangsdaten bleiben in der Server-Konfiguration — hier nur Sichtbarkeitsflags.',
|
'Carrier hier aktivieren. API-Tokens und Widget-URLs unter Integrationen.',
|
||||||
'settings.emailTitle': 'E-Mail / Newsletter',
|
'settings.emailTitle': 'E-Mail / Newsletter',
|
||||||
'settings.fromName': 'Absendername',
|
'settings.fromName': 'Absendername',
|
||||||
'settings.fromAddress': 'Absenderadresse',
|
'settings.fromAddress': 'Absenderadresse',
|
||||||
|
|||||||
@@ -261,6 +261,11 @@ export const EN_TRANSLATIONS: TranslationMap = {
|
|||||||
'orders.markCollected': 'Mark collected',
|
'orders.markCollected': 'Mark collected',
|
||||||
'orders.collected': 'Collected',
|
'orders.collected': 'Collected',
|
||||||
'orders.showCollected': 'Show collected',
|
'orders.showCollected': 'Show collected',
|
||||||
|
'orders.pickByOrder': 'By order',
|
||||||
|
'orders.pickByProduct': 'By product',
|
||||||
|
'orders.pickTotalQty': 'Total to pick: {{count}}',
|
||||||
|
'orders.collectProduct': 'Collect all lines',
|
||||||
|
'orders.productCollected': 'All lines collected',
|
||||||
'orders.pendingItems': 'Pending',
|
'orders.pendingItems': 'Pending',
|
||||||
'orders.collectedItems': 'Collected',
|
'orders.collectedItems': 'Collected',
|
||||||
'orders.orderWarehouse': 'Warehouse for order',
|
'orders.orderWarehouse': 'Warehouse for order',
|
||||||
@@ -463,10 +468,36 @@ export const EN_TRANSLATIONS: TranslationMap = {
|
|||||||
'settings.navIntegrations': 'Integrations',
|
'settings.navIntegrations': 'Integrations',
|
||||||
'settings.navEmail': 'Email',
|
'settings.navEmail': 'Email',
|
||||||
'settings.navAccount': 'My account',
|
'settings.navAccount': 'My account',
|
||||||
'settings.integrationsTitle': 'Marketplace & invoicing',
|
'settings.integrationsTitle': 'Payments, carriers & marketplaces',
|
||||||
'settings.integrationsHint':
|
'settings.integrationsHint':
|
||||||
'Configure Allegro, Amazon, and iFirma credentials. Order sync and auto-invoicing use these settings once connectors are live.',
|
'PayU, InPost, DHL, DPD, Allegro, Amazon, and iFirma — stored in the database. Leave a secret blank to keep the saved value.',
|
||||||
'settings.secretSaved': 'saved',
|
'settings.secretSaved': 'saved',
|
||||||
|
'settings.shippingSandbox': 'Use shipping sandbox / test APIs',
|
||||||
|
'settings.live': 'Live',
|
||||||
|
'settings.payuPosId': 'PayU POS ID',
|
||||||
|
'settings.payuClientId': 'PayU client ID',
|
||||||
|
'settings.payuClientSecret': 'PayU client secret',
|
||||||
|
'settings.payuSecondKey': 'PayU second key (MD5)',
|
||||||
|
'settings.payuOauthUrl': 'PayU OAuth URL',
|
||||||
|
'settings.payuOrdersUrl': 'PayU orders URL',
|
||||||
|
'settings.payuNotifyUrl': 'PayU notify (webhook) URL',
|
||||||
|
'settings.payuContinueUrl': 'PayU continue URL base',
|
||||||
|
'settings.inPostOrgId': 'InPost organization ID',
|
||||||
|
'settings.inPostShipXToken': 'InPost ShipX token',
|
||||||
|
'settings.inPostShipXBaseUrl': 'InPost ShipX base URL',
|
||||||
|
'settings.inPostGeowidgetConfig': 'Geowidget config',
|
||||||
|
'settings.inPostGeowidgetSdk': 'Geowidget SDK base URL',
|
||||||
|
'settings.inPostGeowidgetPublic': 'Geowidget public token',
|
||||||
|
'settings.inPostGeowidgetSandbox': 'Geowidget sandbox token',
|
||||||
|
'settings.dhlApiKey': 'DHL API key',
|
||||||
|
'settings.dhlApiSecret': 'DHL API secret',
|
||||||
|
'settings.dhlApiBaseUrl': 'DHL API base URL',
|
||||||
|
'settings.dhlWidgetUrl': 'DHL parcel shop map URL',
|
||||||
|
'settings.dpdLogin': 'DPD login',
|
||||||
|
'settings.dpdPassword': 'DPD password',
|
||||||
|
'settings.dpdFid': 'DPD FID',
|
||||||
|
'settings.dpdApiBaseUrl': 'DPD API base URL',
|
||||||
|
'settings.dpdWidgetUrl': 'DPD pudofinder URL',
|
||||||
'settings.allegroClientId': 'Allegro client ID',
|
'settings.allegroClientId': 'Allegro client ID',
|
||||||
'settings.allegroClientSecret': 'Allegro client secret',
|
'settings.allegroClientSecret': 'Allegro client secret',
|
||||||
'settings.allegroSellerId': 'Allegro seller ID',
|
'settings.allegroSellerId': 'Allegro seller ID',
|
||||||
@@ -540,7 +571,7 @@ export const EN_TRANSLATIONS: TranslationMap = {
|
|||||||
'settings.mode': 'Mode',
|
'settings.mode': 'Mode',
|
||||||
'settings.sandbox': 'Sandbox',
|
'settings.sandbox': 'Sandbox',
|
||||||
'settings.shippingHint':
|
'settings.shippingHint':
|
||||||
'Credentials stay in server config — this UI only stores visibility flags locally.',
|
'Enable carriers here. API tokens and widget URLs are configured under Integrations.',
|
||||||
'settings.emailTitle': 'Email / newsletter',
|
'settings.emailTitle': 'Email / newsletter',
|
||||||
'settings.fromName': 'From name',
|
'settings.fromName': 'From name',
|
||||||
'settings.fromAddress': 'From address',
|
'settings.fromAddress': 'From address',
|
||||||
|
|||||||
@@ -53,6 +53,20 @@ export const PL_TRANSLATIONS: TranslationMap = {
|
|||||||
'dashboard.errorLoad': 'Nie udało się wczytać pulpitu',
|
'dashboard.errorLoad': 'Nie udało się wczytać pulpitu',
|
||||||
'dashboard.alertLowStock': 'SKU z niskim stanem',
|
'dashboard.alertLowStock': 'SKU z niskim stanem',
|
||||||
'dashboard.alertPending': 'oczekuje na płatność',
|
'dashboard.alertPending': 'oczekuje na płatność',
|
||||||
|
'dashboard.alertReviews': 'opinii do moderacji',
|
||||||
|
'dashboard.viewOrders': 'Zobacz zamówienia',
|
||||||
|
'dashboard.openPickList': 'Otwórz listę kompletacji',
|
||||||
|
'dashboard.pendingFulfillment': 'Do realizacji',
|
||||||
|
'dashboard.pendingFulfillmentHint': 'Wymaga uwagi',
|
||||||
|
'dashboard.lowStockHint': 'Próg ≤ 5',
|
||||||
|
'dashboard.needsAction': 'Wymaga działania — zamówienia',
|
||||||
|
'dashboard.pendingReviews': 'Opinie do moderacji',
|
||||||
|
'dashboard.moderate': 'Moderuj',
|
||||||
|
'dashboard.restock': 'Uzupełnij',
|
||||||
|
'dashboard.noActionOrders': 'Brak zamówień wymagających działania.',
|
||||||
|
'dashboard.noLowStock': 'Stany magazynowe wyglądają dobrze.',
|
||||||
|
'dashboard.noPendingReviews': 'Brak opinii oczekujących.',
|
||||||
|
'orders.order': 'Zamówienie',
|
||||||
'orders.bulkSelected': 'Wybrano {count}',
|
'orders.bulkSelected': 'Wybrano {count}',
|
||||||
'orders.bulkPick': 'Kompletuj / pick',
|
'orders.bulkPick': 'Kompletuj / pick',
|
||||||
'orders.bulkInvoices': 'Generuj faktury',
|
'orders.bulkInvoices': 'Generuj faktury',
|
||||||
@@ -247,6 +261,11 @@ export const PL_TRANSLATIONS: TranslationMap = {
|
|||||||
'orders.markCollected': 'Skompletowano',
|
'orders.markCollected': 'Skompletowano',
|
||||||
'orders.collected': 'Skompletowane',
|
'orders.collected': 'Skompletowane',
|
||||||
'orders.showCollected': 'Pokaż skompletowane',
|
'orders.showCollected': 'Pokaż skompletowane',
|
||||||
|
'orders.pickByOrder': 'Wg zamówień',
|
||||||
|
'orders.pickByProduct': 'Wg produktów',
|
||||||
|
'orders.pickTotalQty': 'Razem do pobrania: {{count}}',
|
||||||
|
'orders.collectProduct': 'Skompletuj wszystkie',
|
||||||
|
'orders.productCollected': 'Wszystkie skompletowane',
|
||||||
'orders.pendingItems': 'Do kompletacji',
|
'orders.pendingItems': 'Do kompletacji',
|
||||||
'orders.collectedItems': 'Skompletowane',
|
'orders.collectedItems': 'Skompletowane',
|
||||||
'orders.orderWarehouse': 'Magazyn dla zamówienia',
|
'orders.orderWarehouse': 'Magazyn dla zamówienia',
|
||||||
@@ -341,6 +360,7 @@ export const PL_TRANSLATIONS: TranslationMap = {
|
|||||||
'newsletter.searchCampaign': 'Szukaj kampanii…',
|
'newsletter.searchCampaign': 'Szukaj kampanii…',
|
||||||
'newsletter.joined': 'Dołączył',
|
'newsletter.joined': 'Dołączył',
|
||||||
'newsletter.sent': 'Wysłano',
|
'newsletter.sent': 'Wysłano',
|
||||||
|
'newsletter.recipients': 'Odbiorcy',
|
||||||
'newsletter.unsubscribed': 'Wypisany',
|
'newsletter.unsubscribed': 'Wypisany',
|
||||||
'newsletter.status.sent': 'Wysłana',
|
'newsletter.status.sent': 'Wysłana',
|
||||||
'newsletter.status.draft': 'Szkic',
|
'newsletter.status.draft': 'Szkic',
|
||||||
@@ -451,10 +471,36 @@ export const PL_TRANSLATIONS: TranslationMap = {
|
|||||||
'settings.navIntegrations': 'Integracje',
|
'settings.navIntegrations': 'Integracje',
|
||||||
'settings.navEmail': 'Email',
|
'settings.navEmail': 'Email',
|
||||||
'settings.navAccount': 'Moje konto',
|
'settings.navAccount': 'Moje konto',
|
||||||
'settings.integrationsTitle': 'Marketplace i faktury',
|
'settings.integrationsTitle': 'Płatności, przewoźnicy i marketplace',
|
||||||
'settings.integrationsHint':
|
'settings.integrationsHint':
|
||||||
'Konfiguracja Allegro, Amazon i iFirma. Synchronizacja zamówień i auto-faktury użyją tych ustawień po włączeniu konektorów.',
|
'PayU, InPost, DHL, DPD, Allegro, Amazon i iFirma — zapis w bazie. Puste tajne pole zostawia dotychczasową wartość.',
|
||||||
'settings.secretSaved': 'zapisany',
|
'settings.secretSaved': 'zapisany',
|
||||||
|
'settings.shippingSandbox': 'Używaj sandbox / testowych API wysyłki',
|
||||||
|
'settings.live': 'Produkcja',
|
||||||
|
'settings.payuPosId': 'PayU POS ID',
|
||||||
|
'settings.payuClientId': 'PayU client ID',
|
||||||
|
'settings.payuClientSecret': 'PayU client secret',
|
||||||
|
'settings.payuSecondKey': 'PayU second key (MD5)',
|
||||||
|
'settings.payuOauthUrl': 'PayU OAuth URL',
|
||||||
|
'settings.payuOrdersUrl': 'PayU orders URL',
|
||||||
|
'settings.payuNotifyUrl': 'PayU notify (webhook) URL',
|
||||||
|
'settings.payuContinueUrl': 'PayU continue URL (baza)',
|
||||||
|
'settings.inPostOrgId': 'InPost organization ID',
|
||||||
|
'settings.inPostShipXToken': 'InPost ShipX token',
|
||||||
|
'settings.inPostShipXBaseUrl': 'InPost ShipX base URL',
|
||||||
|
'settings.inPostGeowidgetConfig': 'Geowidget config',
|
||||||
|
'settings.inPostGeowidgetSdk': 'Geowidget SDK base URL',
|
||||||
|
'settings.inPostGeowidgetPublic': 'Geowidget public token',
|
||||||
|
'settings.inPostGeowidgetSandbox': 'Geowidget sandbox token',
|
||||||
|
'settings.dhlApiKey': 'DHL API key',
|
||||||
|
'settings.dhlApiSecret': 'DHL API secret',
|
||||||
|
'settings.dhlApiBaseUrl': 'DHL API base URL',
|
||||||
|
'settings.dhlWidgetUrl': 'DHL mapa Parcelshop',
|
||||||
|
'settings.dpdLogin': 'DPD login',
|
||||||
|
'settings.dpdPassword': 'DPD hasło',
|
||||||
|
'settings.dpdFid': 'DPD FID',
|
||||||
|
'settings.dpdApiBaseUrl': 'DPD API base URL',
|
||||||
|
'settings.dpdWidgetUrl': 'DPD pudofinder URL',
|
||||||
'settings.allegroClientId': 'Allegro client ID',
|
'settings.allegroClientId': 'Allegro client ID',
|
||||||
'settings.allegroClientSecret': 'Allegro client secret',
|
'settings.allegroClientSecret': 'Allegro client secret',
|
||||||
'settings.allegroSellerId': 'Allegro seller ID',
|
'settings.allegroSellerId': 'Allegro seller ID',
|
||||||
@@ -528,7 +574,7 @@ export const PL_TRANSLATIONS: TranslationMap = {
|
|||||||
'settings.mode': 'Tryb',
|
'settings.mode': 'Tryb',
|
||||||
'settings.sandbox': 'Sandbox',
|
'settings.sandbox': 'Sandbox',
|
||||||
'settings.shippingHint':
|
'settings.shippingHint':
|
||||||
'Dane dostępowe zostają w konfiguracji serwera — tu tylko flagi widoczności.',
|
'Włącz przewoźników tutaj. Tokeny API i URL widgetów ustawisz w Integracjach.',
|
||||||
'settings.emailTitle': 'Email / newsletter',
|
'settings.emailTitle': 'Email / newsletter',
|
||||||
'settings.fromName': 'Nazwa nadawcy',
|
'settings.fromName': 'Nazwa nadawcy',
|
||||||
'settings.fromAddress': 'Adres nadawcy',
|
'settings.fromAddress': 'Adres nadawcy',
|
||||||
|
|||||||
@@ -222,7 +222,11 @@
|
|||||||
>
|
>
|
||||||
{{ 'products.save' | translate }}
|
{{ 'products.save' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<a class="btn btn-secondary" routerLink="/products">
|
<a
|
||||||
|
class="btn btn-secondary"
|
||||||
|
routerLink="/products"
|
||||||
|
[queryParams]="selectedId() ? { categoryId: selectedId() } : {}"
|
||||||
|
>
|
||||||
{{ 'categories.viewProducts' | translate }}
|
{{ 'categories.viewProducts' | translate }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||||
|
import {
|
||||||
|
LucideMapPinned,
|
||||||
|
LucidePackage,
|
||||||
|
LucidePenLine,
|
||||||
|
LucideStore,
|
||||||
|
LucideTag,
|
||||||
|
LucideTruck,
|
||||||
|
} from '@lucide/angular';
|
||||||
|
import { TranslatePipe } from '../../core/i18n/translate.pipe';
|
||||||
|
import {
|
||||||
|
OrderSource,
|
||||||
|
ShippingCarrier,
|
||||||
|
ShippingServiceType,
|
||||||
|
} from './order.models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compact channel + delivery icons for order tables and headers.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-order-channel-icons',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
TranslatePipe,
|
||||||
|
LucideStore,
|
||||||
|
LucideTag,
|
||||||
|
LucidePackage,
|
||||||
|
LucidePenLine,
|
||||||
|
LucideTruck,
|
||||||
|
LucideMapPinned,
|
||||||
|
],
|
||||||
|
template: `
|
||||||
|
<div class="order-meta-icons" [class.order-meta-icons--with-labels]="showLabels()">
|
||||||
|
<span
|
||||||
|
class="meta-icon channel-badge"
|
||||||
|
[attr.data-channel]="channel()"
|
||||||
|
[title]="'orders.source.' + channel() | translate"
|
||||||
|
>
|
||||||
|
@switch (channel()) {
|
||||||
|
@case ('allegro') {
|
||||||
|
<svg lucideTag [size]="iconSize()" aria-hidden="true"></svg>
|
||||||
|
}
|
||||||
|
@case ('amazon') {
|
||||||
|
<svg lucidePackage [size]="iconSize()" aria-hidden="true"></svg>
|
||||||
|
}
|
||||||
|
@case ('manual') {
|
||||||
|
<svg lucidePenLine [size]="iconSize()" aria-hidden="true"></svg>
|
||||||
|
}
|
||||||
|
@default {
|
||||||
|
<svg lucideStore [size]="iconSize()" aria-hidden="true"></svg>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@if (showLabels()) {
|
||||||
|
<span class="meta-icon__label">
|
||||||
|
{{ 'orders.source.' + channel() | translate }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="meta-icon delivery-badge"
|
||||||
|
[attr.data-carrier]="carrier()"
|
||||||
|
[attr.data-service]="service()"
|
||||||
|
[title]="
|
||||||
|
('orders.carrier.' + carrier() | translate) +
|
||||||
|
' · ' +
|
||||||
|
('orders.service.' + service() | translate)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
@if (service() === 'courier') {
|
||||||
|
<svg lucideTruck [size]="iconSize()" aria-hidden="true"></svg>
|
||||||
|
} @else {
|
||||||
|
<svg lucideMapPinned [size]="iconSize()" aria-hidden="true"></svg>
|
||||||
|
}
|
||||||
|
@if (showLabels()) {
|
||||||
|
<span class="meta-icon__label">
|
||||||
|
{{ 'orders.carrier.' + carrier() | translate }}
|
||||||
|
·
|
||||||
|
{{ 'orders.service.' + service() | translate }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class OrderChannelIconsComponent {
|
||||||
|
readonly source = input<OrderSource | string | null | undefined>('shop');
|
||||||
|
readonly shippingCarrier = input<
|
||||||
|
ShippingCarrier | string | null | undefined
|
||||||
|
>('inPost');
|
||||||
|
readonly shippingServiceType = input<
|
||||||
|
ShippingServiceType | string | null | undefined
|
||||||
|
>('parcelLocker');
|
||||||
|
readonly showLabels = input(false);
|
||||||
|
readonly iconSize = input(14);
|
||||||
|
|
||||||
|
protected channel(): OrderSource {
|
||||||
|
const value = this.source() || 'shop';
|
||||||
|
return value as OrderSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected carrier(): ShippingCarrier {
|
||||||
|
const value = this.shippingCarrier() || 'inPost';
|
||||||
|
return value as ShippingCarrier;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected service(): ShippingServiceType {
|
||||||
|
const value = this.shippingServiceType() || 'parcelLocker';
|
||||||
|
return value as ShippingServiceType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,22 +18,14 @@
|
|||||||
}
|
}
|
||||||
</h2>
|
</h2>
|
||||||
@if (detail(); as d) {
|
@if (detail(); as d) {
|
||||||
<div class="order-meta-icons" style="margin-top: 6px">
|
<div style="margin-top: 6px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px">
|
||||||
<span
|
<app-order-channel-icons
|
||||||
class="channel-badge"
|
[source]="d.source"
|
||||||
[attr.data-channel]="d.source || 'shop'"
|
[shippingCarrier]="d.shippingCarrier"
|
||||||
>
|
[shippingServiceType]="d.shippingServiceType"
|
||||||
{{ 'orders.source.' + (d.source || 'shop') | translate }}
|
[showLabels]="true"
|
||||||
</span>
|
[iconSize]="15"
|
||||||
<span
|
/>
|
||||||
class="delivery-badge"
|
|
||||||
[attr.data-carrier]="d.shippingCarrier"
|
|
||||||
[attr.data-service]="d.shippingServiceType"
|
|
||||||
>
|
|
||||||
{{ 'orders.carrier.' + d.shippingCarrier | translate }}
|
|
||||||
·
|
|
||||||
{{ 'orders.service.' + d.shippingServiceType | translate }}
|
|
||||||
</span>
|
|
||||||
@if (d.externalOrderId) {
|
@if (d.externalOrderId) {
|
||||||
<span class="cell-muted text-xs font-mono">
|
<span class="cell-muted text-xs font-mono">
|
||||||
{{ d.externalOrderId }}
|
{{ d.externalOrderId }}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { TranslatePipe } from '../../core/i18n/translate.pipe';
|
|||||||
import { ApiService } from '../../core/api.service';
|
import { ApiService } from '../../core/api.service';
|
||||||
import { API_BASE_URL } from '../../core/constants';
|
import { API_BASE_URL } from '../../core/constants';
|
||||||
import { AdminSkeletonComponent } from '../../shared/admin-skeleton.component';
|
import { AdminSkeletonComponent } from '../../shared/admin-skeleton.component';
|
||||||
|
import { OrderChannelIconsComponent } from './order-channel-icons.component';
|
||||||
import {
|
import {
|
||||||
AdminOrderDetail,
|
AdminOrderDetail,
|
||||||
AdminOrderDetailLine,
|
AdminOrderDetailLine,
|
||||||
@@ -40,6 +41,7 @@ import {
|
|||||||
TranslatePipe,
|
TranslatePipe,
|
||||||
I18nCurrencyPipe,
|
I18nCurrencyPipe,
|
||||||
AdminSkeletonComponent,
|
AdminSkeletonComponent,
|
||||||
|
OrderChannelIconsComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './order-detail-modal.component.html',
|
templateUrl: './order-detail-modal.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { PickListLine, PickListOrderGroup, PickListWarehouseQty } from './order.models';
|
import {
|
||||||
|
PickListLine,
|
||||||
|
PickListOrderGroup,
|
||||||
|
PickListProductGroup,
|
||||||
|
PickListWarehouseQty,
|
||||||
|
} from './order.models';
|
||||||
|
|
||||||
export interface CollectLinePayload {
|
export interface CollectLinePayload {
|
||||||
orderLineId: string;
|
orderLineId: string;
|
||||||
@@ -36,6 +41,66 @@ export function groupPickListByOrder(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregates pick lines by product variant so pickers can
|
||||||
|
* pull total units once (e.g. Gold 1 + Gold 2 → Gold 3).
|
||||||
|
*/
|
||||||
|
export function groupPickListByProduct(
|
||||||
|
items: PickListLine[],
|
||||||
|
): PickListProductGroup[] {
|
||||||
|
const groups = new Map<string, PickListProductGroup>();
|
||||||
|
|
||||||
|
for (const line of items) {
|
||||||
|
const key = line.productVariantId || `${line.sku}|${line.productName}`;
|
||||||
|
const existing = groups.get(key);
|
||||||
|
if (existing) {
|
||||||
|
existing.lines.push(line);
|
||||||
|
existing.totalQuantity += line.quantity;
|
||||||
|
if (line.isCollected) {
|
||||||
|
existing.collectedQuantity += line.quantity;
|
||||||
|
} else {
|
||||||
|
existing.pendingQuantity += line.quantity;
|
||||||
|
}
|
||||||
|
mergeWarehouseStock(existing.warehouseStock, line.warehouseStock);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
groups.set(key, {
|
||||||
|
productVariantId: line.productVariantId,
|
||||||
|
productName: line.productName,
|
||||||
|
variantColor: line.variantColor,
|
||||||
|
sku: line.sku,
|
||||||
|
totalQuantity: line.quantity,
|
||||||
|
pendingQuantity: line.isCollected ? 0 : line.quantity,
|
||||||
|
collectedQuantity: line.isCollected ? line.quantity : 0,
|
||||||
|
lines: [line],
|
||||||
|
warehouseStock: line.warehouseStock.map((ws) => ({ ...ws })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...groups.values()].sort((a, b) => {
|
||||||
|
const byName = a.productName.localeCompare(b.productName);
|
||||||
|
if (byName !== 0) {
|
||||||
|
return byName;
|
||||||
|
}
|
||||||
|
return a.sku.localeCompare(b.sku);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeWarehouseStock(
|
||||||
|
target: PickListWarehouseQty[],
|
||||||
|
incoming: PickListWarehouseQty[],
|
||||||
|
): void {
|
||||||
|
const map = new Map(target.map((ws) => [ws.warehouseId, ws]));
|
||||||
|
for (const ws of incoming) {
|
||||||
|
if (!map.has(ws.warehouseId)) {
|
||||||
|
const copy = { ...ws };
|
||||||
|
map.set(ws.warehouseId, copy);
|
||||||
|
target.push(copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function canCollectPickLine(
|
export function canCollectPickLine(
|
||||||
line: PickListLine,
|
line: PickListLine,
|
||||||
warehouseId: string,
|
warehouseId: string,
|
||||||
|
|||||||
@@ -159,6 +159,18 @@ export interface PickListOrderGroup {
|
|||||||
collectedCount: number;
|
collectedCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PickListProductGroup {
|
||||||
|
productVariantId: string;
|
||||||
|
productName: string;
|
||||||
|
variantColor: string;
|
||||||
|
sku: string;
|
||||||
|
totalQuantity: number;
|
||||||
|
pendingQuantity: number;
|
||||||
|
collectedQuantity: number;
|
||||||
|
lines: PickListLine[];
|
||||||
|
warehouseStock: PickListWarehouseQty[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface OrderInvoice {
|
export interface OrderInvoice {
|
||||||
invoiceNumber: string;
|
invoiceNumber: string;
|
||||||
issueDate: string;
|
issueDate: string;
|
||||||
|
|||||||
@@ -175,33 +175,11 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 font-mono text-xs">{{ o.orderNumber }}</td>
|
<td class="px-4 py-3 font-mono text-xs">{{ o.orderNumber }}</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="order-meta-icons">
|
<app-order-channel-icons
|
||||||
<span
|
[source]="o.source"
|
||||||
class="channel-badge"
|
[shippingCarrier]="o.shippingCarrier"
|
||||||
[attr.data-channel]="o.source || 'shop'"
|
[shippingServiceType]="o.shippingServiceType"
|
||||||
[title]="'orders.source.' + (o.source || 'shop') | translate"
|
/>
|
||||||
>
|
|
||||||
{{ 'orders.sourceShort.' + (o.source || 'shop') | translate }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="delivery-badge"
|
|
||||||
[attr.data-carrier]="o.shippingCarrier || 'inPost'"
|
|
||||||
[attr.data-service]="o.shippingServiceType || 'parcelLocker'"
|
|
||||||
[title]="
|
|
||||||
('orders.carrier.' + (o.shippingCarrier || 'inPost') | translate) +
|
|
||||||
' · ' +
|
|
||||||
('orders.service.' + (o.shippingServiceType || 'parcelLocker') | translate)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ 'orders.carrierShort.' + (o.shippingCarrier || 'inPost') | translate }}
|
|
||||||
·
|
|
||||||
{{
|
|
||||||
o.shippingServiceType === 'courier'
|
|
||||||
? ('orders.serviceShort.courier' | translate)
|
|
||||||
: ('orders.serviceShort.parcelLocker' | translate)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-iled-grey-dark">
|
<td class="px-4 py-3 text-iled-grey-dark">
|
||||||
{{ o.createdAt | date: 'yyyy-MM-dd HH:mm' }}
|
{{ o.createdAt | date: 'yyyy-MM-dd HH:mm' }}
|
||||||
@@ -268,7 +246,25 @@
|
|||||||
· {{ 'orders.pendingItems' | translate }}: {{ pl.pendingCount }}
|
· {{ 'orders.pendingItems' | translate }}: {{ pl.pendingCount }}
|
||||||
· {{ 'orders.collectedItems' | translate }}: {{ pl.collectedCount }}
|
· {{ 'orders.collectedItems' | translate }}: {{ pl.collectedCount }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-2">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<div class="tabs" role="tablist" style="margin: 0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tab"
|
||||||
|
[class.is-active]="pickListMode() === 'order'"
|
||||||
|
(click)="setPickListMode('order')"
|
||||||
|
>
|
||||||
|
{{ 'orders.pickByOrder' | translate }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tab"
|
||||||
|
[class.is-active]="pickListMode() === 'product'"
|
||||||
|
(click)="setPickListMode('product')"
|
||||||
|
>
|
||||||
|
{{ 'orders.pickByProduct' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<label class="flex items-center gap-2 text-sm">
|
<label class="flex items-center gap-2 text-sm">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -287,6 +283,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (pickListMode() === 'order') {
|
||||||
@for (group of pickListGroups(pl.items); track group.orderId) {
|
@for (group of pickListGroups(pl.items); track group.orderId) {
|
||||||
<section class="mb-6 border border-iled-grey-light">
|
<section class="mb-6 border border-iled-grey-light">
|
||||||
<div
|
<div
|
||||||
@@ -419,6 +416,155 @@
|
|||||||
{{ 'orders.pickListEmpty' | translate }}
|
{{ 'orders.pickListEmpty' | translate }}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
|
} @else {
|
||||||
|
@for (
|
||||||
|
group of pickListProductGroups(pl.items);
|
||||||
|
track group.productVariantId || group.sku
|
||||||
|
) {
|
||||||
|
<section class="mb-6 border border-iled-grey-light">
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center justify-between gap-3 border-b border-iled-grey-light bg-iled-bg px-4 py-3"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
{{ group.productName }}
|
||||||
|
@if (group.variantColor) {
|
||||||
|
<span class="font-normal text-iled-grey-dark">
|
||||||
|
({{ group.variantColor }})
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-iled-grey-dark font-mono">{{ group.sku }}</p>
|
||||||
|
<p class="mt-1 text-sm">
|
||||||
|
{{
|
||||||
|
'orders.pickTotalQty'
|
||||||
|
| translate: { count: group.totalQuantity }
|
||||||
|
}}
|
||||||
|
· {{ 'orders.pendingItems' | translate }}:
|
||||||
|
{{ group.pendingQuantity }}
|
||||||
|
· {{ 'orders.collectedItems' | translate }}:
|
||||||
|
{{ group.collectedQuantity }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@if (group.pendingQuantity > 0) {
|
||||||
|
<div class="flex flex-wrap items-end gap-2">
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 block text-xs text-iled-grey-dark">
|
||||||
|
{{ 'orders.orderWarehouse' | translate }}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
class="border border-iled-grey-light px-2 py-1.5 text-xs"
|
||||||
|
[value]="getProductWarehouse(group)"
|
||||||
|
(change)="
|
||||||
|
setProductWarehouse(group, $any($event.target).value)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<option value="">{{ 'orders.selectWarehouse' | translate }}</option>
|
||||||
|
@for (ws of group.warehouseStock; track ws.warehouseId) {
|
||||||
|
<option [value]="ws.warehouseId">
|
||||||
|
{{ ws.warehouseName }} ({{ ws.warehouseCode }}) —
|
||||||
|
{{ ws.quantity }}
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
[disabled]="
|
||||||
|
!canCollectProductGroup(group) ||
|
||||||
|
collectingOrderId() === (group.productVariantId || group.sku)
|
||||||
|
"
|
||||||
|
(click)="handleCollectProduct(group)"
|
||||||
|
>
|
||||||
|
{{ 'orders.collectProduct' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<span class="rounded bg-green-100 px-2 py-1 text-xs font-medium text-green-800">
|
||||||
|
{{ 'orders.productCollected' | translate }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="w-full text-left text-sm">
|
||||||
|
<thead class="border-b border-iled-grey-light">
|
||||||
|
<tr>
|
||||||
|
<th class="px-3 py-2">{{ 'orders.order' | translate }}</th>
|
||||||
|
<th class="px-3 py-2">{{ 'orders.qty' | translate }}</th>
|
||||||
|
<th class="px-3 py-2">{{ 'orders.warehouseStock' | translate }}</th>
|
||||||
|
<th class="px-3 py-2">{{ 'orders.pickFrom' | translate }}</th>
|
||||||
|
<th class="px-3 py-2">{{ 'orders.status' | translate }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (line of group.lines; track line.orderLineId) {
|
||||||
|
<tr
|
||||||
|
class="border-b border-iled-grey-light"
|
||||||
|
[class.bg-green-50]="line.isCollected"
|
||||||
|
>
|
||||||
|
<td class="px-3 py-2 font-mono text-xs">
|
||||||
|
#{{ line.orderNumber }}
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2 font-bold">{{ line.quantity }}</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
@for (ws of line.warehouseStock; track ws.warehouseId) {
|
||||||
|
<div class="text-xs">
|
||||||
|
{{ ws.warehouseName }} ({{ ws.warehouseCode }}):
|
||||||
|
<strong>{{ ws.quantity }}</strong>
|
||||||
|
</div>
|
||||||
|
} @empty {
|
||||||
|
<span class="text-iled-grey-dark">—</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
@if (!line.isCollected) {
|
||||||
|
<select
|
||||||
|
class="border border-iled-grey-light px-2 py-1 text-xs"
|
||||||
|
[value]="getWarehouse(line.orderLineId)"
|
||||||
|
(change)="setWarehouse(line.orderLineId, $any($event.target).value)"
|
||||||
|
>
|
||||||
|
<option value="">{{ 'orders.selectWarehouse' | translate }}</option>
|
||||||
|
@for (ws of line.warehouseStock; track ws.warehouseId) {
|
||||||
|
<option [value]="ws.warehouseId">
|
||||||
|
{{ ws.warehouseName }} ({{ ws.quantity }})
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
} @else if (line.pickedFromWarehouseName) {
|
||||||
|
<span class="text-xs font-medium">
|
||||||
|
{{ line.pickedFromWarehouseName }}
|
||||||
|
({{ line.pickedFromWarehouseCode }})
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
@if (line.isCollected) {
|
||||||
|
<span class="inline-block rounded bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">
|
||||||
|
{{ 'orders.collected' | translate }}
|
||||||
|
</span>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
[disabled]="!canCollect(line) || collectingLineId() === line.orderLineId"
|
||||||
|
(click)="handleCollect(line)"
|
||||||
|
>
|
||||||
|
{{ 'orders.markCollected' | translate }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
} @empty {
|
||||||
|
<p class="py-8 text-center text-iled-grey-dark">
|
||||||
|
{{ 'orders.pickListEmpty' | translate }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe';
|
import { I18nCurrencyPipe } from '../../core/i18n/i18n-currency.pipe';
|
||||||
import { TranslatePipe } from '../../core/i18n/translate.pipe';
|
import { TranslatePipe } from '../../core/i18n/translate.pipe';
|
||||||
import { ApiService } from '../../core/api.service';
|
import { ApiService } from '../../core/api.service';
|
||||||
|
import { OrderChannelIconsComponent } from './order-channel-icons.component';
|
||||||
import { OrderDetailModalComponent } from './order-detail-modal.component';
|
import { OrderDetailModalComponent } from './order-detail-modal.component';
|
||||||
import { OrderInvoiceModalComponent } from './order-invoice-modal.component';
|
import { OrderInvoiceModalComponent } from './order-invoice-modal.component';
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +27,7 @@ import {
|
|||||||
PickListResult,
|
PickListResult,
|
||||||
PickListLine,
|
PickListLine,
|
||||||
PickListOrderGroup,
|
PickListOrderGroup,
|
||||||
|
PickListProductGroup,
|
||||||
} from './order.models';
|
} from './order.models';
|
||||||
import {
|
import {
|
||||||
applyOrderWarehouse,
|
applyOrderWarehouse,
|
||||||
@@ -35,6 +37,7 @@ import {
|
|||||||
canUseWarehouseForOrder,
|
canUseWarehouseForOrder,
|
||||||
defaultWarehouseForLine,
|
defaultWarehouseForLine,
|
||||||
groupPickListByOrder,
|
groupPickListByOrder,
|
||||||
|
groupPickListByProduct,
|
||||||
orderWarehouseOptions,
|
orderWarehouseOptions,
|
||||||
} from './order-pick.utils';
|
} from './order-pick.utils';
|
||||||
import { AdminListToolbarComponent } from '../../shared/admin-list-toolbar.component';
|
import { AdminListToolbarComponent } from '../../shared/admin-list-toolbar.component';
|
||||||
@@ -43,6 +46,7 @@ import { AdminSkeletonComponent } from '../../shared/admin-skeleton.component';
|
|||||||
import { ToastService } from '../../shared/toast.service';
|
import { ToastService } from '../../shared/toast.service';
|
||||||
|
|
||||||
type OrdersTab = 'list' | 'pickList';
|
type OrdersTab = 'list' | 'pickList';
|
||||||
|
type PickListMode = 'order' | 'product';
|
||||||
|
|
||||||
type SortBy = 'date' | 'total' | 'number';
|
type SortBy = 'date' | 'total' | 'number';
|
||||||
type SortDir = 'asc' | 'desc';
|
type SortDir = 'asc' | 'desc';
|
||||||
@@ -58,6 +62,7 @@ type SortDir = 'asc' | 'desc';
|
|||||||
AdminListToolbarComponent,
|
AdminListToolbarComponent,
|
||||||
AdminPaginationComponent,
|
AdminPaginationComponent,
|
||||||
AdminSkeletonComponent,
|
AdminSkeletonComponent,
|
||||||
|
OrderChannelIconsComponent,
|
||||||
OrderDetailModalComponent,
|
OrderDetailModalComponent,
|
||||||
OrderInvoiceModalComponent,
|
OrderInvoiceModalComponent,
|
||||||
],
|
],
|
||||||
@@ -78,6 +83,7 @@ export class OrdersAdminComponent implements OnInit {
|
|||||||
|
|
||||||
readonly pickList = signal<PickListResult | null>(null);
|
readonly pickList = signal<PickListResult | null>(null);
|
||||||
readonly pickListLoading = signal(false);
|
readonly pickListLoading = signal(false);
|
||||||
|
readonly pickListMode = signal<PickListMode>('order');
|
||||||
readonly showCollected = signal(false);
|
readonly showCollected = signal(false);
|
||||||
readonly collectingLineId = signal<string | null>(null);
|
readonly collectingLineId = signal<string | null>(null);
|
||||||
readonly collectingOrderId = signal<string | null>(null);
|
readonly collectingOrderId = signal<string | null>(null);
|
||||||
@@ -200,6 +206,10 @@ export class OrdersAdminComponent implements OnInit {
|
|||||||
this.loadPickList();
|
this.loadPickList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPickListMode(mode: PickListMode): void {
|
||||||
|
this.pickListMode.set(mode);
|
||||||
|
}
|
||||||
|
|
||||||
setWarehouse(lineId: string, warehouseId: string): void {
|
setWarehouse(lineId: string, warehouseId: string): void {
|
||||||
this.selectedWarehouse.set(lineId, warehouseId);
|
this.selectedWarehouse.set(lineId, warehouseId);
|
||||||
this.#cdr.markForCheck();
|
this.#cdr.markForCheck();
|
||||||
@@ -217,6 +227,99 @@ export class OrdersAdminComponent implements OnInit {
|
|||||||
return groupPickListByOrder(items);
|
return groupPickListByOrder(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pickListProductGroups(items: PickListLine[]): PickListProductGroup[] {
|
||||||
|
return groupPickListByProduct(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
setProductWarehouse(
|
||||||
|
group: PickListProductGroup,
|
||||||
|
warehouseId: string,
|
||||||
|
): void {
|
||||||
|
for (const line of group.lines) {
|
||||||
|
if (!line.isCollected) {
|
||||||
|
this.selectedWarehouse.set(line.orderLineId, warehouseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#cdr.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
getProductWarehouse(group: PickListProductGroup): string {
|
||||||
|
const pending = group.lines.find((line) => !line.isCollected);
|
||||||
|
if (!pending) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return this.getWarehouse(pending.orderLineId);
|
||||||
|
}
|
||||||
|
|
||||||
|
canCollectProductGroup(group: PickListProductGroup): boolean {
|
||||||
|
const pending = group.lines.filter((line) => !line.isCollected);
|
||||||
|
if (pending.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const warehouseId = this.getWarehouse(pending[0].orderLineId);
|
||||||
|
if (!warehouseId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!pending.every(
|
||||||
|
(line) => this.getWarehouse(line.orderLineId) === warehouseId,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const needed = pending.reduce((sum, line) => sum + line.quantity, 0);
|
||||||
|
const stock = group.warehouseStock.find(
|
||||||
|
(ws) => ws.warehouseId === warehouseId,
|
||||||
|
);
|
||||||
|
return !!stock && stock.quantity >= needed;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCollectProduct(group: PickListProductGroup): void {
|
||||||
|
if (!this.canCollectProductGroup(group)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = group.lines.filter((line) => !line.isCollected);
|
||||||
|
if (pending.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.collectingOrderId.set(group.productVariantId || group.sku);
|
||||||
|
let remaining = pending.length;
|
||||||
|
let failed = false;
|
||||||
|
|
||||||
|
for (const line of pending) {
|
||||||
|
const warehouseId = this.getWarehouse(line.orderLineId);
|
||||||
|
this.#api
|
||||||
|
.post<void>(`/orders/admin/lines/${line.orderLineId}/collect`, {
|
||||||
|
warehouseId,
|
||||||
|
})
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
remaining -= 1;
|
||||||
|
if (remaining === 0) {
|
||||||
|
this.collectingOrderId.set(null);
|
||||||
|
if (!failed) {
|
||||||
|
this.loadPickList();
|
||||||
|
this.loadOrders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
failed = true;
|
||||||
|
remaining -= 1;
|
||||||
|
if (remaining === 0) {
|
||||||
|
this.collectingOrderId.set(null);
|
||||||
|
this.loadPickList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getOrderWarehouse(orderId: string): string {
|
getOrderWarehouse(orderId: string): string {
|
||||||
return this.selectedOrderWarehouse.get(orderId) ?? '';
|
return this.selectedOrderWarehouse.get(orderId) ?? '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ export class ProductsAdminComponent implements OnInit {
|
|||||||
|
|
||||||
this.filterLowStock =
|
this.filterLowStock =
|
||||||
this.#route.snapshot.queryParamMap.get('lowStock') === '1';
|
this.#route.snapshot.queryParamMap.get('lowStock') === '1';
|
||||||
|
this.filterCategoryId =
|
||||||
|
this.#route.snapshot.queryParamMap.get('categoryId') ?? '';
|
||||||
this.loadProducts();
|
this.loadProducts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +311,11 @@ export class ProductsAdminComponent implements OnInit {
|
|||||||
#syncLowStockQuery(): void {
|
#syncLowStockQuery(): void {
|
||||||
void this.#router.navigate([], {
|
void this.#router.navigate([], {
|
||||||
relativeTo: this.#route,
|
relativeTo: this.#route,
|
||||||
queryParams: this.filterLowStock ? { lowStock: '1' } : {},
|
queryParams: {
|
||||||
|
lowStock: this.filterLowStock ? '1' : null,
|
||||||
|
categoryId: this.filterCategoryId || null,
|
||||||
|
},
|
||||||
|
queryParamsHandling: 'merge',
|
||||||
replaceUrl: true,
|
replaceUrl: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,7 +234,16 @@
|
|||||||
<div class="card-head">
|
<div class="card-head">
|
||||||
<h2>{{ 'settings.shippingTitle' | translate }}</h2>
|
<h2>{{ 'settings.shippingTitle' | translate }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-pad">
|
<div class="card-pad stack" style="gap: 16px">
|
||||||
|
<label class="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="check"
|
||||||
|
name="shippingUseSandbox"
|
||||||
|
[(ngModel)]="form.shippingUseSandbox"
|
||||||
|
/>
|
||||||
|
{{ 'settings.shippingSandbox' | translate }}
|
||||||
|
</label>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="data">
|
<table class="data">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -256,8 +265,17 @@
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge badge-warning">
|
<span
|
||||||
{{ 'settings.sandbox' | translate }}
|
class="badge"
|
||||||
|
[class.badge-warning]="form.shippingUseSandbox"
|
||||||
|
[class.badge-success]="!form.shippingUseSandbox"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
(form.shippingUseSandbox
|
||||||
|
? 'settings.sandbox'
|
||||||
|
: 'settings.live'
|
||||||
|
) | translate
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -272,8 +290,17 @@
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge badge-warning">
|
<span
|
||||||
{{ 'settings.sandbox' | translate }}
|
class="badge"
|
||||||
|
[class.badge-warning]="form.shippingUseSandbox"
|
||||||
|
[class.badge-success]="!form.shippingUseSandbox"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
(form.shippingUseSandbox
|
||||||
|
? 'settings.sandbox'
|
||||||
|
: 'settings.live'
|
||||||
|
) | translate
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -288,15 +315,24 @@
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge badge-warning">
|
<span
|
||||||
{{ 'settings.sandbox' | translate }}
|
class="badge"
|
||||||
|
[class.badge-warning]="form.shippingUseSandbox"
|
||||||
|
[class.badge-success]="!form.shippingUseSandbox"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
(form.shippingUseSandbox
|
||||||
|
? 'settings.sandbox'
|
||||||
|
: 'settings.live'
|
||||||
|
) | translate
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p class="field-hint" style="margin: 14px 0 0">
|
<p class="field-hint" style="margin: 0">
|
||||||
{{ 'settings.shippingHint' | translate }}
|
{{ 'settings.shippingHint' | translate }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -313,6 +349,335 @@
|
|||||||
{{ 'settings.integrationsHint' | translate }}
|
{{ 'settings.integrationsHint' | translate }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<section class="stack" style="gap: 12px">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px">
|
||||||
|
<span class="badge badge-info">PayU</span>
|
||||||
|
<label class="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="check"
|
||||||
|
name="payuSandbox"
|
||||||
|
[(ngModel)]="form.payU.useSandbox"
|
||||||
|
/>
|
||||||
|
{{ 'settings.sandbox' | translate }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div class="field">
|
||||||
|
<label for="payu-pos">{{ 'settings.payuPosId' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="payu-pos"
|
||||||
|
class="input"
|
||||||
|
name="payuPosId"
|
||||||
|
[(ngModel)]="form.payU.posId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="payu-client">{{ 'settings.payuClientId' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="payu-client"
|
||||||
|
class="input"
|
||||||
|
name="payuClientId"
|
||||||
|
[(ngModel)]="form.payU.clientId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="payu-secret">
|
||||||
|
{{ 'settings.payuClientSecret' | translate }}
|
||||||
|
@if (form.payU.hasClientSecret) {
|
||||||
|
<span class="cell-muted">({{ 'settings.secretSaved' | translate }})</span>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="payu-secret"
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
name="payuClientSecret"
|
||||||
|
autocomplete="new-password"
|
||||||
|
[(ngModel)]="form.payU.clientSecret"
|
||||||
|
[placeholder]="form.payU.hasClientSecret ? '••••••••' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="payu-second">
|
||||||
|
{{ 'settings.payuSecondKey' | translate }}
|
||||||
|
@if (form.payU.hasSecondKey) {
|
||||||
|
<span class="cell-muted">({{ 'settings.secretSaved' | translate }})</span>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="payu-second"
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
name="payuSecondKey"
|
||||||
|
autocomplete="new-password"
|
||||||
|
[(ngModel)]="form.payU.secondKey"
|
||||||
|
[placeholder]="form.payU.hasSecondKey ? '••••••••' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="payu-oauth">{{ 'settings.payuOauthUrl' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="payu-oauth"
|
||||||
|
class="input"
|
||||||
|
name="payuOauthUrl"
|
||||||
|
[(ngModel)]="form.payU.oauthUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="payu-orders">{{ 'settings.payuOrdersUrl' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="payu-orders"
|
||||||
|
class="input"
|
||||||
|
name="payuOrdersUrl"
|
||||||
|
[(ngModel)]="form.payU.ordersUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="payu-notify">{{ 'settings.payuNotifyUrl' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="payu-notify"
|
||||||
|
class="input"
|
||||||
|
name="payuNotifyUrl"
|
||||||
|
[(ngModel)]="form.payU.notifyUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="payu-continue">{{ 'settings.payuContinueUrl' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="payu-continue"
|
||||||
|
class="input"
|
||||||
|
name="payuContinueUrl"
|
||||||
|
[(ngModel)]="form.payU.continueUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr style="border: 0; border-top: 1px solid var(--border)" />
|
||||||
|
|
||||||
|
<section class="stack" style="gap: 12px">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px">
|
||||||
|
<span class="delivery-badge" data-carrier="inPost">InPost</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div class="field">
|
||||||
|
<label for="inpost-org">{{ 'settings.inPostOrgId' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="inpost-org"
|
||||||
|
class="input"
|
||||||
|
name="inPostOrgId"
|
||||||
|
[(ngModel)]="form.inPost.organizationId"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="inpost-token">
|
||||||
|
{{ 'settings.inPostShipXToken' | translate }}
|
||||||
|
@if (form.inPost.hasShipXToken) {
|
||||||
|
<span class="cell-muted">({{ 'settings.secretSaved' | translate }})</span>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="inpost-token"
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
name="inPostShipXToken"
|
||||||
|
autocomplete="new-password"
|
||||||
|
[(ngModel)]="form.inPost.shipXToken"
|
||||||
|
[placeholder]="form.inPost.hasShipXToken ? '••••••••' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="inpost-base">{{ 'settings.inPostShipXBaseUrl' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="inpost-base"
|
||||||
|
class="input"
|
||||||
|
name="inPostShipXBaseUrl"
|
||||||
|
[(ngModel)]="form.inPost.shipXBaseUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="inpost-config">{{ 'settings.inPostGeowidgetConfig' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="inpost-config"
|
||||||
|
class="input"
|
||||||
|
name="inPostGeowidgetConfig"
|
||||||
|
[(ngModel)]="form.inPost.geowidgetConfig"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="inpost-sdk">{{ 'settings.inPostGeowidgetSdk' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="inpost-sdk"
|
||||||
|
class="input"
|
||||||
|
name="inPostGeowidgetSdk"
|
||||||
|
[(ngModel)]="form.inPost.geowidgetSdkBaseUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="inpost-geo-pub">
|
||||||
|
{{ 'settings.inPostGeowidgetPublic' | translate }}
|
||||||
|
@if (form.inPost.hasGeowidgetPublicToken) {
|
||||||
|
<span class="cell-muted">({{ 'settings.secretSaved' | translate }})</span>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="inpost-geo-pub"
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
name="inPostGeowidgetPublic"
|
||||||
|
autocomplete="new-password"
|
||||||
|
[(ngModel)]="form.inPost.geowidgetPublicToken"
|
||||||
|
[placeholder]="form.inPost.hasGeowidgetPublicToken ? '••••••••' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="inpost-geo-sb">
|
||||||
|
{{ 'settings.inPostGeowidgetSandbox' | translate }}
|
||||||
|
@if (form.inPost.hasGeowidgetSandboxToken) {
|
||||||
|
<span class="cell-muted">({{ 'settings.secretSaved' | translate }})</span>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="inpost-geo-sb"
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
name="inPostGeowidgetSandbox"
|
||||||
|
autocomplete="new-password"
|
||||||
|
[(ngModel)]="form.inPost.geowidgetSandboxToken"
|
||||||
|
[placeholder]="form.inPost.hasGeowidgetSandboxToken ? '••••••••' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr style="border: 0; border-top: 1px solid var(--border)" />
|
||||||
|
|
||||||
|
<section class="stack" style="gap: 12px">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px">
|
||||||
|
<span class="delivery-badge" data-carrier="dhl">DHL</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div class="field">
|
||||||
|
<label for="dhl-key">
|
||||||
|
{{ 'settings.dhlApiKey' | translate }}
|
||||||
|
@if (form.dhl.hasApiKey) {
|
||||||
|
<span class="cell-muted">({{ 'settings.secretSaved' | translate }})</span>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="dhl-key"
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
name="dhlApiKey"
|
||||||
|
autocomplete="new-password"
|
||||||
|
[(ngModel)]="form.dhl.apiKey"
|
||||||
|
[placeholder]="form.dhl.hasApiKey ? '••••••••' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="dhl-secret">
|
||||||
|
{{ 'settings.dhlApiSecret' | translate }}
|
||||||
|
@if (form.dhl.hasApiSecret) {
|
||||||
|
<span class="cell-muted">({{ 'settings.secretSaved' | translate }})</span>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="dhl-secret"
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
name="dhlApiSecret"
|
||||||
|
autocomplete="new-password"
|
||||||
|
[(ngModel)]="form.dhl.apiSecret"
|
||||||
|
[placeholder]="form.dhl.hasApiSecret ? '••••••••' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="dhl-base">{{ 'settings.dhlApiBaseUrl' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="dhl-base"
|
||||||
|
class="input"
|
||||||
|
name="dhlApiBaseUrl"
|
||||||
|
[(ngModel)]="form.dhl.apiBaseUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="dhl-widget">{{ 'settings.dhlWidgetUrl' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="dhl-widget"
|
||||||
|
class="input"
|
||||||
|
name="dhlWidgetUrl"
|
||||||
|
[(ngModel)]="form.dhl.widgetUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr style="border: 0; border-top: 1px solid var(--border)" />
|
||||||
|
|
||||||
|
<section class="stack" style="gap: 12px">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px">
|
||||||
|
<span class="delivery-badge" data-carrier="dpd">DPD</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div class="field">
|
||||||
|
<label for="dpd-login">{{ 'settings.dpdLogin' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="dpd-login"
|
||||||
|
class="input"
|
||||||
|
name="dpdLogin"
|
||||||
|
[(ngModel)]="form.dpd.login"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="dpd-pass">
|
||||||
|
{{ 'settings.dpdPassword' | translate }}
|
||||||
|
@if (form.dpd.hasPassword) {
|
||||||
|
<span class="cell-muted">({{ 'settings.secretSaved' | translate }})</span>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="dpd-pass"
|
||||||
|
class="input"
|
||||||
|
type="password"
|
||||||
|
name="dpdPassword"
|
||||||
|
autocomplete="new-password"
|
||||||
|
[(ngModel)]="form.dpd.password"
|
||||||
|
[placeholder]="form.dpd.hasPassword ? '••••••••' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="dpd-fid">{{ 'settings.dpdFid' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="dpd-fid"
|
||||||
|
class="input"
|
||||||
|
name="dpdFid"
|
||||||
|
[(ngModel)]="form.dpd.fid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="dpd-base">{{ 'settings.dpdApiBaseUrl' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="dpd-base"
|
||||||
|
class="input"
|
||||||
|
name="dpdApiBaseUrl"
|
||||||
|
[(ngModel)]="form.dpd.apiBaseUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="dpd-widget">{{ 'settings.dpdWidgetUrl' | translate }}</label>
|
||||||
|
<input
|
||||||
|
id="dpd-widget"
|
||||||
|
class="input"
|
||||||
|
name="dpdWidgetUrl"
|
||||||
|
[(ngModel)]="form.dpd.widgetUrl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr style="border: 0; border-top: 1px solid var(--border)" />
|
||||||
|
|
||||||
<section class="stack" style="gap: 12px">
|
<section class="stack" style="gap: 12px">
|
||||||
<div style="display: flex; align-items: center; gap: 10px">
|
<div style="display: flex; align-items: center; gap: 10px">
|
||||||
<span class="channel-badge" data-channel="allegro">Allegro</span>
|
<span class="channel-badge" data-channel="allegro">Allegro</span>
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ interface StoreSettingsApi {
|
|||||||
locales: { pl: boolean; en: boolean; de: boolean; cs: boolean };
|
locales: { pl: boolean; en: boolean; de: boolean; cs: boolean };
|
||||||
lowStockThreshold: number;
|
lowStockThreshold: number;
|
||||||
carriers: { inPost: boolean; dhl: boolean; dpd: boolean };
|
carriers: { inPost: boolean; dhl: boolean; dpd: boolean };
|
||||||
|
shippingUseSandbox: boolean;
|
||||||
emailFromName: string;
|
emailFromName: string;
|
||||||
emailFromAddress: string;
|
emailFromAddress: string;
|
||||||
emailReplyTo: string;
|
emailReplyTo: string;
|
||||||
@@ -61,6 +62,39 @@ interface StoreSettingsApi {
|
|||||||
invoiceSeries?: string | null;
|
invoiceSeries?: string | null;
|
||||||
autoInvoice: boolean;
|
autoInvoice: boolean;
|
||||||
};
|
};
|
||||||
|
inPost: {
|
||||||
|
hasShipXToken: boolean;
|
||||||
|
organizationId?: string | null;
|
||||||
|
shipXBaseUrl?: string | null;
|
||||||
|
hasGeowidgetPublicToken: boolean;
|
||||||
|
hasGeowidgetSandboxToken: boolean;
|
||||||
|
geowidgetConfig?: string | null;
|
||||||
|
geowidgetSdkBaseUrl?: string | null;
|
||||||
|
};
|
||||||
|
dhl: {
|
||||||
|
hasApiKey: boolean;
|
||||||
|
hasApiSecret: boolean;
|
||||||
|
apiBaseUrl?: string | null;
|
||||||
|
widgetUrl?: string | null;
|
||||||
|
};
|
||||||
|
dpd: {
|
||||||
|
login?: string | null;
|
||||||
|
hasPassword: boolean;
|
||||||
|
fid?: string | null;
|
||||||
|
apiBaseUrl?: string | null;
|
||||||
|
widgetUrl?: string | null;
|
||||||
|
};
|
||||||
|
payU: {
|
||||||
|
useSandbox: boolean;
|
||||||
|
posId?: string | null;
|
||||||
|
clientId?: string | null;
|
||||||
|
hasClientSecret: boolean;
|
||||||
|
hasSecondKey: boolean;
|
||||||
|
oauthUrl?: string | null;
|
||||||
|
ordersUrl?: string | null;
|
||||||
|
notifyUrl?: string | null;
|
||||||
|
continueUrl?: string | null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -77,12 +111,7 @@ export class SettingsAdminComponent implements OnInit {
|
|||||||
readonly section = signal<SettingsSection>('store');
|
readonly section = signal<SettingsSection>('store');
|
||||||
readonly loading = signal(false);
|
readonly loading = signal(false);
|
||||||
readonly saving = signal(false);
|
readonly saving = signal(false);
|
||||||
form: AdminSettings = {
|
form: AdminSettings = this.#cloneDefaults();
|
||||||
...DEFAULT_ADMIN_SETTINGS,
|
|
||||||
allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro },
|
|
||||||
amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon },
|
|
||||||
ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma },
|
|
||||||
};
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.load();
|
this.load();
|
||||||
@@ -133,6 +162,19 @@ export class SettingsAdminComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#cloneDefaults(): AdminSettings {
|
||||||
|
return {
|
||||||
|
...DEFAULT_ADMIN_SETTINGS,
|
||||||
|
allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro },
|
||||||
|
amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon },
|
||||||
|
ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma },
|
||||||
|
inPost: { ...DEFAULT_ADMIN_SETTINGS.inPost },
|
||||||
|
dhl: { ...DEFAULT_ADMIN_SETTINGS.dhl },
|
||||||
|
dpd: { ...DEFAULT_ADMIN_SETTINGS.dpd },
|
||||||
|
payU: { ...DEFAULT_ADMIN_SETTINGS.payU },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#fromApi(res: StoreSettingsApi): AdminSettings {
|
#fromApi(res: StoreSettingsApi): AdminSettings {
|
||||||
return {
|
return {
|
||||||
storeName: res.storeName,
|
storeName: res.storeName,
|
||||||
@@ -153,6 +195,7 @@ export class SettingsAdminComponent implements OnInit {
|
|||||||
dhl: res.carriers.dhl,
|
dhl: res.carriers.dhl,
|
||||||
dpd: res.carriers.dpd,
|
dpd: res.carriers.dpd,
|
||||||
},
|
},
|
||||||
|
shippingUseSandbox: res.shippingUseSandbox ?? true,
|
||||||
emailFromName: res.emailFromName,
|
emailFromName: res.emailFromName,
|
||||||
emailFromAddress: res.emailFromAddress,
|
emailFromAddress: res.emailFromAddress,
|
||||||
emailReplyTo: res.emailReplyTo,
|
emailReplyTo: res.emailReplyTo,
|
||||||
@@ -182,6 +225,48 @@ export class SettingsAdminComponent implements OnInit {
|
|||||||
autoInvoice: res.ifirma?.autoInvoice ?? false,
|
autoInvoice: res.ifirma?.autoInvoice ?? false,
|
||||||
hasApiKey: res.ifirma?.hasApiKey ?? false,
|
hasApiKey: res.ifirma?.hasApiKey ?? false,
|
||||||
},
|
},
|
||||||
|
inPost: {
|
||||||
|
shipXToken: '',
|
||||||
|
organizationId: res.inPost?.organizationId ?? '',
|
||||||
|
shipXBaseUrl: res.inPost?.shipXBaseUrl ?? '',
|
||||||
|
geowidgetPublicToken: '',
|
||||||
|
geowidgetSandboxToken: '',
|
||||||
|
geowidgetConfig: res.inPost?.geowidgetConfig ?? 'parcelCollect',
|
||||||
|
geowidgetSdkBaseUrl: res.inPost?.geowidgetSdkBaseUrl ?? '',
|
||||||
|
hasShipXToken: res.inPost?.hasShipXToken ?? false,
|
||||||
|
hasGeowidgetPublicToken: res.inPost?.hasGeowidgetPublicToken ?? false,
|
||||||
|
hasGeowidgetSandboxToken:
|
||||||
|
res.inPost?.hasGeowidgetSandboxToken ?? false,
|
||||||
|
},
|
||||||
|
dhl: {
|
||||||
|
apiKey: '',
|
||||||
|
apiSecret: '',
|
||||||
|
apiBaseUrl: res.dhl?.apiBaseUrl ?? '',
|
||||||
|
widgetUrl: res.dhl?.widgetUrl ?? 'https://parcelshop.dhl.pl/mapa',
|
||||||
|
hasApiKey: res.dhl?.hasApiKey ?? false,
|
||||||
|
hasApiSecret: res.dhl?.hasApiSecret ?? false,
|
||||||
|
},
|
||||||
|
dpd: {
|
||||||
|
login: res.dpd?.login ?? '',
|
||||||
|
password: '',
|
||||||
|
fid: res.dpd?.fid ?? '',
|
||||||
|
apiBaseUrl: res.dpd?.apiBaseUrl ?? '',
|
||||||
|
widgetUrl: res.dpd?.widgetUrl ?? 'https://pudofinder.dpd.com.pl/',
|
||||||
|
hasPassword: res.dpd?.hasPassword ?? false,
|
||||||
|
},
|
||||||
|
payU: {
|
||||||
|
useSandbox: res.payU?.useSandbox ?? true,
|
||||||
|
posId: res.payU?.posId ?? '',
|
||||||
|
clientId: res.payU?.clientId ?? '',
|
||||||
|
clientSecret: '',
|
||||||
|
secondKey: '',
|
||||||
|
oauthUrl: res.payU?.oauthUrl ?? '',
|
||||||
|
ordersUrl: res.payU?.ordersUrl ?? '',
|
||||||
|
notifyUrl: res.payU?.notifyUrl ?? '',
|
||||||
|
continueUrl: res.payU?.continueUrl ?? '',
|
||||||
|
hasClientSecret: res.payU?.hasClientSecret ?? false,
|
||||||
|
hasSecondKey: res.payU?.hasSecondKey ?? false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +281,7 @@ export class SettingsAdminComponent implements OnInit {
|
|||||||
locales: form.locales,
|
locales: form.locales,
|
||||||
lowStockThreshold: form.lowStockThreshold,
|
lowStockThreshold: form.lowStockThreshold,
|
||||||
carriers: form.carriers,
|
carriers: form.carriers,
|
||||||
|
shippingUseSandbox: form.shippingUseSandbox,
|
||||||
emailFromName: form.emailFromName,
|
emailFromName: form.emailFromName,
|
||||||
emailFromAddress: form.emailFromAddress,
|
emailFromAddress: form.emailFromAddress,
|
||||||
emailReplyTo: form.emailReplyTo,
|
emailReplyTo: form.emailReplyTo,
|
||||||
@@ -219,6 +305,39 @@ export class SettingsAdminComponent implements OnInit {
|
|||||||
invoiceSeries: form.ifirma.invoiceSeries || null,
|
invoiceSeries: form.ifirma.invoiceSeries || null,
|
||||||
autoInvoice: form.ifirma.autoInvoice,
|
autoInvoice: form.ifirma.autoInvoice,
|
||||||
},
|
},
|
||||||
|
inPost: {
|
||||||
|
shipXToken: form.inPost.shipXToken || null,
|
||||||
|
organizationId: form.inPost.organizationId || null,
|
||||||
|
shipXBaseUrl: form.inPost.shipXBaseUrl || null,
|
||||||
|
geowidgetPublicToken: form.inPost.geowidgetPublicToken || null,
|
||||||
|
geowidgetSandboxToken: form.inPost.geowidgetSandboxToken || null,
|
||||||
|
geowidgetConfig: form.inPost.geowidgetConfig || null,
|
||||||
|
geowidgetSdkBaseUrl: form.inPost.geowidgetSdkBaseUrl || null,
|
||||||
|
},
|
||||||
|
dhl: {
|
||||||
|
apiKey: form.dhl.apiKey || null,
|
||||||
|
apiSecret: form.dhl.apiSecret || null,
|
||||||
|
apiBaseUrl: form.dhl.apiBaseUrl || null,
|
||||||
|
widgetUrl: form.dhl.widgetUrl || null,
|
||||||
|
},
|
||||||
|
dpd: {
|
||||||
|
login: form.dpd.login || null,
|
||||||
|
password: form.dpd.password || null,
|
||||||
|
fid: form.dpd.fid || null,
|
||||||
|
apiBaseUrl: form.dpd.apiBaseUrl || null,
|
||||||
|
widgetUrl: form.dpd.widgetUrl || null,
|
||||||
|
},
|
||||||
|
payU: {
|
||||||
|
useSandbox: form.payU.useSandbox,
|
||||||
|
posId: form.payU.posId || null,
|
||||||
|
clientId: form.payU.clientId || null,
|
||||||
|
clientSecret: form.payU.clientSecret || null,
|
||||||
|
secondKey: form.payU.secondKey || null,
|
||||||
|
oauthUrl: form.payU.oauthUrl || null,
|
||||||
|
ordersUrl: form.payU.ordersUrl || null,
|
||||||
|
notifyUrl: form.payU.notifyUrl || null,
|
||||||
|
continueUrl: form.payU.continueUrl || null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,51 @@ export interface IfirmaSettingsForm {
|
|||||||
hasApiKey: boolean;
|
hasApiKey: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InPostSettingsForm {
|
||||||
|
shipXToken: string;
|
||||||
|
organizationId: string;
|
||||||
|
shipXBaseUrl: string;
|
||||||
|
geowidgetPublicToken: string;
|
||||||
|
geowidgetSandboxToken: string;
|
||||||
|
geowidgetConfig: string;
|
||||||
|
geowidgetSdkBaseUrl: string;
|
||||||
|
hasShipXToken: boolean;
|
||||||
|
hasGeowidgetPublicToken: boolean;
|
||||||
|
hasGeowidgetSandboxToken: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DhlSettingsForm {
|
||||||
|
apiKey: string;
|
||||||
|
apiSecret: string;
|
||||||
|
apiBaseUrl: string;
|
||||||
|
widgetUrl: string;
|
||||||
|
hasApiKey: boolean;
|
||||||
|
hasApiSecret: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DpdSettingsForm {
|
||||||
|
login: string;
|
||||||
|
password: string;
|
||||||
|
fid: string;
|
||||||
|
apiBaseUrl: string;
|
||||||
|
widgetUrl: string;
|
||||||
|
hasPassword: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayUSettingsForm {
|
||||||
|
useSandbox: boolean;
|
||||||
|
posId: string;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
secondKey: string;
|
||||||
|
oauthUrl: string;
|
||||||
|
ordersUrl: string;
|
||||||
|
notifyUrl: string;
|
||||||
|
continueUrl: string;
|
||||||
|
hasClientSecret: boolean;
|
||||||
|
hasSecondKey: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminSettings {
|
export interface AdminSettings {
|
||||||
storeName: string;
|
storeName: string;
|
||||||
supportEmail: string;
|
supportEmail: string;
|
||||||
@@ -40,12 +85,17 @@ export interface AdminSettings {
|
|||||||
locales: { pl: boolean; en: boolean; de: boolean; cs: boolean };
|
locales: { pl: boolean; en: boolean; de: boolean; cs: boolean };
|
||||||
lowStockThreshold: number;
|
lowStockThreshold: number;
|
||||||
carriers: { inPost: boolean; dhl: boolean; dpd: boolean };
|
carriers: { inPost: boolean; dhl: boolean; dpd: boolean };
|
||||||
|
shippingUseSandbox: boolean;
|
||||||
emailFromName: string;
|
emailFromName: string;
|
||||||
emailFromAddress: string;
|
emailFromAddress: string;
|
||||||
emailReplyTo: string;
|
emailReplyTo: string;
|
||||||
allegro: AllegroSettingsForm;
|
allegro: AllegroSettingsForm;
|
||||||
amazon: AmazonSettingsForm;
|
amazon: AmazonSettingsForm;
|
||||||
ifirma: IfirmaSettingsForm;
|
ifirma: IfirmaSettingsForm;
|
||||||
|
inPost: InPostSettingsForm;
|
||||||
|
dhl: DhlSettingsForm;
|
||||||
|
dpd: DpdSettingsForm;
|
||||||
|
payU: PayUSettingsForm;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_ADMIN_SETTINGS: AdminSettings = {
|
export const DEFAULT_ADMIN_SETTINGS: AdminSettings = {
|
||||||
@@ -58,6 +108,7 @@ export const DEFAULT_ADMIN_SETTINGS: AdminSettings = {
|
|||||||
locales: { pl: true, en: true, de: true, cs: true },
|
locales: { pl: true, en: true, de: true, cs: true },
|
||||||
lowStockThreshold: 5,
|
lowStockThreshold: 5,
|
||||||
carriers: { inPost: true, dhl: true, dpd: true },
|
carriers: { inPost: true, dhl: true, dpd: true },
|
||||||
|
shippingUseSandbox: true,
|
||||||
emailFromName: 'iLED',
|
emailFromName: 'iLED',
|
||||||
emailFromAddress: 'newsletter@iled.pl',
|
emailFromAddress: 'newsletter@iled.pl',
|
||||||
emailReplyTo: 'hello@iled.pl',
|
emailReplyTo: 'hello@iled.pl',
|
||||||
@@ -87,18 +138,54 @@ export const DEFAULT_ADMIN_SETTINGS: AdminSettings = {
|
|||||||
autoInvoice: false,
|
autoInvoice: false,
|
||||||
hasApiKey: false,
|
hasApiKey: false,
|
||||||
},
|
},
|
||||||
|
inPost: {
|
||||||
|
shipXToken: '',
|
||||||
|
organizationId: '',
|
||||||
|
shipXBaseUrl: '',
|
||||||
|
geowidgetPublicToken: '',
|
||||||
|
geowidgetSandboxToken: '',
|
||||||
|
geowidgetConfig: 'parcelCollect',
|
||||||
|
geowidgetSdkBaseUrl: '',
|
||||||
|
hasShipXToken: false,
|
||||||
|
hasGeowidgetPublicToken: false,
|
||||||
|
hasGeowidgetSandboxToken: false,
|
||||||
|
},
|
||||||
|
dhl: {
|
||||||
|
apiKey: '',
|
||||||
|
apiSecret: '',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
widgetUrl: 'https://parcelshop.dhl.pl/mapa',
|
||||||
|
hasApiKey: false,
|
||||||
|
hasApiSecret: false,
|
||||||
|
},
|
||||||
|
dpd: {
|
||||||
|
login: '',
|
||||||
|
password: '',
|
||||||
|
fid: '',
|
||||||
|
apiBaseUrl: '',
|
||||||
|
widgetUrl: 'https://pudofinder.dpd.com.pl/',
|
||||||
|
hasPassword: false,
|
||||||
|
},
|
||||||
|
payU: {
|
||||||
|
useSandbox: true,
|
||||||
|
posId: '',
|
||||||
|
clientId: '',
|
||||||
|
clientSecret: '',
|
||||||
|
secondKey: '',
|
||||||
|
oauthUrl: '',
|
||||||
|
ordersUrl: '',
|
||||||
|
notifyUrl: '',
|
||||||
|
continueUrl: '',
|
||||||
|
hasClientSecret: false,
|
||||||
|
hasSecondKey: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function loadAdminSettings(): AdminSettings {
|
export function loadAdminSettings(): AdminSettings {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(ADMIN_SETTINGS_KEY);
|
const raw = localStorage.getItem(ADMIN_SETTINGS_KEY);
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return {
|
return cloneDefaults();
|
||||||
...DEFAULT_ADMIN_SETTINGS,
|
|
||||||
allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro },
|
|
||||||
amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon },
|
|
||||||
ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
const parsed = JSON.parse(raw) as Partial<AdminSettings>;
|
const parsed = JSON.parse(raw) as Partial<AdminSettings>;
|
||||||
return {
|
return {
|
||||||
@@ -109,14 +196,13 @@ export function loadAdminSettings(): AdminSettings {
|
|||||||
allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro, ...parsed.allegro },
|
allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro, ...parsed.allegro },
|
||||||
amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon, ...parsed.amazon },
|
amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon, ...parsed.amazon },
|
||||||
ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma, ...parsed.ifirma },
|
ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma, ...parsed.ifirma },
|
||||||
|
inPost: { ...DEFAULT_ADMIN_SETTINGS.inPost, ...parsed.inPost },
|
||||||
|
dhl: { ...DEFAULT_ADMIN_SETTINGS.dhl, ...parsed.dhl },
|
||||||
|
dpd: { ...DEFAULT_ADMIN_SETTINGS.dpd, ...parsed.dpd },
|
||||||
|
payU: { ...DEFAULT_ADMIN_SETTINGS.payU, ...parsed.payU },
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return cloneDefaults();
|
||||||
...DEFAULT_ADMIN_SETTINGS,
|
|
||||||
allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro },
|
|
||||||
amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon },
|
|
||||||
ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,3 +213,16 @@ export function saveAdminSettings(settings: AdminSettings): void {
|
|||||||
String(settings.lowStockThreshold),
|
String(settings.lowStockThreshold),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cloneDefaults(): AdminSettings {
|
||||||
|
return {
|
||||||
|
...DEFAULT_ADMIN_SETTINGS,
|
||||||
|
allegro: { ...DEFAULT_ADMIN_SETTINGS.allegro },
|
||||||
|
amazon: { ...DEFAULT_ADMIN_SETTINGS.amazon },
|
||||||
|
ifirma: { ...DEFAULT_ADMIN_SETTINGS.ifirma },
|
||||||
|
inPost: { ...DEFAULT_ADMIN_SETTINGS.inPost },
|
||||||
|
dhl: { ...DEFAULT_ADMIN_SETTINGS.dhl },
|
||||||
|
dpd: { ...DEFAULT_ADMIN_SETTINGS.dpd },
|
||||||
|
payU: { ...DEFAULT_ADMIN_SETTINGS.payU },
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1775,21 +1775,42 @@ table.data tbody tr.is-selected {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.meta-icon,
|
||||||
.channel-badge,
|
.channel-badge,
|
||||||
.delivery-badge {
|
.delivery-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
justify-content: center;
|
||||||
padding: 2px 8px;
|
gap: 6px;
|
||||||
border-radius: 999px;
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
line-height: 1.4;
|
line-height: 1;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: var(--surface-muted);
|
background: var(--surface-muted);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-meta-icons--with-labels .meta-icon,
|
||||||
|
.order-meta-icons--with-labels .channel-badge,
|
||||||
|
.order-meta-icons--with-labels .delivery-badge {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 4px 10px 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-icon__label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-badge[data-channel='shop'] {
|
.channel-badge[data-channel='shop'] {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public sealed class AdminProductsController(IMediator mediator) : ControllerBase
|
|||||||
|
|
||||||
[HttpPost("images/upload")]
|
[HttpPost("images/upload")]
|
||||||
[RequestSizeLimit(10 * 1024 * 1024)]
|
[RequestSizeLimit(10 * 1024 * 1024)]
|
||||||
|
[RequestFormLimits(MultipartBodyLengthLimit = 10 * 1024 * 1024)]
|
||||||
public async Task<IActionResult> UploadImage(
|
public async Task<IActionResult> UploadImage(
|
||||||
IFormFile file,
|
IFormFile file,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ builder.Services.AddAuthentication(options =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddAuthorization();
|
builder.Services.AddAuthorization();
|
||||||
|
builder.Services.Configure<Microsoft.AspNetCore.Http.Features.FormOptions>(options =>
|
||||||
|
{
|
||||||
|
options.MultipartBodyLengthLimit = 10 * 1024 * 1024;
|
||||||
|
});
|
||||||
|
builder.WebHost.ConfigureKestrel(options =>
|
||||||
|
{
|
||||||
|
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024;
|
||||||
|
});
|
||||||
builder.Services.AddControllers()
|
builder.Services.AddControllers()
|
||||||
.AddJsonOptions(options =>
|
.AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,79 +58,87 @@ public sealed class GetAnalyticsQueryHandler(IAppDbContext db)
|
|||||||
var start = end.AddDays(-days);
|
var start = end.AddDays(-days);
|
||||||
var prevStart = start.AddDays(-days);
|
var prevStart = start.AddDays(-days);
|
||||||
|
|
||||||
var orders = db.Orders
|
var baseQuery = db.Orders
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(o => !Excluded.Contains(o.Status));
|
.Where(o => !Excluded.Contains(o.Status));
|
||||||
|
|
||||||
if (request.Source.HasValue)
|
if (request.Source.HasValue)
|
||||||
{
|
{
|
||||||
orders = orders.Where(o => o.Source == request.Source.Value);
|
baseQuery = baseQuery.Where(o => o.Source == request.Source.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
var current = await AggregateAsync(
|
var currentRows = await baseQuery
|
||||||
orders.Where(o => o.CreatedAt >= start && o.CreatedAt < end),
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
var previous = await AggregateAsync(
|
|
||||||
orders.Where(o => o.CreatedAt >= prevStart && o.CreatedAt < start),
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
var trendRaw = await orders
|
|
||||||
.Where(o => o.CreatedAt >= start && o.CreatedAt < end)
|
.Where(o => o.CreatedAt >= start && o.CreatedAt < end)
|
||||||
.GroupBy(o => o.CreatedAt.Date)
|
.Select(o => new OrderAggRow(
|
||||||
.Select(g => new
|
o.Id,
|
||||||
{
|
o.CreatedAt,
|
||||||
Date = g.Key,
|
o.Total,
|
||||||
Revenue = g.Sum(o => o.Total),
|
o.Source))
|
||||||
Orders = g.Count(),
|
|
||||||
})
|
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var trendLookup = trendRaw.ToDictionary(x => x.Date);
|
var previousRows = await baseQuery
|
||||||
|
.Where(o => o.CreatedAt >= prevStart && o.CreatedAt < start)
|
||||||
|
.Select(o => new OrderAggRow(
|
||||||
|
o.Id,
|
||||||
|
o.CreatedAt,
|
||||||
|
o.Total,
|
||||||
|
o.Source))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
var currentOrderIds = currentRows.Select(r => r.Id).ToList();
|
||||||
|
var previousOrderIds = previousRows.Select(r => r.Id).ToList();
|
||||||
|
|
||||||
|
var currentUnits = await SumUnitsAsync(
|
||||||
|
currentOrderIds,
|
||||||
|
cancellationToken);
|
||||||
|
var previousUnits = await SumUnitsAsync(
|
||||||
|
previousOrderIds,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var current = ToKpi(currentRows, currentUnits);
|
||||||
|
var previous = ToKpi(previousRows, previousUnits);
|
||||||
|
|
||||||
|
// Group by calendar day in-memory — Npgsql cannot reliably
|
||||||
|
// translate CreatedAt.Date for timestamp with time zone.
|
||||||
|
var trendLookup = currentRows
|
||||||
|
.GroupBy(r => DateOnly.FromDateTime(r.CreatedAt.ToUniversalTime()))
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => (Revenue: g.Sum(x => x.Total), Orders: g.Count()));
|
||||||
|
|
||||||
var trend = new List<AnalyticsTrendPointDto>(days);
|
var trend = new List<AnalyticsTrendPointDto>(days);
|
||||||
for (var d = start; d < end; d = d.AddDays(1))
|
for (var d = DateOnly.FromDateTime(start); d < DateOnly.FromDateTime(end); d = d.AddDays(1))
|
||||||
{
|
{
|
||||||
trendLookup.TryGetValue(d, out var row);
|
trendLookup.TryGetValue(d, out var row);
|
||||||
trend.Add(new AnalyticsTrendPointDto(
|
trend.Add(new AnalyticsTrendPointDto(
|
||||||
d.ToString("yyyy-MM-dd"),
|
d.ToString("yyyy-MM-dd"),
|
||||||
row?.Revenue ?? 0m,
|
row.Revenue,
|
||||||
row?.Orders ?? 0));
|
row.Orders));
|
||||||
}
|
}
|
||||||
|
|
||||||
var paidOrderIds = await orders
|
var topRaw = currentOrderIds.Count == 0
|
||||||
.Where(o => o.CreatedAt >= start && o.CreatedAt < end)
|
? []
|
||||||
.Select(o => o.Id)
|
: await db.Orders
|
||||||
.ToListAsync(cancellationToken);
|
.AsNoTracking()
|
||||||
|
.Where(o => currentOrderIds.Contains(o.Id))
|
||||||
var topRaw = await db.Orders
|
.SelectMany(o => o.Lines)
|
||||||
.AsNoTracking()
|
.GroupBy(l => new { l.ProductId, l.ProductName, l.Sku })
|
||||||
.Where(o => paidOrderIds.Contains(o.Id))
|
.Select(g => new
|
||||||
.SelectMany(o => o.Lines)
|
{
|
||||||
.GroupBy(l => new { l.ProductVariantId, l.ProductName, l.Sku })
|
g.Key.ProductId,
|
||||||
.Select(g => new
|
g.Key.ProductName,
|
||||||
{
|
g.Key.Sku,
|
||||||
g.Key.ProductName,
|
Units = g.Sum(l => l.Quantity),
|
||||||
g.Key.Sku,
|
Revenue = g.Sum(l => l.UnitPrice * l.Quantity),
|
||||||
Units = g.Sum(l => l.Quantity),
|
})
|
||||||
Revenue = g.Sum(l => l.UnitPrice * l.Quantity),
|
.OrderByDescending(x => x.Revenue)
|
||||||
VariantId = g.Key.ProductVariantId,
|
.Take(10)
|
||||||
})
|
.ToListAsync(cancellationToken);
|
||||||
.OrderByDescending(x => x.Revenue)
|
|
||||||
.Take(10)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var variantIds = topRaw.Select(t => t.VariantId).ToList();
|
|
||||||
var productIds = await db.ProductVariants
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(v => variantIds.Contains(v.Id))
|
|
||||||
.Select(v => new { v.Id, v.ProductId })
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
var productLookup = productIds.ToDictionary(v => v.Id, v => v.ProductId);
|
|
||||||
|
|
||||||
var totalRevenue = current.Revenue == 0 ? 1m : current.Revenue;
|
var totalRevenue = current.Revenue == 0 ? 1m : current.Revenue;
|
||||||
var topProducts = topRaw
|
var topProducts = topRaw
|
||||||
.Select(t => new AnalyticsTopProductDto(
|
.Select(t => new AnalyticsTopProductDto(
|
||||||
productLookup.GetValueOrDefault(t.VariantId),
|
t.ProductId,
|
||||||
t.ProductName,
|
t.ProductName,
|
||||||
t.Sku,
|
t.Sku,
|
||||||
t.Units,
|
t.Units,
|
||||||
@@ -138,15 +146,14 @@ public sealed class GetAnalyticsQueryHandler(IAppDbContext db)
|
|||||||
Math.Round(t.Revenue / totalRevenue * 100m, 1)))
|
Math.Round(t.Revenue / totalRevenue * 100m, 1)))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var byChannel = await orders
|
var byChannel = currentRows
|
||||||
.Where(o => o.CreatedAt >= start && o.CreatedAt < end)
|
.GroupBy(r => r.Source)
|
||||||
.GroupBy(o => o.Source)
|
|
||||||
.Select(g => new AnalyticsChannelBreakdownDto(
|
.Select(g => new AnalyticsChannelBreakdownDto(
|
||||||
g.Key,
|
g.Key,
|
||||||
g.Count(),
|
g.Count(),
|
||||||
g.Sum(o => o.Total)))
|
g.Sum(x => x.Total)))
|
||||||
.OrderByDescending(x => x.Revenue)
|
.OrderByDescending(x => x.Revenue)
|
||||||
.ToListAsync(cancellationToken);
|
.ToList();
|
||||||
|
|
||||||
return new AnalyticsDto(
|
return new AnalyticsDto(
|
||||||
current,
|
current,
|
||||||
@@ -156,20 +163,35 @@ public sealed class GetAnalyticsQueryHandler(IAppDbContext db)
|
|||||||
byChannel);
|
byChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<AnalyticsKpiDto> AggregateAsync(
|
private async Task<int> SumUnitsAsync(
|
||||||
IQueryable<Domain.Orders.Order> query,
|
IReadOnlyList<Guid> orderIds,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var orders = await query.CountAsync(cancellationToken);
|
if (orderIds.Count == 0)
|
||||||
var revenue = orders == 0
|
{
|
||||||
? 0m
|
return 0;
|
||||||
: await query.SumAsync(o => o.Total, cancellationToken);
|
}
|
||||||
var units = orders == 0
|
|
||||||
? 0
|
|
||||||
: await query.SelectMany(o => o.Lines)
|
|
||||||
.SumAsync(l => l.Quantity, cancellationToken);
|
|
||||||
var aov = orders == 0 ? 0m : Math.Round(revenue / orders, 2);
|
|
||||||
|
|
||||||
|
return await db.Orders
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(o => orderIds.Contains(o.Id))
|
||||||
|
.SelectMany(o => o.Lines)
|
||||||
|
.SumAsync(l => (int?)l.Quantity, cancellationToken) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnalyticsKpiDto ToKpi(
|
||||||
|
IReadOnlyList<OrderAggRow> rows,
|
||||||
|
int units)
|
||||||
|
{
|
||||||
|
var orders = rows.Count;
|
||||||
|
var revenue = orders == 0 ? 0m : rows.Sum(r => r.Total);
|
||||||
|
var aov = orders == 0 ? 0m : Math.Round(revenue / orders, 2);
|
||||||
return new AnalyticsKpiDto(revenue, orders, aov, units);
|
return new AnalyticsKpiDto(revenue, orders, aov, units);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed record OrderAggRow(
|
||||||
|
Guid Id,
|
||||||
|
DateTime CreatedAt,
|
||||||
|
decimal Total,
|
||||||
|
OrderSource Source);
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,12 @@ public sealed record StoreSettingsDto(
|
|||||||
string EmailReplyTo,
|
string EmailReplyTo,
|
||||||
AllegroSettingsDto Allegro,
|
AllegroSettingsDto Allegro,
|
||||||
AmazonSettingsDto Amazon,
|
AmazonSettingsDto Amazon,
|
||||||
IfirmaSettingsDto Ifirma);
|
IfirmaSettingsDto Ifirma,
|
||||||
|
InPostSettingsDto InPost,
|
||||||
|
DhlSettingsDto Dhl,
|
||||||
|
DpdSettingsDto Dpd,
|
||||||
|
PayUSettingsDto PayU,
|
||||||
|
bool ShippingUseSandbox);
|
||||||
|
|
||||||
public sealed record LocaleFlagsDto(bool Pl, bool En, bool De, bool Cs);
|
public sealed record LocaleFlagsDto(bool Pl, bool En, bool De, bool Cs);
|
||||||
|
|
||||||
@@ -48,6 +53,39 @@ public sealed record IfirmaSettingsDto(
|
|||||||
string? InvoiceSeries,
|
string? InvoiceSeries,
|
||||||
bool AutoInvoice);
|
bool AutoInvoice);
|
||||||
|
|
||||||
|
public sealed record InPostSettingsDto(
|
||||||
|
bool HasShipXToken,
|
||||||
|
string? OrganizationId,
|
||||||
|
string? ShipXBaseUrl,
|
||||||
|
bool HasGeowidgetPublicToken,
|
||||||
|
bool HasGeowidgetSandboxToken,
|
||||||
|
string? GeowidgetConfig,
|
||||||
|
string? GeowidgetSdkBaseUrl);
|
||||||
|
|
||||||
|
public sealed record DhlSettingsDto(
|
||||||
|
bool HasApiKey,
|
||||||
|
bool HasApiSecret,
|
||||||
|
string? ApiBaseUrl,
|
||||||
|
string? WidgetUrl);
|
||||||
|
|
||||||
|
public sealed record DpdSettingsDto(
|
||||||
|
string? Login,
|
||||||
|
bool HasPassword,
|
||||||
|
string? Fid,
|
||||||
|
string? ApiBaseUrl,
|
||||||
|
string? WidgetUrl);
|
||||||
|
|
||||||
|
public sealed record PayUSettingsDto(
|
||||||
|
bool UseSandbox,
|
||||||
|
string? PosId,
|
||||||
|
string? ClientId,
|
||||||
|
bool HasClientSecret,
|
||||||
|
bool HasSecondKey,
|
||||||
|
string? OauthUrl,
|
||||||
|
string? OrdersUrl,
|
||||||
|
string? NotifyUrl,
|
||||||
|
string? ContinueUrl);
|
||||||
|
|
||||||
public sealed record GetStoreSettingsQuery() : IRequest<StoreSettingsDto>;
|
public sealed record GetStoreSettingsQuery() : IRequest<StoreSettingsDto>;
|
||||||
|
|
||||||
public sealed class GetStoreSettingsQueryHandler(IAppDbContext db)
|
public sealed class GetStoreSettingsQueryHandler(IAppDbContext db)
|
||||||
@@ -98,7 +136,37 @@ public sealed class GetStoreSettingsQueryHandler(IAppDbContext db)
|
|||||||
s.IfirmaLogin,
|
s.IfirmaLogin,
|
||||||
!string.IsNullOrEmpty(s.IfirmaApiKey),
|
!string.IsNullOrEmpty(s.IfirmaApiKey),
|
||||||
s.IfirmaInvoiceSeries,
|
s.IfirmaInvoiceSeries,
|
||||||
s.IfirmaAutoInvoice));
|
s.IfirmaAutoInvoice),
|
||||||
|
new InPostSettingsDto(
|
||||||
|
!string.IsNullOrEmpty(s.InPostShipXToken),
|
||||||
|
s.InPostOrganizationId,
|
||||||
|
s.InPostShipXBaseUrl,
|
||||||
|
!string.IsNullOrEmpty(s.InPostGeowidgetPublicToken),
|
||||||
|
!string.IsNullOrEmpty(s.InPostGeowidgetSandboxToken),
|
||||||
|
s.InPostGeowidgetConfig,
|
||||||
|
s.InPostGeowidgetSdkBaseUrl),
|
||||||
|
new DhlSettingsDto(
|
||||||
|
!string.IsNullOrEmpty(s.DhlApiKey),
|
||||||
|
!string.IsNullOrEmpty(s.DhlApiSecret),
|
||||||
|
s.DhlApiBaseUrl,
|
||||||
|
s.DhlWidgetUrl),
|
||||||
|
new DpdSettingsDto(
|
||||||
|
s.DpdLogin,
|
||||||
|
!string.IsNullOrEmpty(s.DpdPassword),
|
||||||
|
s.DpdFid,
|
||||||
|
s.DpdApiBaseUrl,
|
||||||
|
s.DpdWidgetUrl),
|
||||||
|
new PayUSettingsDto(
|
||||||
|
s.PayUUseSandbox,
|
||||||
|
s.PayUPosId,
|
||||||
|
s.PayUClientId,
|
||||||
|
!string.IsNullOrEmpty(s.PayUClientSecret),
|
||||||
|
!string.IsNullOrEmpty(s.PayUSecondKey),
|
||||||
|
s.PayUOauthUrl,
|
||||||
|
s.PayUOrdersUrl,
|
||||||
|
s.PayUNotifyUrl,
|
||||||
|
s.PayUContinueUrl),
|
||||||
|
s.ShippingUseSandbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record UpdateStoreSettingsCommand(
|
public sealed record UpdateStoreSettingsCommand(
|
||||||
@@ -116,7 +184,12 @@ public sealed record UpdateStoreSettingsCommand(
|
|||||||
string EmailReplyTo,
|
string EmailReplyTo,
|
||||||
UpdateAllegroSettingsDto? Allegro,
|
UpdateAllegroSettingsDto? Allegro,
|
||||||
UpdateAmazonSettingsDto? Amazon,
|
UpdateAmazonSettingsDto? Amazon,
|
||||||
UpdateIfirmaSettingsDto? Ifirma) : IRequest<StoreSettingsDto>;
|
UpdateIfirmaSettingsDto? Ifirma,
|
||||||
|
UpdateInPostSettingsDto? InPost,
|
||||||
|
UpdateDhlSettingsDto? Dhl,
|
||||||
|
UpdateDpdSettingsDto? Dpd,
|
||||||
|
UpdatePayUSettingsDto? PayU,
|
||||||
|
bool ShippingUseSandbox) : IRequest<StoreSettingsDto>;
|
||||||
|
|
||||||
public sealed record UpdateAllegroSettingsDto(
|
public sealed record UpdateAllegroSettingsDto(
|
||||||
bool Enabled,
|
bool Enabled,
|
||||||
@@ -138,6 +211,39 @@ public sealed record UpdateIfirmaSettingsDto(
|
|||||||
string? InvoiceSeries,
|
string? InvoiceSeries,
|
||||||
bool AutoInvoice);
|
bool AutoInvoice);
|
||||||
|
|
||||||
|
public sealed record UpdateInPostSettingsDto(
|
||||||
|
string? ShipXToken,
|
||||||
|
string? OrganizationId,
|
||||||
|
string? ShipXBaseUrl,
|
||||||
|
string? GeowidgetPublicToken,
|
||||||
|
string? GeowidgetSandboxToken,
|
||||||
|
string? GeowidgetConfig,
|
||||||
|
string? GeowidgetSdkBaseUrl);
|
||||||
|
|
||||||
|
public sealed record UpdateDhlSettingsDto(
|
||||||
|
string? ApiKey,
|
||||||
|
string? ApiSecret,
|
||||||
|
string? ApiBaseUrl,
|
||||||
|
string? WidgetUrl);
|
||||||
|
|
||||||
|
public sealed record UpdateDpdSettingsDto(
|
||||||
|
string? Login,
|
||||||
|
string? Password,
|
||||||
|
string? Fid,
|
||||||
|
string? ApiBaseUrl,
|
||||||
|
string? WidgetUrl);
|
||||||
|
|
||||||
|
public sealed record UpdatePayUSettingsDto(
|
||||||
|
bool UseSandbox,
|
||||||
|
string? PosId,
|
||||||
|
string? ClientId,
|
||||||
|
string? ClientSecret,
|
||||||
|
string? SecondKey,
|
||||||
|
string? OauthUrl,
|
||||||
|
string? OrdersUrl,
|
||||||
|
string? NotifyUrl,
|
||||||
|
string? ContinueUrl);
|
||||||
|
|
||||||
public sealed class UpdateStoreSettingsCommandHandler(IAppDbContext db)
|
public sealed class UpdateStoreSettingsCommandHandler(IAppDbContext db)
|
||||||
: IRequestHandler<UpdateStoreSettingsCommand, StoreSettingsDto>
|
: IRequestHandler<UpdateStoreSettingsCommand, StoreSettingsDto>
|
||||||
{
|
{
|
||||||
@@ -175,16 +281,14 @@ public sealed class UpdateStoreSettingsCommandHandler(IAppDbContext db)
|
|||||||
row.EmailFromName = request.EmailFromName.Trim();
|
row.EmailFromName = request.EmailFromName.Trim();
|
||||||
row.EmailFromAddress = request.EmailFromAddress.Trim();
|
row.EmailFromAddress = request.EmailFromAddress.Trim();
|
||||||
row.EmailReplyTo = request.EmailReplyTo.Trim();
|
row.EmailReplyTo = request.EmailReplyTo.Trim();
|
||||||
|
row.ShippingUseSandbox = request.ShippingUseSandbox;
|
||||||
|
|
||||||
if (request.Allegro is not null)
|
if (request.Allegro is not null)
|
||||||
{
|
{
|
||||||
row.AllegroEnabled = request.Allegro.Enabled;
|
row.AllegroEnabled = request.Allegro.Enabled;
|
||||||
row.AllegroClientId = TrimOrNull(request.Allegro.ClientId);
|
row.AllegroClientId = TrimOrNull(request.Allegro.ClientId);
|
||||||
row.AllegroSellerId = TrimOrNull(request.Allegro.SellerId);
|
row.AllegroSellerId = TrimOrNull(request.Allegro.SellerId);
|
||||||
if (!string.IsNullOrWhiteSpace(request.Allegro.ClientSecret))
|
SetSecret(request.Allegro.ClientSecret, v => row.AllegroClientSecret = v);
|
||||||
{
|
|
||||||
row.AllegroClientSecret = request.Allegro.ClientSecret.Trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Amazon is not null)
|
if (request.Amazon is not null)
|
||||||
@@ -192,15 +296,8 @@ public sealed class UpdateStoreSettingsCommandHandler(IAppDbContext db)
|
|||||||
row.AmazonEnabled = request.Amazon.Enabled;
|
row.AmazonEnabled = request.Amazon.Enabled;
|
||||||
row.AmazonSellerId = TrimOrNull(request.Amazon.SellerId);
|
row.AmazonSellerId = TrimOrNull(request.Amazon.SellerId);
|
||||||
row.AmazonMarketplaceId = TrimOrNull(request.Amazon.MarketplaceId);
|
row.AmazonMarketplaceId = TrimOrNull(request.Amazon.MarketplaceId);
|
||||||
if (!string.IsNullOrWhiteSpace(request.Amazon.AccessKey))
|
SetSecret(request.Amazon.AccessKey, v => row.AmazonAccessKey = v);
|
||||||
{
|
SetSecret(request.Amazon.SecretKey, v => row.AmazonSecretKey = v);
|
||||||
row.AmazonAccessKey = request.Amazon.AccessKey.Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.Amazon.SecretKey))
|
|
||||||
{
|
|
||||||
row.AmazonSecretKey = request.Amazon.SecretKey.Trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Ifirma is not null)
|
if (request.Ifirma is not null)
|
||||||
@@ -209,10 +306,52 @@ public sealed class UpdateStoreSettingsCommandHandler(IAppDbContext db)
|
|||||||
row.IfirmaLogin = TrimOrNull(request.Ifirma.Login);
|
row.IfirmaLogin = TrimOrNull(request.Ifirma.Login);
|
||||||
row.IfirmaInvoiceSeries = TrimOrNull(request.Ifirma.InvoiceSeries);
|
row.IfirmaInvoiceSeries = TrimOrNull(request.Ifirma.InvoiceSeries);
|
||||||
row.IfirmaAutoInvoice = request.Ifirma.AutoInvoice;
|
row.IfirmaAutoInvoice = request.Ifirma.AutoInvoice;
|
||||||
if (!string.IsNullOrWhiteSpace(request.Ifirma.ApiKey))
|
SetSecret(request.Ifirma.ApiKey, v => row.IfirmaApiKey = v);
|
||||||
{
|
}
|
||||||
row.IfirmaApiKey = request.Ifirma.ApiKey.Trim();
|
|
||||||
}
|
if (request.InPost is not null)
|
||||||
|
{
|
||||||
|
row.InPostOrganizationId = TrimOrNull(request.InPost.OrganizationId);
|
||||||
|
row.InPostShipXBaseUrl = TrimOrNull(request.InPost.ShipXBaseUrl);
|
||||||
|
row.InPostGeowidgetConfig = TrimOrNull(request.InPost.GeowidgetConfig);
|
||||||
|
row.InPostGeowidgetSdkBaseUrl = TrimOrNull(request.InPost.GeowidgetSdkBaseUrl);
|
||||||
|
SetSecret(request.InPost.ShipXToken, v => row.InPostShipXToken = v);
|
||||||
|
SetSecret(
|
||||||
|
request.InPost.GeowidgetPublicToken,
|
||||||
|
v => row.InPostGeowidgetPublicToken = v);
|
||||||
|
SetSecret(
|
||||||
|
request.InPost.GeowidgetSandboxToken,
|
||||||
|
v => row.InPostGeowidgetSandboxToken = v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Dhl is not null)
|
||||||
|
{
|
||||||
|
row.DhlApiBaseUrl = TrimOrNull(request.Dhl.ApiBaseUrl);
|
||||||
|
row.DhlWidgetUrl = TrimOrNull(request.Dhl.WidgetUrl);
|
||||||
|
SetSecret(request.Dhl.ApiKey, v => row.DhlApiKey = v);
|
||||||
|
SetSecret(request.Dhl.ApiSecret, v => row.DhlApiSecret = v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Dpd is not null)
|
||||||
|
{
|
||||||
|
row.DpdLogin = TrimOrNull(request.Dpd.Login);
|
||||||
|
row.DpdFid = TrimOrNull(request.Dpd.Fid);
|
||||||
|
row.DpdApiBaseUrl = TrimOrNull(request.Dpd.ApiBaseUrl);
|
||||||
|
row.DpdWidgetUrl = TrimOrNull(request.Dpd.WidgetUrl);
|
||||||
|
SetSecret(request.Dpd.Password, v => row.DpdPassword = v);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.PayU is not null)
|
||||||
|
{
|
||||||
|
row.PayUUseSandbox = request.PayU.UseSandbox;
|
||||||
|
row.PayUPosId = TrimOrNull(request.PayU.PosId);
|
||||||
|
row.PayUClientId = TrimOrNull(request.PayU.ClientId);
|
||||||
|
row.PayUOauthUrl = TrimOrNull(request.PayU.OauthUrl);
|
||||||
|
row.PayUOrdersUrl = TrimOrNull(request.PayU.OrdersUrl);
|
||||||
|
row.PayUNotifyUrl = TrimOrNull(request.PayU.NotifyUrl);
|
||||||
|
row.PayUContinueUrl = TrimOrNull(request.PayU.ContinueUrl);
|
||||||
|
SetSecret(request.PayU.ClientSecret, v => row.PayUClientSecret = v);
|
||||||
|
SetSecret(request.PayU.SecondKey, v => row.PayUSecondKey = v);
|
||||||
}
|
}
|
||||||
|
|
||||||
row.UpdatedAt = DateTime.UtcNow;
|
row.UpdatedAt = DateTime.UtcNow;
|
||||||
@@ -244,4 +383,12 @@ public sealed class UpdateStoreSettingsCommandHandler(IAppDbContext db)
|
|||||||
|
|
||||||
private static string? TrimOrNull(string? value) =>
|
private static string? TrimOrNull(string? value) =>
|
||||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
|
||||||
|
private static void SetSecret(string? incoming, Action<string> assign)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(incoming))
|
||||||
|
{
|
||||||
|
assign(incoming.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
namespace iLED.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime PayU + carrier credentials from StoreSettings,
|
||||||
|
/// falling back to appsettings when a DB field is empty.
|
||||||
|
/// </summary>
|
||||||
|
public interface IIntegrationSettingsAccessor
|
||||||
|
{
|
||||||
|
Task<IntegrationRuntimeSettings> GetAsync(
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class IntegrationRuntimeSettings
|
||||||
|
{
|
||||||
|
public bool ShippingUseSandbox { get; init; } = true;
|
||||||
|
|
||||||
|
public bool CarrierInPost { get; init; } = true;
|
||||||
|
|
||||||
|
public bool CarrierDhl { get; init; } = true;
|
||||||
|
|
||||||
|
public bool CarrierDpd { get; init; } = true;
|
||||||
|
|
||||||
|
public string? InPostShipXToken { get; init; }
|
||||||
|
|
||||||
|
public string? InPostOrganizationId { get; init; }
|
||||||
|
|
||||||
|
public string? InPostShipXBaseUrl { get; init; }
|
||||||
|
|
||||||
|
public string? InPostGeowidgetPublicToken { get; init; }
|
||||||
|
|
||||||
|
public string? InPostGeowidgetSandboxToken { get; init; }
|
||||||
|
|
||||||
|
public string? InPostGeowidgetConfig { get; init; }
|
||||||
|
|
||||||
|
public string? InPostGeowidgetSdkBaseUrl { get; init; }
|
||||||
|
|
||||||
|
public string? DhlApiKey { get; init; }
|
||||||
|
|
||||||
|
public string? DhlApiSecret { get; init; }
|
||||||
|
|
||||||
|
public string? DhlApiBaseUrl { get; init; }
|
||||||
|
|
||||||
|
public string? DhlWidgetUrl { get; init; }
|
||||||
|
|
||||||
|
public string? DpdLogin { get; init; }
|
||||||
|
|
||||||
|
public string? DpdPassword { get; init; }
|
||||||
|
|
||||||
|
public string? DpdFid { get; init; }
|
||||||
|
|
||||||
|
public string? DpdApiBaseUrl { get; init; }
|
||||||
|
|
||||||
|
public string? DpdWidgetUrl { get; init; }
|
||||||
|
|
||||||
|
public bool PayUUseSandbox { get; init; } = true;
|
||||||
|
|
||||||
|
public string? PayUPosId { get; init; }
|
||||||
|
|
||||||
|
public string? PayUClientId { get; init; }
|
||||||
|
|
||||||
|
public string? PayUClientSecret { get; init; }
|
||||||
|
|
||||||
|
public string? PayUSecondKey { get; init; }
|
||||||
|
|
||||||
|
public string? PayUOauthUrl { get; init; }
|
||||||
|
|
||||||
|
public string? PayUOrdersUrl { get; init; }
|
||||||
|
|
||||||
|
public string? PayUNotifyUrl { get; init; }
|
||||||
|
|
||||||
|
public string? PayUContinueUrl { get; init; }
|
||||||
|
}
|
||||||
@@ -12,7 +12,8 @@ public interface IShippingService
|
|||||||
ShipmentRequest request,
|
ShipmentRequest request,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
ShippingWidgetConfigDto GetWidgetConfig();
|
Task<ShippingWidgetConfigDto> GetWidgetConfigAsync(
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record ShippingParcelPointSearch(
|
public sealed record ShippingParcelPointSearch(
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ public sealed class InitiatePaymentCommandHandler(
|
|||||||
IOrderService orderService,
|
IOrderService orderService,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IAppDbContext db,
|
IAppDbContext db,
|
||||||
IConfiguration configuration)
|
IConfiguration configuration,
|
||||||
|
IIntegrationSettingsAccessor integrationSettings)
|
||||||
: IRequestHandler<InitiatePaymentCommand, PaymentResultDto>
|
: IRequestHandler<InitiatePaymentCommand, PaymentResultDto>
|
||||||
{
|
{
|
||||||
public async Task<PaymentResultDto> Handle(
|
public async Task<PaymentResultDto> Handle(
|
||||||
@@ -27,10 +28,11 @@ public sealed class InitiatePaymentCommandHandler(
|
|||||||
var order = await orderService.GetOrderAsync(
|
var order = await orderService.GetOrderAsync(
|
||||||
request.OrderId, cancellationToken);
|
request.OrderId, cancellationToken);
|
||||||
|
|
||||||
|
var settings = await integrationSettings.GetAsync(cancellationToken);
|
||||||
var shopBase = configuration["Seo:ShopBaseUrl"] ?? "http://localhost:4200";
|
var shopBase = configuration["Seo:ShopBaseUrl"] ?? "http://localhost:4200";
|
||||||
var notifyUrl = configuration["PayU:NotifyUrl"]
|
var notifyUrl = settings.PayUNotifyUrl
|
||||||
?? "http://localhost:5000/api/v1/payments/webhook";
|
?? "http://localhost:5000/api/v1/payments/webhook";
|
||||||
var continueBase = configuration["PayU:ContinueUrl"] ?? $"{shopBase}/order";
|
var continueBase = settings.PayUContinueUrl ?? $"{shopBase}/order";
|
||||||
|
|
||||||
var paymentRequest = new PaymentInitiationRequest(
|
var paymentRequest = new PaymentInitiationRequest(
|
||||||
order.Id,
|
order.Id,
|
||||||
@@ -69,14 +71,15 @@ public sealed record CompleteSandboxPaymentCommand(Guid OrderId) : IRequest<Paym
|
|||||||
public sealed class CompleteSandboxPaymentCommandHandler(
|
public sealed class CompleteSandboxPaymentCommandHandler(
|
||||||
IOrderService orderService,
|
IOrderService orderService,
|
||||||
IAppDbContext db,
|
IAppDbContext db,
|
||||||
IConfiguration configuration)
|
IIntegrationSettingsAccessor integrationSettings)
|
||||||
: IRequestHandler<CompleteSandboxPaymentCommand, PaymentResultDto>
|
: IRequestHandler<CompleteSandboxPaymentCommand, PaymentResultDto>
|
||||||
{
|
{
|
||||||
public async Task<PaymentResultDto> Handle(
|
public async Task<PaymentResultDto> Handle(
|
||||||
CompleteSandboxPaymentCommand request,
|
CompleteSandboxPaymentCommand request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!configuration.GetValue("PayU:UseSandbox", true))
|
var settings = await integrationSettings.GetAsync(cancellationToken);
|
||||||
|
if (!settings.PayUUseSandbox)
|
||||||
{
|
{
|
||||||
return new PaymentResultDto(
|
return new PaymentResultDto(
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public sealed class GetShippingWidgetConfigQueryHandler(IShippingService shippin
|
|||||||
public Task<ShippingWidgetConfigDto> Handle(
|
public Task<ShippingWidgetConfigDto> Handle(
|
||||||
GetShippingWidgetConfigQuery request,
|
GetShippingWidgetConfigQuery request,
|
||||||
CancellationToken cancellationToken) =>
|
CancellationToken cancellationToken) =>
|
||||||
Task.FromResult(shipping.GetWidgetConfig());
|
shipping.GetWidgetConfigAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record CreateShipmentLabelCommand(Guid OrderId)
|
public sealed record CreateShipmentLabelCommand(Guid OrderId)
|
||||||
|
|||||||
@@ -73,4 +73,58 @@ public class StoreSettings : BaseEntity
|
|||||||
public string? IfirmaInvoiceSeries { get; set; }
|
public string? IfirmaInvoiceSeries { get; set; }
|
||||||
|
|
||||||
public bool IfirmaAutoInvoice { get; set; }
|
public bool IfirmaAutoInvoice { get; set; }
|
||||||
|
|
||||||
|
// ── Shipping credentials ─────────────────────────────────
|
||||||
|
public bool ShippingUseSandbox { get; set; } = true;
|
||||||
|
|
||||||
|
public string? InPostShipXToken { get; set; }
|
||||||
|
|
||||||
|
public string? InPostOrganizationId { get; set; }
|
||||||
|
|
||||||
|
public string? InPostShipXBaseUrl { get; set; }
|
||||||
|
|
||||||
|
public string? InPostGeowidgetPublicToken { get; set; }
|
||||||
|
|
||||||
|
public string? InPostGeowidgetSandboxToken { get; set; }
|
||||||
|
|
||||||
|
public string? InPostGeowidgetConfig { get; set; }
|
||||||
|
|
||||||
|
public string? InPostGeowidgetSdkBaseUrl { get; set; }
|
||||||
|
|
||||||
|
public string? DhlApiKey { get; set; }
|
||||||
|
|
||||||
|
public string? DhlApiSecret { get; set; }
|
||||||
|
|
||||||
|
public string? DhlApiBaseUrl { get; set; }
|
||||||
|
|
||||||
|
public string? DhlWidgetUrl { get; set; }
|
||||||
|
|
||||||
|
public string? DpdLogin { get; set; }
|
||||||
|
|
||||||
|
public string? DpdPassword { get; set; }
|
||||||
|
|
||||||
|
public string? DpdFid { get; set; }
|
||||||
|
|
||||||
|
public string? DpdApiBaseUrl { get; set; }
|
||||||
|
|
||||||
|
public string? DpdWidgetUrl { get; set; }
|
||||||
|
|
||||||
|
// ── PayU ─────────────────────────────────────────────────
|
||||||
|
public bool PayUUseSandbox { get; set; } = true;
|
||||||
|
|
||||||
|
public string? PayUPosId { get; set; }
|
||||||
|
|
||||||
|
public string? PayUClientId { get; set; }
|
||||||
|
|
||||||
|
public string? PayUClientSecret { get; set; }
|
||||||
|
|
||||||
|
public string? PayUSecondKey { get; set; }
|
||||||
|
|
||||||
|
public string? PayUOauthUrl { get; set; }
|
||||||
|
|
||||||
|
public string? PayUOrdersUrl { get; set; }
|
||||||
|
|
||||||
|
public string? PayUNotifyUrl { get; set; }
|
||||||
|
|
||||||
|
public string? PayUContinueUrl { get; set; }
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,7 @@ public static class DependencyInjection
|
|||||||
{
|
{
|
||||||
client.Timeout = TimeSpan.FromSeconds(60);
|
client.Timeout = TimeSpan.FromSeconds(60);
|
||||||
});
|
});
|
||||||
|
services.AddScoped<IIntegrationSettingsAccessor, IntegrationSettingsAccessor>();
|
||||||
services.AddScoped<IPaymentService, PayUPaymentService>();
|
services.AddScoped<IPaymentService, PayUPaymentService>();
|
||||||
services.AddHostedService<PayUPaymentSyncBackgroundService>();
|
services.AddHostedService<PayUPaymentSyncBackgroundService>();
|
||||||
services.AddScoped<ShippingCarrierClients>();
|
services.AddScoped<ShippingCarrierClients>();
|
||||||
|
|||||||
@@ -213,6 +213,33 @@ namespace iLED.Infrastructure.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("DhlApiBaseUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("DhlApiKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("DhlApiSecret")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("DhlWidgetUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("DpdApiBaseUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("DpdFid")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("DpdLogin")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("DpdPassword")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("DpdWidgetUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("EmailFromAddress")
|
b.Property<string>("EmailFromAddress")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
@@ -240,6 +267,27 @@ namespace iLED.Infrastructure.Migrations
|
|||||||
b.Property<string>("IfirmaLogin")
|
b.Property<string>("IfirmaLogin")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("InPostGeowidgetConfig")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("InPostGeowidgetPublicToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("InPostGeowidgetSandboxToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("InPostGeowidgetSdkBaseUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("InPostOrganizationId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("InPostShipXBaseUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("InPostShipXToken")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("LegalName")
|
b.Property<string>("LegalName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
@@ -259,6 +307,36 @@ namespace iLED.Infrastructure.Migrations
|
|||||||
b.Property<int>("LowStockThreshold")
|
b.Property<int>("LowStockThreshold")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("PayUClientId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PayUClientSecret")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PayUContinueUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PayUNotifyUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PayUOauthUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PayUOrdersUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PayUPosId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PayUSecondKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("PayUUseSandbox")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("ShippingUseSandbox")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("StoreName")
|
b.Property<string>("StoreName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|||||||
+2229
File diff suppressed because it is too large.
Load diff
+280
@@ -0,0 +1,280 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace iLED.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class ShippingPayUIntegrations : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DhlApiBaseUrl",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DhlApiKey",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DhlApiSecret",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DhlWidgetUrl",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DpdApiBaseUrl",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DpdFid",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DpdLogin",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DpdPassword",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DpdWidgetUrl",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "InPostGeowidgetConfig",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "InPostGeowidgetPublicToken",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "InPostGeowidgetSandboxToken",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "InPostGeowidgetSdkBaseUrl",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "InPostOrganizationId",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "InPostShipXBaseUrl",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "InPostShipXToken",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PayUClientId",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PayUClientSecret",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PayUContinueUrl",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PayUNotifyUrl",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PayUOauthUrl",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PayUOrdersUrl",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PayUPosId",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PayUSecondKey",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "text",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "PayUUseSandbox",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "ShippingUseSandbox",
|
||||||
|
table: "StoreSettings",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DhlApiBaseUrl",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DhlApiKey",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DhlApiSecret",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DhlWidgetUrl",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DpdApiBaseUrl",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DpdFid",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DpdLogin",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DpdPassword",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DpdWidgetUrl",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "InPostGeowidgetConfig",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "InPostGeowidgetPublicToken",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "InPostGeowidgetSandboxToken",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "InPostGeowidgetSdkBaseUrl",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "InPostOrganizationId",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "InPostShipXBaseUrl",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "InPostShipXToken",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PayUClientId",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PayUClientSecret",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PayUContinueUrl",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PayUNotifyUrl",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PayUOauthUrl",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PayUOrdersUrl",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PayUPosId",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PayUSecondKey",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PayUUseSandbox",
|
||||||
|
table: "StoreSettings");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ShippingUseSandbox",
|
||||||
|
table: "StoreSettings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ using System.Text.Json;
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using iLED.Application.Common.Interfaces;
|
using iLED.Application.Common.Interfaces;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace iLED.Infrastructure.Services;
|
namespace iLED.Infrastructure.Services;
|
||||||
@@ -15,7 +14,7 @@ namespace iLED.Infrastructure.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class DpdPudoClient(
|
public sealed class DpdPudoClient(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IConfiguration configuration,
|
IIntegrationSettingsAccessor integrationSettings,
|
||||||
ILogger<DpdPudoClient> logger)
|
ILogger<DpdPudoClient> logger)
|
||||||
{
|
{
|
||||||
private const string DefaultBaseUrl = "https://pudofinder.dpd.com.pl";
|
private const string DefaultBaseUrl = "https://pudofinder.dpd.com.pl";
|
||||||
@@ -90,7 +89,7 @@ public sealed class DpdPudoClient(
|
|||||||
private async Task<IReadOnlyList<PudoPoint>> GetCatalogueAsync(
|
private async Task<IReadOnlyList<PudoPoint>> GetCatalogueAsync(
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var baseUrl = GetBaseUrl();
|
var baseUrl = await GetBaseUrlAsync(cancellationToken);
|
||||||
if (Cache.TryGetValue(baseUrl, out var cached) &&
|
if (Cache.TryGetValue(baseUrl, out var cached) &&
|
||||||
cached.ExpiresAt > DateTimeOffset.UtcNow)
|
cached.ExpiresAt > DateTimeOffset.UtcNow)
|
||||||
{
|
{
|
||||||
@@ -130,7 +129,7 @@ public sealed class DpdPudoClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var client = httpClientFactory.CreateClient(nameof(DpdPudoClient));
|
var client = httpClientFactory.CreateClient(nameof(DpdPudoClient));
|
||||||
var baseUrl = GetBaseUrl();
|
var baseUrl = await GetBaseUrlAsync(cancellationToken);
|
||||||
var url = $"{baseUrl}/ajax/points/details?lang=pl";
|
var url = $"{baseUrl}/ajax/points/details?lang=pl";
|
||||||
|
|
||||||
using var content = new FormUrlEncodedContent(
|
using var content = new FormUrlEncodedContent(
|
||||||
@@ -161,9 +160,10 @@ public sealed class DpdPudoClient(
|
|||||||
StringComparer.OrdinalIgnoreCase);
|
StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetBaseUrl()
|
private async Task<string> GetBaseUrlAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var configured = configuration["Shipping:Dpd:WidgetUrl"];
|
var settings = await integrationSettings.GetAsync(cancellationToken);
|
||||||
|
var configured = settings.DpdWidgetUrl;
|
||||||
if (string.IsNullOrWhiteSpace(configured))
|
if (string.IsNullOrWhiteSpace(configured))
|
||||||
{
|
{
|
||||||
return DefaultBaseUrl;
|
return DefaultBaseUrl;
|
||||||
|
|||||||
@@ -11,19 +11,20 @@ namespace iLED.Infrastructure.Services;
|
|||||||
|
|
||||||
public sealed class PayUPaymentService(
|
public sealed class PayUPaymentService(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IConfiguration configuration,
|
IIntegrationSettingsAccessor integrationSettings,
|
||||||
ILogger<PayUPaymentService> logger) : IPaymentService
|
ILogger<PayUPaymentService> logger) : IPaymentService
|
||||||
{
|
{
|
||||||
public async Task<PaymentInitiationResult> InitiatePaymentAsync(
|
public async Task<PaymentInitiationResult> InitiatePaymentAsync(
|
||||||
PaymentInitiationRequest request,
|
PaymentInitiationRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var posId = configuration["PayU:PosId"];
|
var settings = await integrationSettings.GetAsync(cancellationToken);
|
||||||
var clientId = configuration["PayU:ClientId"];
|
var posId = settings.PayUPosId;
|
||||||
var clientSecret = configuration["PayU:ClientSecret"];
|
var clientId = settings.PayUClientId;
|
||||||
var oauthUrl = configuration["PayU:OAuthUrl"];
|
var clientSecret = settings.PayUClientSecret;
|
||||||
var ordersUrl = configuration["PayU:OrdersUrl"];
|
var oauthUrl = settings.PayUOauthUrl;
|
||||||
var useSandbox = configuration.GetValue("PayU:UseSandbox", true);
|
var ordersUrl = settings.PayUOrdersUrl;
|
||||||
|
var useSandbox = settings.PayUUseSandbox;
|
||||||
var hasCredentials =
|
var hasCredentials =
|
||||||
!string.IsNullOrWhiteSpace(posId) &&
|
!string.IsNullOrWhiteSpace(posId) &&
|
||||||
!string.IsNullOrWhiteSpace(clientId) &&
|
!string.IsNullOrWhiteSpace(clientId) &&
|
||||||
@@ -40,7 +41,7 @@ public sealed class PayUPaymentService(
|
|||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
"Configure PayU:PosId, ClientId, ClientSecret and URLs.");
|
"Configure PayU credentials in Settings → Integrations.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var externalId = $"PAYU-{Guid.NewGuid():N}";
|
var externalId = $"PAYU-{Guid.NewGuid():N}";
|
||||||
@@ -211,10 +212,11 @@ public sealed class PayUPaymentService(
|
|||||||
"No PayU order id to sync.");
|
"No PayU order id to sync.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientId = configuration["PayU:ClientId"];
|
var settings = await integrationSettings.GetAsync(cancellationToken);
|
||||||
var clientSecret = configuration["PayU:ClientSecret"];
|
var clientId = settings.PayUClientId;
|
||||||
var oauthUrl = configuration["PayU:OAuthUrl"];
|
var clientSecret = settings.PayUClientSecret;
|
||||||
var ordersUrl = configuration["PayU:OrdersUrl"];
|
var oauthUrl = settings.PayUOauthUrl;
|
||||||
|
var ordersUrl = settings.PayUOrdersUrl;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(clientId) ||
|
if (string.IsNullOrWhiteSpace(clientId) ||
|
||||||
string.IsNullOrWhiteSpace(clientSecret) ||
|
string.IsNullOrWhiteSpace(clientSecret) ||
|
||||||
@@ -318,20 +320,21 @@ public sealed class PayUPaymentService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<PaymentWebhookResult> HandleWebhookAsync(
|
public async Task<PaymentWebhookResult> HandleWebhookAsync(
|
||||||
string payload,
|
string payload,
|
||||||
string signature,
|
string signature,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var secondKey = configuration["PayU:SecondKey"];
|
var settings = await integrationSettings.GetAsync(cancellationToken);
|
||||||
|
var secondKey = settings.PayUSecondKey;
|
||||||
if (!string.IsNullOrWhiteSpace(secondKey) &&
|
if (!string.IsNullOrWhiteSpace(secondKey) &&
|
||||||
!VerifySignature(payload, signature, secondKey))
|
!VerifySignature(payload, signature, secondKey))
|
||||||
{
|
{
|
||||||
logger.LogWarning("PayU webhook signature mismatch");
|
logger.LogWarning("PayU webhook signature mismatch");
|
||||||
return Task.FromResult(new PaymentWebhookResult(
|
return new PaymentWebhookResult(
|
||||||
false, null, null, false, "Invalid signature"));
|
false, null, null, false, "Invalid signature");
|
||||||
}
|
}
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(payload);
|
using var doc = JsonDocument.Parse(payload);
|
||||||
@@ -362,18 +365,18 @@ public sealed class PayUPaymentService(
|
|||||||
"COMPLETED",
|
"COMPLETED",
|
||||||
StringComparison.OrdinalIgnoreCase);
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
return Task.FromResult(new PaymentWebhookResult(
|
return new PaymentWebhookResult(
|
||||||
true,
|
true,
|
||||||
orderId,
|
orderId,
|
||||||
payuOrderId ?? extId,
|
payuOrderId ?? extId,
|
||||||
completed,
|
completed,
|
||||||
null));
|
null);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "PayU webhook parse error");
|
logger.LogError(ex, "PayU webhook parse error");
|
||||||
return Task.FromResult(new PaymentWebhookResult(
|
return new PaymentWebhookResult(
|
||||||
false, null, null, false, ex.Message));
|
false, null, null, false, ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
using iLED.Application.Common.Interfaces;
|
||||||
|
using iLED.Domain.Admin;
|
||||||
|
using iLED.Infrastructure.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace iLED.Infrastructure.Services;
|
||||||
|
|
||||||
|
public sealed class IntegrationSettingsAccessor(
|
||||||
|
ApplicationDbContext db,
|
||||||
|
IConfiguration configuration) : IIntegrationSettingsAccessor
|
||||||
|
{
|
||||||
|
public async Task<IntegrationRuntimeSettings> GetAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var row = await db.StoreSettings
|
||||||
|
.AsNoTracking()
|
||||||
|
.OrderBy(s => s.CreatedAt)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
return From(row, configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IntegrationRuntimeSettings From(
|
||||||
|
StoreSettings? row,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
return new IntegrationRuntimeSettings
|
||||||
|
{
|
||||||
|
ShippingUseSandbox = row?.ShippingUseSandbox
|
||||||
|
?? configuration.GetValue("Shipping:UseSandbox", true),
|
||||||
|
CarrierInPost = row?.CarrierInPost ?? true,
|
||||||
|
CarrierDhl = row?.CarrierDhl ?? true,
|
||||||
|
CarrierDpd = row?.CarrierDpd ?? true,
|
||||||
|
InPostShipXToken = Prefer(
|
||||||
|
row?.InPostShipXToken,
|
||||||
|
configuration["Shipping:InPost:ShipXToken"]),
|
||||||
|
InPostOrganizationId = Prefer(
|
||||||
|
row?.InPostOrganizationId,
|
||||||
|
configuration["Shipping:InPost:OrganizationId"]),
|
||||||
|
InPostShipXBaseUrl = Prefer(
|
||||||
|
row?.InPostShipXBaseUrl,
|
||||||
|
configuration["Shipping:InPost:ShipXBaseUrl"]),
|
||||||
|
InPostGeowidgetPublicToken = Prefer(
|
||||||
|
row?.InPostGeowidgetPublicToken,
|
||||||
|
configuration["Shipping:InPost:GeowidgetPublicToken"]),
|
||||||
|
InPostGeowidgetSandboxToken = Prefer(
|
||||||
|
row?.InPostGeowidgetSandboxToken,
|
||||||
|
configuration["Shipping:InPost:GeowidgetSandboxToken"]),
|
||||||
|
InPostGeowidgetConfig = Prefer(
|
||||||
|
row?.InPostGeowidgetConfig,
|
||||||
|
configuration["Shipping:InPost:GeowidgetConfig"]),
|
||||||
|
InPostGeowidgetSdkBaseUrl = Prefer(
|
||||||
|
row?.InPostGeowidgetSdkBaseUrl,
|
||||||
|
configuration["Shipping:InPost:GeowidgetSdkBaseUrl"]),
|
||||||
|
DhlApiKey = Prefer(
|
||||||
|
row?.DhlApiKey,
|
||||||
|
configuration["Shipping:Dhl:ApiKey"]),
|
||||||
|
DhlApiSecret = Prefer(
|
||||||
|
row?.DhlApiSecret,
|
||||||
|
configuration["Shipping:Dhl:ApiSecret"]),
|
||||||
|
DhlApiBaseUrl = Prefer(
|
||||||
|
row?.DhlApiBaseUrl,
|
||||||
|
configuration["Shipping:Dhl:ApiBaseUrl"]),
|
||||||
|
DhlWidgetUrl = Prefer(
|
||||||
|
row?.DhlWidgetUrl,
|
||||||
|
configuration["Shipping:Dhl:WidgetUrl"]),
|
||||||
|
DpdLogin = Prefer(
|
||||||
|
row?.DpdLogin,
|
||||||
|
configuration["Shipping:Dpd:Login"]),
|
||||||
|
DpdPassword = Prefer(
|
||||||
|
row?.DpdPassword,
|
||||||
|
configuration["Shipping:Dpd:Password"]),
|
||||||
|
DpdFid = Prefer(
|
||||||
|
row?.DpdFid,
|
||||||
|
configuration["Shipping:Dpd:Fid"]),
|
||||||
|
DpdApiBaseUrl = Prefer(
|
||||||
|
row?.DpdApiBaseUrl,
|
||||||
|
configuration["Shipping:Dpd:ApiBaseUrl"]),
|
||||||
|
DpdWidgetUrl = Prefer(
|
||||||
|
row?.DpdWidgetUrl,
|
||||||
|
configuration["Shipping:Dpd:WidgetUrl"]),
|
||||||
|
PayUUseSandbox = row?.PayUUseSandbox
|
||||||
|
?? configuration.GetValue("PayU:UseSandbox", true),
|
||||||
|
PayUPosId = Prefer(row?.PayUPosId, configuration["PayU:PosId"]),
|
||||||
|
PayUClientId = Prefer(
|
||||||
|
row?.PayUClientId,
|
||||||
|
configuration["PayU:ClientId"]),
|
||||||
|
PayUClientSecret = Prefer(
|
||||||
|
row?.PayUClientSecret,
|
||||||
|
configuration["PayU:ClientSecret"]),
|
||||||
|
PayUSecondKey = Prefer(
|
||||||
|
row?.PayUSecondKey,
|
||||||
|
configuration["PayU:SecondKey"]),
|
||||||
|
PayUOauthUrl = Prefer(
|
||||||
|
row?.PayUOauthUrl,
|
||||||
|
configuration["PayU:OAuthUrl"]),
|
||||||
|
PayUOrdersUrl = Prefer(
|
||||||
|
row?.PayUOrdersUrl,
|
||||||
|
configuration["PayU:OrdersUrl"]),
|
||||||
|
PayUNotifyUrl = Prefer(
|
||||||
|
row?.PayUNotifyUrl,
|
||||||
|
configuration["PayU:NotifyUrl"]),
|
||||||
|
PayUContinueUrl = Prefer(
|
||||||
|
row?.PayUContinueUrl,
|
||||||
|
configuration["PayU:ContinueUrl"]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Prefer(string? primary, string? fallback) =>
|
||||||
|
!string.IsNullOrWhiteSpace(primary) ? primary.Trim() : fallback;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ namespace iLED.Infrastructure.Services;
|
|||||||
public sealed class ShippingCarrierClients(
|
public sealed class ShippingCarrierClients(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
|
IIntegrationSettingsAccessor integrationSettings,
|
||||||
ILogger<ShippingCarrierClients> logger,
|
ILogger<ShippingCarrierClients> logger,
|
||||||
ShippingLabelStorage labelStorage)
|
ShippingLabelStorage labelStorage)
|
||||||
{
|
{
|
||||||
@@ -31,14 +32,15 @@ public sealed class ShippingCarrierClients(
|
|||||||
ShipmentRequest request,
|
ShipmentRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var token = configuration["Shipping:InPost:ShipXToken"];
|
var settings = await integrationSettings.GetAsync(cancellationToken);
|
||||||
var orgId = configuration["Shipping:InPost:OrganizationId"];
|
var token = settings.InPostShipXToken;
|
||||||
var baseUrl = configuration["Shipping:InPost:ShipXBaseUrl"]
|
var orgId = settings.InPostOrganizationId;
|
||||||
|
var baseUrl = settings.InPostShipXBaseUrl
|
||||||
?? "https://api-shipx-pl.easypack24.net";
|
?? "https://api-shipx-pl.easypack24.net";
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(orgId))
|
if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(orgId))
|
||||||
{
|
{
|
||||||
return Fail("Configure Shipping:InPost:ShipXToken and OrganizationId.");
|
return Fail("Configure InPost ShipX token and organization in Settings → Integrations.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var isLocker = request.ServiceType == ShippingServiceType.ParcelLocker;
|
var isLocker = request.ServiceType == ShippingServiceType.ParcelLocker;
|
||||||
@@ -188,15 +190,16 @@ public sealed class ShippingCarrierClients(
|
|||||||
ShipmentRequest request,
|
ShipmentRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var apiKey = configuration["Shipping:Dhl:ApiKey"];
|
var settings = await integrationSettings.GetAsync(cancellationToken);
|
||||||
var apiSecret = configuration["Shipping:Dhl:ApiSecret"];
|
var apiKey = settings.DhlApiKey;
|
||||||
var baseUrl = configuration["Shipping:Dhl:ApiBaseUrl"];
|
var apiSecret = settings.DhlApiSecret;
|
||||||
|
var baseUrl = settings.DhlApiBaseUrl;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(apiKey) ||
|
if (string.IsNullOrWhiteSpace(apiKey) ||
|
||||||
string.IsNullOrWhiteSpace(apiSecret) ||
|
string.IsNullOrWhiteSpace(apiSecret) ||
|
||||||
string.IsNullOrWhiteSpace(baseUrl))
|
string.IsNullOrWhiteSpace(baseUrl))
|
||||||
{
|
{
|
||||||
return Fail("Configure Shipping:Dhl:ApiKey, ApiSecret and ApiBaseUrl.");
|
return Fail("Configure DHL API key, secret and base URL in Settings → Integrations.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload = new
|
var payload = new
|
||||||
@@ -301,17 +304,18 @@ public sealed class ShippingCarrierClients(
|
|||||||
ShipmentRequest request,
|
ShipmentRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var login = configuration["Shipping:Dpd:Login"];
|
var settings = await integrationSettings.GetAsync(cancellationToken);
|
||||||
var password = configuration["Shipping:Dpd:Password"];
|
var login = settings.DpdLogin;
|
||||||
var fid = configuration["Shipping:Dpd:Fid"];
|
var password = settings.DpdPassword;
|
||||||
var baseUrl = configuration["Shipping:Dpd:ApiBaseUrl"];
|
var fid = settings.DpdFid;
|
||||||
|
var baseUrl = settings.DpdApiBaseUrl;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(login) ||
|
if (string.IsNullOrWhiteSpace(login) ||
|
||||||
string.IsNullOrWhiteSpace(password) ||
|
string.IsNullOrWhiteSpace(password) ||
|
||||||
string.IsNullOrWhiteSpace(fid) ||
|
string.IsNullOrWhiteSpace(fid) ||
|
||||||
string.IsNullOrWhiteSpace(baseUrl))
|
string.IsNullOrWhiteSpace(baseUrl))
|
||||||
{
|
{
|
||||||
return Fail("Configure Shipping:Dpd:Login, Password, Fid and ApiBaseUrl.");
|
return Fail("Configure DPD login, password, FID and API URL in Settings → Integrations.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// DPD PL PackageService-style XML envelope (simplified create).
|
// DPD PL PackageService-style XML envelope (simplified create).
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ namespace iLED.Infrastructure.Services;
|
|||||||
|
|
||||||
public sealed class ShippingService(
|
public sealed class ShippingService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
|
IIntegrationSettingsAccessor integrationSettings,
|
||||||
ILogger<ShippingService> logger,
|
ILogger<ShippingService> logger,
|
||||||
ShippingLabelStorage labelStorage,
|
ShippingLabelStorage labelStorage,
|
||||||
ShippingCarrierClients carrierClients,
|
ShippingCarrierClients carrierClients,
|
||||||
@@ -50,16 +51,17 @@ public sealed class ShippingService(
|
|||||||
return points;
|
return points;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ShippingWidgetConfigDto GetWidgetConfig()
|
public async Task<ShippingWidgetConfigDto> GetWidgetConfigAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var useSandbox = configuration.GetValue("Shipping:UseSandbox", true);
|
var settings = await integrationSettings.GetAsync(cancellationToken);
|
||||||
|
var useSandbox = settings.ShippingUseSandbox;
|
||||||
var inPostToken = useSandbox
|
var inPostToken = useSandbox
|
||||||
? configuration["Shipping:InPost:GeowidgetSandboxToken"]
|
? settings.InPostGeowidgetSandboxToken
|
||||||
?? configuration["Shipping:InPost:GeowidgetPublicToken"]
|
?? settings.InPostGeowidgetPublicToken
|
||||||
: configuration["Shipping:InPost:GeowidgetPublicToken"];
|
: settings.InPostGeowidgetPublicToken;
|
||||||
var inPostConfig = configuration["Shipping:InPost:GeowidgetConfig"]
|
var inPostConfig = settings.InPostGeowidgetConfig ?? "parcelCollect";
|
||||||
?? "parcelCollect";
|
var inPostSdk = settings.InPostGeowidgetSdkBaseUrl;
|
||||||
var inPostSdk = configuration["Shipping:InPost:GeowidgetSdkBaseUrl"];
|
|
||||||
if (string.IsNullOrWhiteSpace(inPostSdk))
|
if (string.IsNullOrWhiteSpace(inPostSdk))
|
||||||
{
|
{
|
||||||
inPostSdk = useSandbox
|
inPostSdk = useSandbox
|
||||||
@@ -67,11 +69,11 @@ public sealed class ShippingService(
|
|||||||
: "https://geowidget.inpost.pl";
|
: "https://geowidget.inpost.pl";
|
||||||
}
|
}
|
||||||
|
|
||||||
var dhlEnabled = configuration.GetValue("Shipping:Dhl:WidgetEnabled", true);
|
var dhlEnabled = settings.CarrierDhl;
|
||||||
var dhlUrl = configuration["Shipping:Dhl:WidgetUrl"]
|
var dhlUrl = settings.DhlWidgetUrl
|
||||||
?? "https://parcelshop.dhl.pl/mapa";
|
?? "https://parcelshop.dhl.pl/mapa";
|
||||||
var dpdEnabled = configuration.GetValue("Shipping:Dpd:WidgetEnabled", true);
|
var dpdEnabled = settings.CarrierDpd;
|
||||||
var dpdUrl = configuration["Shipping:Dpd:WidgetUrl"]
|
var dpdUrl = settings.DpdWidgetUrl
|
||||||
?? "https://pudofinder.dpd.com.pl/";
|
?? "https://pudofinder.dpd.com.pl/";
|
||||||
if (dpdUrl.Contains("pickup.dpd.pl", StringComparison.OrdinalIgnoreCase))
|
if (dpdUrl.Contains("pickup.dpd.pl", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -79,9 +81,10 @@ public sealed class ShippingService(
|
|||||||
dpdUrl = "https://pudofinder.dpd.com.pl/";
|
dpdUrl = "https://pudofinder.dpd.com.pl/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var inPostEnabled = settings.CarrierInPost;
|
||||||
return new ShippingWidgetConfigDto(
|
return new ShippingWidgetConfigDto(
|
||||||
useSandbox,
|
useSandbox,
|
||||||
string.IsNullOrWhiteSpace(inPostToken)
|
!inPostEnabled || string.IsNullOrWhiteSpace(inPostToken)
|
||||||
? null
|
? null
|
||||||
: new InPostGeowidgetConfigDto(
|
: new InPostGeowidgetConfigDto(
|
||||||
inPostToken,
|
inPostToken,
|
||||||
|
|||||||
Reference in new issue
Block a user