Compare commits

...

143 Commits

Author SHA1 Message Date
Andreas Schickinger
1aa9ffec5d #4082 Sidenav Klickbereich erhöht 2023-06-09 10:58:31 +02:00
Nino Righi
d86f595b1f Merged PR 1555: #4074 Implemented Changes from ISA-Integration to ISA-Test
#4074 Implemented Changes from ISA-Integration to ISA-Test
2023-06-07 13:30:14 +00:00
Nino Righi
74bf2133c6 Merged PR 1554: #4078 Fix Searchbox Clear
#4078 Fix Searchbox Clear
2023-06-06 16:07:58 +00:00
Nino Righi
e4570946c4 Merged PR 1551: #4088 Responsive Design Fixed Width of Filter Overlays
#4088 Responsive Design Fixed Width of Filter Overlays
2023-06-06 15:33:02 +00:00
Andreas Schickinger
a8213d79fd Merged PR 1552: #4087 Warenausgabe/Abholfach - Label Breite angepasst
#4087 Warenausgabe/Abholfach - Label Breite angepasst

Related work items: #4087
2023-06-06 15:23:46 +00:00
Nino Righi
13ec323ac4 Merged PR 1550: #4083 Develop Goods In Out Order Edit Bestellkanal orderSource from Order not...
#4083 Develop Goods In Out Order Edit Bestellkanal orderSource from Order not from first item
2023-06-06 14:58:02 +00:00
Andreas Schickinger
c544cebba9 Merged PR 1549: #4082 Navi Dropdown Pfeil
#4082 Navi Dropdown Pfeil

Related work items: #4082
2023-06-06 14:52:54 +00:00
Andreas Schickinger
74dffe8af2 Merged PR 1547: #4081 Vorgangsleiste scrollen
#4081 Vorgangsleiste scrollen

Related work items: #4081
2023-06-06 14:46:57 +00:00
Nino Righi
9d3bb9dcf3 Merged PR 1546: #4080 Responsive Design Article Search Filter Navigation
#4080 Responsive Design Article Search Filter Navigation
2023-06-06 09:39:53 +00:00
Nino Righi
05e58aa060 Merged PR 1545: #4076 RD Navigation Wording Change based on Email Johanna
#4076 RD Navigation Wording Change based on Email Johanna
2023-06-05 15:01:32 +00:00
Andreas Schickinger
f74d14d573 Merged PR 1542: #4075 TK zum Kalender Button Breite angepasst
#4075 TK zum Kalender Button Breite angepasst

Related work items: #4075
2023-06-05 14:25:29 +00:00
Andreas Schickinger
741e651a20 Merged PR 1543: #4079 Kaufoptionen Popup Text überlappt
#4079 Kaufoptionen Popup Text überlappt

Related work items: #4079
2023-06-05 14:24:49 +00:00
Andreas Schickinger
4e1bd89378 Merged PR 1541: #4073 Fix PP Filter wird zurückgesetzt, wenn nicht vollständig geladen
#4073 Fix PP Filter wird zurückgesetzt, wenn nicht vollständig geladen

Related work items: #4073
2023-06-02 14:35:06 +00:00
Andreas Schickinger
503f44891f Merged PR 1538: #4050 Fix Kaufoptionen Popup - Weiterleiten zur Kundensuche
#4050 Weiterleiten zur Kundensuche fix

Related work items: #4050
2023-06-02 11:24:02 +00:00
Andreas Schickinger
5bf326f680 Merged PR 1535: #4068 Fix Develop Warenkorb Rücklage Menge ändern
Related work items: #4068
2023-06-01 15:42:32 +00:00
Lorenz Hilpert
e7793b15e3 Bugfix Shell und UiSvgIcon 2023-05-31 16:36:18 +02:00
Lorenz Hilpert
d2e16ca256 Update Navigation behaviour for Filial-Navigation 2023-05-31 15:58:19 +02:00
Nino Righi
31164befc9 Merged PR 1534: #4066 Responsive Design Article Search Filter Scroll Arrow Clickable and adju...
#4066 Responsive Design Article Search Filter Scroll Arrow Clickable and adjusted Styling
2023-05-31 13:35:34 +00:00
Nino Righi
8f4dfa0674 Merged PR 1533: #4065 Responsive Design Result List Correctly Render Items
#4065 Responsive Design Result List Correctly Render Items
2023-05-31 13:35:23 +00:00
Nino Righi
eb7a01907a Merged PR 1532: #4064 Responsive Design Article Result List new Grid Layout
#4064 Responsive Design Article Result List new Grid Layout
2023-05-31 13:32:48 +00:00
Nino Righi
fb1fd1ec7c Merged PR 1531: #4063 Fix Splitscreen Correctly Remove Details Breadcrumb inside Search Resul...
#4063 Fix Splitscreen Correctly Remove Details Breadcrumb inside Search Results Component
2023-05-31 13:29:27 +00:00
Nino Righi
7528c7df63 Merged PR 1530: #4062 Responsive Design Artikelsuche Filter Closing and Routing updated
#4062 Responsive Design Artikelsuche Filter Closing and Routing updated
2023-05-31 13:29:11 +00:00
Lorenz Hilpert
e6ca19ab91 Update Customer Header and Navigation 2023-05-31 13:44:59 +02:00
Lorenz Hilpert
dc3e097dfd Unit Test Anpassungen 2023-05-30 11:17:29 +02:00
Lorenz Hilpert
a5e8c06dda Update Funktionalität alle Processe Schließen 2023-05-30 11:06:30 +02:00
Lorenz Hilpert
bf7fd13ef2 Unit Test Fix 2023-05-26 18:06:35 +02:00
Lorenz Hilpert
a424e015b4 Prozess Tab font Anpassung 2023-05-26 17:17:32 +02:00
Lorenz Hilpert
c67fef64fe Styling anpassung prozess item 2023-05-26 17:15:26 +02:00
Lorenz Hilpert
12055de1fc Added Dashboard Icon 2023-05-26 16:11:54 +02:00
Lorenz Hilpert
a2ad2f8c0b Shell Prozess Tab 2023-05-26 16:01:42 +02:00
Lorenz Hilpert
af7cebda66 Unit Test Fix 2023-05-26 11:23:50 +02:00
Lorenz Hilpert
2230cf2e7b Header Anpassungen 2023-05-26 11:15:59 +02:00
Lorenz Hilpert
aa048e8d22 Added missing Icons 2023-05-25 16:36:45 +02:00
Lorenz Hilpert
4961fb9756 Fix Unit Sests ShellSideMenuComponent 2023-05-25 16:12:08 +02:00
Lorenz Hilpert
6801e3858a #3007 Side Navigation 2023-05-25 16:02:05 +02:00
Nino Righi
266358f0cc Merged PR 1528: Responsive Design Article Search
Responsive Design Article Search
2023-05-24 16:33:56 +00:00
Lorenz Hilpert
6717f0ee3d #3984 - Filtereinstellungen wenn Artikel für Dig-Versand in Warenkorb sind falsch 2023-05-24 18:21:30 +02:00
Michael Auer
8880ed0df6 Merged PR 1529: Floating Docker Branch-Tags
Es wird zusätzlich zu den bisherigen Docker-Tags, ein Docker-Tag vergeben, das den aktuellen Branch repräsentiert. Da dieses (identische) Tag für jeden neuen Build aus einem Branch wieder vergeben wird, wandert das Tag auf das neueste Docker-Image aus diesem Branch (vgl. Tag _latest_)

Beispiele:
develop => refs_head_develop
release/2.3 => refs_head_release_2.3
2023-05-24 16:12:28 +00:00
Lorenz Hilpert
395fd544e5 #4048 Bestellkanal - Umstellung auf orderSource 2023-05-22 15:44:06 +02:00
Lorenz Hilpert
2c10d6bf10 Kundenbestellung navigation Fix 2023-05-22 14:07:45 +02:00
Lorenz Hilpert
4dc98f7980 Shell Styling 2023-05-11 16:42:37 +02:00
Lorenz Hilpert
c89ee18db3 Update env service added matcher for screen sizes 2023-05-11 16:10:05 +02:00
Lorenz Hilpert
c4aa7999a6 Shell auf max breite 1440px und zentriert 2023-05-11 15:41:09 +02:00
Lorenz Hilpert
696015b6a4 Styling for Searchbox 2023-04-27 18:08:18 +02:00
Lorenz Hilpert
f2f70e1d83 Searchbox Update 2023-04-27 17:14:49 +02:00
Lorenz Hilpert
4d1dbaa2f3 Moved searchbox and filter to shared 2023-04-27 14:26:50 +02:00
Lorenz Hilpert
595bb27d99 Shell Max Content Width 2023-04-26 10:42:15 +02:00
Lorenz Hilpert
7b72532c9e Added Config - Missing Icons 2023-04-24 16:31:25 +02:00
Lorenz Hilpert
6b756fe893 Merge branch 'release/2.3' into develop 2023-04-24 15:35:34 +02:00
Lorenz Hilpert
54c7f51766 #3008 - Fix Process Tab Close 2023-04-24 15:34:29 +02:00
Lorenz Hilpert
94b787655e #3008 Navigation durch Bereich wechseln CTA 2023-04-24 15:29:58 +02:00
Lorenz Hilpert
259d0f1648 #3999 Kaufoptionen - Anzeigefehler in Titel/Format 2023-04-24 15:19:52 +02:00
Lorenz Hilpert
872d3ff383 #3391 - Filiale mit Klick auswählen 2023-04-24 14:57:16 +02:00
Lorenz Hilpert
cf1c4d37b9 Update Package Lock 2023-04-24 14:45:53 +02:00
Lorenz Hilpert
ada16bac6c Merge branch 'develop' into feature/rd-navigation-shell 2023-04-24 14:35:31 +02:00
Michael Auer
d68055f8a9 ~ Version Bump: 2.3 2023-04-24 11:52:12 +02:00
Michael Auer
8d98dcf7e7 Merge tag '2.2' into develop 2023-04-24 11:16:13 +02:00
Michael Auer
c828a69f66 Merge branch 'release/2.2' 2023-04-24 11:16:04 +02:00
Lorenz Hilpert
eefb6062c7 removed fdescribed 2023-04-21 18:49:57 +02:00
Lorenz Hilpert
470a451168 Unit Tests for ShellSideMenuComponent 2023-04-21 18:49:37 +02:00
Lorenz Hilpert
e3b018c5f7 remove fdescribe 2023-04-21 17:45:12 +02:00
Lorenz Hilpert
bd695e21d4 RD Navigation Unit Tests - ProcessBar 2023-04-21 17:44:49 +02:00
Lorenz Hilpert
6311ebe467 Show Navigation for Package-inspection 2023-04-21 10:33:34 +02:00
Lorenz Hilpert
f8a5ceed97 #3391 Fix - Tablet Kaufoptionen PopUp 2023-04-20 13:55:21 +02:00
Lorenz Hilpert
0420bda5da Bugfix - Default Branch in Kaufoptionen 2023-04-20 11:07:45 +02:00
Lorenz Hilpert
6dc532f40e Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2023-04-20 08:31:50 +02:00
Lorenz Hilpert
0560d01f30 Merge branch 'release/2.2' into develop 2023-04-20 08:31:42 +02:00
Lorenz Hilpert
bc67ec3287 URL Change paragon-data -> paragon-systems.de 2023-04-20 08:28:54 +02:00
Lorenz Hilpert
bed35a2377 URL Change wws 2023-04-20 08:10:45 +02:00
Lorenz Hilpert
aaf156cee3 Merge branch 'develop' into feature/rd-navigation-shell 2023-04-18 14:11:34 +02:00
Lorenz Hilpert
e8020ffde6 RD Shell - Navigation 2023-04-18 14:09:36 +02:00
Nino Righi
8d6bd80902 Merged PR 1522: #3989 Hotfix Remission List Item Loading Spinner
#3989 Hotfix Remission List Item Loading Spinner
2023-04-18 11:24:24 +00:00
Nino Righi
8aa870dddd Merged PR 1521: #3506 Fix Removed PackageCode from Payload of ReturnFinalizeReceipt
#3506 Fix Removed PackageCode from Payload of ReturnFinalizeReceipt
2023-04-18 08:44:40 +00:00
Nino Righi
26a3c76d5f Merged PR 1520: #3906 Fix Instock Bug Article Search Results
#3906 Fix Instock Bug Article Search Results
2023-04-17 12:25:29 +00:00
Lorenz Hilpert
18f738f2c3 Bereich Wareneingang aus Navigation entfernt 2023-04-17 11:07:50 +02:00
Lorenz Hilpert
bba50ccbcc Merge branch 'release/2.2' into develop 2023-04-13 14:11:53 +02:00
Lorenz Hilpert
3eea3b913d #3688 PopUp mit Irrläufern - Navigation disabled 2023-04-13 14:10:48 +02:00
Lorenz Hilpert
2dbeec831e #3976 nur ein Titel auf einmal remittieren 2023-04-13 14:00:09 +02:00
Lorenz Hilpert
88a06628e3 #3973 added missing tabindex 2023-04-12 18:24:07 +02:00
Lorenz Hilpert
37ceb30ffb #3973 Bedienung per Tastatur funktioniert fehlerhaft 2023-04-12 13:23:48 +02:00
Lorenz Hilpert
ebd0515e96 #3978 instock-Anfrage wird nicht immer ausgeführt 2023-04-11 16:41:00 +02:00
Nino Righi
ece5d0fa0d Merged PR 1519: #3957 Fix Develop Branch Selector Implementation with new purchasing options...
#3957 Fix Develop Branch Selector Implementation with new purchasing options modal
2023-04-11 14:26:23 +00:00
Lorenz Hilpert
6be214c6cd #3977 Fehlermeldung erscheint an falscher Stelle 2023-04-11 16:25:23 +02:00
Lorenz Hilpert
c470453ea4 Merge branch 'release/2.2' into develop 2023-04-05 11:58:29 +02:00
Nino Righi
bbe9326954 Merged PR 1518: #3957 Filter Current Branch if Role is Store
#3957 Filter Current Branch if Role is Store
2023-04-05 09:55:41 +00:00
Lorenz Hilpert
27961bb4e5 Checkbox Styling Kaufoptionen 2023-04-05 11:09:41 +02:00
Lorenz Hilpert
4952c090ef #3975 Anzahl der Irrläufer in der Liste auf 10 gesetzt 2023-04-05 11:06:17 +02:00
Lorenz Hilpert
74e4016625 #3928 Kaufoptionen - Filialen die IS_ORDERING_ENABLED true sind können ausgewählt werden 2023-04-03 13:14:58 +02:00
Lorenz Hilpert
c389008811 #3967 Fix - Nicht bestellbaren Artikel löschen führt zu Fehlermeldung 2023-04-03 10:21:21 +02:00
Nino Righi
e6dcf22012 Merged PR 1516: #3957 Filialdropdown Sortierung mit Store Rolle
#3957 Filialdropdown Sortierung mit Store Rolle
2023-03-31 15:14:30 +00:00
Nino Righi
d067f925b9 Merged PR 1517: #3927 #3929 #3964 Remission Wannennummer Changes
#3927 #3929 #3964 Remission Wannennummer Changes
2023-03-31 15:05:21 +00:00
Lorenz Hilpert
8b5609f765 #3672 Detailseite 'Falsche Filiale' - Erstes Item wird bei einem Irrläufer auch verblasst angezeigt 2023-03-31 14:08:52 +02:00
Lorenz Hilpert
dd04a1f2af #3963 Fix Falsche Verfügbarkeiten 2023-03-31 14:05:15 +02:00
Lorenz Hilpert
9caabb6cc0 #3960 Buttons Alle auswählen and Alle abwählen will now work correctly. 2023-03-31 13:34:00 +02:00
Nino Righi
33b28d5f41 Merged PR 1515: #3244 Remission Removed "Are you sure you want to remit" - Popup
#3244 Remission Removed "Are you sure you want to remit" - Popup
2023-03-31 11:21:28 +00:00
Lorenz Hilpert
475f9b5e34 Mark PurchaseOptionsModalComponent Fetching if Availabilities are being fetched 2023-03-31 11:29:42 +02:00
Lorenz Hilpert
60f1348ea5 Stock Request Verhindern 2023-03-31 11:10:46 +02:00
Lorenz Hilpert
533b6e1fcf Unit Test Fix 2023-03-31 10:21:38 +02:00
Lorenz Hilpert
8961730b74 #3672 Irrlauefer Details Anpassung 2023-03-30 18:26:51 +02:00
Lorenz Hilpert
5d580714c8 #3953 Minusbestände als Nullbestand anzeigt 2023-03-30 18:08:05 +02:00
Lorenz Hilpert
daf1ead75b #3962 Anrede ist nicht mehr Pflichtfeld 2023-03-30 14:24:12 +02:00
Lorenz Hilpert
aef2654a39 #3962 Anrede ist nicht mehr Pflichtfeld 2023-03-30 13:41:22 +02:00
Lorenz Hilpert
8243cd3528 #3947 PopUp wird nach unten nicht vollständig angezeigt 2023-03-30 11:41:37 +02:00
Lorenz Hilpert
447456d7a6 #3946 Anzeige lieferbare Menge nur in Popup 2023-03-30 11:22:07 +02:00
Lorenz Hilpert
241a34d7a8 Merge branch 'release/2.2' into develop 2023-03-30 11:04:04 +02:00
Lorenz Hilpert
4e67b2e8b9 #3936 - Kundenbestellung Leere Edit Seite
(cherry picked from commit 83406277ad)
2023-03-30 10:54:41 +02:00
Lorenz Hilpert
8bc2ea8373 #3385 Preiseingabe Geschenkkarte 2023-03-30 10:39:40 +02:00
Nino Righi
00a6a113c8 Merged PR 1514: #3956 Hotfix PDP Wording Change Branch Availability
#3956 Hotfix PDP Wording Change Branch Availability
2023-03-30 08:35:25 +00:00
Nino Righi
dc04619128 #3948 Hotfix HSC Kundenbestellungen Details Header Anzeige 2023-03-29 15:12:35 +02:00
Lorenz Hilpert
e066da3762 #3386 Canadd Wurde fuer Download Artikel falsch hinterlegt 2023-03-29 11:54:19 +02:00
Lorenz Hilpert
ad96278956 #3688 List Styling angepasst 2023-03-29 10:29:32 +02:00
Lorenz Hilpert
83406277ad #3936 - Kundenbestellung Leere Edit Seite 2023-03-28 14:40:49 +02:00
Lorenz Hilpert
9e89348381 #3958 Wahl Rücklage Kaufoption wirft Fehler 2023-03-28 14:10:25 +02:00
Lorenz Hilpert
0fb7419598 #3385 HFI Gutschein 2023-03-27 11:19:09 +02:00
Lorenz Hilpert
1790298cb4 #3931 Löschen von Positionen in der Kaufoption 2023-03-24 11:28:53 +01:00
Lorenz Hilpert
ffe8e39c85 #3934 - Anzeige Message Wenn Keine Verfügbarkeiten Existieren 2023-03-24 11:20:14 +01:00
Lorenz Hilpert
67e0f4bd46 #3945 Reinfolge von verfügbar als-Daten ist nicht fest 2023-03-23 19:24:22 +01:00
Lorenz Hilpert
e7b3a58da3 #3946 Fehler Lieferbare Exemplare 2023-03-23 19:20:24 +01:00
Lorenz Hilpert
598f9f3777 #3385 HFI Gutschein Anzeige Kaufoptionen 2023-03-23 18:25:51 +01:00
Lorenz Hilpert
0a7dca2e12 Packstück Kontoller aktivieren 2023-03-23 10:09:48 +01:00
Lorenz Hilpert
4b342778df #3365 Navigation angepasst 2023-03-22 15:32:20 +01:00
Lorenz Hilpert
10e8fd904a #3933 2023-03-22 14:29:57 +01:00
Lorenz Hilpert
1de342fd3b Update Add To Shopping Cart from PDP
(cherry picked from commit 4a5dd23561fdec458447c19c93faa1654ab80c7c)
2023-03-21 11:21:35 +01:00
Lorenz Hilpert
f4c1c3dd7f Merged PR 1513: Kaufoptionen
Related work items: #3365, #3366, #3385, #3386, #3391
2023-03-20 17:11:53 +00:00
Nino Righi
80bfc59356 Merged PR 1500: #3506 Remission Wanne Scanner nach Start der Remission
#3506 Remission Wanne Scanner nach Start der Remission
2023-03-17 09:25:43 +00:00
Lorenz Hilpert
3796f3ed5f Authentication Config Update 2023-03-16 15:30:01 +01:00
Lorenz Hilpert
f2e03d22d8 Merge branch 'develop' into release/2.2 2023-03-16 15:28:36 +01:00
Nino Righi
ef967b66e8 Merged PR 1512: #3887 Removed Scrollbar until Responsive Design
#3887 Removed Scrollbar until Responsive Design
2023-03-16 13:59:47 +00:00
Lorenz Hilpert
58ea70cc6c Merge branch 'develop' into release/2.2 2023-03-15 17:42:51 +01:00
Nino Righi
e4823950df Merged PR 1511: #3887 HSC Details Kundenbestellungen 360 Grad
#3887 HSC Details Kundenbestellungen 360 Grad
2023-03-15 16:41:57 +00:00
Lorenz Hilpert
8f9923ba5d #3673 ArrivalStatus Prüfung 2023-03-14 18:22:17 +01:00
Lorenz Hilpert
72bbd2c36e Checkbox Styling with Sub Options 2023-03-14 14:15:07 +01:00
Lorenz Hilpert
9bdb902a56 #3673 #3905 2023-03-14 13:55:38 +01:00
Nino Righi
bbc2e55ae3 Merged PR 1510: #3878 Updated Section Toggle if Role CallCenter is Active
#3878 Updated Section Toggle if Role CallCenter is Active
2023-03-14 09:16:44 +00:00
Nino Righi
6995bdb527 Merged PR 1508: Filter Doku Readme angelegt mit 2 konkreten Anwendungsbeispielen der Filter I...
Filter Doku Readme angelegt mit 2 konkreten Anwendungsbeispielen der Filter IST Implementierung

Co-authored-by: lorenzh <lorenzh@users.noreply.github.com>
2023-03-13 15:43:59 +00:00
Lorenz Hilpert
772aed597b Wareneingang ausblenden 2023-03-10 08:05:37 +01:00
Lorenz Hilpert
9e18825c27 Merge branch 'develop' into release/2.2 2023-03-10 08:04:44 +01:00
Lorenz Hilpert
0882bc2ca7 Merge branch 'develop' into release/2.2 2023-03-08 13:15:49 +01:00
Lorenz Hilpert
d9a2601f75 Merge branch 'develop' into release/2.2 2023-03-08 11:37:49 +01:00
Lorenz Hilpert
37a04cadf8 Added scandit Lizenz 2023-03-08 11:03:59 +01:00
Lorenz Hilpert
b01ce5b3b6 Merge branch 'develop' into release/2.2 2023-03-02 15:52:30 +01:00
Lorenz Hilpert
c7e6f00ddb Merge branch 'release/2.2' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into release/2.2 2023-03-02 10:58:39 +01:00
Lorenz Hilpert
85831ffe5d Config Fix Statging - wws API 2023-03-02 10:58:26 +01:00
807 changed files with 17986 additions and 11314 deletions

View File

@@ -3,6 +3,5 @@
"johnpapa.angular2",
"esbenp.prettier-vscode",
"angular.ng-template",
"eg2.vscode-npm-script"
]
}

View File

@@ -375,37 +375,6 @@
}
}
},
"@shell/breadcrumb": {
"projectType": "library",
"root": "apps/shell/breadcrumb",
"sourceRoot": "apps/shell/breadcrumb/src",
"prefix": "shell",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/shell/breadcrumb/tsconfig.lib.json",
"project": "apps/shell/breadcrumb/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/breadcrumb/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/breadcrumb/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@domain/defs": {
"projectType": "library",
"root": "apps/domain/defs",
@@ -840,37 +809,6 @@
}
}
},
"@shell/header": {
"projectType": "library",
"root": "apps/shell/header",
"sourceRoot": "apps/shell/header/src",
"prefix": "shell",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/shell/header/tsconfig.lib.json",
"project": "apps/shell/header/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/header/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/header/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@modal/reorder": {
"projectType": "library",
"root": "apps/modal/reorder",
@@ -1058,74 +996,6 @@
}
}
},
"@shell/footer": {
"projectType": "library",
"root": "apps/shell/footer",
"sourceRoot": "apps/shell/footer/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/footer/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/footer/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/footer/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/footer/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@shell/process": {
"projectType": "library",
"root": "apps/shell/process",
"sourceRoot": "apps/shell/process/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/process/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/process/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/process/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/process/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@domain/isa": {
"projectType": "library",
"root": "apps/domain/isa",
@@ -1160,40 +1030,6 @@
}
}
},
"@shell/filter-overlay": {
"projectType": "library",
"root": "apps/shell/filter-overlay",
"sourceRoot": "apps/shell/filter-overlay/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/filter-overlay/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/filter-overlay/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/filter-overlay/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/filter-overlay/tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
},
"@store/search-component-store": {
"projectType": "library",
"root": "apps/store/search-component-store",
@@ -1634,6 +1470,42 @@
}
}
}
},
"shell": {
"projectType": "library",
"root": "apps/shell",
"sourceRoot": "apps/shell/src",
"prefix": "shell",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"tsConfig": "apps/shell/tsconfig.spec.json",
"polyfills": [
"zone.js",
"zone.js/testing"
]
}
}
}
}
},
"cli": {
"analytics": false
}
}

View File

@@ -1,5 +1,4 @@
import { Injectable } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { Store } from '@ngrx/store';
import { BranchDTO } from '@swagger/checkout';
import { isBoolean, isNumber } from '@utils/common';
@@ -16,19 +15,18 @@ import {
selectActivatedProcess,
patchProcess,
patchProcessData,
selectTitle,
setTitle,
} from './store';
@Injectable()
export class ApplicationService {
/** @deprecated */
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
/** @deprecated */
get activatedProcessId() {
return this.activatedProcessIdSubject.value;
}
/** @deprecated */
get activatedProcessId$() {
return this.activatedProcessIdSubject.asObservable();
}
@@ -48,6 +46,14 @@ export class ApplicationService {
return this.store.select(selectSection);
}
getTitle$() {
return this.getSection$().pipe(
map((section) => {
return section === 'customer' ? 'Kundenbereich' : 'Filialbereich';
})
);
}
/** @deprecated */
getActivatedProcessId$() {
return this.store.select(selectActivatedProcess).pipe(map((process) => process?.id));

View File

@@ -3,6 +3,8 @@ import { ApplicationProcess } from '..';
const prefix = '[CORE-APPLICATION]';
export const setTitle = createAction(`${prefix} Set Title`, props<{ title: string }>());
export const setSection = createAction(`${prefix} Set Section`, props<{ section: 'customer' | 'branch' }>());
export const addProcess = createAction(`${prefix} Add Process`, props<{ process: ApplicationProcess }>());

View File

@@ -1,9 +1,18 @@
import { Action, createReducer, on } from '@ngrx/store';
import { setSection, addProcess, removeProcess, setActivatedProcess, patchProcess, patchProcessData } from './application.actions';
import {
setSection,
addProcess,
removeProcess,
setActivatedProcess,
patchProcess,
patchProcessData,
setTitle,
} from './application.actions';
import { ApplicationState, INITIAL_APPLICATION_STATE } from './application.state';
const _applicationReducer = createReducer(
INITIAL_APPLICATION_STATE,
on(setTitle, (state, { title }) => ({ ...state, title })),
on(setSection, (state, { section }) => ({ ...state, section })),
on(addProcess, (state, { process }) => ({ ...state, processes: [...state.processes, { data: {}, ...process }] })),
on(removeProcess, (state, { processId }) => {

View File

@@ -2,6 +2,8 @@ import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ApplicationState } from './application.state';
export const selectApplicationState = createFeatureSelector<ApplicationState>('core-application');
export const selectTitle = createSelector(selectApplicationState, (s) => s.title);
export const selectSection = createSelector(selectApplicationState, (s) => s.section);
export const selectProcesses = createSelector(selectApplicationState, (s) => s.processes);

View File

@@ -1,11 +1,13 @@
import { ApplicationProcess } from '../defs';
export interface ApplicationState {
title: string;
processes: ApplicationProcess[];
section: 'customer' | 'branch';
}
export const INITIAL_APPLICATION_STATE: ApplicationState = {
title: '',
processes: [],
section: 'customer',
};

View File

@@ -135,9 +135,9 @@ export class BreadcrumbService {
crumbs.forEach((crumb) => this.removeBreadcrumb(crumb.id));
}
getLatestBreadcrumbForSection(section: 'customer' | 'branch') {
getLatestBreadcrumbForSection(section: 'customer' | 'branch', predicate: (crumb: Breadcrumb) => boolean = (_) => true) {
return this.store
.select(selectors.selectBreadcrumbsBySection, { section })
.pipe(map((crumbs) => crumbs.sort((a, b) => b.changed - a.changed).find((f) => true)));
.pipe(map((crumbs) => crumbs.sort((a, b) => b.changed - a.changed).find((f) => predicate(f))));
}
}

View File

@@ -22,7 +22,7 @@ export interface Breadcrumb {
/**
* Url
*/
path: string;
path: string | any[];
/**
* Query Parameter

View File

@@ -1,17 +1,52 @@
import { Injectable } from '@angular/core';
import { Platform } from '@angular/cdk/platform';
import { NativeContainerService } from 'native-container';
import { BreakpointObserver } from '@angular/cdk/layout';
const MATCH_TABLET = '(max-width: 1024px)';
const MATCH_DESKTOP_SMALL = '(min-width: 1025px) and (max-width: 1439px)';
const MATCH_DESKTOP = '(min-width: 1440px)';
@Injectable({
providedIn: 'root',
})
export class EnvironmentService {
constructor(private _platform: Platform, private _nativeContainer: NativeContainerService) {}
constructor(
private _platform: Platform,
private _nativeContainer: NativeContainerService,
private _breakpointObserver: BreakpointObserver
) {}
matchTablet(): boolean {
return this._breakpointObserver.isMatched(MATCH_TABLET);
}
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET);
matchDesktopSmall(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_SMALL);
}
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL);
matchDesktop(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP);
}
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP);
/**
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.
*/
isDesktop(): boolean {
return !this.isTablet();
}
/**
* @deprecated Use `matchTablet` instead.
*/
isTablet(): boolean {
return this.isNative() || this.isSafari();
}
@@ -21,6 +56,6 @@ export class EnvironmentService {
}
isSafari(): boolean {
return (this._platform.ANDROID || this._platform.IOS) && this._platform.SAFARI;
return this._platform.IOS && this._platform.SAFARI;
}
}

View File

@@ -3,9 +3,9 @@
<ui-icon icon="close" size="20px"></ui-icon>
</button>
<div class="toast-content flex flex-col justify-center items-center">
<h1 class="text-card-sub font-bold text-center py-3 whitespace-pre-wrap">{{ data.title }}</h1>
<h1 class="text-xl font-bold text-center py-3 whitespace-pre-wrap">{{ data.title }}</h1>
<ng-container *ngIf="data.text; else templateRef">
<p class="block text-base overflow-y-hidden pb-3 text-center overflow-x-hidden">{{ data.text }}</p>
<p class="block text-p2 overflow-y-hidden pb-3 text-center overflow-x-hidden">{{ data.text }}</p>
</ng-container>
</div>
</div>

View File

@@ -34,6 +34,11 @@ export class DomainAvailabilityService {
private _branchService: StoreCheckoutBranchService
) {}
@memorize({ ttl: 10000 })
memorizedAvailabilityShippingAvailability(request: Array<AvailabilityRequestDTO>) {
return this._availabilityService.AvailabilityShippingAvailability(request).pipe(shareReplay(1));
}
@memorize()
getSuppliers(): Observable<SupplierDTO[]> {
return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
@@ -160,7 +165,7 @@ export class DomainAvailabilityService {
),
map(([response, supplier, defaultBranch]) => {
const price = item?.price;
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch.id ?? defaultBranch.id, quantity, price });
return this._mapToTakeAwayAvailability({ response, supplier, branchId: branch?.id ?? defaultBranch?.id, quantity, price });
}),
shareReplay(1)
);
@@ -249,57 +254,53 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
return this._availabilityService
.AvailabilityShippingAvailability([
{
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
qty: quantity,
},
])
.pipe(
timeout(5000),
map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
shareReplay(1)
);
return this.memorizedAvailabilityShippingAvailability([
{
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
qty: quantity,
},
]).pipe(
timeout(5000),
map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
shareReplay(1)
);
}
@memorize({ ttl: 10000 })
getDigDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
return this._availabilityService
.AvailabilityShippingAvailability([
{
qty: quantity,
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
},
])
.pipe(
timeout(5000),
map((r) => {
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
return this.memorizedAvailabilityShippingAvailability([
{
qty: quantity,
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
},
]).pipe(
timeout(5000),
map((r) => {
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
estimatedDelivery: preferred?.estimatedDelivery,
price: preferred?.price,
logistician: { id: preferred?.logisticianId },
supplierProductNumber: preferred?.supplierProductNumber,
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
};
return availability;
}),
shareReplay(1)
);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
estimatedDelivery: preferred?.estimatedDelivery,
price: preferred?.price,
logistician: { id: preferred?.logisticianId },
supplierProductNumber: preferred?.supplierProductNumber,
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
};
return availability;
}),
shareReplay(1)
);
}
@memorize({ ttl: 10000 })
@@ -333,37 +334,35 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDownloadAvailability({ item }: { item: ItemData }): Observable<AvailabilityDTO> {
return this._availabilityService
.AvailabilityShippingAvailability([
{
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
qty: 1,
},
])
.pipe(
map((r) => {
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
return this.memorizedAvailabilityShippingAvailability([
{
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
qty: 1,
},
]).pipe(
map((r) => {
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
price: preferred?.price,
supplierProductNumber: preferred?.supplierProductNumber,
logistician: { id: preferred?.logisticianId },
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
};
return availability;
}),
shareReplay(1)
);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
price: preferred?.price,
supplierProductNumber: preferred?.supplierProductNumber,
logistician: { id: preferred?.logisticianId },
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
};
return availability;
}),
shareReplay(1)
);
}
@memorize({ ttl: 10000 })
@@ -401,7 +400,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe(
return this.memorizedAvailabilityShippingAvailability(payload).pipe(
timeout(20000),
map((response) => this._mapToShippingAvailability(response.result))
);
@@ -409,7 +408,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe(
return this.memorizedAvailabilityShippingAvailability(payload).pipe(
timeout(20000),
map((response) => this._mapToShippingAvailability(response.result))
);
@@ -447,6 +446,9 @@ export class DomainAvailabilityService {
}
isAvailable({ availability }: { availability: AvailabilityDTO }) {
if (availability?.supplier?.id === 16 && availability?.inStock == 0) {
return false;
}
return [2, 32, 256, 1024, 2048, 4096].some((code) => availability?.availabilityType === code);
}

View File

@@ -45,6 +45,8 @@ export class DomainInStockService {
const key = this.getKey({ itemId, branchId });
this._addToInStockQueue({ itemId, branchId });
let _previousValue: InStock;
const sub = combineLatest([this._inStockMap, this._inStockFetchingMap])
.pipe(distinctUntilChanged(isEqual))
.subscribe(([inStockMap, inStockFetchingMap]) => {
@@ -54,7 +56,12 @@ export class DomainInStockService {
inStock: inStockMap[key],
fetching: inStockFetchingMap[key] ?? false,
};
obs.next(inStock);
if (!isEqual(inStock, _previousValue)) {
obs.next(inStock);
}
_previousValue = inStock;
});
return () => {
sub.unsubscribe();

View File

@@ -26,6 +26,7 @@ import {
StoreCheckoutBuyerService,
StoreCheckoutPayerService,
StoreCheckoutBranchService,
ItemsResult,
} from '@swagger/checkout';
import { DisplayOrderDTO, DisplayOrderItemDTO, OrderCheckoutService, ReorderValues } from '@swagger/oms';
import { isNullOrUndefined, memorize } from '@utils/common';
@@ -198,7 +199,15 @@ export class DomainCheckoutService {
);
}
canAddItems({ processId, payload, orderType }: { processId: number; payload: ItemPayload[]; orderType: string }) {
canAddItems({
processId,
payload,
orderType,
}: {
processId: number;
payload: ItemPayload[];
orderType: string;
}): Observable<ItemsResult[]> {
return this.getShoppingCart({ processId }).pipe(
first(),
withLatestFrom(this.store.select(DomainCheckoutSelectors.selectCustomerFeaturesByProcessId, { processId })),
@@ -217,7 +226,8 @@ export class DomainCheckoutService {
})
.pipe(
map((response) => {
return response.result;
// TODO: remove this when the API is fixed
return (response.result as unknown) as ItemsResult[];
})
);
})

View File

@@ -1,6 +1,5 @@
import { Injectable } from '@angular/core';
import {
AutocompleteTokenDTO,
BranchService,
ChangeStockStatusCodeValues,
HistoryDTO,
@@ -11,9 +10,7 @@ import {
OrderItemSubsetDTO,
OrderListItemDTO,
OrderService,
QueryTokenDTO,
ReceiptService,
ResponseArgsOfIEnumerableOfHistoryDTO,
StatusValues,
StockStatusCodeService,
ValueTupleOfLongAndReceiptTypeAndEntityDTOContainerOfReceiptDTO,
@@ -22,7 +19,7 @@ import {
} from '@swagger/oms';
import { memorize } from '@utils/common';
import { Observable } from 'rxjs';
import { map, mergeMap, shareReplay } from 'rxjs/operators';
import { map, shareReplay } from 'rxjs/operators';
@Injectable()
export class DomainOmsService {
@@ -191,6 +188,10 @@ export class DomainOmsService {
);
}
getOrderSource(orderId: number): Observable<string> {
return this.getOrder(orderId).pipe(map((order) => order?.features?.orderSource));
}
updateNotifications(orderId: number, changes: { selected: NotificationChannel; email: string; mobile: string }) {
const communicationDetails = {
email: changes.email,

View File

@@ -447,7 +447,7 @@ export class DomainRemissionService {
* Create a new receipt for the given return/remission
* @param returnId Return ID
* @param receiptNumber Receipt number
* @returns ReturnDTO - ShippingDocument
* @returns ReceiptDTO
*/
async createReceipt(returnDTO: ReturnDTO, receiptNumber?: string): Promise<ReceiptDTO> {
const stock = await this._getStock();
@@ -471,14 +471,41 @@ export class DomainRemissionService {
return receipt;
}
async completeReceipt(returnId: number, receiptId: number, packageCode: string): Promise<ReceiptDTO> {
/**
* Create a new Package and assign it to a receipt
* @param returnId Return ID
* @param receiptId Receipt ID
* @param packageNumber Packagenumber
* @returns ReceiptDTO
*/
async createReceiptAndAssignPackage({
returnId,
receiptId,
packageNumber,
}: {
returnId: number;
receiptId: number;
packageNumber: string;
}): Promise<ReceiptDTO> {
const response = await this._returnService
.ReturnCreateAndAssignPackage({
returnId,
receiptId,
data: {
packageNumber,
},
})
.toPromise();
const receipt: ReceiptDTO = response.result;
return receipt;
}
async completeReceipt(returnId: number, receiptId: number): Promise<ReceiptDTO> {
const res = await this._returnService
.ReturnFinalizeReceipt({
returnId,
receiptId,
data: {
packageCode,
},
data: {},
})
.toPromise();

View File

@@ -1,6 +1,5 @@
import { isDevMode, NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DebugComponent } from './debug/debug.component';
import {
CanActivateCartGuard,
CanActivateCartWithProcessIdGuard,
@@ -17,9 +16,9 @@ import {
} from './guards';
import { CanActivateAssortmentGuard } from './guards/can-activate-assortment.guard';
import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
import { MainComponent } from './main.component';
import { PreviewComponent } from './preview';
import { BranchSectionResolver, CustomerSectionResolver, ProcessIdResolver } from './resolvers';
import { ShellComponent, ShellModule } from './shell';
import { TokenLoginComponent, TokenLoginModule } from './token-login';
const routes: Routes = [
@@ -40,7 +39,7 @@ const routes: Routes = [
children: [
{
path: 'kunde',
component: ShellComponent,
component: MainComponent,
children: [
{
path: 'dashboard',
@@ -106,7 +105,7 @@ const routes: Routes = [
},
{
path: 'filiale',
component: ShellComponent,
component: MainComponent,
children: [
{
path: 'task-calendar',
@@ -152,7 +151,7 @@ if (isDevMode()) {
}
@NgModule({
imports: [RouterModule.forRoot(routes), ShellModule, TokenLoginModule],
imports: [RouterModule.forRoot(routes), TokenLoginModule],
exports: [RouterModule],
})
export class AppRoutingModule {}

View File

@@ -1,7 +1,3 @@
:host {
@apply block box-border;
}
button {
@apply fixed bottom-4 right-2 bg-blue-500 text-white font-bold py-2 px-4 rounded z-tooltip;
@apply block;
}

View File

@@ -32,9 +32,11 @@ import { IsaErrorHandler } from './providers/isa.error-handler';
import { ScanAdapterModule, ScanAdapterService, ScanditScanAdapterModule } from '@adapter/scan';
import { RootStateService } from './store/root-state.service';
import * as Commands from './commands';
import { UiIconModule } from '@ui/icon';
import { UiIconModule, UI_ICON_CFG } from '@ui/icon';
import { PreviewComponent } from './preview';
import { NativeContainerService } from 'native-container';
import { ShellModule } from '@shared/shell';
import { MainComponent } from './main.component';
registerLocaleData(localeDe, localeDeExtra);
registerLocaleData(localeDe, 'de', localeDeExtra);
@@ -74,11 +76,12 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
}
@NgModule({
declarations: [AppComponent],
declarations: [AppComponent, MainComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
ShellModule.forRoot(),
AppRoutingModule,
AppSwaggerModule,
AppDomainModule,
@@ -103,12 +106,7 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
ScanAdapterModule.forRoot(),
ScanditScanAdapterModule.forRoot(),
PlatformModule,
UiIconModule.forRoot({
aliases: [
{ alias: 'd-account', name: 'account' },
{ alias: 'd-no-account', name: 'package-variant-closed' },
],
}),
UiIconModule.forRoot(),
],
providers: [
{
@@ -137,6 +135,11 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
useClass: IsaErrorHandler,
},
{ provide: LOCALE_ID, useValue: 'de-DE' },
{
provide: UI_ICON_CFG,
useFactory: (config: Config) => config.get('@ui/icon'),
deps: [Config],
},
],
bootstrap: [AppComponent],
})

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { ProductCatalogNavigationService } from '@shared/services';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
@@ -9,6 +10,7 @@ export class CanActivateProductGuard implements CanActivate {
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
private readonly _navigationService: ProductCatalogNavigationService,
private readonly _router: Router
) {}
@@ -38,7 +40,7 @@ export class CanActivateProductGuard implements CanActivate {
}
if (!lastActivatedProcessId) {
await this.fromCartProcess(processes, route);
await this.fromCartProcess(processes);
return false;
} else {
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)]));
@@ -48,7 +50,7 @@ export class CanActivateProductGuard implements CanActivate {
}
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Artikelsuche
async fromCartProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) {
async fromCartProcess(processes: ApplicationProcess[]) {
const newProcessId = Date.now();
await this._applicationService.createProcess({
id: newProcessId,
@@ -57,7 +59,7 @@ export class CanActivateProductGuard implements CanActivate {
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(newProcessId)]));
await this._navigationService.navigateToProductSearch({ processId: newProcessId });
}
// Bei offener Warenausgabe und Klick auf Footer Artikelsuche

View File

@@ -0,0 +1,3 @@
<shell-root>
<router-outlet></router-outlet>
</shell-root>

View File

@@ -0,0 +1,10 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-main',
templateUrl: 'main.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MainComponent {
constructor() {}
}

View File

@@ -1,4 +0,0 @@
// start:ng42.barrel
export * from './shell.component';
export * from './shell.module';
// end:ng42.barrel

View File

@@ -1,88 +0,0 @@
<div class="shell-header-wrapper">
<shell-header [section]="section$ | async" (sectionChange)="setSection($event)">
<a [routerLink]="['/kunde/dashboard']" routerLinkActive="active" class="dashboard-btn">
<ui-icon icon="dashboard" size="26px"></ui-icon>
</a>
<button class="notifications-btn" [disabled]="(notificationCount$ | async) === 0" (click)="openNotifications()">
<ui-icon icon="notification" size="26px"></ui-icon>
<span class="notification-counter" *ngIf="notificationCount$ | async; let count">{{ count }}</span>
</button>
<button (click)="logout()" class="logout-btn">
<span *ngIf="currentBranch$ | async; let currentBranch">{{ currentBranch.key | uppercase }}</span>
<ui-icon icon="logout" size="26px"></ui-icon>
</button>
</shell-header>
</div>
<div class="shell-process-wrapper">
<shell-process
[label]="addProcessLabel$ | async"
[canAddProcess]="canAddProcess$ | async"
(addProcess)="addProcess(); processTabs?.last?.triggerAnimation()"
>
<shell-process-tab
#processTabs
(activateProcess)="activateProcess($event)"
(closeProcess)="closeProcess($event)"
(processAction)="processAction($event)"
*ngFor="let process of processes$ | async; trackBy: trackByIdFn"
[isActive]="(activatedProcessId$ | async) === process.id"
[process]="process"
></shell-process-tab>
</shell-process>
</div>
<div class="main-wrapper">
<main>
<router-outlet></router-outlet>
</main>
</div>
<div class="shell-footer-wrapper">
<shell-footer *ngIf="section$ | async; let section">
<ng-container *ngIf="section === 'customer'">
<a [routerLink]="[customerBasePath$ | async, 'product']" routerLinkActive="active">
<ui-icon icon="catalog" size="30px"></ui-icon>
Artikelsuche
</a>
<a [routerLink]="[customerBasePath$ | async, 'customer']" routerLinkActive="active">
<ui-icon icon="customer" size="24px"></ui-icon>
Kundensuche
</a>
<a *ifRole="'Store'" [routerLink]="[customerBasePath$ | async, 'goods', 'out']" routerLinkActive="active">
<ui-icon icon="box_out" size="24px"></ui-icon>
Warenausgabe
</a>
<a *ifRole="'CallCenter'" [routerLink]="[customerBasePath$ | async, 'order']" routerLinkActive="active">
<ui-svg-icon icon="package-variant-closed" [size]="28"></ui-svg-icon>
Kundenbestellungen
</a>
</ng-container>
<ng-container *ngIf="section === 'branch'">
<a [routerLink]="['/filiale/assortment']" routerLinkActive="active">
<ui-svg-icon icon="shape-outline" [size]="24"></ui-svg-icon>
Sortiment
</a>
<a [routerLink]="['/filiale/task-calendar']" routerLinkActive="active">
<ui-icon icon="calendar_check" size="24px"></ui-icon>
Tätigkeitskalender
</a>
<a [routerLink]="['/filiale/goods/in']" routerLinkActive="active">
<ui-icon icon="box_return" size="24px"></ui-icon>
Abholfach
</a>
<a [routerLink]="[remissionUrl$ | async]" [queryParams]="remissionQueryParams$ | async" routerLinkActive="active">
<ui-icon icon="documents_refresh" size="24px"></ui-icon>
Remission
</a>
<a [routerLink]="['/filiale/package-inspection']" routerLinkActive="active" (click)="fetchAndOpenPackages()">
<ui-svg-icon icon="clipboard-check-outline" [size]="24"></ui-svg-icon>
Wareneingang
</a>
</ng-container>
</shell-footer>
</div>
<button *ngIf="isDevelopment" class="block absolute bottom-0 right-0 z-tooltip p-4 opacity-5" (click)="debugOpen = !debugOpen">
<ui-svg-icon icon="bug-outline"></ui-svg-icon>
</button>
<app-debug *ngIf="debugOpen" class="absolute inset-x-0 top-0 max-h-[calc(100vh-80px)]"></app-debug>

View File

@@ -1,60 +0,0 @@
:host {
@apply block relative min-h-screen;
}
.main-wrapper {
@apply fixed right-0 left-0 overflow-auto;
top: 8.375rem;
bottom: 5rem;
main {
@apply w-full max-w-content mx-auto px-4 self-stretch;
}
}
.shell-header-wrapper {
@apply fixed top-0 left-0 right-0 bg-white;
shell-header {
@apply w-full max-w-content mx-auto;
}
button.notifications-btn {
@apply relative;
.notification-counter {
@apply absolute flex items-center justify-center top-2 right-px-3 text-sm rounded-full w-6 h-6 font-semibold;
background-color: var(--shell-notification-counter-background);
color: var(--shell-notification-counter-text);
z-index: 10;
}
}
}
.shell-process-wrapper {
@apply fixed left-0 right-0 bg-white;
top: 5.125rem;
shell-process {
@apply w-full max-w-content mx-auto;
height: 52px;
}
}
shell-process {
height: 52px;
grid-area: process;
}
.shell-footer-wrapper {
@apply fixed bottom-0 left-0 right-0 bg-white z-fixed shadow-card;
shell-footer {
@apply w-full max-w-content mx-auto;
.active {
@apply font-bold;
color: var(--shell-footer-link-active);
}
}
}

View File

@@ -1,445 +0,0 @@
// unit test ShellComponent with Spectator
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { AuthModule, AuthService } from '@core/auth';
import { Config } from '@core/config';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainDashboardService } from '@domain/isa';
import { NotificationsHub } from '@hub/notifications';
import { ModalNotificationsComponent } from '@modal/notifications';
import { Spectator, createComponentFactory, SpyObject, createSpyObject } from '@ngneat/spectator';
import { DashboardComponent } from '@page/dashboard';
import { ShellFooterComponent } from '@shell/footer';
import { ShellHeaderComponent } from '@shell/header';
import { ShellProcessComponent, ShellProcessTabComponent } from '@shell/process';
import { IconRegistry, UiIconComponent, UiIconModule } from '@ui/icon';
import { UiModalService } from '@ui/modal';
import { EnvelopeDTO, MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
import { first } from 'rxjs/operators';
import { ShellComponent } from './shell.component';
import { WrongDestinationModalService } from 'apps/page/package-inspection/src/lib/components/wrong-destination-modal/wrong-destination-modal.service';
// DummyComponent Class
@Component({
selector: 'dummy-component',
template: '<div></div>',
})
class DummyComponent {
constructor() {}
}
describe('ShellComponent', () => {
let spectator: Spectator<ShellComponent>;
let applicationServiceMock: SpyObject<ApplicationService>;
let modalServiceMock: SpyObject<UiModalService>;
let notificationsHubMock: SpyObject<NotificationsHub>;
let router: Router;
let breadcrumbServiceMock: SpyObject<BreadcrumbService>;
let authServiceMock: SpyObject<AuthService>;
const createComponent = createComponentFactory({
component: ShellComponent,
imports: [
UiIconModule,
RouterTestingModule.withRoutes([
{ path: 'kunde', component: DummyComponent },
{ path: 'kunde/dashboard', component: DashboardComponent },
]),
AuthModule,
],
declarations: [
MockComponent(ShellHeaderComponent),
MockComponent(ShellFooterComponent),
MockComponent(ShellProcessComponent),
MockComponent(ShellProcessTabComponent),
],
mocks: [
BreadcrumbService,
DomainAvailabilityService,
AuthService,
DomainDashboardService,
Config,
WrongDestinationModalService,
IconRegistry,
],
});
beforeEach(() => {
applicationServiceMock = createSpyObject(ApplicationService);
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of([]));
applicationServiceMock.getProcessById$.and.returnValue(of({ id: 4000 }));
applicationServiceMock.getActivatedProcessId$.and.returnValue(of(undefined));
applicationServiceMock.getLastActivatedProcessWithSectionAndType$.and.returnValue(of({}));
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({}));
notificationsHubMock = createSpyObject(NotificationsHub);
notificationsHubMock.notifications$ = of({});
modalServiceMock = createSpyObject(UiModalService);
authServiceMock = createSpyObject(AuthService);
spectator = createComponent({
providers: [
{ provide: ApplicationService, useValue: applicationServiceMock },
{ provide: NotificationsHub, useValue: notificationsHubMock },
{ provide: UiModalService, useValue: modalServiceMock },
{ provide: AuthService, useValue: authServiceMock },
],
});
breadcrumbServiceMock = spectator.inject(BreadcrumbService);
router = spectator.inject(Router);
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
describe('shell-header', () => {
it('should call setSection() on sectionChange event with the section argument', () => {
spyOn(spectator.component, 'setSection');
spectator.triggerEventHandler('shell-header', 'sectionChange', 'branch');
expect(spectator.component.setSection).toHaveBeenCalledWith('branch');
});
it('should render the header buttons', () => {
// Test verhält sich anders, wenn die größe des Browserfensters kleiner ist als 640px, da
// die Buttons dann unsichtbar werden und ins Drei-Punkt Menü verschoben werden.
if (document.body.clientWidth > 639) {
expect(spectator.query('shell-header .notifications-btn')).toBeVisible();
expect(spectator.query('shell-header .dashboard-btn')).toBeVisible();
expect(spectator.query('shell-header .logout-btn')).toBeVisible();
} else {
expect(spectator.query('shell-header .notifications-btn')).not.toBeVisible();
expect(spectator.query('shell-header .dashboard-btn')).not.toBeVisible();
expect(spectator.query('shell-header .logout-btn')).not.toBeVisible();
}
});
it('should have a anchor tag which navigates to /kunde/dashboard', () => {
const anchor = spectator.query('shell-header a');
expect(anchor).toHaveAttribute('href', '/kunde/dashboard');
});
});
describe('shell-process', () => {
it('should call addProcess() on addProcess event', () => {
spyOn(spectator.component, 'addProcess');
spectator.triggerEventHandler('shell-process', 'addProcess', undefined);
expect(spectator.component.addProcess).toHaveBeenCalled();
});
describe('shell-process-tab', () => {
it('should render for each process', () => {
const processes = [{}, {}, {}];
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
spectator.detectComponentChanges();
expect(spectator.queryAll('shell-process-tab')).toHaveLength(processes.length);
});
it('should call activateProcess() on activateProcess event', () => {
const processes = [{ id: 1 }];
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
spectator.detectComponentChanges();
spyOn(spectator.component, 'activateProcess');
spectator.triggerEventHandler('shell-process-tab', 'activateProcess', processes[0].id);
expect(spectator.component.activateProcess).toHaveBeenCalledWith(1);
});
it('should call closeProcess() on closeProcess event', () => {
const processes = [{ id: 1 }];
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
spectator.detectComponentChanges();
spyOn(spectator.component, 'closeProcess');
spectator.triggerEventHandler('shell-process-tab', 'closeProcess', processes[0].id);
expect(spectator.component.closeProcess).toHaveBeenCalledWith(1);
});
});
});
describe('shell-footer', () => {
it('should render when section is set', () => {
applicationServiceMock.getSection$.and.returnValue(of('customer'));
spectator.detectComponentChanges();
expect(spectator.query('shell-footer')).toBeVisible();
});
it('should not render when section is undefined', () => {
applicationServiceMock.getSection$.and.returnValue(of(undefined));
spectator.detectComponentChanges();
expect(spectator.query('shell-footer')).not.toBeVisible();
});
xit('should display the menu items for section customer', () => {
applicationServiceMock.getSection$.and.returnValue(of('customer'));
spectator.component.customerBasePath$ = of('/kunde/1');
spectator.detectComponentChanges();
authServiceMock.hasRole.and.returnValue(true);
const anchors = spectator.queryAll('shell-footer a');
expect(anchors[0]).toHaveText('Artikelsuche');
expect(anchors[0]).toHaveAttribute('href', '/kunde/1/product');
expect(anchors[1]).toHaveText('Kundensuche');
expect(anchors[1]).toHaveAttribute('href', '/kunde/1/customer');
expect(anchors[2]).toHaveText('Warenausgabe');
expect(anchors[2]).toHaveAttribute('href', '/kunde/1/goods/out');
});
it('should display the menu items for section branch', () => {
applicationServiceMock.getSection$.and.returnValue(of('branch'));
spectator.detectComponentChanges();
const anchors = spectator.queryAll('shell-footer a');
expect(anchors[0]).toHaveText('Sortiment');
expect(anchors[0]).toHaveAttribute('href', '/filiale/assortment');
expect(anchors[1]).toHaveText('Tätigkeitskalender');
expect(anchors[1]).toHaveAttribute('href', '/filiale/task-calendar');
expect(anchors[2]).toHaveText('Abholfach');
expect(anchors[2]).toHaveAttribute('href', '/filiale/goods/in');
expect(anchors[3]).toHaveText('Remission');
expect(anchors[3]).toHaveAttribute('href', '/filiale/remission');
expect(anchors[4]).toHaveText('Wareneingang');
expect(anchors[4]).toHaveAttribute('href', '/filiale/package-inspection');
});
});
describe('activatedProcessId$', () => {
it('should call _appService.getActivatedProcessId$() and return its value', async () => {
applicationServiceMock.getActivatedProcessId$.and.returnValue(of(1));
const processId = await spectator.component.activatedProcessId$.pipe(first()).toPromise();
expect(processId).toBe(1);
expect(applicationServiceMock.getActivatedProcessId$).toHaveBeenCalled();
});
});
describe('section$', () => {
it('should call _appService.getSection$() and return its value', async () => {
applicationServiceMock.getSection$.and.returnValue(of('branch'));
const section = await spectator.component.section$.pipe(first()).toPromise();
expect(section).toBe('branch');
expect(applicationServiceMock.getSection$).toHaveBeenCalled();
});
});
describe('processes$', () => {
it('should call _appService.processes$() and return its value', async () => {
applicationServiceMock.getProcesses$.and.returnValue(of([{}, {}]));
const processes = await spectator.component.processes$.pipe(first()).toPromise();
expect(processes).toHaveLength(2);
expect(applicationServiceMock.getProcesses$).toHaveBeenCalledWith('customer');
});
});
describe('remissionProcess$', () => {
it('should call _appService.getProcessById$() with Remission Id and return its value', async () => {
applicationServiceMock.getProcessById$.and.returnValue(of({ id: 4000 }));
await spectator.component.remissionProcess$.pipe(first()).toPromise();
expect(applicationServiceMock.getProcessById$).toHaveBeenCalled();
});
});
describe('remissionUrl$', () => {
it('should return the correct url if process.data.active is available', async () => {
const process = {
id: 4000,
data: {
active: 9999,
},
};
applicationServiceMock.getProcessById$.and.returnValue(of(process));
const url = await spectator.component.remissionUrl$.pipe(first()).toPromise();
expect(url).toBe('/filiale/remission/9999/list');
});
it('should return the correct url if process.data.active is not available', async () => {
const process = {
id: 4000,
data: {},
};
applicationServiceMock.getProcessById$.and.returnValue(of(process));
const url = await spectator.component.remissionUrl$.pipe(first()).toPromise();
expect(url).toBe('/filiale/remission');
});
});
describe('remissionQueryParams$', () => {
it('should return the correct queryParams if process.data.active and process.data.queryParams are available', async () => {
const process = {
id: 4000,
data: {
active: 9999,
queryParams: { filter: 'test' },
},
};
applicationServiceMock.getProcessById$.and.returnValue(of(process));
const queryParams = await spectator.component.remissionQueryParams$.pipe(first()).toPromise();
expect(queryParams).toEqual(process.data.queryParams);
});
it('should return the correct queryParams if process.data.active and process.data.queryParams are not available', async () => {
const process = {
id: 4000,
data: {},
};
applicationServiceMock.getProcessById$.and.returnValue(of(process));
const queryParams = await spectator.component.remissionQueryParams$.pipe(first()).toPromise();
expect(queryParams).toEqual({});
});
});
describe('setSection()', () => {
it('should call _appService.setSection() with the argument section', async () => {
await spectator.component.setSection('customer');
expect(applicationServiceMock.setSection).toHaveBeenCalledWith('customer');
});
it('should call activateProcess if getLastActivatedProcessWithSection returns a value', async () => {
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({ id: 1 }));
spyOn(spectator.component, 'activateProcess');
await spectator.component.setSection('customer');
expect(spectator.component.activateProcess).toHaveBeenCalledWith(1);
});
});
describe('logout()', () => {
it('should call _authService.logout()', () => {
spectator.component.logout();
expect(authServiceMock.logout).toHaveBeenCalled();
});
});
describe('addProcess()', () => {
it('should call navigate to /kunde/{timestamp}/product', () => {
spyOn(router, 'navigate');
spyOn(Date, 'now').and.returnValue(123);
spectator.component.addProcess();
expect(router.navigate).toHaveBeenCalledWith(['/kunde', 123, 'product']);
});
});
describe('closeProcess()', () => {
it('should call _appService.removeProcess() with the processId argument', () => {
const processes = [{}, {}, {}];
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
spectator.component.closeProcess(1);
expect(applicationServiceMock.removeProcess).toHaveBeenCalledWith(1);
});
it('should navigate to kunde/dashboard if no process is available', async () => {
spyOn(router, 'navigate');
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of([]));
spectator.detectComponentChanges();
await spectator.component.closeProcess(1);
expect(router.navigate).toHaveBeenCalledWith(['/kunde', 'dashboard']);
});
it('should not navigate to kunde/dashboard if processes are available', async () => {
spyOn(router, 'navigate');
const processes = [
{ id: 1, name: 'test', section: 'customer' },
{ id: 2, name: 'test', section: 'customer' },
];
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(of({}));
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
await spectator.component.closeProcess(1);
expect(router.navigate).not.toHaveBeenCalledWith(['/kunde', 'dashboard']);
});
it('should activate the next process when it was not the last process', async () => {
spyOn(spectator.component, 'activateProcess');
applicationServiceMock.getLastActivatedProcessWithSection$.and.returnValue(
of({
id: 2,
name: 'test',
section: 'customer',
activated: 2,
})
);
const processes = [
{ id: 1, name: 'test', section: 'customer', activated: 1 },
{ id: 2, name: 'test', section: 'customer', activated: 2 },
];
applicationServiceMock.getSection$.and.returnValue(of('customer'));
applicationServiceMock.getProcesses$.and.returnValue(of(processes));
await spectator.component.closeProcess(1);
expect(spectator.component.activateProcess).toHaveBeenCalledWith(2);
});
});
describe('activateProcess()', () => {
it('should get the last activated breadcrumb by key and if it is defined it navigates to its path with queryParams', async () => {
const crumb = { path: '/kunde/product', params: { id: 1 } };
spyOn(router, 'navigate');
breadcrumbServiceMock.getLastActivatedBreadcrumbByKey$.and.returnValue(of(crumb));
await spectator.component.activateProcess(1);
expect(router.navigate).toHaveBeenCalledWith([crumb.path], { queryParams: crumb.params });
});
it('should navigate to /kunde if no breadcrumb for this process exists', async () => {
breadcrumbServiceMock.getLastActivatedBreadcrumbByKey$.and.returnValue(of(undefined));
spyOn(router, 'navigate');
await spectator.component.activateProcess(1);
expect(router.navigate).toHaveBeenCalledWith(['/kunde', 1, 'product']);
});
});
describe('processAction()', () => {
it('should navigate to cart when process type is cart', () => {
spyOn(router, 'navigate');
const process: ApplicationProcess = { id: 1, name: 'Vorgang', section: 'customer', type: 'cart' };
spectator.component.processAction(process);
expect(router.navigate).toHaveBeenCalledWith(['/kunde', process.id, 'cart']);
});
it('should not navigate to when process type is not cart', () => {
spyOn(router, 'navigate');
const process: ApplicationProcess = { id: 1, name: 'Vorgang', section: 'customer', type: 'goods-out' };
spectator.component.processAction(process);
expect(router.navigate).not.toHaveBeenCalled();
});
});
describe('openNotifications()', () => {
it('should call modalService.open() with the ModalNotificationComponent', async () => {
const notifications: EnvelopeDTO<MessageBoardItemDTO[]> = {
data: [{}, {}, {}],
};
spectator.component.notifications$ = of(notifications);
await spectator.component.openNotifications();
expect(modalServiceMock.open).toHaveBeenCalledWith({
content: ModalNotificationsComponent,
data: notifications,
config: {
showScrollbarY: false,
},
});
});
});
});

View File

@@ -1,174 +0,0 @@
import { Component, ChangeDetectionStrategy, ViewChildren, QueryList, TrackByFunction, NgZone } from '@angular/core';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { first, map, shareReplay, switchMap, take, tap } from 'rxjs/operators';
import { NotificationsHub } from '@hub/notifications';
import { ModalNotificationsComponent } from '@modal/notifications';
import { UiModalService } from '@ui/modal';
import { Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { combineLatest } from 'rxjs';
import { AuthService } from '@core/auth';
import { DomainAvailabilityService } from '@domain/availability';
import { ShellProcessTabComponent } from '@shell/process';
import { Config } from '@core/config';
import { WrongDestinationModalService } from 'apps/page/package-inspection/src/lib/components/wrong-destination-modal/wrong-destination-modal.service';
@Component({
selector: 'app-shell',
templateUrl: 'shell.component.html',
styleUrls: ['shell.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShellComponent {
isDevelopment = Boolean(this._config.get('debug'));
debugOpen = false;
@ViewChildren('processTabs')
readonly processTabs: QueryList<ShellProcessTabComponent>;
notifications$ = this._notificationsHub.notifications$;
notificationCount$ = this.notifications$.pipe(map((message) => message?.data?.length));
get activatedProcessId$() {
return this._appService.getActivatedProcessId$().pipe(
tap((activatedProcessId) => {
this.processTabs?.find((process) => process?.process?.id === activatedProcessId && !process?.isActive)?.slideIntoView();
})
);
}
customerBasePath$ = this.activatedProcessId$.pipe(
switchMap((processId) => this._appService.getProcessById$(processId)),
map((process) => {
if (!!process && process.section === 'customer' && process.type !== 'cart-checkout') {
// Übernehme aktiven Prozess
return `/kunde/${process.id}`;
} else {
// Über Guards wird ein neuer Prozess erstellt
return '/kunde';
}
})
);
get section$() {
return this._appService.getSection$().pipe(shareReplay());
}
get processes$() {
return this.section$.pipe(switchMap((section) => this._appService.getProcesses$(section)));
}
get remissionProcess$() {
return this._appService.getProcessById$(this._config.get('process.ids.remission'));
}
get remissionUrl$() {
return this.remissionProcess$.pipe(
map((process) => (process?.data?.active ? `/filiale/remission/${process.data.active}/list` : '/filiale/remission'))
);
}
get remissionQueryParams$() {
return this.remissionProcess$.pipe(
map((process) => (process?.data?.active && process?.data?.queryParams ? process.data.queryParams : {}))
);
}
get addProcessLabel$() {
return combineLatest([this.section$, this.processes$]).pipe(
map(([section, processes]) => (section === 'customer' && processes.length === 0 ? 'VORGANG STARTEN' : ''))
);
}
get canAddProcess$() {
return this.section$.pipe(map((section) => section === 'customer'));
}
get currentBranch$() {
return this._availabilityService.getDefaultBranch();
}
constructor(
private readonly _appService: ApplicationService,
private readonly _config: Config,
private readonly _notificationsHub: NotificationsHub,
private readonly _modal: UiModalService,
private readonly _router: Router,
private readonly _breadcrumbService: BreadcrumbService,
private readonly _authService: AuthService,
private readonly _availabilityService: DomainAvailabilityService,
private readonly _zone: NgZone,
private readonly _wrongDestinationModalService: WrongDestinationModalService
) {}
async setSection(section: 'customer' | 'branch') {
this._appService.setSection(section);
const lastProcessId = (await this._appService.getLastActivatedProcessWithSection$(section).pipe(first()).toPromise())?.id;
if (lastProcessId) {
this.activateProcess(lastProcessId);
} else {
this._router.navigate([section === 'customer' ? '/kunde' : '/filiale']);
}
}
// Process werden über Guards erstellt und aktiviert. An dieser Stelle wird nur navigiert
async addProcess() {
const processId = Date.now();
await this._router.navigate(['/kunde', processId, 'product']);
}
async activateProcess(activatedProcessId: number) {
try {
const latestCrumb = await this._breadcrumbService?.getLastActivatedBreadcrumbByKey$(activatedProcessId)?.pipe(take(1)).toPromise();
await this._zone.run(async () => {
if (latestCrumb) {
await this._router.navigate([latestCrumb.path], { queryParams: latestCrumb.params });
} else {
await this._router.navigate(['/kunde', activatedProcessId, 'product']);
}
});
} catch (error) {}
}
async closeProcess(processId: number) {
this._appService.removeProcess(processId);
const processes = await this.processes$.pipe(first()).toPromise();
if (processes.length === 0) {
await this._router.navigate(['/kunde', 'dashboard']);
return;
}
const section = await this.section$.pipe(first()).toPromise();
const lastActivatedProcess = await this._appService.getLastActivatedProcessWithSection$(section).pipe(first()).toPromise();
this.activateProcess(lastActivatedProcess?.id);
}
processAction(process: ApplicationProcess) {
if (process?.type === 'cart') {
this._router.navigate(['/kunde', process.id, 'cart']);
}
}
async logout() {
await this._authService.logout();
}
async openNotifications() {
const notifications = await this.notifications$.pipe(first()).toPromise();
this._modal.open({
content: ModalNotificationsComponent,
data: notifications,
config: {
showScrollbarY: false,
},
});
}
trackByIdFn: TrackByFunction<ApplicationProcess> = (_, process) => process.id;
fetchAndOpenPackages = () => this._wrongDestinationModalService.fetchAndOpen();
}

View File

@@ -1,31 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { OverlayModule } from '@angular/cdk/overlay';
import { ShellHeaderModule } from '@shell/header';
import { ShellProcessModule } from '@shell/process';
import { ShellFooterModule } from '@shell/footer';
import { ShellComponent } from './shell.component';
import { UiIconModule } from '@ui/icon';
import { RouterModule } from '@angular/router';
import { AuthModule } from '@core/auth';
import { DebugComponent } from '../debug/debug.component';
@NgModule({
imports: [
RouterModule,
CommonModule,
ShellHeaderModule,
ShellProcessModule,
ShellFooterModule,
UiIconModule,
OverlayModule,
AuthModule,
DebugComponent,
],
exports: [ShellComponent],
declarations: [ShellComponent],
providers: [],
})
export class ShellModule {}

View File

@@ -1,20 +0,0 @@
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 100 700;
src: url(./materials-icons-outlined.woff2) format('woff2');
}
@font-face {
font-family: 'Material Symbols Rounded';
font-style: normal;
font-weight: 100 700;
src: url(./materials-icons-rounded.woff2) format('woff2');
}
@font-face {
font-family: 'Material Symbols Sharp';
font-style: normal;
font-weight: 100 700;
src: url(./materials-icons-sharp.woff2) format('woff2');
}

View File

Binary file not shown.

View File

File diff suppressed because one or more lines are too long

View File

@@ -8,9 +8,8 @@
},
"@core/auth": {
"issuer": "https://sso-test.paragon-data.de",
"clientId": "hug-isa",
"responseType": "id_token token",
"oidc": true,
"clientId": "isa-client",
"responseType": "code",
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi"
},
"@core/logger": {
@@ -41,7 +40,7 @@
"rootUrl": "https://filialinformationsystem-integration.paragon-systems.de/eiswebapi/v1"
},
"@swagger/remi": {
"rootUrl": "https://isa-integration.paragon-data.net/inv/v1"
"rootUrl": "https://isa-integration.paragon-data.net/inv/v6"
},
"@swagger/wws": {
"rootUrl": "https://isa-integration.paragon-data.net/wws/v1"

View File

File diff suppressed because one or more lines are too long

View File

@@ -41,10 +41,10 @@
"rootUrl": "https://filialinformationsystem.paragon-systems.de/eiswebapi/v1"
},
"@swagger/remi": {
"rootUrl": "https://isa.paragon-systems.de/inv/v1"
"rootUrl": "https://isa.paragon-systems.de/inv/v6"
},
"@swagger/wws": {
"rootUrl": "https://isa.paragon-data.net/wws/v1"
"rootUrl": "https://isa.paragon-systems.de/wws/v1"
},
"hubs": {
"notifications": {
@@ -69,6 +69,6 @@
},
"checkForUpdates": 3600000,
"licence": {
"scandit": ""
"scandit": "AfHi/mY+RbwJD5nC7SuWn3I14pFUOfSbQ2QG//4aV3zWQjwix30kHqsqraA8ZiipDBql8YlwIyV6VPBMUiAX4s9YHDxHHsWwq2BUB3ImzDEcU1jmMH/5yakGUYpCQ68D0iZ8SG9sS0QBb3iFdCHc1r9DFr1cMTxM7zOvb/AUoIVmieHZXnx9ioUgCvczsLiuX3hwvTW3lhbvJ4uUyqTWK4sWFVwoY4AIWSFrPwwrkV2DksMKT5fMJT3GWgPypvTIGwWvpRfLWwKlc1Z3ckyb84khsnaWD2wr+hdgu/K8YIMmgGszm5KIZ/G05YfDNZtQ4jby+5RZvQwWR8rxM35rJgf73OkMSpuL9jw3T0TTAlvpkGRLzVVuCw9VjlBLqfPNEZ6VsEwFuAla9IYUvFHCsjypg2J6UpxHXrTYmbsSu5Jm8frVfS5znPPTO9D/4rF6ZVv2PxY9PgUgJUvwMa/VMc/nse3RRRf8RGT4rUItfJDFO8pujD76vVEWq/KixQRoMdLgDLyxhsFVftkxqhZhyEfFZzsEy49LSojJ28vpHpBWLeCQBmnZ7JZ4C5yOQiqSQV/assBq2zJN2q+vCDp8qy5j1rED1SX5Ec7JpgpgnU4chLIf5Zn7bP/hNGT3pEYBuXeDXXN8ke1pcc3fc3m0FysDG0o56XVCUqImZ8Ezi8eujZciKDrWbtljhKTj7cnfuJx0sVHF6Bh5i4YfgA/Z+NL+MtH2EVIF67e6hEz6PWYTcoh3ybBaJfxb2FNvGJutNKg04GwMhYq6K2IddBt0fDiBt0SGM0oSBlUP3DKCUmXcf2a6ASbrcqv6Wz1jHt0pY4U8bEpg7qSbW3VDyvdPgyQ="
}
}

View File

@@ -41,7 +41,7 @@
"rootUrl": "https://filialinformationsystem-staging.paragon-systems.de/eiswebapi/v1"
},
"@swagger/remi": {
"rootUrl": "https://isa-staging.paragon-systems.de/inv/v1"
"rootUrl": "https://isa-staging.paragon-systems.de/inv/v6"
},
"@swagger/wws": {
"rootUrl": "https://isa-staging.paragon-systems.de/wws/v1"
@@ -69,6 +69,6 @@
},
"checkForUpdates": 3600000,
"licence": {
"scandit": ""
"scandit": "AfHi/mY+RbwJD5nC7SuWn3I14pFUOfSbQ2QG//4aV3zWQjwix30kHqsqraA8ZiipDBql8YlwIyV6VPBMUiAX4s9YHDxHHsWwq2BUB3ImzDEcU1jmMH/5yakGUYpCQ68D0iZ8SG9sS0QBb3iFdCHc1r9DFr1cMTxM7zOvb/AUoIVmieHZXnx9ioUgCvczsLiuX3hwvTW3lhbvJ4uUyqTWK4sWFVwoY4AIWSFrPwwrkV2DksMKT5fMJT3GWgPypvTIGwWvpRfLWwKlc1Z3ckyb84khsnaWD2wr+hdgu/K8YIMmgGszm5KIZ/G05YfDNZtQ4jby+5RZvQwWR8rxM35rJgf73OkMSpuL9jw3T0TTAlvpkGRLzVVuCw9VjlBLqfPNEZ6VsEwFuAla9IYUvFHCsjypg2J6UpxHXrTYmbsSu5Jm8frVfS5znPPTO9D/4rF6ZVv2PxY9PgUgJUvwMa/VMc/nse3RRRf8RGT4rUItfJDFO8pujD76vVEWq/KixQRoMdLgDLyxhsFVftkxqhZhyEfFZzsEy49LSojJ28vpHpBWLeCQBmnZ7JZ4C5yOQiqSQV/assBq2zJN2q+vCDp8qy5j1rED1SX5Ec7JpgpgnU4chLIf5Zn7bP/hNGT3pEYBuXeDXXN8ke1pcc3fc3m0FysDG0o56XVCUqImZ8Ezi8eujZciKDrWbtljhKTj7cnfuJx0sVHF6Bh5i4YfgA/Z+NL+MtH2EVIF67e6hEz6PWYTcoh3ybBaJfxb2FNvGJutNKg04GwMhYq6K2IddBt0fDiBt0SGM0oSBlUP3DKCUmXcf2a6ASbrcqv6Wz1jHt0pY4U8bEpg7qSbW3VDyvdPgyQ="
}
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link href="/assets/fonts/fonts.css" rel="stylesheet" />
<link href="/assets/icons/icons.css" rel="stylesheet" />
<link rel="manifest" href="manifest.webmanifest" />
<meta name="theme-color" content="#1976d2" />
</head>

View File

@@ -11,15 +11,21 @@
@import './scss/customer';
@import './scss/branch';
* {
@apply font-sans;
}
body {
background: var(--bg-color);
}
@layer base {
:root {
font-size: 16px;
}
@media screen and (min-width: 1680px) {
:root {
font-size: 19px;
}
}
body {
@apply bg-background;
}
::-webkit-scrollbar {
width: 0; // remove scrollbar space
height: 0;

View File

@@ -3,7 +3,7 @@
}
.subtitle {
@apply text-center text-regular my-6;
@apply text-center text-p2 my-6;
}
.bold {
@@ -62,7 +62,7 @@ hr {
@apply flex flex-row items-center;
.cta-reserve {
@apply absolute bg-transparent text-brand text-base font-bold border-none px-1 -mr-1;
@apply absolute bg-transparent text-brand text-p2 font-bold border-none px-1 -mr-1;
right: 85px;
}
}

View File

@@ -6,7 +6,7 @@ modal-notifications {
}
.notification-card {
@apply text-center text-xl text-inactive-branch block bg-white rounded-t-card font-bold no-underline py-4 border-none outline-none shadow-card -ml-4;
@apply text-center text-xl text-inactive-branch block bg-white rounded-t font-bold no-underline py-4 border-none outline-none shadow-card -ml-4;
width: calc(100% + 2rem);
}
@@ -33,7 +33,7 @@ modal-notifications {
}
h2 {
@apply font-bold text-2xl ml-4;
@apply font-bold text-h3 ml-4;
}
}
@@ -66,11 +66,11 @@ modal-notifications {
@apply flex flex-row justify-between items-start;
h1 {
@apply text-regular font-bold mb-2;
@apply text-p2 font-bold mb-2;
}
.notification-edit-cta {
@apply bg-transparent text-brand text-base font-bold border-none px-1 -mr-1;
@apply bg-transparent text-brand text-p2 font-bold border-none px-1 -mr-1;
}
}
}

View File

@@ -6,7 +6,7 @@
}
.error-message {
@apply text-center text-regular font-semibold text-brand;
@apply text-center text-p2 font-semibold text-brand;
}
}
@@ -30,7 +30,7 @@ ui-select {
@apply mt-px-35 text-center;
.print-btn {
@apply border-none outline-none bg-brand text-white font-bold text-cta-l px-px-25 py-px-15 rounded-full my-8;
@apply border-none outline-none bg-brand text-white font-bold text-p1 px-px-25 py-px-15 rounded-full my-8;
&:disabled {
@apply bg-inactive-branch;

View File

@@ -3,7 +3,7 @@
}
h3 {
@apply text-regular font-semibold text-center;
@apply text-p2 font-semibold text-center;
}
hr {
@@ -23,11 +23,11 @@ hr {
@apply flex flex-col ml-5;
.product-name {
@apply text-regular font-bold;
@apply text-p2 font-bold;
}
.product-format {
@apply mt-5 text-regular font-bold flex items-center;
@apply mt-5 text-p2 font-bold flex items-center;
.format-icon {
@apply mr-2;
@@ -35,11 +35,11 @@ hr {
}
.product-ean {
@apply text-regular font-bold;
@apply text-p2 font-bold;
}
.quantity {
@apply text-regular font-bold;
@apply text-p2 font-bold;
}
}
}

View File

@@ -31,12 +31,12 @@
}
.title {
@apply ml-5 text-regular font-bold;
@apply ml-5 text-p2 font-bold;
}
.btn-expand,
.btn-collapse {
@apply border-none bg-transparent outline-none text-regular text-ucla-blue font-bold -mt-2;
@apply border-none bg-transparent outline-none text-p2 text-ucla-blue font-bold -mt-2;
ui-icon {
@apply inline mx-1 align-middle;

View File

@@ -1,8 +1,8 @@
<div
class="page-price-update-item__item-header flex flex-row w-full items-center justify-between bg-[rgba(0,128,121,0.15)] mb-px-2 px-5 h-[53px] rounded-t-card"
class="page-price-update-item__item-header flex flex-row w-full items-center justify-between bg-[rgba(0,128,121,0.15)] mb-px-2 px-5 h-[53px] rounded-t"
>
<p class="page-price-update-item__item-instruction font-bold text-lg">{{ item?.task?.instruction }}</p>
<p class="page-price-update-item__item-due-date text-base">
<p class="page-price-update-item__item-due-date text-p2">
gültig ab <span class="font-bold ml-2">{{ item?.task?.dueDate | date }}</span>
</p>
</div>
@@ -20,15 +20,15 @@
<div class="page-price-update-item__item-details">
<div class="page-price-update-item__item-contributors flex flex-row">
{{ environment.isTablet() ? (item?.product?.contributors | substr: 42) : item?.product?.contributors }}
{{ environment.isTablet() ? (item?.product?.contributors | substr: 38) : item?.product?.contributors }}
</div>
<div
class="page-price-update-item__item-title font-bold text-2xl"
class="page-price-update-item__item-title font-bold text-h3"
[class.text-xl]="item?.product?.name?.length >= 35"
[class.text-lg]="item?.product?.name?.length >= 40"
[class.text-md]="item?.product?.name?.length >= 50"
[class.text-sm]="item?.product?.name?.length >= 60"
[class.text-p3]="item?.product?.name?.length >= 60"
[class.text-xs]="item?.product?.name?.length >= 100"
>
{{ item?.product?.name }}
@@ -43,7 +43,7 @@
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
{{ environment.isTablet() ? (item?.product?.formatDetail | substr: 25) : item?.product?.formatDetail }}
</div>
</div>

View File

@@ -5,8 +5,9 @@ import { EnvironmentService } from '@core/environment';
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
import { ProductListItemDTO } from '@swagger/wws';
import { DateAdapter } from '@ui/common';
import { debounceTime, map, shareReplay, switchMap } from 'rxjs/operators';
import { debounceTime, filter, map, shareReplay, switchMap } from 'rxjs/operators';
import { PriceUpdateComponentStore } from '../price-update.component.store';
import { ReplaySubject, combineLatest } from 'rxjs';
@Component({
selector: 'page-price-update-item',
@@ -16,8 +17,18 @@ import { PriceUpdateComponentStore } from '../price-update.component.store';
providers: [DatePipe],
})
export class PriceUpdateItemComponent {
private _item$ = new ReplaySubject<ProductListItemDTO>(1);
private _item: ProductListItemDTO;
@Input()
item: ProductListItemDTO;
get item() {
return this._item;
}
set item(value) {
this._item = value;
this._item$.next(value);
}
get publicationDate() {
if (!!this.item?.product?.publicationDate) {
@@ -40,21 +51,14 @@ export class PriceUpdateItemComponent {
defaultBranch$ = this._availability.getDefaultBranch();
inStock$ = this.defaultBranch$.pipe(
inStock$ = combineLatest([this.defaultBranch$, this._item$]).pipe(
debounceTime(100),
switchMap(
(defaultBranch) =>
this._stockService.getInStock$({
itemId: Number(this.item?.product?.catalogProductNumber),
branchId: defaultBranch?.id,
})
// TODO: Bugfixing INSTOCK
// .pipe(
// map((instock) => {
// this.item.product.ean === '9783551775559' ? console.log({ item: this.item, instock }) : '';
// return instock;
// })
// )
filter(([defaultBranch, item]) => !!defaultBranch && !!item),
switchMap(([defaultBranch, item]) =>
this._stockService.getInStock$({
itemId: Number(item?.product?.catalogProductNumber),
branchId: defaultBranch?.id,
})
),
shareReplay(1)
);

View File

@@ -8,7 +8,7 @@
Drucken
</button>
<div class="flex flex-row items-center justify-end">
<div *ngIf="getSelectableItems().length > 0" class="text-[#0556B4] font-bold text-sm mr-5">
<div *ngIf="getSelectableItems().length > 0" class="text-[#0556B4] font-bold text-p3 mr-5">
<ng-container *ngIf="selectedItemUids$ | async; let selectedItems">
<button class="page-price-update-list__cta-unselect-all" *ngIf="selectedItems?.length > 0" type="button" (click)="unselectAll()">
Alle entfernen ({{ selectedItems?.length }})
@@ -18,7 +18,7 @@
</button>
</ng-container>
</div>
<div class="page-price-update-list__items-count inline-flex flex-row items-center pr-5 text-sm">
<div class="page-price-update-list__items-count inline-flex flex-row items-center pr-5 text-p3">
{{ items?.length ??
0 }}
Titel
@@ -26,7 +26,7 @@
</div>
</div>
<div class="page-price-update-list__order-by h-[53px] flex flex-row items-center justify-center bg-white rounded-t-card mb-px-2">
<div class="page-price-update-list__order-by h-[53px] flex flex-row items-center justify-center bg-white rounded-t mb-px-2">
<ui-order-by-filter [orderBy]="orderBy$ | async" (selectedOrderByChange)="search()"> </ui-order-by-filter>
</div>

View File

@@ -1,5 +1,5 @@
<div class="flex flex-row items-center h-14 bg-white relative rounded-t font-bold shadow-lg">
<h3 class="text-center grow font-bold text-2xl">Preisänderung</h3>
<h3 class="text-center grow font-bold text-h3">Preisänderung</h3>
<button
(click)="filterOverlay.open()"
class="absolute right-0 top-0 h-14 rounded px-5 text-lg bg-cadet-blue flex flex-row flex-nowrap items-center justify-center"
@@ -54,5 +54,5 @@
</shell-filter-overlay>
<ng-template #noResults>
<div class="bg-white text-2xl text-center pt-10 font-bold rounded-b h-[calc(100vh_-_370px)]">Keine Preisänderungen vorhanden.</div>
<div class="bg-white text-h3 text-center pt-10 font-bold rounded-b h-[calc(100vh_-_370px)]">Keine Preisänderungen vorhanden.</div>
</ng-template>

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core';
import { Config } from '@core/config';
import { provideComponentStore } from '@ngrx/component-store';
import { ShellFilterOverlayComponent } from '@shell/filter-overlay';
import { SharedFilterOverlayComponent } from '@shared/components/filter-overlay';
import { UiFilter, UiFilterComponent } from '@ui/filter';
import { PriceUpdateComponentStore } from './price-update.component.store';
import { combineLatest, Subject, Subscription } from 'rxjs';
@@ -26,8 +26,8 @@ export class PriceUpdateComponent implements OnInit {
hint$ = new Subject<string>();
@ViewChild(ShellFilterOverlayComponent)
filterOverlay: ShellFilterOverlayComponent;
@ViewChild(SharedFilterOverlayComponent)
filterOverlay: SharedFilterOverlayComponent;
/**
* Zeigt die liste an, wenn entweder keine items geladen werden oder wenn items geladen wurden

View File

@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ShellFilterOverlayModule } from '@shell/filter-overlay';
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
import { UiFilterNextModule } from '@ui/filter';
import { UiIconModule } from '@ui/icon';
import { UiSpinnerModule } from '@ui/spinner';
@@ -8,7 +8,7 @@ import { PriceUpdateListModule } from './price-update-list';
import { PriceUpdateComponent } from './price-update.component';
@NgModule({
imports: [CommonModule, PriceUpdateListModule, UiIconModule, UiFilterNextModule, ShellFilterOverlayModule, UiSpinnerModule],
imports: [CommonModule, PriceUpdateListModule, UiIconModule, UiFilterNextModule, SharedFilterOverlayModule, UiSpinnerModule],
exports: [PriceUpdateComponent],
declarations: [PriceUpdateComponent],
providers: [],

View File

@@ -1,124 +1,240 @@
<ng-container *ngIf="!showRecommendations">
<div #detailsContainer class="product-card">
<div class="page-article-details__container px-5 relative">
<ng-container *ngIf="store.item$ | async; let item">
<div class="product-details">
<div class="product-image">
<button class="image-button" (click)="showImages()">
<img (load)="loadImage()" [src]="item.imageId | productImage: 195:315:true" alt="product image" />
<ui-icon *ngIf="imageLoaded$ | async" icon="search_add" size="22px"></ui-icon>
</button>
<div class="page-article-details__product-details mb-3">
<div class="page-article-details__product-bookmark justify-self-end">
<div *ngIf="showArchivBadge$ | async" class="archiv-badge">
<button [uiOverlayTrigger]="archivTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
<img src="/assets/images/bookmark_benachrichtigung_archiv.svg" alt="Archiv Badge" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #archivTooltip [closeable]="true">
<ng-container *ngIf="isAvailable$ | async; else notAvailable">
Archivtitel. Wird nicht mehr gedruckt. Artikel ist bestellbar, weil lieferbar.
</ng-container>
<ng-template #notAvailable>
Archivtitel. Wird nicht mehr gedruckt. Nicht bestellbar.
</ng-template>
</ui-tooltip>
</button>
</div>
<div *ngIf="showSubscriptionBadge$ | async">
<button [uiOverlayTrigger]="subscribtionTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
<img src="/assets/images/bookmark_subscription.svg" alt="Fortsetzungsartikel Badge" />
</button>
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #subscribtionTooltip [closeable]="true"
>Artikel ist ein Fortsetzungsartikel,<br />
Artikel muss über eine Aboabteilung<br />
bestellt werden.
</ui-tooltip>
</div>
<div *ngIf="showPromotionBadge$ | async" class="promotion-badge">
<button [uiOverlayTrigger]="promotionTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
<ui-icon-badge icon="gift" alt="Prämienkatalog Badge"></ui-icon-badge>
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #promotionTooltip [closeable]="true">
Dieser Artikel befindet sich im Prämienkatalog.
</ui-tooltip>
</button>
</div>
</div>
<button (click)="showReviews()" class="recessions" *ngIf="item.reviews?.length > 0">
<div class="page-article-details__product-image-recessions flex flex-col items-center">
<div class="page-article-details__product-image">
<button class="border-none outline-none bg-transparent relative" (click)="showImages()">
<img
class="max-h-[19.6875rem] max-w-[12.1875rem] rounded"
(load)="loadImage()"
[src]="item.imageId | productImage: 195:315:true"
alt="product image"
/>
<ui-icon
class="absolute text-[#A7B9CB] inline-block bottom-[14px] right-[18px]"
*ngIf="imageLoaded$ | async"
icon="search_add"
size="25px"
></ui-icon>
</button>
</div>
<button
(click)="showReviews()"
class="page-article-details__product-recessions flex flex-col mt-2 items-center bg-transparent border-none outline-none"
*ngIf="item.reviews?.length > 0"
>
<ui-stars [rating]="store.reviewRating$ | async"></ui-stars>
<div class="cta-recessions">{{ item.reviews.length }} Rezensionen</div>
<div class="text-p2 text-[#0556B4] font-bold">{{ item.reviews.length }} Rezensionen</div>
</button>
</div>
<div class="product-info">
<div class="row" [class.bookmark-badge-gap]="isBadgeVisible$ | async">
<div>
<a
*ngFor="let contributor of contributors$ | async; let last = last"
class="autor"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
<div class="page-article-details__product-contributors">
<a
*ngFor="let contributor of contributors$ | async; let last = last"
class="text-[#0556B4] font-semibold no-underline text-p2"
[routerLink]="resultsPath"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
</div>
<div class="page-article-details__product-print justify-self-end" [class.mt-4]="isBadgeVisible$ | async">
<button class="bg-transparent text-brand font-bold text-lg outline-none border-none p-0" (click)="print()">Drucken</button>
</div>
<div class="page-article-details__product-title text-h3 font-bold mb-6">
{{ item.product?.name }}
</div>
<div class="page-article-details__product-misc flex flex-col mb-4">
<div
class="page-article-details__product-format flex items-center font-bold text-p3"
*ngIf="item?.product?.format && item?.product?.formatDetail"
>
<img
*ngIf="item?.product?.format !== '--'"
class="flex mr-2 h-[1.125rem]"
[src]="'/assets/images/Icon_' + item.product?.format + '.svg'"
[alt]="item.product?.formatDetail"
/>
{{ item.product?.formatDetail }}
</div>
<div class="page-article-details__product-volume" *ngIf="item?.product?.volume">Band/Reihe {{ item?.product?.volume }}</div>
<div class="page-article-details__product-publication">{{ publicationDate$ | async }}</div>
</div>
<div class="page-article-details__product-price-info flex flex-col mb-4">
<div
class="page-article-details__product-price font-bold text-xl self-end"
*ngIf="item.catalogAvailability?.price?.value?.value; else retailPrice"
>
{{ item.catalogAvailability?.price?.value?.value | currency: item.catalogAvailability?.price?.value?.currency:'code' }}
</div>
<ng-template #retailPrice>
<div
class="page-article-details__product-price font-bold text-xl self-end"
*ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability"
>
{{ takeAwayAvailability?.retailPrice?.value?.value | currency: takeAwayAvailability?.retailPrice?.value?.currency:'code' }}
</div>
</ng-template>
<button class="cta-print right" (click)="print()">Drucken</button>
</div>
<div class="title">
{{ item.product?.name }}
<div class="page-article-details__product-points self-end" *ngIf="store.promotionPoints$ | async; let promotionPoints">
{{ promotionPoints }} Lesepunkte
</div>
<div class="row">
<div>
<div class="format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
class="format-icon"
[src]="'/assets/images/Icon_' + item.product?.format + '.svg'"
[alt]="item.product?.formatDetail"
/>
{{ item.product?.formatDetail }}
<!-- TODO: Ticket PREISGEBUNDEN -->
<div class="page-article-details__product-price-bound self-end"></div>
</div>
<div class="page-article-details__product-origin-infos flex flex-col mb-4">
<div class="page-article-details__product-manufacturer" data-name="product-manufacturer">{{ item.product?.manufacturer }}</div>
<div class="page-article-details__product-language" *ngIf="item?.product?.locale" data-name="product-language">
{{ item?.product?.locale }}
</div>
</div>
<div class="page-article-details__product-stock flex justify-end items-center">
<div class="h-5 w-16 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]" *ngIf="store.fetchingTakeAwayAvailability$ | async"></div>
<div
class="flex flex-row py-4 pl-4"
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
*ngIf="!(store.fetchingTakeAwayAvailability$ | async)"
>
<ng-container *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
<ui-icon class="mr-2 mb-1" icon="home" size="15px"></ui-icon>
<span class="font-bold text-p3">{{ takeAwayAvailability.inStock || 0 }}x</span>
</ng-container>
</div>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
<div class="page-article-details__product-ean-specs flex flex-col">
<div class="page-article-details__product-ean" data-name="product-ean">{{ item.product?.ean }}</div>
<div class="page-article-details__product-specs">
<ng-container *ngIf="item?.specs?.length > 0">
{{ (item?.specs)[0]?.value }}
</ng-container>
</div>
</div>
<div class="page-article-details__product-availabilities flex flex-row items-center justify-end mt-4">
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingTakeAwayAvailability$ | async; else showAvailabilityTakeAwayIcon"
></div>
<ng-template #showAvailabilityTakeAwayIcon>
<div
*ngIf="store.isTakeAwayAvailabilityAvailable$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center"
>
<ui-icon class="mx-1" icon="shopping_bag" size="18px"> </ui-icon>
</div>
</ng-template>
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingPickUpAvailability$ | async; else showAvailabilityPickUpIcon"
></div>
<ng-template #showAvailabilityPickUpIcon>
<div
*ngIf="store.isPickUpAvailabilityAvailable$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
>
<ui-icon class="mx-1" icon="box_out" size="18px"></ui-icon>
</div>
</ng-template>
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingDeliveryAvailability$ | async; else showAvailabilityDeliveryIcon"
></div>
<ng-template #showAvailabilityDeliveryIcon>
<div
*ngIf="showDeliveryTruck$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
>
<ui-icon class="-mb-px-5 -mt-px-5 mx-1" icon="truck" size="30px"></ui-icon>
</div>
</ng-template>
<div
class="h-5 w-6 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]"
*ngIf="store.fetchingDeliveryB2BAvailability$ | async; else showAvailabilityDeliveryB2BIcon"
></div>
<ng-template #showAvailabilityDeliveryB2BIcon>
<div
*ngIf="showDeliveryB2BTruck$ | async"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
>
<ui-icon class="-mb-px-10 -mt-px-10 mx-1" icon="truck_b2b" size="30px"> </ui-icon>
</div>
</ng-template>
<span *ngIf="store.isDownload$ | async" class="flex flex-row items-center">
<div class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3">
<ui-icon class="mx-1" icon="download" size="18px"></ui-icon>
<span class="font-bold">Download</span>
</div>
</span>
</div>
<div class="page-article-details__shelf-ssc">
<div class="page-article-details__ssc flex justify-end my-2 font-bold text-lg">
<div class="w-52 h-px-20 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]" *ngIf="fetchingAvailabilities$ | async"></div>
<ng-container *ngIf="!(fetchingAvailabilities$ | async)">
<div *ngIf="store.sscText$ | async; let sscText">
{{ sscText }}
</div>
<div *ngIf="item?.product?.volume">Band/Reihe {{ item?.product?.volume }}</div>
<div>{{ publicationDate$ | async }}</div>
</div>
<div class="right">
<div class="price" *ngIf="item.catalogAvailability?.price?.value?.value; else retailPrice">
{{ item.catalogAvailability?.price?.value?.value | currency: item.catalogAvailability?.price?.value?.currency:'code' }}
</div>
<ng-template #retailPrice>
<div class="price" *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
{{
takeAwayAvailability?.retailPrice?.value?.value | currency: takeAwayAvailability?.retailPrice?.value?.currency:'code'
}}
</div>
</ng-template>
<div *ngIf="store.promotionPoints$ | async; let promotionPoints">{{ promotionPoints }} Lesepunkte</div>
</div>
</ng-container>
</div>
<div class="row stock">
<div data-name="product-manufacturer">{{ item.product?.manufacturer }}</div>
<div class="right quantity" [uiOverlayTrigger]="tooltip" [overlayTriggerDisabled]="!(stockTooltipText$ | async)">
<div class="fetching small" *ngIf="store.fetchingTakeAwayAvailability$ | async"></div>
<ng-container *ngIf="!(store.fetchingTakeAwayAvailability$ | async)">
<ng-container *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
<ui-icon icon="home" size="22px"></ui-icon>
{{ takeAwayAvailability.inStock || 0 }}x
</ng-container>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
</div>
<div *ngIf="item?.product?.locale" data-name="product-language">{{ item?.product?.locale }}</div>
<div class="row">
<div data-name="product-ean">{{ item.product?.ean }}</div>
<div class="right">
<div class="availability-icons">
<div class="fetching xsmall" *ngIf="store.fetchingTakeAwayAvailability$ | async; else showAvailabilityTakeAwayIcon"></div>
<ng-template #showAvailabilityTakeAwayIcon>
<ui-icon *ngIf="store.isTakeAwayAvailabilityAvailable$ | async" icon="shopping_bag" size="18px"> </ui-icon>
</ng-template>
<div class="fetching xsmall" *ngIf="store.fetchingPickUpAvailability$ | async; else showAvailabilityPickUpIcon"></div>
<ng-template #showAvailabilityPickUpIcon>
<ui-icon *ngIf="store.isPickUpAvailabilityAvailable$ | async" icon="box_out" size="18px"></ui-icon>
</ng-template>
<div class="fetching xsmall" *ngIf="store.fetchingDeliveryAvailability$ | async; else showAvailabilityDeliveryIcon"></div>
<ng-template #showAvailabilityDeliveryIcon>
<ui-icon *ngIf="showDeliveryTruck$ | async" class="truck" icon="truck" size="30px"></ui-icon>
</ng-template>
<div
class="fetching xsmall"
*ngIf="store.fetchingDeliveryB2BAvailability$ | async; else showAvailabilityDeliveryB2BIcon"
></div>
<ng-template #showAvailabilityDeliveryB2BIcon>
<ui-icon *ngIf="showDeliveryB2BTruck$ | async" class="truck_b2b" icon="truck_b2b" size="40px"> </ui-icon>
</ng-template>
<span *ngIf="store.isDownload$ | async" class="download-icon">
<ui-icon icon="download" size="18px"></ui-icon>
<span class="label">Download</span>
</span>
</div>
</div>
</div>
<div class="shelfinfo right" *ngIf="store.isDownload$ | async">
<div class="page-article-details__shelfinfo" *ngIf="store.isDownload$ | async">
<ng-container
*ngIf="
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
@@ -145,24 +261,7 @@
</ng-container>
</ng-template>
</div>
<div class="row">
<div class="specs">
<ng-container *ngIf="item?.specs?.length > 0">
{{ (item?.specs)[0]?.value }}
</ng-container>
</div>
<div class="right ssc">
<div class="fetching" *ngIf="fetchingAvailabilities$ | async"></div>
<ng-container *ngIf="!(fetchingAvailabilities$ | async)">
<div *ngIf="store.sscText$ | async; let sscText">
{{ sscText }}
</div>
</ng-container>
</div>
</div>
<div class="shelfinfo right" *ngIf="!(store.isDownload$ | async)">
<div class="page-article-details__shelfinfo text-right" *ngIf="!(store.isDownload$ | async)">
<ng-container
*ngIf="
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
@@ -186,109 +285,118 @@
</ng-template>
</div>
</div>
</div>
<div class="bookmark">
<div *ngIf="showArchivBadge$ | async" class="archiv-badge">
<button [uiOverlayTrigger]="archivTooltip" class="bookmark-badge">
<img src="/assets/images/bookmark_benachrichtigung_archiv.svg" alt="Archiv Badge" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #archivTooltip [closeable]="true">
<ng-container *ngIf="isAvailable$ | async; else notAvailable">
Archivtitel. Wird nicht mehr gedruckt. Artikel ist bestellbar, weil lieferbar.
</ng-container>
<ng-template #notAvailable>
Archivtitel. Wird nicht mehr gedruckt. Nicht bestellbar.
</ng-template>
</ui-tooltip>
</button>
</div>
<div *ngIf="showSubscriptionBadge$ | async">
<button [uiOverlayTrigger]="subscribtionTooltip" class="bookmark-badge">
<img src="/assets/images/bookmark_subscription.svg" alt="Fortsetzungsartikel Badge" />
</button>
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #subscribtionTooltip [closeable]="true"
>Artikel ist ein Fortsetzungsartikel,<br />
Artikel muss über eine Aboabteilung<br />
bestellt werden.
</ui-tooltip>
</div>
<div *ngIf="showPromotionBadge$ | async" class="promotion-badge">
<button [uiOverlayTrigger]="promotionTooltip" class="bookmark-badge">
<ui-icon-badge icon="gift" alt="Prämienkatalog Badge"></ui-icon-badge>
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #promotionTooltip [closeable]="true">
Dieser Artikel befindet sich im Prämienkatalog.
</ui-tooltip>
</button>
<div class="page-article-details__product-formats-container mt-3" *ngIf="item.family?.length > 0">
<hr class="bg-[#E6EFF9] border-t-2" />
<div class="pt-3">
<div class="page-article-details__product-formats">
<span class="mr-2">Auch verfügbar als</span>
<ui-slider [scrollDistance]="250">
<a
class="mr-4 text-[#0556B4] font-bold no-underline px-2"
*ngFor="let format of item.family"
[routerLink]="getDetailsPath(format.product.ean)"
[queryParamsHandling]="!(isTablet$ | async) ? 'preserve' : ''"
>
<span class="flex items-center">
<img
class="mr-2"
*ngIf="!!format.product?.format"
[src]="'/assets/images/OF_Icon_' + format.product?.format + '.svg'"
alt="format icon"
/>
{{ format.product?.formatDetail }}
<span class="ml-1">{{ format.catalogAvailability?.price?.value?.value | currency: '€' }}</span>
</span>
</a>
</ui-slider>
</div>
</div>
</div>
<div class="product-actions">
<button *ngIf="!(store.isDownload$ | async)" class="cta-availabilities" (click)="showAvailabilities()">
Vorrätig in anderer Filiale
</button>
<button
class="cta-continue"
(click)="showPurchasingModal()"
[disabled]="
!(isAvailable$ | async) || (fetchingAvailabilities$ | async) || (item?.features && (item?.features)[0]?.key === 'PFO')
"
>
In den Warenkorb
</button>
</div>
<hr class="bg-[#E6EFF9] border-t-2 my-3" />
<hr />
<ng-container *ngIf="item.family?.length > 0">
<div class="product-formats">
<span class="label">Auch verfügbar als</span>
<ui-slider [scrollDistance]="250">
<a
class="product-family"
*ngFor="let format of item.family"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'details', 'ean', format.product.ean]"
>
<span class="format-detail">
<img
*ngIf="!!format.product?.format"
[src]="'/assets/images/OF_Icon_' + format.product?.format + '.svg'"
alt="format icon"
/>
{{ format.product?.formatDetail }}
<span class="price">{{ format.catalogAvailability?.price?.value?.value | currency: '€' }}</span>
</span>
</a>
</ui-slider>
</div>
<hr />
</ng-container>
<div class="product-description" *ngIf="item.texts?.length > 0">
<div class="info">
<div
#description
class="page-article-details__product-description flex flex-col flex-grow overflow-hidden overflow-y-scroll"
*ngIf="item.texts?.length > 0"
>
<div class="whitespace-pre-line">
{{ item.texts[0].value }}
</div>
<div class="product-text">
<button
class="font-bold flex flex-row text-[#0556B4] items-center mt-2"
*ngIf="!showMore && item?.texts?.length > 1"
(click)="showMore = !showMore"
>
Mehr <ui-icon class="ml-2" size="15px" icon="arrow"></ui-icon>
</button>
<div *ngIf="showMore" class="page-article-details__product-description-text flex flex-col whitespace-pre-line break-words">
<span *ngFor="let text of item.texts | slice: 1">
<h3 class="header">{{ text.label }}</h3>
<h3 class="my-4 text-p2 font-bold">{{ text.label }}</h3>
{{ text.value }}
</span>
<button class="scroll-top-cta" (click)="scrollTop()">
<ui-icon class="arrow" icon="arrow" size="20px"></ui-icon>
<button class="font-bold flex flex-row text-[#0556B4] items-center mt-2" (click)="showMore = !showMore">
<ui-icon class="transform ml-0 mr-2 rotate-180" size="15px" icon="arrow"></ui-icon> Weniger
</button>
<button class="page-article-details__scroll-top-cta" (click)="scrollTop(description)">
<ui-icon class="text-[#0556B4]" icon="arrow" size="20px"></ui-icon>
</button>
</div>
<div class="min-h-[6.25rem]"></div>
</div>
<ng-container *ngIf="!showRecommendations">
<div
*ngIf="store.item$ | async; let item"
class="page-article-details__actions w-full absolute text-center left-0 bottom-10 z-fixed"
>
<button
*ngIf="!(store.isDownload$ | async)"
class="text-brand border-2 border-brand bg-white font-bold text-lg px-[1.375rem] py-4 rounded-full mr-px-30"
(click)="showAvailabilities()"
>
Bestände in anderen Filialen
</button>
<button
class="text-white bg-brand border-brand font-bold text-lg px-[1.375rem] py-4 rounded-full border-none no-underline"
(click)="showPurchasingModal()"
[disabled]="
!(isAvailable$ | async) || (fetchingAvailabilities$ | async) || (item?.features && (item?.features)[0]?.key === 'PFO')
"
>
In den Warenkorb
</button>
</div>
</ng-container>
<div class="page-article-details__product-recommendations -mx-5">
<button
*ngIf="store.item$ | async; let item"
class="shadow-[#dce2e9_0px_-2px_18px_0px] sticky bottom-4 border-none outline-none left-0 right-0 flex items-center px-5 h-14 min-h-[3.5rem] bg-white w-full"
(click)="showRecommendations = true"
>
<span class="uppercase text-[#0556B4] font-bold text-p3">Empfehlungen</span>
<img class="absolute right-5 bottom-3 h-12" src="assets/images/recommendation_tag.png" alt="recommendation icon" />
</button>
</div>
</ng-container>
</div>
<button *ngIf="store.item$ | async; let item" class="product-recommendations" (click)="showRecommendations = true">
<span class="label">Empfehlungen</span>
<img src="assets/images/recommendation_tag.png" alt="recommendation icon" />
</button>
</ng-container>
<div class="recommendations-overlay" @slideYAnimation *ngIf="showRecommendations">
<button class="product-button" (click)="showRecommendations = false">{{ (store.item$ | async)?.product?.name }}</button>
<div class="page-article-details__recommendations-overlay absolute top-16 rounded-t" @slideYAnimation *ngIf="showRecommendations">
<button
class="h-[3.75rem] shadow-[0_-2px_24px_0_#dce2e9] flex flex-row justify-center items-center w-full text-xl bg-white text-ucla-blue font-bold border-none outline-none rounded-t"
(click)="showRecommendations = false"
>
{{ (store.item$ | async)?.product?.name }}
</button>
<page-article-recommendations (close)="showRecommendations = false"></page-article-recommendations>
</div>

View File

@@ -1,269 +1,99 @@
:host {
@apply flex flex-col;
@apply box-border block h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
}
.product-card {
@apply flex flex-col bg-white w-full rounded-card shadow-card;
.product-details {
@apply flex flex-row p-5;
.bookmark {
@apply absolute flex;
top: 52px;
right: 25px;
z-index: 100;
}
.bookmark-badge {
@apply p-0 m-0 outline-none border-none bg-transparent relative;
}
.promotion-badge {
margin-top: -1px;
}
.bookmark-badge-gap {
@apply mt-px-35;
}
.product-image {
@apply flex flex-col items-center justify-start mr-5;
.recessions {
@apply flex flex-col items-center mt-4 bg-transparent border-none outline-none;
.cta-recessions {
@apply text-regular text-dark-cerulean font-bold mt-2;
}
}
.image-button {
@apply border-none outline-none bg-transparent relative;
ui-icon {
@apply absolute text-dark-cerulean inline-block;
bottom: 1rem;
right: 1rem;
}
}
img {
@apply rounded-xl shadow-card;
box-shadow: 0 0 18px 0 #b8b3b7;
max-height: 315px;
max-width: 195px;
}
}
.product-info {
@apply w-full;
.title {
@apply text-3xl font-bold mb-6;
}
.format,
.ssc,
.quantity {
@apply font-bold text-lg;
}
.stock {
min-height: 44px;
}
.quantity {
@apply flex justify-end mt-4;
ui-icon {
@apply mr-1 text-ucla-blue;
}
}
.format {
@apply flex items-center;
.format-icon {
@apply flex mr-2;
height: 18px;
}
}
.ssc {
@apply flex justify-end my-2;
}
.price {
@apply font-bold text-xl;
}
.shelfinfo {
@apply text-ucla-blue;
}
.fetching {
@apply w-52 h-px-20;
background-color: #e6eff9;
animation: load 0.75s linear infinite;
}
.xsmall {
@apply w-6;
}
.small {
@apply w-16;
}
.medium {
@apply w-40;
}
.availability-icons {
@apply flex flex-row items-center justify-end text-dark-cerulean mt-4;
ui-icon {
@apply mx-1;
}
.truck {
@apply -mb-px-5 -mt-px-5;
}
.truck_b2b {
@apply -mb-px-10 -mt-px-10;
}
.label {
@apply font-bold;
}
.download-icon {
@apply flex flex-row items-center;
}
}
.cta-print {
@apply bg-transparent text-brand font-bold text-xl outline-none border-none p-0;
}
}
.row {
@apply grid items-end;
grid-template-columns: auto auto;
}
}
.right {
@apply text-right self-start;
}
hr {
@apply bg-glitter h-1;
}
.product-description {
@apply flex flex-col flex-grow px-5 py-5;
min-height: calc(100vh - 769px);
.info {
@apply whitespace-pre-line;
}
.product-text {
@apply flex flex-col whitespace-pre-line mb-px-100 break-words;
h3 {
@apply my-4;
}
.header {
@apply text-regular font-bold;
}
.scroll-top-cta {
@apply flex items-center justify-center self-end border-none outline-none bg-white relative rounded p-0 mt-8 mr-4;
box-shadow: 0px 0px 20px 0px rgba(89, 100, 112, 0.5);
transform: rotate(-90deg);
border-radius: 100%;
width: 58px;
height: 58px;
.arrow {
color: #1f466c;
}
}
}
}
.product-actions {
@apply text-right px-5 py-4;
.cta-availabilities {
@apply text-brand border-none border-brand bg-white font-bold text-lg px-4 py-2 rounded-full;
}
.cta-continue {
@apply text-white bg-brand font-bold text-lg px-4 py-2 rounded-full border-none ml-4 no-underline;
&:disabled {
@apply bg-inactive-branch;
}
}
}
.product-formats {
@apply grid whitespace-nowrap items-center px-5 py-4;
grid-template-rows: auto;
grid-template-columns: auto 1fr;
max-width: 100%;
.label {
@apply mr-2;
}
.product-family {
@apply mr-4 text-active-customer font-bold no-underline px-2;
.format-detail {
@apply flex items-center;
img {
@apply mr-2;
}
}
.price {
@apply ml-1;
}
}
}
.page-article-details__container {
@apply h-full w-full bg-white rounded shadow-card flex flex-col;
}
.product-recommendations {
@apply sticky bottom-0 border-none outline-none left-0 right-0 flex items-center px-5 h-16 bg-white w-full;
box-shadow: #dce2e9 0px -2px 18px 0px;
.label {
@apply uppercase text-active-customer font-bold text-small;
}
img {
@apply absolute right-5 bottom-5 h-12;
}
.page-article-details__product-details {
@apply grid gap-x-5;
grid-template-columns: max-content auto;
grid-template-rows: 2.1875rem repeat(11, minmax(auto, max-content));
grid-template-areas:
'. . . bookmark'
'image contributors contributors contributors'
'image title title print'
'image title title .'
'image misc misc price'
'image misc misc price'
'image origin origin stock'
'image origin origin stock'
'image specs availabilities availabilities'
'image specs ssc ssc'
'image . ssc ssc'
'image . ssc ssc';
}
.recommendations-overlay {
@apply absolute w-full top-0 rounded-t-card;
top: 56px;
.page-article-details__product-bookmark {
grid-area: bookmark;
}
.product-button {
@apply flex flex-row justify-center items-center w-full text-xl bg-white text-ucla-blue font-bold border-none outline-none rounded-t-card;
box-shadow: 0 -2px 24px 0 #dce2e9;
height: 60px;
.page-article-details__product-image-recessions {
grid-area: image;
}
.page-article-details__product-contributors {
grid-area: contributors;
}
.page-article-details__product-print {
grid-area: print;
}
.page-article-details__product-title {
grid-area: title;
}
.page-article-details__product-misc {
grid-area: misc;
}
.page-article-details__product-price-info {
grid-area: price;
}
.page-article-details__product-origin-infos {
grid-area: origin;
}
.page-article-details__product-stock {
grid-area: stock;
}
.page-article-details__product-ean-specs {
grid-area: specs;
}
.page-article-details__product-availabilities {
grid-area: availabilities;
}
.page-article-details__shelf-ssc {
grid-area: ssc;
}
.page-article-details__product-description-text {
word-break: break-word;
}
.page-article-details__product-formats {
@apply grid whitespace-nowrap items-center max-w-full;
grid-template-rows: auto;
grid-template-columns: auto 1fr;
}
.page-article-details__scroll-top-cta {
@apply flex items-center justify-center self-end border-none outline-none bg-white relative rounded p-0 mt-8 mr-4;
box-shadow: 0px 0px 20px 0px rgba(89, 100, 112, 0.5);
transform: rotate(-90deg);
border-radius: 100%;
width: 58px;
height: 58px;
}
.page-article-details__actions {
&:disabled {
@apply bg-inactive-branch;
}
}
.autor {
@apply text-active-customer font-bold no-underline;
}

View File

@@ -1,16 +1,14 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainPrinterService } from '@domain/printer';
import { ItemDTO as PrinterItemDTO } from '@swagger/print';
import { PrintModalComponent, PrintModalData } from '@modal/printer';
import { AvailabilityDTO, BranchDTO } from '@swagger/checkout';
import { BranchDTO } from '@swagger/checkout';
import { UiModalService } from '@ui/modal';
import { ModalReviewsComponent } from '@modal/reviews';
import { PurchasingOptionsModalComponent, PurchasingOptionsModalData } from 'apps/page/checkout/src/lib/modals/purchasing-options-modal';
import { PurchasingOptions } from 'apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.store';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
import { debounceTime, filter, first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ArticleDetailsStore } from './article-details.store';
import { ModalImagesComponent } from 'apps/modal/images/src/public-api';
import { ProductImageService } from 'apps/cdn/product-image/src/public-api';
@@ -20,7 +18,11 @@ import { BreadcrumbService } from '@core/breadcrumb';
import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { DatePipe } from '@angular/common';
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
import { DomainAvailabilityService } from '@domain/availability';
import { EnvironmentService } from '@core/environment';
import { ProductCatalogNavigationService } from '@shared/services';
import { DomainCheckoutService } from '@domain/checkout';
@Component({
selector: 'page-article-details',
@@ -66,6 +68,10 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
this.store.isDeliveryB2BAvailabilityAvailable$,
]).pipe(map(([digDelivery, b2bDelivery]) => b2bDelivery && !digDelivery));
customerFeatures$ = this.applicationService.activatedProcessId$.pipe(
switchMap((processId) => this._domainCheckoutService.getCustomerFeatures({ processId }))
);
showSubscriptionBadge$ = this.store.item$.pipe(map((item) => item?.features?.find((i) => i.key === 'PFO')));
showPromotionBadge$ = this.store.item$.pipe(map((item) => item?.features?.find((i) => i.key === 'Promotion')));
@@ -114,6 +120,19 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
shareReplay(1)
);
get isTablet$() {
return this._environment.matchTablet$.pipe(
map((state) => state?.matches),
shareReplay()
);
}
get resultsPath() {
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
}
showMore: boolean = false;
constructor(
public readonly applicationService: ApplicationService,
private activatedRoute: ActivatedRoute,
@@ -125,7 +144,12 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
private _dateAdapter: DateAdapter,
private _datePipe: DatePipe,
public elementRef: ElementRef,
private _availability: DomainAvailabilityService
private _purchaseOptionsModalService: PurchaseOptionsModalService,
private _availability: DomainAvailabilityService,
private _navigationService: ProductCatalogNavigationService,
private _environment: EnvironmentService,
private _router: Router,
private _domainCheckoutService: DomainCheckoutService
) {}
ngOnInit() {
@@ -160,16 +184,30 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
filter((f) => !!f)
);
const more$ = this.activatedRoute.params.subscribe(() => (this.showMore = false));
this.subscriptions.add(processIdSubscription);
this.subscriptions.add(more$);
this.subscriptions.add(this.store.loadItemById(id$));
this.subscriptions.add(this.store.loadItemByEan(ean$));
this.subscriptions.add(this.store.item$.pipe(filter((item) => !!item)).subscribe((item) => this.updateBreadcrumb(item)));
this.subscriptions.add(
this.store.item$
.pipe(
withLatestFrom(this.isTablet$),
filter(([item, isTablet]) => !!item)
)
.subscribe(([item, isTablet]) => (isTablet ? this.updateBreadcrumb(item) : this.updateBreadcrumbDesktop(item)))
);
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
getDetailsPath(ean?: string) {
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, ean });
}
async updateBreadcrumb(item: ItemDTO) {
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details', `${item.id}`])
@@ -183,13 +221,41 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
this.breadcrumb.addBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
name: item.product?.name,
path: `/kunde/${this.applicationService.activatedProcessId}/product/details/${item.id}`,
path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
params: this.activatedRoute.snapshot.queryParams,
tags: ['catalog', 'details', `${item.id}`],
section: 'customer',
});
}
async updateBreadcrumbDesktop(item: ItemDTO) {
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details'])
.pipe(first())
.toPromise();
if (crumbs.length === 0) {
this.breadcrumb.addBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
name: item.product?.name,
path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
params: this.activatedRoute.snapshot.queryParams,
tags: ['catalog', 'details', `${item.id}`],
section: 'customer',
});
} else {
const crumb = crumbs.find((_) => true);
this.breadcrumb.patchBreadcrumb(crumb.id, {
key: this.applicationService.activatedProcessId,
name: item.product?.name,
path: this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: item.id }),
params: this.activatedRoute.snapshot.queryParams,
tags: ['catalog', 'details', `${item.id}`],
section: 'customer',
});
}
}
async print() {
const item = await this.store.item$.pipe(first()).toPromise();
this.uiModal.open({
@@ -262,64 +328,73 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
}
async showPurchasingModal(selectedBranch?: BranchDTO) {
let availableOptions: PurchasingOptions[] = [];
const availabilities: { [key: string]: AvailabilityDTO } = {};
const item = await this.store.item$.pipe(first()).toPromise();
const takeNow = await this.store.isTakeAwayAvailabilityAvailable$.pipe(first()).toPromise();
if (takeNow) {
availableOptions.push('take-away');
availabilities['take-away'] = await this.store.takeAwayAvailability$.pipe(first()).toPromise();
}
const download = await this.store.isDownloadAvailabilityAvailable$.pipe(first()).toPromise();
if (download) {
availableOptions.push('download');
availabilities['download'] = await this.store.downloadAvailability$.pipe(first()).toPromise();
}
const pickup = await this.store.isPickUpAvailabilityAvailable$.pipe(first()).toPromise();
if (pickup) {
availableOptions.push('pick-up');
availabilities['pick-up'] = await this.store.pickUpAvailability$.pipe(first()).toPromise();
}
const digDelivery = await this.store.isDeliveryDigAvailabilityAvailable$.pipe(first()).toPromise();
if (digDelivery) {
availableOptions.push('dig-delivery');
availabilities['dig-delivery'] = await this.store.deliveryDigAvailability$.pipe(first()).toPromise();
}
const b2b = await this.store.isDeliveryB2BAvailabilityAvailable$.pipe(first()).toPromise();
if (b2b) {
availableOptions.push('b2b-delivery');
availabilities['b2b-delivery'] = await this.store.deliveryB2BAvailability$.pipe(first()).toPromise();
}
if (availableOptions.includes('dig-delivery') && availableOptions.includes('b2b-delivery')) {
availableOptions.push('delivery');
availabilities['delivery'] = await this.store.deliveryAvailability$.pipe(first()).toPromise();
availableOptions = availableOptions.filter((option) => !(option === 'dig-delivery' || option === 'b2b-delivery'));
}
const branch = selectedBranch || (await this.store.branch$.pipe(first()).toPromise());
this.uiModal.open({
content: PurchasingOptionsModalComponent,
data: {
availableOptions,
option: selectedBranch ? 'take-away' : undefined,
item: await this.store.item$.pipe(first()).toPromise(),
branchId: branch?.id,
this._purchaseOptionsModalService
.open({
type: 'add',
processId: this.applicationService.activatedProcessId,
availabilities,
} as PurchasingOptionsModalData,
});
items: [item],
})
.afterClosed$.subscribe(async (result) => {
if (result?.data === 'continue') {
const customer = await this._domainCheckoutService
.getBuyer({ processId: this.applicationService.activatedProcessId })
.pipe(first())
.toPromise();
if (customer) {
this.navigateToShoppingCart();
} else {
this.navigateToCustomerSearch();
}
console.log('continue');
} else if (result?.data === 'continue-shopping') {
this.navigateToResultList();
console.log('continue-shopping');
}
});
}
scrollTop() {
const element = this.elementRef.nativeElement.closest('.main-wrapper');
element?.scrollTo({ top: 0, behavior: 'smooth' });
navigateToShoppingCart() {
this._router.navigate([`/kunde/${this.applicationService.activatedProcessId}/cart/review`]);
}
async navigateToCustomerSearch() {
try {
const response = await this.customerFeatures$
.pipe(
first(),
switchMap((customerFeatures) => {
return this._domainCheckoutService.canSetCustomer({ processId: this.applicationService.activatedProcessId, customerFeatures });
})
)
.toPromise();
this._router.navigate(['/kunde', this.applicationService.activatedProcessId, 'customer', 'search'], {
queryParams: { filter_customertype: response.filter.customertype },
});
} catch (error) {
this._router.navigate(['/kunde', this.applicationService.activatedProcessId, 'customer', 'search']);
}
}
async navigateToResultList() {
let crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog'])
.pipe(first())
.toPromise();
crumbs = crumbs.filter((crumb) => !crumb.tags?.includes('details'));
const crumb = crumbs[crumbs.length - 1];
if (crumb) {
this._router.navigate([crumb.path], { queryParams: crumb.params });
} else {
this._router.navigate([`/kunde/${this.applicationService.activatedProcessId}/product`]);
}
}
scrollTop(div: HTMLDivElement) {
div?.scrollTo({ top: 0, behavior: 'smooth' });
}
loadImage() {

View File

@@ -1,7 +1,6 @@
:host {
@apply flex flex-col bg-white;
@apply flex flex-col bg-white h-[calc(100vh-16.5rem-3.75rem)] desktop-small:h-[calc(100vh-15.1rem-3.75rem)];
box-shadow: 0px -2px 24px 0px #dce2e9;
height: calc(100vh - 342px);
}
h1 {
@@ -18,7 +17,7 @@ p {
@apply mt-10;
.label {
@apply flex flex-row items-center uppercase text-dark-cerulean font-bold text-sm ml-6;
@apply flex flex-row items-center uppercase text-dark-cerulean font-bold text-p3 ml-6;
ui-icon {
@apply mr-2;
@@ -26,7 +25,7 @@ p {
}
.empty-message {
@apply flex justify-center text-cta-l mt-12;
@apply flex justify-center text-p1 mt-12;
}
.article {
@@ -40,7 +39,7 @@ p {
}
.format {
@apply text-regular mt-2;
@apply text-p2 mt-2;
}
.price {

View File

@@ -1,13 +1 @@
<button class="filter" [class.active]="hasFilter$ | async" (click)="filterActive$.next(true); shellFilterOverlay.open()">
<ui-icon size="20px" icon="filter_alit"></ui-icon>
<span class="label">Filter</span>
</button>
<router-outlet></router-outlet>
<shell-filter-overlay #shellFilterOverlay>
<page-article-search-filter
*ngIf="filterActive$ | async"
(close)="filterActive$.next(false); shellFilterOverlay.close()"
></page-article-search-filter>
</shell-filter-overlay>

View File

@@ -1,17 +1,3 @@
:host {
@apply flex flex-col w-full box-content relative;
}
.filter {
@apply font-sans flex self-end items-center mb-4 font-bold bg-wild-blue-yonder border-0 text-regular py-px-8 px-px-15 rounded-filter justify-center z-sticky;
width: 106px;
min-width: 106px;
.label {
@apply ml-px-5;
}
&.active {
@apply bg-active-customer text-white ml-px-5;
}
@apply flex flex-col w-full h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)] box-content relative;
}

View File

@@ -1,13 +1,15 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { UiFilterAutocompleteProvider } from '@ui/filter';
import { isEqual } from 'lodash';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { filter, first, map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { Observable, Subject } from 'rxjs';
import { map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from './article-search.store';
import { FocusSearchboxEvent } from './focus-searchbox.event';
import { ArticleSearchMainAutocompleteProvider } from './providers';
import { ProductCatalogNavigationService } from '@shared/services';
import { FilterAutocompleteProvider } from 'apps/shared/components/filter/src/lib';
import { isEqual } from 'lodash';
import { EnvironmentService } from '@core/environment';
@Component({
selector: 'page-article-search',
@@ -15,9 +17,8 @@ import { ArticleSearchMainAutocompleteProvider } from './providers';
styleUrls: ['article-search.component.scss'],
providers: [
FocusSearchboxEvent,
ArticleSearchService,
{
provide: UiFilterAutocompleteProvider,
provide: FilterAutocompleteProvider,
useClass: ArticleSearchMainAutocompleteProvider,
multi: true,
},
@@ -28,22 +29,16 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
private _onDestroy$ = new Subject();
private _processId$: Observable<number>;
initialFilter$ = this._articleSearch.filter$.pipe(
filter((filter) => !!filter),
first()
);
get isTablet() {
return this._environmentService.matchTablet();
}
hasFilter$ = this._articleSearch.filter$.pipe(
withLatestFrom(this.initialFilter$),
map(([filter, initialFilter]) => !isEqual(filter?.getQueryParams(), initialFilter?.getQueryParams()))
);
filterActive$ = new BehaviorSubject<boolean>(false);
constructor(
private _breadcrumb: BreadcrumbService,
private _router: Router,
private _articleSearch: ArticleSearchService,
private _activatedRoute: ActivatedRoute
private _activatedRoute: ActivatedRoute,
private _navigationService: ProductCatalogNavigationService,
private _environmentService: EnvironmentService
) {}
ngOnInit() {
@@ -60,12 +55,17 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
.subscribe(([state, processId]) => {
if (state.searchState === '') {
const params = state.filter.getQueryParams();
if (state.hits === 1) {
const item = state.items.find((f) => f);
this._router.navigate(['/kunde', processId, 'product', 'details', item.id]);
this._navigationService.navigateToDetails({
processId,
itemId: item.id,
queryParams: this.isTablet ? undefined : params,
});
} else {
const params = state.filter.getQueryParams();
this._router.navigate(['/kunde', processId, 'product', 'search', 'results'], {
this._navigationService.navigateToResults({
processId,
queryParams: params,
});
}
@@ -73,6 +73,20 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
});
}
cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params };
for (const key in clean) {
if (Object.prototype.hasOwnProperty.call(clean, key)) {
if (clean[key] == undefined) {
delete clean[key];
}
}
}
return clean;
}
initProcessId() {
this._processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId)));
}
@@ -96,7 +110,7 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
await this._breadcrumb.addBreadcrumbIfNotExists({
key: processId,
name: 'Artikelsuche',
path: `/kunde/${processId}/product`,
path: this._navigationService.getArticleSearchBasePath(processId),
params: queryParams,
tags: ['catalog', 'main'],
section: 'customer',

View File

@@ -6,12 +6,13 @@ import { ArticleSearchComponent } from './article-search.component';
import { SearchResultsModule } from './search-results/search-results.module';
import { SearchMainModule } from './search-main/search-main.module';
import { SearchFilterModule } from './search-filter/search-filter.module';
import { ShellFilterOverlayModule } from '@shell/filter-overlay';
import { ArticleSearchService } from './article-search.store';
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
@NgModule({
imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, ShellFilterOverlayModule],
imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, SharedFilterOverlayModule],
exports: [ArticleSearchComponent],
declarations: [ArticleSearchComponent],
providers: [],
providers: [ArticleSearchService],
})
export class ArticleSearchModule {}

View File

@@ -3,24 +3,31 @@ import { DomainCatalogService } from '@domain/catalog';
import { Observable, Subject } from 'rxjs';
import { debounceTime, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { UiFilter } from '@ui/filter';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ItemDTO, QueryTokenDTO } from '@swagger/cat';
import { ItemDTO, QueryTokenDTO, UISettingsDTO } from '@swagger/cat';
import { ApplicationService } from '@core/application';
import { BranchDTO } from '@swagger/checkout';
import { Filter } from 'apps/shared/components/filter/src/lib';
export interface ArticleSearchState {
processId: number;
filter: UiFilter;
filter: Filter;
searchState: '' | 'fetching' | 'empty' | 'error';
items: ItemDTO[];
hits: number;
selectedBranch: BranchDTO;
selectedItemIds: number[];
defaultSettings?: UISettingsDTO;
}
@Injectable()
export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
get defaultSettings() {
return this.get((s) => s.defaultSettings);
}
readonly defaultSettings$ = this.select((s) => s.defaultSettings);
get processId() {
return this.get((s) => s.processId);
}
@@ -100,19 +107,19 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
}
async setDefaultFilter(defaultQueryParams?: Record<string, string>) {
const filter = await this.catalog
.getSettings()
.pipe(map((settings) => UiFilter.create(settings)))
.toPromise();
const defaultSettings = await this.catalog.getSettings().toPromise();
const filter = Filter.create(defaultSettings);
if (!!defaultQueryParams) {
filter?.fromQueryParams(defaultQueryParams);
}
this.setFilter(filter);
this.patchState({ defaultSettings });
}
setFilter(filter: UiFilter) {
setFilter(filter: Filter) {
this.patchState({ filter });
}

View File

@@ -1,18 +1,18 @@
import { Injectable } from '@angular/core';
import { DomainCatalogService } from '@domain/catalog';
import { UiFilterAutocomplete, UiFilterAutocompleteProvider, UiInput } from '@ui/filter';
import { FilterAutocomplete, FilterAutocompleteProvider, FilterInput } from 'apps/shared/components/filter/src/lib';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
@Injectable()
export class ArticleSearchMainAutocompleteProvider extends UiFilterAutocompleteProvider {
export class ArticleSearchMainAutocompleteProvider extends FilterAutocompleteProvider {
for = 'catalog';
constructor(private domainCatalogSearch: DomainCatalogService) {
super();
}
complete(input: UiInput): Observable<UiFilterAutocomplete[]> {
complete(input: FilterInput): Observable<FilterAutocomplete[]> {
const token = input?.parent?.parent?.getQueryToken();
const filter = token?.filter;
const type = Object.keys(token?.input).join(';');

View File

@@ -1,30 +1,38 @@
<ng-container *ngIf="filter$ | async; let filter">
<div class="catalog-search-filter-content">
<button class="btn-close" type="button" (click)="close.emit()">
<ui-icon icon="close" size="20px"></ui-icon>
</button>
<div class="w-full flex flex-row justify-end items-center">
<button (click)="clearFilter(filter)" class="text-[#0556B4] p-4">Alle Filter entfernen</button>
<a
*ngIf="showFilterClose$ | async"
class="text-black p-4 outline-none border-none bg-transparent"
[routerLink]="closeFilterRoute"
queryParamsHandling="preserve"
>
<ui-icon icon="close" size="15px"></ui-icon>
</a>
</div>
<div class="catalog-search-filter-content-main">
<h1 class="text-3xl font-bold text-center py-4">Filter</h1>
<ui-filter
<div class="catalog-search-filter-content-main -mt-14 desktop-small:-mt-8 desktop:-mt-12">
<h1 class="text-h3 text-[1.625rem] font-bold text-center pt-6 pb-10">Filter</h1>
<shared-filter
[filter]="filter"
[loading]="fetching$ | async"
(search)="applyFilter(filter)"
[hint]="searchboxHint$ | async"
resizeInputOptionsToElement="page-article-search-filter .cta-wrapper"
[scanner]="true"
></ui-filter>
></shared-filter>
</div>
<div class="cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter(filter)" [disabled]="fetching$ | async">
Filter zurücksetzen
</button>
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="fetching$ | async">
<ui-spinner [show]="fetching$ | async">
Filter anwenden
</ui-spinner>
</button>
</div>
</div>
<div class="cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter(filter)" [disabled]="fetching$ | async">
Filter zurücksetzen
</button>
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="fetching$ | async">
<ui-spinner [show]="fetching$ | async">
Filter anwenden
</ui-spinner>
</button>
</div>
</ng-container>

View File

@@ -1,15 +1,11 @@
:host {
@apply block bg-glitter;
@apply block bg-white h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
}
.catalog-search-filter-content {
@apply relative mx-auto p-4;
}
.btn-close {
@apply absolute text-cool-grey top-3 p-4 right-4 outline-none border-none bg-transparent;
}
.catalog-search-filter-content-main {
h1.title {
@apply text-center;
@@ -17,14 +13,12 @@
}
.cta-wrapper {
@apply fixed bottom-8 whitespace-nowrap;
left: 50%;
transform: translateX(-50%);
@apply text-center whitespace-nowrap absolute bottom-8 left-0 w-full;
}
.cta-reset-filter,
.cta-apply-filter {
@apply text-lg font-bold px-6 py-3 rounded-full border-solid border-2 border-brand outline-none mx-2;
@apply text-lg font-bold px-6 py-[0.85rem] rounded-full border-solid border-2 border-brand outline-none mx-2;
&:disabled {
@apply bg-inactive-branch cursor-not-allowed border-none text-white;
@@ -38,3 +32,7 @@
.cta-apply-filter {
@apply text-white bg-brand;
}
::ng-deep page-article-search-filter shared-filter shared-filter-input-group-main {
@apply desktop:hidden px-16;
}

View File

@@ -1,8 +1,12 @@
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
import { UiFilter, UiFilterComponent } from '@ui/filter';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { Observable, Subject } from 'rxjs';
import { map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { ActivatedRoute } from '@angular/router';
import { ProductCatalogNavigationService } from '@shared/services';
import { Filter, FilterComponent } from 'apps/shared/components/filter/src/lib';
@Component({
selector: 'page-article-search-filter',
@@ -10,51 +14,102 @@ import { ArticleSearchService } from '../article-search.store';
styleUrls: ['search-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArticleSearchFilterComponent implements OnInit {
export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
@Output()
close = new EventEmitter();
_processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId)));
fetching$: Observable<boolean>;
filter$: Observable<UiFilter>;
filter$: Observable<Filter>;
searchboxHint$ = this.articleSearch.searchboxHint$;
@ViewChild(UiFilterComponent, { static: false })
uiFilterComponent: UiFilterComponent;
@ViewChild(FilterComponent, { static: false })
uiFilterComponent: FilterComponent;
constructor(private articleSearch: ArticleSearchService) {}
get isDesktop() {
return this._environment.matchDesktop();
}
get showFilterClose$() {
return this._environment.matchDesktop$.pipe(map((state) => !(state?.matches && this.leftOutlet === 'search')));
}
get leftOutlet() {
return this._navigationService.getOutletLocations(this._activatedRoute)?.left;
}
get mainOutlet() {
return this._navigationService.getOutletLocations(this._activatedRoute)?.main;
}
get closeFilterRoute() {
const processId = Number(this._activatedRoute?.parent?.snapshot?.data?.processId);
const itemId = this._navigationService.getOutletParams(this._activatedRoute)?.right?.id;
if (!itemId) {
if (this.leftOutlet === 'search') {
return this._navigationService.getArticleSearchBasePath(processId);
} else if (this.mainOutlet === 'results' || this.leftOutlet === 'results') {
return this._navigationService.getArticleSearchResultsPath(processId);
}
} else {
return this._navigationService.getArticleDetailsPath({ processId, itemId });
}
}
private _onDestroy$ = new Subject();
constructor(
private articleSearch: ArticleSearchService,
private _environment: EnvironmentService,
private _activatedRoute: ActivatedRoute,
public application: ApplicationService,
private _navigationService: ProductCatalogNavigationService
) {}
ngOnInit() {
this.fetching$ = this.articleSearch.fetching$;
this.filter$ = this.articleSearch.filter$.pipe(
map((filter) => UiFilter.create(filter))
// tap((filter) =>
// filter.fromQueryParams({
// main_qs: 'harry potter',
// filter_format: 'eb;!hc',
// filter_dbhwgr: '110;121',
// main_author: 'author',
// filter_region: '9780*|9781*;97884*',
// 'filter_reading-age': '1-10',
// main_stock: '1-',
// })
// )
);
this.filter$ = this.articleSearch.filter$.pipe(map((filter) => Filter.create(filter)));
}
applyFilter(value: UiFilter) {
ngOnDestroy(): void {
this._onDestroy$.next();
this._onDestroy$.complete();
}
applyFilter(value: Filter) {
this.uiFilterComponent?.cancelAutocomplete();
this.articleSearch.setFilter(value);
this.articleSearch.search({ clear: true });
this.articleSearch.searchCompleted.pipe(take(1)).subscribe((s) => {
if (s.searchState === '') {
this.close.emit();
}
});
this.articleSearch.searchCompleted
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
.subscribe(([state, processId]) => {
if (state.searchState === '') {
// Check if desktop is necessary, otherwise it would trigger navigation twice (Inside Article-Search.component and here)
if (state.hits === 1 && !this.isDesktop) {
const item = state.items.find((f) => f);
this._navigationService.navigateToDetails({
processId,
itemId: item.id,
});
} else if (!this.isDesktop) {
const params = state.filter.getQueryParams();
this._navigationService.navigateToResults({
processId,
queryParams: params,
});
}
}
});
}
resetFilter(value: UiFilter) {
clearFilter(value: Filter) {
value.unselectAll();
}
resetFilter(value: Filter) {
const queryParams = { main_qs: value?.getQueryParams()?.main_qs || '' };
this.articleSearch.setDefaultFilter(queryParams);
}

View File

@@ -1,13 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UiFilterNextModule } from '@ui/filter';
import { RouterModule } from '@angular/router';
import { UiIconModule } from '@ui/icon';
import { UiSpinnerModule } from '@ui/spinner';
import { ArticleSearchFilterComponent } from './search-filter.component';
import { FilterNextModule } from 'apps/shared/components/filter/src/lib';
@NgModule({
imports: [CommonModule, UiFilterNextModule, UiIconModule, UiSpinnerModule],
imports: [CommonModule, RouterModule, FilterNextModule, UiIconModule, UiSpinnerModule],
exports: [ArticleSearchFilterComponent],
declarations: [ArticleSearchFilterComponent],
providers: [],

View File

@@ -1,26 +1,50 @@
<div class="card-search-article">
<h1 class="title">Artikelsuche</h1>
<p class="info">
<div class="bg-white rounded py-10 px-4 text-center shadow-[0_-2px_24px_0_#dce2e9] h-full">
<h1 class="text-h3 text-[1.625rem] font-bold mb-[0.375rem]">Artikelsuche</h1>
<p class="text-lg mb-10">
Welchen Artikel suchen Sie?
</p>
<ng-container *ngIf="filter$ | async; let filter">
<ui-filter-filter-group-main [inputGroup]="filter?.filter | group: 'main'"></ui-filter-filter-group-main>
<ui-filter-input-group-main
[hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
[showDescription]="false"
[scanner]="true"
></ui-filter-input-group-main>
<shared-filter-filter-group-main
class="mb-8 w-full"
*ngIf="!(isDesktop$ | async)"
[inputGroup]="filter?.filter | group: 'main'"
></shared-filter-filter-group-main>
<div class="flex flex-row px-12 justify-center desktop:px-0">
<shared-filter-input-group-main
class="block w-full mr-3 desktop:mx-auto"
[hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
<a
*ngIf="!(isDesktop$ | async)"
class="page-search-main__filter w-[6.75rem] h-14 rounded font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
[routerLink]="openFilterRoute"
queryParamsHandling="preserve"
>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</a>
</div>
<div class="recent-searches-wrapper">
<h3 class="recent-searches-header">Deine letzten Suchanfragen</h3>
<ul>
<li class="recent-searches-items" *ngFor="let recentQuery of history$ | async">
<button (click)="setQueryHistory(filter, recentQuery.friendlyName)">
<ui-icon icon="search" size="15px"></ui-icon>
<p>{{ recentQuery.friendlyName }}</p>
<div class="flex flex-col items-start ml-12 desktop:ml-8 py-6 bg-white">
<h3 class="text-p3 font-bold mb-3">Deine letzten Suchanfragen</h3>
<ul class="flex flex-col justify-start overflow-hidden items-start m-0 p-0 bg-white w-full">
<li class="list-none pb-3" *ngFor="let recentQuery of history$ | async">
<button
class="flex flex-row items-center outline-none border-none bg-white text-black text-p2 m-0 p-0"
(click)="setQueryHistory(filter, recentQuery.friendlyName)"
>
<ui-icon
class="flex w-8 h-8 justify-center items-center mr-3 rounded-full text-black bg-[#edeff0]"
icon="search"
size="0.875rem"
></ui-icon>
<p class="m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-[25rem]">{{ recentQuery.friendlyName }}</p>
</button>
</li>
</ul>

View File

@@ -1,71 +1,13 @@
:host {
@apply flex flex-col box-border;
@apply flex flex-col box-border h-full;
}
.title {
@apply text-page-heading font-bold;
}
.info {
@apply text-2xl mt-1 mb-px-30;
}
.filter-chips {
@apply flex flex-row justify-center;
}
.card-search-article {
@apply bg-white rounded p-4 text-center;
box-shadow: 0 -2px 24px 0 #dce2e9;
}
.card-search-article {
min-height: calc(100vh - 380px);
}
ui-filter-filter-group-main {
@apply mb-8 w-full;
.page-search-main__filter {
&.active {
@apply bg-[#596470] text-white ml-px-5;
}
}
::ng-deep page-article-search-main ui-filter-filter-group-main .ui-filter-chip:not(.selected) {
@apply bg-glitter;
}
ui-filter-input-group-main {
@apply block mx-auto;
max-width: 600px;
}
.recent-searches-wrapper {
@apply flex flex-col mx-auto items-start py-6 bg-white;
width: 50%;
z-index: 0;
.recent-searches-header {
@apply text-sm font-bold mb-4;
}
ul {
@apply flex flex-col justify-start overflow-hidden items-start m-0 p-0 bg-white w-full;
z-index: 0;
.recent-searches-items {
@apply list-none pb-px-15;
button {
@apply flex flex-row items-center outline-none border-none bg-white text-black text-base m-0 p-0;
ui-icon {
@apply flex w-px-35 h-px-35 justify-center items-center mr-3 rounded-full text-black;
background-color: #e6eff9;
}
p {
@apply m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis;
max-width: 400px;
}
}
}
}
}

View File

@@ -3,11 +3,13 @@ import { ActivatedRoute } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { ApplicationService } from '@core/application';
import { DomainCatalogService } from '@domain/catalog';
import { UiFilter, UiFilterInputGroupMainComponent } from '@ui/filter';
import { combineLatest, NEVER, Subscription } from 'rxjs';
import { catchError, debounceTime, first, switchMap } from 'rxjs/operators';
import { catchError, debounceTime, first, switchMap, map, shareReplay } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { isEqual } from 'lodash';
import { EnvironmentService } from '@core/environment';
import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib';
import { ProductCatalogNavigationService } from '@shared/services';
@Component({
selector: 'page-article-search-main',
@@ -26,15 +28,47 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
subscriptions = new Subscription();
@ViewChild(UiFilterInputGroupMainComponent, { static: false })
uiInputGroupMain: UiFilterInputGroupMainComponent;
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
map(([filter, defaultFilter]) => {
const filterQueryParams = filter?.getQueryParams();
return !isEqual(this.resetQueryParamsQueryAndOrderBy(filterQueryParams), Filter.create(defaultFilter).getQueryParams());
})
);
@ViewChild(FilterInputGroupMainComponent, { static: false })
sharedFilterInputGroupMain: FilterInputGroupMainComponent;
get isDesktop$() {
return this._environment.matchDesktop$.pipe(
map((state) => state?.matches),
shareReplay()
);
}
get leftOutlet() {
return this._navigationService.getOutletLocations(this.route.parent)?.left;
}
get openFilterRoute() {
if (this.leftOutlet === 'search') {
return this._navigationService.getArticleSearchBaseAndFilterPath(this.application.activatedProcessId);
} else {
const itemId = this._navigationService?.getOutletParams(this.route)?.right?.id;
return this._navigationService.getArticleSearchResultsAndFilterPath({
processId: this.application.activatedProcessId,
itemId,
});
}
}
constructor(
private searchService: ArticleSearchService,
private catalog: DomainCatalogService,
private route: ActivatedRoute,
private application: ApplicationService,
private breadcrumb: BreadcrumbService
private breadcrumb: BreadcrumbService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService
) {}
ngOnInit() {
@@ -44,7 +78,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
.subscribe(async ([processId, queryParams]) => {
const processChanged = processId !== this.searchService.processId;
if (!(this.searchService.filter instanceof UiFilter)) {
if (!(this.searchService.filter instanceof Filter)) {
await this.searchService.setDefaultFilter();
}
@@ -57,6 +91,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
const cleanQueryParams = this.cleanupQueryParams(queryParams);
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
// Reset Filter on Product Search Shell Navigation click
await this.searchService.setDefaultFilter(queryParams);
}
@@ -85,16 +120,30 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
this.updateBreadcrumb(this.searchService.processId, this.searchService.filter.getQueryParams());
}
search(filter: UiFilter) {
this.uiInputGroupMain.cancelAutocomplete();
search(filter: Filter) {
this.sharedFilterInputGroupMain.cancelAutocomplete();
this.searchService.setFilter(filter);
this.searchService.search({ clear: true });
}
setQueryHistory(filter: UiFilter, query: string) {
setQueryHistory(filter: Filter, query: string) {
filter.fromQueryParams({ main_qs: query });
}
resetQueryParamsQueryAndOrderBy(params: Record<string, string> = {}) {
const clean = { ...params };
for (const key in clean) {
if (key === 'main_qs' || key?.includes('order_by')) {
clean[key] = undefined;
} else if (key?.includes('order_by')) {
delete clean[key];
}
}
return clean;
}
cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params };

View File

@@ -1,11 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UiFilterNextModule } from '@ui/filter';
import { UiIconModule } from '@ui/icon';
import { ArticleSearchMainComponent } from './search-main.component';
import { FilterNextModule } from 'apps/shared/components/filter/src/lib';
import { RouterModule } from '@angular/router';
@NgModule({
imports: [CommonModule, UiIconModule, UiFilterNextModule],
imports: [CommonModule, RouterModule, UiIconModule, FilterNextModule],
exports: [ArticleSearchMainComponent],
declarations: [ArticleSearchMainComponent],
providers: [],

View File

@@ -1,17 +1,38 @@
<div class="thumbnail animation"></div>
<div class="col">
<div class="author animation"></div>
<div class="row">
<div class="title animation"></div>
<div class="price animation"></div>
<ng-container *ngIf="!(mainOutletActive$ | async); else mainOutlet">
<div class="bg-ucla-blue rounded w-[4.375rem] h-[5.625rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-col flex-grow">
<div class="h-4 bg-ucla-blue ml-4 mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-row justify-between flex-grow">
<div class="h-6 bg-ucla-blue ml-4 w-[12.5rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-6 bg-ucla-blue ml-4 w-[4.6875rem] animate-[load_1s_linear_infinite]"></div>
</div>
<div class="flex-grow"></div>
<div class="flex flex-row justify-between flex-grow">
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue ml-4 w-[3.125rem] animate-[load_1s_linear_infinite]"></div>
</div>
<div class="flex flex-row justify-between flex-grow">
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
</div>
<div class="space"></div>
<div class="row">
<div class="category animation"></div>
<div class="stock animation"></div>
</ng-container>
<ng-template #mainOutlet>
<div class="bg-ucla-blue rounded w-[3rem] h-[4.125rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-col ml-4 w-[36.6%]">
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-6 bg-ucla-blue mb-2 w-[13.5rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-6 bg-ucla-blue w-[13.5rem] animate-[load_1s_linear_infinite]"></div>
</div>
<div class="row">
<div class="manufacturer animation"></div>
<div class="ava animation"></div>
<div class="flex flex-col w-[32.5%]">
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
</div>
<div class="flex flex-col w-[20%]">
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
</ng-template>

View File

@@ -1,61 +1,3 @@
:host {
@apply flex flex-row rounded-card bg-white mb-2 p-4;
height: 187px;
}
.thumbnail {
width: 70px;
height: 90px;
@apply bg-ucla-blue rounded-card;
}
.space {
@apply flex-grow;
}
.col {
@apply flex flex-col flex-grow;
}
.row {
@apply flex flex-row justify-between flex-grow;
}
.author {
width: 150px;
@apply h-4 bg-ucla-blue ml-4 mb-2;
}
.title {
width: 300px;
@apply h-6 bg-ucla-blue ml-4;
}
.price {
width: 100px;
@apply h-6 bg-ucla-blue ml-4;
}
.category {
width: 200px;
@apply h-4 bg-ucla-blue ml-4;
}
.manufacturer {
width: 200px;
@apply h-4 bg-ucla-blue ml-4;
}
.stock {
width: 75px;
@apply h-4 bg-ucla-blue ml-4;
}
.ava {
width: 150px;
@apply h-4 bg-ucla-blue ml-4;
}
.animation {
animation: load 1s linear infinite;
@apply flex flex-row rounded bg-white mb-2 p-4 w-full h-[212px] desktop-small:h-[181px];
}

View File

@@ -1,4 +1,7 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { Component, ChangeDetectionStrategy, HostBinding } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductCatalogNavigationService } from '@shared/services';
import { shareReplay } from 'rxjs/operators';
@Component({
selector: 'page-search-result-item-loading',
@@ -7,5 +10,13 @@ import { Component, ChangeDetectionStrategy } from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchResultItemLoadingComponent {
constructor() {}
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
}
constructor(private _navigationService: ProductCatalogNavigationService, private _activatedRoute: ActivatedRoute) {}
@HostBinding('style') get class() {
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
}
}

View File

@@ -1,81 +1,126 @@
<a class="product-list-result-content" [routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'details', item?.id]">
<div class="item-thumbnail">
<img loading="lazy" *ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" />
</div>
<div class="item-contributors">
<a
*ngFor="let contributor of contributors; let last = last"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
<a
class="page-search-result-item__item-card hover p-5 desktop-small:px-4 desktop-small:py-[0.625rem] h-[13.25rem] desktop-small:h-[11.3125rem] bg-white border border-solid border-transparent rounded"
[class.page-search-result-item__item-card-main]="mainOutletActive$ | async"
[routerLink]="detailsPath"
[routerLinkActive]="!isTablet && !(mainOutletActive$ | async) ? 'active' : ''"
[queryParamsHandling]="!isTablet ? 'preserve' : ''"
>
<div class="page-search-result-item__item-thumbnail text-center mr-4 w-[50px] h-[79px]">
<img
class="page-search-result-item__item-image w-[50px] h-[79px]"
loading="lazy"
*ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl"
[src]="thumbnailUrl"
[alt]="item?.product?.name"
/>
</div>
<div
class="item-title"
[class.xl]="item?.product?.name?.length >= 35"
[class.lg]="item?.product?.name?.length >= 40"
[class.md]="item?.product?.name?.length >= 50"
[class.sm]="item?.product?.name?.length >= 60"
[class.xs]="item?.product?.name?.length >= 100"
class="page-search-result-item__item-grid-container"
[class.page-search-result-item__item-grid-container-main]="mainOutletActive$ | async"
>
{{ item?.product?.name }}
</div>
<div
class="page-search-result-item__item-contributors desktop-small:text-p3 font-bold text-[#0556B4] text-ellipsis overflow-hidden max-w-[24rem] whitespace-nowrap"
>
<a
*ngFor="let contributor of contributors; let last = last"
[routerLink]="resultsPath"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
</div>
<div class="item-price">
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
</div>
<div
class="page-search-result-item__item-title font-bold text-h3"
[class.text-xl]="item?.product?.name?.length >= 35 && isTablet"
[class.text-lg]="item?.product?.name?.length >= 40 && isTablet"
[class.text-md]="item?.product?.name?.length >= 50 && isTablet"
[class.text-p3]="item?.product?.name?.length >= 60 || !isTablet"
[class.text-xs]="item?.product?.name?.length >= 100"
>
{{ item?.product?.name }}
</div>
<div *ngIf="selectable" class="item-data-selector">
<ui-select-bullet [ngModel]="selected" (ngModelChange)="setSelected($event)"></ui-select-bullet>
</div>
<div class="item-stock z-dropdown" [uiOverlayTrigger]="tooltip" [overlayTriggerDisabled]="!(stockTooltipText$ | async)">
<ng-container *ngIf="isOrderBranch$ | async">
<div class="flex flex-row items-center justify-between">
<ui-icon icon="home" size="1em"></ui-icon>
<span
*ngIf="inStock$ | async; let stock"
[class.skeleton]="stock.inStock === undefined"
class="min-w-[1rem] text-right inline-block"
>{{ stock?.inStock }}</span
>
<span>x</span>
<div class="page-search-result-item__item-format desktop-small:text-p3">
<div *ngIf="item?.product?.format && item?.product?.formatDetail" class="font-bold flex flex-row">
<img
class="mr-3"
*ngIf="item?.product?.format !== '--'"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail | substr: 30 }}
</div>
</ng-container>
<ng-container *ngIf="!(isOrderBranch$ | async)">
<div class="flex flex-row items-center justify-between z-dropdown">
<ui-icon class="block" icon="home" size="1em"></ui-icon>
<span class="min-w-[1rem] text-center inline-block">-</span>
<span>x</span>
</div>
<div class="page-search-result-item__item-manufacturer desktop-small:text-p3">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }}
</div>
<div class="page-search-result-item__item-misc desktop-small:text-p3">
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
{{ publicationDate }}
</div>
<div
class="page-search-result-item__item-price desktop-small:text-p3 font-bold justify-self-end"
[class.page-search-result-item__item-price-main]="mainOutletActive$ | async"
>
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
</div>
<div class="page-search-result-item__item-select-bullet justify-self-end">
<input
*ngIf="selectable"
(click)="$event.stopPropagation()"
[ngModel]="selected$ | async"
(ngModelChange)="setSelected()"
class="isa-select-bullet"
type="checkbox"
/>
</div>
<div
class="page-search-result-item__item-stock desktop-small:text-p3 font-bold z-dropdown justify-self-start"
[class.justify-self-end]="!(mainOutletActive$ | async)"
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
>
<ng-container *ngIf="isOrderBranch$ | async">
<div class="flex flex-row items-center justify-between">
<ui-icon icon="home" size="1em"></ui-icon>
<span
*ngIf="inStock$ | async; let stock"
[class.skeleton]="stock.inStock === undefined"
class="min-w-[1rem] text-right inline-block"
>{{ stock?.inStock }}</span
>
<span>x</span>
</div>
</ng-container>
<ng-container *ngIf="!(isOrderBranch$ | async)">
<div class="flex flex-row items-center justify-between z-dropdown">
<ui-icon class="block" icon="home" size="1em"></ui-icon>
<span class="min-w-[1rem] text-center inline-block">-</span>
<span>x</span>
</div>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
<div
class="page-search-result-item__item-ssc desktop-small:text-p3 w-full text-right overflow-hidden text-ellipsis whitespace-nowrap"
[class.page-search-result-item__item-ssc-main]="mainOutletActive$ | async"
>
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive$ | async">
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
</div>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
<!-- <div class="item-stock"><ui-icon icon="home" size="1em"></ui-icon> {{ item?.stockInfos | stockInfos }} x</div> -->
<div class="item-ssc" [class.xs]="item?.catalogAvailability?.sscText?.length >= 60">
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
</div>
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
</div>
<div class="item-misc">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br />
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
{{ publicationDate }}
<strong>{{ item?.catalogAvailability?.ssc }}</strong> - {{ item?.catalogAvailability?.sscText }}
</div>
</div>
</a>

View File

@@ -1,113 +1,104 @@
.product-list-result-content {
@apply text-black no-underline grid;
grid-template-columns: 102px 50% auto;
grid-template-rows: auto;
:host {
@apply flex flex-col w-full h-[13.25rem] desktop-small:h-[11.3125rem];
}
.page-search-result-item__item-card {
@apply grid grid-flow-col;
grid-template-columns: 3.9375rem auto;
box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5);
}
.page-search-result-item__item-grid-container {
@apply grid grid-flow-row gap-y-[0.375rem] gap-x-6;
grid-template-areas:
'item-thumbnail item-contributors item-contributors'
'item-thumbnail item-title item-price'
'item-thumbnail item-title item-data-selector'
'item-thumbnail item-format item-stock'
'item-thumbnail item-misc item-ssc';
'contributors contributors contributors'
'title title price'
'title title price'
'title title select'
'format format select'
'manufacturer manufacturer stock'
'misc ssc ssc';
}
.item-thumbnail {
grid-area: item-thumbnail;
width: 70px;
@apply mr-8;
img {
max-width: 100%;
max-height: 150px;
@apply rounded-card shadow-cta;
.page-search-result-item__item-grid-container-main {
@apply gap-x-4;
grid-template-rows: 1.3125rem 1.3125rem auto;
grid-template-columns: 37.5% 32.5% 20% auto;
grid-template-areas:
'contributors format price .'
'title manufacturer stock select'
'title misc ssc .';
}
.page-search-result-item__item-contributors {
grid-area: contributors;
}
.page-search-result-item__item-price {
grid-area: price;
}
.page-search-result-item__item-price-main {
@apply justify-self-start;
}
.page-search-result-item__item-title {
grid-area: title;
}
.page-search-result-item__item-format {
grid-area: format;
}
.page-search-result-item__item-manufacturer {
grid-area: manufacturer;
}
.page-search-result-item__item-misc {
grid-area: misc;
}
.page-search-result-item__item-select-bullet {
grid-area: select;
}
.page-search-result-item__item-stock {
grid-area: stock;
}
.page-search-result-item__item-ssc {
grid-area: ssc;
}
.page-search-result-item__item-ssc:hover {
.page-search-result-item__item-ssc-tooltip {
@apply absolute whitespace-normal w-[25%] block rounded bg-white z-tooltip p-2 -mt-[2rem] -ml-[4rem] text-p2 shadow;
}
}
.item-contributors {
grid-area: item-contributors;
height: 22px;
text-overflow: ellipsis;
overflow: hidden;
max-width: 600px;
white-space: nowrap;
.page-search-result-item__item-ssc-main {
@apply text-left overflow-hidden text-ellipsis whitespace-nowrap;
}
a {
@apply text-active-customer font-bold no-underline;
}
}
.item-title {
grid-area: item-title;
@apply font-bold text-2xl;
height: 64px;
max-height: 64px;
}
.item-title.xl {
@apply font-bold text-xl;
}
.item-title.lg {
@apply font-bold text-lg;
}
.item-title.md {
@apply font-bold text-base;
}
.item-title.sm {
@apply font-bold text-sm;
}
.item-title.xs {
@apply font-bold text-xs;
}
.item-price {
grid-area: item-price;
@apply font-bold text-xl text-right;
}
.item-format {
grid-area: item-format;
@apply flex flex-row items-center font-bold text-lg whitespace-nowrap;
img {
@apply mr-2;
}
}
.item-stock {
grid-area: item-stock;
@apply flex flex-row justify-end items-baseline font-bold text-lg;
ui-icon {
@apply text-active-customer mr-2;
}
}
.item-misc {
grid-area: item-misc;
}
.item-ssc {
grid-area: item-ssc;
@apply font-bold text-right;
}
.item-ssc.xs {
@apply font-bold text-xs;
}
.item-data-selector {
@apply w-full flex justify-end;
grid-area: item-data-selector;
}
ui-select-bullet {
@apply p-4 -m-4 z-dropdown;
}
@media (min-width: 1025px) {
.item-contributors {
max-width: 780px;
.page-search-result-item__item-image {
box-shadow: 0px 6px 18px rgba(0, 0, 0, 0.197935);
}
.active,
.hover:hover {
@apply bg-[#D8DFE5] border border-solid border-[#0556B4];
.page-search-result-item__item-select-bullet {
.isa-select-bullet::before {
@apply bg-[#fff];
}
.isa-select-bullet:checked:before {
@apply bg-[#596470];
}
.isa-select-bullet:hover::before {
@apply bg-[#778490];
}
}
}

View File

@@ -1,14 +1,17 @@
import { DatePipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostListener, HostBinding } from '@angular/core';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
import { ComponentStore } from '@ngrx/component-store';
import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { isEqual } from 'lodash';
import { combineLatest } from 'rxjs';
import { debounceTime, switchMap, map, tap, shareReplay } from 'rxjs/operators';
import { debounceTime, switchMap, map, shareReplay, filter, first } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { ProductCatalogNavigationService } from '@shared/services';
import { ActivatedRoute } from '@angular/router';
export interface SearchResultItemComponentState {
item?: ItemDTO;
@@ -36,16 +39,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
readonly item$ = this.select((s) => s.item);
@Input()
get selected() {
return this.get((s) => s.selected);
}
set selected(selected: boolean) {
if (this.selected !== selected) {
this.patchState({ selected });
}
}
readonly selected$ = this.select((s) => s.selected);
selected$ = this._articleSearchService.selectedItemIds$.pipe(map((selectedItemIds) => selectedItemIds.includes(this.item?.id)));
@Input()
get selectable() {
@@ -58,7 +52,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
}
@Output()
selectedChange = new EventEmitter<boolean>();
selectedChange = new EventEmitter<ItemDTO>();
get contributors() {
return this.item?.product?.contributors?.split(';').map((val) => val.trim());
@@ -77,6 +71,22 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
return '';
}
get isTablet() {
return this._environment.matchTablet();
}
get detailsPath() {
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: this.item?.id });
}
get resultsPath() {
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
}
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
}
defaultBranch$ = this._availability.getDefaultBranch();
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
@@ -110,9 +120,11 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
inStock$ = combineLatest([this.item$, this.selectedBranchId$, this.defaultBranch$]).pipe(
debounceTime(100),
filter(([item, branch, defaultBranch]) => !!item && !!defaultBranch),
switchMap(([item, branch, defaultBranch]) =>
this._stockService.getInStock$({ itemId: item.id, branchId: branch?.id ?? defaultBranch?.id })
)
),
shareReplay(1)
);
constructor(
@@ -121,7 +133,10 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
private _articleSearchService: ArticleSearchService,
public applicationService: ApplicationService,
private _stockService: DomainInStockService,
private _availability: DomainAvailabilityService
private _availability: DomainAvailabilityService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService,
private _activatedRoute: ActivatedRoute
) {
super({
selected: false,
@@ -129,7 +144,16 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
});
}
setSelected(selected: boolean) {
this._articleSearchService.setSelected({ selected, itemId: this.item?.id });
setSelected() {
const isSelected = this._articleSearchService.selectedItemIds.includes(this.item?.id);
this._articleSearchService.setSelected({ selected: !isSelected, itemId: this.item?.id });
if (!this.isTablet) {
this.selectedChange.emit(this.item);
}
}
@HostBinding('style') get class() {
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
}
}

View File

@@ -1,32 +1,77 @@
<div class="filter-wrapper">
<div class="hits" *ngIf="hits$ | async; let hits">{{ hits }} Titel</div>
<ui-order-by-filter [orderBy]="(filter$ | async)?.orderBy" (selectedOrderByChange)="search(); updateBreadcrumbs()"> </ui-order-by-filter>
<div
class="page-search-results__header bg-background-liste flex items-end justify-between"
[class.pb-4]="!(mainOutletActive$ | async)"
[class.flex-col]="!(mainOutletActive$ | async)"
>
<div class="flex flex-row w-full desktop-small:w-min" [class.desktop:w-full]="!(mainOutletActive$ | async)">
<shared-filter-input-group-main
*ngIf="filter$ | async; let filter"
class="block mr-3 w-full desktop-small:w-[23.5rem]"
[class.desktop:w-full]="!(mainOutletActive$ | async)"
[hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
<a
class="page-search-results__filter w-[6.75rem] h-14 rounded font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
[routerLink]="filterRoute"
queryParamsHandling="preserve"
>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</a>
</div>
<div
*ngIf="hits$ | async; let hits"
class="page-search-results__items-count inline-flex flex-row items-center pr-5 text-p3"
[class.mb-4]="mainOutletActive$ | async"
>
{{ hits ??
0 }}
Titel
</div>
</div>
<div class="page-search-results__order-by" [class.page-search-results__order-by-main]="mainOutletActive$ | async">
<shared-order-by-filter
[groupBy]="(mainOutletActive$ | async) ? [2, 2, 2] : []"
[orderBy]="(filter$ | async)?.orderBy"
(selectedOrderByChange)="search(); updateBreadcrumbs()"
>
</shared-order-by-filter>
</div>
<cdk-virtual-scroll-viewport
#scrollContainer
class="product-list scroll-bar scroll-bar-margin"
[itemSize]="187"
minBufferPx="1200"
maxBufferPx="1200"
class="product-list"
[itemSize]="(mainOutletActive$ | async) ? 98 : 187"
minBufferPx="2800"
maxBufferPx="2800"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<div class="product-list-result" *cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId">
<search-result-item
[selected]="item | searchResultSelected: searchService.selectedItemIds"
[selectable]="isSelectable(item)"
[item]="item"
></search-result-item>
</div>
<search-result-item
class="page-search-results__result-item"
[class.page-search-results__result-item-main]="mainOutletActive$ | async"
*cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId"
(selectedChange)="addToCart($event)"
[selectable]="isSelectable(item)"
[item]="item"
></search-result-item>
<page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading>
</cdk-virtual-scroll-viewport>
<div class="actions">
<div *ngIf="isTablet" class="actions z-fixed">
<button
[disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0"
class="cta-cart cta-action-primary"
(click)="addSelectedItemsToCart()"
(click)="addToCart()"
>
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button>

View File

@@ -1,34 +1,36 @@
:host {
@apply box-border grid;
max-height: calc(100vh - 364px);
height: 100vh;
grid-template-rows: auto 1fr;
@apply box-border grid h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
grid-template-rows: auto auto 1fr;
}
.product-list {
@apply m-0 p-0 mt-2;
@apply m-0 p-0 mt-px-2;
}
.product-list-result {
@apply list-none bg-white rounded-card p-4 mb-2;
height: 187px;
max-height: 187px;
.page-search-results__result-item {
@apply mb-px-10;
}
.filter-wrapper {
@apply block relative;
.page-search-results__result-item-main {
@apply mb-[5px];
}
.hits {
@apply text-inactive-branch font-semibold absolute top-2 right-0;
}
.page-search-results__order-by {
@apply bg-white rounded px-6 desktop-small:px-8;
}
ui-order-by-filter {
@apply mx-auto;
.page-search-results__order-by-main {
@apply pl-[4.9375rem] px-4;
}
.page-search-results__filter {
&.active {
@apply bg-[#596470] text-white ml-px-5;
}
}
.actions {
@apply fixed bottom-28 inline-grid grid-flow-col gap-7;
@apply fixed bottom-16 inline-grid grid-flow-col gap-7;
left: 50%;
transform: translateX(-50%);
@@ -44,3 +46,16 @@
@apply bg-brand text-white;
}
}
::ng-deep page-search-results .page-search-results__order-by-main shared-order-by-filter {
@apply grid grid-flow-col justify-items-start gap-x-4 justify-start;
grid-template-columns: 37.5% 32.5% 20% auto;
.group {
@apply desktop-small:justify-start;
}
.order-by-filter-button {
@apply ml-0 mr-7;
}
}

View File

@@ -3,18 +3,20 @@ import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, ViewC
import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { DomainCheckoutService } from '@domain/checkout';
import { ItemDTO } from '@swagger/cat';
import { AddToShoppingCartDTO } from '@swagger/checkout';
import { UiFilter } from '@ui/filter';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { CacheService } from 'apps/core/cache/src/public-api';
import { isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, first, map, switchMap } from 'rxjs/operators';
import { debounceTime, first, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
import { SearchResultItemComponent } from './search-result-item.component';
import { ProductCatalogNavigationService } from '@shared/services';
import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib';
@Component({
selector: 'page-search-results',
@@ -27,6 +29,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
@ViewChild('scrollContainer', { static: true })
scrollContainer: CdkVirtualScrollViewport;
@ViewChild(FilterInputGroupMainComponent, { static: false })
sharedFilterInputGroupMain: FilterInputGroupMainComponent;
results$ = this.searchService.items$;
fetching$ = this.searchService.fetching$;
hits$ = this.searchService.hits$;
@@ -35,6 +40,8 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
selectedItemIds$ = this.searchService.selectedItemIds$;
searchboxHint$ = this.searchService.searchboxHint$;
selectedItems$ = combineLatest([this.results$, this.selectedItemIds$]).pipe(
map(([items, selectedItemIds]) => {
return items?.filter((item) => selectedItemIds?.find((selectedItemId) => item.id === selectedItemId));
@@ -47,14 +54,39 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
trackByItemId: TrackByFunction<ItemDTO> = (index, item) => item.id;
get isTablet() {
return this._environment.matchTablet();
}
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
map(([filter, defaultFilter]) => {
const filterQueryParams = filter?.getQueryParams();
return !isEqual(this.resetQueryParamsQueryAndOrderBy(filterQueryParams), Filter.create(defaultFilter).getQueryParams());
})
);
get filterRoute() {
const itemId = this._navigationService?.getOutletParams(this.route)?.right?.id;
return this._navigationService.getArticleSearchResultsAndFilterPath({
processId: this.application.activatedProcessId,
itemId,
});
}
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this.route).pipe(shareReplay());
}
constructor(
public searchService: ArticleSearchService,
private route: ActivatedRoute,
private application: ApplicationService,
public application: ApplicationService,
private breadcrumb: BreadcrumbService,
private cache: CacheService,
private _uiModal: UiModalService,
private _checkoutService: DomainCheckoutService
private _checkoutService: DomainCheckoutService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService
) {}
ngOnInit() {
@@ -72,7 +104,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id;
if (processChanged) {
if (!!this.searchService.processId && this.searchService.filter instanceof UiFilter) {
if (!!this.searchService.processId && this.searchService.filter instanceof Filter) {
this.cacheCurrentData(
this.searchService.processId,
this.searchService.filter.getQueryParams(),
@@ -87,7 +119,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.searchService.setBranch(selectedBranch);
}
if (!(this.searchService.filter instanceof UiFilter)) {
if (!(this.searchService.filter instanceof Filter)) {
await this.searchService.setDefaultFilter();
}
@@ -114,9 +146,32 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
await this.updateBreadcrumbs(processId, queryParams);
await this.createBreadcrumb(processId, queryParams);
await this.removeDetailsBreadcrumb(processId);
if (this.isTablet || this.route?.outlet === 'main') {
await this.removeDetailsBreadcrumb(processId);
}
})
);
this.subscriptions.add(
this.searchService.searchCompleted.pipe(withLatestFrom(this.application.activatedProcessId$)).subscribe(([state, processId]) => {
if (state.searchState === '') {
const params = state.filter.getQueryParams();
if ((state.hits === 1 && this.isTablet) || (!this.isTablet && !this._navigationService.mainOutletActive(this.route))) {
const item = state.items.find((f) => f);
this._navigationService.navigateToDetails({
processId,
itemId: item.id,
queryParams: this.isTablet ? undefined : params,
});
} else if (this.isTablet || this._navigationService.mainOutletActive(this.route)) {
this._navigationService.navigateToResults({
processId,
queryParams: params,
});
}
}
})
);
}
ngOnDestroy() {
@@ -127,7 +182,26 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.unselectAll();
}
search() {
resetQueryParamsQueryAndOrderBy(params: Record<string, string> = {}) {
const clean = { ...params };
for (const key in clean) {
if (key === 'main_qs') {
clean[key] = undefined;
} else if (key?.includes('order_by')) {
delete clean[key];
}
}
return clean;
}
search(filter?: Filter) {
if (!!filter) {
this.sharedFilterInputGroupMain.cancelAutocomplete();
this.searchService.setFilter(filter);
}
this.searchService.search({ clear: true });
}
@@ -187,7 +261,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
await this.breadcrumb.addBreadcrumbIfNotExists({
key: processId,
name,
path: `/kunde/${this.application.activatedProcessId}/product/search/results`,
path: this._navigationService.getArticleSearchResultsPath(this.application.activatedProcessId),
params: queryParams,
section: 'customer',
tags: ['catalog', 'filter', 'results'],
@@ -241,6 +315,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
// Zeige Select Radio Button nicht an wenn Item Archivartikel oder Fortsetzungsartikel ist
const isArchiv = item?.catalogAvailability?.status === 1;
const isFortsetzung = item?.features?.find((i) => i?.key === 'PFO');
return !(isArchiv || isFortsetzung);
}
@@ -249,29 +324,44 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.searchService.patchState({ selectedItemIds: [] });
}
async addSelectedItemsToCart() {
async addToCart(item?: ItemDTO) {
this.loading$.next(true);
const selectedItems = await this.selectedItems$.pipe(first()).toPromise();
if (!!item) {
await this.addItemsToCart(item);
} else {
await this.addItemsToCart();
}
this.loading$.next(false);
}
private _createShoppingCartItem(item: ItemDTO): AddToShoppingCartDTO {
return {
quantity: 1,
availability: {
availabilityType: item?.catalogAvailability?.status,
price: item?.catalogAvailability?.price,
supplierProductNumber: item?.ids?.dig ? String(item?.ids?.dig) : item?.product?.supplierProductNumber,
},
product: {
catalogProductNumber: String(item?.id),
...item?.product,
},
itemType: item?.type,
promotion: { points: item?.promoPoints },
};
}
async addItemsToCart(item?: ItemDTO) {
const selectedItems = !item ? await this.selectedItems$.pipe(first()).toPromise() : [item];
const items: AddToShoppingCartDTO[] = [];
const canAddItemsPayload = [];
for (const item of selectedItems) {
const shoppingCartItem = this._createShoppingCartItem(item);
const isDownload = item?.product?.format === 'EB' || item?.product?.format === 'DL';
const shoppingCartItem: AddToShoppingCartDTO = {
quantity: 1,
availability: {
availabilityType: item?.catalogAvailability?.status,
price: item?.catalogAvailability?.price,
supplierProductNumber: item?.ids?.dig ? String(item.ids?.dig) : item?.product?.supplierProductNumber,
},
product: {
catalogProductNumber: String(item?.id),
...item?.product,
},
itemType: item.type,
promotion: { points: item?.promoPoints },
};
if (isDownload) {
shoppingCartItem.destination = { data: { target: 16 } };
canAddItemsPayload.push({

View File

@@ -8,7 +8,6 @@ import { UiCommonModule } from '@ui/common';
import { UiIconModule } from '@ui/icon';
import { UiSelectBulletModule } from '@ui/select-bullet';
import { UiTooltipModule } from '@ui/tooltip';
import { UiOrderByFilterModule } from 'apps/ui/filter/src/lib/next/order-by-filter/order-by-filter.module';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
import { StockInfosPipe } from './order-by-filter/stick-infos.pipe';
@@ -16,6 +15,9 @@ import { SearchResultItemLoadingComponent } from './search-result-item-loading.c
import { SearchResultItemComponent } from './search-result-item.component';
import { ArticleSearchResultsComponent } from './search-results.component';
import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe';
import { FilterAutocompleteProvider, FilterNextModule, OrderByFilterModule } from 'apps/shared/components/filter/src/lib';
import { FocusSearchboxEvent } from '../focus-searchbox.event';
import { ArticleSearchMainAutocompleteProvider } from '../providers';
@NgModule({
imports: [
@@ -27,9 +29,10 @@ import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe
UiIconModule,
UiSelectBulletModule,
UiSpinnerModule,
UiOrderByFilterModule,
OrderByFilterModule,
ScrollingModule,
UiTooltipModule,
FilterNextModule,
],
exports: [ArticleSearchResultsComponent, SearchResultItemComponent],
declarations: [
@@ -40,6 +43,13 @@ import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe
SearchResultSelectedPipe,
AddedToCartModalComponent,
],
providers: [],
providers: [
FocusSearchboxEvent,
{
provide: FilterAutocompleteProvider,
useClass: ArticleSearchMainAutocompleteProvider,
multi: true,
},
],
})
export class SearchResultsModule {}

View File

@@ -2,10 +2,65 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ArticleDetailsComponent } from './article-details/article-details.component';
import { ArticleSearchComponent } from './article-search/article-search.component';
import { ArticleSearchFilterComponent } from './article-search/search-filter/search-filter.component';
import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component';
import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component';
import { PageCatalogComponent } from './page-catalog.component';
const auxiliaryRoutes = [
{
path: 'search',
component: ArticleSearchComponent,
outlet: 'left',
children: [
{
path: '',
component: ArticleSearchMainComponent,
},
],
},
{
path: 'filter',
component: ArticleSearchFilterComponent,
outlet: 'right',
},
{
path: 'filter/:id',
component: ArticleSearchFilterComponent,
outlet: 'right',
},
{
path: 'results',
component: ArticleSearchResultsComponent,
outlet: 'left',
},
{
path: 'results',
component: ArticleSearchResultsComponent,
outlet: 'main',
},
{
path: 'results/:id',
component: ArticleSearchResultsComponent,
outlet: 'left',
},
{
path: 'results/ean/:ean',
component: ArticleSearchResultsComponent,
outlet: 'left',
},
{
path: 'details/ean/:ean',
component: ArticleDetailsComponent,
outlet: 'right',
},
{
path: 'details/:id',
component: ArticleDetailsComponent,
outlet: 'right',
},
];
const routes: Routes = [
{
path: '',
@@ -19,12 +74,16 @@ const routes: Routes = [
path: '',
component: ArticleSearchMainComponent,
},
{
path: 'results',
component: ArticleSearchResultsComponent,
},
],
},
{
path: 'results',
component: ArticleSearchResultsComponent,
},
{
path: 'filter',
component: ArticleSearchFilterComponent,
},
{
path: 'details/ean/:ean',
component: ArticleDetailsComponent,
@@ -33,6 +92,7 @@ const routes: Routes = [
path: 'details/:id',
component: ArticleDetailsComponent,
},
...auxiliaryRoutes,
{
path: '',
pathMatch: 'full',

View File

@@ -1,5 +1,32 @@
<shared-breadcrumb class="my-4" [key]="activatedProcessId$ | async" [tags]="['catalog']">
<shared-branch-selector [branchType]="1" [value]="selectedBranch$ | async" (valueChange)="patchProcessData($event)">
<shared-branch-selector
[filterCurrentBranch]="!!auth.hasRole('Store')"
[orderBy]="auth.hasRole('Store') ? 'distance' : 'name'"
[branchType]="1"
[value]="selectedBranch$ | async"
(valueChange)="patchProcessData($event)"
>
</shared-branch-selector>
</shared-breadcrumb>
<router-outlet></router-outlet>
<ng-container *ngIf="routerEvents$ | async">
<ng-container *ngIf="!(isDesktop$ | async); else desktop">
<router-outlet></router-outlet>
</ng-container>
<ng-template #desktop>
<ng-container *ngIf="showMainOutlet$ | async">
<router-outlet name="main"></router-outlet>
</ng-container>
<div class="grid grid-cols-[minmax(31rem,.5fr)_1fr] gap-6">
<div *ngIf="showLeftOutlet$ | async" class="block">
<router-outlet name="left"></router-outlet>
</div>
<div *ngIf="showRightOutlet$ | async">
<router-outlet name="right"></router-outlet>
</div>
</div>
</ng-template>
</ng-container>

View File

@@ -1,5 +1,5 @@
:host {
@apply block relative;
@apply block relative h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
}
shell-breadcrumb {

View File

@@ -1,12 +1,14 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { AuthService } from '@core/auth';
import { EnvironmentService } from '@core/environment';
import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { BreadcrumbComponent } from '@shared/components/breadcrumb';
import { BranchDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { BehaviorSubject, from, fromEvent, Observable, Subject } from 'rxjs';
import { first, map, switchMap, takeUntil } from 'rxjs/operators';
import { fromEvent, Observable, Subject } from 'rxjs';
import { first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
@Component({
selector: 'page-catalog',
@@ -27,11 +29,38 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
_onDestroy$ = new Subject<boolean>();
get isTablet$() {
return this._environmentService.matchTablet$.pipe(
map((state) => state.matches),
shareReplay()
);
}
get isDesktop$() {
return this._environmentService.matchDesktop$.pipe(
map((state) => {
return state.matches;
}),
shareReplay()
);
}
routerEvents$ = this._router.events.pipe(shareReplay());
showMainOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'main')));
showLeftOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'left')));
showRightOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'right')));
constructor(
public application: ApplicationService,
private _uiModal: UiModalService,
public auth: AuthService,
private _environmentService: EnvironmentService,
private _renderer: Renderer2
private _renderer: Renderer2,
private _activatedRoute: ActivatedRoute,
private _router: Router
) {}
ngOnInit() {
@@ -41,19 +70,19 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
}
ngAfterViewInit(): void {
if (this._environmentService.isTablet()) {
fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
.pipe(takeUntil(this._onDestroy$))
.subscribe((_) => {
fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.isTablet$))
.subscribe(([_, isTablet]) => {
if (isTablet) {
this._renderer.setStyle(this.branchSelectorRef?.nativeElement, 'width', this.branchSelectorWidth);
});
}
});
fromEvent(this.branchSelectorRef.nativeElement, 'focusout')
.pipe(takeUntil(this._onDestroy$))
.subscribe((_) => {
this._renderer.removeStyle(this.branchSelectorRef?.nativeElement, 'width');
});
}
fromEvent(this.branchSelectorRef.nativeElement, 'focusout')
.pipe(takeUntil(this._onDestroy$))
.subscribe((_) => {
this._renderer.removeStyle(this.branchSelectorRef?.nativeElement, 'width');
});
}
ngOnDestroy(): void {

View File

@@ -2,22 +2,13 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { BreadcrumbModule } from '@shared/components/breadcrumb';
import { ShellBreadcrumbModule } from '@shell/breadcrumb';
import { ArticleDetailsModule } from './article-details/article-details.module';
import { ArticleSearchModule } from './article-search/article-search.module';
import { PageCatalogRoutingModule } from './page-catalog-routing.module';
import { PageCatalogComponent } from './page-catalog.component';
@NgModule({
imports: [
CommonModule,
PageCatalogRoutingModule,
ShellBreadcrumbModule,
ArticleSearchModule,
ArticleDetailsModule,
BreadcrumbModule,
BranchSelectorComponent,
],
imports: [CommonModule, PageCatalogRoutingModule, ArticleSearchModule, ArticleDetailsModule, BreadcrumbModule, BranchSelectorComponent],
exports: [],
declarations: [PageCatalogComponent],
})

View File

@@ -3,7 +3,7 @@
}
.wrapper {
@apply block bg-white shadow-card rounded-card pt-6;
@apply block bg-white shadow-card rounded pt-6;
.header {
@apply text-right;
@@ -12,11 +12,11 @@
@apply text-center;
.title {
@apply text-2xl text-page-heading font-bold;
@apply text-h3 text-h2 font-bold;
}
.paragraph {
@apply text-2xl mt-1 mb-0;
@apply text-h3 mt-1 mb-0;
}
}
}

View File

@@ -14,7 +14,7 @@ button {
}
.card {
@apply bg-white rounded-card shadow-card;
@apply bg-white rounded shadow-card;
}
.card-empty {
@@ -26,7 +26,7 @@ button {
width: fit-content;
h1 {
@apply text-2xl font-bold my-1;
@apply text-h3 font-bold my-1;
font-size: 26px;
}
@@ -87,7 +87,7 @@ button {
}
.sub-header {
@apply text-center text-2xl font-normal my-0 mb-8;
@apply text-center text-h3 font-normal my-0 mb-8;
}
hr {
@@ -134,7 +134,7 @@ h1 {
}
.branch-name {
@apply text-regular overflow-hidden overflow-ellipsis ml-4;
@apply text-p2 overflow-hidden overflow-ellipsis ml-4;
}
.book-icon {
@@ -156,7 +156,7 @@ h1 {
}
.shipping-cost-info {
@apply text-sm mr-4 self-end;
@apply text-p3 mr-4 self-end;
}
.total-value {
@@ -164,5 +164,5 @@ h1 {
}
.total-item-reading-points {
@apply text-base font-bold text-ucla-blue;
@apply text-p2 font-bold text-ucla-blue;
}

View File

@@ -6,8 +6,6 @@ import { DomainCheckoutService } from '@domain/checkout';
import { AvailabilityDTO, DestinationDTO, NotificationChannel, ShoppingCartItemDTO, ShoppingCartDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiMessageModalComponent, UiModalService } from '@ui/modal';
import { PrintModalData, PrintModalComponent } from '@modal/printer';
import { PurchasingOptionsModalComponent, PurchasingOptionsModalData } from '../modals/purchasing-options-modal';
import { PurchasingOptions } from '../modals/purchasing-options-modal/purchasing-options-modal.store';
import { AuthService } from '@core/auth';
import { first, map, shareReplay, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs';
@@ -15,13 +13,11 @@ import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainPrinterService } from '@domain/printer';
import { CheckoutDummyComponent } from '../checkout-dummy/checkout-dummy.component';
import { ResponseArgsOfItemDTO } from '@swagger/cat';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { emailNotificationValidator, mobileNotificationValidator } from '@shared/components/notification-channel-control';
import { PurchasingOptionsListModalComponent } from '../modals/purchasing-options-list-modal';
import { PurchasingOptionsListModalData } from '../modals/purchasing-options-list-modal/purchasing-options-list-modal.data';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { CheckoutDummyData } from '../checkout-dummy/checkout-dummy-data';
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
export interface CheckoutReviewComponentState {
shoppingCart: ShoppingCartDTO;
@@ -242,7 +238,8 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
private domainCatalogService: DomainCatalogService,
private breadcrumb: BreadcrumbService,
private domainPrinterService: DomainPrinterService,
private _fb: UntypedFormBuilder
private _fb: UntypedFormBuilder,
private _purchaseOptionsModalService: PurchaseOptionsModalService
) {
super({
shoppingCart: undefined,
@@ -274,7 +271,7 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
shoppingCart,
shoppingCartItems,
});
this.checkQuantityErrors(shoppingCartItems);
// this.checkQuantityErrors(shoppingCartItems);
},
(err) => {},
() => {}
@@ -285,15 +282,15 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
)
);
checkQuantityErrors(shoppingCartItems: ShoppingCartItemDTO[]) {
shoppingCartItems.forEach((item) => {
if (item.features?.orderType === 'Rücklage') {
this.setQuantityError(item, item.availability, item.quantity > item.availability?.inStock);
} else {
this.setQuantityError(item, item.availability, false);
}
});
}
// checkQuantityErrors(shoppingCartItems: ShoppingCartItemDTO[]) {
// shoppingCartItems.forEach((item) => {
// if (item.features?.orderType === 'Abholung') {
// this.setQuantityError(item, item.availability, item.quantity > item.availability?.inStock);
// } else {
// this.setQuantityError(item, item.availability, false);
// }
// });
// }
async updateBreadcrumb() {
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
@@ -434,171 +431,10 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
}
async changeItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) {
this.loadingOnItemChangeById$.next(shoppingCartItem.id);
const quantity = shoppingCartItem.quantity;
const branchNo = this.auth.getClaimByKey('branch_no');
const branchId = shoppingCartItem?.destination?.data?.targetBranch?.id;
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise();
let branch = await this.domainCheckoutService
.getBranches()
.pipe(map((branches) => branches.find((branch) => (branchId ? branch.id === branchId : branch.branchNumber === branchNo))))
.toPromise();
if (!branch) {
branch = await this.applicationService.getSelectedBranch$().pipe(take(1)).toPromise();
}
let catalogItem: ResponseArgsOfItemDTO;
if (Number.isInteger(shoppingCartItem?.product?.catalogProductNumber)) {
catalogItem = await this.domainCatalogService
.getDetailsById({ id: Number(shoppingCartItem.product.catalogProductNumber) })
.toPromise();
} else if (shoppingCartItem?.product?.ean) {
catalogItem = await this.domainCatalogService.getDetailsByEan({ ean: shoppingCartItem.product.ean }).toPromise();
}
let takeAwayAvailability: AvailabilityDTO;
if (!!catalogItem?.result?.product) {
takeAwayAvailability = await this.availabilityService
.getTakeAwayAvailability({
item: {
itemId: catalogItem.result.id,
ean: catalogItem.result.product.ean,
price: catalogItem.result.catalogAvailability?.price,
},
quantity,
})
.toPromise();
}
const pickupAvailability = await this.availabilityService
.getPickUpAvailability({
item: {
itemId: Number(shoppingCartItem.product.catalogProductNumber),
ean: shoppingCartItem.product.ean,
price: shoppingCartItem.availability.price,
},
branch,
quantity,
})
.toPromise();
const digAvailability = await this.availabilityService
.getDigDeliveryAvailability({
item: {
itemId: Number(shoppingCartItem.product.catalogProductNumber),
ean: shoppingCartItem.product.ean,
price: shoppingCartItem.availability.price,
},
quantity,
})
.toPromise();
const b2bAvailability = await this.availabilityService
.getB2bDeliveryAvailability({
item: {
itemId: Number(shoppingCartItem.product.catalogProductNumber),
ean: shoppingCartItem.product.ean,
price: shoppingCartItem.availability.price,
},
quantity,
})
.toPromise();
const downloadAvailability = await this.availabilityService
.getDownloadAvailability({
item: {
itemId: Number(shoppingCartItem.product.catalogProductNumber),
ean: shoppingCartItem.product.ean,
price: shoppingCartItem.availability.price,
},
})
.toPromise();
let availableOptions: PurchasingOptions[] = [];
const availabilities: { [key: string]: AvailabilityDTO } = {};
if (takeAwayAvailability && this.availabilityService.isAvailable({ availability: takeAwayAvailability })) {
availableOptions.push('take-away');
availabilities['take-away'] = takeAwayAvailability;
}
if (downloadAvailability && this.availabilityService.isAvailable({ availability: downloadAvailability })) {
availableOptions.push('download');
availabilities['download'] = downloadAvailability;
}
if (pickupAvailability && this.availabilityService.isAvailable({ availability: pickupAvailability[0] })) {
if (pickupAvailability[1].availableFor) {
if ((pickupAvailability[1].availableFor & 2) === 2) {
availableOptions.push('pick-up');
availabilities['pick-up'] = pickupAvailability[0];
}
} else {
availableOptions.push('pick-up');
availabilities['pick-up'] = pickupAvailability[0];
}
if (!customerFeatures?.webshop && this.availabilityService.isAvailable({ availability: b2bAvailability })) {
availableOptions.push('b2b-delivery');
availabilities['b2b-delivery'] = b2bAvailability;
}
}
if (digAvailability && this.availabilityService.isAvailable({ availability: digAvailability }) && !customerFeatures?.b2b) {
availableOptions.push('dig-delivery');
availabilities['dig-delivery'] = digAvailability;
}
if (availableOptions.includes('dig-delivery') && availableOptions.includes('b2b-delivery')) {
let shippingAvailability = await this.availabilityService
.getDeliveryAvailability({
item: {
itemId: Number(shoppingCartItem.product.catalogProductNumber),
ean: shoppingCartItem.product.ean,
price: shoppingCartItem.availability.price,
},
quantity,
})
.toPromise();
if (shippingAvailability && this.availabilityService.isAvailable({ availability: shippingAvailability })) {
availableOptions.push('delivery');
availabilities['delivery'] = shippingAvailability;
availableOptions = availableOptions.filter((option) => !(option === 'dig-delivery' || option === 'b2b-delivery'));
}
}
this.loadingOnItemChangeById$.next(undefined);
this.cdr.markForCheck();
const itemId = Number(shoppingCartItem.product.catalogProductNumber);
const modal = this.uiModal.open({
content: PurchasingOptionsModalComponent,
data: {
availableOptions,
item: {
id: itemId,
itemId: itemId,
product: shoppingCartItem.product,
price: shoppingCartItem.availability.price,
catalogAvailability: {
status: shoppingCartItem.availability.availabilityType,
price: shoppingCartItem.availability.price,
},
},
shoppingCartItem,
branchId: branch?.id,
processId: this.applicationService.activatedProcessId,
availabilities,
} as PurchasingOptionsModalData,
});
modal.afterClosed$.pipe(takeUntil(this._orderCompleted)).subscribe(() => {
this.setQuantityError(shoppingCartItem, undefined, false);
this._purchaseOptionsModalService.open({
processId: this.applicationService.activatedProcessId,
items: [shoppingCartItem],
type: 'update',
});
}
@@ -649,7 +485,7 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
})
.toPromise();
this.setQuantityError(shoppingCartItem, availability, availability?.inStock < quantity);
// this.setQuantityError(shoppingCartItem, availability, availability?.inStock < quantity);
break;
case 'Abholung':
availability = await this.availabilityService
@@ -727,7 +563,7 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
},
})
.toPromise();
this.setQuantityError(shoppingCartItem, availability, false);
// this.setQuantityError(shoppingCartItem, availability, false);
} else if (availability) {
// Wenn das Ergebnis der Availability Abfrage keinen Preis zurückliefert (z.B. HFI Geschenkkarte), wird der Preis aus der
// Availability vor der Abfrage verwendet
@@ -758,16 +594,16 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
this.loadingOnQuantityChangeById$.next(undefined);
}
setQuantityError(item: ShoppingCartItemDTO, availability: AvailabilityDTO, error: boolean) {
const quantityErrors: { [key: string]: string } = this.quantityError$.value;
if (error) {
quantityErrors[item.product.catalogProductNumber] = `${availability.inStock} Exemplar(e) sofort lieferbar`;
this.quantityError$.next({ ...quantityErrors });
} else {
delete quantityErrors[item.product.catalogProductNumber];
this.quantityError$.next({ ...quantityErrors });
}
}
// setQuantityError(item: ShoppingCartItemDTO, availability: AvailabilityDTO, error: boolean) {
// const quantityErrors: { [key: string]: string } = this.quantityError$.value;
// if (error) {
// quantityErrors[item.product.catalogProductNumber] = `${availability.inStock} Exemplar(e) sofort lieferbar`;
// this.quantityError$.next({ ...quantityErrors });
// } else {
// delete quantityErrors[item.product.catalogProductNumber];
// this.quantityError$.next({ ...quantityErrors });
// }
// }
// Bei unbekannten Kunden und DIG Bestellung findet ein Vergleich der Preise statt
compareDeliveryAndCatalogPrice(availability: AvailabilityDTO, orderType: string, shoppingCartItemPrice: number) {
@@ -816,16 +652,10 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
}
async showPurchasingListModal(shoppingCartItems: ShoppingCartItemDTO[]) {
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise();
this.uiModal.open({
content: PurchasingOptionsListModalComponent,
title: 'Wie möchten Sie die Artikel erhalten?',
config: { showScrollbarY: false },
data: {
processId: this.applicationService.activatedProcessId,
shoppingCartItems: shoppingCartItems,
customerFeatures,
} as PurchasingOptionsListModalData,
this._purchaseOptionsModalService.open({
processId: this.applicationService.activatedProcessId,
items: shoppingCartItems,
type: 'update',
});
}

View File

@@ -23,7 +23,7 @@ button {
img {
max-width: 100%;
max-height: 150px;
@apply rounded-card shadow-cta;
@apply rounded shadow-cta;
}
}
@@ -59,11 +59,11 @@ button {
}
.item-title.md {
@apply font-bold text-base;
@apply font-bold text-p2;
}
.item-title.sm {
@apply font-bold text-sm;
@apply font-bold text-p3;
}
.item-title.xs {
@@ -88,7 +88,7 @@ button {
}
.quantity-error {
@apply text-dark-goldenrod font-bold text-sm whitespace-nowrap;
@apply text-dark-goldenrod font-bold text-p3 whitespace-nowrap;
}
ui-quantity-dropdown {
@@ -113,7 +113,7 @@ button {
}
.item-availability-message {
@apply text-dark-goldenrod font-bold text-sm whitespace-nowrap;
@apply text-dark-goldenrod font-bold text-p3 whitespace-nowrap;
}
}

View File

@@ -13,11 +13,11 @@ label {
}
textarea {
@apply flex-grow text-base border-none outline-none p-0 resize-none;
@apply flex-grow text-p2 border-none outline-none p-0 resize-none;
}
button {
@apply text-brand font-bold text-cta-l outline-none border-none bg-transparent ml-1;
@apply text-brand font-bold text-p1 outline-none border-none bg-transparent ml-1;
ui-icon {
@apply text-ucla-blue;

View File

@@ -8,7 +8,7 @@
}
.card {
@apply bg-white rounded-card shadow-card overflow-scroll;
@apply bg-white rounded shadow-card overflow-scroll;
height: calc(100vh - 410px);
}
@@ -61,7 +61,7 @@
}
.order-customer {
@apply font-bold text-2xl;
@apply font-bold text-h3;
}
.grow {
@@ -178,15 +178,15 @@
}
.promotion-points {
@apply text-ucla-blue text-regular font-bold flex-grow;
@apply text-ucla-blue text-p2 font-bold flex-grow;
}
.total-price {
@apply font-bold text-cta-l;
@apply font-bold text-p1;
}
.price-info {
@apply text-right text-sm;
@apply text-right text-p3;
}
.actions {

View File

@@ -1,4 +1,3 @@
// start:ng42.barrel
export * from './page-checkout.module';
export * from './page-checkout-modals.module';
// end:ng42.barrel

View File

@@ -1,13 +0,0 @@
<div class="option-icon">
<ui-icon size="50px" icon="truck"></ui-icon>
</div>
<button
class="option-chip"
[disabled]="optionChipDisabled$ | async"
(click)="optionChange('delivery')"
[class.selected]="(selectedOption$ | async) === 'delivery'"
>
Versand
</button>
<p>Möchten Sie die Artikel<br />geliefert bekommen?</p>
<p>Versandkostenfrei</p>

View File

@@ -1,23 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-delivery-option-list',
templateUrl: 'delivery-option-list.component.html',
styleUrls: ['../list-options.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeliveryOptionListComponent {
selectedOption$ = this._store.selectedFilterOption$;
optionChipDisabled$ = this._store.fetchingAvailabilities$;
constructor(private _store: PurchasingOptionsListModalStore) {}
optionChange(option: string) {
if (this._store.selectedFilterOption === option) {
this._store.selectedFilterOption = undefined;
} else {
this._store.selectedFilterOption = option;
}
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './delivery-option-list.component';
// end:ng42.barrel

View File

@@ -1,7 +0,0 @@
// start:ng42.barrel
export * from './delivery-option';
export * from './pick-up-option';
export * from './take-away-option';
export * from './purchasing-options-list-modal.component';
export * from './purchasing-options-list-modal.module';
// end:ng42.barrel

View File

@@ -1,44 +0,0 @@
:host {
@apply block w-72;
}
.option-icon {
@apply text-ucla-blue mx-auto;
width: 40px;
.truck-b2b {
margin-top: -21px;
margin-bottom: -12px;
width: 70px;
}
}
.option-chip {
@apply rounded-full text-base px-4 py-3 bg-glitter text-inactive-customer border-none font-bold;
&.selected {
@apply bg-active-customer text-white;
}
}
.option-description {
@apply my-2;
}
.option-select {
@apply mt-4 mb-4 border-2 border-solid border-brand text-brand text-cta-l font-bold bg-white rounded-full py-3 px-6;
}
p {
@apply my-4;
}
::ng-deep page-purchasing-options-list-modal ui-branch-dropdown .wrapper {
@apply mx-auto;
width: 80%;
}
::ng-deep page-pick-up-option-list .option-chip:disabled,
::ng-deep page-take-away-option-list .option-chip:disabled {
@apply bg-disabled-branch border-disabled-branch text-white;
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './pick-up-option-list.component';
// end:ng42.barrel

View File

@@ -1,18 +0,0 @@
<div class="option-icon">
<ui-icon size="50px" icon="box_out"></ui-icon>
</div>
<button
class="option-chip"
[disabled]="optionChipDisabled$ | async"
(click)="optionChange('pick-up')"
[class.selected]="(selectedOption$ | async) === 'pick-up'"
>
Abholung
</button>
<p>Möchten Sie die Artikel<br />in einer unserer Filialen<br />abholen?</p>
<ui-branch-dropdown
[branches]="branches$ | async"
[selected]="selectedBranch$ | async"
(selectBranch)="selectBranch($event)"
></ui-branch-dropdown>

View File

@@ -1,48 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BranchDTO } from '@swagger/checkout';
import { combineLatest } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-pick-up-option-list',
templateUrl: 'pick-up-option-list.component.html',
styleUrls: ['../list-options.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PickUpOptionListComponent {
branches$ = this._store.branches$;
selectedBranch$ = this._store.selectedPickUpBranch$.pipe(
map((branch) => {
// Determins if branch is targetBranch
if (branch?.branchType === 1) {
return branch.name;
}
})
);
selectedOption$ = this._store.selectedFilterOption$;
optionChipDisabled$ = combineLatest([this._store.fetchingAvailabilities$, this.selectedBranch$]).pipe(
map(([fetching, selectedBranch]) => {
return fetching || !selectedBranch;
})
);
constructor(private _store: PurchasingOptionsListModalStore) {}
optionChange(option: string) {
if (this._store.selectedFilterOption === option) {
this._store.selectedFilterOption = undefined;
} else {
this._store.selectedFilterOption = option;
}
}
async selectBranch(branch: BranchDTO) {
this._store.lastSelectedFilterOption$.next(undefined);
this._store.selectedPickUpBranch = branch;
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
shoppingCartItems.forEach((item) => this._store.loadPickUpAvailability({ item }));
}
}

View File

@@ -1,115 +0,0 @@
<div class="item-thumbnail">
<img loading="lazy" *ngIf="item?.product?.ean | productImage; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" />
</div>
<div class="item-contributors">
{{ item.product.contributors }}
</div>
<div
class="item-title"
[class.xl]="item?.product?.name?.length >= 35"
[class.lg]="item?.product?.name?.length >= 40"
[class.md]="item?.product?.name?.length >= 50"
[class.sm]="item?.product?.name?.length >= 60"
[class.xs]="item?.product?.name?.length >= 100"
>
{{ item?.product?.name }}
</div>
<ng-container *ngIf="canAdd$ | async; let canAdd">
<div class="item-can-add" *ngIf="canAdd !== true">
{{ canAdd }}
</div>
</ng-container>
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
</div>
<div class="item-info">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br />
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
{{ item?.product?.publicationDate | date: 'dd. MMMM yyyy' }}
</div>
<div class="item-price-stock">
<div class="price">
<ng-container *ngIf="showTooltip$ | async">
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
i
</button>
<ui-tooltip #tooltipContent yPosition="above" xPosition="after" [yOffset]="-16">
Günstigerer Preis aus Hugendubel Katalog wird übernommen
</ui-tooltip>
</ng-container>
<div *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</div>
</div>
<div>
<ui-quantity-dropdown
[disabled]="fetchingAvailabilities$ | async"
[ngModel]="item.quantity"
(ngModelChange)="changeQuantity($event)"
[range]="quantityRange$ | async"
>
</ui-quantity-dropdown>
</div>
</div>
<div class="item-select">
<ui-select-bullet
*ngIf="selectVisible$ | async"
[disabled]="selectDisabled$ | async"
[ngModel]="isSelected$ | async"
(ngModelChange)="selected($event)"
></ui-select-bullet>
</div>
<div class="item-availability">
<div class="fetching" *ngIf="fetchingAvailabilities$ | async; else availabilities"></div>
<ng-template #availabilities>
<ng-container *ngIf="notAvailable$ | async; else available">
<span class="hint">Derzeit nicht bestellbar</span>
</ng-container>
<ng-template #available>
<span>Verfügbar als</span>
<div *ngIf="takeAwayAvailabilities$ | async; let takeAwayAvailabilites">
<ui-icon icon="shopping_bag" size="18px"></ui-icon>
<span class="instock">{{ takeAwayAvailabilites?.inStock }}x</span> ab sofort
</div>
<div *ngIf="!!(pickUpAvailabilities$ | async)">
<ui-icon icon="box_out" size="18px"></ui-icon>
{{ (pickUpAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
</div>
<div *ngIf="!!(deliveryDigAvailabilities$ | async); else b2b">
<ui-icon class="truck" icon="truck" size="30px"></ui-icon>
<ng-container *ngIf="deliveryDigAvailabilities$ | async; let deliveryDigAvailabilities">
<ng-container *ngIf="deliveryDigAvailabilities?.estimatedDelivery; else estimatedShippingDate">
{{ (deliveryDigAvailabilities?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} -
{{ (deliveryDigAvailabilities?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
</ng-container>
<ng-template #estimatedShippingDate>
{{ deliveryDigAvailabilities.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
</ng-template>
</ng-container>
</div>
<ng-template #b2b>
<div *ngIf="!!(deliveryB2bAvailabilities$ | async)">
<ui-icon class="truck-b2b" icon="truck_b2b" size="40px"></ui-icon>
{{ (deliveryB2bAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
</div>
</ng-template>
</ng-template>
</ng-template>
</div>

Some files were not shown because too many files have changed in this diff Show More