This commit is contained in:
Thomas Peterson 2026-07-03 17:30:15 +02:00
parent ed599c5253
commit 45642fce51
76 changed files with 9350 additions and 2889 deletions

View File

@ -3,6 +3,12 @@ APP_ENV=dev
APP_SECRET=347829efiubvf347fbisdc27f APP_SECRET=347829efiubvf347fbisdc27f
###< symfony/framework-bundle ### ###< 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 ### ###> doctrine/doctrine-bundle ###
DATABASE_URL=mysql://psc:psc@mysql:3306/psc DATABASE_URL=mysql://psc:psc@mysql:3306/psc
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###

View File

@ -3,6 +3,12 @@ APP_ENV=prod
APP_SECRET=347829efiubvf347fbisdc27f APP_SECRET=347829efiubvf347fbisdc27f
###< symfony/framework-bundle ### ###< 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 ### ###> doctrine/doctrine-bundle ###
DATABASE_URL=mysql://root:Wichtig1@mysql:3306/psc DATABASE_URL=mysql://root:Wichtig1@mysql:3306/psc
###< doctrine/doctrine-bundle ### ###< doctrine/doctrine-bundle ###

View File

@ -1,2 +1,10 @@
[ 377ms] [LOG] details: {application: X} @ http://type-dev-tp.local/apps/assets/vendor/@hotwired/stimulus/stimulus.index-S4zNcea.js:6 [ 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 [ 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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]

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@ const Order_List_Preview = (uploadTypeObject) => {
return notAvailable; 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"> <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" /> <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> </svg>
@ -19,69 +19,107 @@ const Order_List_Preview = (uploadTypeObject) => {
return uploadTypeObject.uploads.map(upload => { return uploadTypeObject.uploads.map(upload => {
const previewUrl = `/apps/backend/order/upload/preview?path=${encodeURIComponent(upload.path)}`; 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}" <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}" title="${upload.fileName}"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';" /> onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';" />
${fileIconHtml(upload.typ)} ${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>`; </div>`;
}).join(''); }).join('');
}; };
const Order_List_Detail = ({ uuid, basketField1, customerInfo, basketField2, pos, price, product, status, allNet, reOrder, reOrderOrder, reOrderPos, uploadTypeObject }, orderUuid) => ` const euro = (cents) => new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(cents / 100);
<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}"> // Tabellenkopf identisch zur Auftrags-Detailansicht ohne Spalte "Aktionen".
<div class="grid grid-cols-12 gap-4 flex-1"> const Order_List_Detail_Head = `
<div class="col-span-1"> <th class="px-3 py-3 text-left font-medium text-gray-700 w-10">Pos</th>
<span class="text-xs font-semibold text-gray-700">Pos:</span> <th class="px-3 py-3 text-left font-medium text-gray-700">Name / Details</th>
<div class="font-medium">${pos}</div> <th class="px-3 py-3 text-left font-medium text-gray-700">WK_Pos_1 und WK_Pos_2</th>
</div> <th class="px-3 py-3 text-left font-medium text-gray-700">Eigenschaften / Auflage</th>
<div class="col-span-1"> <th class="px-3 py-3 text-left font-medium text-gray-700">Preise</th>
${Order_List_Preview(uploadTypeObject)} <th class="px-3 py-3 text-left font-medium text-gray-700">Widerruf / Status</th>
</div> `;
<div class="col-span-4">
<span class="text-xs font-semibold text-gray-700">Produkt:</span> // Widerruf-Badge identisch zur Detailansicht (drei Zustände).
<div> const Order_List_Revoke = (revoked, revokedAt, revokeAllowed) => {
${(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) 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)} ${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>
<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>
<div class="col-span-1"> <div class="flex justify-between gap-4 border-t border-gray-100 pt-1">
<span class="text-xs font-semibold text-gray-700">Auflage:</span> <dt class="text-gray-700 font-semibold">Brutto Preis</dt>
<div>${price.count}</div> <dd class="text-gray-900 font-semibold text-right">${euro(price.allGross)}</dd>
</div> </div>
<div class="col-span-2"> </dl>
<span class="text-xs font-semibold text-gray-700">Kunden Info:</span> </td>
<div class="text-sm">${customerInfo}${(reOrder ? `<br/><strong class="text-yellow-600">Ist eine Nachbestellung</strong>` : ``)}</div>
</div> <td class="px-3 py-4">
<div class="col-span-1"> ${Order_List_Revoke(revoked, revokedAt, revokeAllowed)}
<span class="text-xs font-semibold text-gray-700">Preis:</span> <div class="relative inline-block text-left">
<div class="text-right whitespace-nowrap"> <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">
${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">
${psc.order.get_pos_status(status).internalName} ${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> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg> </svg>
</button> </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)} ${psc.order.get_pos_status_loop(orderUuid, uuid)}
</div> </div>
</div> </div>
</div> </td>
</div>
</div> </tr>
</div>
`; `;
const Order_List_Detail_Simple = ({ uuid, basketField1, customerInfo, basketField2, pos, price, product, status, allNet }, orderUuid) => ` 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> </button>
`; `;
const Order_List_Pos_Calc_Option = (elm) => ` const Order_List_Pos_Calc_Option = (elm) => `<li>${elm.name}: <span class="font-medium">${elm.value}</span></li>`;
<tr><td>${elm.name}</td><td>${elm.value}</td></tr>
`;
export class order { export class order {
@ -169,11 +205,12 @@ export class order {
return ''; 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') { if (elm.type != 'hidden') {
return Order_List_Pos_Calc_Option(elm); return Order_List_Pos_Calc_Option(elm);
} }
}).join('') + '</table>'; return '';
}).join('') + '</ul>';
} }
load_status() { load_status() {
@ -244,7 +281,15 @@ export class order {
if (simple) { if (simple) {
$('#rows-' + result.uuid + ' td').html(result.positions.map(x => Order_List_Detail_Simple(x, orderUuid)).join('')); $('#rows-' + result.uuid + ' td').html(result.positions.map(x => Order_List_Detail_Simple(x, orderUuid)).join(''));
} else { } 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)); result.positions.map(x => this.loadPluginDetails(x));
} }

View File

@ -65,6 +65,7 @@
"scheb/2fa-bundle": "^8.6", "scheb/2fa-bundle": "^8.6",
"scheb/2fa-email": "^8.6", "scheb/2fa-email": "^8.6",
"scheb/2fa-totp": "^8.6", "scheb/2fa-totp": "^8.6",
"scheb/2fa-trusted-device": "^8.6",
"scssphp/scssphp": "v1.11.1", "scssphp/scssphp": "v1.11.1",
"setasign/fpdi-tcpdf": "^2.3", "setasign/fpdi-tcpdf": "^2.3",
"sofort/sofortlib-php": "3.3.2", "sofort/sofortlib-php": "3.3.2",

117
src/new/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "e04b81bc87a7231b11cc580757ba49a0", "content-hash": "f863fc8c87dbe8935f9ac19c1e038484",
"packages": [ "packages": [
{ {
"name": "apimatic/core", "name": "apimatic/core",
@ -4442,6 +4442,70 @@
], ],
"time": "2025-11-01T09:38:14+00:00" "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", "name": "lcobucci/jwt",
"version": "5.6.0", "version": "5.6.0",
@ -8327,6 +8391,57 @@
}, },
"time": "2026-01-24T13:27:55+00:00" "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", "name": "scssphp/scssphp",
"version": "v1.11.1", "version": "v1.11.1",

View File

@ -20,3 +20,15 @@ scheb_two_factor:
algorithm: sha1 algorithm: sha1
digits: 6 digits: 6
period: 30 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)%'

View File

@ -2966,6 +2966,18 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* code_reuse_cache?: scalar|Param|null, // Default: null * code_reuse_cache?: scalar|Param|null, // Default: null
* code_reuse_cache_duration?: int|Param, // Default: 60 * code_reuse_cache_duration?: int|Param, // Default: 60
* code_reuse_default_handler?: scalar|Param|null, // Default: null * 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{ * email?: bool|array{
* enabled?: scalar|Param|null, // Default: false * enabled?: scalar|Param|null, // Default: false
* mailer?: scalar|Param|null, // Default: null * mailer?: scalar|Param|null, // Default: null

View File

@ -22,6 +22,7 @@ use Scheb\TwoFactorBundle\Model\Email\TwoFactorInterface as EmailTwoFactorInterf
use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration; use Scheb\TwoFactorBundle\Model\Totp\TotpConfiguration;
use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface; use Scheb\TwoFactorBundle\Model\Totp\TotpConfigurationInterface;
use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface as TotpTwoFactorInterface; 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\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
@ -34,7 +35,7 @@ use Symfony\Component\Security\Core\User\UserInterface;
*/ */
#[ORM\Table(name: 'contact')] #[ORM\Table(name: 'contact')]
#[ORM\Entity(repositoryClass: 'PSC\Shop\ContactBundle\Repository\ContactRepository')] #[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 $kundenNr = '';
public $calcValue1 = ''; public $calcValue1 = '';
@ -424,6 +425,16 @@ class Contact implements UserInterface, PasswordAuthenticatedUserInterface, \Ser
return $this->totpSecret; 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 public function setTotpSecret(?string $secret): void
{ {
$this->totpSecret = $secret; $this->totpSecret = $secret;

View File

@ -93,6 +93,15 @@ class Position
#[OA\Property(type: 'string')] #[OA\Property(type: 'string')]
private string $basketField2; 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() public function __construct()
{ {
$this->uuid = Uuid::uuid4()->toString(); $this->uuid = Uuid::uuid4()->toString();
@ -103,6 +112,36 @@ class Position
$this->price = new Price(); $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 public function getPos(): int
{ {
return $this->pos; return $this->pos;

View File

@ -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">{{'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">{{'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">{{'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">{{'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> <th class="px-3 py-3 text-left font-medium text-gray-700">{{'actions'|trans}}</th>
</tr> </tr>
</thead> </thead>
@ -547,6 +547,24 @@
{% endfor %} {% endfor %}
</td> </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 #} {# Widerruf / Status #}
<td class="px-3 py-4"> <td class="px-3 py-4">
{% if pos.obj.revoked %} {% if pos.obj.revoked %}
@ -586,24 +604,6 @@
</div> </div>
</td> </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 #} {# Aktionen #}
<td class="px-3 py-4"> <td class="px-3 py-4">
<div class="flex flex-col gap-4 w-64"> <div class="flex flex-col gap-4 w-64">

View File

@ -110,9 +110,21 @@
{{ orderObj.getInvoiceAddress().street }} {{ orderObj.getInvoiceAddress().houseNumber }}<br/> {{ orderObj.getInvoiceAddress().street }} {{ orderObj.getInvoiceAddress().houseNumber }}<br/>
{{ orderObj.getInvoiceAddress().zip }} {{ orderObj.getInvoiceAddress().city }} {{ orderObj.getInvoiceAddress().zip }} {{ orderObj.getInvoiceAddress().city }}
</td> </td>
<td class="px-2 py-3 text-right whitespace-nowrap"> <td class="px-2 py-3">
{{ order.netto|number_format(2, ',', '.') }}€<br/> <dl class="text-xs space-y-1 w-40 ml-auto">
<strong>({{ order.brutto|number_format(2, ',', '.') }}€)</strong> <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>
<td class="px-2 py-3 text-right"> <td class="px-2 py-3 text-right">
<div class="flex flex-col gap-1 min-w-[180px]"> <div class="flex flex-col gap-1 min-w-[180px]">

View File

@ -149,6 +149,9 @@ class Position extends Base
$position->setDownloadAllowed($positionDoc->isDownloadAllowed()); $position->setDownloadAllowed($positionDoc->isDownloadAllowed());
$position->setBasketField1((string) $pos->getBasketField1()); $position->setBasketField1((string) $pos->getBasketField1());
$position->setBasketField2((string) $pos->getBasketField2()); $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->setLayouterMode((int) $pos->getLayouterMode());
$position->setWeight((int) $pos->getWeight()); $position->setWeight((int) $pos->getWeight());
$position->setUploadMode((string) $positionDoc->getUploadMode()); $position->setUploadMode((string) $positionDoc->getUploadMode());

View File

@ -31,7 +31,7 @@
{% if displayTrustedOption %} {% if displayTrustedOption %}
<label class="mt-3 flex items-center gap-2 text-sm text-gray-700"> <label class="mt-3 flex items-center gap-2 text-sm text-gray-700">
<input type="checkbox" name="{{ trustedParameterName }}" class="rounded border-gray-300"> <input type="checkbox" name="{{ trustedParameterName }}" class="rounded border-gray-300">
{{ "trusted"|trans({}, 'SchebTwoFactorBundle') }} Dieses Gerät 30 Tage merken
</label> </label>
{% endif %} {% endif %}
{% if isCsrfProtectionEnabled %} {% if isCsrfProtectionEnabled %}

View File

@ -12,33 +12,30 @@
"jsxImportSourceTypes": "@types/react" "jsxImportSourceTypes": "@types/react"
}, },
"imports": { "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", "@ebay/nice-modal-react": "npm:@ebay/nice-modal-react@^1.2.13",
"@heroicons/react": "npm:@heroicons/react@^2.2.0", "@heroicons/react": "npm:@heroicons/react@^2.2.0",
"@rjsf/core": "npm:@rjsf/core@^5.23.2", "@rjsf/core": "npm:@rjsf/core@^5.24.13",
"@rjsf/validator-ajv6": "npm:@rjsf/validator-ajv6@^5.23.2", "@rjsf/utils": "npm:@rjsf/utils@^5.24.13",
"@types/react": "npm:@types/react@^18.3.12", "@rjsf/validator-ajv6": "npm:@rjsf/validator-ajv6@^5.24.13",
"@types/react-dom": "npm:@types/react-dom@^18.3.1", "@tailwindcss/vite": "npm:@tailwindcss/vite@^4.3.2",
"@udibo/esbuild-plugin-postcss": "jsr:@udibo/esbuild-plugin-postcss@^0.1.3", "@types/react": "npm:@types/react@^19.2.17",
"@vitejs/plugin-react-swc": "npm:@vitejs/plugin-react-swc@^3.7.1", "@types/react-dom": "npm:@types/react-dom@^19.2.3",
"autoprefixer": "npm:autoprefixer@^10.4.20", "@vitejs/plugin-react-swc": "npm:@vitejs/plugin-react-swc@^4.3.1",
"axios": "npm:axios@^1.7.9", "axios": "npm:axios@^1.18.1",
"flowbite-react": "npm:flowbite-react@^0.10.2",
"postcss": "npm:postcss@^8.4.49",
"prop-types": "npm:prop-types@^15.8.1", "prop-types": "npm:prop-types@^15.8.1",
"react": "npm:react@^18.2.0", "react": "npm:react@^19.2.7",
"react-dom": "npm:react-dom@^18.2.0", "react-dom": "npm:react-dom@^19.2.7",
"react-dom/client": "npm:react-dom@^18.2.0/client", "react-dom/client": "npm:react-dom@^19.2.7/client",
"react-router-dom": "npm:react-router-dom@^7.0.2", "react-router-dom": "npm:react-router-dom@^7.18.1",
"react-select": "npm:react-select@^5.10.2", "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", "reflect-metadata": "npm:reflect-metadata@^0.2.2",
"rxjs": "npm:rxjs@^7.8.1", "rxjs": "npm:rxjs@^7.8.2",
"styled-components": "npm:styled-components@^6.1.13", "tailwindcss": "npm:tailwindcss@^4.3.2",
"tailwindcss": "npm:tailwindcss@^3.4.17",
"tsyringe-neo": "npm:tsyringe-neo@^5.1.0", "tsyringe-neo": "npm:tsyringe-neo@^5.1.0",
"use-debounce": "npm:use-debounce@^10.0.4", "use-debounce": "npm:use-debounce@^10.1.1",
"uuid": "npm:uuid@^11.0.3", "uuid": "npm:uuid@^14.0.1",
"vite": "npm:vite@^6.0.5" "vite": "npm:vite@^8.1.3"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,18 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="de">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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> </head>
<body> <body class="bg-psc-bg font-sans text-psc-text">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>

View File

@ -1,6 +0,0 @@
import tailwindcss from "tailwindcss";
import autoprefixer from "autoprefixer";
export default {
plugins: [tailwindcss, autoprefixer],
};

View File

@ -1,4 +1,3 @@
import * as Eta from 'eta'
import Token from '../services/token' import Token from '../services/token'
import { container } from "tsyringe-neo" import { container } from "tsyringe-neo"
import * as React from 'react' import * as React from 'react'

View File

@ -1,80 +1,35 @@
@tailwind base; @import "tailwindcss";
@tailwind components;
@tailwind utilities; /* 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 { @layer base {
/* Smooth scrolling */
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
/* Better font rendering */
body { body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
} }
@layer components { /* Sanfte Einblend-Animation (in mehreren Komponenten genutzt) */
/* Card hover effects */ .animate-fade-in {
.card-hover { animation: fadeIn 0.3s ease-in;
@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 { @keyframes fadeIn {
/* Animation utilities */
.animate-fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { from {
opacity: 0; opacity: 0;
transform: translateY(-10px); transform: translateY(-10px);
@ -83,21 +38,10 @@
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
}
/* 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. /* Tailwind-Styling für das Standard-Markup von @rjsf/core.
Keep outside @layer so Tailwind does not purge it from generated CSS. */ Außerhalb @layer, damit es nicht weg-optimiert wird. */
.rjsf { .rjsf {
@apply space-y-4; @apply space-y-4;
} }
@ -131,17 +75,17 @@
.rjsf input[type="text"], .rjsf input[type="text"],
.rjsf input[type="email"], .rjsf input[type="email"],
.rjsf input[type="number"] { .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="checkbox"],
.rjsf input[type="radio"] { .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 .checkbox label,
.rjsf .radio 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, .rjsf .text-danger,

View File

@ -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;
}
}

View File

@ -1,7 +1,6 @@
import "./assets/index.css" import "./assets/index.css"
import "reflect-metadata"; import "reflect-metadata";
import "./modules/uploadOptions"; import "./modules/uploadOptions";
import * as $ from "jquery";
import { App } from "./app/app"; import { App } from "./app/app";
declare var jwt_token: String; declare var jwt_token: String;

View File

@ -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

View File

@ -1,7 +1,7 @@
import { Shop } from '../../model/shop' import { Shop } from '../../model/shop'
import ContactComponent from '../contact/ContactComponent' import ContactComponent from '../contact/ContactComponent'
import PositionsComponent from '../positions/PositionsComponent' import PositionsComponent from '../positions/PositionsComponent'
import { useEffect, useState } from 'react' import { useEffect, useReducer, useState } from 'react'
import Order from "../../model/order" import Order from "../../model/order"
import PaymentComponent from "../payment/PaymentComponent" import PaymentComponent from "../payment/PaymentComponent"
import ShippingComponent from "../shipping/ShippingComponent" import ShippingComponent from "../shipping/ShippingComponent"
@ -12,24 +12,63 @@ import OrderState from "../../state/order"
import OrderService from "../../services/order" import OrderService from "../../services/order"
import InfoFieldComponent from "../info/InfoFieldComponent" import InfoFieldComponent from "../info/InfoFieldComponent"
import React from 'react' import React from 'react'
import { debounceTime } from 'rxjs'
import CalcComponent from '../calc/CalcComponent' import CalcComponent from '../calc/CalcComponent'
import DraftComponent from '../draft/DraftComponent' import DraftComponent from '../draft/DraftComponent'
import TypeSelectComponent from '../type/TypeSelectComponent' import TypeSelectComponent from '../type/TypeSelectComponent'
import ShopSelectComponent from '../shop/ShopSelectComponent' import ShopSelectComponent from '../shop/ShopSelectComponent'
import Box from './Box'
const BaseComponent = (props) => { const TYPE_LABELS: Record<number, string> = {
console.log('BaseComponent rendering') 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 [shop, setShop] = useState<Shop>(new Shop())
const [order, setOrder] = useState<Order>(new Order()) 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 order_state = container.resolve(OrderState)
const orderService = container.resolve(OrderService) const orderService = container.resolve(OrderService)
const loadOrder = async (uuid: String) => { const loadOrder = async (uuid: String) => {
orderService.getOrder(uuid).then((orderObj) => { orderService.getOrder(uuid).then((orderObj) => {
order_state.setCurrentOrder(orderObj) order_state.setCurrentOrder(orderObj)
setShop(orderObj.shop) setShop(orderObj.shop)
setOrder(orderObj) setOrder(orderObj)
setType(Number(orderObj.type) || 0)
setIsSetup(false)
}) })
} }
@ -38,86 +77,142 @@ const BaseComponent = (props) => {
useEffect(() => { useEffect(() => {
if (params.uuid) { if (params.uuid) {
loadOrder(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 ( 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="min-h-screen bg-psc-bg font-sans text-psc-text antialiased">
<div className='w-full px-6 py-4'> <main className="w-full px-4 py-4 lg:px-6">
{/* Top Header Section - Settings & Controls */} {/* Header */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 mb-4 animate-fade-in"> <header className="rounded-sm border border-psc-border bg-white px-7.5 py-6 shadow-lg">
{/* First Row: Order Info & Action Buttons */} <div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div className="flex flex-wrap items-center gap-4 mb-4"> <div className="flex min-w-0 items-center gap-4">
{/* Left: Order Number & Draft Toggle */} <div className="grid h-10 w-10 shrink-0 place-items-center border border-orange-200 bg-orange-50 text-psc-orange">
<div className="flex items-center gap-4"> <DocIcon />
<div className="font-semibold text-lg text-gray-800 dark:text-gray-100">
NR: <span className="text-[#EA641B]">{order.alias || '---'}</span>
</div> </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} /> <DraftComponent order={order} />
</div> </div>
{/* Right: Action Buttons */} {!isSetup && (
<div className="ml-auto"> <div className="mt-2 flex flex-wrap items-center gap-2">
<ButtonComponent loadOrder={loadOrder} /> <span className="rounded-sm border border-psc-border bg-slate-50 px-3 py-1 text-xs font-medium text-psc-muted">
</div> {TYPE_LABELS[type] || 'Typ'}
</div> </span>
<span className="rounded-sm border border-psc-border bg-slate-50 px-3 py-1 text-xs font-medium text-psc-muted">
{/* Second Row: Type & Shop Selection */} {shop.name || 'Shop'}
<div className="flex flex-wrap items-center gap-4 pt-4 border-t border-gray-200 dark:border-gray-700"> </span>
<div className="flex-1 min-w-[200px]"> <button
<TypeSelectComponent order={order} /> type="button"
</div> onClick={() => setIsSetup(true)}
<div className="flex-1 min-w-[200px]"> className="rounded-sm border border-psc-border bg-white px-3 py-1 text-xs font-medium text-psc-orange hover:bg-orange-50"
<ShopSelectComponent shop={shop} change={setShop} /> >
</div> Typ / Shop ändern
</div> </button>
</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>
</div> </div>
)} )}
</div> </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 export default BaseComponent

View File

@ -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

View File

@ -1,35 +1,102 @@
import { Button as BaseButton } from "flowbite-react"
import React from 'react' import React from 'react'
const Button = ({ type , variant , onClick, className = "" }) => { type ButtonProps = {
return ( type?: number
<BaseButton color={variant} onClick={onClick} size="sm" className={`p-1.5 ${className}`}> variant?: string
{ type == 1 && color?: string
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" className="w-4 h-4"> size?: 'xs' | 'sm' | 'md' | 'lg'
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15"></path> disabled?: boolean
</svg> onClick?: (e: any) => void
} className?: string
{ type == 2 && title?: string
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="2" stroke="currentColor" className="w-4 h-4"> htmlType?: 'button' | 'submit' | 'reset'
<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> children?: React.ReactNode
</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

View File

@ -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

View File

@ -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

View File

@ -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`)};
`;

View File

@ -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

View File

@ -4,7 +4,7 @@ import OrderService from '../../services/order'
import { Component } from 'react' import { Component } from 'react'
import { debounceTime } from "rxjs" import { debounceTime } from "rxjs"
import { Order } from "../../model/order" import { Order } from "../../model/order"
import { Button } from "flowbite-react" import Button, { ButtonGroup } from '../base/Button'
import React from 'react' import React from 'react'
class ButtonComponent extends Component<{ loadOrder }, { disabled: boolean, saving: boolean }> { class ButtonComponent extends Component<{ loadOrder }, { disabled: boolean, saving: boolean }> {
@ -53,31 +53,29 @@ class ButtonComponent extends Component<{ loadOrder }, { disabled: boolean, savi
render() { render() {
return ( return (
<Button.Group> <ButtonGroup className="gap-3">
<Button <Button
size="md" color="success"
color="blue"
disabled={this.state.disabled} disabled={this.state.disabled}
onClick={(e: any) => this.handleSave(e)} 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> <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> </svg>
{this.state.saving === true && <span>wird gespeichert...</span>} {this.state.saving === true && <span>wird gespeichert...</span>}
{this.state.saving === false && <span>Speichern</span>} {this.state.saving === false && <span>Speichern</span>}
</Button> </Button>
<Button <Button
size="md" color="light"
color="success"
disabled={!this.orderState.getCurrentOrder().value.saved} disabled={!this.orderState.getCurrentOrder().value.saved}
onClick={(e: any) => this.handlePrint(e)} 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> <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> </svg>
Drucken Drucken
</Button> </Button>
</Button.Group> </ButtonGroup>
) )
} }
} }

View File

@ -39,21 +39,21 @@ class CalcComponent extends React.Component<{ },MyState> {
render() { render() {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700"> <div className="flex items-center justify-between gap-6 text-sm text-psc-muted">
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Netto</span> <span>Netto</span>
<span className="text-base font-semibold text-gray-800 dark:text-gray-200"> <span className="font-medium text-psc-text">
<Currency price={this.state.order.net} /> <Currency price={this.state.order.net} />
</span> </span>
</div> </div>
<div className="flex justify-between items-center py-2 border-b border-gray-200 dark:border-gray-700"> <div className="flex items-center justify-between gap-6 text-sm text-psc-muted">
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">MwSt.</span> <span>MwSt.</span>
<span className="text-base font-semibold text-gray-800 dark:text-gray-200"> <span className="font-medium text-psc-text">
<Currency price={this.state.order.vat} /> <Currency price={this.state.order.vat} />
</span> </span>
</div> </div>
<div className="flex justify-between items-center py-3 bg-orange-50 dark:bg-orange-900/20 rounded-lg px-3 mt-2"> <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-base font-bold text-gray-900 dark:text-gray-100">Brutto</span> <span className="text-sm font-medium text-psc-text">Brutto</span>
<span className="text-xl font-bold text-[#EA641B] dark:text-orange-400"> <span className="text-2xl font-medium text-psc-orange">
<Currency price={this.state.order.gross} /> <Currency price={this.state.order.gross} />
</span> </span>
</div> </div>

View File

@ -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

View File

@ -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

View File

@ -5,7 +5,7 @@ import { RJSFSchema } from "@rjsf/utils"
import validator from "@rjsf/validator-ajv6" import validator from "@rjsf/validator-ajv6"
import Form from "@rjsf/core" import Form from "@rjsf/core"
import CountryService from '../../services/country' import CountryService from '../../services/country'
import { Modal } from "flowbite-react" import Modal from '../base/Modal'
import React from 'react' import React from 'react'
const Addressdiv = NiceModal.create( const Addressdiv = NiceModal.create(

View File

@ -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

View File

@ -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

View File

@ -1,100 +1,206 @@
import * as React from 'react' import * as React from 'react'
import { useState } from 'react'
import * as PropTypes from 'prop-types' import * as PropTypes from 'prop-types'
import ContactSelect from './ContactSelect' import { useModal } from '@ebay/nice-modal-react'
import AddressSelect from './AddressSelect'
import { Contact } from '../../model/contact' import { Contact } from '../../model/contact'
import { Address } from '../../model/address' import { Address } from '../../model/address'
import { useEffect, useState } from 'react' import { container } from 'tsyringe-neo'
import OrderState from '../../state/order' import OrderState from '../../state/order'
import { container } from "tsyringe-neo" import AddressService from '../../services/address'
import AccountSelectComponent from '../account/AccountSelectComponent'
import Order from '../../model/order' import Order from '../../model/order'
import { Shop } from '../../model/shop' 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 }) => { 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) => { const [invoiceAddress, setInvoiceAddress] = useState<Address | null>(order.invoiceAddress?.uuid ? order.invoiceAddress : null)
if (type == 1) { const [deliveryAddress, setDeliveryAddress] = useState<Address | null>(order.deliveryAddress?.uuid ? order.deliveryAddress : null)
orderState.getCurrentOrder().value.invoiceAddress = address const [senderAddress, setSenderAddress] = useState<Address | null>(order.senderAddress?.uuid ? order.senderAddress : null)
order.invoiceAddress = address
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.setCurrentOrder(cur)
orderState.getCurrentOrder().value.deliveryAddress = address setRerender((n) => n + 1)
order.deliveryAddress = address
} }
if (type == 3) {
orderState.getCurrentOrder().value.senderAddress = address const pickerModal = useModal(ContactPickerModal)
order.senderAddress = address
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 ( 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"> <Box
<svg className="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> title="Kontakt"
<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" /> subtitle="Kunde und Accountdaten."
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /> 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> </svg>
Adressen }
</h3> >
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4"> {contactSelected ? (
<div> <div className="rounded-sm border border-psc-border bg-slate-50 p-4">
<AddressSelect <div className="flex min-w-0 items-center gap-3">
address={order.invoiceAddress} <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">
contact={order.contact} {initials(order.contact)}
changeAddress={changeAddress}
name='Rechnungsadresse'
type={1}
shop={shop}
/>
</div> </div>
<div> <div className="min-w-0">
<AddressSelect <strong className="block truncate text-sm font-medium text-psc-text">
address={order.deliveryAddress} {order.contact.username || order.contact.email}
contact={order.contact} </strong>
changeAddress={changeAddress} <span className="text-sm text-psc-muted">Kunde ist ausgewählt</span>
name='Lieferadresse' </div>
type={2} </div>
shop={shop} <div className="mt-4 grid grid-cols-2 gap-2">
/> <button
</div> type="button"
<div> onClick={openPicker}
<AddressSelect className="h-10 rounded-sm border border-psc-border bg-white px-4 text-sm font-medium text-psc-orange hover:bg-orange-50"
address={order.senderAddress} >
changeAddress={changeAddress} Ändern
contact={order.contact} </button>
name='Absenderadresse' <button
type={3} type="button"
shop={shop} 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>
</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> </div>
</Box>
)}
</>
) )
} }
@ -103,5 +209,4 @@ ContactComponent.propTypes = {
shop: PropTypes.instanceOf(Shop), shop: PropTypes.instanceOf(Shop),
} }
export default ContactComponent export default ContactComponent

View File

@ -6,7 +6,7 @@ import { Contact } from "../../model/contact";
import validator from "@rjsf/validator-ajv6"; import validator from "@rjsf/validator-ajv6";
import Form from "@rjsf/core"; import Form from "@rjsf/core";
import CountryService from "../../services/country"; import CountryService from "../../services/country";
import { Modal } from "flowbite-react"; import Modal from '../base/Modal';
import React from 'react' import React from 'react'
const ContactModal = NiceModal.create( const ContactModal = NiceModal.create(

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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 }
}

View File

@ -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 }
}

View File

@ -1,7 +1,6 @@
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import { container } from 'tsyringe-neo' import { container } from 'tsyringe-neo'
import OrderState from'../../state/order' import OrderState from'../../state/order'
import { SelectLabel } from '../base/SelectLabel'
import React from 'react' import React from 'react'
const DraftComponent = ({order}) => { const DraftComponent = ({order}) => {
@ -24,8 +23,8 @@ const DraftComponent = ({order}) => {
<div className="gap-2"> <div className="gap-2">
<label className="inline-flex items-center cursor-pointer"> <label className="inline-flex items-center cursor-pointer">
<input type="checkbox" value="1" className="sr-only peer" defaultChecked={draft} onChange={(e) => onChange(e)}/> <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> <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-gray-900 dark:text-gray-300">Entwurf</span> <span className="ms-3 text-sm font-medium text-psc-text">Entwurf</span>
</label> </label>
</div> </div>
) )

View File

@ -7,6 +7,7 @@ import OrderState from "../../state/order"
import Order from "../../model/order" import Order from "../../model/order"
import {useEffect, useState} from "react" import {useEffect, useState} from "react"
import React from 'react' import React from 'react'
import Box from "../base/Box"
const InfoFieldComponent = ({shop, order}) => { const InfoFieldComponent = ({shop, order}) => {
@ -35,16 +36,18 @@ const InfoFieldComponent = ({shop, order}) => {
} }
return ( 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"> <Box
<h2 className="text-lg font-semibold mb-3 text-gray-800 dark:text-gray-100 flex items-center gap-2"> title="Zusatzinformationen"
<svg className="w-6 h-6 text-[#EA641B]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> subtitle="Hinweise und Zusatzfelder."
<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" /> 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> </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> <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"} {shop.basketField1 || "Info 1"}
</label> </label>
<input <input
@ -52,12 +55,12 @@ const InfoFieldComponent = ({shop, order}) => {
name="basketField1" name="basketField1"
value={basketField1} value={basketField1}
onChange={e => changeBasketField1(e.target.value)} 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"} placeholder={shop.basketField1 || "Info 1"}
/> />
</div> </div>
<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"} {shop.basketField2 || "Info 2"}
</label> </label>
<input <input
@ -65,12 +68,12 @@ const InfoFieldComponent = ({shop, order}) => {
name="basketField2" name="basketField2"
value={basketField2} value={basketField2}
onChange={e => changeBasketField2(e.target.value)} 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"} placeholder={shop.basketField2 || "Info 2"}
/> />
</div> </div>
</div> </div>
</div> </Box>
); );
}; };

View File

@ -8,7 +8,7 @@ import OrderState from "../../state/order"
import Order from "../../model/order" import Order from "../../model/order"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import Select from 'react-select' import Select from 'react-select'
import { SelectLabel } from '../base/SelectLabel' import Box from '../base/Box'
const PaymentComponent = ({ shop, order }) => { const PaymentComponent = ({ shop, order }) => {
@ -44,17 +44,18 @@ const PaymentComponent = ({ shop, order }) => {
} }
return ( 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"> <Box
<h2 className="text-lg font-semibold mb-3 text-gray-800 dark:text-gray-100 flex items-center gap-2"> title="Zahlart"
<svg className="w-6 h-6 text-[#EA641B]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> subtitle="Zahlungsbedingung"
<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" /> 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> </svg>
Zahlart }
</h2> >
<AsyncPaginate <AsyncPaginate
defaultOptions defaultOptions
key={JSON.stringify(shopUuid)} key={JSON.stringify(shopUuid)}
components={{ Control: SelectLabel }}
defaultValue={order.payment} defaultValue={order.payment}
loadOptions={loadOptions} loadOptions={loadOptions}
placeholder='' placeholder=''
@ -63,9 +64,9 @@ const PaymentComponent = ({ shop, order }) => {
getOptionLabel={(option) => option.title} getOptionLabel={(option) => option.title}
onChange={onChange} onChange={onChange}
isSearchable={false} isSearchable={false}
className={`${payment == 0 ? "border-2 border-red-500 rounded" : ""}`} className={`${payment == 0 ? "border border-red-400 rounded-sm" : ""}`}
/> />
</div> </Box>
) )
} }

View File

@ -9,8 +9,7 @@ import ProductSelect from "../product/ProductSelect"
import ProductForm from "../product/ProductForm" import ProductForm from "../product/ProductForm"
import Order from "../../model/order" import Order from "../../model/order"
import { Pos } from '../../model/pos' import { Pos } from '../../model/pos'
import Button from '../base/Button' import Modal from '../base/Modal';
import { Modal } from "flowbite-react";
const AddPositionComponent = (props) => { const AddPositionComponent = (props) => {
@ -33,10 +32,16 @@ const AddPositionComponent = (props) => {
pos.product = product; pos.product = product;
return ( 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> <Modal.Header>
add position add position
@ -69,7 +74,7 @@ const AddPositionComponent = (props) => {
/> />
</Modal.Body> </Modal.Body>
</Modal> </Modal>
</div> </>
) )
} }

View File

@ -8,7 +8,7 @@ import ProductSelect from "../product/ProductSelect"
import { Pos } from '../../model/pos' import { Pos } from '../../model/pos'
import Button from '../base/Button' import Button from '../base/Button'
import ProductForm from '../product/ProductForm' import ProductForm from '../product/ProductForm'
import { Modal } from "flowbite-react"; import Modal from '../base/Modal';
import React from 'react' import React from 'react'
const EditPositionComponent = ({position, shop, changePos}) => { const EditPositionComponent = ({position, shop, changePos}) => {
@ -22,7 +22,7 @@ const EditPositionComponent = ({position, shop, changePos}) => {
<div> <div>
<Button type={2} variant="gray" onClick={handleShow} className="!rounded-r-none" /> <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"> <Modal.Header className="text-mauve12 m-0 text-[17px] font-medium">
edit position edit position
</Modal.Header> </Modal.Header>

View File

@ -9,21 +9,21 @@ const ItemsComponent = ({positions, delPos, shop, changePos}) => {
return ( 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"> <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"> <thead className="bg-slate-50">
<tr className="border-b-2 border-orange-200 dark:border-orange-800"> <tr className="border-b border-psc-border">
<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="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-psc-muted">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="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-psc-muted">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="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-psc-muted">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="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-psc-muted">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="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-psc-muted">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="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-psc-muted">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="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wider text-psc-muted">Druckdaten</th>
<th className="py-4 px-4 text-xs font-bold uppercase tracking-wider text-[#EA641B] dark:text-orange-300"></th> <th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-psc-muted"></th>
</tr> </tr>
</thead> </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) => ( {positions.map((object, i) => (
<PosComponent <PosComponent
pos={object} pos={object}
@ -38,12 +38,12 @@ const ItemsComponent = ({positions, delPos, shop, changePos}) => {
</table> </table>
{positions.length === 0 && ( {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"> <div className="bg-slate-50 py-12 text-center">
<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"> <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" /> <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> </svg>
<p className="text-lg font-medium text-[#EA641B]">Keine Positionen vorhanden</p> <p className="text-lg font-medium text-psc-orange">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="mt-2 text-sm text-psc-muted">Fügen Sie Ihre erste Position hinzu</p>
</div> </div>
)} )}
</div> </div>

View File

@ -1,11 +1,11 @@
import React, {useEffect, useState} from 'react' import React, {useEffect, useState} from 'react'
import * as PropTypes from "prop-types" import * as PropTypes from "prop-types"
import {Pos} from "../../model/pos" import {Pos} from "../../model/pos"
import Button from '../base/Button' import Button, { ButtonGroup } from '../base/Button'
import EditPositionComponent from './EditPositionComponent' import EditPositionComponent from './EditPositionComponent'
import { Shop } from '../../model/shop' import { Shop } from '../../model/shop'
import Currency from '../base/Currency' import Currency from '../base/Currency'
import { Button as FlowbiteButton, Spinner } from "flowbite-react" import Spinner from '../base/Spinner'
import ProductService from '../../services/product' import ProductService from '../../services/product'
import {UploadOption} from '../../model/uploadOption' import {UploadOption} from '../../model/uploadOption'
import UploadOptionModal from './UploadOptionModal' import UploadOptionModal from './UploadOptionModal'
@ -33,28 +33,28 @@ const PosComponent = ({index, pos, delPos, changePos, shop}) => {
} }
return ( return (
<tr className="hover:bg-orange-50/50 dark:hover:bg-orange-950/30 transition-all duration-150"> <tr className="transition-colors hover:bg-orange-50/50">
<td className="py-4 px-4"> <td className="px-4 py-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"> <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} {index + 1}
</span> </span>
</td> </td>
<td className="py-4 px-4 text-sm text-gray-900 dark:text-gray-100 font-medium">{pos.product.title}</td> <td className="px-4 py-4 text-sm font-medium text-psc-text">{pos.product.title}</td>
<td className="py-4 px-4"> <td className="px-4 py-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"> <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 {pos.count}x
</span> </span>
</td> </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} /> <Currency price={pos.price.allNet} />
</td> </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} /> <Currency price={pos.price.allVat} />
</td> </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} /> <Currency price={pos.price.allGross} />
</td> </td>
<td className="py-4 px-4 text-sm text-right"> <td className="px-4 py-4 text-right text-sm">
{loadingOption ? ( {loadingOption ? (
<div className="flex justify-end"> <div className="flex justify-end">
<Spinner size="sm" /> <Spinner size="sm" />
@ -65,10 +65,10 @@ const PosComponent = ({index, pos, delPos, changePos, shop}) => {
</td> </td>
<td className="py-4 px-4 text-sm text-right"> <td className="py-4 px-4 text-sm text-right">
<div className="flex justify-end"> <div className="flex justify-end">
<FlowbiteButton.Group> <ButtonGroup>
<EditPositionComponent shop={shop} position={pos} changePos={changePos} /> <EditPositionComponent shop={shop} position={pos} changePos={changePos} />
<Button type={5} variant={'failure'} onClick={() => deletePos(pos.uuid)} className="!rounded-l-none" /> <Button type={5} variant={'failure'} onClick={() => deletePos(pos.uuid)} className="!rounded-l-none" />
</FlowbiteButton.Group> </ButtonGroup>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -8,6 +8,7 @@ import {Pos} from "../../model/pos"
import Order from "../../model/order" import Order from "../../model/order"
import OrderState from "../../state/order" import OrderState from "../../state/order"
import {container} from "tsyringe-neo" import {container} from "tsyringe-neo"
import Box from "../base/Box"
const PositionsComponent = ({order, shop, updateOrder}) => { const PositionsComponent = ({order, shop, updateOrder}) => {
const orderState = container.resolve(OrderState) const orderState = container.resolve(OrderState)
@ -43,18 +44,18 @@ const PositionsComponent = ({order, shop, updateOrder}) => {
}, [positions]) }, [positions])
return ( 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"> <Box
<h2 className="text-lg font-semibold mb-3 text-gray-800 dark:text-gray-100 flex items-center gap-2"> title="Positionen"
<svg className="w-6 h-6 text-[#EA641B]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> subtitle="Zentraler Arbeitsbereich für Artikel, Mengen und Druckdaten."
<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" /> 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> </svg>
Positionen }
</h2> action={<AddPositionComponent shop={shop} addPos={addPos} />}
>
<ItemsComponent positions={positions} shop={shop} delPos={delPos} changePos={changePos}/> <ItemsComponent positions={positions} shop={shop} delPos={delPos} changePos={changePos}/>
<div className="mt-4"> </Box>
<AddPositionComponent shop={shop} addPos={addPos}/>
</div>
</div>
) )
} }

View File

@ -1,6 +1,6 @@
import React, {useState} from 'react' import React, {useState} from 'react'
import {createPortal} from 'react-dom' import {createPortal} from 'react-dom'
import {Button as FlowbiteButton} from 'flowbite-react' import Button from '../base/Button'
import {UploadOption} from '../../model/uploadOption' import {UploadOption} from '../../model/uploadOption'
import {UploadFile} from '../../model/upload' import {UploadFile} from '../../model/upload'
import {Pos} from '../../model/pos' import {Pos} from '../../model/pos'
@ -38,9 +38,9 @@ const UploadOptionModal = ({option, pos}: {option: UploadOption, pos: Pos}) => {
return ( 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} {option.label}
</FlowbiteButton> </Button>
{show && ( {show && (
<BigModal title={`${option.label}${pos.product.title}`} onClose={() => setShow(false)}> <BigModal title={`${option.label}${pos.product.title}`} onClose={() => setShow(false)}>

View File

@ -7,7 +7,7 @@ import {container} from "tsyringe-neo"
import OrderState from "../../state/order" import OrderState from "../../state/order"
import Order from "../../model/order" import Order from "../../model/order"
import {useEffect, useState} from "react" import {useEffect, useState} from "react"
import { SelectLabel } from '../base/SelectLabel' import Box from '../base/Box'
const ShippingComponent = ({shop, order}) => { const ShippingComponent = ({shop, order}) => {
@ -42,17 +42,18 @@ const ShippingComponent = ({shop, order}) => {
} }
return ( 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"> <Box
<h2 className="text-lg font-semibold mb-3 text-gray-800 dark:text-gray-100 flex items-center gap-2"> title="Versandart"
<svg className="w-6 h-6 text-[#EA641B]" fill="none" stroke="currentColor" viewBox="0 0 24 24"> subtitle="Versand oder Abholung"
<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" /> 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> </svg>
Versandart }
</h2> >
<AsyncPaginate <AsyncPaginate
defaultOptions defaultOptions
key={JSON.stringify(shopUuid)} key={JSON.stringify(shopUuid)}
components={{Control: SelectLabel}}
defaultValue={order.shipping} defaultValue={order.shipping}
placeholder='' placeholder=''
name='Versandart' name='Versandart'
@ -61,9 +62,9 @@ const ShippingComponent = ({shop, order}) => {
getOptionLabel={(option) => option.title} getOptionLabel={(option) => option.title}
onChange={onChange} onChange={onChange}
isSearchable={false} isSearchable={false}
className={`${shipping == 0 ? "border-2 border-red-500 rounded" : ""}`} className={`${shipping == 0 ? "border border-red-400 rounded-sm" : ""}`}
/> />
</div> </Box>
) )
} }

View File

@ -2,7 +2,7 @@ import { AsyncPaginate } from "react-select-async-paginate"
import ShopService from '../../services/shop' import ShopService from '../../services/shop'
import { container } from 'tsyringe-neo' import { container } from 'tsyringe-neo'
import OrderState from '../../state/order' import OrderState from '../../state/order'
import { SelectLabel } from '../base/SelectLabel' import FieldLabel from '../base/FieldLabel'
import React from 'react' import React from 'react'
const ShopSelectComponent = (props) => { const ShopSelectComponent = (props) => {
@ -28,16 +28,16 @@ const ShopSelectComponent = (props) => {
return ( return (
<div> <div>
<FieldLabel>Shop</FieldLabel>
<AsyncPaginate <AsyncPaginate
defaultOptions defaultOptions
value={props.shop} value={props.shop}
components={{ Control: SelectLabel }}
loadOptions={loadOptions} loadOptions={loadOptions}
getOptionValue={(option) => option.id} getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.name}
onChange={onChange} onChange={onChange}
isSearchable={false} 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' name='Shop'
/> />

View File

@ -2,10 +2,10 @@ import Select from 'react-select'
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import { container } from 'tsyringe-neo' import { container } from 'tsyringe-neo'
import OrderState from '../../state/order' import OrderState from '../../state/order'
import { SelectLabel } from '../base/SelectLabel' import FieldLabel from '../base/FieldLabel'
import React from 'react' import React from 'react'
const TypeSelectComponent = ({order}) => { const TypeSelectComponent = ({order, onChange: onChangeProp}) => {
const [type, setType] = useState(0); const [type, setType] = useState(0);
const orderState: OrderState = container.resolve(OrderState); const orderState: OrderState = container.resolve(OrderState);
@ -25,6 +25,7 @@ const TypeSelectComponent = ({order}) => {
order.type = option.value; order.type = option.value;
orderState.setCurrentOrder(order); orderState.setCurrentOrder(order);
setType(option); setType(option);
if (onChangeProp) onChangeProp(option.value);
}; };
useEffect(() => { useEffect(() => {
@ -33,14 +34,13 @@ const TypeSelectComponent = ({order}) => {
return ( return (
<div> <div>
<FieldLabel>Typ</FieldLabel>
<Select <Select
className={`${type == 0 ? "border border-red-400 rounded-sm" : ""}`}
className={`${type == 0 ? "border border-danger" : ""}`}
options={options} options={options}
defaultValue={options.filter((e) => e.value == type).pop()} defaultValue={options.filter((e) => e.value == type).pop()}
value={options.filter((e) => e.value == type).pop()} value={options.filter((e) => e.value == type).pop()}
onChange={onChange} onChange={onChange}
components={{ Control: SelectLabel }}
placeholder="" placeholder=""
name="Typ" name="Typ"
/> />

View File

@ -1,5 +1,6 @@
import React, {useRef, useState} from 'react' 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 {UploadOptionRendererProps, registerUploadOptionRenderer} from '../../lib/uploadOptionRegistry'
import UploadService from '../../services/upload' import UploadService from '../../services/upload'
@ -53,7 +54,7 @@ const FileUploadRenderer = ({pos, files, onUploaded}: UploadOptionRendererProps)
<div> <div>
<input ref={fileInputRef} type="file" className="hidden" onChange={handleFileChange} /> <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 ? ( {uploading ? (
<><Spinner size="sm" className="mr-2" />Wird hochgeladen</> <><Spinner size="sm" className="mr-2" />Wird hochgeladen</>
) : ( ) : (
@ -64,7 +65,7 @@ const FileUploadRenderer = ({pos, files, onUploaded}: UploadOptionRendererProps)
Datei hochladen Datei hochladen
</> </>
)} )}
</FlowbiteButton> </Button>
</div> </div>
</div> </div>
) )

View File

@ -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()],
}

View File

@ -1,11 +1,13 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import deno from '@deno/vite-plugin' import deno from '@deno/vite-plugin'
import react from '@vitejs/plugin-react-swc' import react from '@vitejs/plugin-react-swc'
import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
deno(), deno(),
tailwindcss(),
react({ react({
tsDecorators: true tsDecorators: true
}) })

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,14 +1,20 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="de">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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> <script type="module" crossorigin src="/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index.css"> <link rel="stylesheet" crossorigin href="/assets/index.css">
</head> </head>
<body> <body class="bg-psc-bg font-sans text-psc-text">
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

View File

@ -13,7 +13,9 @@
{% endblock %} {% endblock %}
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<!-- GOOGLE FONT --> <!-- 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> </head>
<body> <body>
<main id="root"></main> <main id="root"></main>

View File

@ -926,10 +926,6 @@ html {
grid-column: span 3 / span 3; grid-column: span 3 / span 3;
} }
.col-span-4{
grid-column: span 4 / span 4;
}
.col-span-5{ .col-span-5{
grid-column: span 5 / span 5; grid-column: span 5 / span 5;
} }

View File

@ -7,10 +7,18 @@ changelog:
datum: 24.06.2026 datum: 24.06.2026
changes: changes:
- "Zwei-Faktor-Authentifizierung (2FA) fürs Backend optional pro Benutzer" - "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)" - "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" - "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 - version: 2.3.7
datum: 23.06.2026 datum: 23.06.2026
changes: changes: