Compare commits

...

151 Commits

Author SHA1 Message Date
Lorenz Hilpert
ddd1b81d0d Merge branch 'feature/responsive-customer-orders' into feature/responsive-customer-orders-breakpoints 2023-05-24 15:45:20 +02:00
Nino
c79a1cdad1 #2605 Focus Searchbox on Navigation Click - Finish first Version of Article Search Responsive Design - Reset customer Orders back to pre Responsive 2023-05-23 17:56:38 +02:00
Nino
edf978f5cf #3387 Hover Styling on Article Search Result List 2023-05-23 16:52:12 +02:00
Nino
4bfe35a8b9 Bugfix OrderBy Tablet Navigation 2023-05-23 16:07:46 +02:00
Nino
4b7c26b009 Update new Filter and Branch Selector Changes from ISA-Integration 2023-05-22 17:26:15 +02:00
Nino
2a442dde85 Filter Design angepasst 2023-05-17 16:33:11 +02:00
Nino
45bb39f466 Merge branch 'feature/responsive-customer-orders' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into feature/responsive-customer-orders 2023-05-17 16:26:18 +02:00
Nino
b2731432ed #4009 Alle Filter Entfernen eingebaut in Artikelsuche Filter Overlay 2023-05-17 16:25:46 +02:00
Nino Righi
8c424bab43 Merge branch 'feature/responsive-customer-orders' into feature/responsive-customer-orders-breakpoints 2023-05-16 16:33:54 +02:00
Nino Righi
571c748a66 Show Outlet Bugfix 2023-05-16 16:26:14 +02:00
Nino Righi
c104f36255 Updated Breakpoint, set MaxWidth to 1920px 2023-05-16 14:31:28 +02:00
Lorenz Hilpert
672245c467 Breakpoints RD 2023-05-16 10:54:47 +02:00
Nino Righi
0ae92e34c6 Breakpoints Handling Improved 2023-05-15 18:44:03 +02:00
Nino Righi
ce72ce48b2 New Breakpoints Update 2023-05-12 17:48:27 +02:00
Nino Righi
bd5ec27425 Merge branch 'develop' into feature/responsive-customer-orders 2023-05-11 16:54:54 +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
Nino Righi
27a83242c5 Filter Adjustment and Bugfix 2023-05-09 18:01:56 +02:00
Nino Righi
7bf23c4bcc Improved Routing and QueryParams handling 2023-05-09 17:34:30 +02:00
Nino Righi
0bc80e12aa #4000 Reset Selected Branch on Shell Navigation click, Product Search and Customer Orders 2023-05-09 14:40:44 +02:00
Nino Righi
b586e3da9e Responsive Design - Tablet Styling Bugfixes and Adjustments - Filter Layout - Container Heights - Routing Bugfixes 2023-05-08 17:09:15 +02:00
Nino Righi
a433f2cfe4 Responsive Design Article Result List Add Item To Cart via Select Bullets 2023-05-05 16:50:21 +02:00
Nino Righi
ff368d68b7 Responsive Design Order by Ipad fix 2023-05-04 15:09:05 +02:00
Nino Righi
40ebd72263 Article Search Results Searchbox Responsive Design and Filter Box Shadow Fix 2023-05-03 17:00:11 +02:00
Nino Righi
11eca1e25a Added new Icons, Fixed Searchbox Autocomplete 2023-05-03 16:20:12 +02:00
Nino Righi
b8d0153232 Filter Responsive Design 2023-05-02 18:08:18 +02:00
Nino Righi
0a195e78e5 OrderBy Responsive Design 2023-05-02 15:37:48 +02:00
Nino Righi
97c26f5fa1 Finished Article Details Styling and Started Filter Responsive Design 2023-04-28 18:24:57 +02:00
Nino Righi
8dac64d5b8 Merge branch 'develop' into feature/responsive-customer-orders 2023-04-28 10:21:46 +02:00
Nino Righi
2c295b0797 Article Details Page Responsive Design Layout and Styling Refactor 2023-04-27 18:31:28 +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
Nino Righi
4344f4617c Merge branch 'develop' into feature/responsive-customer-orders 2023-04-27 10:56:36 +02:00
Nino Righi
c451d2b329 Update Styling Catalog Main and Results fullscreen 2023-04-26 18:00:14 +02:00
Nino Righi
96d1a4b826 Page Catalog Layout Changes 2023-04-26 15:55:07 +02:00
Lorenz Hilpert
595bb27d99 Shell Max Content Width 2023-04-26 10:42:15 +02:00
Nino Righi
958a388fb5 Updated Navigation Service Implementation, Unit Test Fixes 2023-04-25 17:09:13 +02:00
Nino Righi
a908767f08 Merge branch 'develop' into feature/responsive-customer-orders 2023-04-24 17:41:36 +02:00
Nino Righi
3c0406031a Process, Breadcrumb, Shell Article Search Navigation Update 2023-04-24 16:47:31 +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
Nino Righi
79bb9b8c11 Routing and Breadcrumb Update Article Search, Created Navigation Service 2023-04-21 17:12:55 +02:00
Lorenz Hilpert
6311ebe467 Show Navigation for Package-inspection 2023-04-21 10:33:34 +02:00
Nino Righi
341b202bc4 Kundenbestellungen Zwischencommit 2023-04-20 18:10:00 +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
Nino Righi
f284dc1db5 Article Search and Customer Order Filter Navigation Update Based on previous Routes, Updated Routing on Tablet 2023-04-18 17:36:08 +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
Nino Righi
2144ec838c Customer Order Splitscreen Navigation Update 2023-04-06 16:12:29 +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
Nino Righi
bff10cb2ff Customer Order Changes 2023-04-05 11:04:38 +02:00
Nino Righi
d303b1444b Zoom Update and Article Result List Styling Update 2023-04-03 17:54:08 +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
Nino Righi
150e7965ee Zwischencommit 2023-03-29 12:57:05 +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
Nino Righi
3fcf3d9396 Responsive Design Splitscreen Article Search Results Update 2023-03-28 15:08:31 +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
Nino Righi
52278b8baf Initial Responsive Design Implementation based on Article Search 2023-03-24 17:19:59 +01: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
Nino Righi
a7abc35316 Merged PR 1509: #3899 Fix Customer Orders loadItems request with compartmentCode
#3899 Fix Customer Orders loadItems request with compartmentCode
2023-03-10 14:28:29 +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
27b7ffcf99 #3898 Trefferliste wird in anderen Vorgang übernommen 2023-03-09 14:44:17 +01:00
Nino Righi
d804a744b6 Merged PR 1506: #3895 Purchaseing Options Modal Selected branch check if correct branchType i...
#3895 Purchaseing Options Modal Selected branch check if correct branchType is selected
2023-03-08 19:29:49 +00:00
Nino Righi
fc5cf27bd1 Merged PR 1507: #3882 Fix Hide Select Bullets if items have no actions
#3882 Fix Hide Select Bullets if items have no actions
2023-03-08 19:24:42 +00:00
Lorenz Hilpert
355bba8966 Merge branch 'ipad-bugfix' into develop 2023-03-08 20:22:02 +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
690 changed files with 16921 additions and 10895 deletions

View File

@@ -3,6 +3,5 @@
"johnpapa.angular2", "johnpapa.angular2",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"angular.ng-template", "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": { "@domain/defs": {
"projectType": "library", "projectType": "library",
"root": "apps/domain/defs", "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": { "@modal/reorder": {
"projectType": "library", "projectType": "library",
"root": "apps/modal/reorder", "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": { "@domain/isa": {
"projectType": "library", "projectType": "library",
"root": "apps/domain/isa", "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": { "@store/search-component-store": {
"projectType": "library", "projectType": "library",
"root": "apps/store/search-component-store", "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 { Injectable } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { BranchDTO } from '@swagger/checkout'; import { BranchDTO } from '@swagger/checkout';
import { isBoolean, isNumber } from '@utils/common'; import { isBoolean, isNumber } from '@utils/common';
@@ -16,25 +15,30 @@ import {
selectActivatedProcess, selectActivatedProcess,
patchProcess, patchProcess,
patchProcessData, patchProcessData,
selectTitle,
setTitle,
} from './store'; } from './store';
@Injectable() @Injectable()
export class ApplicationService { export class ApplicationService {
/** @deprecated */
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined); private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
/** @deprecated */
get activatedProcessId() { get activatedProcessId() {
return this.activatedProcessIdSubject.value; return this.activatedProcessIdSubject.value;
} }
/** @deprecated */
get activatedProcessId$() { get activatedProcessId$() {
return this.activatedProcessIdSubject.asObservable(); return this.activatedProcessIdSubject.asObservable();
} }
title$ = this.store.select(selectTitle);
constructor(private store: Store) {} constructor(private store: Store) {}
setTitle(title: string) {
this.store.dispatch(setTitle({ title }));
}
getProcesses$(section?: 'customer' | 'branch') { getProcesses$(section?: 'customer' | 'branch') {
const processes$ = this.store.select(selectProcesses); const processes$ = this.store.select(selectProcesses);
return processes$.pipe(map((processes) => processes.filter((process) => (section ? process.section === section : true)))); return processes$.pipe(map((processes) => processes.filter((process) => (section ? process.section === section : true))));

View File

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

View File

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

View File

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

View File

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

View File

@@ -135,9 +135,9 @@ export class BreadcrumbService {
crumbs.forEach((crumb) => this.removeBreadcrumb(crumb.id)); crumbs.forEach((crumb) => this.removeBreadcrumb(crumb.id));
} }
getLatestBreadcrumbForSection(section: 'customer' | 'branch') { getLatestBreadcrumbForSection(section: 'customer' | 'branch', predicate: (crumb: Breadcrumb) => boolean = (_) => true) {
return this.store return this.store
.select(selectors.selectBreadcrumbsBySection, { section }) .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 * Url
*/ */
path: string; path: string | any[];
/** /**
* Query Parameter * Query Parameter

View File

@@ -1,17 +1,76 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Platform } from '@angular/cdk/platform'; import { Platform } from '@angular/cdk/platform';
import { NativeContainerService } from 'native-container'; 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: 1280px)';
const MATCH_DESKTOP_LARGE = '(min-width: 1440px)';
const MATCH_DESKTOP_X_LARGE = '(min-width: 1920px)';
const MATCH_DESKTOP_XX_LARGE = '(min-width: 2736px)';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class EnvironmentService { 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);
matchDesktopLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_LARGE);
}
matchDesktopLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_LARGE);
matchDesktopXLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_X_LARGE);
}
matchDesktopXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_X_LARGE);
matchDesktopXXLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_XX_LARGE);
}
matchDesktopXXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_XX_LARGE);
/**
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.
*/
isDesktop(): boolean { isDesktop(): boolean {
return !this.isTablet(); return !this.isTablet();
} }
/**
* @deprecated Use `matchTablet` instead.
*/
isTablet(): boolean { isTablet(): boolean {
return this.isNative() || this.isSafari(); return this.isNative() || this.isSafari();
} }
@@ -21,6 +80,6 @@ export class EnvironmentService {
} }
isSafari(): boolean { isSafari(): boolean {
return (this._platform.ANDROID || this._platform.IOS) && this._platform.SAFARI; return this._platform.IOS && this._platform.SAFARI;
} }
} }

View File

@@ -34,6 +34,11 @@ export class DomainAvailabilityService {
private _branchService: StoreCheckoutBranchService private _branchService: StoreCheckoutBranchService
) {} ) {}
@memorize({ ttl: 10000 })
memorizedAvailabilityShippingAvailability(request: Array<AvailabilityRequestDTO>) {
return this._availabilityService.AvailabilityShippingAvailability(request).pipe(shareReplay(1));
}
@memorize() @memorize()
getSuppliers(): Observable<SupplierDTO[]> { getSuppliers(): Observable<SupplierDTO[]> {
return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe( return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
@@ -249,57 +254,53 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 }) @memorize({ ttl: 10000 })
getDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> { getDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
return this._availabilityService return this.memorizedAvailabilityShippingAvailability([
.AvailabilityShippingAvailability([ {
{ ean: item?.ean,
ean: item?.ean, itemId: item?.itemId ? String(item?.itemId) : null,
itemId: item?.itemId ? String(item?.itemId) : null, price: item?.price,
price: item?.price, qty: quantity,
qty: quantity, },
}, ]).pipe(
]) timeout(5000),
.pipe( map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
timeout(5000), shareReplay(1)
map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)), );
shareReplay(1)
);
} }
@memorize({ ttl: 10000 }) @memorize({ ttl: 10000 })
getDigDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> { getDigDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
return this._availabilityService return this.memorizedAvailabilityShippingAvailability([
.AvailabilityShippingAvailability([ {
{ qty: quantity,
qty: quantity, ean: item?.ean,
ean: item?.ean, itemId: item?.itemId ? String(item?.itemId) : null,
itemId: item?.itemId ? String(item?.itemId) : null, price: item?.price,
price: item?.price, },
}, ]).pipe(
]) timeout(5000),
.pipe( map((r) => {
timeout(5000), const availabilities = r.result;
map((r) => { const preferred = availabilities?.find((f) => f.preferred === 1);
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
const availability: AvailabilityDTO = { const availability: AvailabilityDTO = {
availabilityType: preferred?.status, availabilityType: preferred?.status,
ssc: preferred?.ssc, ssc: preferred?.ssc,
sscText: preferred?.sscText, sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId }, supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked, isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at, estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
estimatedDelivery: preferred?.estimatedDelivery, estimatedDelivery: preferred?.estimatedDelivery,
price: preferred?.price, price: preferred?.price,
logistician: { id: preferred?.logisticianId }, logistician: { id: preferred?.logisticianId },
supplierProductNumber: preferred?.supplierProductNumber, supplierProductNumber: preferred?.supplierProductNumber,
supplierInfo: preferred?.requestStatusCode, supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested, lastRequest: preferred?.requested,
}; };
return availability; return availability;
}), }),
shareReplay(1) shareReplay(1)
); );
} }
@memorize({ ttl: 10000 }) @memorize({ ttl: 10000 })
@@ -333,37 +334,35 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 }) @memorize({ ttl: 10000 })
getDownloadAvailability({ item }: { item: ItemData }): Observable<AvailabilityDTO> { getDownloadAvailability({ item }: { item: ItemData }): Observable<AvailabilityDTO> {
return this._availabilityService return this.memorizedAvailabilityShippingAvailability([
.AvailabilityShippingAvailability([ {
{ ean: item?.ean,
ean: item?.ean, itemId: item?.itemId ? String(item?.itemId) : null,
itemId: item?.itemId ? String(item?.itemId) : null, price: item?.price,
price: item?.price, qty: 1,
qty: 1, },
}, ]).pipe(
]) map((r) => {
.pipe( const availabilities = r.result;
map((r) => { const preferred = availabilities?.find((f) => f.preferred === 1);
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
const availability: AvailabilityDTO = { const availability: AvailabilityDTO = {
availabilityType: preferred?.status, availabilityType: preferred?.status,
ssc: preferred?.ssc, ssc: preferred?.ssc,
sscText: preferred?.sscText, sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId }, supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked, isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at, estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
price: preferred?.price, price: preferred?.price,
supplierProductNumber: preferred?.supplierProductNumber, supplierProductNumber: preferred?.supplierProductNumber,
logistician: { id: preferred?.logisticianId }, logistician: { id: preferred?.logisticianId },
supplierInfo: preferred?.requestStatusCode, supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested, lastRequest: preferred?.requested,
}; };
return availability; return availability;
}), }),
shareReplay(1) shareReplay(1)
); );
} }
@memorize({ ttl: 10000 }) @memorize({ ttl: 10000 })
@@ -401,7 +400,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 }) @memorize({ ttl: 10000 })
getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) { getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe( return this.memorizedAvailabilityShippingAvailability(payload).pipe(
timeout(20000), timeout(20000),
map((response) => this._mapToShippingAvailability(response.result)) map((response) => this._mapToShippingAvailability(response.result))
); );
@@ -409,7 +408,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 }) @memorize({ ttl: 10000 })
getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) { getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe( return this.memorizedAvailabilityShippingAvailability(payload).pipe(
timeout(20000), timeout(20000),
map((response) => this._mapToShippingAvailability(response.result)) map((response) => this._mapToShippingAvailability(response.result))
); );
@@ -447,6 +446,9 @@ export class DomainAvailabilityService {
} }
isAvailable({ availability }: { availability: AvailabilityDTO }) { 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); 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 }); const key = this.getKey({ itemId, branchId });
this._addToInStockQueue({ itemId, branchId }); this._addToInStockQueue({ itemId, branchId });
let _previousValue: InStock;
const sub = combineLatest([this._inStockMap, this._inStockFetchingMap]) const sub = combineLatest([this._inStockMap, this._inStockFetchingMap])
.pipe(distinctUntilChanged(isEqual)) .pipe(distinctUntilChanged(isEqual))
.subscribe(([inStockMap, inStockFetchingMap]) => { .subscribe(([inStockMap, inStockFetchingMap]) => {
@@ -54,7 +56,12 @@ export class DomainInStockService {
inStock: inStockMap[key], inStock: inStockMap[key],
fetching: inStockFetchingMap[key] ?? false, fetching: inStockFetchingMap[key] ?? false,
}; };
obs.next(inStock);
if (!isEqual(inStock, _previousValue)) {
obs.next(inStock);
}
_previousValue = inStock;
}); });
return () => { return () => {
sub.unsubscribe(); sub.unsubscribe();

View File

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

View File

@@ -447,7 +447,7 @@ export class DomainRemissionService {
* Create a new receipt for the given return/remission * Create a new receipt for the given return/remission
* @param returnId Return ID * @param returnId Return ID
* @param receiptNumber Receipt number * @param receiptNumber Receipt number
* @returns ReturnDTO - ShippingDocument * @returns ReceiptDTO
*/ */
async createReceipt(returnDTO: ReturnDTO, receiptNumber?: string): Promise<ReceiptDTO> { async createReceipt(returnDTO: ReturnDTO, receiptNumber?: string): Promise<ReceiptDTO> {
const stock = await this._getStock(); const stock = await this._getStock();
@@ -471,14 +471,41 @@ export class DomainRemissionService {
return receipt; 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 const res = await this._returnService
.ReturnFinalizeReceipt({ .ReturnFinalizeReceipt({
returnId, returnId,
receiptId, receiptId,
data: { data: {},
packageCode,
},
}) })
.toPromise(); .toPromise();

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application'; import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout'; import { DomainCheckoutService } from '@domain/checkout';
import { ProductCatalogNavigationService } from '@shared/services';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
@@ -9,6 +10,7 @@ export class CanActivateProductGuard implements CanActivate {
constructor( constructor(
private readonly _applicationService: ApplicationService, private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService, private readonly _checkoutService: DomainCheckoutService,
private readonly _navigationService: ProductCatalogNavigationService,
private readonly _router: Router private readonly _router: Router
) {} ) {}
@@ -38,7 +40,7 @@ export class CanActivateProductGuard implements CanActivate {
} }
if (!lastActivatedProcessId) { if (!lastActivatedProcessId) {
await this.fromCartProcess(processes, route); await this.fromCartProcess(processes);
return false; return false;
} else { } else {
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)])); 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 // Bei offener Artikelsuche/Kundensuche und Klick auf Footer Artikelsuche
async fromCartProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) { async fromCartProcess(processes: ApplicationProcess[]) {
const newProcessId = Date.now(); const newProcessId = Date.now();
await this._applicationService.createProcess({ await this._applicationService.createProcess({
id: newProcessId, id: newProcessId,
@@ -57,7 +59,7 @@ export class CanActivateProductGuard implements CanActivate {
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`, 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 // 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": { "@core/auth": {
"issuer": "https://sso-test.paragon-data.de", "issuer": "https://sso-test.paragon-data.de",
"clientId": "hug-isa", "clientId": "isa-client",
"responseType": "id_token token", "responseType": "code",
"oidc": true,
"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" "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": { "@core/logger": {
@@ -41,7 +40,7 @@
"rootUrl": "https://filialinformationsystem-integration.paragon-systems.de/eiswebapi/v1" "rootUrl": "https://filialinformationsystem-integration.paragon-systems.de/eiswebapi/v1"
}, },
"@swagger/remi": { "@swagger/remi": {
"rootUrl": "https://isa-integration.paragon-data.net/inv/v1" "rootUrl": "https://isa-integration.paragon-data.net/inv/v6"
}, },
"@swagger/wws": { "@swagger/wws": {
"rootUrl": "https://isa-integration.paragon-data.net/wws/v1" "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" "rootUrl": "https://filialinformationsystem.paragon-systems.de/eiswebapi/v1"
}, },
"@swagger/remi": { "@swagger/remi": {
"rootUrl": "https://isa.paragon-systems.de/inv/v1" "rootUrl": "https://isa.paragon-systems.de/inv/v6"
}, },
"@swagger/wws": { "@swagger/wws": {
"rootUrl": "https://isa.paragon-data.net/wws/v1" "rootUrl": "https://isa.paragon-systems.de/wws/v1"
}, },
"hubs": { "hubs": {
"notifications": { "notifications": {
@@ -69,6 +69,6 @@
}, },
"checkForUpdates": 3600000, "checkForUpdates": 3600000,
"licence": { "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" "rootUrl": "https://filialinformationsystem-staging.paragon-systems.de/eiswebapi/v1"
}, },
"@swagger/remi": { "@swagger/remi": {
"rootUrl": "https://isa-staging.paragon-systems.de/inv/v1" "rootUrl": "https://isa-staging.paragon-systems.de/inv/v6"
}, },
"@swagger/wws": { "@swagger/wws": {
"rootUrl": "https://isa-staging.paragon-systems.de/wws/v1" "rootUrl": "https://isa-staging.paragon-systems.de/wws/v1"
@@ -69,6 +69,6 @@
}, },
"checkForUpdates": 3600000, "checkForUpdates": 3600000,
"licence": { "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" /> <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 rel="icon" type="image/x-icon" href="favicon.ico" />
<link href="/assets/fonts/fonts.css" rel="stylesheet" /> <link href="/assets/fonts/fonts.css" rel="stylesheet" />
<link href="/assets/icons/icons.css" rel="stylesheet" />
<link rel="manifest" href="manifest.webmanifest" /> <link rel="manifest" href="manifest.webmanifest" />
<meta name="theme-color" content="#1976d2" /> <meta name="theme-color" content="#1976d2" />
</head> </head>

View File

@@ -16,7 +16,7 @@
} }
body { body {
background: var(--bg-color); @apply bg-background;
} }
@layer base { @layer base {

View File

@@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ApplicationService } from '@core/application';
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb'; import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
import { Config } from '@core/config'; import { Config } from '@core/config';
@@ -13,10 +14,11 @@ export class AssortmentComponent implements OnInit {
return this._config.get('process.ids.assortment'); return this._config.get('process.ids.assortment');
} }
constructor(private _config: Config, private _breadcrumb: BreadcrumbService) {} constructor(private _config: Config, private _breadcrumb: BreadcrumbService, private _app: ApplicationService) {}
ngOnInit() { ngOnInit() {
this.createBreadcrumbIfNotExists(); this.createBreadcrumbIfNotExists();
this._app.setTitle('Sortiment');
} }
async createBreadcrumbIfNotExists(): Promise<void> { async createBreadcrumbIfNotExists(): Promise<void> {

View File

@@ -20,7 +20,7 @@
<div class="page-price-update-item__item-details"> <div class="page-price-update-item__item-details">
<div class="page-price-update-item__item-contributors flex flex-row"> <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>
<div <div
@@ -43,7 +43,7 @@
src="assets/images/Icon_{{ item?.product?.format }}.svg" src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail" [alt]="item?.product?.formatDetail"
/> />
{{ item?.product?.formatDetail }} {{ environment.isTablet() ? (item?.product?.formatDetail | substr: 25) : item?.product?.formatDetail }}
</div> </div>
</div> </div>

View File

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

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core';
import { Config } from '@core/config'; import { Config } from '@core/config';
import { provideComponentStore } from '@ngrx/component-store'; 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 { UiFilter, UiFilterComponent } from '@ui/filter';
import { PriceUpdateComponentStore } from './price-update.component.store'; import { PriceUpdateComponentStore } from './price-update.component.store';
import { combineLatest, Subject, Subscription } from 'rxjs'; import { combineLatest, Subject, Subscription } from 'rxjs';
@@ -26,8 +26,8 @@ export class PriceUpdateComponent implements OnInit {
hint$ = new Subject<string>(); hint$ = new Subject<string>();
@ViewChild(ShellFilterOverlayComponent) @ViewChild(SharedFilterOverlayComponent)
filterOverlay: ShellFilterOverlayComponent; filterOverlay: SharedFilterOverlayComponent;
/** /**
* Zeigt die liste an, wenn entweder keine items geladen werden oder wenn items geladen wurden * 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 { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ShellFilterOverlayModule } from '@shell/filter-overlay'; import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
import { UiFilterNextModule } from '@ui/filter'; import { UiFilterNextModule } from '@ui/filter';
import { UiIconModule } from '@ui/icon'; import { UiIconModule } from '@ui/icon';
import { UiSpinnerModule } from '@ui/spinner'; import { UiSpinnerModule } from '@ui/spinner';
@@ -8,7 +8,7 @@ import { PriceUpdateListModule } from './price-update-list';
import { PriceUpdateComponent } from './price-update.component'; import { PriceUpdateComponent } from './price-update.component';
@NgModule({ @NgModule({
imports: [CommonModule, PriceUpdateListModule, UiIconModule, UiFilterNextModule, ShellFilterOverlayModule, UiSpinnerModule], imports: [CommonModule, PriceUpdateListModule, UiIconModule, UiFilterNextModule, SharedFilterOverlayModule, UiSpinnerModule],
exports: [PriceUpdateComponent], exports: [PriceUpdateComponent],
declarations: [PriceUpdateComponent], declarations: [PriceUpdateComponent],
providers: [], providers: [],

View File

@@ -1,124 +1,240 @@
<ng-container *ngIf="!showRecommendations"> <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"> <ng-container *ngIf="store.item$ | async; let item">
<div class="product-details"> <div class="page-article-details__product-details mb-3">
<div class="product-image"> <div class="page-article-details__product-bookmark justify-self-end">
<button class="image-button" (click)="showImages()"> <div *ngIf="showArchivBadge$ | async" class="archiv-badge">
<img (load)="loadImage()" [src]="item.imageId | productImage: 195:315:true" alt="product image" /> <button [uiOverlayTrigger]="archivTooltip" class="p-0 m-0 outline-none border-none bg-transparent relative -top-px-5">
<ui-icon *ngIf="imageLoaded$ | async" icon="search_add" size="22px"></ui-icon> <img src="/assets/images/bookmark_benachrichtigung_archiv.svg" alt="Archiv Badge" />
</button> <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-card"
(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> <ui-stars [rating]="store.reviewRating$ | async"></ui-stars>
<div class="cta-recessions">{{ item.reviews.length }} Rezensionen</div> <div class="text-regular text-[#0556B4] font-bold">{{ item.reviews.length }} Rezensionen</div>
</button> </button>
</div> </div>
<div class="product-info"> <div class="page-article-details__product-contributors">
<div class="row" [class.bookmark-badge-gap]="isBadgeVisible$ | async"> <a
<div> *ngFor="let contributor of contributors$ | async; let last = last"
<a class="text-[#0556B4] font-semibold no-underline text-base"
*ngFor="let contributor of contributors$ | async; let last = last" [routerLink]="resultsPath"
class="autor" [queryParams]="{ main_qs: contributor, main_author: 'author' }"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']" >
[queryParams]="{ main_qs: contributor, main_author: 'author' }" {{ contributor }}{{ last ? '' : ';' }}
> </a>
{{ contributor }}{{ last ? '' : ';' }} </div>
</a>
<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-2xl 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-sm"
*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> </div>
</ng-template>
<button class="cta-print right" (click)="print()">Drucken</button> <div class="page-article-details__product-points self-end" *ngIf="store.promotionPoints$ | async; let promotionPoints">
</div> {{ promotionPoints }} Lesepunkte
<div class="title">
{{ item.product?.name }}
</div> </div>
<div class="row"> <!-- TODO: Ticket PREISGEBUNDEN -->
<div> <div class="page-article-details__product-price-bound self-end"></div>
<div class="format" *ngIf="item?.product?.format && item?.product?.formatDetail"> </div>
<img
*ngIf="item?.product?.format !== '--'" <div class="page-article-details__product-origin-infos flex flex-col mb-4">
class="format-icon" <div class="page-article-details__product-manufacturer" data-name="product-manufacturer">{{ item.product?.manufacturer }}</div>
[src]="'/assets/images/Icon_' + item.product?.format + '.svg'"
[alt]="item.product?.formatDetail" <div class="page-article-details__product-language" *ngIf="item?.product?.locale" data-name="product-language">
/> {{ item?.product?.locale }}
{{ item.product?.formatDetail }} </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-sm">{{ 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>
<div *ngIf="item?.product?.volume">Band/Reihe {{ item?.product?.volume }}</div> </ng-container>
<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>
</div> </div>
<div class="row stock"> <div class="page-article-details__shelfinfo" *ngIf="store.isDownload$ | async">
<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">
<ng-container <ng-container
*ngIf=" *ngIf="
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label; item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
@@ -145,24 +261,7 @@
</ng-container> </ng-container>
</ng-template> </ng-template>
</div> </div>
<div class="page-article-details__shelfinfo text-right" *ngIf="!(store.isDownload$ | async)">
<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)">
<ng-container <ng-container
*ngIf=" *ngIf="
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label; item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
@@ -186,109 +285,115 @@
</ng-template> </ng-template>
</div> </div>
</div> </div>
</div>
<div class="bookmark"> <div class="page-article-details__product-formats-container mt-3" *ngIf="item.family?.length > 0">
<div *ngIf="showArchivBadge$ | async" class="archiv-badge"> <hr class="bg-[#E6EFF9] border-t-2" />
<button [uiOverlayTrigger]="archivTooltip" class="bookmark-badge"> <div class="pt-3">
<img src="/assets/images/bookmark_benachrichtigung_archiv.svg" alt="Archiv Badge" /> <div class="page-article-details__product-formats">
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #archivTooltip [closeable]="true"> <span class="mr-2">Auch verfügbar als</span>
<ng-container *ngIf="isAvailable$ | async; else notAvailable">
Archivtitel. Wird nicht mehr gedruckt. Artikel ist bestellbar, weil lieferbar. <ui-slider [scrollDistance]="250">
</ng-container> <a
<ng-template #notAvailable> class="mr-4 text-[#0556B4] font-bold no-underline px-2"
Archivtitel. Wird nicht mehr gedruckt. Nicht bestellbar. *ngFor="let format of item.family"
</ng-template> [routerLink]="getDetailsPath(format.product.ean)"
</ui-tooltip> [queryParamsHandling]="!(isTablet$ | async) ? 'preserve' : ''"
</button> >
</div> <span class="flex items-center">
<div *ngIf="showSubscriptionBadge$ | async"> <img
<button [uiOverlayTrigger]="subscribtionTooltip" class="bookmark-badge"> class="mr-2"
<img src="/assets/images/bookmark_subscription.svg" alt="Fortsetzungsartikel Badge" /> *ngIf="!!format.product?.format"
</button> [src]="'/assets/images/OF_Icon_' + format.product?.format + '.svg'"
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #subscribtionTooltip [closeable]="true" alt="format icon"
>Artikel ist ein Fortsetzungsartikel,<br /> />
Artikel muss über eine Aboabteilung<br /> {{ format.product?.formatDetail }}
bestellt werden. <span class="ml-1">{{ format.catalogAvailability?.price?.value?.value | currency: '€' }}</span>
</ui-tooltip> </span>
</div> </a>
<div *ngIf="showPromotionBadge$ | async" class="promotion-badge"> </ui-slider>
<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> </div>
</div> </div>
</div> </div>
<div class="product-actions"> <hr class="bg-[#E6EFF9] border-t-2 my-3" />
<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 /> <div
<ng-container *ngIf="item.family?.length > 0"> #description
<div class="product-formats"> class="page-article-details__product-description flex flex-col flex-grow overflow-hidden overflow-y-scroll"
<span class="label">Auch verfügbar als</span> *ngIf="item.texts?.length > 0"
>
<ui-slider [scrollDistance]="250"> <div class="whitespace-pre-line">
<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">
{{ item.texts[0].value }} {{ item.texts[0].value }}
</div> </div>
<div class="product-text"> <button class="font-bold flex flex-row text-[#0556B4] items-center mt-2" *ngIf="!showMore" (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 mb-px-100 break-words"
>
<span *ngFor="let text of item.texts | slice: 1"> <span *ngFor="let text of item.texts | slice: 1">
<h3 class="header">{{ text.label }}</h3> <h3 class="my-4 text-regular font-bold">{{ text.label }}</h3>
{{ text.value }} {{ text.value }}
</span> </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> </button>
</div> </div>
</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-small">Empfehlungen</span>
<img class="absolute right-5 bottom-3 h-12" src="assets/images/recommendation_tag.png" alt="recommendation icon" />
</button>
</div>
</ng-container> </ng-container>
</div> </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> </ng-container>
<div class="recommendations-overlay" @slideYAnimation *ngIf="showRecommendations"> <div class="page-article-details__recommendations-overlay absolute top-16 rounded-t-card" @slideYAnimation *ngIf="showRecommendations">
<button class="product-button" (click)="showRecommendations = false">{{ (store.item$ | async)?.product?.name }}</button> <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-card"
(click)="showRecommendations = false"
>
{{ (store.item$ | async)?.product?.name }}
</button>
<page-article-recommendations (close)="showRecommendations = false"></page-article-recommendations> <page-article-recommendations (close)="showRecommendations = false"></page-article-recommendations>
</div> </div>

View File

@@ -1,269 +1,99 @@
:host { :host {
@apply flex flex-col; @apply box-border block h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
} }
.product-card { .page-article-details__container {
@apply flex flex-col bg-white w-full rounded-card shadow-card; @apply h-full w-full bg-white rounded-card shadow-card flex flex-col;
.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;
}
}
}
} }
.product-recommendations { .page-article-details__product-details {
@apply sticky bottom-0 border-none outline-none left-0 right-0 flex items-center px-5 h-16 bg-white w-full; @apply grid gap-x-5;
box-shadow: #dce2e9 0px -2px 18px 0px; grid-template-columns: max-content auto;
grid-template-rows: 2.1875rem repeat(11, minmax(auto, max-content));
.label { grid-template-areas:
@apply uppercase text-active-customer font-bold text-small; '. . . bookmark'
} 'image contributors contributors contributors'
'image title title print'
img { 'image title title .'
@apply absolute right-5 bottom-5 h-12; '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 { .page-article-details__product-bookmark {
@apply absolute w-full top-0 rounded-t-card; grid-area: bookmark;
top: 56px; }
.product-button { .page-article-details__product-image-recessions {
@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; grid-area: image;
box-shadow: 0 -2px 24px 0 #dce2e9; }
height: 60px;
.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 { 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 { ApplicationService } from '@core/application';
import { DomainPrinterService } from '@domain/printer'; import { DomainPrinterService } from '@domain/printer';
import { ItemDTO as PrinterItemDTO } from '@swagger/print'; import { ItemDTO as PrinterItemDTO } from '@swagger/print';
import { PrintModalComponent, PrintModalData } from '@modal/printer'; import { PrintModalComponent, PrintModalData } from '@modal/printer';
import { AvailabilityDTO, BranchDTO } from '@swagger/checkout'; import { BranchDTO } from '@swagger/checkout';
import { UiModalService } from '@ui/modal'; import { UiModalService } from '@ui/modal';
import { ModalReviewsComponent } from '@modal/reviews'; 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 { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, filter, first, map, shareReplay, switchMap } from 'rxjs/operators'; import { debounceTime, filter, first, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
import { ArticleDetailsStore } from './article-details.store'; import { ArticleDetailsStore } from './article-details.store';
import { ModalImagesComponent } from 'apps/modal/images/src/public-api'; import { ModalImagesComponent } from 'apps/modal/images/src/public-api';
import { ProductImageService } from 'apps/cdn/product-image/src/public-api'; import { ProductImageService } from 'apps/cdn/product-image/src/public-api';
@@ -20,7 +18,10 @@ import { BreadcrumbService } from '@core/breadcrumb';
import { ItemDTO } from '@swagger/cat'; import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common'; import { DateAdapter } from '@ui/common';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
import { DomainAvailabilityService } from '@domain/availability'; import { DomainAvailabilityService } from '@domain/availability';
import { EnvironmentService } from '@core/environment';
import { ProductCatalogNavigationService } from '@shared/services';
@Component({ @Component({
selector: 'page-article-details', selector: 'page-article-details',
@@ -114,6 +115,19 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
shareReplay(1) 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( constructor(
public readonly applicationService: ApplicationService, public readonly applicationService: ApplicationService,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
@@ -125,7 +139,11 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
private _dateAdapter: DateAdapter, private _dateAdapter: DateAdapter,
private _datePipe: DatePipe, private _datePipe: DatePipe,
public elementRef: ElementRef, public elementRef: ElementRef,
private _availability: DomainAvailabilityService private _purchaseOptionsModalService: PurchaseOptionsModalService,
private _availability: DomainAvailabilityService,
private _navigationService: ProductCatalogNavigationService,
private _environment: EnvironmentService,
private _router: Router
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -160,16 +178,30 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
filter((f) => !!f) filter((f) => !!f)
); );
const more$ = this.activatedRoute.params.subscribe(() => (this.showMore = false));
this.subscriptions.add(processIdSubscription); this.subscriptions.add(processIdSubscription);
this.subscriptions.add(more$);
this.subscriptions.add(this.store.loadItemById(id$)); this.subscriptions.add(this.store.loadItemById(id$));
this.subscriptions.add(this.store.loadItemByEan(ean$)); 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() { ngOnDestroy() {
this.subscriptions.unsubscribe(); this.subscriptions.unsubscribe();
} }
getDetailsPath(ean?: string) {
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, ean });
}
async updateBreadcrumb(item: ItemDTO) { async updateBreadcrumb(item: ItemDTO) {
const crumbs = await this.breadcrumb const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details', `${item.id}`]) .getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details', `${item.id}`])
@@ -183,13 +215,41 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
this.breadcrumb.addBreadcrumbIfNotExists({ this.breadcrumb.addBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId, key: this.applicationService.activatedProcessId,
name: item.product?.name, 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, params: this.activatedRoute.snapshot.queryParams,
tags: ['catalog', 'details', `${item.id}`], tags: ['catalog', 'details', `${item.id}`],
section: 'customer', 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() { async print() {
const item = await this.store.item$.pipe(first()).toPromise(); const item = await this.store.item$.pipe(first()).toPromise();
this.uiModal.open({ this.uiModal.open({
@@ -262,64 +322,47 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
} }
async showPurchasingModal(selectedBranch?: BranchDTO) { async showPurchasingModal(selectedBranch?: BranchDTO) {
let availableOptions: PurchasingOptions[] = []; const item = await this.store.item$.pipe(first()).toPromise();
const availabilities: { [key: string]: AvailabilityDTO } = {};
const takeNow = await this.store.isTakeAwayAvailabilityAvailable$.pipe(first()).toPromise(); this._purchaseOptionsModalService
if (takeNow) { .open({
availableOptions.push('take-away'); type: 'add',
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,
processId: this.applicationService.activatedProcessId, processId: this.applicationService.activatedProcessId,
availabilities, items: [item],
} as PurchasingOptionsModalData, })
}); .afterClosed$.subscribe((result) => {
if (result?.data === 'continue') {
this.navigateToShoppingCart();
console.log('continue');
} else if (result?.data === 'continue-shopping') {
this.navigateToResultList();
console.log('continue-shopping');
}
});
} }
scrollTop() { navigateToShoppingCart() {
const element = this.elementRef.nativeElement.closest('.main-wrapper'); this._router.navigate([`/kunde/${this.applicationService.activatedProcessId}/cart/review`]);
element?.scrollTo({ top: 0, behavior: 'smooth' }); }
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() { loadImage() {

View File

@@ -1,7 +1,6 @@
:host { :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; box-shadow: 0px -2px 24px 0px #dce2e9;
height: calc(100vh - 342px);
} }
h1 { h1 {

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> <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 { :host {
@apply flex flex-col w-full box-content relative; @apply flex flex-col w-full h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)] 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;
}
} }

View File

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

View File

@@ -6,12 +6,13 @@ import { ArticleSearchComponent } from './article-search.component';
import { SearchResultsModule } from './search-results/search-results.module'; import { SearchResultsModule } from './search-results/search-results.module';
import { SearchMainModule } from './search-main/search-main.module'; import { SearchMainModule } from './search-main/search-main.module';
import { SearchFilterModule } from './search-filter/search-filter.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({ @NgModule({
imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, ShellFilterOverlayModule], imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, SharedFilterOverlayModule],
exports: [ArticleSearchComponent], exports: [ArticleSearchComponent],
declarations: [ArticleSearchComponent], declarations: [ArticleSearchComponent],
providers: [], providers: [ArticleSearchService],
}) })
export class ArticleSearchModule {} export class ArticleSearchModule {}

View File

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

View File

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

View File

@@ -1,30 +1,33 @@
<ng-container *ngIf="filter$ | async; let filter"> <ng-container *ngIf="filter$ | async; let filter">
<div class="catalog-search-filter-content"> <div class="catalog-search-filter-content">
<button class="btn-close" type="button" (click)="close.emit()"> <div class="w-full flex flex-row justify-end items-center">
<ui-icon icon="close" size="20px"></ui-icon> <button (click)="clearFilter(filter)" class="text-[#0556B4] mr-[0.8125rem]">Alle Filter entfernen</button>
</button> <button class="text-black p-4 outline-none border-none bg-transparent" type="button" (click)="closeFilter()">
<ui-icon icon="close" size="15px"></ui-icon>
</button>
</div>
<div class="catalog-search-filter-content-main"> <div class="catalog-search-filter-content-main -mt-12">
<h1 class="text-3xl font-bold text-center py-4">Filter</h1> <h1 class="text-2xl text-[1.625rem] font-bold text-center pt-6 pb-10">Filter</h1>
<ui-filter <shared-filter
[filter]="filter" [filter]="filter"
[loading]="fetching$ | async" [loading]="fetching$ | async"
(search)="applyFilter(filter)" (search)="applyFilter(filter)"
[hint]="searchboxHint$ | async" [hint]="searchboxHint$ | async"
resizeInputOptionsToElement="page-article-search-filter .cta-wrapper"
[scanner]="true" [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> </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> </ng-container>

View File

@@ -1,15 +1,11 @@
:host { :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 { .catalog-search-filter-content {
@apply relative mx-auto p-4; @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 { .catalog-search-filter-content-main {
h1.title { h1.title {
@apply text-center; @apply text-center;
@@ -17,14 +13,12 @@
} }
.cta-wrapper { .cta-wrapper {
@apply fixed bottom-8 whitespace-nowrap; @apply text-center whitespace-nowrap absolute bottom-8 left-0 w-full;
left: 50%;
transform: translateX(-50%);
} }
.cta-reset-filter, .cta-reset-filter,
.cta-apply-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 { &:disabled {
@apply bg-inactive-branch cursor-not-allowed border-none text-white; @apply bg-inactive-branch cursor-not-allowed border-none text-white;
@@ -38,3 +32,7 @@
.cta-apply-filter { .cta-apply-filter {
@apply text-white bg-brand; @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,13 @@
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { UiFilter, UiFilterComponent } from '@ui/filter'; import { ApplicationService } from '@core/application';
import { Observable } from 'rxjs'; import { EnvironmentService } from '@core/environment';
import { map, take } from 'rxjs/operators'; import { Observable, Subject } from 'rxjs';
import { first, map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store'; 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';
import { BreadcrumbService } from '@core/breadcrumb';
@Component({ @Component({
selector: 'page-article-search-filter', selector: 'page-article-search-filter',
@@ -10,51 +15,104 @@ import { ArticleSearchService } from '../article-search.store';
styleUrls: ['search-filter.component.scss'], styleUrls: ['search-filter.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ArticleSearchFilterComponent implements OnInit { export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
@Output() @Output()
close = new EventEmitter(); close = new EventEmitter();
_processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId)));
fetching$: Observable<boolean>; fetching$: Observable<boolean>;
filter$: Observable<UiFilter>; filter$: Observable<Filter>;
searchboxHint$ = this.articleSearch.searchboxHint$; searchboxHint$ = this.articleSearch.searchboxHint$;
@ViewChild(UiFilterComponent, { static: false }) @ViewChild(FilterComponent, { static: false })
uiFilterComponent: UiFilterComponent; uiFilterComponent: FilterComponent;
constructor(private articleSearch: ArticleSearchService) {} get isTablet() {
return this._environment.matchTablet();
}
private _onDestroy$ = new Subject();
constructor(
private articleSearch: ArticleSearchService,
private _environment: EnvironmentService,
private _activatedRoute: ActivatedRoute,
public application: ApplicationService,
private _navigationService: ProductCatalogNavigationService,
private _breadcrumb: BreadcrumbService
) {}
ngOnInit() { ngOnInit() {
this.fetching$ = this.articleSearch.fetching$; this.fetching$ = this.articleSearch.fetching$;
this.filter$ = this.articleSearch.filter$.pipe( this.filter$ = this.articleSearch.filter$.pipe(map((filter) => Filter.create(filter)));
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-',
// })
// )
);
} }
applyFilter(value: UiFilter) { ngOnDestroy(): void {
this._onDestroy$.next();
this._onDestroy$.complete();
}
async closeFilter(): Promise<void> {
const processId = await this._processId$.pipe(first()).toPromise();
const itemId = this._navigationService.getOutletParams(this._activatedRoute)?.right?.id;
if (this.isTablet) {
return this.closeFilterTablet(processId);
}
if (!itemId) {
if (this._navigationService.getOutletLocations(this._activatedRoute)?.left === 'search') {
return await this._navigationService.navigateToProductSearch({ processId, queryParamsHandling: 'preserve' });
}
return await this._navigationService.navigateToResults({ processId, queryParamsHandling: 'preserve' });
} else {
return await this._navigationService.navigateToDetails({ processId, itemId, queryParamsHandling: 'preserve' });
}
}
async closeFilterTablet(processId: number): Promise<void> {
const latestBreadcrumb = await this._breadcrumb.getLastActivatedBreadcrumbByKey$(processId).pipe(first()).toPromise();
if (latestBreadcrumb?.tags?.find((tag) => tag === 'results')) {
return await this._navigationService.navigateToResults({ processId, queryParamsHandling: 'preserve' });
} else {
return await this._navigationService.navigateToProductSearch({ processId, queryParamsHandling: 'preserve' });
}
}
applyFilter(value: Filter) {
this.uiFilterComponent?.cancelAutocomplete(); this.uiFilterComponent?.cancelAutocomplete();
this.articleSearch.setFilter(value); this.articleSearch.setFilter(value);
this.articleSearch.search({ clear: true }); this.articleSearch.search({ clear: true });
this.articleSearch.searchCompleted.pipe(take(1)).subscribe((s) => { this.articleSearch.searchCompleted
if (s.searchState === '') { .pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
this.close.emit(); .subscribe(([state, processId]) => {
} if (state.searchState === '') {
}); const params = state.filter.getQueryParams();
if (state.hits === 1 && this.isTablet) {
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.navigateToResults({
processId,
queryParams: params,
});
}
}
});
} }
resetFilter(value: UiFilter) { clearFilter(value: Filter) {
value.unselectAll();
}
resetFilter(value: Filter) {
const queryParams = { main_qs: value?.getQueryParams()?.main_qs || '' }; const queryParams = { main_qs: value?.getQueryParams()?.main_qs || '' };
this.articleSearch.setDefaultFilter(queryParams); this.articleSearch.setDefaultFilter(queryParams);
} }

View File

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

View File

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

View File

@@ -1,71 +1,13 @@
:host { :host {
@apply flex flex-col box-border; @apply flex flex-col box-border h-full;
} }
.title { .page-search-main__filter {
@apply text-page-heading font-bold; &.active {
} @apply bg-[#596470] text-white ml-px-5;
}
.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;
} }
::ng-deep page-article-search-main ui-filter-filter-group-main .ui-filter-chip:not(.selected) { ::ng-deep page-article-search-main ui-filter-filter-group-main .ui-filter-chip:not(.selected) {
@apply bg-glitter; @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 { BreadcrumbService } from '@core/breadcrumb';
import { ApplicationService } from '@core/application'; import { ApplicationService } from '@core/application';
import { DomainCatalogService } from '@domain/catalog'; import { DomainCatalogService } from '@domain/catalog';
import { UiFilter, UiFilterInputGroupMainComponent } from '@ui/filter';
import { combineLatest, NEVER, Subscription } from 'rxjs'; 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 { ArticleSearchService } from '../article-search.store';
import { isEqual } from 'lodash'; 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({ @Component({
selector: 'page-article-search-main', selector: 'page-article-search-main',
@@ -26,15 +28,39 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
subscriptions = new Subscription(); subscriptions = new Subscription();
@ViewChild(UiFilterInputGroupMainComponent, { static: false }) hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
uiInputGroupMain: UiFilterInputGroupMainComponent; 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 filterRoute() {
const itemId = this._navigationService?.getOutletParams(this.route)?.right?.id;
return this._navigationService.getArticleSearchResultsAndFilterPath({
processId: this.application.activatedProcessId,
itemId,
});
}
constructor( constructor(
private searchService: ArticleSearchService, private searchService: ArticleSearchService,
private catalog: DomainCatalogService, private catalog: DomainCatalogService,
private route: ActivatedRoute, private route: ActivatedRoute,
private application: ApplicationService, private application: ApplicationService,
private breadcrumb: BreadcrumbService private breadcrumb: BreadcrumbService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -44,7 +70,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
.subscribe(async ([processId, queryParams]) => { .subscribe(async ([processId, queryParams]) => {
const processChanged = processId !== this.searchService.processId; const processChanged = processId !== this.searchService.processId;
if (!(this.searchService.filter instanceof UiFilter)) { if (!(this.searchService.filter instanceof Filter)) {
await this.searchService.setDefaultFilter(); await this.searchService.setDefaultFilter();
} }
@@ -57,6 +83,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
const cleanQueryParams = this.cleanupQueryParams(queryParams); const cleanQueryParams = this.cleanupQueryParams(queryParams);
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) { if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
// Reset Filter on Product Search Shell Navigation click
await this.searchService.setDefaultFilter(queryParams); await this.searchService.setDefaultFilter(queryParams);
} }
@@ -85,16 +112,30 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
this.updateBreadcrumb(this.searchService.processId, this.searchService.filter.getQueryParams()); this.updateBreadcrumb(this.searchService.processId, this.searchService.filter.getQueryParams());
} }
search(filter: UiFilter) { search(filter: Filter) {
this.uiInputGroupMain.cancelAutocomplete(); this.sharedFilterInputGroupMain.cancelAutocomplete();
this.searchService.setFilter(filter); this.searchService.setFilter(filter);
this.searchService.search({ clear: true }); this.searchService.search({ clear: true });
} }
setQueryHistory(filter: UiFilter, query: string) { setQueryHistory(filter: Filter, query: string) {
filter.fromQueryParams({ main_qs: query }); 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> = {}) { cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params }; const clean = { ...params };

View File

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

View File

@@ -1,17 +1,43 @@
<div class="thumbnail animation"></div> <ng-container *ngIf="!(mainOutletActive$ | async); else mainOutlet">
<div class="col"> <div class="bg-ucla-blue rounded-card w-[4.375rem] h-[5.625rem] animate-[load_1s_linear_infinite]"></div>
<div class="author animation"></div> <div class="flex flex-col flex-grow">
<div class="row"> <div class="h-4 bg-ucla-blue ml-4 mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="title animation"></div> <div class="flex flex-row justify-between flex-grow">
<div class="price animation"></div> <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>
<div class="space"></div> </ng-container>
<div class="row">
<div class="category animation"></div> <ng-template #mainOutlet>
<div class="stock animation"></div> <div class="bg-ucla-blue rounded-card w-[3rem] h-[4.125rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-col ml-4 w-[30%]">
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-6 bg-ucla-blue mb-2 w-[12.5rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-6 bg-ucla-blue w-[12.5rem] animate-[load_1s_linear_infinite]"></div>
</div> </div>
<div class="row"> <div class="flex flex-col ml-14 w-[30%]">
<div class="manufacturer animation"></div> <div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="ava animation"></div> <div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div> </div>
</div> <div class="flex flex-col ml-10 w-[20%]">
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
<div class="flex flex-col ml-2 w-[20%] items-end">
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
</ng-template>

View File

@@ -1,61 +1,3 @@
:host { :host {
@apply flex flex-row rounded-card bg-white mb-2 p-4; @apply flex flex-row rounded-card bg-white mb-2 p-4 w-full h-[212px] desktop-small:h-[181px];
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;
} }

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({ @Component({
selector: 'page-search-result-item-loading', selector: 'page-search-result-item-loading',
@@ -7,5 +10,13 @@ import { Component, ChangeDetectionStrategy } from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class SearchResultItemLoadingComponent { 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,122 @@
<a class="product-list-result-content" [routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'details', item?.id]"> <a
<div class="item-thumbnail"> 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-card"
<img loading="lazy" *ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" /> [class.page-search-result-item__item-card-main]="mainOutletActive$ | async"
</div> [routerLink]="detailsPath"
[routerLinkActive]="!isTablet && !(mainOutletActive$ | async) ? 'active' : ''"
<div class="item-contributors"> [queryParamsHandling]="!isTablet ? 'preserve' : ''"
<a >
*ngFor="let contributor of contributors; let last = last" <div class="page-search-result-item__item-thumbnail text-center mr-4 w-[50px] h-[79px]">
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']" <img
[queryParams]="{ main_qs: contributor, main_author: 'author' }" class="page-search-result-item__item-image w-[50px] h-[79px]"
(click)="$event?.stopPropagation()" loading="lazy"
> *ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl"
{{ contributor }}{{ last ? '' : ';' }} [src]="thumbnailUrl"
</a> [alt]="item?.product?.name"
/>
</div> </div>
<div <div
class="item-title" class="page-search-result-item__item-grid-container"
[class.xl]="item?.product?.name?.length >= 35" [class.page-search-result-item__item-grid-container-main]="mainOutletActive$ | async"
[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
</div> class="page-search-result-item__item-contributors desktop-small:text-sm 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"> <div
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }} class="page-search-result-item__item-title font-bold text-2xl"
</div> [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-sm]="item?.product?.name?.length >= 60 || !isTablet"
[class.text-xs]="item?.product?.name?.length >= 100 || (!isTablet && item?.product?.name?.length >= 70)"
>
{{ item?.product?.name }}
</div>
<div *ngIf="selectable" class="item-data-selector"> <div class="page-search-result-item__item-format desktop-small:text-sm">
<ui-select-bullet [ngModel]="selected" (ngModelChange)="setSelected($event)"></ui-select-bullet> <div *ngIf="item?.product?.format && item?.product?.formatDetail" class="font-bold flex flex-row">
</div> <img
class="mr-3"
<div class="item-stock z-dropdown" [uiOverlayTrigger]="tooltip" [overlayTriggerDisabled]="!(stockTooltipText$ | async)"> *ngIf="item?.product?.format !== '--'"
<ng-container *ngIf="isOrderBranch$ | async"> loading="lazy"
<div class="flex flex-row items-center justify-between"> src="assets/images/Icon_{{ item?.product?.format }}.svg"
<ui-icon icon="home" size="1em"></ui-icon> [alt]="item?.product?.formatDetail"
<span />
*ngIf="inStock$ | async; let stock" {{ item?.product?.formatDetail | substr: 25 }}
[class.skeleton]="stock.inStock === undefined"
class="min-w-[1rem] text-right inline-block"
>{{ stock?.inStock }}</span
>
<span>x</span>
</div> </div>
</ng-container> </div>
<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="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"> <div class="page-search-result-item__item-manufacturer desktop-small:text-sm">
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }} {{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }}
</div> </div>
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail"> <div class="page-search-result-item__item-misc desktop-small:text-sm">
<img {{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
*ngIf="item?.product?.format !== '--'" {{ publicationDate }}
loading="lazy" </div>
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
</div>
<div class="item-misc"> <div class="page-search-result-item__item-price desktop-small:text-sm font-bold justify-self-end">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br /> {{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span> </div>
{{ publicationDate }}
<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-sm 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-sm justify-self-start"
[class.justify-self-end]="!(mainOutletActive$ | async)"
[class.xs]="item?.catalogAvailability?.sscText?.length >= 60"
>
<strong>{{ item?.catalogAvailability?.ssc }}</strong> -
{{ !isTablet ? item?.catalogAvailability?.sscText : (item?.catalogAvailability?.sscText | substr: 18) }}
</div>
</div> </div>
</a> </a>

View File

@@ -1,113 +1,90 @@
.product-list-result-content { :host {
@apply text-black no-underline grid; @apply flex flex-col w-full h-[13.25rem] desktop-small:h-[11.3125rem];
grid-template-columns: 102px 50% auto; }
grid-template-rows: auto;
.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-[0.375rem];
grid-template-areas: grid-template-areas:
'item-thumbnail item-contributors item-contributors' 'contributors contributors contributors'
'item-thumbnail item-title item-price' 'title title price'
'item-thumbnail item-title item-data-selector' 'title title price'
'item-thumbnail item-format item-stock' 'title title select'
'item-thumbnail item-misc item-ssc'; 'format format select'
'manufacturer manufacturer stock'
'misc ssc ssc';
} }
.item-thumbnail { .page-search-result-item__item-grid-container-main {
grid-area: item-thumbnail; @apply gap-x-4;
width: 70px; grid-template-rows: 1.3125rem 1.3125rem auto;
@apply mr-8; grid-template-columns: 30% 30% 20% 8.8% auto;
img { grid-template-areas:
max-width: 100%; 'contributors format stock price .'
max-height: 150px; 'title manufacturer ssc . select'
@apply rounded-card shadow-cta; 'title misc . . .';
} }
}
.page-search-result-item__item-contributors {
.item-contributors { grid-area: contributors;
grid-area: item-contributors; }
height: 22px;
text-overflow: ellipsis; .page-search-result-item__item-price {
overflow: hidden; grid-area: price;
max-width: 600px; }
white-space: nowrap;
.page-search-result-item__item-title {
a { grid-area: title;
@apply text-active-customer font-bold no-underline; }
}
} .page-search-result-item__item-format {
grid-area: format;
.item-title { }
grid-area: item-title;
@apply font-bold text-2xl; .page-search-result-item__item-manufacturer {
height: 64px; grid-area: manufacturer;
max-height: 64px; }
}
.page-search-result-item__item-misc {
.item-title.xl { grid-area: misc;
@apply font-bold text-xl; }
}
.page-search-result-item__item-select-bullet {
.item-title.lg { grid-area: select;
@apply font-bold text-lg; }
}
.page-search-result-item__item-stock {
.item-title.md { grid-area: stock;
@apply font-bold text-base; }
}
.page-search-result-item__item-ssc {
.item-title.sm { grid-area: ssc;
@apply font-bold text-sm; }
}
.page-search-result-item__item-image {
.item-title.xs { box-shadow: 0px 6px 18px rgba(0, 0, 0, 0.197935);
@apply font-bold text-xs; }
}
.active,
.item-price { .hover:hover {
grid-area: item-price; @apply bg-[#D8DFE5] border border-solid border-[#0556B4];
@apply font-bold text-xl text-right;
} .page-search-result-item__item-select-bullet {
.isa-select-bullet::before {
.item-format { @apply bg-[#fff];
grid-area: item-format; }
@apply flex flex-row items-center font-bold text-lg whitespace-nowrap;
.isa-select-bullet:checked:before {
img { @apply bg-[#596470];
@apply mr-2; }
}
} .isa-select-bullet:hover::before {
@apply bg-[#778490];
.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;
} }
} }

View File

@@ -1,14 +1,17 @@
import { DatePipe } from '@angular/common'; 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 { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability'; import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
import { ComponentStore } from '@ngrx/component-store'; import { ComponentStore } from '@ngrx/component-store';
import { ItemDTO } from '@swagger/cat'; import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common'; import { DateAdapter } from '@ui/common';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { combineLatest } from 'rxjs'; 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 { ArticleSearchService } from '../article-search.store';
import { ProductCatalogNavigationService } from '@shared/services';
import { ActivatedRoute } from '@angular/router';
export interface SearchResultItemComponentState { export interface SearchResultItemComponentState {
item?: ItemDTO; item?: ItemDTO;
@@ -36,16 +39,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
readonly item$ = this.select((s) => s.item); readonly item$ = this.select((s) => s.item);
@Input() selected$ = this._articleSearchService.selectedItemIds$.pipe(map((selectedItemIds) => selectedItemIds.includes(this.item?.id)));
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);
@Input() @Input()
get selectable() { get selectable() {
@@ -58,7 +52,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
} }
@Output() @Output()
selectedChange = new EventEmitter<boolean>(); selectedChange = new EventEmitter<ItemDTO>();
get contributors() { get contributors() {
return this.item?.product?.contributors?.split(';').map((val) => val.trim()); return this.item?.product?.contributors?.split(';').map((val) => val.trim());
@@ -77,6 +71,22 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
return ''; 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(); defaultBranch$ = this._availability.getDefaultBranch();
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe( selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
@@ -110,9 +120,11 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
inStock$ = combineLatest([this.item$, this.selectedBranchId$, this.defaultBranch$]).pipe( inStock$ = combineLatest([this.item$, this.selectedBranchId$, this.defaultBranch$]).pipe(
debounceTime(100), debounceTime(100),
filter(([item, branch, defaultBranch]) => !!item && !!defaultBranch),
switchMap(([item, branch, defaultBranch]) => switchMap(([item, branch, defaultBranch]) =>
this._stockService.getInStock$({ itemId: item.id, branchId: branch?.id ?? defaultBranch?.id }) this._stockService.getInStock$({ itemId: item.id, branchId: branch?.id ?? defaultBranch?.id })
) ),
shareReplay(1)
); );
constructor( constructor(
@@ -121,7 +133,10 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
private _articleSearchService: ArticleSearchService, private _articleSearchService: ArticleSearchService,
public applicationService: ApplicationService, public applicationService: ApplicationService,
private _stockService: DomainInStockService, private _stockService: DomainInStockService,
private _availability: DomainAvailabilityService private _availability: DomainAvailabilityService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService,
private _activatedRoute: ActivatedRoute
) { ) {
super({ super({
selected: false, selected: false,
@@ -129,7 +144,16 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
}); });
} }
setSelected(selected: boolean) { setSelected() {
this._articleSearchService.setSelected({ selected, itemId: this.item?.id }); 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
<div class="hits" *ngIf="hits$ | async; let hits">{{ hits }} Titel</div> class="page-search-results__header bg-background-liste flex items-end justify-between"
<ui-order-by-filter [orderBy]="(filter$ | async)?.orderBy" (selectedOrderByChange)="search(); updateBreadcrumbs()"> </ui-order-by-filter> [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-card 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-sm"
[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, 1, 1] : []"
[orderBy]="(filter$ | async)?.orderBy"
(selectedOrderByChange)="search(); updateBreadcrumbs()"
>
</shared-order-by-filter>
</div> </div>
<cdk-virtual-scroll-viewport <cdk-virtual-scroll-viewport
#scrollContainer #scrollContainer
class="product-list scroll-bar scroll-bar-margin" class="product-list"
[itemSize]="187" [itemSize]="(mainOutletActive$ | async) ? 187 : 98"
minBufferPx="1200" minBufferPx="2800"
maxBufferPx="1200" maxBufferPx="2800"
(scrolledIndexChange)="scrolledIndexChange($event)" (scrolledIndexChange)="scrolledIndexChange($event)"
> >
<div class="product-list-result" *cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId"> <search-result-item
<search-result-item class="page-search-results__result-item"
[selected]="item | searchResultSelected: searchService.selectedItemIds" [class.page-search-results__result-item-main]="mainOutletActive$ | async"
[selectable]="isSelectable(item)" *cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId"
[item]="item" (selectedChange)="addToCart($event)"
></search-result-item> [selectable]="isSelectable(item)"
</div> [item]="item"
></search-result-item>
<page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading> <page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading>
</cdk-virtual-scroll-viewport> </cdk-virtual-scroll-viewport>
<div class="actions"> <div *ngIf="isTablet" class="actions z-fixed">
<button <button
[disabled]="loading$ | async" [disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0" *ngIf="(selectedItemIds$ | async)?.length > 0"
class="cta-cart cta-action-primary" class="cta-cart cta-action-primary"
(click)="addSelectedItemsToCart()" (click)="addToCart()"
> >
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner> <ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button> </button>

View File

@@ -1,34 +1,36 @@
:host { :host {
@apply box-border grid; @apply box-border grid h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
max-height: calc(100vh - 364px); grid-template-rows: auto auto 1fr;
height: 100vh;
grid-template-rows: auto 1fr;
} }
.product-list { .product-list {
@apply m-0 p-0 mt-2; @apply m-0 p-0 mt-px-2;
} }
.product-list-result { .page-search-results__result-item {
@apply list-none bg-white rounded-card p-4 mb-2; @apply mb-px-10;
height: 187px;
max-height: 187px;
} }
.filter-wrapper { .page-search-results__result-item-main {
@apply block relative; @apply mb-[5px];
}
.hits { .page-search-results__order-by {
@apply text-inactive-branch font-semibold absolute top-2 right-0; @apply bg-white rounded-t-card px-6 desktop-small:px-8;
} }
ui-order-by-filter { .page-search-results__order-by-main {
@apply mx-auto; @apply pl-[4.9375rem] px-4;
}
.page-search-results__filter {
&.active {
@apply bg-[#596470] text-white ml-px-5;
} }
} }
.actions { .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%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
@@ -44,3 +46,24 @@
@apply bg-brand text-white; @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: 30% 30% 20% 8.8% auto;
.group {
@apply desktop-small:justify-start;
}
.group:last-child {
@apply justify-self-end;
.order-by-filter-button {
@apply ml-12 mr-0;
}
}
.order-by-filter-button {
@apply ml-0 mr-12;
}
}

View File

@@ -3,18 +3,20 @@ import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, ViewC
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application'; import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb'; import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { DomainCheckoutService } from '@domain/checkout'; import { DomainCheckoutService } from '@domain/checkout';
import { ItemDTO } from '@swagger/cat'; import { ItemDTO } from '@swagger/cat';
import { AddToShoppingCartDTO } from '@swagger/checkout'; import { AddToShoppingCartDTO } from '@swagger/checkout';
import { UiFilter } from '@ui/filter';
import { UiErrorModalComponent, UiModalService } from '@ui/modal'; import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { CacheService } from 'apps/core/cache/src/public-api'; import { CacheService } from 'apps/core/cache/src/public-api';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; 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 { ArticleSearchService } from '../article-search.store';
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component'; import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
import { SearchResultItemComponent } from './search-result-item.component'; import { SearchResultItemComponent } from './search-result-item.component';
import { ProductCatalogNavigationService } from '@shared/services';
import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib';
@Component({ @Component({
selector: 'page-search-results', selector: 'page-search-results',
@@ -27,6 +29,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
@ViewChild('scrollContainer', { static: true }) @ViewChild('scrollContainer', { static: true })
scrollContainer: CdkVirtualScrollViewport; scrollContainer: CdkVirtualScrollViewport;
@ViewChild(FilterInputGroupMainComponent, { static: false })
sharedFilterInputGroupMain: FilterInputGroupMainComponent;
results$ = this.searchService.items$; results$ = this.searchService.items$;
fetching$ = this.searchService.fetching$; fetching$ = this.searchService.fetching$;
hits$ = this.searchService.hits$; hits$ = this.searchService.hits$;
@@ -35,6 +40,8 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
selectedItemIds$ = this.searchService.selectedItemIds$; selectedItemIds$ = this.searchService.selectedItemIds$;
searchboxHint$ = this.searchService.searchboxHint$;
selectedItems$ = combineLatest([this.results$, this.selectedItemIds$]).pipe( selectedItems$ = combineLatest([this.results$, this.selectedItemIds$]).pipe(
map(([items, selectedItemIds]) => { map(([items, selectedItemIds]) => {
return items?.filter((item) => selectedItemIds?.find((selectedItemId) => item.id === selectedItemId)); 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; 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( constructor(
public searchService: ArticleSearchService, public searchService: ArticleSearchService,
private route: ActivatedRoute, private route: ActivatedRoute,
private application: ApplicationService, public application: ApplicationService,
private breadcrumb: BreadcrumbService, private breadcrumb: BreadcrumbService,
private cache: CacheService, private cache: CacheService,
private _uiModal: UiModalService, private _uiModal: UiModalService,
private _checkoutService: DomainCheckoutService private _checkoutService: DomainCheckoutService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService
) {} ) {}
ngOnInit() { ngOnInit() {
@@ -72,7 +104,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id; const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id;
if (processChanged) { if (processChanged) {
if (!!this.searchService.processId && this.searchService.filter instanceof UiFilter) { if (!!this.searchService.processId && this.searchService.filter instanceof Filter) {
this.cacheCurrentData( this.cacheCurrentData(
this.searchService.processId, this.searchService.processId,
this.searchService.filter.getQueryParams(), this.searchService.filter.getQueryParams(),
@@ -87,7 +119,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.searchService.setBranch(selectedBranch); this.searchService.setBranch(selectedBranch);
} }
if (!(this.searchService.filter instanceof UiFilter)) { if (!(this.searchService.filter instanceof Filter)) {
await this.searchService.setDefaultFilter(); await this.searchService.setDefaultFilter();
} }
@@ -117,6 +149,27 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
await this.removeDetailsBreadcrumb(processId); 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() { ngOnDestroy() {
@@ -127,7 +180,26 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.unselectAll(); 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 }); this.searchService.search({ clear: true });
} }
@@ -187,7 +259,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
await this.breadcrumb.addBreadcrumbIfNotExists({ await this.breadcrumb.addBreadcrumbIfNotExists({
key: processId, key: processId,
name, name,
path: `/kunde/${this.application.activatedProcessId}/product/search/results`, path: this._navigationService.getArticleSearchResultsPath(this.application.activatedProcessId),
params: queryParams, params: queryParams,
section: 'customer', section: 'customer',
tags: ['catalog', 'filter', 'results'], tags: ['catalog', 'filter', 'results'],
@@ -241,6 +313,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
// Zeige Select Radio Button nicht an wenn Item Archivartikel oder Fortsetzungsartikel ist // Zeige Select Radio Button nicht an wenn Item Archivartikel oder Fortsetzungsartikel ist
const isArchiv = item?.catalogAvailability?.status === 1; const isArchiv = item?.catalogAvailability?.status === 1;
const isFortsetzung = item?.features?.find((i) => i?.key === 'PFO'); const isFortsetzung = item?.features?.find((i) => i?.key === 'PFO');
return !(isArchiv || isFortsetzung); return !(isArchiv || isFortsetzung);
} }
@@ -249,29 +322,44 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.searchService.patchState({ selectedItemIds: [] }); this.searchService.patchState({ selectedItemIds: [] });
} }
async addSelectedItemsToCart() { async addToCart(item?: ItemDTO) {
this.loading$.next(true); 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 items: AddToShoppingCartDTO[] = [];
const canAddItemsPayload = []; const canAddItemsPayload = [];
for (const item of selectedItems) { for (const item of selectedItems) {
const shoppingCartItem = this._createShoppingCartItem(item);
const isDownload = item?.product?.format === 'EB' || item?.product?.format === 'DL'; 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) { if (isDownload) {
shoppingCartItem.destination = { data: { target: 16 } }; shoppingCartItem.destination = { data: { target: 16 } };
canAddItemsPayload.push({ canAddItemsPayload.push({

View File

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

View File

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

View File

@@ -1,5 +1,32 @@
<shared-breadcrumb class="my-4" [key]="activatedProcessId$ | async" [tags]="['catalog']"> <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-branch-selector>
</shared-breadcrumb> </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 { :host {
@apply block relative; @apply block relative h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
} }
shell-breadcrumb { shell-breadcrumb {

View File

@@ -1,12 +1,14 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApplicationService } from '@core/application'; import { ApplicationService } from '@core/application';
import { AuthService } from '@core/auth';
import { EnvironmentService } from '@core/environment'; import { EnvironmentService } from '@core/environment';
import { BranchSelectorComponent } from '@shared/components/branch-selector'; import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { BreadcrumbComponent } from '@shared/components/breadcrumb'; import { BreadcrumbComponent } from '@shared/components/breadcrumb';
import { BranchDTO } from '@swagger/checkout'; import { BranchDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalService } from '@ui/modal'; import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { BehaviorSubject, from, fromEvent, Observable, Subject } from 'rxjs'; import { fromEvent, Observable, Subject } from 'rxjs';
import { first, map, switchMap, takeUntil } from 'rxjs/operators'; import { first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
@Component({ @Component({
selector: 'page-catalog', selector: 'page-catalog',
@@ -27,33 +29,62 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
_onDestroy$ = new Subject<boolean>(); _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( constructor(
public application: ApplicationService, public application: ApplicationService,
private _uiModal: UiModalService, private _uiModal: UiModalService,
public auth: AuthService,
private _environmentService: EnvironmentService, private _environmentService: EnvironmentService,
private _renderer: Renderer2 private _renderer: Renderer2,
private _activatedRoute: ActivatedRoute,
private _router: Router
) {} ) {}
ngOnInit() { ngOnInit() {
this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId))); this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId)));
this.selectedBranch$ = this.activatedProcessId$.pipe(switchMap((processId) => this.application.getSelectedBranch$(Number(processId)))); this.selectedBranch$ = this.activatedProcessId$.pipe(switchMap((processId) => this.application.getSelectedBranch$(Number(processId))));
this.application.setTitle('Artikelsuche');
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
if (this._environmentService.isTablet()) { fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
fromEvent(this.branchSelectorRef.nativeElement, 'focusin') .pipe(takeUntil(this._onDestroy$), withLatestFrom(this.isTablet$))
.pipe(takeUntil(this._onDestroy$)) .subscribe(([_, isTablet]) => {
.subscribe((_) => { if (isTablet) {
this._renderer.setStyle(this.branchSelectorRef?.nativeElement, 'width', this.branchSelectorWidth); this._renderer.setStyle(this.branchSelectorRef?.nativeElement, 'width', this.branchSelectorWidth);
}); }
});
fromEvent(this.branchSelectorRef.nativeElement, 'focusout') fromEvent(this.branchSelectorRef.nativeElement, 'focusout')
.pipe(takeUntil(this._onDestroy$)) .pipe(takeUntil(this._onDestroy$))
.subscribe((_) => { .subscribe((_) => {
this._renderer.removeStyle(this.branchSelectorRef?.nativeElement, 'width'); this._renderer.removeStyle(this.branchSelectorRef?.nativeElement, 'width');
}); });
}
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

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

View File

@@ -6,8 +6,6 @@ import { DomainCheckoutService } from '@domain/checkout';
import { AvailabilityDTO, DestinationDTO, NotificationChannel, ShoppingCartItemDTO, ShoppingCartDTO } from '@swagger/checkout'; import { AvailabilityDTO, DestinationDTO, NotificationChannel, ShoppingCartItemDTO, ShoppingCartDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiMessageModalComponent, UiModalService } from '@ui/modal'; import { UiErrorModalComponent, UiMessageModalComponent, UiModalService } from '@ui/modal';
import { PrintModalData, PrintModalComponent } from '@modal/printer'; 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 { AuthService } from '@core/auth';
import { first, map, shareReplay, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators'; import { first, map, shareReplay, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs'; import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs';
@@ -15,13 +13,11 @@ import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb'; import { BreadcrumbService } from '@core/breadcrumb';
import { DomainPrinterService } from '@domain/printer'; import { DomainPrinterService } from '@domain/printer';
import { CheckoutDummyComponent } from '../checkout-dummy/checkout-dummy.component'; import { CheckoutDummyComponent } from '../checkout-dummy/checkout-dummy.component';
import { ResponseArgsOfItemDTO } from '@swagger/cat';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { emailNotificationValidator, mobileNotificationValidator } from '@shared/components/notification-channel-control'; 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 { ComponentStore, tapResponse } from '@ngrx/component-store';
import { CheckoutDummyData } from '../checkout-dummy/checkout-dummy-data'; import { CheckoutDummyData } from '../checkout-dummy/checkout-dummy-data';
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
export interface CheckoutReviewComponentState { export interface CheckoutReviewComponentState {
shoppingCart: ShoppingCartDTO; shoppingCart: ShoppingCartDTO;
@@ -242,7 +238,8 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
private domainCatalogService: DomainCatalogService, private domainCatalogService: DomainCatalogService,
private breadcrumb: BreadcrumbService, private breadcrumb: BreadcrumbService,
private domainPrinterService: DomainPrinterService, private domainPrinterService: DomainPrinterService,
private _fb: UntypedFormBuilder private _fb: UntypedFormBuilder,
private _purchaseOptionsModalService: PurchaseOptionsModalService
) { ) {
super({ super({
shoppingCart: undefined, shoppingCart: undefined,
@@ -274,7 +271,7 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
shoppingCart, shoppingCart,
shoppingCartItems, shoppingCartItems,
}); });
this.checkQuantityErrors(shoppingCartItems); // this.checkQuantityErrors(shoppingCartItems);
}, },
(err) => {}, (err) => {},
() => {} () => {}
@@ -285,15 +282,15 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
) )
); );
checkQuantityErrors(shoppingCartItems: ShoppingCartItemDTO[]) { // checkQuantityErrors(shoppingCartItems: ShoppingCartItemDTO[]) {
shoppingCartItems.forEach((item) => { // shoppingCartItems.forEach((item) => {
if (item.features?.orderType === 'Rücklage') { // if (item.features?.orderType === 'Abholung') {
this.setQuantityError(item, item.availability, item.quantity > item.availability?.inStock); // this.setQuantityError(item, item.availability, item.quantity > item.availability?.inStock);
} else { // } else {
this.setQuantityError(item, item.availability, false); // this.setQuantityError(item, item.availability, false);
} // }
}); // });
} // }
async updateBreadcrumb() { async updateBreadcrumb() {
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({ await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
@@ -434,171 +431,10 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
} }
async changeItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) { async changeItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) {
this.loadingOnItemChangeById$.next(shoppingCartItem.id); this._purchaseOptionsModalService.open({
processId: this.applicationService.activatedProcessId,
const quantity = shoppingCartItem.quantity; items: [shoppingCartItem],
type: 'update',
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);
}); });
} }
@@ -649,7 +485,7 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
}) })
.toPromise(); .toPromise();
this.setQuantityError(shoppingCartItem, availability, availability?.inStock < quantity); // this.setQuantityError(shoppingCartItem, availability, availability?.inStock < quantity);
break; break;
case 'Abholung': case 'Abholung':
availability = await this.availabilityService availability = await this.availabilityService
@@ -727,7 +563,7 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
}, },
}) })
.toPromise(); .toPromise();
this.setQuantityError(shoppingCartItem, availability, false); // this.setQuantityError(shoppingCartItem, availability, false);
} else if (availability) { } else if (availability) {
// Wenn das Ergebnis der Availability Abfrage keinen Preis zurückliefert (z.B. HFI Geschenkkarte), wird der Preis aus der // Wenn das Ergebnis der Availability Abfrage keinen Preis zurückliefert (z.B. HFI Geschenkkarte), wird der Preis aus der
// Availability vor der Abfrage verwendet // Availability vor der Abfrage verwendet
@@ -758,16 +594,16 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
this.loadingOnQuantityChangeById$.next(undefined); this.loadingOnQuantityChangeById$.next(undefined);
} }
setQuantityError(item: ShoppingCartItemDTO, availability: AvailabilityDTO, error: boolean) { // setQuantityError(item: ShoppingCartItemDTO, availability: AvailabilityDTO, error: boolean) {
const quantityErrors: { [key: string]: string } = this.quantityError$.value; // const quantityErrors: { [key: string]: string } = this.quantityError$.value;
if (error) { // if (error) {
quantityErrors[item.product.catalogProductNumber] = `${availability.inStock} Exemplar(e) sofort lieferbar`; // quantityErrors[item.product.catalogProductNumber] = `${availability.inStock} Exemplar(e) sofort lieferbar`;
this.quantityError$.next({ ...quantityErrors }); // this.quantityError$.next({ ...quantityErrors });
} else { // } else {
delete quantityErrors[item.product.catalogProductNumber]; // delete quantityErrors[item.product.catalogProductNumber];
this.quantityError$.next({ ...quantityErrors }); // this.quantityError$.next({ ...quantityErrors });
} // }
} // }
// Bei unbekannten Kunden und DIG Bestellung findet ein Vergleich der Preise statt // Bei unbekannten Kunden und DIG Bestellung findet ein Vergleich der Preise statt
compareDeliveryAndCatalogPrice(availability: AvailabilityDTO, orderType: string, shoppingCartItemPrice: number) { compareDeliveryAndCatalogPrice(availability: AvailabilityDTO, orderType: string, shoppingCartItemPrice: number) {
@@ -816,16 +652,10 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
} }
async showPurchasingListModal(shoppingCartItems: ShoppingCartItemDTO[]) { async showPurchasingListModal(shoppingCartItems: ShoppingCartItemDTO[]) {
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise(); this._purchaseOptionsModalService.open({
this.uiModal.open({ processId: this.applicationService.activatedProcessId,
content: PurchasingOptionsListModalComponent, items: shoppingCartItems,
title: 'Wie möchten Sie die Artikel erhalten?', type: 'update',
config: { showScrollbarY: false },
data: {
processId: this.applicationService.activatedProcessId,
shoppingCartItems: shoppingCartItems,
customerFeatures,
} as PurchasingOptionsListModalData,
}); });
} }

View File

@@ -1,4 +1,3 @@
// start:ng42.barrel // start:ng42.barrel
export * from './page-checkout.module'; export * from './page-checkout.module';
export * from './page-checkout-modals.module';
// end:ng42.barrel // 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,39 +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%;
}

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)?.name"
(selectBranch)="selectBranch($event)"
></ui-branch-dropdown>

View File

@@ -1,36 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BranchDTO } from '@swagger/checkout';
import { first } 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$;
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;
}
}
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>

View File

@@ -1,180 +0,0 @@
:host {
@apply text-black no-underline grid py-4;
grid-template-columns: 102px 60% auto;
grid-template-rows: auto;
grid-template-areas:
'item-thumbnail item-contributors item-contributors'
'item-thumbnail item-title item-price-stock'
'item-thumbnail item-can-add item-price-stock'
'item-thumbnail item-format item-price-stock'
'item-thumbnail item-info item-select'
'item-thumbnail item-date item-select'
'item-thumbnail item-ssc item-select'
'item-thumbnail item-availability item-select';
}
.item-thumbnail {
grid-area: item-thumbnail;
width: 70px;
@apply mr-8;
img {
max-width: 100%;
max-height: 150px;
@apply rounded-card shadow-cta;
}
}
.item-contributors {
@apply font-bold no-underline;
grid-area: item-contributors;
height: 22px;
text-overflow: ellipsis;
overflow: hidden;
max-width: 600px;
white-space: nowrap;
}
.item-title {
grid-area: item-title;
@apply font-bold text-lg mb-4;
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-format {
grid-area: item-format;
@apply flex flex-row items-center font-bold text-lg whitespace-nowrap;
img {
@apply mr-2;
}
}
.item-price-stock {
grid-area: item-price-stock;
@apply font-bold text-xl text-right;
.price {
@apply flex flex-row justify-end items-center;
}
.info-tooltip-button {
@apply border-font-customer border-solid border-2 bg-white rounded-full text-base font-bold mr-3;
border-style: outset;
width: 31px;
height: 31px;
margin-left: 10px;
}
.quantity-btn {
@apply flex flex-row items-center p-0 w-full text-right outline-none border-none bg-transparent text-lg;
}
.quantity-btn-icon {
@apply inline-flex ml-2;
transition: transform 200ms ease-in-out;
}
ui-quantity-dropdown {
@apply flex justify-end mt-2;
&.disabled {
@apply cursor-not-allowed bg-inactive-branch;
}
}
}
.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-info {
grid-area: item-info;
}
.item-availability {
@apply flex flex-row items-center mt-4 whitespace-nowrap text-sm;
grid-area: item-availability;
.fetching {
@apply w-52 h-px-20;
background-color: #e6eff9;
animation: load 0.75s linear infinite;
}
span {
@apply mr-4;
}
.instock {
@apply mr-2 font-bold;
}
ui-icon {
@apply text-dark-cerulean mx-2;
}
div {
@apply mr-4 flex items-center;
}
.truck {
@apply -mb-px-5 -mt-px-5;
}
.truck-b2b {
@apply -mb-px-10 -mt-px-10;
}
}
.item-can-add {
@apply text-xl text-dark-goldenrod font-semibold;
grid-area: item-can-add;
}
.item-select {
@apply flex items-center justify-end;
grid-area: item-select;
ui-select-bullet {
@apply cursor-pointer p-4 -m-4 z-dropdown;
&.disabled {
@apply cursor-not-allowed;
}
}
}
.hint {
@apply text-xl text-dark-goldenrod font-semibold;
}
@screen desktop {
.item-availability {
@apply text-base;
}
}

View File

@@ -1,250 +0,0 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { AvailabilityDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { combineLatest, Observable } from 'rxjs';
import { filter, map, shareReplay, withLatestFrom } from 'rxjs/operators';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-purchasing-options-list-item',
templateUrl: 'purchasing-options-list-item.component.html',
styleUrls: ['purchasing-options-list-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PurchasingOptionsListItemComponent {
@Input()
item: ShoppingCartItemDTO;
isSelected$ = this._store.selectedShoppingCartItems$.pipe(
map((selectedShoppingCartItems) => !!selectedShoppingCartItems?.find((item) => item.id === this.item.id))
);
fetchingAvailabilities$ = combineLatest([
this._store.takeAwayAvailabilities$,
this._store.pickUpAvailabilities$,
this._store.deliveryAvailabilities$,
this._store.deliveryDigAvailabilities$,
this._store.deliveryB2bAvailabilities$,
]).pipe(
map(
([takeAway, pickUp, delivery, digDelivery, b2bDelivery]) =>
!takeAway ||
takeAway[this.item.product.catalogProductNumber] === true ||
!pickUp ||
pickUp[this.item.product.catalogProductNumber] === true ||
!delivery ||
delivery[this.item.product.catalogProductNumber] === true ||
!digDelivery ||
digDelivery[this.item.product.catalogProductNumber] === true ||
!b2bDelivery ||
b2bDelivery[this.item.product.catalogProductNumber] === true
)
);
takeAwayAvailabilities$ = this._store.takeAwayAvailabilities$.pipe(
map((takeAwayAvailabilities) => {
if (takeAwayAvailabilities) {
const availability = takeAwayAvailabilities[this.item.product?.catalogProductNumber];
if (typeof availability === 'boolean') {
return undefined;
}
return availability;
}
return undefined;
}),
shareReplay()
);
pickUpAvailabilities$: Observable<AvailabilityDTO> = this._store.pickUpAvailabilities$.pipe(
map((pickUpAvailabilities) => {
if (pickUpAvailabilities) {
const availability = pickUpAvailabilities[this.item.product?.catalogProductNumber];
if (typeof availability === 'boolean') {
return undefined;
}
return availability;
}
return undefined;
}),
shareReplay()
);
deliveryAvailabilities$ = this._store.deliveryAvailabilities$.pipe(
map((shippingAvailabilities) => (!!shippingAvailabilities ? shippingAvailabilities[this.item.product?.catalogProductNumber] : [])),
shareReplay()
);
deliveryDigAvailabilities$: Observable<AvailabilityDTO> = this._store.deliveryDigAvailabilities$.pipe(
map((shippingAvailabilities) => {
if (shippingAvailabilities) {
const availability = shippingAvailabilities[this.item.product?.catalogProductNumber];
if (typeof availability === 'boolean') {
return undefined;
}
return availability;
}
return undefined;
}),
shareReplay()
);
deliveryB2bAvailabilities$ = this._store.deliveryB2bAvailabilities$.pipe(
map((shippingAvailabilities) => {
if (shippingAvailabilities) {
const availability = shippingAvailabilities[this.item.product?.catalogProductNumber];
if (typeof availability === 'boolean') {
return undefined;
}
return availability;
}
return undefined;
}),
shareReplay()
);
notAvailable$ = combineLatest([
this.fetchingAvailabilities$,
this.takeAwayAvailabilities$,
this.pickUpAvailabilities$,
this.deliveryAvailabilities$,
this.deliveryDigAvailabilities$,
this.deliveryB2bAvailabilities$,
]).pipe(
map(
([fetching, takeAway, store, delivery, deliveryDig, deliveryB2b]) =>
!fetching && !takeAway && !store && !delivery && !deliveryDig && !deliveryB2b
)
);
showTooltip$ = this._store.selectedFilterOption$.pipe(
withLatestFrom(this.deliveryAvailabilities$, this.deliveryDigAvailabilities$),
map(([option, delivery, deliveryDig]) => {
if (option === 'delivery') {
const deliveryAvailability = (deliveryDig as AvailabilityDTO) || (delivery as AvailabilityDTO);
const shippingPrice = deliveryAvailability?.price?.value?.value;
const catalogPrice = this.item?.availability?.price?.value?.value;
return catalogPrice < shippingPrice;
}
return false;
})
);
price$ = combineLatest([this.fetchingAvailabilities$, this._store.selectedFilterOption$]).pipe(
filter(([fetching]) => !fetching),
withLatestFrom(
this.takeAwayAvailabilities$,
this.pickUpAvailabilities$,
this.deliveryAvailabilities$,
this.deliveryDigAvailabilities$,
this.deliveryB2bAvailabilities$
),
map(([[_, option], takeAway, pickUp, delivery, deliveryDig, deliveryB2b]) => {
let availability;
switch (option) {
case 'take-away':
availability = takeAway;
break;
case 'pick-up':
availability = pickUp;
break;
case 'delivery':
if (deliveryDig || delivery) {
availability = deliveryDig || delivery;
} else {
availability = deliveryB2b;
option = 'b2b-delivery';
availability.p;
}
break;
default:
return this.item.availability?.price ?? this.item.unitPrice;
}
return this._availabilityService.getPriceForAvailability(option, this.item.availability, availability) ?? this.item.unitPrice;
})
);
selectDisabled$ = this._store.selectedFilterOption$.pipe(map((selectedFilterOption) => !selectedFilterOption));
selectVisible$ = combineLatest([this._store.canAdd$, this._store.selectedShoppingCartItems$]).pipe(
withLatestFrom(
this._store.selectedFilterOption$,
this._store.deliveryAvailabilities$,
this._store.deliveryDigAvailabilities$,
this._store.deliveryB2bAvailabilities$,
this._store.fetchingAvailabilities$
),
map(([[canAdd, items], option, delivery, deliveryDig, deliveryB2b, fetching]) => {
if (!option || fetching) {
return false;
}
// Select immer sichtbar bei ausgewählten Items
if (items?.find((item) => item.product?.catalogProductNumber === this.item.product?.catalogProductNumber)) {
return true;
}
// Select nur anzeigen, wenn ein anderes ausgewähltes Item die gleiche Verfügbarkeit hat (B2B Versand z.B.)
if (items?.length > 0 && option === 'delivery' && canAdd[this.item.product.catalogProductNumber]?.status < 2) {
if (items.every((item) => delivery[item.product?.catalogProductNumber]) && delivery[this.item.product?.catalogProductNumber]) {
return true;
}
if (
items.every((item) => deliveryDig[item.product?.catalogProductNumber]) &&
deliveryDig[this.item.product?.catalogProductNumber]
) {
return true;
}
if (
items.every((item) => deliveryB2b[item.product?.catalogProductNumber]) &&
deliveryB2b[this.item.product?.catalogProductNumber]
) {
return true;
}
return false;
}
return canAdd && canAdd[this.item.product.catalogProductNumber]?.status < 2;
})
);
canAdd$ = this._store.canAdd$.pipe(
filter((canAdd) => !!this.item && !!canAdd),
map((canAdd) => !!canAdd[this.item.product.catalogProductNumber]?.message)
);
quantityRange$ = combineLatest([this._store.selectedFilterOption$, this.takeAwayAvailabilities$]).pipe(
map(([option, availability]) => (option === 'take-away' ? (availability as AvailabilityDTO)?.inStock : 999))
);
constructor(private _store: PurchasingOptionsListModalStore, private _availabilityService: DomainAvailabilityService) {}
selected(value: boolean) {
this._store.selectShoppingCartItem([this.item], value);
}
changeQuantity(quantity: number) {
if (quantity === 0) {
this._store.removeShoppingCartItem(this.item);
} else {
this._store.updateItemQuantity({ itemId: this.item.id, quantity });
this._store.loadAvailabilities({ items: [{ ...this.item, quantity }] });
}
}
}

View File

@@ -1,49 +0,0 @@
<div class="options">
<page-take-away-option-list></page-take-away-option-list>
<page-pick-up-option-list></page-pick-up-option-list>
<page-delivery-option-list></page-delivery-option-list>
</div>
<div class="items" *ngIf="shoppingCartItems$ | async; let shoppingCartItems">
<div class="item-actions">
<ng-container>
<button
*ngIf="!(allShoppingCartItemsSelected$ | async); else unselectAll"
class="cta-select-all"
[disabled]="selectAllCtaDisabled$ | async"
(click)="selectAll(shoppingCartItems, true)"
>
Alle auswählen
</button>
<ng-template #unselectAll>
<button class="cta-select-all" [disabled]="selectAllCtaDisabled$ | async" (click)="selectAll(shoppingCartItems, false)">
Alle abwählen
</button>
</ng-template>
</ng-container>
<br />
{{ (selectedShoppingCartItems$ | async)?.length || 0 }} von {{ shoppingCartItems?.length || 0 }} Artikeln
</div>
<div class="item-list scroll-bar" *ngIf="shoppingCartItems?.length > 0; else emptyMessage">
<hr />
<ng-container *ngFor="let item of shoppingCartItems">
<page-purchasing-options-list-item [item]="item"></page-purchasing-options-list-item>
<hr />
</ng-container>
</div>
<ng-template #emptyMessage>
<div class="empty-message">Keine Artikel für die ausgewählte Kaufoption verfügbar</div>
</ng-template>
</div>
<div class="actions">
<button class="cta-apply" [disabled]="applyCtaDisabled$ | async" (click)="apply()">
<ui-spinner [show]="addItemsLoader$ | async">
Übernehmen
</ui-spinner>
</button>
</div>

View File

@@ -1,49 +0,0 @@
:host {
@apply block box-border;
}
.options {
@apply flex flex-row box-border text-center justify-center mt-4;
}
.items {
min-height: 440px;
.item-actions {
@apply text-right;
.cta-select-all {
@apply text-brand bg-transparent text-base font-bold outline-none border-none px-4 py-4 -mr-4;
&:disabled {
@apply text-inactive-branch;
}
}
}
.item-list {
@apply overflow-y-scroll overflow-x-hidden -ml-4;
max-height: calc(100vh - 580px);
width: calc(100% + 2rem);
page-purchasing-options-list-item {
@apply px-4;
}
}
.empty-message {
@apply text-inactive-branch my-8 text-center font-bold;
}
}
.actions {
@apply flex justify-center mt-8;
.cta-apply {
@apply text-white border-2 border-solid border-brand bg-brand font-bold text-lg px-4 py-2 rounded-full;
&:disabled {
@apply bg-inactive-branch border-inactive-branch;
}
}
}

View File

@@ -1,266 +0,0 @@
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import { ShoppingCartItemDTO, UpdateShoppingCartItemDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalRef, UiModalService } from '@ui/modal';
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
import { debounceTime, filter, first, map, shareReplay, takeUntil, withLatestFrom } from 'rxjs/operators';
import { PurchasingOptionsListModalData } from './purchasing-options-list-modal.data';
import { PurchasingOptionsListModalStore } from './purchasing-options-list-modal.store';
@Component({
selector: 'page-purchasing-options-list-modal',
templateUrl: 'purchasing-options-list-modal.component.html',
styleUrls: ['purchasing-options-list-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [PurchasingOptionsListModalStore],
})
export class PurchasingOptionsListModalComponent implements OnInit {
private _onDestroy$ = new Subject();
addItemsLoader$ = new BehaviorSubject<boolean>(false);
shoppingCartItems$ = combineLatest([
this._store.fetchingAvailabilities$,
this._store.selectedFilterOption$,
this._store.shoppingCartItems$,
]).pipe(
withLatestFrom(
this._store.takeAwayAvailabilities$,
this._store.pickUpAvailabilities$,
this._store.deliveryAvailabilities$,
this._store.deliveryDigAvailabilities$,
this._store.deliveryB2bAvailabilities$
),
map(
([
[_, selectedFilterOption, shoppingCartItems],
takeAwayAvailability,
pickUpAvailability,
deliveryAvailability,
deliveryDigAvailability,
deliveryB2bAvailability,
]) => {
if (!!takeAwayAvailability && !!pickUpAvailability && !!deliveryAvailability) {
switch (selectedFilterOption) {
case 'take-away':
return shoppingCartItems.filter((item) => !!takeAwayAvailability[item.product?.catalogProductNumber]);
case 'pick-up':
return shoppingCartItems.filter((item) => !!pickUpAvailability[item.product?.catalogProductNumber]);
case 'delivery':
return shoppingCartItems.filter(
(item) =>
!!deliveryAvailability[item.product?.catalogProductNumber] ||
!!deliveryDigAvailability[item.product?.catalogProductNumber] ||
!!deliveryB2bAvailability[item.product?.catalogProductNumber]
);
}
}
return shoppingCartItems;
}
),
map((shoppingCartItems) => shoppingCartItems?.sort((a, b) => a.product?.name.localeCompare(b.product?.name))),
shareReplay()
);
selectedShoppingCartItems$ = this._store.selectedShoppingCartItems$;
allShoppingCartItemsSelected$ = combineLatest([this.shoppingCartItems$, this.selectedShoppingCartItems$]).pipe(
map(
([shoppingCartItems, selectedShoppingCartItems]) =>
shoppingCartItems.every((item) => selectedShoppingCartItems.find((i) => item.id === i.id)) && shoppingCartItems?.length > 0
)
);
canAddItems$ = this._store.canAdd$.pipe(
map((canAdd) => {
for (const key in canAdd) {
if (Object.prototype.hasOwnProperty.call(canAdd, key)) {
if (!!canAdd[key]?.message) {
return false;
}
}
}
return true;
}),
shareReplay()
);
selectAllCtaDisabled$ = combineLatest([this._store.selectedFilterOption$, this.canAddItems$]).pipe(
withLatestFrom(this.shoppingCartItems$),
map(([[selectedFilterOption, canAddItems], items]) => !selectedFilterOption || items?.length === 0 || !canAddItems)
);
applyCtaDisabled$ = combineLatest([this.addItemsLoader$, this._store.selectedFilterOption$, this._store.selectedShoppingCartItems$]).pipe(
withLatestFrom(this.shoppingCartItems$),
map(
([[addItemsLoader, selectedFilterOption, selectedShoppingCartItems], shoppingCartItems]) =>
addItemsLoader || !selectedFilterOption || shoppingCartItems?.length === 0 || selectedShoppingCartItems?.length === 0
)
);
constructor(
private _modalRef: UiModalRef<any, PurchasingOptionsListModalData>,
private _modal: UiModalService,
private _store: PurchasingOptionsListModalStore,
private _availability: DomainAvailabilityService,
private _checkout: DomainCheckoutService
) {
this._store.shoppingCartItems = _modalRef.data.shoppingCartItems;
this._store.customerFeatures = _modalRef.data.customerFeatures;
this._store.processId = _modalRef.data.processId;
}
ngOnInit() {
this._store.loadBranches();
// Beim Wechsel der ausgewählten Filteroption oder der Branches die Auswahl leeren
combineLatest([this._store.selectedFilterOption$, this._store.selectedTakeAwayBranch$, this._store.selectedPickUpBranch$])
.pipe(takeUntil(this._onDestroy$))
.subscribe(() => this._store.clearSelectedShoppingCartItems());
this._store.selectedFilterOption$
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.shoppingCartItems$))
.subscribe(([option, items]) => this.checkCanAdd(option, items));
this._store.fetchingAvailabilities$
.pipe(
takeUntil(this._onDestroy$),
debounceTime(250),
filter((fetching) => !fetching),
withLatestFrom(this.shoppingCartItems$, this._store.selectedFilterOption$)
)
.subscribe(([_, items, option]) => this.checkCanAdd(option, items));
this.canAddItems$
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.shoppingCartItems$, this._store.selectedFilterOption$))
.subscribe(([showSelectAll, items, option]) => {
if (items?.length > 0 && this._store.lastSelectedFilterOption$.value !== option) {
this.selectAll(items, showSelectAll && !!option);
}
// Nach dem Übernehmen von Items wird eine neue CanAdd Abfrage ausgeführt, in diesem Fall soll aber nicht alles ausgewählt werden
this._store.lastSelectedFilterOption$.next(option);
});
}
checkCanAdd(selectedFilterOption: string, items: ShoppingCartItemDTO[]) {
if (!!selectedFilterOption && items?.length > 0) {
this._store.checkCanAddItems(items);
} else {
this._store.patchState({ canAdd: {} });
}
}
async selectAll(items: ShoppingCartItemDTO[], value: boolean) {
this._store.selectShoppingCartItem(items, value);
}
async apply() {
this.addItemsLoader$.next(true);
try {
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
const items = await this._store.selectedShoppingCartItems$.pipe(first()).toPromise();
const takeAwayAvailabilities = await this._store.takeAwayAvailabilities$.pipe(first()).toPromise();
const pickupAvailabilities = await this._store.pickUpAvailabilities$.pipe(first()).toPromise();
const deliveryAvailabilities = await this._store.deliveryAvailabilities$.pipe(first()).toPromise();
const deliveryB2bAvailabilities = await this._store.deliveryB2bAvailabilities$.pipe(first()).toPromise();
const deliveryDigAvailabilities = await this._store.deliveryDigAvailabilities$.pipe(first()).toPromise();
const selectedTakeAwayBranch = await this._store.selectedTakeAwayBranch$.pipe(first()).toPromise();
const selectedPickUpBranch = await this._store.selectedPickUpBranch$.pipe(first()).toPromise();
let option = this._store.selectedFilterOption;
for (const item of items) {
let availability;
switch (this._store.selectedFilterOption) {
case 'take-away':
availability = takeAwayAvailabilities[item.product.catalogProductNumber];
break;
case 'pick-up':
availability = pickupAvailabilities[item.product.catalogProductNumber];
break;
case 'delivery':
if (
deliveryDigAvailabilities[item.product.catalogProductNumber] &&
deliveryB2bAvailabilities[item.product.catalogProductNumber] &&
deliveryAvailabilities[item.product.catalogProductNumber]
) {
availability = deliveryAvailabilities[item.product.catalogProductNumber];
} else if (deliveryDigAvailabilities[item.product.catalogProductNumber]) {
availability = deliveryDigAvailabilities[item.product.catalogProductNumber];
} else if (deliveryB2bAvailabilities[item.product.catalogProductNumber]) {
availability = deliveryB2bAvailabilities[item.product.catalogProductNumber];
option = 'b2b-delivery';
}
break;
}
const price = this._availability.getPriceForAvailability(option, item.availability, availability);
// Negative Preise und nicht vorhandene Availability ignorieren
if (price?.value?.value < 0 || !availability) {
continue;
}
const updateItem: UpdateShoppingCartItemDTO = {
quantity: item.quantity,
availability: {
...availability,
price: price ? price : item.unitPrice,
},
promotion: item?.promotion?.points ? { points: item.promotion.points } : undefined,
};
switch (this._store.selectedFilterOption) {
case 'take-away':
updateItem.destination = {
data: { target: 1, targetBranch: { id: selectedTakeAwayBranch.id } },
};
break;
case 'pick-up':
updateItem.destination = {
data: { target: 1, targetBranch: { id: selectedPickUpBranch.id } },
};
break;
case 'delivery':
case 'dig-delivery':
case 'b2b-delivery':
updateItem.destination = {
data: { target: 2, logistician: availability?.logistician },
};
break;
}
await this._checkout
.updateItemInShoppingCart({
processId: this._modalRef.data.processId,
shoppingCartItemId: item.id,
update: {
...updateItem,
},
})
.toPromise();
}
const remainingItems = shoppingCartItems.filter((i) => !items.find((j) => i.id === j.id));
this._store.shoppingCartItems = [...remainingItems];
this._store.clearSelectedShoppingCartItems();
if (remainingItems?.length === 0) {
this._modalRef.close();
}
} catch (error) {
console.error(error);
this._modal.open({ content: UiErrorModalComponent, data: error, title: 'Fehler beim Hinzufügen zum Warenkorb' });
} finally {
this.addItemsLoader$.next(false);
}
const shoppingCartItems = await this.shoppingCartItems$.pipe(first()).toPromise();
if (shoppingCartItems?.length > 0) {
this._store.checkCanAddItems(shoppingCartItems);
}
}
}

View File

@@ -1,7 +0,0 @@
import { ShoppingCartItemDTO } from '@swagger/checkout';
export interface PurchasingOptionsListModalData {
processId: number;
shoppingCartItems?: ShoppingCartItemDTO[];
customerFeatures: { [key: string]: string };
}

View File

@@ -1,41 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PurchasingOptionsListModalComponent } from './purchasing-options-list-modal.component';
import { UiIconModule } from '@ui/icon';
import { ProductImageModule } from '@cdn/product-image';
import { UiCommonModule } from '@ui/common';
import { UiSelectBulletModule } from '@ui/select-bullet';
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
import { PickUpOptionListComponent } from './pick-up-option/pick-up-option-list.component';
import { TakeAwayOptionListComponent } from './take-away-option/take-away-option-list.component';
import { DeliveryOptionListComponent } from './delivery-option/delivery-option-list.component';
import { PurchasingOptionsListItemComponent } from './purchasing-options-list-item/purchasing-options-list-item.component';
import { FormsModule } from '@angular/forms';
import { UiBranchDropdownModule } from '@ui/branch-dropdown';
import { UiTooltipModule } from '@ui/tooltip';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
UiCommonModule,
UiIconModule,
UiSelectBulletModule,
UiQuantityDropdownModule,
ProductImageModule,
UiBranchDropdownModule,
UiTooltipModule,
UiSpinnerModule,
],
exports: [PurchasingOptionsListModalComponent],
declarations: [
PurchasingOptionsListModalComponent,
PurchasingOptionsListItemComponent,
PickUpOptionListComponent,
TakeAwayOptionListComponent,
DeliveryOptionListComponent,
],
})
export class PurchasingOptionsListModalModule {}

View File

@@ -1,598 +0,0 @@
import { Injectable } from '@angular/core';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { AvailabilityDTO, BranchDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { map, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators';
import { DomainAvailabilityService } from '@domain/availability';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { DomainCheckoutService } from '@domain/checkout';
import { ApplicationService } from '@core/application';
interface PurchasingOptionsListModalState {
processId: number;
shoppingCartItems: ShoppingCartItemDTO[];
selectedFilterOption: string;
takeAwayAvailabilities: { [key: string]: AvailabilityDTO | true };
pickUpAvailabilities: { [key: string]: AvailabilityDTO | true };
deliveryAvailabilities: { [key: string]: AvailabilityDTO | true };
deliveryB2bAvailabilities: { [key: string]: AvailabilityDTO | true };
deliveryDigAvailabilities: { [key: string]: AvailabilityDTO | true };
customerFeatures: { [key: string]: string };
canAdd: { [key: string]: { message: string; status: number } };
selectedShoppingCartItems: ShoppingCartItemDTO[];
branches: BranchDTO[];
currentBranch: BranchDTO;
selectedTakeAwayBranch: BranchDTO;
selectedPickUpBranch: BranchDTO;
}
@Injectable()
export class PurchasingOptionsListModalStore extends ComponentStore<PurchasingOptionsListModalState> {
lastSelectedFilterOption$ = new BehaviorSubject<string>(undefined);
branches$ = this.select((s) => s.branches);
currentBranch$ = this.select((s) => s.currentBranch);
takeAwayAvailabilities$ = this.select((s) => s.takeAwayAvailabilities);
pickUpAvailabilities$ = this.select((s) => s.pickUpAvailabilities);
deliveryAvailabilities$ = this.select((s) => s.deliveryAvailabilities);
deliveryB2bAvailabilities$ = this.select((s) => s.deliveryB2bAvailabilities);
canAdd$ = this.select((s) => s.canAdd);
deliveryDigAvailabilities$ = this.select((s) => s.deliveryDigAvailabilities);
shoppingCartItems$ = this.select((s) => s.shoppingCartItems);
set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) {
shoppingCartItems = shoppingCartItems.sort((a, b) => a.product?.name.localeCompare(b.product.name));
this.patchState({ shoppingCartItems });
}
processId$ = this.select((s) => s.processId);
set processId(processId: number) {
this.patchState({ processId });
}
customerFeatures$ = this.select((s) => s.customerFeatures);
set customerFeatures(customerFeatures: { [key: string]: string }) {
this.patchState({ customerFeatures });
}
selectedFilterOption$ = this.select((s) => s.selectedFilterOption);
set selectedFilterOption(selectedFilterOption: string) {
this.patchState({ selectedFilterOption });
}
get selectedFilterOption() {
return this.get((s) => s.selectedFilterOption);
}
selectedShoppingCartItems$ = this.select((s) => s.selectedShoppingCartItems);
get selectedShoppingCartItems() {
return this.get((s) => s.selectedShoppingCartItems);
}
selectedTakeAwayBranch$ = this.select((s) => s.selectedTakeAwayBranch);
set selectedTakeAwayBranch(selectedTakeAwayBranch: BranchDTO) {
this.patchState({ selectedTakeAwayBranch });
}
selectedPickUpBranch$ = this.select((s) => s.selectedPickUpBranch);
set selectedPickUpBranch(selectedPickUpBranch: BranchDTO) {
this.patchState({ selectedPickUpBranch });
}
fetchingAvailabilities$ = combineLatest([this.takeAwayAvailabilities$, this.pickUpAvailabilities$, this.deliveryAvailabilities$]).pipe(
map(([takeAway, pickUp, delivery]) => {
const fetchingCheck = (obj) => {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const element = obj[key];
if (typeof element === 'boolean') {
return true;
}
}
}
return false;
};
return !takeAway || fetchingCheck(takeAway) || !pickUp || fetchingCheck(pickUp) || !delivery || fetchingCheck(delivery);
})
);
constructor(
private _availabilityService: DomainAvailabilityService,
private _checkoutService: DomainCheckoutService,
private _application: ApplicationService
) {
super({
processId: undefined,
shoppingCartItems: [],
selectedFilterOption: undefined,
pickUpAvailabilities: undefined,
deliveryAvailabilities: undefined,
takeAwayAvailabilities: undefined,
deliveryB2bAvailabilities: undefined,
deliveryDigAvailabilities: undefined,
selectedShoppingCartItems: [],
branches: [],
currentBranch: undefined,
selectedTakeAwayBranch: undefined,
selectedPickUpBranch: undefined,
customerFeatures: undefined,
canAdd: undefined,
});
}
loadAvailabilities(options: { items?: ShoppingCartItemDTO[] }) {
const shoppingCartItems = options.items ?? this.get((s) => s.shoppingCartItems);
for (const item of shoppingCartItems) {
this.loadTakeAwayAvailability({ item });
this.loadPickUpAvailability({ item });
this.loadDeliveryAvailability({ item });
this.loadDeliveryB2bAvailability({ item });
this.loadDeliveryDigAvailability({ item });
}
}
readonly setAvailabilityFetching = this.updater((state, { name, id, fetching }: { name: string; id: string; fetching?: boolean }) => {
const availability = { ...state[name] };
if (fetching) {
availability[id] = fetching;
} else {
delete availability[id];
}
return {
...state,
[name]: {
...availability,
},
};
});
readonly setAvailability = this.updater((state, { name, availability }: { name: string; availability: any }) => {
const av = { ...state[name] };
if (this._availabilityService.isAvailable({ availability })) {
av[availability.itemId] = availability;
}
return {
...state,
[name]: av,
};
});
loadPickUpAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
withLatestFrom(this.selectedPickUpBranch$),
mergeMap(([options, branch]) => {
this.setAvailabilityFetching({
name: 'pickUpAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getPickUpAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
branch,
quantity: options.item.quantity,
})
.pipe(
map((av) => {
if (av?.length > 0) {
if (av[1].availableFor) {
if ((av[1].availableFor & 2) === 2) {
return av[0];
} else {
return undefined;
}
} else {
return av[0];
}
}
}),
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'pickUpAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'pickUpAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'pickUpAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'pickUpAvailabilities', availability: {} });
}
)
);
})
)
);
loadDeliveryAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
mergeMap((options) => {
this.setAvailabilityFetching({
name: 'deliveryAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getDeliveryAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
quantity: options.item.quantity,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'deliveryAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'deliveryAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'deliveryAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'deliveryAvailabilities', availability: {} });
}
)
);
})
)
);
loadDeliveryB2bAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
mergeMap((options) => {
this.setAvailabilityFetching({
name: 'deliveryB2bAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getB2bDeliveryAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
quantity: options.item.quantity,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'deliveryB2bAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'deliveryB2bAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'deliveryB2bAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'deliveryB2bAvailabilities', availability: {} });
}
)
);
})
)
);
loadDeliveryDigAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
mergeMap((options) => {
this.setAvailabilityFetching({
name: 'deliveryDigAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getDigDeliveryAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
quantity: options.item.quantity,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'deliveryDigAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'deliveryDigAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'deliveryDigAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'deliveryDigAvailabilities', availability: {} });
}
)
);
})
)
);
loadTakeAwayAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
withLatestFrom(this.selectedTakeAwayBranch$),
mergeMap(([options, branch]) => {
this.setAvailabilityFetching({
name: 'takeAwayAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getTakeAwayAvailabilityByBranch({
itemId: +options.item.product.catalogProductNumber,
price: options.item.availability.price,
quantity: options.item.quantity,
branch,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'takeAwayAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'takeAwayAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'takeAwayAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'takeAwayAvailabilities', availability: {} });
}
)
);
})
)
);
getCurrentBranch() {
return combineLatest([this._application.getSelectedBranch$(), this._availabilityService.getDefaultBranch()]).pipe(
map(([selectedBranch, defaultBranch]) => selectedBranch || defaultBranch)
);
}
loadBranches = this.effect(($) =>
$.pipe(
switchMap(() =>
this._availabilityService.getBranches().pipe(
map((branches) =>
branches.filter(
(branch) => branch.status === 1 && branch.branchType === 1 && branch.isOnline === true && branch.isShippingEnabled === true
)
),
withLatestFrom(this.getCurrentBranch()),
tapResponse(
([branches, currentBranch]) => {
this.patchState({
branches,
selectedTakeAwayBranch: currentBranch,
selectedPickUpBranch: currentBranch,
currentBranch,
});
this.loadAvailabilities({});
},
() =>
this.patchState({
branches: [],
selectedTakeAwayBranch: undefined,
selectedPickUpBranch: undefined,
currentBranch: undefined,
})
)
)
)
)
);
checkCanAddItems = this.effect((items$: Observable<ShoppingCartItemDTO[]>) =>
items$.pipe(
withLatestFrom(
this.processId$,
this.selectedFilterOption$,
this.takeAwayAvailabilities$,
this.pickUpAvailabilities$,
this.deliveryAvailabilities$,
this.deliveryB2bAvailabilities$,
this.deliveryDigAvailabilities$
),
mergeMap(([items, processId, selectedOption, takeAway, pickUp, delivery, deliveryB2b, deliveryDig]) => {
let orderType: string;
const payload = items.map((item) => {
switch (selectedOption) {
case 'take-away':
orderType = 'Rücklage';
return {
availabilities: [this.getOlaAvailability(takeAway[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
case 'pick-up':
orderType = 'Abholung';
return {
availabilities: [this.getOlaAvailability(pickUp[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
case 'delivery':
orderType = 'Versand';
if (
deliveryDig[item.product.catalogProductNumber] &&
deliveryB2b[item.product.catalogProductNumber] &&
delivery[item.product.catalogProductNumber]
) {
return {
availabilities: [this.getOlaAvailability(delivery[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
} else if (deliveryDig[item.product.catalogProductNumber]) {
return {
availabilities: [this.getOlaAvailability(deliveryDig[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
} else if (deliveryB2b[item.product.catalogProductNumber]) {
return {
availabilities: [this.getOlaAvailability(deliveryB2b[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
}
break;
}
});
return this._checkoutService.canAddItems({ processId, payload, orderType }).pipe(
tapResponse(
(result: any) => {
const canAdd = {};
result?.forEach((r) => {
canAdd[r.id] = { message: r.message, status: r.status };
});
this.patchState({ canAdd });
},
(error: Error) => {
const canAdd = {};
items?.forEach((i) => {
canAdd[i.product?.catalogProductNumber] = { message: error?.message };
});
this.patchState({ canAdd });
}
)
);
})
)
);
getOlaAvailability(availability: AvailabilityDTO, item: ShoppingCartItemDTO) {
return {
qty: item.quantity,
ean: item.product.ean,
itemId: item.product.catalogProductNumber,
format: item.product.format,
at: availability?.estimatedShippingDate,
isPrebooked: availability?.isPrebooked,
status: availability?.availabilityType,
logisticianId: availability?.logistician?.id,
price: availability?.price,
ssc: availability?.ssc,
sscText: availability?.sscText,
supplierId: availability?.supplier?.id,
};
}
readonly updateItemQuantity = this.updater((state, value: { itemId: number; quantity: number }) => {
const itemToUpdate = state.shoppingCartItems.find((item) => item.id === value.itemId);
const otherItems = state.shoppingCartItems.filter((item) => item.id !== value.itemId);
const updatedItem = { ...itemToUpdate, quantity: value.quantity };
const shoppingCartItems = [...otherItems, updatedItem].sort((a, b) => a.product?.name.localeCompare(b.product.name));
// Ausgewählte Items auch aktualisieren
let selectedShoppingCartItems = state.selectedShoppingCartItems;
if (state.selectedShoppingCartItems.find((item) => item.id === value.itemId)) {
const selectedItems = state.selectedShoppingCartItems.filter((item) => item.id !== value.itemId);
selectedShoppingCartItems = [...selectedItems, updatedItem].sort((a, b) => a.product?.name.localeCompare(b.product.name));
}
return {
...state,
shoppingCartItems,
selectedShoppingCartItems,
};
});
async removeShoppingCartItem(item: ShoppingCartItemDTO) {
const items = this.get((s) => s.shoppingCartItems);
const processId = this.get((s) => s.processId);
await this._checkoutService
.updateItemInShoppingCart({
processId,
shoppingCartItemId: item.id,
update: {
quantity: 0,
availability: null,
},
})
.toPromise();
this.selectShoppingCartItem([item], false);
const shoppingCartItems = items.filter((i) => i.id !== item.id);
this.patchState({ shoppingCartItems });
}
selectShoppingCartItem(shoppingCartItems: ShoppingCartItemDTO[], selected: boolean) {
if (selected) {
this.patchState({
selectedShoppingCartItems: [
...this.selectedShoppingCartItems.filter((item) => !shoppingCartItems.find((i) => item.id === i.id)),
...shoppingCartItems,
],
});
} else {
this.patchState({
selectedShoppingCartItems: this.selectedShoppingCartItems.filter((item) => !shoppingCartItems.find((i) => item.id === i.id)),
});
}
}
clearSelectedShoppingCartItems() {
this.patchState({ selectedShoppingCartItems: [] });
}
}

View File

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

View File

@@ -1,18 +0,0 @@
<div class="option-icon">
<ui-icon size="50px" icon="shopping_bag"></ui-icon>
</div>
<button
class="option-chip"
[disabled]="optionChipDisabled$ | async"
(click)="optionChange('take-away')"
[class.selected]="(selectedOption$ | async) === 'take-away'"
>
Rücklage
</button>
<p>Möchten Sie die Artikel<br />zurücklegen lassen oder<br />sofort mitnehmen?</p>
<ui-branch-dropdown
[branches]="branches$ | async"
[selected]="(selectedBranch$ | async)?.name"
(selectBranch)="selectBranch($event)"
></ui-branch-dropdown>

View File

@@ -1,36 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BranchDTO } from '@swagger/checkout';
import { first } from 'rxjs/operators';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-take-away-option-list',
templateUrl: 'take-away-option-list.component.html',
styleUrls: ['../list-options.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TakeAwayOptionListComponent {
branches$ = this._store.branches$;
selectedBranch$ = this._store.selectedTakeAwayBranch$;
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;
}
}
async selectBranch(branch: BranchDTO) {
this._store.lastSelectedFilterOption$.next(undefined);
this._store.selectedTakeAwayBranch = branch;
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
shoppingCartItems.forEach((item) => this._store.loadTakeAwayAvailability({ item }));
}
}

View File

@@ -1,23 +0,0 @@
<ng-container *ngIf="item$ | async; let item">
<ng-container *ngIf="availability$ | async; let availability">
<div class="option-icon">
<ui-icon size="80px" icon="truck_b2b"></ui-icon>
</div>
<h4>B2B Versand</h4>
<p>
Als B2B Kunde können wir Ihnen den Artikel auch liefern.
</p>
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<div class="grow"></div>
<span class="delivery">Versandkostenfrei</span>
<span class="date"
>Versanddatum <strong>{{ availability?.estimatedShippingDate | date: 'shortDate' }}</strong></span
>
<div>
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">
Auswählen
</button>
</div>
</ng-container>
</ng-container>

View File

@@ -1,9 +0,0 @@
.option-icon {
margin-top: -12px;
width: 70px;
}
h4 {
@apply font-bold;
margin-top: -2px;
}

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