backup
This commit is contained in:
parent
ed599c5253
commit
45642fce51
@ -3,6 +3,12 @@ APP_ENV=dev
|
||||
APP_SECRET=347829efiubvf347fbisdc27f
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> scheb/2fa-trusted-device ###
|
||||
# Signierschlüssel für das "Gerät 30 Tage merken"-Cookie (HMAC-SHA256, >=256 Bit / 32 Byte).
|
||||
# Wird dieser Wert geändert, verlieren alle bereits vertrauten Geräte ihre Gültigkeit.
|
||||
TWO_FACTOR_TRUSTED_KEY=84ec35e78ece86bbd0d8106404ef547ead44e0cdf1d2091ee7cc6e15b5246dd7
|
||||
###< scheb/2fa-trusted-device ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
DATABASE_URL=mysql://psc:psc@mysql:3306/psc
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
@ -3,6 +3,12 @@ APP_ENV=prod
|
||||
APP_SECRET=347829efiubvf347fbisdc27f
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> scheb/2fa-trusted-device ###
|
||||
# Signierschlüssel für das "Gerät 30 Tage merken"-Cookie (HMAC-SHA256, >=256 Bit / 32 Byte).
|
||||
# Wird dieser Wert geändert, verlieren alle bereits vertrauten Geräte ihre Gültigkeit.
|
||||
TWO_FACTOR_TRUSTED_KEY=84ec35e78ece86bbd0d8106404ef547ead44e0cdf1d2091ee7cc6e15b5246dd7
|
||||
###< scheb/2fa-trusted-device ###
|
||||
|
||||
###> doctrine/doctrine-bundle ###
|
||||
DATABASE_URL=mysql://root:Wichtig1@mysql:3306/psc
|
||||
###< doctrine/doctrine-bundle ###
|
||||
|
||||
@ -1,2 +1,10 @@
|
||||
[ 377ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 378ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 1189096ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 1189096ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 1633348ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 1633351ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 1951316ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 1951318ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 2167201ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 2167201ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
[ 6746ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 6747ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
@ -0,0 +1,2 @@
|
||||
[ 5383ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 5384ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
@ -0,0 +1,2 @@
|
||||
[ 4173ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 4174ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
@ -0,0 +1,2 @@
|
||||
[ 740ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 741ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
20
src/new/.playwright-mcp/console-2026-06-25T19-36-22-273Z.log
Normal file
20
src/new/.playwright-mcp/console-2026-06-25T19-36-22-273Z.log
Normal file
@ -0,0 +1,20 @@
|
||||
[ 3110ms] TypeError: Cannot read properties of null (reading 'consent')
|
||||
at https://www.heise.de/:10:8
|
||||
[ 3380ms] [LOG] Performance Polyfills [ms]: 35 @ https://www.heise.de/assets/akwa/v24/js/akwa.js:1
|
||||
[ 3418ms] [DIR] ##########################################################################################################
|
||||
|
||||
%%% 888 d8b 888 d8b
|
||||
%%% 888 Y8P 888 Y8P
|
||||
%%% 888 888
|
||||
%%% 88888b. .d88b. 888 .d8888b .d88b. .d88b. 88888b. 888 888 88888b. .d88b.
|
||||
%%% %%% 888 "88b d8P Y8b 888 88K d8P Y8b d88""88b 888 "88b 888 888 888 "88b d8P Y8b
|
||||
%%% %%% 888 888 88888888 888 "Y8888b. 88888888 888 888 888 888 888 888 888 888 88888888
|
||||
%%% %%% 888 888 Y8b. 888 X88 Y8b. Y88..88P 888 888 888 888 888 888 Y8b.
|
||||
%%% %%% 888 888 "Y8888 888 88888P' "Y8888 "Y88P" 888 888 888 888 888 888 "Y8888
|
||||
|
||||
👩💻 Du interessierst dich für Code? Schau dir doch mal unsere Job-Angebote an und werde Teil unseres Teams: https://karriere.heise.de
|
||||
|
||||
########################################################################################################## @ https://www.heise.de/assets/akwa/v24/js/akwa.js:148
|
||||
[ 3952ms] [LOG] Messaging without detection successfully executed. @ https://cmp.heise.de/unified/wrapperMessagingWithoutDetection.js:0
|
||||
[ 3952ms] [LOG] [event] onSPPMObjectReady [undefined] @ https://www.heise.de/:47
|
||||
[ 3965ms] [LOG] [event] onMessageReceiveData [gdpr, Object] @ https://www.heise.de/:44
|
||||
@ -0,0 +1,2 @@
|
||||
[ 912ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
[ 913ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6
|
||||
1614
src/new/.playwright-mcp/page-2026-06-25T19-20-54-958Z.yml
Normal file
1614
src/new/.playwright-mcp/page-2026-06-25T19-20-54-958Z.yml
Normal file
File diff suppressed because it is too large
Load Diff
1614
src/new/.playwright-mcp/page-2026-06-25T19-25-56-611Z.yml
Normal file
1614
src/new/.playwright-mcp/page-2026-06-25T19-25-56-611Z.yml
Normal file
File diff suppressed because it is too large
Load Diff
1614
src/new/.playwright-mcp/page-2026-06-25T19-34-58-495Z.yml
Normal file
1614
src/new/.playwright-mcp/page-2026-06-25T19-34-58-495Z.yml
Normal file
File diff suppressed because it is too large
Load Diff
504
src/new/.playwright-mcp/page-2026-06-25T19-35-14-304Z.yml
Normal file
504
src/new/.playwright-mcp/page-2026-06-25T19-35-14-304Z.yml
Normal file
@ -0,0 +1,504 @@
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- complementary [ref=e3]:
|
||||
- generic [ref=e5]:
|
||||
- button "Collapse sidebar" [ref=e6] [cursor=pointer]:
|
||||
- img [ref=e7]
|
||||
- img "PrintshopCreator Logo" [ref=e10]
|
||||
- navigation [ref=e11]:
|
||||
- list [ref=e12]:
|
||||
- listitem [ref=e13]:
|
||||
- list [ref=e14]:
|
||||
- listitem [ref=e15]:
|
||||
- link " Dashboard" [ref=e17] [cursor=pointer]:
|
||||
- /url: /apps/backend/dashboard
|
||||
- generic [ref=e18]:
|
||||
- generic [ref=e19]: Dashboard
|
||||
- listitem [ref=e20]:
|
||||
- link " Aufträge" [ref=e22] [cursor=pointer]:
|
||||
- /url: /apps/backend/order/list/index
|
||||
- generic [ref=e23]:
|
||||
- generic [ref=e24]: Aufträge
|
||||
- listitem [ref=e25]:
|
||||
- link " Sendcloud" [ref=e27] [cursor=pointer]:
|
||||
- /url: /apps/backend/r2_sendcloud/list
|
||||
- generic [ref=e28]:
|
||||
- generic [ref=e29]: Sendcloud
|
||||
- listitem [ref=e30]:
|
||||
- generic [ref=e31]:
|
||||
- generic [ref=e32]:
|
||||
- generic [ref=e33]:
|
||||
- generic [ref=e34]: Produktion
|
||||
- generic [ref=e36]:
|
||||
- text:
|
||||
- listitem [ref=e37]:
|
||||
- generic [ref=e38]:
|
||||
- generic [ref=e39]:
|
||||
- generic [ref=e40]:
|
||||
- generic [ref=e41]: Produkte
|
||||
- generic [ref=e43]:
|
||||
- text:
|
||||
- listitem [ref=e44]:
|
||||
- link " Firmen" [ref=e46] [cursor=pointer]:
|
||||
- /url: /apps/backend/account/list/index
|
||||
- generic [ref=e47]:
|
||||
- generic [ref=e48]: Firmen
|
||||
- listitem [ref=e49]:
|
||||
- link " Kunden" [ref=e51] [cursor=pointer]:
|
||||
- /url: /apps/backend/contact/list/index
|
||||
- generic [ref=e52]:
|
||||
- generic [ref=e53]: Kunden
|
||||
- listitem [ref=e54]:
|
||||
- link " CMS" [ref=e56] [cursor=pointer]:
|
||||
- /url: /apps/backend/cms/list
|
||||
- generic [ref=e57]:
|
||||
- generic [ref=e58]: CMS
|
||||
- listitem [ref=e59]:
|
||||
- link " Versandarten" [ref=e61] [cursor=pointer]:
|
||||
- /url: /apps/backend/shipping/list/index
|
||||
- generic [ref=e62]:
|
||||
- generic [ref=e63]: Versandarten
|
||||
- listitem [ref=e64]:
|
||||
- link " Zahlarten" [ref=e66] [cursor=pointer]:
|
||||
- /url: /apps/backend/payment/list/index
|
||||
- generic [ref=e67]:
|
||||
- generic [ref=e68]: Zahlarten
|
||||
- listitem [ref=e69]:
|
||||
- link " News" [ref=e71] [cursor=pointer]:
|
||||
- /url: /apps/backend/news/list/index
|
||||
- generic [ref=e72]:
|
||||
- generic [ref=e73]: News
|
||||
- listitem [ref=e74]:
|
||||
- link " Gutschein" [ref=e76] [cursor=pointer]:
|
||||
- /url: /apps/backend/voucher/list/index
|
||||
- generic [ref=e77]:
|
||||
- generic [ref=e78]: Gutschein
|
||||
- listitem [ref=e79]:
|
||||
- generic [ref=e80]:
|
||||
- generic [ref=e81]:
|
||||
- generic [ref=e82]:
|
||||
- generic [ref=e83]: Tools
|
||||
- generic [ref=e85]:
|
||||
- text:
|
||||
- listitem [ref=e86]:
|
||||
- generic [ref=e87]:
|
||||
- generic [ref=e88]:
|
||||
- generic [ref=e89]:
|
||||
- generic [ref=e90]: Shop Einstellungen
|
||||
- generic [ref=e92]:
|
||||
- text:
|
||||
- listitem [ref=e93]:
|
||||
- link " Aktionen" [ref=e95] [cursor=pointer]:
|
||||
- /url: /apps/backend/queue/list/index
|
||||
- generic [ref=e96]:
|
||||
- generic [ref=e97]: Aktionen
|
||||
- list [ref=e98]:
|
||||
- listitem [ref=e99]:
|
||||
- button "Systemeinstellungen" [ref=e100] [cursor=pointer]:
|
||||
- paragraph [ref=e102]: Systemeinstellungen
|
||||
- list [ref=e103]:
|
||||
- listitem [ref=e104]:
|
||||
- link " Statusverwaltung" [ref=e106] [cursor=pointer]:
|
||||
- /url: /apps/backend/system/settings/status/index
|
||||
- generic [ref=e107]:
|
||||
- generic [ref=e108]: Statusverwaltung
|
||||
- listitem [ref=e109]:
|
||||
- link " Papierdatenbank" [ref=e111] [cursor=pointer]:
|
||||
- /url: /apps/backend/system/settings/paper/index
|
||||
- generic [ref=e112]:
|
||||
- generic [ref=e113]: Papierdatenbank
|
||||
- listitem [ref=e114]:
|
||||
- link " Hilfetexte" [ref=e116] [cursor=pointer]:
|
||||
- /url: /apps/backend/system/settings/help/index
|
||||
- generic [ref=e117]:
|
||||
- generic [ref=e118]: Hilfetexte
|
||||
- listitem [ref=e119]:
|
||||
- link " Systemeinstellung" [ref=e121] [cursor=pointer]:
|
||||
- /url: /apps/backend/system/settings/settings/index
|
||||
- generic [ref=e122]:
|
||||
- generic [ref=e123]: Systemeinstellung
|
||||
- listitem [ref=e124]:
|
||||
- link " Logs" [ref=e126] [cursor=pointer]:
|
||||
- /url: /apps/backend/system/settings/log/index
|
||||
- generic [ref=e127]:
|
||||
- generic [ref=e128]: Logs
|
||||
- listitem [ref=e129]:
|
||||
- link " Media" [ref=e131] [cursor=pointer]:
|
||||
- /url: /apps/backend/media/list/folder/show
|
||||
- generic [ref=e132]:
|
||||
- generic [ref=e133]: Media
|
||||
- listitem [ref=e134]:
|
||||
- list [ref=e135]:
|
||||
- listitem [ref=e136]:
|
||||
- link " Apps & App Store" [ref=e138] [cursor=pointer]:
|
||||
- /url: /apps/backend/plugin/list
|
||||
- generic [ref=e139]:
|
||||
- generic [ref=e140]: Apps & App Store
|
||||
- separator [ref=e141]
|
||||
- paragraph [ref=e142]: 25.06.2026 21:35:14
|
||||
- paragraph [ref=e143]:
|
||||
- link "v2.3.8 (24.06.2026)" [ref=e144] [cursor=pointer]:
|
||||
- /url: /apps/backend/system/settings/backend/system/version/changelog
|
||||
- generic [ref=e145]:
|
||||
- banner [ref=e146]:
|
||||
- generic [ref=e148]:
|
||||
- generic [ref=e150]:
|
||||
- combobox [ref=e151]:
|
||||
- option "Bestesvonhier"
|
||||
- option "eder"
|
||||
- option "nowe"
|
||||
- option "Printchampion"
|
||||
- option "Produkttypen" [selected]
|
||||
- link "Shop anzeigen" [ref=e152] [cursor=pointer]:
|
||||
- /url: https://type-dev-tp.local
|
||||
- img [ref=e153]
|
||||
- generic [ref=e155]:
|
||||
- checkbox [ref=e156]
|
||||
- text: Deaktive Shops zeigen
|
||||
- link "Kein 2FA aktiviert" [ref=e158] [cursor=pointer]:
|
||||
- /url: /apps/backend/2fa-settings
|
||||
- link "EN" [ref=e160] [cursor=pointer]:
|
||||
- /url: http://type-dev-tp.local/apps/backend/order/detail/show/928d23d0-61b8-eae7-9418-2cd2729e8b3b?_locale=en
|
||||
- button "User menu" [ref=e163] [cursor=pointer]
|
||||
- generic [ref=e165]:
|
||||
- banner [ref=e166]:
|
||||
- generic [ref=e167]:
|
||||
- heading "Auftrag › Detail AF-08.04.2026-90" [level=1] [ref=e168]:
|
||||
- img [ref=e169]
|
||||
- text: Auftrag
|
||||
- generic [ref=e171]: › Detail
|
||||
- generic [ref=e172]: AF-08.04.2026-90
|
||||
- link "Zurück" [ref=e173] [cursor=pointer]:
|
||||
- /url: /apps/backend/order/list/index
|
||||
- img [ref=e174]
|
||||
- text: Zurück
|
||||
- generic [ref=e176]:
|
||||
- generic [ref=e178]:
|
||||
- link "Paket herunterladen" [ref=e179] [cursor=pointer]:
|
||||
- /url: /apps/backend/order/detail/package/download/928d23d0-61b8-eae7-9418-2cd2729e8b3b
|
||||
- img [ref=e180]
|
||||
- text: Paket herunterladen
|
||||
- link "Rechnung" [ref=e182] [cursor=pointer]:
|
||||
- /url: /apps/backend/order/detail/print/1/928d23d0-61b8-eae7-9418-2cd2729e8b3b
|
||||
- img [ref=e183]
|
||||
- text: Rechnung
|
||||
- link "Jobticket" [ref=e185] [cursor=pointer]:
|
||||
- /url: /apps/backend/order/detail/print/7/928d23d0-61b8-eae7-9418-2cd2729e8b3b
|
||||
- img [ref=e186]
|
||||
- text: Jobticket
|
||||
- link "XRechnung" [ref=e188] [cursor=pointer]:
|
||||
- /url: /apps/backend/order/detail/xrechnung/928d23d0-61b8-eae7-9418-2cd2729e8b3b
|
||||
- img [ref=e189]
|
||||
- text: XRechnung
|
||||
- generic [ref=e191]:
|
||||
- generic [ref=e192]:
|
||||
- heading "Auftrag AF-08.04.2026-90" [level=4] [ref=e193]
|
||||
- generic [ref=e194]:
|
||||
- generic [ref=e195]:
|
||||
- term [ref=e196]: "Zahlart:"
|
||||
- definition [ref=e197]: molli
|
||||
- generic [ref=e198]:
|
||||
- term [ref=e199]: "Zahlung Gateway (Ref):"
|
||||
- definition [ref=e200]: mollie (62c5de1)
|
||||
- generic [ref=e201]:
|
||||
- term [ref=e202]: "Versandart:"
|
||||
- definition [ref=e203]: DHL
|
||||
- generic [ref=e204]:
|
||||
- term [ref=e205]: "Daten an Versanddienstleister:"
|
||||
- definition [ref=e206]:
|
||||
- generic [ref=e207]: Ja
|
||||
- generic [ref=e208]:
|
||||
- term [ref=e209]: "Gewicht:"
|
||||
- definition [ref=e210]: "0"
|
||||
- generic [ref=e211]:
|
||||
- term [ref=e212]: "Warenkorbfeld 1:"
|
||||
- definition
|
||||
- generic [ref=e213]:
|
||||
- term [ref=e214]: "Warenkorbfeld 2:"
|
||||
- definition
|
||||
- generic [ref=e215]:
|
||||
- generic [ref=e216]:
|
||||
- term [ref=e217]: "Versandkosten:"
|
||||
- definition [ref=e218]: 10,00€
|
||||
- generic [ref=e219]:
|
||||
- term [ref=e220]: "Zahlartkosten:"
|
||||
- definition [ref=e221]: 0,00€
|
||||
- generic [ref=e222]:
|
||||
- term [ref=e223]: "Netto Preis:"
|
||||
- definition [ref=e224]: 22,00€
|
||||
- generic [ref=e225]:
|
||||
- term [ref=e226]: "MwSt.:"
|
||||
- definition [ref=e227]: 4,18€
|
||||
- generic [ref=e228]:
|
||||
- term [ref=e229]: "Brutto Preis:"
|
||||
- definition [ref=e230]: 26,18€
|
||||
- generic [ref=e231]:
|
||||
- heading "Status" [level=4] [ref=e232]
|
||||
- generic [ref=e233]: "Auftragsdatum: 08.04.2026 18:35:44"
|
||||
- generic [ref=e234]:
|
||||
- generic [ref=e235]:
|
||||
- generic [ref=e236]: Package exportiert?
|
||||
- generic [ref=e237]:
|
||||
- generic [ref=e238]: Ja
|
||||
- link "Nochmal gernerieren" [ref=e239] [cursor=pointer]:
|
||||
- /url: /apps/backend/order/package/exported/928d23d0-61b8-eae7-9418-2cd2729e8b3b
|
||||
- generic [ref=e240]:
|
||||
- generic [ref=e242]: Paketnummer
|
||||
- textbox "Paketnummer" [ref=e243]
|
||||
- generic [ref=e244]:
|
||||
- generic [ref=e246]: Warenkorbstatus
|
||||
- combobox "Warenkorbstatus" [ref=e247]:
|
||||
- option
|
||||
- option "neue Bestellung" [selected]
|
||||
- option "neue Anfrage"
|
||||
- option "Upload offen"
|
||||
- option "Daten hochgeladen"
|
||||
- option "Upload abgeschlossen"
|
||||
- option "Daten werden überprüft"
|
||||
- option "Daten Fehler"
|
||||
- option "Daten Ok"
|
||||
- option "Preflight wird ausgeführt"
|
||||
- option "Preflightcheck Fehler"
|
||||
- option "Preflight Ok"
|
||||
- option "wartet auf Freigabe Einer"
|
||||
- option "wartet auf Freigabe Alle"
|
||||
- option "Freigabe ist erfolgt"
|
||||
- option "Freigabe abgelehnt"
|
||||
- option "Angebot abgegeben"
|
||||
- option "wartet auf Zahlungseingang"
|
||||
- option "Zahlung erhalten"
|
||||
- option "Download steht bereit"
|
||||
- option "in Produktion"
|
||||
- option "abgebrochen oder storniert"
|
||||
- option "produziert"
|
||||
- option "Zur Abholung bereit"
|
||||
- option "in Bearbeitung"
|
||||
- option "wartet auf Versand"
|
||||
- option "versendet"
|
||||
- option "abgeschlossen"
|
||||
- generic [ref=e248]:
|
||||
- generic [ref=e250]: Notizen
|
||||
- textbox "Notizen" [ref=e251]
|
||||
- button "Speichern" [ref=e253] [cursor=pointer]: Speichern
|
||||
- generic [ref=e254]:
|
||||
- generic [ref=e255]:
|
||||
- heading "Kunde" [level=4] [ref=e256]
|
||||
- generic [ref=e257]:
|
||||
- generic [ref=e258]:
|
||||
- term [ref=e259]: "Username/Login:"
|
||||
- definition [ref=e260]:
|
||||
- link "admin@shop.de" [ref=e261] [cursor=pointer]:
|
||||
- /url: mailto:admin@shop.de
|
||||
- generic [ref=e262]:
|
||||
- term [ref=e263]: "Name:"
|
||||
- definition [ref=e264]:
|
||||
- link "Martina Peterson" [ref=e265] [cursor=pointer]:
|
||||
- /url: /apps/backend/contact/edit/edit/0001-528bd456-4bcd48e8-13e1-0683b94f
|
||||
- generic [ref=e266]:
|
||||
- term [ref=e267]: "Firma:"
|
||||
- definition [ref=e268]:
|
||||
- link "Oberfirma" [ref=e269] [cursor=pointer]:
|
||||
- /url: /apps/backend/account/edit/edit/24
|
||||
- generic [ref=e270]:
|
||||
- term [ref=e271]: "Gast:"
|
||||
- definition [ref=e272]:
|
||||
- generic [ref=e273]: Nein
|
||||
- generic [ref=e275]:
|
||||
- link "Neuer Auftrag Aktion erneut auslösen" [ref=e276] [cursor=pointer]:
|
||||
- /url: /apps/backend/order/detail/create/new/order/status/928d23d0-61b8-eae7-9418-2cd2729e8b3b
|
||||
- button "Auftrag löschen" [ref=e277] [cursor=pointer]
|
||||
- link "Uploads Löschen" [ref=e278] [cursor=pointer]:
|
||||
- /url: /apps/backend/order/upload/deleteAll/928d23d0-61b8-eae7-9418-2cd2729e8b3b
|
||||
- img [ref=e279]
|
||||
- text: Uploads Löschen
|
||||
- link "Auftrag bearbeiten (BETA)" [ref=e281] [cursor=pointer]:
|
||||
- /url: /apps/backend/component/invoice/index/create#/928d23d0-61b8-eae7-9418-2cd2729e8b3b
|
||||
- generic [ref=e282]:
|
||||
- generic [ref=e283]: Saxoprint
|
||||
- generic [ref=e284]:
|
||||
- link "Sammelbestellung für diesen Auftrag ausführen" [ref=e285] [cursor=pointer]:
|
||||
- /url: /apps/backend/plugin/custom/psc/saxoprint/collect/928d23d0-61b8-eae7-9418-2cd2729e8b3b
|
||||
- table [ref=e286]:
|
||||
- rowgroup [ref=e287]:
|
||||
- row "Pos External Number Status" [ref=e288]:
|
||||
- columnheader "Pos" [ref=e289]
|
||||
- columnheader "External Number" [ref=e290]
|
||||
- columnheader "Status" [ref=e291]
|
||||
- rowgroup
|
||||
- generic [ref=e292]:
|
||||
- generic [ref=e293]: Wir machen Druck
|
||||
- table [ref=e295]:
|
||||
- rowgroup [ref=e296]:
|
||||
- row "Pos External Number Status" [ref=e297]:
|
||||
- columnheader "Pos" [ref=e298]
|
||||
- columnheader "External Number" [ref=e299]
|
||||
- columnheader "Status" [ref=e300]
|
||||
- rowgroup
|
||||
- generic [ref=e301]:
|
||||
- heading "Adressen" [level=4] [ref=e302]
|
||||
- generic [ref=e303]:
|
||||
- generic [ref=e304]:
|
||||
- heading "Rechnungsanschrift" [level=5] [ref=e305]
|
||||
- generic [ref=e306]:
|
||||
- generic [ref=e307]:
|
||||
- generic [ref=e309]: Firma
|
||||
- textbox "Firma" [ref=e310]: Musterfirma
|
||||
- generic [ref=e311]:
|
||||
- generic [ref=e313]: Zusatz
|
||||
- textbox "Zusatz" [ref=e314]
|
||||
- generic [ref=e315]:
|
||||
- generic [ref=e317]: Vorname
|
||||
- textbox "Vorname" [ref=e318]: admin
|
||||
- generic [ref=e319]:
|
||||
- generic [ref=e321]: Nachname
|
||||
- textbox "Nachname" [ref=e322]: admin
|
||||
- generic [ref=e323]:
|
||||
- generic [ref=e325]: EMail
|
||||
- textbox "EMail" [ref=e326]: admin@shop.de
|
||||
- generic [ref=e327]:
|
||||
- generic [ref=e329]: Telefon
|
||||
- textbox "Telefon" [ref=e330]: "12345"
|
||||
- generic [ref=e331]:
|
||||
- generic [ref=e332]: PLZ / Stadt / LK
|
||||
- generic [ref=e333]:
|
||||
- textbox [ref=e335]: "12345"
|
||||
- textbox [ref=e337]: Musterort
|
||||
- textbox [ref=e339]: DE
|
||||
- generic [ref=e340]:
|
||||
- generic [ref=e341]: Straße / Nr.
|
||||
- generic [ref=e342]:
|
||||
- textbox [ref=e344]: Musterstrasse
|
||||
- textbox [ref=e346]: "1"
|
||||
- button "Speichern" [ref=e348] [cursor=pointer]: Speichern
|
||||
- generic [ref=e349]:
|
||||
- heading "Lieferanschrift" [level=5] [ref=e350]
|
||||
- generic [ref=e351]:
|
||||
- generic [ref=e352]:
|
||||
- generic [ref=e354]: Firma
|
||||
- textbox "Firma" [ref=e355]: Musterfirma
|
||||
- generic [ref=e356]:
|
||||
- generic [ref=e358]: Zusatz
|
||||
- textbox "Zusatz" [ref=e359]
|
||||
- generic [ref=e360]:
|
||||
- generic [ref=e362]: Vorname
|
||||
- textbox "Vorname" [ref=e363]: admin
|
||||
- generic [ref=e364]:
|
||||
- generic [ref=e366]: Nachname
|
||||
- textbox "Nachname" [ref=e367]: admin
|
||||
- generic [ref=e368]:
|
||||
- generic [ref=e370]: EMail
|
||||
- textbox "EMail" [ref=e371]: admin@shop.de
|
||||
- generic [ref=e372]:
|
||||
- generic [ref=e374]: Telefon
|
||||
- textbox "Telefon" [ref=e375]: "123456"
|
||||
- generic [ref=e376]:
|
||||
- generic [ref=e377]: PLZ / Stadt / LK
|
||||
- generic [ref=e378]:
|
||||
- textbox [ref=e380]: "12345"
|
||||
- textbox [ref=e382]: Musterort
|
||||
- textbox [ref=e384]: DE
|
||||
- generic [ref=e385]:
|
||||
- generic [ref=e386]: Straße / Nr.
|
||||
- generic [ref=e387]:
|
||||
- textbox [ref=e389]: Musterstrasse
|
||||
- textbox [ref=e391]: "1"
|
||||
- button "Speichern" [ref=e393] [cursor=pointer]: Speichern
|
||||
- generic [ref=e394]:
|
||||
- heading "Absenderadresse" [level=5] [ref=e395]
|
||||
- generic [ref=e396]:
|
||||
- generic [ref=e397]:
|
||||
- generic [ref=e399]: Firma
|
||||
- textbox "Firma" [ref=e400]: PSC
|
||||
- generic [ref=e401]:
|
||||
- generic [ref=e403]: Zusatz
|
||||
- textbox "Zusatz" [ref=e404]
|
||||
- generic [ref=e405]:
|
||||
- generic [ref=e407]: Vorname
|
||||
- textbox "Vorname" [ref=e408]: Thomas
|
||||
- generic [ref=e409]:
|
||||
- generic [ref=e411]: Nachname
|
||||
- textbox "Nachname" [ref=e412]: Peterson
|
||||
- generic [ref=e413]:
|
||||
- generic [ref=e415]: EMail
|
||||
- textbox "EMail" [ref=e416]: psc@printshopcreator.com
|
||||
- generic [ref=e417]:
|
||||
- generic [ref=e419]: Telefon
|
||||
- textbox "Telefon" [ref=e420]: "01705045438"
|
||||
- generic [ref=e421]:
|
||||
- generic [ref=e422]: PLZ / Stadt / LK
|
||||
- generic [ref=e423]:
|
||||
- textbox [ref=e425]: "17506"
|
||||
- textbox [ref=e427]: Gribow
|
||||
- textbox [ref=e429]: DE
|
||||
- generic [ref=e430]:
|
||||
- generic [ref=e431]: Straße / Nr.
|
||||
- generic [ref=e432]:
|
||||
- textbox [ref=e434]: Chausseestraße
|
||||
- textbox [ref=e436]: 24b
|
||||
- button "Speichern" [ref=e438] [cursor=pointer]: Speichern
|
||||
- generic [ref=e439]:
|
||||
- heading "Positionen" [level=4] [ref=e441]
|
||||
- table [ref=e443]:
|
||||
- rowgroup [ref=e444]:
|
||||
- row "Pos. Name / Details WK_Pos_1 und WK_Pos_2 Eigenschaften / Auflage Preise Widerruf / Status Aktionen" [ref=e445]:
|
||||
- columnheader "Pos." [ref=e446]
|
||||
- columnheader "Name / Details" [ref=e447]
|
||||
- columnheader "WK_Pos_1 und WK_Pos_2" [ref=e448]
|
||||
- columnheader "Eigenschaften / Auflage" [ref=e449]
|
||||
- columnheader "Preise" [ref=e450]
|
||||
- columnheader "Widerruf / Status" [ref=e451]
|
||||
- columnheader "Aktionen" [ref=e452]
|
||||
- rowgroup [ref=e453]:
|
||||
- 'row "1 test_drweigert ArtNr intern: Gewicht: 0 g not available Dokumentnummer: Ausgabedatum: Auflage: 1 Breite: 20 Netto Preis 12,00 € MwSt. 2,28 € Brutto Preis 14,28 € Widerruf erfolgte am 24.06.2026 08:52 Uhr neue Bestellung Printpartner benachrichtigen Dem Kunden in seinem Account anzeigen" [ref=e454]':
|
||||
- cell "1" [ref=e455]
|
||||
- 'cell "test_drweigert ArtNr intern: Gewicht: 0 g not available" [ref=e456]':
|
||||
- link "test_drweigert" [ref=e457] [cursor=pointer]:
|
||||
- /url: /apps/backend/product/edit/index/717ab0b7-e710-421d-bb8a-ccac2b963f6d
|
||||
- generic [ref=e458]: "ArtNr intern:"
|
||||
- generic [ref=e459]: "Gewicht: 0 g"
|
||||
- generic [ref=e461]:
|
||||
- img [ref=e462]
|
||||
- generic [ref=e464]: not available
|
||||
- cell [ref=e465]
|
||||
- 'cell "Dokumentnummer: Ausgabedatum: Auflage: 1 Breite: 20" [ref=e466]':
|
||||
- list [ref=e467]:
|
||||
- listitem [ref=e468]: "Dokumentnummer:"
|
||||
- listitem [ref=e469]: "Ausgabedatum:"
|
||||
- listitem [ref=e470]: "Auflage: 1"
|
||||
- listitem [ref=e471]: "Breite: 20"
|
||||
- cell "Netto Preis 12,00 € MwSt. 2,28 € Brutto Preis 14,28 €" [ref=e472]:
|
||||
- generic [ref=e473]:
|
||||
- generic [ref=e474]:
|
||||
- term [ref=e475]: Netto Preis
|
||||
- definition [ref=e476]: 12,00 €
|
||||
- generic [ref=e477]:
|
||||
- term [ref=e478]: MwSt.
|
||||
- definition [ref=e479]: 2,28 €
|
||||
- generic [ref=e480]:
|
||||
- term [ref=e481]: Brutto Preis
|
||||
- definition [ref=e482]: 14,28 €
|
||||
- cell "Widerruf erfolgte am 24.06.2026 08:52 Uhr neue Bestellung" [ref=e483]:
|
||||
- generic [ref=e484]:
|
||||
- img [ref=e485]
|
||||
- generic [ref=e487]: Widerruf erfolgte am 24.06.2026 08:52 Uhr
|
||||
- button "neue Bestellung" [ref=e489] [cursor=pointer]:
|
||||
- text: neue Bestellung
|
||||
- img [ref=e490]
|
||||
- cell "Printpartner benachrichtigen Dem Kunden in seinem Account anzeigen" [ref=e492]:
|
||||
- generic [ref=e493]:
|
||||
- generic [ref=e495]:
|
||||
- button "Printpartner benachrichtigen" [ref=e496] [cursor=pointer]
|
||||
- textbox [ref=e497]
|
||||
- generic [ref=e499]:
|
||||
- button "Dem Kunden in seinem Account anzeigen" [ref=e500] [cursor=pointer]
|
||||
- textbox [ref=e501]
|
||||
- region "Symfony Web Debug Toolbar" [ref=e502]:
|
||||
- generic [ref=e505]:
|
||||
- link "Symfony Loading…" [ref=e507] [cursor=pointer]:
|
||||
- /url: http://type-dev-tp.local/apps/_profiler/be25d0?panel=request
|
||||
- generic [ref=e508]:
|
||||
- img "Symfony" [ref=e509]
|
||||
- generic [ref=e511]: Loading…
|
||||
- button [expanded] [ref=e512] [cursor=pointer]:
|
||||
- generic "Close Toolbar" [ref=e513]:
|
||||
- img [ref=e514]
|
||||
1854
src/new/.playwright-mcp/page-2026-06-25T19-41-01-399Z.yml
Normal file
1854
src/new/.playwright-mcp/page-2026-06-25T19-41-01-399Z.yml
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,7 @@ const Order_List_Preview = (uploadTypeObject) => {
|
||||
return notAvailable;
|
||||
}
|
||||
|
||||
const fileIconHtml = (typ) => `<div style="display:none" class="w-28 h-28 rounded-md border border-gray-200 bg-gray-100 flex flex-col items-center justify-center text-gray-400 gap-1">
|
||||
const fileIconHtml = (typ) => `<div style="display:none" class="w-24 h-24 rounded-md border border-gray-200 bg-gray-100 flex flex-col items-center justify-center text-gray-400 gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-7 h-7">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||
</svg>
|
||||
@ -19,69 +19,107 @@ const Order_List_Preview = (uploadTypeObject) => {
|
||||
|
||||
return uploadTypeObject.uploads.map(upload => {
|
||||
const previewUrl = `/apps/backend/order/upload/preview?path=${encodeURIComponent(upload.path)}`;
|
||||
return `<div class="shrink-0 flex flex-col items-center gap-1">
|
||||
return `<div class="flex flex-col items-center gap-1 w-24">
|
||||
<img src="${previewUrl}"
|
||||
class="w-28 h-28 object-cover rounded-md border border-gray-200 shadow-sm"
|
||||
class="w-24 h-24 object-cover rounded-md border border-gray-200 shadow-sm"
|
||||
title="${upload.fileName}"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';" />
|
||||
${fileIconHtml(upload.typ)}
|
||||
<span class="text-xs text-gray-500">${upload.typ}</span>
|
||||
<span class="text-xs text-gray-500 w-24 truncate text-center" title="${upload.fileName}">${upload.fileName}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
};
|
||||
|
||||
const Order_List_Detail = ({ uuid, basketField1, customerInfo, basketField2, pos, price, product, status, allNet, reOrder, reOrderOrder, reOrderPos, uploadTypeObject }, orderUuid) => `
|
||||
<div class="px-6 py-4 bg-gray-50" style="${psc.order.get_pos_bg_color(status)}">
|
||||
<div class="flex gap-4 items-start" id="row-${uuid}">
|
||||
<div class="grid grid-cols-12 gap-4 flex-1">
|
||||
<div class="col-span-1">
|
||||
<span class="text-xs font-semibold text-gray-700">Pos:</span>
|
||||
<div class="font-medium">${pos}</div>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
${Order_List_Preview(uploadTypeObject)}
|
||||
</div>
|
||||
<div class="col-span-4">
|
||||
<span class="text-xs font-semibold text-gray-700">Produkt:</span>
|
||||
<div>
|
||||
${(product => product.originalProduct ? `<a href="/apps/backend/product/edit/index/${product.originalProduct.uuid}" class="text-psc-500 hover:underline">${product.originalProduct.title}</a>` : `<a href="/apps/backend/product/edit/index/${product.uuid}" class="text-psc-500 hover:underline">${product.title}</a>`)(product)
|
||||
const euro = (cents) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(cents / 100);
|
||||
|
||||
// Tabellenkopf identisch zur Auftrags-Detailansicht – ohne Spalte "Aktionen".
|
||||
const Order_List_Detail_Head = `
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700 w-10">Pos</th>
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">Name / Details</th>
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">WK_Pos_1 und WK_Pos_2</th>
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">Eigenschaften / Auflage</th>
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">Preise</th>
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">Widerruf / Status</th>
|
||||
`;
|
||||
|
||||
// Widerruf-Badge identisch zur Detailansicht (drei Zustände).
|
||||
const Order_List_Revoke = (revoked, revokedAt, revokeAllowed) => {
|
||||
if (revoked) {
|
||||
return `<div class="flex items-start gap-2 rounded-md border border-sky-200 bg-sky-50 px-3 py-2 text-xs text-sky-800 mb-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mt-0.5 shrink-0"><path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" /></svg>
|
||||
<span>Widerruf erfolgte am ${revokedAt ? `<span class="font-medium">${revokedAt} Uhr</span>` : ``}</span>
|
||||
</div>`;
|
||||
}
|
||||
<br/><span class="text-xs text-gray-600">ArtNr intern: ${product.nrIntern}</span><br/>
|
||||
if (revokeAllowed) {
|
||||
return `<div class="flex items-center gap-2 rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-medium text-emerald-700 mb-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 shrink-0"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
Widerruf möglich
|
||||
</div>`;
|
||||
}
|
||||
return `<div class="flex items-center gap-2 rounded-md border border-rose-200 bg-rose-50 px-3 py-2 text-xs font-medium text-rose-700 mb-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 shrink-0"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
Widerruf nicht möglich
|
||||
</div>`;
|
||||
};
|
||||
|
||||
// Positionszeile – Aufbau identisch zur Auftrags-Detailansicht, nur ohne die Spalte "Aktionen".
|
||||
const Order_List_Detail = ({ uuid, basketField1, basketField2, pos, price, product, status, weight, revoked, revokedAt, revokeAllowed, reOrder, uploadTypeObject }, orderUuid) => `
|
||||
<tr class="align-top border-b-2 border-psc-500" style="${psc.order.get_pos_bg_color(status)}">
|
||||
|
||||
<td class="px-3 py-4 font-semibold text-gray-800">${pos}</td>
|
||||
|
||||
<td class="px-3 py-4">
|
||||
${(product => product.originalProduct ? `<a href="/apps/backend/product/edit/index/${product.originalProduct.uuid}" class="text-psc-500 hover:underline font-semibold">${product.originalProduct.title}</a>` : `<a href="/apps/backend/product/edit/index/${product.uuid}" class="text-psc-500 hover:underline font-semibold">${product.title}</a>`)(product)}
|
||||
<div class="text-xs text-gray-500 mt-1">ArtNr intern: ${product.nrIntern}</div>
|
||||
<div class="text-xs text-gray-500">Gewicht: ${weight} g</div>
|
||||
<div class="mt-3"><div class="flex flex-wrap gap-3">${Order_List_Preview(uploadTypeObject)}</div></div>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-4 max-w-xs">
|
||||
${basketField1 ? `<p class="text-xs font-semibold text-gray-700">Hinweis für den Kunden</p><p class="text-xs text-gray-600 whitespace-pre-line mb-3">${basketField1}</p>` : ``}
|
||||
${basketField2 ? `<p class="text-xs font-semibold text-gray-700">Hinweis für den Druckpartner</p><p class="text-xs text-gray-600 whitespace-pre-line">${basketField2}</p>` : ``}
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-4">
|
||||
${psc.order.get_special_product_options(product.specialProductTypeObject)}
|
||||
<div class="text-xs text-gray-600 mt-1">Auflage: <span class="font-medium">${price.count}</span></div>
|
||||
${reOrder ? `<p class="text-xs text-yellow-600 font-semibold mt-2">Ist eine Nachbestellung</p>` : ``}
|
||||
<div id="row-${uuid}"></div>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-4">
|
||||
<dl class="text-xs space-y-1 w-40">
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-gray-500">Netto Preis</dt>
|
||||
<dd class="text-gray-800 text-right">${euro(price.allNet)}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-gray-500">MwSt.</dt>
|
||||
<dd class="text-gray-800 text-right">${euro(price.allVat)}</dd>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<span class="text-xs font-semibold text-gray-700">Auflage:</span>
|
||||
<div>${price.count}</div>
|
||||
<div class="flex justify-between gap-4 border-t border-gray-100 pt-1">
|
||||
<dt class="text-gray-700 font-semibold">Brutto Preis</dt>
|
||||
<dd class="text-gray-900 font-semibold text-right">${euro(price.allGross)}</dd>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<span class="text-xs font-semibold text-gray-700">Kunden Info:</span>
|
||||
<div class="text-sm">${customerInfo}${(reOrder ? `<br/><strong class="text-yellow-600">Ist eine Nachbestellung</strong>` : ``)}</div>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<span class="text-xs font-semibold text-gray-700">Preis:</span>
|
||||
<div class="text-right whitespace-nowrap">
|
||||
${new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price.allNet / 100)}<br/>
|
||||
<strong>(${new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(price.allGross / 100)})</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<span class="text-xs font-semibold text-gray-700">Status:</span>
|
||||
<div class="relative inline-block text-left w-full">
|
||||
<button type="button" class="inline-flex justify-between items-center w-full px-3 py-1.5 text-xs font-medium text-white bg-psc-500 hover:bg-psc-600 rounded-sm shadow-sm">
|
||||
</dl>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-4">
|
||||
${Order_List_Revoke(revoked, revokedAt, revokeAllowed)}
|
||||
<div class="relative inline-block text-left">
|
||||
<button type="button" class="inline-flex justify-between items-center px-3 py-1.5 text-xs font-medium text-white bg-psc-500 hover:bg-psc-600 rounded-sm shadow-sm gap-1">
|
||||
${psc.order.get_pos_status(status).internalName}
|
||||
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="hidden absolute right-0 z-10 mt-1 w-56 rounded-sm shadow-xl bg-white border border-gray-200 py-1">
|
||||
<div class="hidden absolute left-0 z-10 mt-1 w-48 rounded-sm shadow-xl bg-white border border-gray-200 py-1">
|
||||
${psc.order.get_pos_status_loop(orderUuid, uuid)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
`;
|
||||
|
||||
const Order_List_Detail_Simple = ({ uuid, basketField1, customerInfo, basketField2, pos, price, product, status, allNet }, orderUuid) => `
|
||||
@ -129,9 +167,7 @@ const Order_List_Pos_Status = (status, orderUuid, posUuid) => `
|
||||
</button>
|
||||
`;
|
||||
|
||||
const Order_List_Pos_Calc_Option = (elm) => `
|
||||
<tr><td>${elm.name}</td><td>${elm.value}</td></tr>
|
||||
`;
|
||||
const Order_List_Pos_Calc_Option = (elm) => `<li>${elm.name}: <span class="font-medium">${elm.value}</span></li>`;
|
||||
|
||||
export class order {
|
||||
|
||||
@ -169,11 +205,12 @@ export class order {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '<table>' + specialProductTypeObject.options.map(elm => {
|
||||
return '<ul class="text-xs text-gray-600 space-y-0.5 list-disc list-inside">' + specialProductTypeObject.options.map(elm => {
|
||||
if (elm.type != 'hidden') {
|
||||
return Order_List_Pos_Calc_Option(elm);
|
||||
}
|
||||
}).join('') + '</table>';
|
||||
return '';
|
||||
}).join('') + '</ul>';
|
||||
}
|
||||
|
||||
load_status() {
|
||||
@ -244,7 +281,15 @@ export class order {
|
||||
if (simple) {
|
||||
$('#rows-' + result.uuid + ' td').html(result.positions.map(x => Order_List_Detail_Simple(x, orderUuid)).join(''));
|
||||
} else {
|
||||
$('#rows-' + result.uuid + ' td').html(result.positions.map(x => Order_List_Detail(x, orderUuid)).join(''));
|
||||
const rows = result.positions.map(x => Order_List_Detail(x, orderUuid)).join('');
|
||||
$('#rows-' + result.uuid + ' td').html(
|
||||
`<div class="overflow-x-auto bg-white">
|
||||
<table class="w-full text-sm border-collapse">
|
||||
<thead class="bg-slate-100 border-t border-stroke"><tr>${Order_List_Detail_Head}</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</div>`
|
||||
);
|
||||
result.positions.map(x => this.loadPluginDetails(x));
|
||||
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
"scheb/2fa-bundle": "^8.6",
|
||||
"scheb/2fa-email": "^8.6",
|
||||
"scheb/2fa-totp": "^8.6",
|
||||
"scheb/2fa-trusted-device": "^8.6",
|
||||
"scssphp/scssphp": "v1.11.1",
|
||||
"setasign/fpdi-tcpdf": "^2.3",
|
||||
"sofort/sofortlib-php": "3.3.2",
|
||||
|
||||
117
src/new/composer.lock
generated
117
src/new/composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e04b81bc87a7231b11cc580757ba49a0",
|
||||
"content-hash": "f863fc8c87dbe8935f9ac19c1e038484",
|
||||
"packages": [
|
||||
{
|
||||
"name": "apimatic/core",
|
||||
@ -4442,6 +4442,70 @@
|
||||
],
|
||||
"time": "2025-11-01T09:38:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lcobucci/clock",
|
||||
"version": "3.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lcobucci/clock.git",
|
||||
"reference": "4cdd88f761e9be9095ccbedf3e08d61ae216c643"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/lcobucci/clock/zipball/4cdd88f761e9be9095ccbedf3e08d61ae216c643",
|
||||
"reference": "4cdd88f761e9be9095ccbedf3e08d61ae216c643",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "~8.4.0 || ~8.5.0",
|
||||
"psr/clock": "^1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/clock-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"infection/infection": "^0.32",
|
||||
"lcobucci/coding-standard": "^12.0",
|
||||
"phpstan/extension-installer": "^1.3.1",
|
||||
"phpstan/phpstan": "^2.1",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpunit/phpunit": "^13.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Lcobucci\\Clock\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Luís Cobucci",
|
||||
"email": "lcobucci@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Yet another clock abstraction",
|
||||
"support": {
|
||||
"issues": "https://github.com/lcobucci/clock/issues",
|
||||
"source": "https://github.com/lcobucci/clock/tree/3.6.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/lcobucci",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/lcobucci",
|
||||
"type": "patreon"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-13T21:30:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "lcobucci/jwt",
|
||||
"version": "5.6.0",
|
||||
@ -8327,6 +8391,57 @@
|
||||
},
|
||||
"time": "2026-01-24T13:27:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "scheb/2fa-trusted-device",
|
||||
"version": "v8.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/scheb/2fa-trusted-device.git",
|
||||
"reference": "1e43d2c6dc39ed08fc68657cbad0e386c481cb7f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/scheb/2fa-trusted-device/zipball/1e43d2c6dc39ed08fc68657cbad0e386c481cb7f",
|
||||
"reference": "1e43d2c6dc39ed08fc68657cbad0e386c481cb7f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"lcobucci/clock": "^3.0",
|
||||
"lcobucci/jwt": "^5.0",
|
||||
"php": "~8.4.0 || ~8.5.0",
|
||||
"scheb/2fa-bundle": "self.version"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Scheb\\TwoFactorBundle\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Scheb",
|
||||
"email": "me@christianscheb.de"
|
||||
}
|
||||
],
|
||||
"description": "Extends scheb/2fa-bundle with trusted devices support",
|
||||
"homepage": "https://github.com/scheb/2fa",
|
||||
"keywords": [
|
||||
"2fa",
|
||||
"Authentication",
|
||||
"symfony",
|
||||
"trusted-device",
|
||||
"two-factor",
|
||||
"two-step"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/scheb/2fa-trusted-device/tree/v8.6.0"
|
||||
},
|
||||
"time": "2026-01-24T13:26:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "scssphp/scssphp",
|
||||
"version": "v1.11.1",
|
||||
|
||||
@ -20,3 +20,15 @@ scheb_two_factor:
|
||||
algorithm: sha1
|
||||
digits: 6
|
||||
period: 30
|
||||
|
||||
# "Dieses Gerät 30 Tage merken" – nach erfolgreicher 2FA wird ein
|
||||
# signiertes Cookie gesetzt, sodass am selben Gerät 30 Tage lang
|
||||
# keine erneute 2FA-Abfrage nötig ist.
|
||||
trusted_device:
|
||||
enabled: true
|
||||
lifetime: 2592000 # 30 Tage (in Sekunden)
|
||||
extend_lifetime: false
|
||||
cookie_name: psc_2fa_trusted
|
||||
cookie_same_site: "lax"
|
||||
# Eigener Signierschlüssel (>=256 Bit); APP_SECRET ist zu kurz (200 Bit).
|
||||
key: '%env(TWO_FACTOR_TRUSTED_KEY)%'
|
||||
|
||||
@ -2966,6 +2966,18 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* code_reuse_cache?: scalar|Param|null, // Default: null
|
||||
* code_reuse_cache_duration?: int|Param, // Default: 60
|
||||
* code_reuse_default_handler?: scalar|Param|null, // Default: null
|
||||
* trusted_device?: bool|array{
|
||||
* enabled?: scalar|Param|null, // Default: false
|
||||
* manager?: scalar|Param|null, // Default: "scheb_two_factor.default_trusted_device_manager"
|
||||
* lifetime?: int|Param, // Default: 5184000
|
||||
* extend_lifetime?: bool|Param, // Default: false
|
||||
* key?: scalar|Param|null, // Default: null
|
||||
* cookie_name?: scalar|Param|null, // Default: "trusted_device"
|
||||
* cookie_secure?: true|false|"auto"|Param, // Default: "auto"
|
||||
* cookie_domain?: scalar|Param|null, // Default: null
|
||||
* cookie_path?: scalar|Param|null, // Default: "/"
|
||||
* cookie_same_site?: scalar|Param|null, // Default: "lax"
|
||||
* },
|
||||
* email?: bool|array{
|
||||
* enabled?: scalar|Param|null, // Default: false
|
||||
* mailer?: scalar|Param|null, // Default: null
|
||||
|
||||
@ -22,6 +22,7 @@ use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface as EmailTwoFactorInterf
|
||||
use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
|
||||
use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface;
|
||||
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface as TotpTwoFactorInterface;
|
||||
use Scheb\TwoFactorBundle\Model\TrustedDeviceInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
@ -34,7 +35,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
|
||||
*/
|
||||
#[ORM\Table(name: 'contact')]
|
||||
#[ORM\Entity(repositoryClass: 'PSC\Shop\ContactBundle\Repository\ContactRepository')]
|
||||
class Contact implements UserInterface, PasswordAuthenticatedUserInterface, \Serializable, EmailTwoFactorInterface, TotpTwoFactorInterface
|
||||
class Contact implements UserInterface, PasswordAuthenticatedUserInterface, \Serializable, EmailTwoFactorInterface, TotpTwoFactorInterface, TrustedDeviceInterface
|
||||
{
|
||||
public $kundenNr = '';
|
||||
public $calcValue1 = '';
|
||||
@ -424,6 +425,16 @@ class Contact implements UserInterface, PasswordAuthenticatedUserInterface, \Ser
|
||||
return $this->totpSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version des Trusted-Device-Tokens ("Gerät 30 Tage merken").
|
||||
* Wird die Version erhöht, verlieren alle bereits vertrauten Geräte
|
||||
* des Benutzers ihre Gültigkeit.
|
||||
*/
|
||||
public function getTrustedTokenVersion(): int
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function setTotpSecret(?string $secret): void
|
||||
{
|
||||
$this->totpSecret = $secret;
|
||||
|
||||
@ -93,6 +93,15 @@ class Position
|
||||
#[OA\Property(type: 'string')]
|
||||
private string $basketField2;
|
||||
|
||||
#[OA\Property(type: 'boolean')]
|
||||
private bool $revoked = false;
|
||||
|
||||
#[OA\Property(type: 'string')]
|
||||
private ?string $revokedAt = null;
|
||||
|
||||
#[OA\Property(type: 'boolean')]
|
||||
private bool $revokeAllowed = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->uuid = Uuid::uuid4()->toString();
|
||||
@ -103,6 +112,36 @@ class Position
|
||||
$this->price = new Price();
|
||||
}
|
||||
|
||||
public function isRevoked(): bool
|
||||
{
|
||||
return $this->revoked;
|
||||
}
|
||||
|
||||
public function setRevoked(bool $revoked): void
|
||||
{
|
||||
$this->revoked = $revoked;
|
||||
}
|
||||
|
||||
public function getRevokedAt(): ?string
|
||||
{
|
||||
return $this->revokedAt;
|
||||
}
|
||||
|
||||
public function setRevokedAt(?string $revokedAt): void
|
||||
{
|
||||
$this->revokedAt = $revokedAt;
|
||||
}
|
||||
|
||||
public function isRevokeAllowed(): bool
|
||||
{
|
||||
return $this->revokeAllowed;
|
||||
}
|
||||
|
||||
public function setRevokeAllowed(bool $revokeAllowed): void
|
||||
{
|
||||
$this->revokeAllowed = $revokeAllowed;
|
||||
}
|
||||
|
||||
public function getPos(): int
|
||||
{
|
||||
return $this->pos;
|
||||
|
||||
@ -437,8 +437,8 @@
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'nameDetails'|trans}}</th>
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'wkpos1'|trans}} {{'and'|trans}} {{'wkpos2'|trans}}</th>
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'propertiesEdition'|trans}}</th>
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'revokeStatus'|trans}}</th>
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'prices'|trans}}</th>
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'revokeStatus'|trans}}</th>
|
||||
<th class="px-3 py-3 text-left font-medium text-gray-700">{{'actions'|trans}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -547,6 +547,24 @@
|
||||
{% endfor %}
|
||||
</td>
|
||||
|
||||
{# Preise #}
|
||||
<td class="px-3 py-4">
|
||||
<dl class="text-xs space-y-1 w-40">
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-gray-500">{{'netto'|trans}}</dt>
|
||||
<dd class="text-gray-800 text-right">{{ pos.obj.priceAllNetto|number_format(2, ',', '.') }} €</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-gray-500">{{'vat'|trans}}</dt>
|
||||
<dd class="text-gray-800 text-right">{{ pos.obj.priceAllSteuer|number_format(2, ',', '.') }} €</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 border-t border-gray-100 pt-1">
|
||||
<dt class="text-gray-700 font-semibold">{{'brutto'|trans}}</dt>
|
||||
<dd class="text-gray-900 font-semibold text-right">{{ pos.obj.priceAllBrutto|number_format(2, ',', '.') }} €</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</td>
|
||||
|
||||
{# Widerruf / Status #}
|
||||
<td class="px-3 py-4">
|
||||
{% if pos.obj.revoked %}
|
||||
@ -586,24 +604,6 @@
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{# Preise #}
|
||||
<td class="px-3 py-4">
|
||||
<dl class="text-xs space-y-1 w-40">
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-gray-500">{{'netto'|trans}}</dt>
|
||||
<dd class="text-gray-800 text-right">{{ pos.obj.priceAllNetto|number_format(2, ',', '.') }} €</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-gray-500">{{'vat'|trans}}</dt>
|
||||
<dd class="text-gray-800 text-right">{{ pos.obj.priceAllSteuer|number_format(2, ',', '.') }} €</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 border-t border-gray-100 pt-1">
|
||||
<dt class="text-gray-700 font-semibold">{{'brutto'|trans}}</dt>
|
||||
<dd class="text-gray-900 font-semibold text-right">{{ pos.obj.priceAllBrutto|number_format(2, ',', '.') }} €</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</td>
|
||||
|
||||
{# Aktionen #}
|
||||
<td class="px-3 py-4">
|
||||
<div class="flex flex-col gap-4 w-64">
|
||||
|
||||
@ -110,9 +110,21 @@
|
||||
{{ orderObj.getInvoiceAddress().street }} {{ orderObj.getInvoiceAddress().houseNumber }}<br/>
|
||||
{{ orderObj.getInvoiceAddress().zip }} {{ orderObj.getInvoiceAddress().city }}
|
||||
</td>
|
||||
<td class="px-2 py-3 text-right whitespace-nowrap">
|
||||
{{ order.netto|number_format(2, ',', '.') }}€<br/>
|
||||
<strong>({{ order.brutto|number_format(2, ',', '.') }}€)</strong>
|
||||
<td class="px-2 py-3">
|
||||
<dl class="text-xs space-y-1 w-40 ml-auto">
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-gray-500">{{'netto'|trans({}, 'core_order_detail')}}</dt>
|
||||
<dd class="text-gray-800 text-right">{{ order.netto|number_format(2, ',', '.') }} €</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4">
|
||||
<dt class="text-gray-500">{{'vat'|trans({}, 'core_order_detail')}}</dt>
|
||||
<dd class="text-gray-800 text-right">{{ order.steuer|number_format(2, ',', '.') }} €</dd>
|
||||
</div>
|
||||
<div class="flex justify-between gap-4 border-t border-gray-100 pt-1">
|
||||
<dt class="text-gray-700 font-semibold">{{'brutto'|trans({}, 'core_order_detail')}}</dt>
|
||||
<dd class="text-gray-900 font-semibold text-right">{{ order.brutto|number_format(2, ',', '.') }} €</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</td>
|
||||
<td class="px-2 py-3 text-right">
|
||||
<div class="flex flex-col gap-1 min-w-[180px]">
|
||||
|
||||
@ -149,6 +149,9 @@ class Position extends Base
|
||||
$position->setDownloadAllowed($positionDoc->isDownloadAllowed());
|
||||
$position->setBasketField1((string) $pos->getBasketField1());
|
||||
$position->setBasketField2((string) $pos->getBasketField2());
|
||||
$position->setRevoked($pos->isRevoked());
|
||||
$position->setRevokedAt($pos->getRevokedAt() ? $pos->getRevokedAt()->format('d.m.Y H:i') : null);
|
||||
$position->setRevokeAllowed($pos->isRevokeAllowed());
|
||||
$position->setLayouterMode((int) $pos->getLayouterMode());
|
||||
$position->setWeight((int) $pos->getWeight());
|
||||
$position->setUploadMode((string) $positionDoc->getUploadMode());
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
{% if displayTrustedOption %}
|
||||
<label class="mt-3 flex items-center gap-2 text-sm text-gray-700">
|
||||
<input type="checkbox" name="{{ trustedParameterName }}" class="rounded border-gray-300">
|
||||
{{ "trusted"|trans({}, 'SchebTwoFactorBundle') }}
|
||||
Dieses Gerät 30 Tage merken
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if isCsrfProtectionEnabled %}
|
||||
|
||||
@ -12,33 +12,30 @@
|
||||
"jsxImportSourceTypes": "@types/react"
|
||||
},
|
||||
"imports": {
|
||||
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0",
|
||||
"@deno/vite-plugin": "npm:@deno/vite-plugin@^2.0.2",
|
||||
"@ebay/nice-modal-react": "npm:@ebay/nice-modal-react@^1.2.13",
|
||||
"@heroicons/react": "npm:@heroicons/react@^2.2.0",
|
||||
"@rjsf/core": "npm:@rjsf/core@^5.23.2",
|
||||
"@rjsf/validator-ajv6": "npm:@rjsf/validator-ajv6@^5.23.2",
|
||||
"@types/react": "npm:@types/react@^18.3.12",
|
||||
"@types/react-dom": "npm:@types/react-dom@^18.3.1",
|
||||
"@udibo/esbuild-plugin-postcss": "jsr:@udibo/esbuild-plugin-postcss@^0.1.3",
|
||||
"@vitejs/plugin-react-swc": "npm:@vitejs/plugin-react-swc@^3.7.1",
|
||||
"autoprefixer": "npm:autoprefixer@^10.4.20",
|
||||
"axios": "npm:axios@^1.7.9",
|
||||
"flowbite-react": "npm:flowbite-react@^0.10.2",
|
||||
"postcss": "npm:postcss@^8.4.49",
|
||||
"@rjsf/core": "npm:@rjsf/core@^5.24.13",
|
||||
"@rjsf/utils": "npm:@rjsf/utils@^5.24.13",
|
||||
"@rjsf/validator-ajv6": "npm:@rjsf/validator-ajv6@^5.24.13",
|
||||
"@tailwindcss/vite": "npm:@tailwindcss/vite@^4.3.2",
|
||||
"@types/react": "npm:@types/react@^19.2.17",
|
||||
"@types/react-dom": "npm:@types/react-dom@^19.2.3",
|
||||
"@vitejs/plugin-react-swc": "npm:@vitejs/plugin-react-swc@^4.3.1",
|
||||
"axios": "npm:axios@^1.18.1",
|
||||
"prop-types": "npm:prop-types@^15.8.1",
|
||||
"react": "npm:react@^18.2.0",
|
||||
"react-dom": "npm:react-dom@^18.2.0",
|
||||
"react-dom/client": "npm:react-dom@^18.2.0/client",
|
||||
"react-router-dom": "npm:react-router-dom@^7.0.2",
|
||||
"react": "npm:react@^19.2.7",
|
||||
"react-dom": "npm:react-dom@^19.2.7",
|
||||
"react-dom/client": "npm:react-dom@^19.2.7/client",
|
||||
"react-router-dom": "npm:react-router-dom@^7.18.1",
|
||||
"react-select": "npm:react-select@^5.10.2",
|
||||
"react-select-async-paginate": "npm:react-select-async-paginate@^0.7.7",
|
||||
"react-select-async-paginate": "npm:react-select-async-paginate@^0.7.11",
|
||||
"reflect-metadata": "npm:reflect-metadata@^0.2.2",
|
||||
"rxjs": "npm:rxjs@^7.8.1",
|
||||
"styled-components": "npm:styled-components@^6.1.13",
|
||||
"tailwindcss": "npm:tailwindcss@^3.4.17",
|
||||
"rxjs": "npm:rxjs@^7.8.2",
|
||||
"tailwindcss": "npm:tailwindcss@^4.3.2",
|
||||
"tsyringe-neo": "npm:tsyringe-neo@^5.1.0",
|
||||
"use-debounce": "npm:use-debounce@^10.0.4",
|
||||
"uuid": "npm:uuid@^11.0.3",
|
||||
"vite": "npm:vite@^6.0.5"
|
||||
"use-debounce": "npm:use-debounce@^10.1.1",
|
||||
"uuid": "npm:uuid@^14.0.1",
|
||||
"vite": "npm:vite@^8.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>Auftrag – PrintShopCreator</title>
|
||||
</head>
|
||||
<body>
|
||||
<body class="bg-psc-bg font-sans text-psc-text">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
import tailwindcss from "tailwindcss";
|
||||
import autoprefixer from "autoprefixer";
|
||||
|
||||
export default {
|
||||
plugins: [tailwindcss, autoprefixer],
|
||||
};
|
||||
@ -1,4 +1,3 @@
|
||||
import * as Eta from 'eta'
|
||||
import Token from '../services/token'
|
||||
import { container } from "tsyringe-neo"
|
||||
import * as React from 'react'
|
||||
|
||||
@ -1,75 +1,30 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
/* PrintShopCreator Box-Stil Design-Tokens (siehe preview.html) */
|
||||
@theme {
|
||||
--color-psc-orange: #f26c21;
|
||||
--color-psc-orange-dark: #db5b15;
|
||||
--color-psc-bg: #edf1f5;
|
||||
--color-psc-border: #d9e0e7;
|
||||
--color-psc-text: #1f2937;
|
||||
--color-psc-muted: #5f6b7a;
|
||||
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system,
|
||||
BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Better font rendering */
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Card hover effects */
|
||||
.card-hover {
|
||||
@apply transition-all duration-200 hover:shadow-lg hover:-translate-y-1;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for webkit browsers */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
@apply bg-gray-100 dark:bg-gray-800 rounded;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
@apply bg-gray-300 dark:bg-gray-600 rounded hover:bg-gray-400 dark:hover:bg-gray-500;
|
||||
}
|
||||
|
||||
/* Improved select styling */
|
||||
.select-container {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
/* Focus ring improvements */
|
||||
.focus-ring {
|
||||
@apply focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:outline-none;
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.badge {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
@apply bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-800 dark:text-yellow-100;
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
@apply bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
@apply bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Animation utilities */
|
||||
/* Sanfte Einblend-Animation (in mehreren Komponenten genutzt) */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
@ -85,19 +40,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Glass morphism effect */
|
||||
.glass {
|
||||
@apply bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm;
|
||||
}
|
||||
|
||||
/* Gradient text */
|
||||
.gradient-text {
|
||||
@apply bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-purple-600;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tailwind styling for default @rjsf/core form markup.
|
||||
Keep outside @layer so Tailwind does not purge it from generated CSS. */
|
||||
/* Tailwind-Styling für das Standard-Markup von @rjsf/core.
|
||||
Außerhalb @layer, damit es nicht weg-optimiert wird. */
|
||||
.rjsf {
|
||||
@apply space-y-4;
|
||||
}
|
||||
@ -131,17 +75,17 @@
|
||||
.rjsf input[type="text"],
|
||||
.rjsf input[type="email"],
|
||||
.rjsf input[type="number"] {
|
||||
@apply w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500;
|
||||
@apply w-full rounded-sm border border-psc-border bg-white px-3 py-2 text-sm text-psc-text shadow-sm focus:border-psc-orange focus:ring-2 focus:ring-psc-orange/40 focus:outline-none;
|
||||
}
|
||||
|
||||
.rjsf input[type="checkbox"],
|
||||
.rjsf input[type="radio"] {
|
||||
@apply h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500;
|
||||
@apply h-4 w-4 border-psc-border text-psc-orange focus:ring-psc-orange;
|
||||
}
|
||||
|
||||
.rjsf .checkbox label,
|
||||
.rjsf .radio label {
|
||||
@apply flex items-center gap-2 text-sm text-gray-700;
|
||||
@apply flex items-center gap-2 text-sm text-psc-text;
|
||||
}
|
||||
|
||||
.rjsf .text-danger,
|
||||
|
||||
@ -1,69 +0,0 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import "./assets/index.css"
|
||||
import "reflect-metadata";
|
||||
import "./modules/uploadOptions";
|
||||
import * as $ from "jquery";
|
||||
import { App } from "./app/app";
|
||||
|
||||
declare var jwt_token: String;
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
import { AsyncPaginate } from "react-select-async-paginate"
|
||||
import AccountService from '../../services/account'
|
||||
import { container } from 'tsyringe-neo'
|
||||
import OrderState from '../../state/order'
|
||||
import {useEffect, useState} from 'react'
|
||||
import * as PropTypes from 'prop-types'
|
||||
import { Shop } from '../../model/shop'
|
||||
import Order from "../../model/order"
|
||||
import { SelectLabel } from "../base/SelectLabel"
|
||||
import React from 'react'
|
||||
const AccountSelectComponent = ({shop, order}) => {
|
||||
|
||||
const [shopUuid, setShopUuid] = useState(null)
|
||||
|
||||
const [account, setAccount] = useState(0)
|
||||
useEffect(() => {
|
||||
setShopUuid(shop.uuid)
|
||||
}, [shop.uuid])
|
||||
|
||||
const orderState: OrderState = container.resolve(OrderState)
|
||||
|
||||
const loadOptions = async (searchQuery, loadedOptions) => {
|
||||
|
||||
const account_service = new AccountService()
|
||||
|
||||
const data = await account_service.getAccounts(shop)
|
||||
|
||||
return {
|
||||
options: data,
|
||||
hasMore: false,
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setAccount(order.account.uuid)
|
||||
}, [order.account])
|
||||
|
||||
const onChange = (option) => {
|
||||
order.account = option
|
||||
let orderObj = orderState.getCurrentOrder().getValue()
|
||||
orderObj.account = option
|
||||
orderState.setCurrentOrder(orderObj)
|
||||
setAccount(option.uid)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<AsyncPaginate
|
||||
defaultOptions
|
||||
className={`${account == 0 ? "border border-danger" : ""}`}
|
||||
key={JSON.stringify(shopUuid)}
|
||||
defaultValue={order.account}
|
||||
components={{Control: SelectLabel}}
|
||||
name={'Firma'}
|
||||
loadOptions={loadOptions}
|
||||
placeholder=''
|
||||
getOptionValue={(option) => option.uuid}
|
||||
getOptionLabel={(option) => option.title}
|
||||
onChange={onChange}
|
||||
isSearchable={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
AccountSelectComponent.propTypes = {
|
||||
shop: PropTypes.instanceOf(Shop),
|
||||
order: PropTypes.instanceOf(Order),
|
||||
}
|
||||
|
||||
export default AccountSelectComponent
|
||||
@ -1,7 +1,7 @@
|
||||
import { Shop } from '../../model/shop'
|
||||
import ContactComponent from '../contact/ContactComponent'
|
||||
import PositionsComponent from '../positions/PositionsComponent'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useReducer, useState } from 'react'
|
||||
import Order from "../../model/order"
|
||||
import PaymentComponent from "../payment/PaymentComponent"
|
||||
import ShippingComponent from "../shipping/ShippingComponent"
|
||||
@ -12,24 +12,63 @@ import OrderState from "../../state/order"
|
||||
import OrderService from "../../services/order"
|
||||
import InfoFieldComponent from "../info/InfoFieldComponent"
|
||||
import React from 'react'
|
||||
import { debounceTime } from 'rxjs'
|
||||
import CalcComponent from '../calc/CalcComponent'
|
||||
import DraftComponent from '../draft/DraftComponent'
|
||||
import TypeSelectComponent from '../type/TypeSelectComponent'
|
||||
import ShopSelectComponent from '../shop/ShopSelectComponent'
|
||||
import Box from './Box'
|
||||
|
||||
const BaseComponent = (props) => {
|
||||
console.log('BaseComponent rendering')
|
||||
const TYPE_LABELS: Record<number, string> = {
|
||||
1: 'Auftrag',
|
||||
2: 'Angebot',
|
||||
3: 'Storno',
|
||||
4: 'Gutschrift',
|
||||
5: 'Rechnung',
|
||||
6: 'Teilrechnung',
|
||||
7: 'Teilstorno',
|
||||
}
|
||||
|
||||
const DocIcon = () => (
|
||||
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M7 3h7l4 4v14H7V3z" stroke="currentColor" strokeWidth="2" />
|
||||
<path d="M14 3v5h5M9 13h6M9 17h6" stroke="currentColor" strokeWidth="2" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const StatusRow = ({ ok, label }: { ok: boolean; label: string }) => (
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span
|
||||
className={`grid h-6 w-6 place-items-center rounded-sm border ${
|
||||
ok
|
||||
? 'border-green-200 bg-green-100 text-green-700'
|
||||
: 'border-orange-200 bg-orange-50 text-psc-orange'
|
||||
}`}
|
||||
>
|
||||
{ok ? '✓' : '!'}
|
||||
</span>
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
|
||||
const BaseComponent = () => {
|
||||
|
||||
const [shop, setShop] = useState<Shop>(new Shop())
|
||||
const [order, setOrder] = useState<Order>(new Order())
|
||||
const [type, setType] = useState<number>(0)
|
||||
const [isSetup, setIsSetup] = useState<boolean>(true)
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0)
|
||||
|
||||
const order_state = container.resolve(OrderState)
|
||||
const orderService = container.resolve(OrderService)
|
||||
|
||||
const loadOrder = async (uuid: String) => {
|
||||
orderService.getOrder(uuid).then((orderObj) => {
|
||||
order_state.setCurrentOrder(orderObj)
|
||||
setShop(orderObj.shop)
|
||||
setOrder(orderObj)
|
||||
setType(Number(orderObj.type) || 0)
|
||||
setIsSetup(false)
|
||||
})
|
||||
}
|
||||
|
||||
@ -38,86 +77,142 @@ const BaseComponent = (props) => {
|
||||
useEffect(() => {
|
||||
if (params.uuid) {
|
||||
loadOrder(params.uuid)
|
||||
} else {
|
||||
const current = order_state.getCurrentOrder().value
|
||||
setOrder(current)
|
||||
setType(Number(current.type) || 0)
|
||||
setIsSetup(true)
|
||||
}
|
||||
|
||||
const sub = order_state.getCurrentOrder().pipe(debounceTime(300)).subscribe(() => forceUpdate())
|
||||
return () => sub.unsubscribe()
|
||||
}, [])
|
||||
|
||||
const setupReady = shop.id != 0 && type != 0
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-gray-900 dark:to-gray-800 text-gray-900 dark:text-gray-100 antialiased">
|
||||
<div className='w-full px-6 py-4'>
|
||||
{/* Top Header Section - Settings & Controls */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 mb-4 animate-fade-in">
|
||||
{/* First Row: Order Info & Action Buttons */}
|
||||
<div className="flex flex-wrap items-center gap-4 mb-4">
|
||||
{/* Left: Order Number & Draft Toggle */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="font-semibold text-lg text-gray-800 dark:text-gray-100">
|
||||
NR: <span className="text-[#EA641B]">{order.alias || '---'}</span>
|
||||
<div className="min-h-screen bg-psc-bg font-sans text-psc-text antialiased">
|
||||
<main className="w-full px-4 py-4 lg:px-6">
|
||||
{/* Header */}
|
||||
<header className="rounded-sm border border-psc-border bg-white px-7.5 py-6 shadow-lg">
|
||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div className="flex min-w-0 items-center gap-4">
|
||||
<div className="grid h-10 w-10 shrink-0 place-items-center border border-orange-200 bg-orange-50 text-psc-orange">
|
||||
<DocIcon />
|
||||
</div>
|
||||
<div className="border-l border-gray-300 dark:border-gray-600 h-8"></div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
||||
<h1 className="truncate text-2xl font-semibold">
|
||||
NR: <span className="text-psc-orange">{order.alias || '---'}</span>
|
||||
</h1>
|
||||
<span className="hidden h-7 w-px bg-slate-200 sm:block"></span>
|
||||
<DraftComponent order={order} />
|
||||
</div>
|
||||
|
||||
{/* Right: Action Buttons */}
|
||||
<div className="ml-auto">
|
||||
<ButtonComponent loadOrder={loadOrder} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Second Row: Type & Shop Selection */}
|
||||
<div className="flex flex-wrap items-center gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<TypeSelectComponent order={order} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<ShopSelectComponent shop={shop} change={setShop} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
{shop.id != 0 && (
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
{/* Left Column - Main Content (2/3 width) */}
|
||||
<div className="xl:col-span-2 space-y-4">
|
||||
{/* Contact Section */}
|
||||
<ContactComponent shop={shop} order={order} />
|
||||
|
||||
{/* Positions Section */}
|
||||
<PositionsComponent shop={shop} order={order} />
|
||||
|
||||
{/* Payment & Shipping Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<PaymentComponent shop={shop} order={order} />
|
||||
<ShippingComponent shop={shop} order={order} />
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<InfoFieldComponent shop={shop} order={order} />
|
||||
</div>
|
||||
|
||||
{/* Right Column - Summary Sidebar (1/3 width) */}
|
||||
<div className="xl:col-span-1">
|
||||
<div className="sticky top-4 space-y-4">
|
||||
{/* Price Summary Card */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 border-2 border-orange-200 dark:border-orange-900">
|
||||
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-100 flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-[#EA641B]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
Preisübersicht
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<CalcComponent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isSetup && (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<span className="rounded-sm border border-psc-border bg-slate-50 px-3 py-1 text-xs font-medium text-psc-muted">
|
||||
{TYPE_LABELS[type] || 'Typ'}
|
||||
</span>
|
||||
<span className="rounded-sm border border-psc-border bg-slate-50 px-3 py-1 text-xs font-medium text-psc-muted">
|
||||
{shop.name || 'Shop'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSetup(true)}
|
||||
className="rounded-sm border border-psc-border bg-white px-3 py-1 text-xs font-medium text-psc-orange hover:bg-orange-50"
|
||||
>
|
||||
Typ / Shop ändern
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
{!isSetup && (
|
||||
<div className="flex items-center">
|
||||
<ButtonComponent loadOrder={loadOrder} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* First-run Setup */}
|
||||
{isSetup && (
|
||||
<section className="mx-auto mt-10 max-w-4xl">
|
||||
<div className="rounded-sm border border-psc-border bg-white px-7.5 py-6 shadow-lg">
|
||||
<div className="mb-7">
|
||||
<p className="text-sm font-semibold uppercase tracking-[.16em] text-psc-orange">Startauswahl</p>
|
||||
<h3 className="mt-2 text-2xl font-semibold">Wofür soll der Auftrag erstellt werden?</h3>
|
||||
<p className="mt-2 text-psc-muted">Diese Angaben stehen anschließend kompakt im Kopfbereich.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-5">
|
||||
<TypeSelectComponent order={order} onChange={setType} />
|
||||
<ShopSelectComponent shop={shop} change={setShop} />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={!setupReady}
|
||||
onClick={() => setIsSetup(false)}
|
||||
className="mt-2 h-11 rounded-sm border border-psc-orange bg-psc-orange px-6 text-sm font-medium text-white transition hover:bg-psc-orange-dark disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Weiter zur Auftragsmaske
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Work area */}
|
||||
{!isSetup && shop.id != 0 && (
|
||||
<div className="mt-5 grid w-full grid-cols-1 gap-5 xl:grid-cols-[400px_minmax(0,1fr)_360px] 2xl:grid-cols-[430px_minmax(0,1fr)_380px]">
|
||||
{/* Left */}
|
||||
<aside className="grid gap-5 xl:sticky xl:top-24 xl:self-start">
|
||||
<ContactComponent shop={shop} order={order} />
|
||||
</aside>
|
||||
|
||||
{/* Center */}
|
||||
<section className="grid gap-5">
|
||||
<PositionsComponent shop={shop} order={order} />
|
||||
<div className="grid gap-5 lg:grid-cols-2">
|
||||
<ShippingComponent shop={shop} order={order} />
|
||||
<PaymentComponent shop={shop} order={order} />
|
||||
</div>
|
||||
<InfoFieldComponent shop={shop} order={order} />
|
||||
</section>
|
||||
|
||||
{/* Right */}
|
||||
<aside className="grid gap-5 xl:sticky xl:top-24 xl:self-start">
|
||||
<Box
|
||||
title="Status & Preis"
|
||||
subtitle="Was noch für den Abschluss fehlt."
|
||||
icon={
|
||||
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M7 3h10v18H7V3zM9 7h6M9 11h.01M12 11h.01M15 11h.01M9 15h.01M12 15h.01M15 15h.01" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-3">
|
||||
<StatusRow ok={setupReady} label="Typ & Shop gewählt" />
|
||||
<StatusRow ok={!!order.contact?.username} label="Kunde ausgewählt" />
|
||||
<StatusRow ok={!!order.invoiceAddress?.uuid} label="Rechnungsadresse" />
|
||||
<StatusRow ok={!!order.deliveryAddress?.uuid} label="Lieferadresse" />
|
||||
<StatusRow ok={!!order.payment?.title} label="Zahlart" />
|
||||
<StatusRow ok={!!order.shipping?.title} label="Versandart" />
|
||||
</div>
|
||||
|
||||
<div className="my-5 border-t border-psc-border"></div>
|
||||
|
||||
<CalcComponent />
|
||||
</Box>
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BaseComponent
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
|
||||
type BoxProps = {
|
||||
icon?: React.ReactNode
|
||||
title?: React.ReactNode
|
||||
subtitle?: React.ReactNode
|
||||
action?: React.ReactNode
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const Box = ({ icon, title, subtitle, action, className = '', children }: BoxProps) => (
|
||||
<section
|
||||
className={`rounded-sm border border-psc-border bg-white px-7.5 py-6 shadow-lg ${className}`}
|
||||
>
|
||||
{(title || icon || action) && (
|
||||
<div className="mb-6 flex flex-col gap-4 border-b border-psc-border pb-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
{icon && (
|
||||
<div className="grid h-9 w-9 shrink-0 place-items-center rounded-sm border border-orange-200 bg-orange-50 text-psc-orange">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{title && <h2 className="text-xl font-medium text-psc-text">{title}</h2>}
|
||||
{subtitle && <p className="mt-0.5 text-sm text-psc-muted">{subtitle}</p>}
|
||||
</div>
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
|
||||
export default Box
|
||||
@ -1,35 +1,102 @@
|
||||
import { Button as BaseButton } from "flowbite-react"
|
||||
import React from 'react'
|
||||
|
||||
const Button = ({ type , variant , onClick, className = "" }) => {
|
||||
return (
|
||||
<BaseButton color={variant} onClick={onClick} size="sm" className={`p-1.5 ${className}`}>
|
||||
{ type == 1 &&
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" className="w-4 h-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15"></path>
|
||||
</svg>
|
||||
type ButtonProps = {
|
||||
type?: number
|
||||
variant?: string
|
||||
color?: string
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg'
|
||||
disabled?: boolean
|
||||
onClick?: (e: any) => void
|
||||
className?: string
|
||||
title?: string
|
||||
htmlType?: 'button' | 'submit' | 'reset'
|
||||
children?: React.ReactNode
|
||||
}
|
||||
{ type == 2 &&
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" className="w-4 h-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"></path>
|
||||
</svg>
|
||||
}
|
||||
{ type == 3 &&
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" fill="none" className="w-4 h-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 20.25h12A2.25 2.25 0 0 0 20.25 18V7.5L16.5 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25zm9.75-16.5v5h-9.5v-5zM13 5.5V7m-6.75 4.25h11.5v6.5H6.25Z"></path>
|
||||
</svg>
|
||||
}
|
||||
{ type == 4 &&
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" className="w-4 h-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"></path>
|
||||
</svg>
|
||||
}
|
||||
{ type == 5 &&
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" className="w-4 h-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"></path>
|
||||
</svg> }
|
||||
</BaseButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
const COLORS: Record<string, string> = {
|
||||
orange: 'border border-psc-orange bg-psc-orange text-white hover:bg-psc-orange-dark',
|
||||
success: 'border border-psc-orange bg-psc-orange text-white hover:bg-psc-orange-dark',
|
||||
blue: 'border border-blue-600 bg-blue-600 text-white hover:bg-blue-700',
|
||||
info: 'border border-sky-500 bg-sky-500 text-white hover:bg-sky-600',
|
||||
green: 'border border-emerald-600 bg-emerald-600 text-white hover:bg-emerald-700',
|
||||
danger: 'border border-red-200 bg-red-50 text-red-600 hover:bg-red-100',
|
||||
failure: 'border border-red-200 bg-red-50 text-red-600 hover:bg-red-100',
|
||||
gray: 'border border-psc-border bg-white text-psc-text hover:bg-slate-50',
|
||||
light: 'border border-psc-border bg-white text-psc-text hover:bg-slate-50',
|
||||
}
|
||||
|
||||
const SIZE_TEXT: Record<string, string> = {
|
||||
xs: 'h-8 px-2.5 text-xs',
|
||||
sm: 'h-9 px-3 text-sm',
|
||||
md: 'h-10 px-4 text-sm',
|
||||
lg: 'h-11 px-6 text-sm',
|
||||
}
|
||||
|
||||
const SIZE_ICON: Record<string, string> = {
|
||||
xs: 'h-8 w-8',
|
||||
sm: 'h-9 w-9',
|
||||
md: 'h-10 w-10',
|
||||
lg: 'h-11 w-11',
|
||||
}
|
||||
|
||||
const ICON_PATHS: Record<number, string> = {
|
||||
1: 'M12 4.5v15m7.5-7.5h-15',
|
||||
2: 'm16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125',
|
||||
3: 'M6 20.25h12A2.25 2.25 0 0 0 20.25 18V7.5L16.5 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25zm9.75-16.5v5h-9.5v-5zM13 5.5V7m-6.75 4.25h11.5v6.5H6.25Z',
|
||||
4: 'M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z',
|
||||
5: 'm9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z',
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
type,
|
||||
variant,
|
||||
color,
|
||||
size,
|
||||
disabled = false,
|
||||
onClick,
|
||||
className = '',
|
||||
title,
|
||||
htmlType = 'button',
|
||||
children,
|
||||
}: ButtonProps) => {
|
||||
const colorKey = (color ?? variant ?? 'orange').toLowerCase()
|
||||
const colorClasses = COLORS[colorKey] ?? COLORS.orange
|
||||
|
||||
const iconOnly = type != null && !children
|
||||
const sizeKey = size ?? (iconOnly ? 'sm' : 'md')
|
||||
const sizeClasses = iconOnly ? SIZE_ICON[sizeKey] : SIZE_TEXT[sizeKey]
|
||||
|
||||
return (
|
||||
<button
|
||||
type={htmlType}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
className={`inline-flex items-center justify-center gap-2 rounded-sm font-medium transition disabled:cursor-not-allowed disabled:opacity-50 ${colorClasses} ${sizeClasses} ${className}`}
|
||||
>
|
||||
{type != null && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
className="h-4 w-4"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d={ICON_PATHS[type]} />
|
||||
</svg>
|
||||
)}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export const ButtonGroup = ({
|
||||
children,
|
||||
className = '',
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) => <div className={`inline-flex ${className}`}>{children}</div>
|
||||
|
||||
export default Button
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
const FieldLabel = ({ children }: { children: React.ReactNode }) => (
|
||||
<span className="mb-2 block text-sm font-medium text-psc-text">{children}</span>
|
||||
)
|
||||
|
||||
export default FieldLabel
|
||||
@ -0,0 +1,97 @@
|
||||
import React, { createContext, useContext, useEffect } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
type ModalContextValue = { onClose: () => void }
|
||||
const ModalContext = createContext<ModalContextValue>({ onClose: () => {} })
|
||||
|
||||
const SIZE_MAP: Record<string, string> = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-md',
|
||||
lg: 'max-w-lg',
|
||||
xl: 'max-w-xl',
|
||||
'2xl': 'max-w-2xl',
|
||||
'3xl': 'max-w-3xl',
|
||||
'4xl': 'max-w-4xl',
|
||||
'5xl': 'max-w-5xl',
|
||||
'6xl': 'max-w-6xl',
|
||||
'7xl': 'max-w-7xl',
|
||||
'80': 'max-w-[80vw]',
|
||||
}
|
||||
|
||||
type ModalProps = {
|
||||
show: boolean
|
||||
onClose: () => void
|
||||
size?: keyof typeof SIZE_MAP
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const Modal = ({ show, onClose, size = '80', className = '', children }: ModalProps) => {
|
||||
useEffect(() => {
|
||||
if (!show) return
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
document.addEventListener('keydown', onKey)
|
||||
const prevOverflow = document.body.style.overflow
|
||||
document.body.style.overflow = 'hidden'
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKey)
|
||||
document.body.style.overflow = prevOverflow
|
||||
}
|
||||
}, [show, onClose])
|
||||
|
||||
if (!show) return null
|
||||
|
||||
return createPortal(
|
||||
<ModalContext.Provider value={{ onClose }}>
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/50 p-4"
|
||||
onMouseDown={onClose}
|
||||
>
|
||||
<div
|
||||
className={`relative my-10 w-full ${SIZE_MAP[size] ?? SIZE_MAP['2xl']} rounded-sm border border-psc-border bg-white shadow-lg ${className}`}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</ModalContext.Provider>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({ className = '', children }: { className?: string; children?: React.ReactNode }) => {
|
||||
const { onClose } = useContext(ModalContext)
|
||||
return (
|
||||
<div className={`flex items-center justify-between border-b border-psc-border px-6 py-4 ${className}`}>
|
||||
<h3 className="text-lg font-medium text-psc-text">{children}</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Schließen"
|
||||
className="rounded-sm p-1 text-psc-muted transition hover:bg-slate-100 hover:text-psc-text"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Body = ({ className = '', children }: { className?: string; children?: React.ReactNode }) => (
|
||||
<div className={`px-6 py-4 ${className}`}>{children}</div>
|
||||
)
|
||||
|
||||
const Footer = ({ className = '', children }: { className?: string; children?: React.ReactNode }) => (
|
||||
<div className={`flex items-center justify-end gap-2 border-t border-psc-border px-6 py-4 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
Modal.Header = Header
|
||||
Modal.Body = Body
|
||||
Modal.Footer = Footer
|
||||
|
||||
export default Modal
|
||||
@ -1,29 +0,0 @@
|
||||
import styled from "styled-components"
|
||||
import { components } from "react-select"
|
||||
import * as React from 'react'
|
||||
|
||||
export const SelectLabel = ({ children, ...props }: any) => {
|
||||
const hasValue = props.hasValue || (props.getValue && props.getValue().length > 0 && props.getValue()[0]?.uuid !== "");
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<components.Control {...props} children={children} />
|
||||
<Label $isFloating={!hasValue}>{props.selectProps.name}</Label>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Label = styled.label<{ $isFloating?: boolean }>`
|
||||
left: 10px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
transition: 0.2s ease all;
|
||||
-moz-transition: 0.2s ease all;
|
||||
-webkit-transition: 0.2s ease all;
|
||||
top: ${(props) => (props.$isFloating ? `20%` : `0px`)};
|
||||
font-size: ${(props) => (props.$isFloating ? `1rem` : `0.6rem`)};
|
||||
`;
|
||||
@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
|
||||
const SIZES: Record<string, string> = {
|
||||
xs: 'h-3 w-3',
|
||||
sm: 'h-4 w-4',
|
||||
md: 'h-5 w-5',
|
||||
lg: 'h-6 w-6',
|
||||
}
|
||||
|
||||
const Spinner = ({
|
||||
size = 'md',
|
||||
className = '',
|
||||
}: {
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg'
|
||||
className?: string
|
||||
}) => (
|
||||
<svg
|
||||
className={`inline animate-spin text-psc-orange ${SIZES[size] ?? SIZES.md} ${className}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="status"
|
||||
aria-label="Lädt"
|
||||
>
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default Spinner
|
||||
@ -4,7 +4,7 @@ import OrderService from '../../services/order'
|
||||
import { Component } from 'react'
|
||||
import { debounceTime } from "rxjs"
|
||||
import { Order } from "../../model/order"
|
||||
import { Button } from "flowbite-react"
|
||||
import Button, { ButtonGroup } from '../base/Button'
|
||||
import React from 'react'
|
||||
|
||||
class ButtonComponent extends Component<{ loadOrder }, { disabled: boolean, saving: boolean }> {
|
||||
@ -53,31 +53,29 @@ class ButtonComponent extends Component<{ loadOrder }, { disabled: boolean, savi
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button.Group>
|
||||
<ButtonGroup className="gap-3">
|
||||
<Button
|
||||
size="md"
|
||||
color="blue"
|
||||
color="success"
|
||||
disabled={this.state.disabled}
|
||||
onClick={(e: any) => this.handleSave(e)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" fill="none" className="mr-2 h-5 w-5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" fill="none" className="h-5 w-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 20.25h12A2.25 2.25 0 0 0 20.25 18V7.5L16.5 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25zm9.75-16.5v5h-9.5v-5zM13 5.5V7m-6.75 4.25h11.5v6.5H6.25Z"></path>
|
||||
</svg>
|
||||
{this.state.saving === true && <span>wird gespeichert...</span>}
|
||||
{this.state.saving === false && <span>Speichern</span>}
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
color="success"
|
||||
color="light"
|
||||
disabled={!this.orderState.getCurrentOrder().value.saved}
|
||||
onClick={(e: any) => this.handlePrint(e)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="mr-2 h-5 w-5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="h-5 w-5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0 .229 2.523a1.125 1.125 0 0 1-1.12 1.227H7.231c-.662 0-1.18-.568-1.12-1.227L6.34 18m11.318 0h1.091A2.25 2.25 0 0 0 21 15.75V9.456c0-1.081-.768-2.015-1.837-2.175a48.055 48.055 0 0 0-1.913-.247M6.34 18H5.25A2.25 2.25 0 0 1 3 15.75V9.456c0-1.081.768-2.015 1.837-2.175a48.041 48.041 0 0 1 1.913-.247m10.5 0a48.536 48.536 0 0 0-10.5 0m10.5 0V3.375c0-.621-.504-1.125-1.125-1.125h-8.25c-.621 0-1.125.504-1.125 1.125v3.659M18 10.5h.008v.008H18V10.5Zm-3 0h.008v.008H15V10.5Z"></path>
|
||||
</svg>
|
||||
Drucken
|
||||
</Button>
|
||||
</Button.Group>
|
||||
</ButtonGroup>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,21 +39,21 @@ class CalcComponent extends React.Component<{ },MyState> {
|
||||
render() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Netto</span>
|
||||
<span className="text-base font-semibold text-gray-800 dark:text-gray-200">
|
||||
<div className="flex items-center justify-between gap-6 text-sm text-psc-muted">
|
||||
<span>Netto</span>
|
||||
<span className="font-medium text-psc-text">
|
||||
<Currency price={this.state.order.net} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">MwSt.</span>
|
||||
<span className="text-base font-semibold text-gray-800 dark:text-gray-200">
|
||||
<div className="flex items-center justify-between gap-6 text-sm text-psc-muted">
|
||||
<span>MwSt.</span>
|
||||
<span className="font-medium text-psc-text">
|
||||
<Currency price={this.state.order.vat} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center py-3 bg-orange-50 dark:bg-orange-900/20 rounded-lg px-3 mt-2">
|
||||
<span className="text-base font-bold text-gray-900 dark:text-gray-100">Brutto</span>
|
||||
<span className="text-xl font-bold text-[#EA641B] dark:text-orange-400">
|
||||
<div className="mt-1 flex items-baseline justify-between gap-6 rounded-sm border border-orange-200 bg-orange-50 p-4">
|
||||
<span className="text-sm font-medium text-psc-text">Brutto</span>
|
||||
<span className="text-2xl font-medium text-psc-orange">
|
||||
<Currency price={this.state.order.gross} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,112 @@
|
||||
import React, { useState } from 'react'
|
||||
import { AsyncPaginate } from 'react-select-async-paginate'
|
||||
import { Address } from '../../model/address'
|
||||
import { Contact } from '../../model/contact'
|
||||
import AddressService from '../../services/address'
|
||||
import { useAddressModal } from './useAddressModal'
|
||||
|
||||
type AddressCardProps = {
|
||||
title: string
|
||||
emptyHint: string
|
||||
type: number
|
||||
contact: Contact
|
||||
shop: any
|
||||
value: Address | null
|
||||
onChange: (address: Address, type: number) => void
|
||||
}
|
||||
|
||||
const formatAddress = (a: Address) =>
|
||||
[a.company, a.firstname, a.lastname, a.street, a.houseNumber, a.zip, a.city]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
const AddressCard = ({ title, emptyHint, type, contact, shop, value, onChange }: AddressCardProps) => {
|
||||
const address_service = new AddressService()
|
||||
const [choosing, setChoosing] = useState(false)
|
||||
|
||||
const isSet = !!value?.uuid
|
||||
|
||||
const loadOptions = async () => {
|
||||
const data = await address_service.getContactAddress(contact.uuid, type)
|
||||
return { options: data, hasMore: false }
|
||||
}
|
||||
|
||||
const select = (option: Address) => {
|
||||
onChange(option, type)
|
||||
setChoosing(false)
|
||||
}
|
||||
|
||||
const persist = (addr: Address, create: boolean) => {
|
||||
addr.type = type
|
||||
addr.contact.uuid = contact.uuid
|
||||
const req = create ? address_service.create(addr) : address_service.update(addr)
|
||||
req.then(() => {
|
||||
onChange(addr, type)
|
||||
setChoosing(false)
|
||||
})
|
||||
}
|
||||
|
||||
const { showAdd, showEdit } = useAddressModal(
|
||||
shop,
|
||||
(addr) => persist(addr, true),
|
||||
(addr) => persist(addr, false),
|
||||
)
|
||||
|
||||
return (
|
||||
<article className="rounded-sm border border-psc-border bg-slate-50 p-4">
|
||||
<div className="mb-3 flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-medium text-psc-text">{title}</h3>
|
||||
<p className="mt-1 text-sm text-psc-muted">{isSet ? formatAddress(value as Address) : emptyHint}</p>
|
||||
</div>
|
||||
<span
|
||||
className={`shrink-0 rounded-sm border px-2.5 py-1 text-xs font-medium ${
|
||||
isSet
|
||||
? 'border-green-200 bg-green-50 text-green-700'
|
||||
: 'border-orange-200 bg-orange-50 text-psc-orange'
|
||||
}`}
|
||||
>
|
||||
{isSet ? 'OK' : 'Fehlt'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{choosing ? (
|
||||
<div>
|
||||
<AsyncPaginate
|
||||
defaultOptions
|
||||
key={contact.uuid + '-' + type}
|
||||
loadOptions={loadOptions}
|
||||
getOptionValue={(o: Address) => o.uuid as string}
|
||||
getOptionLabel={(o: Address) =>
|
||||
`${o.firstname} ${o.lastname}`.trim() || (o.company as string) || (o.uuid as string)
|
||||
}
|
||||
onChange={select}
|
||||
isSearchable={true}
|
||||
placeholder="Adresse suchen…"
|
||||
/>
|
||||
<div className="mt-2 flex flex-wrap gap-3">
|
||||
<button type="button" onClick={showAdd} className="text-sm font-medium text-psc-orange hover:underline">
|
||||
+ Neue Adresse
|
||||
</button>
|
||||
<button type="button" onClick={() => setChoosing(false)} className="text-sm font-medium text-psc-muted hover:underline">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<button type="button" onClick={() => setChoosing(true)} className="text-left text-sm font-medium text-psc-orange hover:underline">
|
||||
{isSet ? 'Adresse ändern' : 'Adresse auswählen'}
|
||||
</button>
|
||||
{isSet && (
|
||||
<button type="button" onClick={() => showEdit(value as Address)} className="text-left text-sm font-medium text-psc-muted hover:underline">
|
||||
Bearbeiten
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddressCard
|
||||
@ -1,16 +0,0 @@
|
||||
import * as PropTypes from 'prop-types'
|
||||
import { Address } from '../../model/address'
|
||||
import React from 'react'
|
||||
const AddressDetail = ({address}) => {
|
||||
|
||||
return (
|
||||
<small>{address.company} {address.firstname} {address.lastname} {address.street} {address.houseNumber} {address.country} {address.zip} {address.city}</small>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
AddressDetail.propTypes = {
|
||||
address: PropTypes.instanceOf(Address)
|
||||
}
|
||||
|
||||
export default AddressDetail
|
||||
@ -5,7 +5,7 @@ import { RJSFSchema } from "@rjsf/utils"
|
||||
import validator from "@rjsf/validator-ajv6"
|
||||
import Form from "@rjsf/core"
|
||||
import CountryService from '../../services/country'
|
||||
import { Modal } from "flowbite-react"
|
||||
import Modal from '../base/Modal'
|
||||
import React from 'react'
|
||||
|
||||
const Addressdiv = NiceModal.create(
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
import * as PropTypes from "prop-types"
|
||||
import Button from "../base/Button"
|
||||
import AddressModal from "./AddressModal"
|
||||
import { useCallback } from 'react'
|
||||
import { useModal } from '@ebay/nice-modal-react'
|
||||
import { Address } from "../../model/address"
|
||||
import { Shop } from "../../model/shop"
|
||||
import React from 'react'
|
||||
import { Button as FlowbiteButton } from "flowbite-react"
|
||||
|
||||
const AddressModalComponent = ({handleAdd, handleEdit, address, shop}) => {
|
||||
const addressModal = useModal(AddressModal)
|
||||
|
||||
const showAddModal = useCallback(() => {
|
||||
addressModal.show({title: 'Add Address', action: 'Save', shop}).then((formData) => {
|
||||
addAddress(formData)
|
||||
})
|
||||
},[addressModal])
|
||||
|
||||
const showEditModal = useCallback((address) => {
|
||||
addressModal.show({ title: 'Edit Address', action: 'Save', address: address, shop }).then((formData) => {
|
||||
updateAddress(formData)
|
||||
})
|
||||
},[addressModal])
|
||||
|
||||
const updateAddress = (formData) => {
|
||||
address.parseFromFormData(formData)
|
||||
handleEdit(address)
|
||||
}
|
||||
|
||||
const addAddress = (formData) => {
|
||||
address = new Address()
|
||||
address.parseFromFormData(formData)
|
||||
handleAdd(address)
|
||||
}
|
||||
|
||||
return (
|
||||
<FlowbiteButton.Group>
|
||||
<Button type={1} variant="success" onClick={showAddModal} className="!rounded-r-none" />
|
||||
<Button type={2} variant="info" onClick={() => {
|
||||
showEditModal(address)
|
||||
}} className="!rounded-l-none" />
|
||||
</FlowbiteButton.Group>
|
||||
)
|
||||
}
|
||||
|
||||
AddressModalComponent.propTypes = {
|
||||
handleAdd: PropTypes.func,
|
||||
handleEdit: PropTypes.func,
|
||||
address: PropTypes.instanceOf(Address),
|
||||
shop: PropTypes.instanceOf(Shop)
|
||||
}
|
||||
|
||||
export default AddressModalComponent
|
||||
@ -1,108 +0,0 @@
|
||||
import * as PropTypes from 'prop-types'
|
||||
import { AsyncPaginate } from "react-select-async-paginate"
|
||||
import { Contact } from '../../model/contact'
|
||||
import { Address } from '../../model/address'
|
||||
import {container} from "tsyringe-neo"
|
||||
import {useEffect,useState} from 'react'
|
||||
import OrderState from "../../state/order"
|
||||
import AddressService from "../../services/address"
|
||||
import AddressModalComponent from "./AddressModalComponent"
|
||||
import { SelectLabel } from '../base/SelectLabel'
|
||||
import AddressDetail from './AddressDetail'
|
||||
import { Shop } from '../../model/shop'
|
||||
import React from 'react'
|
||||
|
||||
const AddressSelect = ({address, contact, type, changeAddress, name, shop}) => {
|
||||
|
||||
const [contactUuid, setContactUuid] = useState(null)
|
||||
|
||||
const [localAddress, setLocalAddress] = useState(null)
|
||||
|
||||
const address_service = new AddressService()
|
||||
|
||||
let orderState = container.resolve(OrderState)
|
||||
|
||||
useEffect(() => {
|
||||
if(contact.uuid != "") {
|
||||
setContactUuid(contact.uuid)
|
||||
}
|
||||
}, [contact.uuid])
|
||||
|
||||
useEffect(() => {
|
||||
setLocalAddress(address)
|
||||
}, [address])
|
||||
|
||||
const order: OrderState = container.resolve(OrderState)
|
||||
|
||||
const loadOptions = async (searchQuery, loadedOptions) => {
|
||||
|
||||
const data = await address_service.getContactAddress(contact.uuid, type)
|
||||
|
||||
return {
|
||||
options: data,
|
||||
hasMore: false,
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = (address: Address) => {
|
||||
address.type = type
|
||||
address.contact.uuid = contact.uuid
|
||||
address_service.create(address).then(() => {
|
||||
setLocalAddress(address)
|
||||
changeAddress(address, type)
|
||||
setContactUuid(Math.random())
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = (address: Address) => {
|
||||
address.type = type
|
||||
address.contact.uuid = contact.uuid
|
||||
address_service.update(address).then(() => {
|
||||
setLocalAddress(address)
|
||||
changeAddress(address, type)
|
||||
setContactUuid(Math.random())
|
||||
})
|
||||
}
|
||||
|
||||
const setAddressInOrder = (option) => {
|
||||
setLocalAddress(option)
|
||||
changeAddress(option, type)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex gap-1'>
|
||||
<div className='flex-1'>
|
||||
<AsyncPaginate
|
||||
defaultOptions
|
||||
defaultValue={address}
|
||||
key={JSON.stringify(contactUuid)}
|
||||
loadOptions={loadOptions}
|
||||
getOptionValue={(option) => option.uuid}
|
||||
getOptionLabel={(option) => option.firstname + ' ' + option.lastname}
|
||||
onChange={setAddressInOrder}
|
||||
isSearchable={false}
|
||||
name={name}
|
||||
placeholder=''
|
||||
components={{ Control: SelectLabel }}
|
||||
/>
|
||||
|
||||
{ localAddress && <AddressDetail address={localAddress} />}
|
||||
</div>
|
||||
<div >
|
||||
<AddressModalComponent address={localAddress} handleAdd={handleAdd} handleEdit={handleEdit} shop={shop}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
AddressSelect.propTypes = {
|
||||
address: PropTypes.instanceOf(Address),
|
||||
contact: PropTypes.instanceOf(Contact),
|
||||
shop: PropTypes.instanceOf(Shop),
|
||||
type: PropTypes.number,
|
||||
changeAddress: PropTypes.func,
|
||||
name: PropTypes.string
|
||||
}
|
||||
|
||||
export default AddressSelect
|
||||
@ -1,100 +1,206 @@
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import * as PropTypes from 'prop-types'
|
||||
import ContactSelect from './ContactSelect'
|
||||
import AddressSelect from './AddressSelect'
|
||||
import { useModal } from '@ebay/nice-modal-react'
|
||||
import { Contact } from '../../model/contact'
|
||||
import { Address } from '../../model/address'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { container } from 'tsyringe-neo'
|
||||
import OrderState from '../../state/order'
|
||||
import { container } from "tsyringe-neo"
|
||||
import AccountSelectComponent from '../account/AccountSelectComponent'
|
||||
import AddressService from '../../services/address'
|
||||
import Order from '../../model/order'
|
||||
import { Shop } from '../../model/shop'
|
||||
import Box from '../base/Box'
|
||||
import AddressCard from './AddressCard'
|
||||
import ContactPickerModal from './ContactPickerModal'
|
||||
import { useContactModal } from './useContactModal'
|
||||
import { useAddressModal } from './useAddressModal'
|
||||
|
||||
const initials = (c: Contact) => {
|
||||
const fn = c.layouter_data?.firstname?.charAt(0) || ''
|
||||
const ln = c.layouter_data?.lastname?.charAt(0) || ''
|
||||
const i = (fn + ln).toUpperCase()
|
||||
return i || (c.username || c.email || '?').slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
const ContactComponent = ({ order, shop }) => {
|
||||
|
||||
let orderState = container.resolve(OrderState)
|
||||
const orderState = container.resolve(OrderState)
|
||||
|
||||
const [contact, setContact] = useState<Contact>(new Contact())
|
||||
const [, setRerender] = useState(0)
|
||||
|
||||
const changeAddress = (address: Address, type: Number) => {
|
||||
if (type == 1) {
|
||||
orderState.getCurrentOrder().value.invoiceAddress = address
|
||||
order.invoiceAddress = address
|
||||
const [invoiceAddress, setInvoiceAddress] = useState<Address | null>(order.invoiceAddress?.uuid ? order.invoiceAddress : null)
|
||||
const [deliveryAddress, setDeliveryAddress] = useState<Address | null>(order.deliveryAddress?.uuid ? order.deliveryAddress : null)
|
||||
const [senderAddress, setSenderAddress] = useState<Address | null>(order.senderAddress?.uuid ? order.senderAddress : null)
|
||||
|
||||
const contactSelected = !!order.contact?.uuid
|
||||
|
||||
const applyContact = (contact: Contact, resetAddresses: boolean) => {
|
||||
const cur = orderState.getCurrentOrder().value
|
||||
cur.contact = contact
|
||||
order.contact = contact
|
||||
if (resetAddresses) {
|
||||
cur.invoiceAddress = new Address()
|
||||
cur.deliveryAddress = new Address()
|
||||
cur.senderAddress = new Address()
|
||||
order.invoiceAddress = cur.invoiceAddress
|
||||
order.deliveryAddress = cur.deliveryAddress
|
||||
order.senderAddress = cur.senderAddress
|
||||
setInvoiceAddress(null)
|
||||
setDeliveryAddress(null)
|
||||
setSenderAddress(null)
|
||||
}
|
||||
if (type == 2) {
|
||||
orderState.getCurrentOrder().value.deliveryAddress = address
|
||||
order.deliveryAddress = address
|
||||
orderState.setCurrentOrder(cur)
|
||||
setRerender((n) => n + 1)
|
||||
}
|
||||
if (type == 3) {
|
||||
orderState.getCurrentOrder().value.senderAddress = address
|
||||
order.senderAddress = address
|
||||
|
||||
const pickerModal = useModal(ContactPickerModal)
|
||||
|
||||
const openPicker = () => {
|
||||
pickerModal.show({ shop, current: contactSelected ? order.contact : undefined }).then((contact: Contact) => {
|
||||
if (contact && contact.uuid) {
|
||||
applyContact(contact, contact.uuid !== order.contact?.uuid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const { showAdd: showAddContact, showEdit: showEditContact } = useContactModal(
|
||||
shop,
|
||||
(contact) => applyContact(contact, true),
|
||||
(contact) => applyContact(contact, false),
|
||||
)
|
||||
|
||||
const changeAddress = (address: Address, type: number) => {
|
||||
const cur = orderState.getCurrentOrder().value
|
||||
if (type === 1) { cur.invoiceAddress = address; order.invoiceAddress = address; setInvoiceAddress(address) }
|
||||
if (type === 2) { cur.deliveryAddress = address; order.deliveryAddress = address; setDeliveryAddress(address) }
|
||||
if (type === 3) { cur.senderAddress = address; order.senderAddress = address; setSenderAddress(address) }
|
||||
orderState.setCurrentOrder(cur)
|
||||
}
|
||||
|
||||
const { showAdd: showAddAddress } = useAddressModal(
|
||||
shop,
|
||||
(addr) => {
|
||||
addr.type = 1
|
||||
addr.contact.uuid = order.contact.uuid
|
||||
new AddressService().create(addr).then(() => changeAddress(addr, 1))
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 animate-fade-in border-2 border-orange-200 dark:border-orange-900">
|
||||
<h2 className="text-lg font-semibold mb-3 text-gray-800 dark:text-gray-100 flex items-center gap-2">
|
||||
<svg className="w-6 h-6 text-[#EA641B]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
Kontakt & Konto
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<ContactSelect
|
||||
order={order}
|
||||
shop={shop}
|
||||
onChange={setContact}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{order.contact.uuid != "" && (
|
||||
<>
|
||||
<h3 className="text-base font-medium mb-2 text-gray-700 dark:text-gray-200 flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<Box
|
||||
title="Kontakt"
|
||||
subtitle="Kunde und Accountdaten."
|
||||
icon={
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20 21a8 8 0 1 0-16 0M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z" />
|
||||
</svg>
|
||||
Adressen
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<AddressSelect
|
||||
address={order.invoiceAddress}
|
||||
contact={order.contact}
|
||||
changeAddress={changeAddress}
|
||||
name='Rechnungsadresse'
|
||||
type={1}
|
||||
shop={shop}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{contactSelected ? (
|
||||
<div className="rounded-sm border border-psc-border bg-slate-50 p-4">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="grid h-11 w-11 shrink-0 place-items-center rounded-sm border border-orange-200 bg-orange-50 text-sm font-semibold text-psc-orange">
|
||||
{initials(order.contact)}
|
||||
</div>
|
||||
<div>
|
||||
<AddressSelect
|
||||
address={order.deliveryAddress}
|
||||
contact={order.contact}
|
||||
changeAddress={changeAddress}
|
||||
name='Lieferadresse'
|
||||
type={2}
|
||||
shop={shop}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<AddressSelect
|
||||
address={order.senderAddress}
|
||||
changeAddress={changeAddress}
|
||||
contact={order.contact}
|
||||
name='Absenderadresse'
|
||||
type={3}
|
||||
shop={shop}
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<strong className="block truncate text-sm font-medium text-psc-text">
|
||||
{order.contact.username || order.contact.email}
|
||||
</strong>
|
||||
<span className="text-sm text-psc-muted">Kunde ist ausgewählt</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openPicker}
|
||||
className="h-10 rounded-sm border border-psc-border bg-white px-4 text-sm font-medium text-psc-orange hover:bg-orange-50"
|
||||
>
|
||||
Ändern
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showEditContact(order.contact)}
|
||||
className="h-10 rounded-sm border border-psc-border bg-white px-4 text-sm font-medium text-psc-text hover:bg-slate-50"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-sm border border-dashed border-psc-border bg-slate-50 p-5 text-center">
|
||||
<p className="text-sm text-psc-muted">Noch kein Kunde ausgewählt.</p>
|
||||
<div className="mt-4 flex flex-wrap items-center justify-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={openPicker}
|
||||
className="h-10 rounded-sm border border-psc-orange bg-psc-orange px-4 text-sm font-medium text-white transition hover:bg-psc-orange-dark"
|
||||
>
|
||||
Kunde zuweisen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={showAddContact}
|
||||
className="text-sm font-medium text-psc-orange hover:underline"
|
||||
>
|
||||
+ Kontakt anlegen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{contactSelected && (
|
||||
<Box
|
||||
title="Adressdaten"
|
||||
subtitle="Rechnung, Lieferung und Absender."
|
||||
icon={
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21s7-5.2 7-12A7 7 0 1 0 5 9c0 6.8 7 12 7 12zM12 11.5A2.5 2.5 0 1 0 12 6a2.5 2.5 0 0 0 0 5.5z" />
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-3">
|
||||
<AddressCard
|
||||
title="Rechnungsadresse"
|
||||
emptyHint="Noch keine Adresse für die Rechnung hinterlegt."
|
||||
type={1}
|
||||
contact={order.contact}
|
||||
shop={shop}
|
||||
value={invoiceAddress}
|
||||
onChange={changeAddress}
|
||||
/>
|
||||
<AddressCard
|
||||
title="Lieferadresse"
|
||||
emptyHint="Noch keine Adresse für die Lieferung hinterlegt."
|
||||
type={2}
|
||||
contact={order.contact}
|
||||
shop={shop}
|
||||
value={deliveryAddress}
|
||||
onChange={changeAddress}
|
||||
/>
|
||||
<AddressCard
|
||||
title="Absenderadresse"
|
||||
emptyHint="Noch keine Absenderadresse hinterlegt."
|
||||
type={3}
|
||||
contact={order.contact}
|
||||
shop={shop}
|
||||
value={senderAddress}
|
||||
onChange={changeAddress}
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={showAddAddress}
|
||||
className="mt-1 h-10 rounded-sm border border-psc-border bg-white px-4 text-sm font-medium text-psc-orange hover:bg-orange-50"
|
||||
>
|
||||
+ Neue Adresse anlegen
|
||||
</button>
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -103,5 +209,4 @@ ContactComponent.propTypes = {
|
||||
shop: PropTypes.instanceOf(Shop),
|
||||
}
|
||||
|
||||
|
||||
export default ContactComponent
|
||||
|
||||
@ -6,7 +6,7 @@ import { Contact } from "../../model/contact";
|
||||
import validator from "@rjsf/validator-ajv6";
|
||||
import Form from "@rjsf/core";
|
||||
import CountryService from "../../services/country";
|
||||
import { Modal } from "flowbite-react";
|
||||
import Modal from '../base/Modal';
|
||||
import React from 'react'
|
||||
|
||||
const ContactModal = NiceModal.create(
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
import {Contact} from "../../model/contact"
|
||||
import * as PropTypes from "prop-types"
|
||||
import {Shop} from "../../model/shop"
|
||||
import Button from "../base/Button"
|
||||
import ContactModal from "./ContactModal"
|
||||
import { useCallback } from 'react'
|
||||
import { useModal } from '@ebay/nice-modal-react'
|
||||
import ContactService from "../../services/contact"
|
||||
import { Address } from "../../model/address"
|
||||
import AddressService from "../../services/address"
|
||||
import React from 'react'
|
||||
import { Button as FlowbiteButton } from "flowbite-react"
|
||||
|
||||
const ContactModalComponent = ({shop, handleAdd, handleEdit, contact}) => {
|
||||
|
||||
|
||||
const contactModal = useModal(ContactModal)
|
||||
|
||||
const showAddModal = useCallback(() => {
|
||||
contactModal.show({title: 'Add Contact', action: 'Save', shop: shop, contact: new Contact()}).then((formData) => {
|
||||
addContact(formData)
|
||||
})
|
||||
},[contactModal])
|
||||
|
||||
const showEditModal = useCallback((contact) => {
|
||||
contact = contact
|
||||
contactModal.show({ title: 'Edit Contact', action: 'Save', contact: contact, shop: shop }).then((formData) => {
|
||||
contact.parseFromFormData(formData)
|
||||
updateContact(contact)
|
||||
})
|
||||
},[contactModal])
|
||||
|
||||
const updateContact = (contact) => {
|
||||
const contact_api = new ContactService()
|
||||
contact_api.updateContact(contact).then((e: any) => {
|
||||
handleEdit(contact)
|
||||
})
|
||||
}
|
||||
|
||||
const addContact = (formData) => {
|
||||
var contact = new Contact()
|
||||
contact.parseFromFormData(formData, shop)
|
||||
const contact_api = new ContactService()
|
||||
contact_api.createContact(contact).then((e: any) => {
|
||||
contact.parseFromJson(e)
|
||||
var address = new Address()
|
||||
address.parseFromFormData(formData)
|
||||
address.contact.uuid = contact.uuid
|
||||
address.type = 1
|
||||
const address_api = new AddressService()
|
||||
address_api.create(address)
|
||||
address.type = 2
|
||||
address_api.create(address)
|
||||
|
||||
handleAdd(contact)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<FlowbiteButton.Group>
|
||||
<Button type={1} variant="success" onClick={showAddModal} className="!rounded-r-none" />
|
||||
<Button type={2} variant="info" onClick={() => {
|
||||
showEditModal(contact)
|
||||
}} className="!rounded-l-none" />
|
||||
</FlowbiteButton.Group>
|
||||
)
|
||||
}
|
||||
|
||||
ContactModalComponent.propTypes = {
|
||||
shop: PropTypes.instanceOf(Shop),
|
||||
handleAdd: PropTypes.func,
|
||||
handleEdit: PropTypes.func,
|
||||
contact: PropTypes.instanceOf(Contact)
|
||||
}
|
||||
|
||||
export default ContactModalComponent
|
||||
@ -0,0 +1,135 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import NiceModal, { useModal } from '@ebay/nice-modal-react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import Modal from '../base/Modal'
|
||||
import Button from '../base/Button'
|
||||
import Spinner from '../base/Spinner'
|
||||
import { Contact } from '../../model/contact'
|
||||
import ContactService from '../../services/contact'
|
||||
|
||||
const fullName = (c: Contact) =>
|
||||
`${c.layouter_data?.firstname ?? ''} ${c.layouter_data?.lastname ?? ''}`.trim()
|
||||
|
||||
const ContactPickerModal = NiceModal.create(({ shop, current }: { shop: any; current?: Contact }) => {
|
||||
const modal = useModal()
|
||||
|
||||
const [term, setTerm] = useState('')
|
||||
const [debouncedTerm] = useDebounce(term, 300)
|
||||
const [contacts, setContacts] = useState<Contact[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [selected, setSelected] = useState<Contact | null>(current?.uuid ? current : null)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
setLoading(true)
|
||||
const api = new ContactService()
|
||||
api
|
||||
.getContacts(debouncedTerm, 1, shop)
|
||||
.then((data) => {
|
||||
if (active) setContacts(data)
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) setLoading(false)
|
||||
})
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [debouncedTerm])
|
||||
|
||||
const apply = () => {
|
||||
if (!selected) return
|
||||
modal.resolve(selected)
|
||||
modal.remove()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal show={modal.visible} onClose={() => modal.remove()} size="80">
|
||||
<Modal.Header>Kunde auswählen</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="relative mb-4">
|
||||
<svg className="pointer-events-none absolute left-3 top-1/2 h-5 w-5 -translate-y-1/2 text-psc-muted" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m21 21-4.35-4.35M17 11a6 6 0 1 1-12 0 6 6 0 0 1 12 0Z" />
|
||||
</svg>
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={term}
|
||||
onChange={(e) => setTerm(e.target.value)}
|
||||
placeholder="Nach Name, Firma oder E-Mail suchen…"
|
||||
className="h-11 w-full rounded-sm border border-psc-border bg-white pl-10 pr-4 text-sm text-psc-text outline-none focus:border-psc-orange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[55vh] overflow-y-auto rounded-sm border border-psc-border">
|
||||
<table className="w-full border-collapse">
|
||||
<thead className="sticky top-0 bg-slate-50">
|
||||
<tr className="border-b border-psc-border">
|
||||
<th className="w-10 px-4 py-3"></th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-psc-muted">Name</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-psc-muted">Firma</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-psc-muted">E-Mail</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-psc-muted">Straße</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-psc-muted">Ort</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-psc-border bg-white">
|
||||
{contacts.map((c) => {
|
||||
const isSel = selected?.uuid === c.uuid
|
||||
return (
|
||||
<tr
|
||||
key={c.uuid}
|
||||
onClick={() => setSelected(c)}
|
||||
className={`cursor-pointer transition-colors ${isSel ? 'bg-orange-50' : 'hover:bg-slate-50'}`}
|
||||
>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`grid h-5 w-5 place-items-center rounded-full border ${isSel ? 'border-psc-orange bg-psc-orange text-white' : 'border-psc-border'}`}>
|
||||
{isSel && (
|
||||
<svg className="h-3 w-3" fill="none" stroke="currentColor" strokeWidth={3} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m5 13 4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm font-medium text-psc-text">{fullName(c) || '—'}</td>
|
||||
<td className="px-4 py-3 text-sm text-psc-muted">{c.layouter_data?.company || '—'}</td>
|
||||
<td className="px-4 py-3 text-sm text-psc-muted">{c.username || c.email}</td>
|
||||
<td className="px-4 py-3 text-sm text-psc-muted">
|
||||
{[c.layouter_data?.street, c.layouter_data?.houseNumber].filter(Boolean).join(' ') || '—'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-psc-muted">
|
||||
{[c.layouter_data?.zip, c.layouter_data?.city].filter(Boolean).join(' ') || '—'}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{!loading && contacts.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-4 py-10 text-center text-sm text-psc-muted">
|
||||
Keine Kunden gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{loading && (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-4 py-10 text-center">
|
||||
<Spinner />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button color="light" onClick={() => modal.remove()}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button color="success" disabled={!selected} onClick={apply}>
|
||||
Übernehmen
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)
|
||||
})
|
||||
|
||||
export default ContactPickerModal
|
||||
@ -1,89 +0,0 @@
|
||||
import * as PropTypes from 'prop-types'
|
||||
import { AsyncPaginate } from "react-select-async-paginate"
|
||||
import { Contact } from '../../model/contact'
|
||||
import ContactService from '../../services/contact'
|
||||
import { Shop } from '../../model/shop'
|
||||
import {useEffect, useState} from 'react'
|
||||
import { container } from 'tsyringe-neo'
|
||||
import OrderState from '../../state/order'
|
||||
import ContactModalComponent from "./ContactModalComponent"
|
||||
import Order from '../../model/order'
|
||||
import { SelectLabel } from '../base/SelectLabel'
|
||||
import React from 'react'
|
||||
|
||||
const ContactSelect = ({order, shop, onChange}) => {
|
||||
|
||||
const [shopUuid, setShopUuid] = useState(null);
|
||||
|
||||
const orderState: OrderState = container.resolve(OrderState);
|
||||
const contact_api = new ContactService();
|
||||
|
||||
useEffect(() => {
|
||||
setShopUuid(shop.uuid);
|
||||
}, [shop]);
|
||||
|
||||
const loadOptions = async (searchQuery, loadedOptions, { page }) => {
|
||||
|
||||
let contacts = await contact_api.getContacts(searchQuery, page, shop);
|
||||
|
||||
return {
|
||||
options: contacts,
|
||||
hasMore: false,
|
||||
additional: {
|
||||
page: page + 1,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const setContactInOrder = (option) => {
|
||||
orderState.getCurrentOrder().value.contact = option;
|
||||
order.contact = option;
|
||||
onChange(option);
|
||||
};
|
||||
|
||||
const handleAdd = (contact: Contact) => {
|
||||
setShopUuid(Math.random());
|
||||
setContactInOrder(contact);
|
||||
}
|
||||
|
||||
const handleEdit = (contact: Contact) => {
|
||||
setShopUuid(Math.random());
|
||||
setContactInOrder(contact);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
<div className="flex-1">
|
||||
<AsyncPaginate
|
||||
defaultOptions
|
||||
className={`${orderState.getCurrentOrder().value.contact.username == "" ? "border border-danger" : ""}`}
|
||||
key={JSON.stringify(shopUuid)}
|
||||
defaultValue={order.contact}
|
||||
components={{Control: SelectLabel}}
|
||||
placeholder=''
|
||||
name='Kunde'
|
||||
value={order.contact}
|
||||
loadOptions={loadOptions}
|
||||
getOptionValue={(option) => option.uuid}
|
||||
getOptionLabel={(option) => option.username}
|
||||
onChange={setContactInOrder}
|
||||
isSearchable={true}
|
||||
additional={{
|
||||
page: 1,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<ContactModalComponent shop={shop} contact={order.contact} handleAdd={handleAdd} handleEdit={handleEdit}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ContactSelect.propTypes = {
|
||||
order: PropTypes.instanceOf(Order),
|
||||
onChange: PropTypes.func,
|
||||
shop: PropTypes.instanceOf(Shop),
|
||||
};
|
||||
|
||||
export default ContactSelect;
|
||||
@ -0,0 +1,38 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useModal } from '@ebay/nice-modal-react'
|
||||
import AddressModal from './AddressModal'
|
||||
import { Address } from '../../model/address'
|
||||
|
||||
/**
|
||||
* Kapselt das Anlegen/Bearbeiten einer Adresse über das AddressModal.
|
||||
* Liefert die geparste Adresse zurück; das Persistieren übernimmt der Aufrufer.
|
||||
*/
|
||||
export function useAddressModal(
|
||||
shop: any,
|
||||
onAdded: (address: Address) => void,
|
||||
onEdited: (address: Address) => void,
|
||||
) {
|
||||
const addressModal = useModal(AddressModal)
|
||||
|
||||
const showAdd = useCallback(() => {
|
||||
addressModal.show({ title: 'Adresse anlegen', action: 'Save', shop }).then((formData: any) => {
|
||||
const address = new Address()
|
||||
address.parseFromFormData(formData)
|
||||
onAdded(address)
|
||||
})
|
||||
}, [addressModal, shop, onAdded])
|
||||
|
||||
const showEdit = useCallback(
|
||||
(address: Address) => {
|
||||
addressModal
|
||||
.show({ title: 'Adresse bearbeiten', action: 'Save', address, shop })
|
||||
.then((formData: any) => {
|
||||
address.parseFromFormData(formData)
|
||||
onEdited(address)
|
||||
})
|
||||
},
|
||||
[addressModal, shop, onEdited],
|
||||
)
|
||||
|
||||
return { showAdd, showEdit }
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useModal } from '@ebay/nice-modal-react'
|
||||
import ContactModal from './ContactModal'
|
||||
import { Contact } from '../../model/contact'
|
||||
import { Address } from '../../model/address'
|
||||
import ContactService from '../../services/contact'
|
||||
import AddressService from '../../services/address'
|
||||
|
||||
/**
|
||||
* Kapselt das Anlegen/Bearbeiten eines Kontakts über das ContactModal.
|
||||
* (Service-Logik zuvor in ContactModalComponent.)
|
||||
*/
|
||||
export function useContactModal(
|
||||
shop: any,
|
||||
onAdded: (contact: Contact) => void,
|
||||
onEdited: (contact: Contact) => void,
|
||||
) {
|
||||
const contactModal = useModal(ContactModal)
|
||||
|
||||
const showAdd = useCallback(() => {
|
||||
contactModal
|
||||
.show({ title: 'Kontakt anlegen', action: 'Save', shop, contact: new Contact() })
|
||||
.then((formData: any) => {
|
||||
const contact = new Contact()
|
||||
contact.parseFromFormData(formData, shop)
|
||||
const contact_api = new ContactService()
|
||||
contact_api.createContact(contact).then((e: any) => {
|
||||
contact.parseFromJson(e)
|
||||
const address = new Address()
|
||||
address.parseFromFormData(formData)
|
||||
address.contact.uuid = contact.uuid
|
||||
address.type = 1
|
||||
const address_api = new AddressService()
|
||||
address_api.create(address)
|
||||
address.type = 2
|
||||
address_api.create(address)
|
||||
onAdded(contact)
|
||||
})
|
||||
})
|
||||
}, [contactModal, shop, onAdded])
|
||||
|
||||
const showEdit = useCallback(
|
||||
(contact: Contact) => {
|
||||
contactModal
|
||||
.show({ title: 'Kontakt bearbeiten', action: 'Save', contact, shop })
|
||||
.then((formData: any) => {
|
||||
contact.parseFromFormData(formData)
|
||||
const contact_api = new ContactService()
|
||||
contact_api.updateContact(contact).then(() => onEdited(contact))
|
||||
})
|
||||
},
|
||||
[contactModal, shop, onEdited],
|
||||
)
|
||||
|
||||
return { showAdd, showEdit }
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import { container } from 'tsyringe-neo'
|
||||
import OrderState from'../../state/order'
|
||||
import { SelectLabel } from '../base/SelectLabel'
|
||||
import React from 'react'
|
||||
|
||||
const DraftComponent = ({order}) => {
|
||||
@ -24,8 +23,8 @@ const DraftComponent = ({order}) => {
|
||||
<div className="gap-2">
|
||||
<label className="inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" value="1" className="sr-only peer" defaultChecked={draft} onChange={(e) => onChange(e)}/>
|
||||
<div className="relative w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 dark:bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
<span className="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">Entwurf</span>
|
||||
<div className="relative w-11 h-6 bg-slate-200 rounded-full peer peer-focus:ring-2 peer-focus:ring-orange-200 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-psc-orange"></div>
|
||||
<span className="ms-3 text-sm font-medium text-psc-text">Entwurf</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -7,6 +7,7 @@ import OrderState from "../../state/order"
|
||||
import Order from "../../model/order"
|
||||
import {useEffect, useState} from "react"
|
||||
import React from 'react'
|
||||
import Box from "../base/Box"
|
||||
|
||||
const InfoFieldComponent = ({shop, order}) => {
|
||||
|
||||
@ -35,16 +36,18 @@ const InfoFieldComponent = ({shop, order}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 animate-fade-in border-2 border-orange-200 dark:border-orange-900">
|
||||
<h2 className="text-lg font-semibold mb-3 text-gray-800 dark:text-gray-100 flex items-center gap-2">
|
||||
<svg className="w-6 h-6 text-[#EA641B]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<Box
|
||||
title="Zusatzinformationen"
|
||||
subtitle="Hinweise und Zusatzfelder."
|
||||
icon={
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Zusatzinformationen
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label className="mb-2 block text-sm font-medium text-psc-text">
|
||||
{shop.basketField1 || "Info 1"}
|
||||
</label>
|
||||
<input
|
||||
@ -52,12 +55,12 @@ const InfoFieldComponent = ({shop, order}) => {
|
||||
name="basketField1"
|
||||
value={basketField1}
|
||||
onChange={e => changeBasketField1(e.target.value)}
|
||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-[#EA641B] focus:border-[#EA641B] block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-[#EA641B] dark:focus:border-[#EA641B]"
|
||||
className="block w-full rounded-sm border border-psc-border bg-white p-2.5 text-sm text-psc-text outline-none focus:border-psc-orange"
|
||||
placeholder={shop.basketField1 || "Info 1"}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
<label className="mb-2 block text-sm font-medium text-psc-text">
|
||||
{shop.basketField2 || "Info 2"}
|
||||
</label>
|
||||
<input
|
||||
@ -65,12 +68,12 @@ const InfoFieldComponent = ({shop, order}) => {
|
||||
name="basketField2"
|
||||
value={basketField2}
|
||||
onChange={e => changeBasketField2(e.target.value)}
|
||||
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-[#EA641B] focus:border-[#EA641B] block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-[#EA641B] dark:focus:border-[#EA641B]"
|
||||
className="block w-full rounded-sm border border-psc-border bg-white p-2.5 text-sm text-psc-text outline-none focus:border-psc-orange"
|
||||
placeholder={shop.basketField2 || "Info 2"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import OrderState from "../../state/order"
|
||||
import Order from "../../model/order"
|
||||
import { useEffect, useState } from "react"
|
||||
import Select from 'react-select'
|
||||
import { SelectLabel } from '../base/SelectLabel'
|
||||
import Box from '../base/Box'
|
||||
|
||||
const PaymentComponent = ({ shop, order }) => {
|
||||
|
||||
@ -44,17 +44,18 @@ const PaymentComponent = ({ shop, order }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 animate-fade-in border-2 border-orange-200 dark:border-orange-900">
|
||||
<h2 className="text-lg font-semibold mb-3 text-gray-800 dark:text-gray-100 flex items-center gap-2">
|
||||
<svg className="w-6 h-6 text-[#EA641B]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
<Box
|
||||
title="Zahlart"
|
||||
subtitle="Zahlungsbedingung"
|
||||
icon={
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
</svg>
|
||||
Zahlart
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<AsyncPaginate
|
||||
defaultOptions
|
||||
key={JSON.stringify(shopUuid)}
|
||||
components={{ Control: SelectLabel }}
|
||||
defaultValue={order.payment}
|
||||
loadOptions={loadOptions}
|
||||
placeholder=''
|
||||
@ -63,9 +64,9 @@ const PaymentComponent = ({ shop, order }) => {
|
||||
getOptionLabel={(option) => option.title}
|
||||
onChange={onChange}
|
||||
isSearchable={false}
|
||||
className={`${payment == 0 ? "border-2 border-red-500 rounded" : ""}`}
|
||||
className={`${payment == 0 ? "border border-red-400 rounded-sm" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -9,8 +9,7 @@ import ProductSelect from "../product/ProductSelect"
|
||||
import ProductForm from "../product/ProductForm"
|
||||
import Order from "../../model/order"
|
||||
import { Pos } from '../../model/pos'
|
||||
import Button from '../base/Button'
|
||||
import { Modal } from "flowbite-react";
|
||||
import Modal from '../base/Modal';
|
||||
|
||||
const AddPositionComponent = (props) => {
|
||||
|
||||
@ -33,10 +32,16 @@ const AddPositionComponent = (props) => {
|
||||
pos.product = product;
|
||||
|
||||
return (
|
||||
<div className='text-center'>
|
||||
<Button type={1} onClick={handleShow} variant="success"/>
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleShow}
|
||||
className="inline-flex h-10 shrink-0 items-center gap-1 rounded-sm border border-psc-border bg-white px-4 text-sm font-medium text-psc-orange transition hover:bg-orange-50"
|
||||
>
|
||||
+ Position hinzufügen
|
||||
</button>
|
||||
|
||||
<Modal size="7xl" show={show} onClose={() => handleClose()}
|
||||
<Modal size="80" show={show} onClose={() => handleClose()}
|
||||
>
|
||||
<Modal.Header>
|
||||
add position
|
||||
@ -69,7 +74,7 @@ const AddPositionComponent = (props) => {
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import ProductSelect from "../product/ProductSelect"
|
||||
import { Pos } from '../../model/pos'
|
||||
import Button from '../base/Button'
|
||||
import ProductForm from '../product/ProductForm'
|
||||
import { Modal } from "flowbite-react";
|
||||
import Modal from '../base/Modal';
|
||||
import React from 'react'
|
||||
|
||||
const EditPositionComponent = ({position, shop, changePos}) => {
|
||||
@ -22,7 +22,7 @@ const EditPositionComponent = ({position, shop, changePos}) => {
|
||||
<div>
|
||||
<Button type={2} variant="gray" onClick={handleShow} className="!rounded-r-none" />
|
||||
|
||||
<Modal size="7xl" show={show} onClose={() => handleClose()}>
|
||||
<Modal size="80" show={show} onClose={() => handleClose()}>
|
||||
<Modal.Header className="text-mauve12 m-0 text-[17px] font-medium">
|
||||
edit position
|
||||
</Modal.Header>
|
||||
|
||||
@ -9,21 +9,21 @@ const ItemsComponent = ({positions, delPos, shop, changePos}) => {
|
||||
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-lg border border-orange-100 dark:border-orange-900">
|
||||
<div className="overflow-x-auto rounded-sm border border-psc-border">
|
||||
<table className="w-full border-collapse">
|
||||
<thead className="bg-gradient-to-r from-orange-50 to-red-50 dark:from-orange-950 dark:to-red-950">
|
||||
<tr className="border-b-2 border-orange-200 dark:border-orange-800">
|
||||
<th className="text-left py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">Pos</th>
|
||||
<th className="text-left py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">Titel</th>
|
||||
<th className="text-left py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">Anzahl</th>
|
||||
<th className="text-right py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">Netto</th>
|
||||
<th className="text-right py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">MwSt</th>
|
||||
<th className="text-right py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">Brutto</th>
|
||||
<th className="text-right py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300">Druckdaten</th>
|
||||
<th className="py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300"></th>
|
||||
<thead className="bg-slate-50">
|
||||
<tr className="border-b border-psc-border">
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-psc-muted">Pos</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-psc-muted">Titel</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-psc-muted">Anzahl</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-psc-muted">Netto</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-psc-muted">MwSt</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-psc-muted">Brutto</th>
|
||||
<th className="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-psc-muted">Druckdaten</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-psc-muted"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-orange-100 dark:divide-orange-900 bg-white dark:bg-gray-800">
|
||||
<tbody className="divide-y divide-psc-border bg-white">
|
||||
{positions.map((object, i) => (
|
||||
<PosComponent
|
||||
pos={object}
|
||||
@ -38,12 +38,12 @@ const ItemsComponent = ({positions, delPos, shop, changePos}) => {
|
||||
</table>
|
||||
|
||||
{positions.length === 0 && (
|
||||
<div className="text-center py-12 bg-gradient-to-br from-orange-50/50 to-red-50/50 dark:from-orange-950/30 dark:to-red-950/30">
|
||||
<svg className="w-20 h-20 mx-auto mb-4 text-orange-300 dark:text-orange-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div className="bg-slate-50 py-12 text-center">
|
||||
<svg className="mx-auto mb-4 h-20 w-20 text-orange-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<p className="text-lg font-medium text-[#EA641B]">Keine Positionen vorhanden</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">Fügen Sie Ihre erste Position hinzu</p>
|
||||
<p className="text-lg font-medium text-psc-orange">Keine Positionen vorhanden</p>
|
||||
<p className="mt-2 text-sm text-psc-muted">Fügen Sie Ihre erste Position hinzu</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import * as PropTypes from "prop-types"
|
||||
import {Pos} from "../../model/pos"
|
||||
import Button from '../base/Button'
|
||||
import Button, { ButtonGroup } from '../base/Button'
|
||||
import EditPositionComponent from './EditPositionComponent'
|
||||
import { Shop } from '../../model/shop'
|
||||
import Currency from '../base/Currency'
|
||||
import { Button as FlowbiteButton, Spinner } from "flowbite-react"
|
||||
import Spinner from '../base/Spinner'
|
||||
import ProductService from '../../services/product'
|
||||
import {UploadOption} from '../../model/uploadOption'
|
||||
import UploadOptionModal from './UploadOptionModal'
|
||||
@ -33,28 +33,28 @@ const PosComponent = ({index, pos, delPos, changePos, shop}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className="hover:bg-orange-50/50 dark:hover:bg-orange-950/30 transition-all duration-150">
|
||||
<td className="py-4 px-4">
|
||||
<span className="inline-flex items-center justify-center w-7 h-7 rounded-full bg-orange-100 dark:bg-orange-900 text-[#EA641B] dark:text-orange-300 text-xs font-bold">
|
||||
<tr className="transition-colors hover:bg-orange-50/50">
|
||||
<td className="px-4 py-4">
|
||||
<span className="inline-flex h-7 w-7 items-center justify-center rounded-sm border border-orange-200 bg-orange-50 text-xs font-medium text-psc-orange">
|
||||
{index + 1}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-4 text-sm text-gray-900 dark:text-gray-100 font-medium">{pos.product.title}</td>
|
||||
<td className="py-4 px-4">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-[#EA641B] dark:bg-orange-900 dark:text-orange-300">
|
||||
<td className="px-4 py-4 text-sm font-medium text-psc-text">{pos.product.title}</td>
|
||||
<td className="px-4 py-4">
|
||||
<span className="inline-flex items-center rounded-sm border border-sky-200 bg-sky-50 px-2.5 py-0.5 text-xs font-medium text-sky-600">
|
||||
{pos.count}x
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-4 text-sm text-right text-gray-700 dark:text-gray-300">
|
||||
<td className="px-4 py-4 text-right text-sm text-psc-text">
|
||||
<Currency price={pos.price.allNet} />
|
||||
</td>
|
||||
<td className="py-4 px-4 text-sm text-right text-gray-600 dark:text-gray-400">
|
||||
<td className="px-4 py-4 text-right text-sm text-psc-muted">
|
||||
<Currency price={pos.price.allVat} />
|
||||
</td>
|
||||
<td className="py-4 px-4 text-sm text-right font-semibold text-[#EA641B] dark:text-orange-300">
|
||||
<td className="px-4 py-4 text-right text-sm font-semibold text-psc-orange">
|
||||
<Currency price={pos.price.allGross} />
|
||||
</td>
|
||||
<td className="py-4 px-4 text-sm text-right">
|
||||
<td className="px-4 py-4 text-right text-sm">
|
||||
{loadingOption ? (
|
||||
<div className="flex justify-end">
|
||||
<Spinner size="sm" />
|
||||
@ -65,10 +65,10 @@ const PosComponent = ({index, pos, delPos, changePos, shop}) => {
|
||||
</td>
|
||||
<td className="py-4 px-4 text-sm text-right">
|
||||
<div className="flex justify-end">
|
||||
<FlowbiteButton.Group>
|
||||
<ButtonGroup>
|
||||
<EditPositionComponent shop={shop} position={pos} changePos={changePos} />
|
||||
<Button type={5} variant={'failure'} onClick={() => deletePos(pos.uuid)} className="!rounded-l-none" />
|
||||
</FlowbiteButton.Group>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -8,6 +8,7 @@ import {Pos} from "../../model/pos"
|
||||
import Order from "../../model/order"
|
||||
import OrderState from "../../state/order"
|
||||
import {container} from "tsyringe-neo"
|
||||
import Box from "../base/Box"
|
||||
const PositionsComponent = ({order, shop, updateOrder}) => {
|
||||
|
||||
const orderState = container.resolve(OrderState)
|
||||
@ -43,18 +44,18 @@ const PositionsComponent = ({order, shop, updateOrder}) => {
|
||||
}, [positions])
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 animate-fade-in border-2 border-orange-200 dark:border-orange-900">
|
||||
<h2 className="text-lg font-semibold mb-3 text-gray-800 dark:text-gray-100 flex items-center gap-2">
|
||||
<svg className="w-6 h-6 text-[#EA641B]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
<Box
|
||||
title="Positionen"
|
||||
subtitle="Zentraler Arbeitsbereich für Artikel, Mengen und Druckdaten."
|
||||
icon={
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
|
||||
</svg>
|
||||
Positionen
|
||||
</h2>
|
||||
}
|
||||
action={<AddPositionComponent shop={shop} addPos={addPos} />}
|
||||
>
|
||||
<ItemsComponent positions={positions} shop={shop} delPos={delPos} changePos={changePos}/>
|
||||
<div className="mt-4">
|
||||
<AddPositionComponent shop={shop} addPos={addPos}/>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, {useState} from 'react'
|
||||
import {createPortal} from 'react-dom'
|
||||
import {Button as FlowbiteButton} from 'flowbite-react'
|
||||
import Button from '../base/Button'
|
||||
import {UploadOption} from '../../model/uploadOption'
|
||||
import {UploadFile} from '../../model/upload'
|
||||
import {Pos} from '../../model/pos'
|
||||
@ -38,9 +38,9 @@ const UploadOptionModal = ({option, pos}: {option: UploadOption, pos: Pos}) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<FlowbiteButton size="xs" color="light" onClick={() => setShow(true)} title={option.label}>
|
||||
<Button size="xs" color="light" onClick={() => setShow(true)} title={option.label}>
|
||||
{option.label}
|
||||
</FlowbiteButton>
|
||||
</Button>
|
||||
|
||||
{show && (
|
||||
<BigModal title={`${option.label} — ${pos.product.title}`} onClose={() => setShow(false)}>
|
||||
|
||||
@ -7,7 +7,7 @@ import {container} from "tsyringe-neo"
|
||||
import OrderState from "../../state/order"
|
||||
import Order from "../../model/order"
|
||||
import {useEffect, useState} from "react"
|
||||
import { SelectLabel } from '../base/SelectLabel'
|
||||
import Box from '../base/Box'
|
||||
|
||||
const ShippingComponent = ({shop, order}) => {
|
||||
|
||||
@ -42,17 +42,18 @@ const ShippingComponent = ({shop, order}) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 animate-fade-in border-2 border-orange-200 dark:border-orange-900">
|
||||
<h2 className="text-lg font-semibold mb-3 text-gray-800 dark:text-gray-100 flex items-center gap-2">
|
||||
<svg className="w-6 h-6 text-[#EA641B]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
<Box
|
||||
title="Versandart"
|
||||
subtitle="Versand oder Abholung"
|
||||
icon={
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
</svg>
|
||||
Versandart
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<AsyncPaginate
|
||||
defaultOptions
|
||||
key={JSON.stringify(shopUuid)}
|
||||
components={{Control: SelectLabel}}
|
||||
defaultValue={order.shipping}
|
||||
placeholder=''
|
||||
name='Versandart'
|
||||
@ -61,9 +62,9 @@ const ShippingComponent = ({shop, order}) => {
|
||||
getOptionLabel={(option) => option.title}
|
||||
onChange={onChange}
|
||||
isSearchable={false}
|
||||
className={`${shipping == 0 ? "border-2 border-red-500 rounded" : ""}`}
|
||||
className={`${shipping == 0 ? "border border-red-400 rounded-sm" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { AsyncPaginate } from "react-select-async-paginate"
|
||||
import ShopService from '../../services/shop'
|
||||
import { container } from 'tsyringe-neo'
|
||||
import OrderState from '../../state/order'
|
||||
import { SelectLabel } from '../base/SelectLabel'
|
||||
import FieldLabel from '../base/FieldLabel'
|
||||
import React from 'react'
|
||||
|
||||
const ShopSelectComponent = (props) => {
|
||||
@ -28,16 +28,16 @@ const ShopSelectComponent = (props) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FieldLabel>Shop</FieldLabel>
|
||||
<AsyncPaginate
|
||||
defaultOptions
|
||||
value={props.shop}
|
||||
components={{ Control: SelectLabel }}
|
||||
loadOptions={loadOptions}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={onChange}
|
||||
isSearchable={false}
|
||||
className={`${props.shop.uuid == "" ? "border border-danger" : "border border-slate-200"}`}
|
||||
className={`${props.shop.uuid == "" ? "border border-red-400 rounded-sm" : "border border-psc-border rounded-sm"}`}
|
||||
name='Shop'
|
||||
/>
|
||||
|
||||
|
||||
@ -2,10 +2,10 @@ import Select from 'react-select'
|
||||
import {useEffect, useState} from 'react'
|
||||
import { container } from 'tsyringe-neo'
|
||||
import OrderState from '../../state/order'
|
||||
import { SelectLabel } from '../base/SelectLabel'
|
||||
import FieldLabel from '../base/FieldLabel'
|
||||
import React from 'react'
|
||||
|
||||
const TypeSelectComponent = ({order}) => {
|
||||
const TypeSelectComponent = ({order, onChange: onChangeProp}) => {
|
||||
|
||||
const [type, setType] = useState(0);
|
||||
const orderState: OrderState = container.resolve(OrderState);
|
||||
@ -25,6 +25,7 @@ const TypeSelectComponent = ({order}) => {
|
||||
order.type = option.value;
|
||||
orderState.setCurrentOrder(order);
|
||||
setType(option);
|
||||
if (onChangeProp) onChangeProp(option.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -33,14 +34,13 @@ const TypeSelectComponent = ({order}) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FieldLabel>Typ</FieldLabel>
|
||||
<Select
|
||||
|
||||
className={`${type == 0 ? "border border-danger" : ""}`}
|
||||
className={`${type == 0 ? "border border-red-400 rounded-sm" : ""}`}
|
||||
options={options}
|
||||
defaultValue={options.filter((e) => e.value == type).pop()}
|
||||
value={options.filter((e) => e.value == type).pop()}
|
||||
onChange={onChange}
|
||||
components={{ Control: SelectLabel }}
|
||||
placeholder=""
|
||||
name="Typ"
|
||||
/>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React, {useRef, useState} from 'react'
|
||||
import {Button as FlowbiteButton, Spinner} from 'flowbite-react'
|
||||
import Button from '../base/Button'
|
||||
import Spinner from '../base/Spinner'
|
||||
import {UploadOptionRendererProps, registerUploadOptionRenderer} from '../../lib/uploadOptionRegistry'
|
||||
import UploadService from '../../services/upload'
|
||||
|
||||
@ -53,7 +54,7 @@ const FileUploadRenderer = ({pos, files, onUploaded}: UploadOptionRendererProps)
|
||||
|
||||
<div>
|
||||
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileChange} />
|
||||
<FlowbiteButton size="sm" color="light" onClick={() => fileInputRef.current?.click()} disabled={uploading}>
|
||||
<Button size="sm" color="light" onClick={() => fileInputRef.current?.click()} disabled={uploading}>
|
||||
{uploading ? (
|
||||
<><Spinner size="sm" className="mr-2" />Wird hochgeladen…</>
|
||||
) : (
|
||||
@ -64,7 +65,7 @@ const FileUploadRenderer = ({pos, files, onUploaded}: UploadOptionRendererProps)
|
||||
Datei hochladen
|
||||
</>
|
||||
)}
|
||||
</FlowbiteButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import flowbite from "flowbite-react/tailwind"
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.{js,jsx,ts,tsx}',
|
||||
flowbite.content()
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [flowbite.plugin()],
|
||||
}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import deno from '@deno/vite-plugin'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
deno(),
|
||||
tailwindcss(),
|
||||
react({
|
||||
tsDecorators: true
|
||||
})
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,14 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>Auftrag – PrintShopCreator</title>
|
||||
<script type="module" crossorigin src="/assets/index.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<body class="bg-psc-bg font-sans text-psc-text">
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -13,7 +13,9 @@
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
|
||||
<!-- GOOGLE FONT -->
|
||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,300,400,700">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
</head>
|
||||
<body>
|
||||
<main id="root"></main>
|
||||
|
||||
@ -926,10 +926,6 @@ html {
|
||||
grid-column: span 3 / span 3;
|
||||
}
|
||||
|
||||
.col-span-4{
|
||||
grid-column: span 4 / span 4;
|
||||
}
|
||||
|
||||
.col-span-5{
|
||||
grid-column: span 5 / span 5;
|
||||
}
|
||||
|
||||
@ -7,10 +7,18 @@ changelog:
|
||||
datum: 24.06.2026
|
||||
changes:
|
||||
- "Zwei-Faktor-Authentifizierung (2FA) fürs Backend – optional pro Benutzer"
|
||||
- "2FA-Verfahren: TOTP (Authenticator-App) und E-Mail-Code, einzeln oder zusammen aktivierbar"
|
||||
- "2FA-Verfahren: Authenticator-App und E-Mail-Code, einzeln oder zusammen aktivierbar"
|
||||
- "E-Mail-Code geht an eine separate, frei wählbare 2FA-Adresse (Fallback: Login-E-Mail)"
|
||||
- "Selbstverwaltung unter /backend/2fa-settings (QR-Einrichtung für TOTP, Aktivieren/Deaktivieren)"
|
||||
- "E-Mail-2FA nutzt die System-SMTP-Einstellungen inkl. Absender; ohne vollständige SMTP-Daten ist E-Mail-2FA nicht verfügbar (Hinweis im Backend)"
|
||||
- "Selbstverwaltung unter /backend/2fa-settings (QR-Einrichtung für die Authenticator-App, Aktivieren/Deaktivieren)"
|
||||
- "2FA: Option \"Dieses Gerät 30 Tage merken\" – nach erfolgreicher Anmeldung wird am selben Gerät 30 Tage lang keine erneute 2FA-Abfrage nötig"
|
||||
- "Topbar-Hinweis \"Kein 2FA aktiviert\", solange kein Verfahren eingerichtet ist"
|
||||
- "Systemeinstellungen auf Tailwind umgestellt (Einstellungen, Mehrwertsteuer, SMTP-Test) im Aufbau wie CMS/Aufträge"
|
||||
- "Systemeinstellungen: SMTP um Absender-E-Mail und -Name erweitert (für Systemmails)"
|
||||
- "Dashboard: Anzeige der aktuellen Version sowie verfügbarer (neuerer) Releases inkl. Pre-Release-Kennzeichnung"
|
||||
- "Auftragsliste: aufklappbare Positionen jetzt im selben Aufbau wie die Auftragsdetails (Name/Details mit Vorschau, WK-Hinweise, Eigenschaften/Auflage, Preise, Widerruf/Status) – ohne Aktionen"
|
||||
- "Auftragsdetails und -liste: Spaltenreihenfolge angepasst – Preise stehen jetzt vor dem Widerruf/Status"
|
||||
- "Auftragsliste: Auftragspreise als Netto/MwSt./Brutto-Aufstellung wie die Positionspreise"
|
||||
- version: 2.3.7
|
||||
datum: 23.06.2026
|
||||
changes:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user