Compare commits

...

164 Commits

Author SHA1 Message Date
Lorenz Hilpert
967df6316b Merge branch 'release/3.0' into feature/3968-Artikeldetails-Mehrwertsteuer 2023-08-31 14:11:06 +02:00
Lorenz Hilpert
b440ddbe82 Merge branch 'hotfix/4270-4269-Archiv-Artikel'
(cherry picked from commit f4df6e8799)
2023-08-30 14:38:54 +02:00
Nino
878bf44d0b #3968 Article Search Details display vat and if priceMaintained true 2023-08-28 16:51:52 +02:00
Lorenz Hilpert
d6e0d92132 (cherry picked from commit d09b5b1ce7) 2023-08-24 20:18:19 +02:00
Lorenz Hilpert
da6489eb7a Merge tag '4266-Archivartikel' into develop
Hotfix 4266 Archivartikel Preisauswahl
2023-08-24 20:06:36 +02:00
Lorenz Hilpert
819827cc4c Merge branch 'hotfix/4266-Archivartikel' 2023-08-24 20:04:40 +02:00
Lorenz Hilpert
d09b5b1ce7 #4266 Archivartikel nach Preis-Eingabe Button ausgegraut 2023-08-24 20:03:59 +02:00
Lorenz Hilpert
cc03ef4f9c #4264 Fix Ola Refresh Calls 2023-08-24 12:58:53 +02:00
Lorenz Hilpert
b4dbd8889d Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2023-08-24 12:12:50 +02:00
Lorenz Hilpert
483faad86a #4264 - Bestellen-Button ausgegraut 2023-08-24 11:57:47 +02:00
Michael Auer
0dbc745ed0 Merge tag '2.3' into develop
# Conflicts:
#	apps/isa-app/src/app/shell/shell.component.html
2023-08-24 11:56:11 +02:00
Michael Auer
180e93a7da Merge branch 'release/2.3' 2023-08-24 11:50:39 +02:00
Lorenz Hilpert
5c6f416391 #4263 Versand - Fehler vor Kundensuche 2023-08-23 14:56:57 +02:00
Lorenz Hilpert
d97b6afac8 #4205 Reihensuche 2023-08-23 14:31:55 +02:00
Lorenz Hilpert
771816f3af #4185 OLA Warenkorb - 500 Fix 2023-08-22 13:47:22 +02:00
Lorenz Hilpert
0626538aea #4261 Fehler bei Artikel aus Liste in Warenkorb - Weiter Button War Nicht Aktiv 2023-08-21 15:33:35 +02:00
Lorenz Hilpert
a1ad4e4a05 #4261 Fehler bei Artikel aus Liste in Warenkorb 2023-08-21 15:12:22 +02:00
Lorenz Hilpert
6df48eb555 #4255 Neue Filteroption - Erscheinungsdatum 2023-08-21 14:18:40 +02:00
Lorenz Hilpert
27ab4526e2 Logs entfernt und kleinere Änderungen rückgängig gemacht 2023-08-18 12:48:09 +02:00
Lorenz Hilpert
9a24b34fbc #4255 Verbesserung des Datumsinputs 2023-08-18 12:45:27 +02:00
Lorenz Hilpert
d01e01534b #4185 Bugfix - Bestellabschluss 2023-08-17 17:01:33 +02:00
Lorenz Hilpert
5bca1f2a81 Bugfix - zu viele aurufe bei ola 2023-08-16 15:26:08 +02:00
Lorenz Hilpert
807b300885 #4185 OLA im Warenkorb 2023-08-16 14:54:14 +02:00
Lorenz Hilpert
b16ffa4352 Merge branch 'develop' into release/3.0 2023-08-11 15:34:45 +02:00
Lorenz Hilpert
da79d04ef4 Bugfix Erscheinungsdatum 2023-08-11 15:34:09 +02:00
Lorenz Hilpert
cf619df576 Merge branch 'release/3.0' into develop 2023-08-11 10:29:47 +02:00
Lorenz Hilpert
8054c96315 #3376 Erscheinungstermin Filter Option Mit Input Box 2023-08-11 10:28:14 +02:00
Nino Righi
2363f424f5 Merged PR 1617: #3360 Show Branch Tooltip
#3360 Show Branch Tooltip
2023-08-11 08:11:16 +00:00
Lorenz Hilpert
6ab1ea2e70 #4254 Bestellungen ohne Konto werden nicht als Kunde erkannt 2023-08-10 14:03:18 +02:00
Lorenz Hilpert
c9ce7d6762 #4253 Kundensuche - Typo 2023-08-09 17:53:58 +02:00
Lorenz Hilpert
6ee1b0a7f8 #4250 Vorgänge zählen nicht hoch 2023-08-09 16:50:54 +02:00
Lorenz Hilpert
d881920312 Merge branch 'develop' into release/3.0 2023-08-07 07:07:36 +02:00
Lorenz Hilpert
516465db37 #4246 UI Searchbox Hint Erneut anzeigen
(cherry picked from commit 9671683a93)
2023-08-06 05:11:40 +02:00
Lorenz Hilpert
08e95cec55 #4245 Wannernummer-Prüfung - Leerzeichen entfernen
(cherry picked from commit 15c50779b4)
2023-08-06 05:10:46 +02:00
Lorenz Hilpert
9671683a93 #4246 UI Searchbox Hint Erneut anzeigen 2023-08-04 15:56:51 +02:00
Lorenz Hilpert
d909d6e804 #4236 Kulturpass - Artikel ohne Preisbindung erhalten günstigeren Preis
(cherry picked from commit 1d865c47d7)
2023-08-03 17:06:46 +02:00
Lorenz Hilpert
15c50779b4 #4245 Wannernummer-Prüfung - Leerzeichen entfernen 2023-08-03 17:05:45 +02:00
Lorenz Hilpert
1d865c47d7 #4236 Kulturpass - Artikel ohne Preisbindung erhalten günstigeren Preis 2023-08-03 13:57:09 +02:00
Lorenz Hilpert
5bdfec7c3f #4222 Packstückprüfung aktiviert 2023-08-02 10:55:54 +02:00
Nino Righi
6b0beba1d9 Merged PR 1616: #3378 SSC Sync PDP and Search Result List
#3378 SSC Sync PDP and Search Result List
2023-08-01 16:16:57 +00:00
Lorenz Hilpert
8aafee672e #4244 Kundenkarten-Ansicht wirf Fehler 2023-08-01 15:21:12 +02:00
Lorenz Hilpert
9d886cd33f Merge branch 'develop' into release/3.0 2023-07-31 18:31:30 +02:00
Nino Righi
95d1ea3530 Merged PR 1615: #4235 Fix Scroll Position Customer Orders
#4235 Fix Scroll Position Customer Orders
2023-07-31 14:19:53 +00:00
Lorenz Hilpert
d3014e7e9a #4243 kein Kundenkarten-Checkbox fuer HSC 2023-07-31 15:09:59 +02:00
Lorenz Hilpert
da143c3412 #4241 Seite Bestellungen von Kundendetails wird nach einige Sekunden zu Kunden Trefferliste navigiert 2023-07-31 14:36:51 +02:00
Nino Righi
b10a7a923e Merged PR 1613: #4240 Bestellbestätigung Fix Leere Kachel
#4240 Bestellbestätigung Fix Leere Kachel
2023-07-31 11:45:23 +00:00
Nino Righi
08601203df Merged PR 1614: #4238 Removed QueryChangeDebounce
#4238 Removed QueryChangeDebounce
2023-07-31 11:44:54 +00:00
Lorenz Hilpert
526ba9f2a0 Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2023-07-28 18:06:45 +02:00
Lorenz Hilpert
f1fc1d17a5 Message bei leerer suche 2023-07-28 18:06:15 +02:00
Nino Righi
6e07c86eed Merged PR 1612: #4228 Gruppierung Bestellbestätigung Abholung und Rücklage
#4228 Gruppierung Bestellbestätigung Abholung und Rücklage
2023-07-28 16:03:14 +00:00
Lorenz Hilpert
14199391e0 #4239 Bereich Kundenkarte hat kein Schließ-Button 2023-07-28 17:33:07 +02:00
Nino
bb834f6274 #4224 Fix Kundenbestellungen Trefferliste Splitscreen Breite der Kachel bzgl der Kundennummer angepasst 2023-07-28 16:43:22 +02:00
Nino Righi
8688935f25 Merged PR 1611: #4233 Fix branch$ should always atleast return defaultBranch if available
#4233 Fix branch$ should always atleast return defaultBranch if available
2023-07-28 14:22:14 +00:00
Lorenz Hilpert
1b7d257e97 #4172 Console.log entfernt 2023-07-28 15:49:37 +02:00
Lorenz Hilpert
fff4b222cb #4172 Keine Suchergebnisse text bei der Ergebnisliste 2023-07-28 15:48:55 +02:00
Lorenz Hilpert
c63dee8509 Merge branch 'release/2.3' into develop 2023-07-28 14:51:41 +02:00
Lorenz Hilpert
810653c4d1 #4194 Icons DR und PP hinzugefügt 2023-07-28 14:50:44 +02:00
Nino Righi
607bc320fb Merged PR 1609: #3395 Bestellbestätigung Verlinkungen und Wording
#3395 Bestellbestätigung Verlinkungen und Wording
2023-07-28 12:26:49 +00:00
Nino Righi
72393ebca5 Merged PR 1610: #4224 Fix Customer Orders Result List Card Size
#4224 Fix Customer Orders Result List Card Size
2023-07-28 12:26:20 +00:00
Lorenz Hilpert
ecef17846b #4226 #4234 Bei Einstieg in Kundensuche wird suche mit Default Filter getriggert 2023-07-28 14:10:00 +02:00
Lorenz Hilpert
45265eedd4 #4230 Keine Kachel in Trefferliste wenn ein Bestellung ohne Konto-Datensatz angelegt wird 2023-07-28 13:34:32 +02:00
Nino Righi
04da34e677 Merged PR 1608: #4233 Fix Article Search Details Fetching Branch correctly
#4233 Fix Article Search Details Fetching Branch correctly
2023-07-27 16:29:43 +00:00
Lorenz Hilpert
1e504d9e0c Merge branch 'release/3.0' into develop 2023-07-27 17:51:51 +02:00
Lorenz Hilpert
75528d37d3 #4232 Preisanzeige bei Versanbestellung
(cherry picked from commit eddff0d93f)
2023-07-27 17:51:29 +02:00
Lorenz Hilpert
eddff0d93f #4232 Preisanzeige bei Versanbestellung 2023-07-27 17:50:09 +02:00
Lorenz Hilpert
f4579ef8dc #4229 Styling und Filter Anpoassung für Kundenbereich 2023-07-27 14:01:06 +02:00
Lorenz Hilpert
fc5496fda6 #4229 Fix ScrollPosition 2023-07-27 13:32:27 +02:00
Lorenz Hilpert
5cf6f4da38 Merged PR 1607: #4148 Suche nach Versandbestellung mit Order Id
#4148 Suche nach Versandbestellung mit Order Id
2023-07-26 15:32:18 +00:00
Lorenz Hilpert
60fde8b103 #4194 Artikel Format Icon wird falsch angezeigt 2023-07-26 17:20:32 +02:00
Lorenz Hilpert
734fe33f40 #4226 Am Ende der Trefferliste fehlt den Link zum Kundendaten erfassen 2023-07-26 16:54:25 +02:00
Nino Righi
64da928c36 Merged PR 1606: #4172 Kundensuche "Keine Suchergebnisse"
#4172 Kundensuche "Keine Suchergebnisse"
2023-07-26 14:29:41 +00:00
Nino
dba0b1b3c7 Customer Orders Breadcrumb Fix, Styling Fix Result List, Adjusted Position of Address Info correctly 2023-07-26 15:53:01 +02:00
Nino Righi
073746a9bc Merged PR 1605: #4220 Responsive Design Customer Orders Search History
#4220 Responsive Design Customer Orders Search History
2023-07-26 11:58:00 +00:00
Lorenz Hilpert
45c14e3b79 Toaster ueber console erstellen und schließen 2023-07-25 16:49:22 +02:00
Nino Righi
7023fe747b Merged PR 1604: #4219 RD Customer Orders Result List Display OrderNumber at item and BuyerNum...
#4219 RD Customer Orders Result List Display OrderNumber at item and BuyerNumber at customer name
2023-07-25 12:58:24 +00:00
Lorenz Hilpert
141c7fe1d6 #4221 Scanner - Adapter sind nicht bereit
(cherry picked from commit 78e76818b5)
(cherry picked from commit 44b406fad4)
2023-07-25 14:57:40 +02:00
Lorenz Hilpert
44b406fad4 #4221 Scanner - Adapter sind nicht bereit
(cherry picked from commit 78e76818b5)
2023-07-25 14:57:09 +02:00
Lorenz Hilpert
78e76818b5 #4221 Scanner - Adapter sind nicht bereit 2023-07-25 14:52:18 +02:00
Lorenz Hilpert
f78a773fab Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2023-07-25 14:19:07 +02:00
Nino Righi
60d007d9eb Merged PR 1603: #3394 #3395 #4218 Responsive Design Checkout Summary
#3394 #3395 #4218 Responsive Design Checkout Summary
2023-07-25 12:18:35 +00:00
Lorenz Hilpert
1ec253333e Close Button - Text gege Icon getsuscht 2023-07-25 14:18:18 +02:00
Nino Righi
bd332b6bd9 Merged PR 1602: #4215 #4137 Alle Filter Entfernen in Kundensuche eingebaut, Alle Filter entfernen entfernt die Query nicht mehr
#4215 #4137 Alle Filter Entfernen in Kundensuche eingebaut, Alle Filter entfernen entfernt die Query nicht mehr
2023-07-24 16:20:20 +00:00
Lorenz Hilpert
bc9bdbebe3 #3860 Toaster - Platzierung 2023-07-24 18:18:49 +02:00
Lorenz Hilpert
0b471cc5bc Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2023-07-24 11:26:55 +02:00
Lorenz Hilpert
c2eed61b83 #4213 Trefferlsite leer - scroll index fix 2023-07-24 11:26:41 +02:00
Lorenz Hilpert
2d403b4c56 Merge branch 'release/3.0' into develop 2023-07-24 01:13:27 +02:00
Lorenz Hilpert
b3e4ca90ee Merge branch 'release/2.3' into develop 2023-07-24 01:12:38 +02:00
Nino Righi
d4a3a4bc06 Merged PR 1601: #4141 Disable Filter Anwenden CTA if no filter is selected or no query available
#4141 Disable Filter Anwenden CTA if no filter is selected or no query available
2023-07-21 13:59:01 +00:00
Lorenz Hilpert
b674b5faf6 #4216 Header - Schriftgröße über Header anpassen 2023-07-21 15:40:45 +02:00
Lorenz Hilpert
8416028113 (cherry picked from commit edb21308d4) 2023-07-21 10:52:21 +02:00
Lorenz Hilpert
edb21308d4 #4194 Remission - Artikel Format Icon wird falsch angezeigt 2023-07-21 10:50:57 +02:00
Lorenz Hilpert
26ad0153d8 #4214 Trefferliste Kunden IPad 2023-07-21 10:26:42 +02:00
Nino Righi
e9b2acca5b Merged PR 1600: #4172 #4200 Artikelsuche und Kundenbestellungen Navigation Trefferliste und Anzeige Suchbox Hint
#4172 #4200 Artikelsuche und Kundenbestellungen Navigation Trefferliste und Anzeige Suchbox Hint
2023-07-21 08:15:57 +00:00
Nino Righi
4180ee61d6 Merged PR 1598: #3922 Kundenbestellungen Bugfixes, Filter angepasst, Breadcrumb bugfix
#3922 Kundenbestellungen Bugfixes, Filter angepasst, Breadcrumb bugfix
2023-07-21 07:40:03 +00:00
Lorenz Hilpert
a442a50708 Update Test 2023-07-20 15:59:52 +02:00
Lorenz Hilpert
59e650a1f1 #4201 Bearbeitung von Bestellung ohne Konto deaktiviert 2023-07-20 14:21:14 +02:00
Lorenz Hilpert
69b6470cda Revert "#4209 - FIX - KulturPass-Einlösecode lässt Abholfrist ändern"
This reverts commit 02d60e9bd5.
2023-07-20 13:57:31 +02:00
Lorenz Hilpert
44b33c1e4c Revert "#4209 - FIX - KulturPass-Einlösecode lässt Abholfrist ändern"
This reverts commit 02d60e9bd5.
2023-07-20 13:56:08 +02:00
Lorenz Hilpert
08f00c6578 Update CheckForUpdate interval - 15 min 2023-07-20 11:49:44 +02:00
Lorenz Hilpert
51c4066222 (cherry picked from commit 6fb72e4b2f) 2023-07-20 11:46:13 +02:00
Lorenz Hilpert
6fb72e4b2f #4121 Vormerker kann nicht manuell bearbeitet werden 2023-07-20 11:42:41 +02:00
Lorenz Hilpert
c9fbbd78a8 #4213 FIX - Kundensuche Trefferliste verschwindet nach Tab wechseln 2023-07-20 10:07:22 +02:00
Nino Righi
ce5388be61 Merged PR 1599: #4206 Checkout Cart display notification channels if buyer or payer is available
#4206 Checkout Cart display notification channels if buyer or payer is available
2023-07-19 16:59:52 +00:00
Lorenz Hilpert
02d60e9bd5 #4209 - FIX - KulturPass-Einlösecode lässt Abholfrist ändern
(cherry picked from commit fce50daff6)
2023-07-19 18:42:27 +02:00
Lorenz Hilpert
fce50daff6 #4209 - FIX - KulturPass-Einlösecode lässt Abholfrist ändern 2023-07-19 18:41:06 +02:00
Lorenz Hilpert
35b7f5700f #4211 Kaufoptionen popup - Prüfung ob Artikel mit Kunden kombinierbar ist
(cherry picked from commit ddd5d50c5d)
2023-07-19 17:46:52 +02:00
Lorenz Hilpert
b954947bb7 Merge branch 'release/2.3' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into release/2.3 2023-07-19 17:45:36 +02:00
Lorenz Hilpert
ddd5d50c5d #4211 Kaufoptionen popup - Prüfung ob Artikel mit Kunden kombinierbar ist 2023-07-19 17:45:17 +02:00
Lorenz Hilpert
efec7ecb26 #4210 Änderungen werden nicht sofort angezeigt 2023-07-19 17:25:22 +02:00
Lorenz Hilpert
ee81f795fe Removed Deprecated unit tests 2023-07-19 15:14:54 +02:00
Lorenz Hilpert
ad557ff919 #4193 Bestellpostensuche Seite nicht erreichbar 2023-07-19 15:06:00 +02:00
Lorenz Hilpert
bf5fae08b2 #4203 Scrollposition auf Kundentrefferliste wird nicht gespeichert 2023-07-19 14:36:18 +02:00
Lorenz Hilpert
88321928bf #4201 Bestellung ohne Konto können Kundendaten nicht bearbeiten 2023-07-19 11:33:13 +02:00
Lorenz Hilpert
a7d50a9439 #4196 Rechnungs- und Lieferadresse ändern bei eine Bestellung für Versand führt zu Dashboard Seite 2023-07-18 16:35:27 +02:00
Lorenz Hilpert
4c6dcd15da #4198 In Kundenkarten Konto Formular der Text geht über den Rand hinaus 2023-07-18 16:27:42 +02:00
Lorenz Hilpert
84f9d14be0 #4199 Warnmeldung bei Geburtsdatumeingabe 2023-07-18 15:25:05 +02:00
Lorenz Hilpert
e2f173f250 #4201 Kunden Detail Seite des Onlinekontos - nicht bearbeitbar 2023-07-18 14:33:22 +02:00
Lorenz Hilpert
fdd9617604 #4202 Kundendetails Bearbeiten Seite von Business Konto 2023-07-18 14:25:49 +02:00
Lorenz Hilpert
bc3cedfe50 #4204 Ladekreis in Kundensuche Suchfeld verschwindet nicht 2023-07-18 14:09:04 +02:00
Lorenz Hilpert
78f9093931 Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2023-07-18 13:22:59 +02:00
Lorenz Hilpert
a889c768d1 #3761 Bestellposten Design 2023-07-18 13:22:22 +02:00
Nino Righi
e36319a73e Merged PR 1596: #4179 Fix Process Bar Scroll to Active Process
#4179 Fix Process Bar Scroll to Active Process
2023-07-18 11:14:59 +00:00
Nino Righi
199ae95bcd Merged PR 1597: #4186 Fix Price Update and Article Search Result List selected Select Bullet...
#4186 Fix Price Update and Article Search Result List selected Select Bullet handling improved
2023-07-18 08:22:05 +00:00
Lorenz Hilpert
30875f0491 #4195 Anzeige der Hauptseite der Kundensuche angepasst - IPad 2023-07-17 15:16:44 +02:00
Lorenz Hilpert
95d9d17aa7 Merge branch 'release/2.3' into develop 2023-07-17 01:31:46 +02:00
Lorenz Hilpert
215d7ca341 Benachrichtigung für Packstückprüfung einblenden 2023-07-17 01:31:17 +02:00
Lorenz Hilpert
1255df10e0 Benachrichtigungen für Packstückprüfung ausblenden 2023-07-17 01:30:40 +02:00
Nino Righi
4c641adeda Merged PR 1595: #4189 Dont Display Customer Name if Shipping Address is Available
#4189 Dont Display Customer Name if Shipping Address is Available
2023-07-14 15:17:51 +00:00
Lorenz Hilpert
4bd4158dab select inputs können gecleart werden 2023-07-14 17:14:44 +02:00
Nino
4965976f6c Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2023-07-14 16:59:36 +02:00
Nino
b43f512887 Responsive Design Container Height, Splitscreen Tailwind changes to Page Catalog, Customer-Orders, Checkout - Small Styling bugfixes 2023-07-14 16:59:15 +02:00
Lorenz Hilpert
e6f2b46fce Navigation nach Kundenanlage 2023-07-14 16:30:18 +02:00
Lorenz Hilpert
01c84b361a Kundenbereich Div Size 2023-07-14 15:47:19 +02:00
Lorenz Hilpert
ac35cc237e Leere Notifications nicht anzeigen 2023-07-14 14:50:08 +02:00
Lorenz Hilpert
28ad07b372 Filter Searchbox - Tablet - Desktop 2023-07-14 14:42:39 +02:00
Lorenz Hilpert
6ade77d458 Tailwindcss anpassung splitscreen werte 2023-07-14 14:41:18 +02:00
Lorenz Hilpert
4187e13861 Merge branch 'release/2.3' into develop 2023-07-14 13:25:05 +02:00
Lorenz Hilpert
4fe5034e1c #3666 Design Anpassung 2023-07-14 13:12:48 +02:00
Lorenz Hilpert
729451fa48 Kundennavigation angepasst 2023-07-14 12:19:03 +02:00
Lorenz Hilpert
d77fe8c540 Kundenbereich Nachname Vorname 2023-07-14 12:05:21 +02:00
Lorenz Hilpert
a257ddd8e0 Dev Scanner Entfernt 2023-07-14 11:14:19 +02:00
Lorenz Hilpert
90154bd497 desktop_ zu desktop-large_ geändert, searchprovider für kundensuche angepasst 2023-07-14 10:54:57 +02:00
Lorenz Hilpert
f96224569f Autocomplete Provider für Kundensuche 2023-07-14 03:51:31 +02:00
Lorenz Hilpert
1c695104f9 click event auf Weiter zum Warenkorb 2023-07-14 03:40:47 +02:00
Lorenz Hilpert
ab9a35dd89 Merge branch 'feature/rd-customer' into develop 2023-07-14 03:27:46 +02:00
Lorenz Hilpert
2fd2d701dd Merge branch 'feature/rd-customer' into develop 2023-07-13 19:09:53 +02:00
Lorenz Hilpert
5574252b5b Merge branch 'develop' into release/3.0 2023-07-13 19:09:05 +02:00
Lorenz Hilpert
5e79d4dc52 Merge branch 'release/3.0' into develop 2023-07-13 19:08:47 +02:00
Nino Righi
bb91782079 Merged PR 1594: #4176 Fix Order Deadline inside Purchasing Options
#4176 Fix Order Deadline inside Purchasing Options
2023-07-13 17:07:20 +00:00
Nino Righi
2bbcb15740 Merged PR 1593: #4189 Warenkorb Anzeige Änderungen
#4189 Warenkorb Anzeige Änderungen
2023-07-13 17:06:59 +00:00
Nino Righi
e18b9a4200 Merged PR 1592: #3730 Article Search Results Order By Adjustments
#3730 Article Search Results Order By Adjustments
2023-07-13 15:23:42 +00:00
Nino Righi
310395d166 Merged PR 1591: #4184 Fix Article Search Results Remember Scroll Position Correctly
#4184 Fix Article Search Results Remember Scroll Position Correctly
2023-07-13 15:23:24 +00:00
Nino Righi
651f44914f Merged PR 1590: #4187 RD Cart Delivery Address and Wording Change
#4187 RD Cart Delivery Address and Wording Change
2023-07-13 14:55:41 +00:00
Lorenz Hilpert
f2e124903c Merge branch 'release/2.3' into develop 2023-07-13 12:08:38 +02:00
Nino Righi
eec1cb5666 Merged PR 1589: #4180 Hotfix Mehrfachauswahl Für Download Artikel direkt Logistician setzen
#4180 Hotfix Mehrfachauswahl Für Download Artikel direkt Logistician setzen
2023-07-13 08:05:16 +00:00
Nino Righi
cb7334d63b Merged PR 1588: #4140 Fix Article Search Results Set Default Max Buffer Size for Scrollcontainer
#4140 Fix Article Search Results Set Default Max Buffer Size for Scrollcontainer
2023-07-13 08:03:59 +00:00
Lorenz Hilpert
b62e6e8e35 #3666 Benachrichtigung über die Glocke 2023-07-12 17:57:31 +02:00
Nino Righi
b845147050 Merged PR 1584: #4180 Hotfix Add Logistician to Download Destination
#4180 Hotfix Add Logistician to Download Destination
2023-07-11 11:45:10 +00:00
Michael Auer
6bc265a358 Merge branch 'release/2.3' 2023-07-11 12:20:14 +02:00
Lorenz Hilpert
ca5dbb9d6f Merge branch 'develop' into release/3.0 2023-07-06 16:58:04 +02:00
Lorenz Hilpert
27e5afacde Merge branch 'develop' into release/3.0 2023-07-06 16:33:14 +02:00
Lorenz Hilpert
ba01807add Merge branch 'develop' into release/3.0 2023-07-05 22:06:55 +02:00
Lorenz Hilpert
bb510788eb Merge branch 'develop' into release/3.0 2023-07-05 17:44:32 +02:00
Lorenz Hilpert
600687f652 Version Set To Major 3 Minor 0 2023-06-27 11:01:12 +02:00
349 changed files with 5229 additions and 3671 deletions

View File

@@ -11,9 +11,10 @@ export class DevScanAdapter implements ScanAdapter {
constructor(private _modal: UiModalService, private _environmentService: EnvironmentService) {}
async init(): Promise<boolean> {
return new Promise((resolve, reject) => {
resolve(isDevMode());
});
return Promise.resolve(false);
// return new Promise((resolve, reject) => {
// resolve(isDevMode());
// });
}
scan(): Observable<string> {

View File

@@ -14,7 +14,6 @@ export class ScanAdapterService {
async init(): Promise<void> {
for (const adapter of this.scanAdapters) {
const isReady = await adapter.init();
console.log('ScanAdapterService.init', adapter.name, isReady);
this._readyAdapters[adapter.name] = isReady;
}
}
@@ -24,52 +23,30 @@ export class ScanAdapterService {
}
getAdapter(name: string): ScanAdapter | undefined {
return this.scanAdapters.find((adapter) => adapter.name === name);
return this._readyAdapters[name] && this.scanAdapters.find((adapter) => adapter.name === name);
}
// return true if at least one adapter is ready
isReady(): boolean {
return Object.values(this._readyAdapters).some((ready) => ready);
}
scan(ops: { use?: string; include?: string[]; exclude?: string[] } = {}): Observable<string> {
scan(): Observable<string> {
const adapterOrder = ['Native', 'Scandit', 'Dev'];
let adapter: ScanAdapter;
if (ops.use == undefined) {
const adapterOrder = ['Native', 'Scandit', 'Dev'];
for (const name of adapterOrder) {
adapter = this.getAdapter(name);
for (const name of adapterOrder) {
adapter = this.getAdapter(name);
if (adapter) {
break;
}
if (adapter) {
break;
}
// get the first adapter that is ready to use
// adapter = this.scanAdapters
// .filter((adapter) => {
// if (ops.include?.length) {
// return ops.include.includes(adapter.name);
// } else if (ops.exclude?.length) {
// return !ops.exclude.includes(adapter.name);
// } else {
// return true;
// }
// })
// .find((adapter) => this._readyAdapters[adapter.name]);
} else {
adapter = this.getAdapter(ops.use);
}
if (!adapter) {
return throwError('No adapter found');
}
if (this._readyAdapters[adapter.name] == false) {
return throwError('Adapter is not ready');
}
return adapter.scan();
}
}

View File

@@ -2,13 +2,19 @@ import { Injectable } from '@angular/core';
import { Platform } from '@angular/cdk/platform';
import { NativeContainerService } from 'native-container';
import { BreakpointObserver } from '@angular/cdk/layout';
import { shareReplay } from 'rxjs/operators';
import { map } from 'rxjs/operators';
const MATCH_TABLET = '(max-width: 1024px)';
const MATCH_TABLET = '(max-width: 1023px)';
const MATCH_DESKTOP_SMALL = '(min-width: 1025px) and (max-width: 1439px)';
const MATCH_DESKTOP_SMALL = '(min-width: 1024px) and (max-width: 1439px)';
const MATCH_DESKTOP = '(min-width: 1440px)';
const MATCH_DESKTOP = '(min-width: 1280px)';
const MATCH_DESKTOP_LARGE = '(min-width: 1440px)';
const MATCH_DESKTOP_XLARGE = '(min-width: 1920px)';
const MATCH_DESKTOP_XXLARGE = '(min-width: 2736px)';
@Injectable({
providedIn: 'root',
@@ -24,19 +30,37 @@ export class EnvironmentService {
return this._breakpointObserver.isMatched(MATCH_TABLET);
}
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET).pipe(shareReplay());
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET).pipe(map((result) => result.matches));
matchDesktopSmall(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_SMALL);
}
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL).pipe(shareReplay());
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL).pipe(map((result) => result.matches));
matchDesktop(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP);
}
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP).pipe(shareReplay());
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP).pipe(map((result) => result.matches));
matchDesktopLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_LARGE);
}
matchDesktopLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_LARGE).pipe(map((result) => result.matches));
matchDesktopXLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_XLARGE);
}
matchDesktopXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_XLARGE).pipe(map((result) => result.matches));
matchDesktopXXLarge(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_XXLARGE);
}
matchDesktopXXLarge$ = this._breakpointObserver.observe(MATCH_DESKTOP_XXLARGE).pipe(map((result) => result.matches));
/**
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './toast-animation';
// end:ng42.barrel

View File

@@ -1,14 +0,0 @@
import { AnimationTriggerMetadata, trigger, state, transition, style, animate } from '@angular/animations';
export const slideAnimationTime = 150;
export const toastAnimations: {
readonly slideToast: AnimationTriggerMetadata;
} = {
slideToast: trigger('slideAnimation', [
state('default', style({ transform: 'translateY(0%)' })),
transition('void => *', [style({ transform: 'translateY(-100%)' }), animate(`${slideAnimationTime}ms ease-in`)]),
transition('default => closing', animate(`${slideAnimationTime}ms ease-in`, style({ transform: 'translateY(-100%)' }))),
]),
};
export type ToastAnimationState = 'default' | 'closing';

View File

@@ -1,4 +0,0 @@
// start:ng42.barrel
export * from './toast';
export * from './toast-ref';
// end:ng42.barrel

View File

@@ -1,17 +0,0 @@
import { OverlayRef } from '@angular/cdk/overlay';
export class ToastRef {
constructor(private readonly _overlay: OverlayRef) {}
close() {
this._overlay.dispose();
}
isVisible() {
return this._overlay && this._overlay.overlayElement;
}
getPosition() {
return this._overlay.overlayElement.getBoundingClientRect();
}
}

View File

@@ -1,11 +0,0 @@
import { TemplateRef } from '@angular/core';
export interface Toast {
title?: string;
text?: string;
timer?: number;
position?: 'top-left' | 'top' | 'top-right' | 'bottom-right' | 'bottom' | 'bottom-left';
size?: 'width-full' | 'content';
template?: TemplateRef<any>; // For rendering dynamic content
templateContext?: {}; // For rendering dynamic content
}

View File

@@ -1,8 +0,0 @@
// start:ng42.barrel
export * from './toast.component';
export * from './toast.module';
export * from './toast.service';
export * from './defs';
export * from './animation';
export * from './tokens';
// end:ng42.barrel

View File

@@ -1,15 +0,0 @@
<div class="toast-main" [style.width]="width" [@slideAnimation]="{ value: animationState }" (@slideAnimation.done)="onSlideFinished()">
<button class="absolute top-2 right-2 p-6 border-none bg-transparent" (click)="close()">
<ui-icon icon="close" size="20px"></ui-icon>
</button>
<div class="toast-content flex flex-col justify-center items-center">
<h1 class="text-xl font-bold text-center py-3 whitespace-pre-wrap">{{ data.title }}</h1>
<ng-container *ngIf="data.text; else templateRef">
<p class="block text-p2 overflow-y-hidden pb-3 text-center overflow-x-hidden">{{ data.text }}</p>
</ng-container>
</div>
</div>
<ng-template #templateRef>
<ng-container *ngTemplateOutlet="data.template; context: data.templateContext"> </ng-container>
</ng-template>

View File

@@ -1,12 +0,0 @@
.toast-main {
@apply block relative mx-auto box-border text-white p-4;
background-color: var(--toast-background);
min-width: 18.75rem;
max-width: calc(100vw - 2rem);
min-height: 5rem;
border-radius: 25px;
}
.toast-content {
min-height: 3rem;
}

View File

@@ -1,48 +0,0 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { toastAnimations, ToastAnimationState, slideAnimationTime } from './animation';
import { Toast, ToastRef } from './defs';
import { TOAST_CONFIG_TOKEN } from './tokens';
@Component({
selector: 'lib-toast',
templateUrl: 'toast.component.html',
styleUrls: ['toast.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [toastAnimations.slideToast],
})
export class ToastComponent implements OnInit, OnDestroy {
timeoutRef?: any;
animationState: ToastAnimationState = 'default';
width = '55.25rem';
constructor(
@Inject(TOAST_CONFIG_TOKEN) public readonly data: Toast,
private readonly _ref: ToastRef,
private readonly _cdr: ChangeDetectorRef
) {}
ngOnInit(): void {
if (this.data?.size) {
this.width = this.data?.size === 'width-full' ? '100vw' : '55.25rem';
}
this.timeoutRef = setTimeout(() => {
this.close();
this._cdr.markForCheck();
}, slideAnimationTime + (this.data.timer ?? 5000));
}
ngOnDestroy() {
clearTimeout(this.timeoutRef);
}
close() {
this.animationState = 'closing';
}
onSlideFinished() {
if (this.animationState === 'closing') {
this._ref.close();
}
}
}

View File

@@ -1,12 +0,0 @@
import { OverlayModule } from '@angular/cdk/overlay';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UiIconModule } from '@ui/icon';
import { ToastComponent } from './toast.component';
@NgModule({
declarations: [ToastComponent],
imports: [CommonModule, OverlayModule, UiIconModule],
exports: [ToastComponent],
})
export class ToastModule {}

View File

@@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ToastService } from './toast.service';
describe('ToastService', () => {
let service: ToastService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ToastService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,79 +0,0 @@
import { Overlay } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { Injectable, Injector } from '@angular/core';
import { Toast, ToastRef } from './defs';
import { ToastComponent } from './toast.component';
import { TOAST_CONFIG_TOKEN } from './tokens';
@Injectable({
providedIn: 'root',
})
export class ToastService {
private _lastToastRef: ToastRef;
get lastToastRef() {
return this._lastToastRef;
}
set lastToastRef(toastRef: ToastRef) {
this._lastToastRef = toastRef;
}
constructor(private readonly _overlay: Overlay, private readonly _injector: Injector) {}
create(data: Toast) {
const positionStrategy = this.getPositionStrategy(data);
const overlayRef = this._overlay.create({ positionStrategy });
this.lastToastRef = new ToastRef(overlayRef);
const injector = this.getInjector(data, this.lastToastRef);
const toastPortal = new ComponentPortal(ToastComponent, null, injector);
overlayRef.attach(toastPortal);
return this.lastToastRef;
}
getInjector(data: Toast, ref: ToastRef) {
return Injector.create({
parent: this._injector,
providers: [
{ provide: TOAST_CONFIG_TOKEN, useValue: data },
{ provide: ToastRef, useValue: ref },
],
});
}
getPositionStrategy(data: Toast) {
switch (data?.position) {
case 'top':
return this._overlay.position().global().top(this.getNextPosition()).centerHorizontally();
case 'top-left':
return this._overlay.position().global().top(this.getNextPosition()).left('1rem');
case 'top-right':
return this._overlay.position().global().top(this.getNextPosition()).right('1rem');
case 'bottom':
return this._overlay.position().global().bottom(this.getNextPosition(true)).centerHorizontally();
case 'bottom-left':
return this._overlay.position().global().bottom(this.getNextPosition(true)).left('1rem');
case 'bottom-right':
return this._overlay.position().global().bottom(this.getNextPosition(true)).right('1rem');
default:
return this._overlay.position().global().top(this.getNextPosition()).centerHorizontally();
}
}
getNextPosition(fromBottom?: boolean) {
const lastToastIsVisible = this.lastToastRef && this.lastToastRef.isVisible();
let position = fromBottom ? 6 : 9;
if (lastToastIsVisible && fromBottom) {
position = (window.innerHeight - this.lastToastRef.getPosition().bottom + this.lastToastRef.getPosition().height + 16) / 16;
} else if (lastToastIsVisible) {
position = (this.lastToastRef.getPosition().bottom + 16) / 16;
}
return position + 'rem';
}
}

View File

@@ -1,4 +0,0 @@
import { InjectionToken } from '@angular/core';
import { Toast } from './defs';
export const TOAST_CONFIG_TOKEN = new InjectionToken<Toast>('TOAST_DATA');

View File

@@ -1,5 +0,0 @@
/*
* Public API Surface of toast
*/
export * from './lib';

View File

@@ -8,7 +8,7 @@ import {
StoreCheckoutSupplierService,
SupplierDTO,
} from '@swagger/checkout';
import { combineLatest, Observable, of } from 'rxjs';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import {
AvailabilityRequestDTO,
AvailabilityService,
@@ -21,11 +21,16 @@ import { isArray, memorize } from '@utils/common';
import { LogisticianDTO, LogisticianService } from '@swagger/oms';
import { ResponseArgsOfIEnumerableOfStockInfoDTO, StockDTO, StockInfoDTO, StockService } from '@swagger/remi';
import { PriceDTO } from '@swagger/availability';
import { AvailabilityByBranchDTO, ItemData } from './defs';
import { AvailabilityByBranchDTO, ItemData, Ssc } from './defs';
import { Availability } from './defs/availability';
import { isEmpty } from 'lodash';
@Injectable()
export class DomainAvailabilityService {
// Ticket #3378 Keep Result List Items and Details Page SSC in sync
sscs$ = new BehaviorSubject<Array<Ssc>>([]);
sscsObs$ = this.sscs$.asObservable();
constructor(
private _availabilityService: AvailabilityService,
private _logisticanService: LogisticianService,
@@ -155,7 +160,6 @@ export class DomainAvailabilityService {
quantity: number;
branch?: BranchDTO;
}): Observable<AvailabilityDTO> {
console.log('getTakeAwayAvailability', item, quantity, branch);
const request = !!branch ? this.getStockByBranch(branch.id) : this.getDefaultStock();
return request.pipe(
switchMap((s) =>
@@ -480,6 +484,10 @@ export class DomainAvailabilityService {
};
}
private _priceIsEmpty(price: PriceDTO) {
return isEmpty(price?.value) || isEmpty(price?.vat);
}
private _mapToTakeAwayAvailability({
response,
supplier,
@@ -500,7 +508,7 @@ export class DomainAvailabilityService {
inStock: inStock,
supplierSSC: quantity <= inStock ? '999' : '',
supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '',
price: price ?? stockInfo?.retailPrice,
price: this._priceIsEmpty(price) ? stockInfo?.retailPrice : price,
supplier: { id: supplier?.id },
// TODO: Change after API Update
// LH: 2021-03-09 preis Property hat nun ein Fallback auf retailPrice

View File

@@ -1,3 +1,4 @@
export * from './availability-by-branch-dto';
export * from './availability';
export * from './item-data';
export * from './ssc';

View File

@@ -0,0 +1,5 @@
export interface Ssc {
itemId?: number;
ssc?: string;
sscText?: string;
}

View File

@@ -27,6 +27,7 @@ import {
StoreCheckoutPayerService,
StoreCheckoutBranchService,
ItemsResult,
ShoppingCartItemDTO,
} from '@swagger/checkout';
import {
DisplayOrderDTO,
@@ -36,20 +37,45 @@ import {
ResponseArgsOfValueTupleOfIEnumerableOfDisplayOrderDTOAndIEnumerableOfKeyValueDTOOfStringAndString,
} from '@swagger/oms';
import { isNullOrUndefined, memorize } from '@utils/common';
import { combineLatest, Observable, of, concat, isObservable, throwError } from 'rxjs';
import { bufferCount, catchError, filter, first, map, mergeMap, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { combineLatest, Observable, of, concat, isObservable, throwError, interval, zip, EMPTY, Subscription } from 'rxjs';
import {
bufferCount,
catchError,
debounceTime,
distinctUntilChanged,
filter,
first,
map,
mergeMap,
share,
shareReplay,
switchMap,
take,
tap,
withLatestFrom,
} from 'rxjs/operators';
import * as DomainCheckoutSelectors from './store/domain-checkout.selectors';
import * as DomainCheckoutActions from './store/domain-checkout.actions';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainAvailabilityService, ItemData } from '@domain/availability';
import { HttpErrorResponse } from '@angular/common/http';
import { ApplicationService } from '@core/application';
import { CustomerDTO, EntityDTOContainerOfAttributeDTO } from '@swagger/crm';
import { CustomerDTO } from '@swagger/crm';
import { Config } from '@core/config';
import parseDuration from 'parse-duration';
import { CheckoutEntity } from './store/defs/checkout.entity';
import { isEqual } from 'lodash';
@Injectable()
export class DomainCheckoutService {
get olaExpiration() {
const exp = this._config.get('@domain/checkout.olaExpiration') ?? '5m';
return parseDuration(exp);
}
constructor(
private store: Store<any>,
private _config: Config,
private applicationService: ApplicationService,
private storeCheckoutService: StoreCheckoutService,
private orderCheckoutService: OrderCheckoutService,
@@ -119,14 +145,14 @@ export class DomainCheckoutService {
})
.pipe(
map((response) => response.result),
tap((shoppingCart) =>
tap((shoppingCart) => {
this.store.dispatch(
DomainCheckoutActions.setShoppingCart({
processId,
shoppingCart,
})
)
),
);
}),
tap((shoppingCart) => this.updateProcessCount(processId, shoppingCart?.items?.length))
)
)
@@ -249,11 +275,24 @@ export class DomainCheckoutService {
shoppingCartItemId: number;
availability: AvailabilityDTO;
}) {
return this._shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
shoppingCartId,
shoppingCartItemId,
availability,
});
return this._shoppingCartService
.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
shoppingCartId,
shoppingCartItemId,
availability,
})
.pipe(
map((response) => response.result),
tap((shoppingCart) => {
this.store.dispatch(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId({
shoppingCartId,
availability,
shoppingCartItemId,
})
);
})
);
}
updateItemInShoppingCart({
@@ -265,7 +304,7 @@ export class DomainCheckoutService {
shoppingCartItemId: number;
update: UpdateShoppingCartItemDTO;
}): Observable<ShoppingCartDTO> {
return this.getShoppingCart({ processId }).pipe(
return this.getShoppingCart({ processId, latest: true }).pipe(
first(),
mergeMap((shoppingCart) =>
this._shoppingCartService
@@ -276,8 +315,21 @@ export class DomainCheckoutService {
})
.pipe(
map((response) => response.result),
tap((shoppingCart) => this.store.dispatch(DomainCheckoutActions.setShoppingCart({ processId, shoppingCart }))),
tap((shoppingCart) => this.updateProcessCount(processId, shoppingCart?.items?.length))
tap((shoppingCart) => {
this.store.dispatch(DomainCheckoutActions.setShoppingCart({ processId, shoppingCart }));
if (update.availability) {
this.store.dispatch(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory({
processId,
availability: update.availability,
shoppingCartItemId,
})
);
}
this.updateProcessCount(processId, shoppingCart?.items?.length);
})
)
)
);
@@ -547,6 +599,172 @@ export class DomainCheckoutService {
);
}
async refreshAvailability({
processId,
shoppingCartItemId,
}: {
processId: number;
shoppingCartItemId: number;
}): Promise<AvailabilityDTO> {
const shoppingCart = await this.getShoppingCart({ processId }).pipe(first()).toPromise();
const item = shoppingCart?.items.find((item) => item.id === shoppingCartItemId)?.data;
if (!item) {
return;
}
const itemData: ItemData = {
ean: item.product.ean,
itemId: Number(item.product.catalogProductNumber),
price: item.availability.price,
};
let availability: AvailabilityDTO;
switch (item.features.orderType) {
case 'Abholung':
const abholung = await this.availabilityService
.getPickUpAvailability({
item: itemData,
branch: item.destination?.data?.targetBranch?.data,
quantity: item.quantity,
})
.toPromise();
availability = abholung[0];
break;
case 'Rücklage':
const ruecklage = await this.availabilityService
.getTakeAwayAvailability({
item: itemData,
quantity: item.quantity,
branch: item.destination?.data?.targetBranch?.data,
})
.toPromise();
availability = ruecklage;
break;
case 'Download':
const download = await this.availabilityService
.getDownloadAvailability({
item: itemData,
})
.toPromise();
availability = download;
break;
case 'Versand':
const versand = await this.availabilityService
.getDeliveryAvailability({
item: itemData,
quantity: item.quantity,
})
.toPromise();
availability = versand;
break;
case 'DIG-Versand':
const digVersand = await this.availabilityService
.getDigDeliveryAvailability({
item: itemData,
quantity: item.quantity,
})
.toPromise();
availability = digVersand;
break;
case 'B2B-Versand':
const b2bVersand = await this.availabilityService
.getB2bDeliveryAvailability({
item: itemData,
quantity: item.quantity,
})
.toPromise();
availability = b2bVersand;
break;
}
await this.updateItemInShoppingCart({
processId,
update: { availability },
shoppingCartItemId: item.id,
}).toPromise();
return availability;
}
/**
* Check if the availability of all items is valid
* @param param0 Process Id
* @returns true if the availability of all items is valid
*/
validateOlaStatus({ processId, interval }: { processId: number; interval?: number }): Observable<boolean> {
return new Observable((observer) => {
const enity$ = this.store.select(DomainCheckoutSelectors.selectCheckoutEntityByProcessId, { processId });
const olaExpiration = this.olaExpiration;
let timeout: any;
let subscription: Subscription;
function check() {
const now = Date.now();
subscription?.unsubscribe();
subscription = enity$.pipe(take(1)).subscribe((entity) => {
if (!entity || !entity.shoppingCart || !entity.shoppingCart.items) {
return;
}
const itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp ?? {};
const shoppingCart = entity.shoppingCart;
const timestamps = shoppingCart.items
?.map((i) => i.data)
?.filter((item) => !!item?.features?.orderType)
?.map((item) => itemAvailabilityTimestamp[`${item.id}_${item.features.orderType}`]);
if (timestamps?.length > 0) {
const oldestTimestamp = Math.min(...timestamps);
observer.next(now - oldestTimestamp < olaExpiration);
}
timeout = setTimeout(() => {
check.call(this);
}, interval ?? olaExpiration / 10);
});
}
check.call(this);
return () => {
subscription?.unsubscribe();
clearTimeout(timeout);
};
});
}
validateAvailabilities({ processId }: { processId: number }): Observable<boolean> {
return this.getShoppingCart({ processId }).pipe(
map((shoppingCart) => {
const items = shoppingCart?.items?.map((item) => item.data) || [];
return items.every((i) => this.availabilityService.isAvailable({ availability: i.availability }));
})
);
}
checkoutIsValid({ processId }: { processId: number }): Observable<boolean> {
const olaStatus$ = this.validateOlaStatus({ processId, interval: 250 });
const availabilities$ = this.validateAvailabilities({ processId });
return combineLatest([olaStatus$, availabilities$]).pipe(map(([olaStatus, availabilities]) => olaStatus && availabilities));
}
completeCheckout({ processId }: { processId: number }): Observable<DisplayOrderDTO[]> {
const refreshShoppingCart$ = this.getShoppingCart({ processId, latest: true }).pipe(first());
const refreshCheckout$ = this.getCheckout({ processId, refresh: true }).pipe(first());
@@ -700,21 +918,23 @@ export class DomainCheckoutService {
)
);
return updateDestination$
.pipe(tap(console.log.bind(window, 'updateDestination$')))
return of(undefined)
.pipe(
mergeMap((_) => updateDestination$.pipe(tap(console.log.bind(window, 'updateDestination$')))),
mergeMap((_) => refreshShoppingCart$.pipe(tap(console.log.bind(window, 'refreshShoppingCart$')))),
mergeMap((_) => setSpecialComment$.pipe(tap(console.log.bind(window, 'setSpecialComment$')))),
mergeMap((_) => refreshCheckout$.pipe(tap(console.log.bind(window, 'refreshCheckout$')))),
mergeMap((_) => checkAvailabilities$.pipe(tap(console.log.bind(window, 'checkAvailabilities$')))),
mergeMap((_) => updateAvailabilities$.pipe(tap(console.log.bind(window, 'updateAvailabilities$')))),
mergeMap((_) => updateAvailabilities$.pipe(tap(console.log.bind(window, 'updateAvailabilities$'))))
)
.pipe(
mergeMap((_) => setBuyer$.pipe(tap(console.log.bind(window, 'setBuyer$')))),
mergeMap((_) => setNotificationChannels$.pipe(tap(console.log.bind(window, 'setNotificationChannels$')))),
mergeMap((_) => setPayer$.pipe(tap(console.log.bind(window, 'setPayer$')))),
mergeMap((_) => setPaymentType$.pipe(tap(console.log.bind(window, 'setPaymentType$')))),
mergeMap((_) => setDestination$.pipe(tap(console.log.bind(window, 'setDestination$'))))
)
.pipe(mergeMap((_) => completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$')))));
mergeMap((_) => setDestination$.pipe(tap(console.log.bind(window, 'setDestination$')))),
mergeMap((_) => completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$'))))
);
}
completeKulturpassOrder({

View File

@@ -1,4 +1,12 @@
import { BuyerDTO, CheckoutDTO, NotificationChannel, PayerDTO, ShippingAddressDTO, ShoppingCartDTO } from '@swagger/checkout';
import {
AvailabilityDTO,
BuyerDTO,
CheckoutDTO,
NotificationChannel,
PayerDTO,
ShippingAddressDTO,
ShoppingCartDTO,
} from '@swagger/checkout';
import { CustomerDTO } from '@swagger/crm';
import { DisplayOrderDTO } from '@swagger/oms';
@@ -14,4 +22,5 @@ export interface CheckoutEntity {
specialComment: string;
notificationChannels: NotificationChannel;
olaErrorIds: number[];
itemAvailabilityTimestamp: Record<string, number>;
}

View File

@@ -7,6 +7,7 @@ import {
ShippingAddressDTO,
BuyerDTO,
PayerDTO,
AvailabilityDTO,
} from '@swagger/checkout';
import { CustomerDTO } from '@swagger/crm';
import { DisplayOrderDTO, DisplayOrderItemDTO } from '@swagger/oms';
@@ -61,3 +62,13 @@ export const setSpecialComment = createAction(`${prefix} Set Agent Comment`, pro
export const setOlaError = createAction(`${prefix} Set Ola Error`, props<{ processId: number; olaErrorIds: number[] }>());
export const setCustomer = createAction(`${prefix} Set Customer`, props<{ processId: number; customer: CustomerDTO }>());
export const addShoppingCartItemAvailabilityToHistory = createAction(
`${prefix} Add Shopping Cart Item Availability To History`,
props<{ processId: number; shoppingCartItemId: number; availability: AvailabilityDTO }>()
);
export const addShoppingCartItemAvailabilityToHistoryByShoppingCartId = createAction(
`${prefix} Add Shopping Cart Item Availability To History By Shopping Cart Id`,
props<{ shoppingCartId: number; shoppingCartItemId: number; availability: AvailabilityDTO }>()
);

View File

@@ -10,7 +10,22 @@ const _domainCheckoutReducer = createReducer(
initialCheckoutState,
on(DomainCheckoutActions.setShoppingCart, (s, { processId, shoppingCart }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
const addedShoppingCartItems =
shoppingCart?.items?.filter((item) => !entity.shoppingCart?.items?.find((i) => i.id === item.id))?.map((item) => item.data) ?? [];
entity.shoppingCart = shoppingCart;
entity.itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp ? { ...entity.itemAvailabilityTimestamp } : {};
const now = Date.now();
for (let shoppingCartItem of addedShoppingCartItems) {
if (shoppingCartItem.features?.orderType) {
entity.itemAvailabilityTimestamp[`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`] = now;
}
}
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
@@ -100,7 +115,40 @@ const _domainCheckoutReducer = createReducer(
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.customer = customer;
return storeCheckoutAdapter.setOne(entity, s);
})
}),
on(DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory, (s, { processId, shoppingCartItemId, availability }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp ? { ...entity?.itemAvailabilityTimestamp } : {};
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
if (!item?.features?.orderType) return s;
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
const entity = getCheckoutEntityByShoppingCartId({ shoppingCartId, entities: s.entities });
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp ? { ...entity?.itemAvailabilityTimestamp } : {};
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
if (!item?.features?.orderType) return s;
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
return storeCheckoutAdapter.setOne(entity, s);
}
)
);
export function domainCheckoutReducer(state, action) {
@@ -123,8 +171,20 @@ function getOrCreateCheckoutEntity({ entities, processId }: { entities: Dictiona
notificationChannels: 0,
olaErrorIds: [],
customer: undefined,
// availabilityHistory: [],
itemAvailabilityTimestamp: {},
};
}
return { ...entity };
}
function getCheckoutEntityByShoppingCartId({
entities,
shoppingCartId,
}: {
entities: Dictionary<CheckoutEntity>;
shoppingCartId: number;
}): CheckoutEntity {
return Object.values(entities).find((entity) => entity.shoppingCart?.id === shoppingCartId);
}

View File

@@ -5,7 +5,7 @@ import { UiModalService } from '@ui/modal';
import { ReorderModalComponent, ReorderResult } from '@modal/reorder';
import { DomainCheckoutService } from '@domain/checkout';
import { AvailabilityDTO2, OrderItemListItemDTO } from '@swagger/oms';
import { ToastService } from '@core/toast';
import { ToasterService } from '@shared/shell';
@Injectable()
export class ReOrderActionHandler extends ActionHandler<OrderItemsContext> {
@@ -13,7 +13,7 @@ export class ReOrderActionHandler extends ActionHandler<OrderItemsContext> {
private _command: CommandService,
private _domainCheckoutService: DomainCheckoutService,
private _uiModal: UiModalService,
private _toastService: ToastService
private _toastService: ToasterService
) {
super('REORDER');
}
@@ -71,8 +71,8 @@ export class ReOrderActionHandler extends ActionHandler<OrderItemsContext> {
case 'Falscher Titel geliefert (richtiges Etikett)':
break;
default:
this._toastService.create({
title: 'Artikel wurde nachbestellt',
this._toastService.open({
message: 'Artikel wurde nachbestellt',
});
}
}

View File

@@ -16,12 +16,10 @@ export class DomainCustomerOrderService {
// branch_id'
}
getOrderItemsByOrderNumber(orderNumber: string) {
getOrderItemsByOrderNumber(params: { compartmentCode?: string; orderId: number }) {
return this._orderService.OrderKundenbestellungen({
filter: { all_branches: 'true', archive: 'true' },
input: {
qs: orderNumber,
},
input: { order_id: String(params.orderId), compartment_code: params.compartmentCode },
});
}

View File

@@ -1,4 +1,5 @@
// start:ng42.barrel
export * from './hub-notification.module';
export * from './notifications.hub';
export * from './defs';
// end:ng42.barrel

View File

@@ -4,61 +4,118 @@ import { SignalrHub, SignalRHubOptions } from '@core/signalr';
import { BehaviorSubject, merge, of } from 'rxjs';
import { filter, map, shareReplay, tap, withLatestFrom } from 'rxjs/operators';
import { EnvelopeDTO, MessageBoardItemDTO } from './defs';
import { cloneDeep } from 'lodash';
export const NOTIFICATIONS_HUB_OPTIONS = new InjectionToken<SignalRHubOptions>('hub.notifications.options');
@Injectable()
export class NotificationsHub extends SignalrHub {
updateNotification$ = new BehaviorSubject<MessageBoardItemDTO>(undefined);
get branchNo() {
return String(this._auth.getClaimByKey('branch_no') || this._auth.getClaimByKey('sub'));
}
// get sessionStoragesessionStorageKey() {
// return `NOTIFICATIONS_BOARD_${this.branchNo}`;
// }
get sessionStoragesessionStorageKey() {
return `NOTIFICATIONS_BOARD_${this.branchNo}`;
return `NOTIFICATIONS_BOARD_AREA_${this.branchNo}`;
}
messageBoardItems$ = new BehaviorSubject<Record<string, MessageBoardItemDTO[]>>({});
constructor(@Inject(NOTIFICATIONS_HUB_OPTIONS) options: SignalRHubOptions, private _auth: AuthService) {
super(options);
this.messageBoardItems$.next(this._getNotifications());
this.messageBoardItems$.subscribe((data) => {
this._storeNotifactions(data);
});
this.listen<EnvelopeDTO<MessageBoardItemDTO[]>>('messageBoard').subscribe((envelope) => {
if (envelope.action === 'refresh') {
this.refreshMessageBoardItems(envelope.target.area, envelope.data);
}
});
}
notifications$ = merge(
of(this._getNotifications()).pipe(filter((f) => !!f)),
this.listen<EnvelopeDTO<MessageBoardItemDTO[]>>('messageBoard')
).pipe(
withLatestFrom(this.updateNotification$),
map(([d, update]) => {
const data = d;
if (update && !!data && !data?.data?.find((message) => message?.category === 'ISA-Update')) {
data.data.push(update);
refreshMessageBoardItems(targetArea: string, messages: MessageBoardItemDTO[]) {
const current = cloneDeep(this.messageBoardItems$.value);
current[targetArea] = messages ?? [];
this.messageBoardItems$.next(current);
}
notifications$ = this.messageBoardItems$.asObservable().pipe(
map((data) => {
const messages = { ...data };
const keys = Object.keys(data);
for (let key of keys) {
if (data[key].length === 0 || data[key] === undefined) {
delete messages[key];
}
}
return data;
}),
tap((data) => this._storeNotifactions(data)),
shareReplay(1)
return messages;
})
);
private _storeNotifactions(data: EnvelopeDTO<MessageBoardItemDTO[]>) {
if (data) {
sessionStorage.setItem(this.sessionStoragesessionStorageKey, JSON.stringify(data));
}
}
// notifications$ = merge(
// of(this._getNotifications()).pipe(filter((f) => !!f)),
// this.listen<EnvelopeDTO<MessageBoardItemDTO[]>>('messageBoard')
// ).pipe(
// withLatestFrom(this.updateNotification$),
// map(([d, update]) => {
// console.log('notifications$', d, update);
// const data = d;
// if (update && !!data && !data?.data?.find((message) => message?.category === 'ISA-Update')) {
// data.data.push(update);
// }
// return data;
// }),
// tap((data) => this._storeNotifactions(data)),
// shareReplay(1)
// );
private _getNotifications(): EnvelopeDTO<MessageBoardItemDTO[]> {
// private _storeNotifactions(data: EnvelopeDTO<MessageBoardItemDTO[]>) {
// if (data) {
// sessionStorage.setItem(this.sessionStoragesessionStorageKey, JSON.stringify(data));
// }
// }
// private _getNotifications(): EnvelopeDTO<MessageBoardItemDTO[]> {
// const stringData = sessionStorage.getItem(this.sessionStoragesessionStorageKey);
// if (stringData) {
// return JSON.parse(stringData);
// }
// return undefined;
// }
private _getNotifications(): Record<string, MessageBoardItemDTO[]> {
const stringData = sessionStorage.getItem(this.sessionStoragesessionStorageKey);
if (stringData) {
return JSON.parse(stringData);
}
return undefined;
return {};
}
private _storeNotifactions(data: Record<string, MessageBoardItemDTO[]>) {
if (data) {
delete data['messageBoard/isa-update'];
sessionStorage.setItem(this.sessionStoragesessionStorageKey, JSON.stringify(data));
}
}
updateNotification() {
this.updateNotification$.next({
category: 'ISA-Update',
type: 'update',
headline: 'Update Benachrichtigung',
text: 'Es steht eine aktuellere Version der ISA bereit. Bitte aktualisieren Sie die Anwendung.',
});
this.refreshMessageBoardItems('messageBoard/isa-update', [
{
category: 'ISA-Update',
type: 'update',
headline: 'Update Benachrichtigung',
text: 'Es steht eine aktuellere Version der ISA bereit. Bitte aktualisieren Sie die Anwendung.',
},
]);
}
}

View File

@@ -116,6 +116,7 @@ export class AppComponent implements OnInit {
checkForUpdate() {
interval(this._checkForUpdates).subscribe(() => {
this._swUpdate.checkForUpdate().then((value) => {
console.log('check for update', value);
if (value) {
this._notifications.updateNotification();
}
@@ -125,6 +126,7 @@ export class AppComponent implements OnInit {
initialCheckForUpdate() {
this._swUpdate.checkForUpdate().then((value) => {
console.log('initial check for update', value);
if (value) {
location.reload();
}

View File

@@ -256,6 +256,21 @@
"name": "badge",
"data": "M140-80q-24 0-42-18t-18-42v-480q0-24 18-42t42-18h250v-140q0-24 18-42t42.411-18h59.178Q534-880 552-862t18 42v140h250q24 0 42 18t18 42v480q0 24-18 42t-42 18H140Zm0-60h680v-480H570v30q0 28-18 44t-42.411 16h-59.178Q426-530 408-546t-18-44v-30H140v480Zm92-107h239v-14q0-18-9-32t-23-19q-32-11-50-14.5t-35-3.5q-19 0-40.5 4.5T265-312q-15 5-24 19t-9 32v14Zm336-67h170v-50H568v50Zm-214-50q22.5 0 38.25-15.75T408-418q0-22.5-15.75-38.25T354-472q-22.5 0-38.25 15.75T300-418q0 22.5 15.75 38.25T354-364Zm214-63h170v-50H568v50ZM450-590h60v-230h-60v230Zm30 210Z",
"viewBox": "0 -960 960 960"
},
{
"name": "text-increase",
"data": "m40-200 220-560h80l220 560h-75l-57-150H172l-57 150H40Zm156-214h208L302-685h-4L196-414Zm534 94v-130H600v-60h130v-130h60v130h130v60H790v130h-60Z",
"viewBox": "0 -960 960 960"
},
{
"name": "text-decrease",
"data": "m40-200 220-560h80l220 560h-75l-57-150H172l-57 150H40Zm156-214h208L302-685h-4L196-414Zm414-36v-60h310v60H610Z",
"viewBox":"0 -960 960 960"
},
{
"name": "calendar-today",
"data": "M180-80q-24 0-42-18t-18-42v-620q0-24 18-42t42-18h65v-60h65v60h340v-60h65v60h65q24 0 42 18t18 42v620q0 24-18 42t-42 18H180Zm0-60h600v-430H180v430Zm0-490h600v-130H180v130Zm0 0v-130 130Z",
"viewBox": "0 -960 960 960"
}
],
@@ -320,6 +335,18 @@
"name": "isa-hard-cover",
"alias": "GEB"
},
{
"name": "isa-hard-cover",
"alias": "GEH"
},
{
"name": "isa-hard-cover",
"alias": "PP"
},
{
"name": "isa-hard-cover",
"alias": "DR"
},
{
"name": "isa-hard-cover",
"alias": "HC"

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="22px" height="18px" viewBox="0 0 22 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.2 (67145) - http://www.bohemiancoding.com/sketch -->
<title>Icon_HC</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Hugendubel_Icons" transform="translate(-29.000000, -177.000000)" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.3">
<path d="M32.725699,178.000628 C32.7430505,177.999791 32.7604307,177.999791 32.7777823,178.000628 L37.5260449,178.000628 C38.53109,178.000628 39.44018,178.547926 40.0000026,179.340692 C40.5598253,178.547926 41.4689153,178.000628 42.4739603,178.000628 L47.222223,178.000628 C47.529035,178.00066 47.7777477,178.256627 47.7777784,178.572389 L47.7777784,179.71591 L49.4444446,179.71591 C49.7512567,179.715942 49.9999694,179.971909 50,180.287671 L50,192.008763 C49.9999694,192.324524 49.7512567,192.580492 49.4444446,192.580523 L42.4739603,192.580523 C41.4766486,192.580523 40.8524541,192.949423 40.4947942,193.688309 C40.3998428,193.879608 40.208727,194 40.0000026,194 C39.7912783,194 39.6001624,193.879608 39.5052111,193.688309 C39.1475512,192.949423 38.5233566,192.580523 37.5260449,192.580523 L30.5555607,192.580523 C30.2487486,192.580492 30.0000359,192.324524 30.0000053,192.008763 L30.0000053,180.287671 C29.9987652,179.991708 30.2171687,179.743681 30.5034773,179.71591 C30.5208289,179.715072 30.5382091,179.715072 30.5555607,179.71591 L32.2222269,179.71591 L32.2222269,178.572389 C32.2209869,178.276426 32.4393904,178.028399 32.725699,178.000628 Z M33.3333377,179.14415 L33.3333377,189.43584 L36.6232674,189.43584 C37.6190746,189.43584 38.5547188,189.729711 39.2621556,190.356017 C39.3270606,190.413476 39.3849347,190.480265 39.4444472,190.543626 L39.4444472,180.957703 C39.4433388,180.936873 39.4433388,180.915996 39.4444472,180.895166 C39.4444472,180.068361 38.4768339,179.14415 37.5260449,179.14415 L33.3333377,179.14415 Z M42.4739603,179.14415 C41.5231714,179.14415 40.555558,180.068361 40.555558,180.895166 C40.5555806,180.898144 40.5555806,180.901122 40.555558,180.9041 L40.555558,190.543626 C40.6150705,190.480265 40.6729447,190.413476 40.7378497,190.356017 C41.4452864,189.729711 42.3809306,189.43584 43.3767379,189.43584 L46.6666675,189.43584 L46.6666675,179.14415 L42.4739603,179.14415 Z M31.1111161,180.859431 L31.1111161,191.437002 L37.5260449,191.437002 C38.0365968,191.437002 38.5189494,191.531483 38.9496557,191.713949 C38.8273679,191.527334 38.6888269,191.36055 38.5329891,191.222592 C38.0547048,190.799175 37.405738,190.579361 36.6232674,190.579361 L32.7777823,190.579361 C32.4709702,190.57933 32.2222575,190.323362 32.2222269,190.007601 L32.2222269,180.859431 L31.1111161,180.859431 Z M47.7777784,180.859431 L47.7777784,190.007601 C47.7777477,190.323362 47.529035,190.57933 47.222223,190.579361 L43.3767379,190.579361 C42.5942672,190.579361 41.9453004,190.799175 41.4670161,191.222592 C41.3107359,191.360944 41.1725734,191.526903 41.0503496,191.713949 C41.4810558,191.531483 41.9634085,191.437002 42.4739603,191.437002 L48.8888892,191.437002 L48.8888892,180.859431 L47.7777784,180.859431 Z M35.5,182.116677 L37.5,182.116677 C37.7761424,182.116677 38,182.340535 38,182.616677 L38,182.645847 C38,182.921989 37.7761424,183.145847 37.5,183.145847 L35.5,183.145847 C35.2238576,183.145847 35,182.921989 35,182.645847 L35,182.616677 C35,182.340535 35.2238576,182.116677 35.5,182.116677 Z M35.5,184.175016 L37.5,184.175016 C37.7761424,184.175016 38,184.398874 38,184.675016 L38,184.704185 C38,184.980328 37.7761424,185.204185 37.5,185.204185 L35.5,185.204185 C35.2238576,185.204185 35,184.980328 35,184.704185 L35,184.675016 C35,184.398874 35.2238576,184.175016 35.5,184.175016 Z M35.5,186.233355 L37.5,186.233355 C37.7761424,186.233355 38,186.457212 38,186.733355 L38,186.762524 C38,187.038666 37.7761424,187.262524 37.5,187.262524 L35.5,187.262524 C35.2238576,187.262524 35,187.038666 35,186.762524 L35,186.733355 C35,186.457212 35.2238576,186.233355 35.5,186.233355 Z M42.5,182.116677 L44.5,182.116677 C44.7761424,182.116677 45,182.340535 45,182.616677 L45,182.645847 C45,182.921989 44.7761424,183.145847 44.5,183.145847 L42.5,183.145847 C42.2238576,183.145847 42,182.921989 42,182.645847 L42,182.616677 C42,182.340535 42.2238576,182.116677 42.5,182.116677 Z M42.5,184.175016 L44.5,184.175016 C44.7761424,184.175016 45,184.398874 45,184.675016 L45,184.704185 C45,184.980328 44.7761424,185.204185 44.5,185.204185 L42.5,185.204185 C42.2238576,185.204185 42,184.980328 42,184.704185 L42,184.675016 C42,184.398874 42.2238576,184.175016 42.5,184.175016 Z M42.5,186.233355 L44.5,186.233355 C44.7761424,186.233355 45,186.457212 45,186.733355 L45,186.762524 C45,187.038666 44.7761424,187.262524 44.5,187.262524 L42.5,187.262524 C42.2238576,187.262524 42,187.038666 42,186.762524 L42,186.733355 C42,186.457212 42.2238576,186.233355 42.5,186.233355 Z" id="Icon_HC"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="22px" height="18px" viewBox="0 0 22 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.2 (67145) - http://www.bohemiancoding.com/sketch -->
<title>Icon_HC</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Hugendubel_Icons" transform="translate(-29.000000, -177.000000)" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.3">
<path d="M32.725699,178.000628 C32.7430505,177.999791 32.7604307,177.999791 32.7777823,178.000628 L37.5260449,178.000628 C38.53109,178.000628 39.44018,178.547926 40.0000026,179.340692 C40.5598253,178.547926 41.4689153,178.000628 42.4739603,178.000628 L47.222223,178.000628 C47.529035,178.00066 47.7777477,178.256627 47.7777784,178.572389 L47.7777784,179.71591 L49.4444446,179.71591 C49.7512567,179.715942 49.9999694,179.971909 50,180.287671 L50,192.008763 C49.9999694,192.324524 49.7512567,192.580492 49.4444446,192.580523 L42.4739603,192.580523 C41.4766486,192.580523 40.8524541,192.949423 40.4947942,193.688309 C40.3998428,193.879608 40.208727,194 40.0000026,194 C39.7912783,194 39.6001624,193.879608 39.5052111,193.688309 C39.1475512,192.949423 38.5233566,192.580523 37.5260449,192.580523 L30.5555607,192.580523 C30.2487486,192.580492 30.0000359,192.324524 30.0000053,192.008763 L30.0000053,180.287671 C29.9987652,179.991708 30.2171687,179.743681 30.5034773,179.71591 C30.5208289,179.715072 30.5382091,179.715072 30.5555607,179.71591 L32.2222269,179.71591 L32.2222269,178.572389 C32.2209869,178.276426 32.4393904,178.028399 32.725699,178.000628 Z M33.3333377,179.14415 L33.3333377,189.43584 L36.6232674,189.43584 C37.6190746,189.43584 38.5547188,189.729711 39.2621556,190.356017 C39.3270606,190.413476 39.3849347,190.480265 39.4444472,190.543626 L39.4444472,180.957703 C39.4433388,180.936873 39.4433388,180.915996 39.4444472,180.895166 C39.4444472,180.068361 38.4768339,179.14415 37.5260449,179.14415 L33.3333377,179.14415 Z M42.4739603,179.14415 C41.5231714,179.14415 40.555558,180.068361 40.555558,180.895166 C40.5555806,180.898144 40.5555806,180.901122 40.555558,180.9041 L40.555558,190.543626 C40.6150705,190.480265 40.6729447,190.413476 40.7378497,190.356017 C41.4452864,189.729711 42.3809306,189.43584 43.3767379,189.43584 L46.6666675,189.43584 L46.6666675,179.14415 L42.4739603,179.14415 Z M31.1111161,180.859431 L31.1111161,191.437002 L37.5260449,191.437002 C38.0365968,191.437002 38.5189494,191.531483 38.9496557,191.713949 C38.8273679,191.527334 38.6888269,191.36055 38.5329891,191.222592 C38.0547048,190.799175 37.405738,190.579361 36.6232674,190.579361 L32.7777823,190.579361 C32.4709702,190.57933 32.2222575,190.323362 32.2222269,190.007601 L32.2222269,180.859431 L31.1111161,180.859431 Z M47.7777784,180.859431 L47.7777784,190.007601 C47.7777477,190.323362 47.529035,190.57933 47.222223,190.579361 L43.3767379,190.579361 C42.5942672,190.579361 41.9453004,190.799175 41.4670161,191.222592 C41.3107359,191.360944 41.1725734,191.526903 41.0503496,191.713949 C41.4810558,191.531483 41.9634085,191.437002 42.4739603,191.437002 L48.8888892,191.437002 L48.8888892,180.859431 L47.7777784,180.859431 Z M35.5,182.116677 L37.5,182.116677 C37.7761424,182.116677 38,182.340535 38,182.616677 L38,182.645847 C38,182.921989 37.7761424,183.145847 37.5,183.145847 L35.5,183.145847 C35.2238576,183.145847 35,182.921989 35,182.645847 L35,182.616677 C35,182.340535 35.2238576,182.116677 35.5,182.116677 Z M35.5,184.175016 L37.5,184.175016 C37.7761424,184.175016 38,184.398874 38,184.675016 L38,184.704185 C38,184.980328 37.7761424,185.204185 37.5,185.204185 L35.5,185.204185 C35.2238576,185.204185 35,184.980328 35,184.704185 L35,184.675016 C35,184.398874 35.2238576,184.175016 35.5,184.175016 Z M35.5,186.233355 L37.5,186.233355 C37.7761424,186.233355 38,186.457212 38,186.733355 L38,186.762524 C38,187.038666 37.7761424,187.262524 37.5,187.262524 L35.5,187.262524 C35.2238576,187.262524 35,187.038666 35,186.762524 L35,186.733355 C35,186.457212 35.2238576,186.233355 35.5,186.233355 Z M42.5,182.116677 L44.5,182.116677 C44.7761424,182.116677 45,182.340535 45,182.616677 L45,182.645847 C45,182.921989 44.7761424,183.145847 44.5,183.145847 L42.5,183.145847 C42.2238576,183.145847 42,182.921989 42,182.645847 L42,182.616677 C42,182.340535 42.2238576,182.116677 42.5,182.116677 Z M42.5,184.175016 L44.5,184.175016 C44.7761424,184.175016 45,184.398874 45,184.675016 L45,184.704185 C45,184.980328 44.7761424,185.204185 44.5,185.204185 L42.5,185.204185 C42.2238576,185.204185 42,184.980328 42,184.704185 L42,184.675016 C42,184.398874 42.2238576,184.175016 42.5,184.175016 Z M42.5,186.233355 L44.5,186.233355 C44.7761424,186.233355 45,186.457212 45,186.733355 L45,186.762524 C45,187.038666 44.7761424,187.262524 44.5,187.262524 L42.5,187.262524 C42.2238576,187.262524 42,187.038666 42,186.762524 L42,186.733355 C42,186.457212 42.2238576,186.233355 42.5,186.233355 Z" id="Icon_HC"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="22px" height="18px" viewBox="0 0 22 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.2 (67145) - http://www.bohemiancoding.com/sketch -->
<title>Icon_HC</title>
<desc>Created with Sketch.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Hugendubel_Icons" transform="translate(-29.000000, -177.000000)" fill="#000000" fill-rule="nonzero" stroke="#000000" stroke-width="0.3">
<path d="M32.725699,178.000628 C32.7430505,177.999791 32.7604307,177.999791 32.7777823,178.000628 L37.5260449,178.000628 C38.53109,178.000628 39.44018,178.547926 40.0000026,179.340692 C40.5598253,178.547926 41.4689153,178.000628 42.4739603,178.000628 L47.222223,178.000628 C47.529035,178.00066 47.7777477,178.256627 47.7777784,178.572389 L47.7777784,179.71591 L49.4444446,179.71591 C49.7512567,179.715942 49.9999694,179.971909 50,180.287671 L50,192.008763 C49.9999694,192.324524 49.7512567,192.580492 49.4444446,192.580523 L42.4739603,192.580523 C41.4766486,192.580523 40.8524541,192.949423 40.4947942,193.688309 C40.3998428,193.879608 40.208727,194 40.0000026,194 C39.7912783,194 39.6001624,193.879608 39.5052111,193.688309 C39.1475512,192.949423 38.5233566,192.580523 37.5260449,192.580523 L30.5555607,192.580523 C30.2487486,192.580492 30.0000359,192.324524 30.0000053,192.008763 L30.0000053,180.287671 C29.9987652,179.991708 30.2171687,179.743681 30.5034773,179.71591 C30.5208289,179.715072 30.5382091,179.715072 30.5555607,179.71591 L32.2222269,179.71591 L32.2222269,178.572389 C32.2209869,178.276426 32.4393904,178.028399 32.725699,178.000628 Z M33.3333377,179.14415 L33.3333377,189.43584 L36.6232674,189.43584 C37.6190746,189.43584 38.5547188,189.729711 39.2621556,190.356017 C39.3270606,190.413476 39.3849347,190.480265 39.4444472,190.543626 L39.4444472,180.957703 C39.4433388,180.936873 39.4433388,180.915996 39.4444472,180.895166 C39.4444472,180.068361 38.4768339,179.14415 37.5260449,179.14415 L33.3333377,179.14415 Z M42.4739603,179.14415 C41.5231714,179.14415 40.555558,180.068361 40.555558,180.895166 C40.5555806,180.898144 40.5555806,180.901122 40.555558,180.9041 L40.555558,190.543626 C40.6150705,190.480265 40.6729447,190.413476 40.7378497,190.356017 C41.4452864,189.729711 42.3809306,189.43584 43.3767379,189.43584 L46.6666675,189.43584 L46.6666675,179.14415 L42.4739603,179.14415 Z M31.1111161,180.859431 L31.1111161,191.437002 L37.5260449,191.437002 C38.0365968,191.437002 38.5189494,191.531483 38.9496557,191.713949 C38.8273679,191.527334 38.6888269,191.36055 38.5329891,191.222592 C38.0547048,190.799175 37.405738,190.579361 36.6232674,190.579361 L32.7777823,190.579361 C32.4709702,190.57933 32.2222575,190.323362 32.2222269,190.007601 L32.2222269,180.859431 L31.1111161,180.859431 Z M47.7777784,180.859431 L47.7777784,190.007601 C47.7777477,190.323362 47.529035,190.57933 47.222223,190.579361 L43.3767379,190.579361 C42.5942672,190.579361 41.9453004,190.799175 41.4670161,191.222592 C41.3107359,191.360944 41.1725734,191.526903 41.0503496,191.713949 C41.4810558,191.531483 41.9634085,191.437002 42.4739603,191.437002 L48.8888892,191.437002 L48.8888892,180.859431 L47.7777784,180.859431 Z M35.5,182.116677 L37.5,182.116677 C37.7761424,182.116677 38,182.340535 38,182.616677 L38,182.645847 C38,182.921989 37.7761424,183.145847 37.5,183.145847 L35.5,183.145847 C35.2238576,183.145847 35,182.921989 35,182.645847 L35,182.616677 C35,182.340535 35.2238576,182.116677 35.5,182.116677 Z M35.5,184.175016 L37.5,184.175016 C37.7761424,184.175016 38,184.398874 38,184.675016 L38,184.704185 C38,184.980328 37.7761424,185.204185 37.5,185.204185 L35.5,185.204185 C35.2238576,185.204185 35,184.980328 35,184.704185 L35,184.675016 C35,184.398874 35.2238576,184.175016 35.5,184.175016 Z M35.5,186.233355 L37.5,186.233355 C37.7761424,186.233355 38,186.457212 38,186.733355 L38,186.762524 C38,187.038666 37.7761424,187.262524 37.5,187.262524 L35.5,187.262524 C35.2238576,187.262524 35,187.038666 35,186.762524 L35,186.733355 C35,186.457212 35.2238576,186.233355 35.5,186.233355 Z M42.5,182.116677 L44.5,182.116677 C44.7761424,182.116677 45,182.340535 45,182.616677 L45,182.645847 C45,182.921989 44.7761424,183.145847 44.5,183.145847 L42.5,183.145847 C42.2238576,183.145847 42,182.921989 42,182.645847 L42,182.616677 C42,182.340535 42.2238576,182.116677 42.5,182.116677 Z M42.5,184.175016 L44.5,184.175016 C44.7761424,184.175016 45,184.398874 45,184.675016 L45,184.704185 C45,184.980328 44.7761424,185.204185 44.5,185.204185 L42.5,185.204185 C42.2238576,185.204185 42,184.980328 42,184.704185 L42,184.675016 C42,184.398874 42.2238576,184.175016 42.5,184.175016 Z M42.5,186.233355 L44.5,186.233355 C44.7761424,186.233355 45,186.457212 45,186.733355 L45,186.762524 C45,187.038666 44.7761424,187.262524 44.5,187.262524 L42.5,187.262524 C42.2238576,187.262524 42,187.038666 42,186.762524 L42,186.733355 C42,186.457212 42.2238576,186.233355 42.5,186.233355 Z" id="Icon_HC"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -16,6 +16,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
},

View File

@@ -1,7 +1,7 @@
{
"title": "ISA - Integration",
"silentRefresh": {
"interval": 300000
"interval": 60000
},
"@cdn/product-image": {
"url": "https://produktbilder.paragon-data.net"
@@ -15,6 +15,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-integration.paragon-data.net/isa/v1"
},
@@ -66,7 +69,7 @@
"assortment": 6000
}
},
"checkForUpdates": 3600000,
"checkForUpdates": 900000,
"licence": {
"scandit": "AZ7zLw2eLmFWHbYP4RDq8VAEgAxmNGYcPU8YpOc3DryEXj4zMzYQFrQuUm0YewGQYEESXjpRwGX1NYmKY3pXHnAn2DeqIzh2an+FUu9socQlbQnJiHJHoWBAqcqWSua+P12tc95P3s9aaEEYvSjUy7Md88f7N+sk6zZbUmqbMXeXqmZwdkmRoUY/2w0CiiiA4gBFHgu4sMeNQ9dWyfxKTUPf5AnsxnuYpCt5KLxJWSYDv8HHj0mx8DCJTe1m2ony97Lge3JbJ5Dd+Zz6SCwqik7fv53Qole9s/3m66lYFWKAzWRKkHN1zts78CmPxPb+AAHVoqlBM3duvYmnCxxGOmlXabKUNuDR2ExaMu/nlo532jqqy25Cet/FP1UAs96ZGRgzEcHxGPp6kA53lJ15zd+cxz6G93E83AmYJkhddXBQElWEaGtQRfrEzRGmvcksR+V8MMYjGmhkVbQxGGqpnfP4IxbuEFcef6bxxTiulzo75gXoqZTt+7C1qpDcrMM3Yp0Z8RBw3JlV2tLk4FYFZpxY8QrXIcjvRYKExtQ9e5sSbST4Vx95YhEUd6iX0SBPDzcmgR4/Ef6gvJfoWgz68+rqhBGckphdHi2Mf/pYuAlh2jbwtrkErE2xWARBejR/UcU/A3F7k9RkFd5/QZC7qhsE6bZH7uhpkptIbi5XkXagwYy1oJD7yJs4VLOJteYWferRm8h1auxXew5tL8VLHciF+lLj6h8PTUDt2blLgUjHtualqlCwdSTzJyYwk4oswGGDk6E48X7LXpzuhtR8TYTOi2REN0uuTbO/slFBRw+CaYUnD0LjB9p2lb8ndcdV9adzBKmwPxiOtlOELQ=="
},

View File

@@ -47,6 +47,9 @@
"@swagger/wws": {
"rootUrl": "https://isa-test.paragon-data.net/wws/v1"
},
"@domain/checkout": {
"olaExpiration": "30s"
},
"hubs": {
"notifications": {
"url": "https://isa-test.paragon-data.net/isa/v1/rt",

View File

@@ -16,6 +16,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa.paragon-systems.de/isa/v1"
},

View File

@@ -16,6 +16,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-staging.paragon-systems.de/isa/v1"
},

View File

@@ -17,6 +17,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
},

View File

@@ -12,16 +12,6 @@
@import './scss/branch';
@layer base {
:root {
font-size: 16px;
}
@media screen and (min-width: 1680px) {
:root {
font-size: 19px;
}
}
body {
@apply bg-background;
}

View File

@@ -1,8 +1,11 @@
<div class="notification-headline">
<h1>{{ item.headline }}</h1>
<button class="notification-edit-cta" (click)="itemSelected.emit(item)">
Bearbeiten
</button>
<div class="grid grid-cols-[1fr_auto] items-center gap-4">
<div class="grid grid-flow-row gap-4">
<h1 class="text-left font-bold text-lg">{{ item.headline }}</h1>
<div class="notification-text">{{ item.text }}</div>
</div>
<div>
<button *ngIf="editButton" class="notification-edit-cta text-brand font-bold text-lg px-4 py-3" (click)="itemSelected.emit(item)">
{{ editButtonLabel }}
</button>
</div>
</div>
<div class="notification-text">{{ item.text }}</div>

View File

@@ -13,5 +13,11 @@ export class ModalNotificationsListItemComponent {
@Output()
itemSelected = new EventEmitter<MessageBoardItemDTO>();
@Input()
editButton = true;
@Input()
editButtonLabel = 'Bearbeiten';
constructor() {}
}

View File

@@ -0,0 +1,11 @@
<div class="notification-list scroll-bar">
<ng-container *ngFor="let notification of notifications">
<modal-notifications-list-item
(click)="itemSelected(notification)"
[editButtonLabel]="'Packstück-Prüfung'"
[item]="notification"
(itemSelected)="itemSelected($event)"
></modal-notifications-list-item>
<hr />
</ng-container>
</div>

View File

@@ -0,0 +1,23 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Router } from '@angular/router';
import { MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
@Component({
selector: 'modal-notifications-package-inspection-group',
templateUrl: 'notifications-package-inspection-group.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ModalNotificationsPackageInspectionGroupComponent {
@Input()
notifications: MessageBoardItemDTO[];
@Output()
navigated = new EventEmitter<void>();
constructor(private _router: Router) {}
itemSelected(item: MessageBoardItemDTO) {
this._router.navigate(['/filiale/package-inspection/packages']);
this.navigated.emit();
}
}

View File

@@ -1,13 +1,3 @@
<div class="header">
<div class="notification-icon">
<span class="notification-counter">{{ notifications.length }}</span>
<ui-icon icon="notification" size="26px"></ui-icon>
</div>
<h2>Remission</h2>
</div>
<hr />
<div class="notification-list scroll-bar">
<ng-container *ngFor="let notification of notifications">
<modal-notifications-list-item [item]="notification" (itemSelected)="itemSelected($event)"></modal-notifications-list-item>

View File

@@ -1,101 +0,0 @@
import { createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator';
import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { UiIconModule } from '@ui/icon';
import { Router } from '@angular/router';
import { UiFilter } from '@ui/filter';
import { MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
import { Component } from '@angular/core';
import { ModalNotificationsListItemComponent } from '../notifications-list-item/notifications-list-item.component';
import { ModalNotificationsRemissionGroupComponent } from './notifications-remission-group.component';
// DummyComponent Class
@Component({
selector: 'dummy-component',
template: '<div></div>',
})
class DummyComponent {
constructor() {}
}
describe('ModalNotificationsRemissionGroupComponent', () => {
let spectator: Spectator<ModalNotificationsRemissionGroupComponent>;
let uiFilterMock: SpyObject<UiFilter>;
let router: Router;
const createComponent = createComponentFactory({
component: ModalNotificationsRemissionGroupComponent,
declarations: [ModalNotificationsListItemComponent],
imports: [
CommonModule,
RouterTestingModule.withRoutes([
{ path: 'filiale/goods/in/results', component: DummyComponent },
{ path: 'filiale/remission/create', component: DummyComponent },
]),
UiIconModule,
],
providers: [],
mocks: [UiFilter],
});
beforeEach(() => {
spectator = createComponent({ props: { notifications: [] } });
router = spectator.inject(Router);
uiFilterMock = spectator.inject(UiFilter);
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
describe('notifications input', () => {
it('should display the right notification-counter value based on the length of the input array', () => {
spectator.setInput({ notifications: [{}, {}, {}] });
expect(spectator.query('.notification-counter')).toHaveText('3');
});
it('should not display notification-counter if input array has length 0', () => {
spectator.setInput({ notifications: [] });
expect(spectator.query('.notification-counter')).toHaveText('');
});
it('should render modal-notifications-list-item based on the input array', () => {
const notifications = [{}, {}];
spectator.setInput({ notifications });
spectator.detectComponentChanges();
expect(spectator.queryAll('modal-notifications-list-item')).toHaveLength(notifications.length);
});
});
describe('itemSelected()', () => {
it('should navigate to results with queryParams from UiFilter.getQueryParamsFromQueryTokenDTO()', () => {
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
spyOn(UiFilter, 'getQueryParamsFromQueryTokenDTO').and.returnValue(item.queryToken.input);
spyOn(router, 'navigate');
spectator.component.itemSelected(item);
expect(router.navigate).toHaveBeenCalledWith(['/filiale/goods/in/results'], { queryParams: item.queryToken.input });
});
it('should emit the navigated event after select item', () => {
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
spyOn(spectator.component.navigated, 'emit');
spectator.component.itemSelected(item);
expect(spectator.component.navigated.emit).toHaveBeenCalled();
});
});
describe('actions CTA', () => {
it('should navigate to remission page after clicking the CTA', () => {
const cta = spectator.query('.cta-primary');
expect(cta).toHaveText('Zur Remission');
expect(cta).toHaveAttribute('href', '/filiale/remission/create');
});
it('should emit the navigated event after clicking the CTA', () => {
const cta = spectator.query('.cta-primary') as HTMLAnchorElement;
spyOn(spectator.component.navigated, 'emit');
spectator.click(cta);
expect(spectator.component.navigated.emit).toHaveBeenCalled();
});
});
});

View File

@@ -1,13 +1,3 @@
<div class="header">
<div class="notification-icon">
<div class="notification-counter">{{ notifications.length }}</div>
<ui-icon icon="notification" size="26px"></ui-icon>
</div>
<h2>Reservierungsanfragen</h2>
</div>
<hr />
<div class="notification-list scroll-bar">
<ng-container *ngFor="let notification of notifications">
<modal-notifications-list-item [item]="notification" (itemSelected)="itemSelected($event)"></modal-notifications-list-item>

View File

@@ -1,101 +0,0 @@
import { createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator';
import { CommonModule } from '@angular/common';
import { ModalNotificationsReservationGroupComponent } from './notifications-reservation-group.component';
import { RouterTestingModule } from '@angular/router/testing';
import { UiIconModule } from '@ui/icon';
import { Router } from '@angular/router';
import { UiFilter } from '@ui/filter';
import { MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
import { Component } from '@angular/core';
import { ModalNotificationsListItemComponent } from '../notifications-list-item/notifications-list-item.component';
// DummyComponent Class
@Component({
selector: 'dummy-component',
template: '<div></div>',
})
class DummyComponent {
constructor() {}
}
describe('ModalNotificationsReservationGroupComponent', () => {
let spectator: Spectator<ModalNotificationsReservationGroupComponent>;
let uiFilterMock: SpyObject<UiFilter>;
let router: Router;
const createComponent = createComponentFactory({
component: ModalNotificationsReservationGroupComponent,
declarations: [ModalNotificationsListItemComponent],
imports: [
CommonModule,
RouterTestingModule.withRoutes([
{ path: 'filiale/goods/in/results', component: DummyComponent },
{ path: 'filiale/goods/in/reservation', component: DummyComponent },
]),
UiIconModule,
],
providers: [],
mocks: [UiFilter],
});
beforeEach(() => {
spectator = createComponent({ props: { notifications: [] } });
router = spectator.inject(Router);
uiFilterMock = spectator.inject(UiFilter);
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
describe('notifications input', () => {
it('should display the right notification-counter value based on the length of the input array', () => {
spectator.setInput({ notifications: [{}, {}, {}] });
expect(spectator.query('.notification-counter')).toHaveText('3');
});
it('should not display notification-counter if input array has length 0', () => {
spectator.setInput({ notifications: [] });
expect(spectator.query('.notification-counter')).toHaveText('');
});
it('should render modal-notifications-list-item based on the input array', () => {
const notifications = [{}, {}];
spectator.setInput({ notifications });
spectator.detectComponentChanges();
expect(spectator.queryAll('modal-notifications-list-item')).toHaveLength(notifications.length);
});
});
describe('itemSelected()', () => {
it('should navigate to results with queryParams from UiFilter.getQueryParamsFromQueryTokenDTO()', () => {
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
spyOn(UiFilter, 'getQueryParamsFromQueryTokenDTO').and.returnValue(item.queryToken.input);
spyOn(router, 'navigate');
spectator.component.itemSelected(item);
expect(router.navigate).toHaveBeenCalledWith(['/filiale/goods/in/results'], { queryParams: item.queryToken.input });
});
it('should emit the navigated event after select item', () => {
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
spyOn(spectator.component.navigated, 'emit');
spectator.component.itemSelected(item);
expect(spectator.component.navigated.emit).toHaveBeenCalled();
});
});
describe('actions CTA', () => {
it('should navigate to reservation page after clicking the CTA', () => {
const cta = spectator.query('.cta-primary');
expect(cta).toHaveText('Zu den Reservierungen');
expect(cta).toHaveAttribute('href', '/filiale/goods/in/reservation');
});
it('should emit the navigated event after clicking the CTA', () => {
const cta = spectator.query('.cta-primary') as HTMLAnchorElement;
spyOn(spectator.component.navigated, 'emit');
spectator.click(cta);
expect(spectator.component.navigated.emit).toHaveBeenCalled();
});
});
});

View File

@@ -1,13 +1,3 @@
<div class="header">
<div class="notification-icon">
<span class="notification-counter">{{ notifications.length }}</span>
<ui-icon icon="notification" size="26px"></ui-icon>
</div>
<h2>Tätigkeitskalender</h2>
</div>
<hr />
<div class="notification-list scroll-bar">
<ng-container *ngFor="let notification of notifications">
<modal-notifications-list-item [item]="notification" (itemSelected)="itemSelected($event)"></modal-notifications-list-item>

View File

@@ -1,101 +0,0 @@
import { createComponentFactory, Spectator, SpyObject } from '@ngneat/spectator';
import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { UiIconModule } from '@ui/icon';
import { Router } from '@angular/router';
import { UiFilter } from '@ui/filter';
import { MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
import { Component } from '@angular/core';
import { ModalNotificationsListItemComponent } from '../notifications-list-item/notifications-list-item.component';
import { ModalNotificationsTaskCalendarGroupComponent } from './notifications-task-calendar-group.component';
// DummyComponent Class
@Component({
selector: 'dummy-component',
template: '<div></div>',
})
class DummyComponent {
constructor() {}
}
describe('ModalNotificationsTaskCalendarGroupComponent', () => {
let spectator: Spectator<ModalNotificationsTaskCalendarGroupComponent>;
let uiFilterMock: SpyObject<UiFilter>;
let router: Router;
const createComponent = createComponentFactory({
component: ModalNotificationsTaskCalendarGroupComponent,
declarations: [ModalNotificationsListItemComponent],
imports: [
CommonModule,
RouterTestingModule.withRoutes([
{ path: 'filiale/goods/in/results', component: DummyComponent },
{ path: 'filiale/task-calendar/calendar', component: DummyComponent },
]),
UiIconModule,
],
providers: [],
mocks: [UiFilter],
});
beforeEach(() => {
spectator = createComponent({ props: { notifications: [] } });
router = spectator.inject(Router);
uiFilterMock = spectator.inject(UiFilter);
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
describe('notifications input', () => {
it('should display the right notification-counter value based on the length of the input array', () => {
spectator.setInput({ notifications: [{}, {}, {}] });
expect(spectator.query('.notification-counter')).toHaveText('3');
});
it('should not display notification-counter if input array has length 0', () => {
spectator.setInput({ notifications: [] });
expect(spectator.query('.notification-counter')).toHaveText('');
});
it('should render modal-notifications-list-item based on the input array', () => {
const notifications = [{}, {}];
spectator.setInput({ notifications });
spectator.detectComponentChanges();
expect(spectator.queryAll('modal-notifications-list-item')).toHaveLength(notifications.length);
});
});
describe('itemSelected()', () => {
it('should navigate to results with queryParams from UiFilter.getQueryParamsFromQueryTokenDTO()', () => {
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
spyOn(UiFilter, 'getQueryParamsFromQueryTokenDTO').and.returnValue(item.queryToken.input);
spyOn(router, 'navigate');
spectator.component.itemSelected(item);
expect(router.navigate).toHaveBeenCalledWith(['/filiale/goods/in/results'], { queryParams: item.queryToken.input });
});
it('should emit the navigated event after select item', () => {
const item: MessageBoardItemDTO = { queryToken: { input: { main_qs: 'test' } } };
spyOn(spectator.component.navigated, 'emit');
spectator.component.itemSelected(item);
expect(spectator.component.navigated.emit).toHaveBeenCalled();
});
});
describe('actions CTA', () => {
it('should navigate to reservation page after clicking the CTA', () => {
const cta = spectator.query('.cta-primary');
expect(cta).toHaveText('Zum Tätigkeitskalender');
expect(cta).toHaveAttribute('href', '/filiale/task-calendar/calendar');
});
it('should emit the navigated event after clicking the CTA', () => {
const cta = spectator.query('.cta-primary') as HTMLAnchorElement;
spyOn(spectator.component.navigated, 'emit');
spectator.click(cta);
expect(spectator.component.navigated.emit).toHaveBeenCalled();
});
});
});

View File

@@ -1,13 +1,3 @@
<div class="header">
<div class="notification-icon">
<span class="notification-counter">{{ notifications.length }}</span>
<ui-icon icon="notification" size="26px"></ui-icon>
</div>
<h2>ISA-Update</h2>
</div>
<hr />
<div class="notification-list scroll-bar">
<ng-container *ngFor="let notification of notifications">
<div class="notification-headline">

View File

@@ -1,41 +0,0 @@
import { createComponentFactory, Spectator } from '@ngneat/spectator';
import { CommonModule } from '@angular/common';
import { UiIconModule } from '@ui/icon';
import { ModalNotificationsUpdateGroupComponent } from './notifications-update-group.component';
describe('ModalNotificationsUpdateGroupComponent', () => {
let spectator: Spectator<ModalNotificationsUpdateGroupComponent>;
const createComponent = createComponentFactory({
component: ModalNotificationsUpdateGroupComponent,
imports: [CommonModule, UiIconModule],
});
beforeEach(() => {
spectator = createComponent({ props: { notifications: [] } });
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
describe('notifications input', () => {
it('should display the right notification-counter value based on the length of the input array', () => {
spectator.setInput({ notifications: [{}, {}, {}] });
expect(spectator.query('.notification-counter')).toHaveText('3');
});
it('should not display notification-counter if input array has length 0', () => {
spectator.setInput({ notifications: [] });
expect(spectator.query('.notification-counter')).toHaveText('');
});
it('should render notification-headline and notification-text based on the input array', () => {
const notifications = [{}, {}];
spectator.setInput({ notifications });
spectator.detectComponentChanges();
expect(spectator.queryAll('.notification-headline')).toHaveLength(notifications.length);
expect(spectator.queryAll('.notification-text')).toHaveLength(notifications.length);
});
});
});

View File

@@ -1,34 +1,39 @@
<h1>Sie haben neue Nachrichten</h1>
<ng-container *ngFor="let notification of groupedNotifications$ | async">
<button
*ngIf="notification.group !== (activeCard$ | async)"
type="button"
class="notification-card"
(click)="activeCard = notification.group"
>
{{ notification.group }}
<ng-container *ngFor="let notification of notifications$ | async | keyvalue">
<button type="button" class="notification-card" (click)="selectArea(notification.key)">
<div class="notification-icon">
<div class="notification-counter">{{ notification.value?.length }}</div>
<ui-icon icon="notification" size="26px"></ui-icon>
</div>
<span>{{ notification.value?.[0]?.category }}</span>
</button>
</ng-container>
<ng-container [ngSwitch]="activeCard$ | async">
<modal-notifications-update-group
*ngSwitchCase="'ISA-Update'"
[notifications]="activeNotifications$ | async"
></modal-notifications-update-group>
<modal-notifications-reservation-group
*ngSwitchCase="'Reservierungsanfragen'"
[notifications]="activeNotifications$ | async"
(navigated)="close()"
></modal-notifications-reservation-group>
<modal-notifications-remission-group
*ngSwitchCase="'Remission'"
[notifications]="activeNotifications$ | async"
(navigated)="close()"
></modal-notifications-remission-group>
<modal-notifications-task-calendar-group
*ngSwitchCase="'Tätigkeitskalender'"
[notifications]="activeNotifications$ | async"
(navigated)="close()"
></modal-notifications-task-calendar-group>
<hr class="-mx-4" />
<ng-container *ngIf="notification.key === selectedArea" [ngSwitch]="notification.value?.[0]?.category">
<modal-notifications-update-group
*ngSwitchCase="'ISA-Update'"
[notifications]="notifications[selectedArea]"
></modal-notifications-update-group>
<modal-notifications-reservation-group
*ngSwitchCase="'Reservierungsanfragen'"
[notifications]="notifications[selectedArea]"
(navigated)="close()"
></modal-notifications-reservation-group>
<modal-notifications-remission-group
*ngSwitchCase="'Remission'"
[notifications]="notifications[selectedArea]"
(navigated)="close()"
></modal-notifications-remission-group>
<modal-notifications-task-calendar-group
*ngSwitchCase="'Tätigkeitskalender'"
[notifications]="notifications[selectedArea]"
(navigated)="close()"
></modal-notifications-task-calendar-group>
<modal-notifications-package-inspection-group
*ngSwitchCase="'Wareneingang Lagerware'"
[notifications]="notifications[selectedArea]"
(navigated)="close()"
>
</modal-notifications-package-inspection-group>
</ng-container>
</ng-container>

View File

@@ -2,46 +2,44 @@ modal-notifications {
@apply flex flex-col relative h-full;
h1 {
@apply text-xl font-bold text-center mb-10;
@apply text-xl font-bold text-center;
}
// .notification-card {
// @apply text-center text-xl text-inactive-branch block bg-white rounded-t-card font-bold no-underline py-4 border-none outline-none shadow-card -ml-4;
// width: calc(100% + 2rem);
// }
.notification-card {
@apply text-center text-xl text-inactive-branch block bg-white rounded-t font-bold no-underline py-4 border-none outline-none shadow-card -ml-4;
width: calc(100% + 2rem);
@apply grid grid-flow-col items-center justify-center gap-4;
@apply text-inactive-branch bg-white;
@apply font-bold text-xl -mx-4 py-4;
.notification-icon {
@apply relative;
.notification-counter {
@apply absolute font-normal text-base -top-2 -right-1 bg-brand text-white rounded-full w-5 h-5 flex items-center justify-center;
z-index: 10;
}
ui-icon {
@apply text-inactive-branch;
}
}
h2 {
@apply font-bold text-2xl ml-4;
}
}
modal-notifications-remission-group,
modal-notifications-reservation-group,
modal-notifications-task-calendar-group,
modal-notifications-update-group {
modal-notifications-update-group,
modal-notifications-package-inspection-group {
@apply flex flex-col relative pb-2;
.header {
@apply flex flex-row justify-center items-center mt-5;
.notification-icon {
@apply relative;
.notification-counter {
@apply absolute -top-2 -right-1 bg-brand text-white rounded-full w-5 h-5 flex items-center justify-center;
z-index: 10;
}
ui-icon {
@apply text-inactive-branch;
}
}
h2 {
@apply font-bold text-h3 ml-4;
}
}
hr {
@apply bg-disabled-branch h-px-2 -ml-4 my-4;
width: calc(100% + 2rem);
}
.notification-list {
@apply overflow-y-scroll -ml-4;
max-height: calc(100vh - 450px);
@@ -60,7 +58,7 @@ modal-notifications {
modal-notifications-list-item,
modal-notifications-update-group {
@apply flex flex-col relative py-1 px-4;
@apply flex flex-col relative p-4;
.notification-headline {
@apply flex flex-row justify-between items-start;

View File

@@ -1,105 +0,0 @@
import { createComponentFactory, mockProvider, Spectator, SpyObject } from '@ngneat/spectator';
import { ModalNotificationsComponent } from './notifications.component';
import { CommonModule } from '@angular/common';
import { UiModalRef, UiModalService } from '@ui/modal';
import { of } from 'rxjs';
describe('ModalNotificationsComponent', () => {
let spectator: Spectator<ModalNotificationsComponent>;
let modalRefMock: SpyObject<UiModalRef>;
const createComponent = createComponentFactory({
component: ModalNotificationsComponent,
imports: [CommonModule],
providers: [mockProvider(UiModalRef, { data: { product: {} } })],
mocks: [UiModalService],
});
beforeEach(() => {
spectator = createComponent();
modalRefMock = spectator.inject(UiModalRef);
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
describe('close()', () => {
it('should call _modalRef.close()', () => {
spectator.component.close();
expect(modalRefMock.close).toHaveBeenCalled();
});
});
describe('get activeCard', () => {
it('should return the activeCard', () => {
spectator.component.activeCard = 'testCard';
expect(spectator.component.activeCard).toBe('testCard');
});
});
describe('get activeCard$', () => {
it('should return activeCard$', () => {
spectator.component.activeCard = 'testCard';
spectator.component.activeCard$
.subscribe((c) => {
expect(c).toBe('testCard');
})
.unsubscribe();
});
});
describe('get groupedNotifications', () => {
it('should return the groupedNotifications', () => {
spectator.component.groupedNotifications = [{ group: 'test', items: [] }];
expect(spectator.component.groupedNotifications).toEqual([{ group: 'test', items: [] }]);
});
});
describe('get groupedNotifications$', () => {
it('should return the groupedNotifications$', () => {
spectator.component.groupedNotifications = [{ group: 'test', items: [] }];
spectator.component.groupedNotifications$
.subscribe((gn) => {
expect(gn).toEqual([{ group: 'test', items: [] }]);
})
.unsubscribe();
});
});
describe('activeNotifications$', () => {
it('should return the item with the activeCard within the group', () => {
spectator.component.groupedNotifications = [{ group: 'testCard', items: [{ text: 'testmessage' }] }];
spectator.component.activeCard = 'testCard';
spectator.component.activeNotifications$
.subscribe((an) => {
expect(an).toEqual([{ text: 'testmessage' }]);
})
.unsubscribe();
});
it('should not return anything if the item with the activeCard is not within the group', () => {
spectator.component.groupedNotifications = [{ group: 'testCard', items: [{ text: 'testmessage' }] }];
spectator.component.activeCard = 'testCardtest';
spectator.component.activeNotifications$
.subscribe((an) => {
expect(an).toBeUndefined();
})
.unsubscribe();
});
});
describe('ngOnInit()', () => {
it('should call patchState with activeCard', () => {
const activeCard = 'test';
spectator = createComponent({ props: { activeCard, groupedNotifications: [{ group: activeCard, items: [] }] } });
spyOn(spectator.component, 'patchState');
spectator.component.ngOnInit();
expect(spectator.component.patchState).toHaveBeenCalledWith({
activeCard: [{ group: activeCard, items: [] }].find((_) => true)?.group,
});
});
});
});

View File

@@ -1,14 +1,11 @@
import { ChangeDetectionStrategy, Component, OnInit, ViewEncapsulation } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { Group, groupBy } from '@ui/common';
import { UiModalRef } from '@ui/modal';
import { EnvelopeDTO, MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { MessageBoardItemDTO } from 'apps/hub/notifications/src/lib/defs';
interface ModalNotificationComponentState {
activeCard: string;
groupedNotifications: Group<string, MessageBoardItemDTO>[];
selectedArea: string;
notifications: Record<string, MessageBoardItemDTO[]>;
}
@Component({
@@ -18,44 +15,61 @@ interface ModalNotificationComponentState {
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class ModalNotificationsComponent extends ComponentStore<ModalNotificationComponentState> implements OnInit {
set activeCard(activeCard: string) {
if (this.activeCard !== activeCard) {
this.patchState({ activeCard });
export class ModalNotificationsComponent extends ComponentStore<ModalNotificationComponentState> {
private _selectedAreaSelector = (state: ModalNotificationComponentState) => {
if (state.selectedArea) {
return state.selectedArea;
}
const keys = Object.keys(state.notifications);
for (const key of keys) {
if (state.notifications[key]?.length > 0) {
return key;
}
}
return undefined;
};
get selectedArea() {
return this.get(this._selectedAreaSelector);
}
activeCard$ = this.select((s) => s.activeCard);
selectedArea$ = this.select(this._selectedAreaSelector);
get activeCard() {
return this.get((s) => s.activeCard);
private _categorySelector = (state: ModalNotificationComponentState) => {
const selectedArea = this._selectedAreaSelector(state);
console.log('_categorySelector', state.notifications[selectedArea]?.[0]?.category);
return state.notifications[selectedArea]?.[0]?.category;
};
get category() {
return this.get(this._categorySelector);
}
get groupedNotifications() {
return this.get((s) => s.groupedNotifications);
}
groupedNotifications$ = this.select((s) => s.groupedNotifications);
category$ = this.select(this._categorySelector);
set groupedNotifications(groupedNotifications: Group<string, MessageBoardItemDTO>[]) {
this.patchState({ groupedNotifications });
get notifications() {
return this.get((s) => s.notifications);
}
activeNotifications$ = combineLatest([this.activeCard$, this.groupedNotifications$]).pipe(
map(([activeCard, notifications]) => notifications.find((n) => n.group === activeCard)?.items)
);
notifications$ = this.select((s) => s.notifications);
constructor(private _modalRef: UiModalRef<any, EnvelopeDTO<MessageBoardItemDTO[]>>) {
constructor(private _modalRef: UiModalRef<any, Record<string, MessageBoardItemDTO[]>>) {
super({
activeCard: undefined,
groupedNotifications: groupBy(_modalRef.data.data, (item: MessageBoardItemDTO) => item.category),
selectedArea: undefined,
notifications: _modalRef.data,
});
}
ngOnInit() {
this.patchState({ activeCard: this.groupedNotifications?.find((_) => true)?.group });
}
close() {
this._modalRef.close();
}
selectArea(area: string) {
this.patchState({
selectedArea: area,
});
}
}

View File

@@ -9,6 +9,7 @@ import { ModalNotificationsReservationGroupComponent } from './notifications-res
import { ModalNotificationsTaskCalendarGroupComponent } from './notifications-task-calendar-group/notifications-task-calendar-group.component';
import { ModalNotificationsUpdateGroupComponent } from './notifications-update-group/notifications-update-group.component';
import { ModalNotificationsComponent } from './notifications.component';
import { ModalNotificationsPackageInspectionGroupComponent } from './notifications-package-inspection-group/notifications-package-inspection-group.component';
@NgModule({
imports: [CommonModule, UiCommonModule, UiIconModule, RouterModule],
@@ -19,6 +20,7 @@ import { ModalNotificationsComponent } from './notifications.component';
ModalNotificationsTaskCalendarGroupComponent,
ModalNotificationsUpdateGroupComponent,
ModalNotificationsListItemComponent,
ModalNotificationsPackageInspectionGroupComponent,
],
exports: [
ModalNotificationsComponent,
@@ -27,6 +29,7 @@ import { ModalNotificationsComponent } from './notifications.component';
ModalNotificationsTaskCalendarGroupComponent,
ModalNotificationsUpdateGroupComponent,
ModalNotificationsListItemComponent,
ModalNotificationsPackageInspectionGroupComponent,
],
})
export class ModalNotificationsModule {}

View File

@@ -65,7 +65,7 @@
</div>
<div class="page-price-update-item__item-select-bullet">
<input *ngIf="isSelectable" [ngModel]="selected$ | async" (ngModelChange)="setSelected()" class="isa-select-bullet" type="checkbox" />
<input *ngIf="isSelectable" [ngModel]="selected" (ngModelChange)="setSelected()" class="isa-select-bullet" type="checkbox" />
</div>
<div class="page-price-update-item__item-stock flex flex-row font-bold">

View File

@@ -47,7 +47,7 @@ export class PriceUpdateItemComponent {
return this._store.isSelectable(this.item);
}
selected$ = this._store.selectedItemUids$.pipe(map((selectedItemUids) => selectedItemUids.includes(this.item?.uId)));
@Input() selected: boolean = false;
defaultBranch$ = this._availability.getDefaultBranch();

View File

@@ -34,6 +34,7 @@
<page-price-update-item
*cdkVirtualFor="let item of items; let first; trackBy: trackByFn"
[item]="item"
[selected]="isSelected(item)"
[class.mt-px-10]="!first"
></page-price-update-item>

View File

@@ -34,6 +34,10 @@ export class PriceUpdateListComponent {
return this._store.items.filter((item) => this._store.isSelectable(item)) ?? [];
}
isSelected(item: ProductListItemDTO) {
return this._store.selectedItemUids.includes(item.uId);
}
selectAll() {
const selectedItemUids = this.getSelectableItems().map((item) => item.uId);
this._store.patchState({ selectedItemUids });

View File

@@ -0,0 +1,3 @@
:host {
@apply text-[#0556B4];
}

View File

@@ -0,0 +1,3 @@
<a [routerLink]="route?.path" [queryParams]="route?.queryParams">
<ng-content></ng-content>
</a>

View File

@@ -0,0 +1,18 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'page-article-details-text-link',
templateUrl: 'article-details-text-link.component.html',
styleUrls: ['article-details-text-link.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-article-details-text-link' },
standalone: true,
imports: [RouterLink],
})
export class ArticleDetailsTextLinkComponent {
@Input()
route: { path: string[]; queryParams?: Record<string, string> };
constructor() {}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block whitespace-pre-line;
}

View File

@@ -0,0 +1,6 @@
<ng-container *ngFor="let line of lines">
<ng-container [ngSwitch]="line | lineType">
<page-article-details-text-link *ngSwitchCase="'reihe'" [route]="line | reiheRoute"> {{ line }} <br /> </page-article-details-text-link>
<ng-container *ngSwitchDefault> {{ line }} <br /> </ng-container>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,63 @@
import { Component, ChangeDetectionStrategy, Input, Renderer2, ElementRef, OnChanges, SimpleChanges, HostBinding } from '@angular/core';
import { TextDTO } from '@swagger/cat';
import { ArticleDetailsTextLinkComponent } from './article-details-text-link.component';
import { NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';
import { LineTypePipe } from './line-type.pipe';
import { ReiheRoutePipe } from './reihe-route.pipe';
const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/m;
@Component({
selector: 'page-article-details-text',
templateUrl: 'article-details-text.component.html',
styleUrls: ['article-details-text.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-article-details-text' },
standalone: true,
imports: [ArticleDetailsTextLinkComponent, NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault, LineTypePipe, ReiheRoutePipe],
})
export class ArticleDetailsTextComponent {
@Input()
text: TextDTO;
get lines() {
return this.text?.value?.split('\n');
}
constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
// ngOnChanges(changes: SimpleChanges): void {
// if (changes.text) {
// this.renderText();
// }
// }
// renderText() {
// console.log(this.getReihe());
// }
// getReihe() {
// REIHE_REGEX.exec(this.text?.value)?.forEach((match, groupIndex) => {
// if (groupIndex === 0) return;
// return match;
// });
// }
// @HostBinding('innerHTML')
// get htmlContent() {
// let content = this.text?.value;
// REIHE_REGEX.exec(content)?.forEach((match, groupIndex) => {
// if (groupIndex === 0) return;
// const aElement: HTMLAnchorElement = this.renderer.createElement('a');
// const text = this.renderer.createText(match);
// this.renderer.appendChild(aElement, text);
// this.renderer.setAttribute(aElement, 'href', `/search?query=${match}`);
// content = content.replace(match, aElement.outerHTML);
// });
// return content;
// }
}

View File

@@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'lineType',
standalone: true,
pure: true,
})
export class LineTypePipe implements PipeTransform {
transform(value: string, ...args: any[]): 'text' | 'reihe' {
const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/g;
const reihe = REIHE_REGEX.exec(value)?.[1];
return reihe ? 'reihe' : 'text';
}
}

View File

@@ -0,0 +1,69 @@
import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
import { ApplicationService } from '@core/application';
import { ProductCatalogNavigationService } from '@shared/services';
import { isEqual } from 'lodash';
import { Subscription, combineLatest, BehaviorSubject } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
@Pipe({
name: 'reiheRoute',
standalone: true,
pure: false,
})
export class ReiheRoutePipe implements PipeTransform, OnDestroy {
private subscription: Subscription;
value$ = new BehaviorSubject<string>('');
result: { path: string[]; queryParams?: Record<string, string> };
constructor(
private navigation: ProductCatalogNavigationService,
private application: ApplicationService,
private cdr: ChangeDetectorRef
) {
this.subscription = combineLatest([this.application.activatedProcessId$, this.value$])
.pipe(distinctUntilChanged(isEqual))
.subscribe(([processId, value]) => {
const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/g;
const reihe = REIHE_REGEX.exec(value)?.[1];
if (!reihe) {
this.result = null;
return;
}
const main_qs = reihe.split('/')[0];
const path = this.navigation.getArticleSearchResultsPath(processId);
this.result = {
path,
queryParams: {
main_qs,
main_serial: 'serial',
},
};
this.cdr.detectChanges();
});
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
this.value$?.unsubscribe();
}
transform(value: string, ...args: any[]) {
this.value$.next(value);
return this.result;
// const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/g;
// const reihe = REIHE_REGEX.exec(value)?.[1];
// this.navigation.getArticleSearchResultsPath(this.)
// return reihe ? `/search?query=${reihe}` : null;
}
}

View File

@@ -104,27 +104,15 @@
</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 class="page-article-details__product-price font-bold text-xl self-end" *ngIf="price$ | async; let price">
{{ price?.value?.value | currency: price?.value?.currency:'code' }}
</div>
<div *ngIf="price$ | async; let price" class="page-article-details__product-price-bound self-end">
{{ price?.vat?.vatType | vat: (priceMaintained$ | async) }}
</div>
<ng-template #retailPrice>
<div
class="page-article-details__product-price font-bold text-xl self-end"
*ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability"
>
{{ takeAwayAvailability?.retailPrice?.value?.value | currency: takeAwayAvailability?.retailPrice?.value?.currency:'code' }}
</div>
</ng-template>
<div class="page-article-details__product-points self-end" *ngIf="store.promotionPoints$ | async; let promotionPoints">
{{ promotionPoints }} Lesepunkte
</div>
<!-- TODO: Ticket PREISGEBUNDEN -->
<div class="page-article-details__product-price-bound self-end"></div>
</div>
<div class="page-article-details__product-origin-infos flex flex-col mb-4">
@@ -137,21 +125,23 @@
<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
<button
class="flex flex-row py-4 pl-4"
type="button"
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
(click)="showTooltip()"
*ngIf="!(store.fetchingTakeAwayAvailability$ | async)"
>
<ng-container *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
<ui-icon class="mr-2 mb-1" icon="home" size="15px"></ui-icon>
<span class="font-bold text-p3">{{ takeAwayAvailability.inStock || 0 }}x</span>
</ng-container>
</div>
</button>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-12" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
</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>
@@ -186,14 +176,14 @@
#uiOverlayTrigger="uiOverlayTrigger"
[uiOverlayTrigger]="orderDeadlineTooltip"
*ngIf="store.isPickUpAvailabilityAvailable$ | async"
class="page-article-details__product-pick-up-availability w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
class="page-article-details__product-pick-up-availability w-[2.25rem] h-[2.25rem] cursor-pointer bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
[class.tooltip-active]="uiOverlayTrigger.opened"
>
<shared-icon icon="isa-box-out" [size]="24"></shared-icon>
</div>
<ui-tooltip [warning]="true" yPosition="above" xPosition="after" [yOffset]="-12" #orderDeadlineTooltip [closeable]="true">
<b #orderDeadline>{{ (store.pickUpAvailability$ | async)?.orderDeadline | orderDeadline }}</b>
<b>{{ (store.pickUpAvailability$ | async)?.orderDeadline | orderDeadline }}</b>
</ui-tooltip>
</ng-template>
@@ -234,7 +224,7 @@
<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">
<div class="text-right" *ngIf="store.sscText$ | async; let sscText">
{{ sscText }}
</div>
</ng-container>
@@ -325,9 +315,7 @@
<hr class="bg-[#E6EFF9] border-t-2 my-3" />
<div #description class="page-article-details__product-description flex flex-col flex-grow" *ngIf="item.texts?.length > 0">
<div class="whitespace-pre-line">
{{ item.texts[0].value }}
</div>
<page-article-details-text [text]="item.texts[0]"> </page-article-details-text>
<button
class="font-bold flex flex-row text-[#0556B4] items-center mt-2"
@@ -392,12 +380,6 @@
</div>
</ng-container>
<div class="page-article-details__recommendations-overlay absolute top-16 rounded-t" @slideYAnimation *ngIf="showRecommendations">
<button
class="h-[3.75rem] shadow-[0_-2px_24px_0_#dce2e9] flex flex-row justify-center items-center w-full text-xl bg-white text-ucla-blue font-bold border-none outline-none rounded-t"
(click)="showRecommendations = false"
>
{{ (store.item$ | async)?.product?.name }}
</button>
<div class="page-article-details__recommendations-overlay absolute rounded-t" @slideYAnimation *ngIf="showRecommendations">
<page-article-recommendations (close)="showRecommendations = false"></page-article-recommendations>
</div>

View File

@@ -1,5 +1,5 @@
:host {
@apply box-border block h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
@apply box-border block h-split-screen-tablet desktop-small:h-split-screen-desktop;
}
.page-article-details__container {
@@ -15,8 +15,8 @@
'image contributors contributors contributors'
'image title title print'
'image title title .'
'image misc misc price'
'image misc misc price'
'image misc price price'
'image misc price price'
'image origin origin stock'
'image origin origin stock'
'image specs availabilities availabilities'

View File

@@ -19,10 +19,10 @@ import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { DatePipe } from '@angular/common';
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
import { DomainAvailabilityService } from '@domain/availability';
import { EnvironmentService } from '@core/environment';
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
import { DomainCheckoutService } from '@domain/checkout';
import { Store } from '@ngrx/store';
@Component({
selector: 'page-article-details',
@@ -97,34 +97,12 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
})
);
defaultBranch$ = this._availability.getDefaultBranch();
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
switchMap((processId) => this.applicationService.getSelectedBranch$(processId))
);
stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
map(([defaultBranch, selectedBranch]) => {
if (defaultBranch?.branchType === 4) {
if (!selectedBranch) {
return 'Wählen Sie eine Filiale aus, um den Bestand zu sehen.';
}
return 'Sie sehen den Bestand einer anderen Filiale.';
} else {
if (selectedBranch && defaultBranch.id !== selectedBranch?.id) {
return 'Sie sehen den Bestand einer anderen Filiale.';
}
}
return '';
}),
shareReplay(1)
);
get isTablet$() {
return this._environment.matchTablet$.pipe(
map((state) => state?.matches),
shareReplay()
);
return this._environment.matchTablet$;
}
get resultsPath() {
@@ -140,6 +118,59 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
return this.detailsContainer?.nativeElement;
}
stockTooltipText$ = combineLatest([this.store.defaultBranch$, this.selectedBranchId$]).pipe(
map(([defaultBranch, selectedBranch]) => {
if (defaultBranch?.branchType !== 4 && selectedBranch && defaultBranch.id !== selectedBranch?.id) {
return 'Sie sehen den Bestand einer anderen Filiale.';
}
return '';
})
);
priceMaintained$ = combineLatest([
this.store.item$,
this.store.takeAwayAvailability$,
this.store.deliveryAvailability$,
this.store.deliveryDigAvailability$,
this.store.deliveryB2BAvailability$,
]).pipe(
map(([item, takeAway, delivery, deliveryDig, deliveryB2B]) =>
[item, takeAway, delivery, deliveryDig, deliveryB2B].some((i) => i?.priceMaintained)
)
);
price$ = combineLatest([
this.store.item$,
this.store.takeAwayAvailability$,
this.store.deliveryAvailability$,
this.store.deliveryDigAvailability$,
this.store.deliveryB2BAvailability$,
]).pipe(
map(([item, takeAway, delivery, deliveryDig, deliveryB2B]) => {
if (item?.catalogAvailability?.price?.value?.value) {
return item?.catalogAvailability?.price;
}
if (takeAway?.price?.value?.value) {
return takeAway.price;
}
if (delivery?.price?.value?.value) {
return delivery.price;
}
if (deliveryDig?.price?.value?.value) {
return deliveryDig.price;
}
if (deliveryB2B?.price?.value?.value) {
return deliveryB2B.price;
}
return null;
})
);
constructor(
public readonly applicationService: ApplicationService,
private activatedRoute: ActivatedRoute,
@@ -151,12 +182,12 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
private _dateAdapter: DateAdapter,
private _datePipe: DatePipe,
private _purchaseOptionsModalService: PurchaseOptionsModalService,
private _availability: DomainAvailabilityService,
private _navigationService: ProductCatalogNavigationService,
private _checkoutNavigationService: CheckoutNavigationService,
private _environment: EnvironmentService,
private _router: Router,
private _domainCheckoutService: DomainCheckoutService
private _domainCheckoutService: DomainCheckoutService,
private _store: Store
) {}
ngOnInit() {
@@ -197,6 +228,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
this.subscriptions.add(processIdSubscription);
this.subscriptions.add(more$);
this.subscriptions.add(this.store.loadDefaultBranch());
this.subscriptions.add(this.store.loadItemById(id$));
this.subscriptions.add(this.store.loadItemByEan(ean$));
this.subscriptions.add(
@@ -237,6 +269,14 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
});
}
async showTooltip() {
const text = await this.stockTooltipText$.pipe(first()).toPromise();
if (!text) {
// Show Tooltip attached to branch selector dropdown
this._store.dispatch({ type: 'OPEN_TOOLTIP_NO_BRANCH_SELECTED' });
}
}
async updateBreadcrumbDesktop(item: ItemDTO) {
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details'])

View File

@@ -12,6 +12,7 @@ import { UiTooltipModule } from '@ui/tooltip';
import { UiCommonModule } from '@ui/common';
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
import { IconModule } from '@shared/components/icon';
import { ArticleDetailsTextComponent } from './article-details-text/article-details-text.component';
@NgModule({
imports: [
@@ -26,6 +27,7 @@ import { IconModule } from '@shared/components/icon';
IconModule,
PipesModule,
OrderDeadlinePipeModule,
ArticleDetailsTextComponent,
],
exports: [ArticleDetailsComponent, ArticleRecommendationsComponent],
declarations: [ArticleDetailsComponent, ArticleRecommendationsComponent],

View File

@@ -61,8 +61,12 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
return this.get((s) => s.branch);
}
get defaultBranch$() {
return this.domainAvailabilityService.getDefaultBranch();
}
readonly branch$ = this.select((s) => s.branch).pipe(
withLatestFrom(this.domainAvailabilityService.getDefaultBranch()),
withLatestFrom(this.defaultBranch$),
map(([selectedBranch, defaultBranch]) => selectedBranch ?? defaultBranch)
);
@@ -267,9 +271,8 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
this.deliveryB2BAvailability$,
this.downloadAvailability$,
]).pipe(
map(([item, isDownload, pickupAvailability, deliveryDigAvailability, deliveryB2BAvailability, downloadAvailability]) => {
// const availability = isDownload ? downloadAvailability : pickupAvailability || deliveryDigAvailability || deliveryB2BAvailability;
withLatestFrom(this.domainAvailabilityService.sscs$),
map(([[item, isDownload, pickupAvailability, deliveryDigAvailability, deliveryB2BAvailability, downloadAvailability], sscs]) => {
let availability: AvailabilityDTO;
if (isDownload) {
@@ -284,15 +287,30 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
}
}
let ssc = '';
let sscText = 'Keine Lieferanten vorhanden';
if (item?.catalogAvailability?.supplier === 'S' && !isDownload) {
return [item?.catalogAvailability?.ssc, item?.catalogAvailability?.sscText].filter((f) => !!f).join(' - ');
ssc = item?.catalogAvailability?.ssc;
sscText = item?.catalogAvailability?.sscText;
return [ssc, sscText].filter((f) => !!f).join(' - ');
}
if (availability?.ssc || availability?.sscText) {
return [availability?.ssc, availability?.sscText].filter((f) => !!f).join(' - ');
ssc = availability?.ssc;
sscText = availability?.sscText;
const sscExists = !!sscs?.find((ssc) => !!item?.id && ssc?.itemId === item.id);
const sscEqualsCatalogSsc = ssc === item.catalogAvailability.ssc && sscText === item.catalogAvailability.sscText;
// To keep result list in sync with details page
if (!sscExists && !sscEqualsCatalogSsc) {
this.domainAvailabilityService.sscs$.next([...sscs, { itemId: item?.id, ssc, sscText }]);
}
}
return 'Keine Lieferanten vorhanden';
return [ssc, sscText].filter((f) => !!f).join(' - ');
})
);
@@ -349,6 +367,24 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
)
);
readonly loadDefaultBranch = this.effect(($) =>
$.pipe(
switchMap(() => this.domainAvailabilityService.getDefaultBranch()),
withLatestFrom(this.branch$),
tapResponse<[BranchDTO, BranchDTO]>(
([defaultBranch, selectedBranch]) => this.patchState({ branch: selectedBranch ?? defaultBranch }),
async (err) => {
this.patchState({ branch: undefined });
const errorModalRef = this._modal.open({
content: UiErrorModalComponent,
title: 'Fehler beim Laden der Filiale',
});
await errorModalRef.afterClosed$.toPromise();
}
)
)
);
setProcessId = this.updater((s, processId: number) => {
return {
...s,

View File

@@ -1,4 +1,10 @@
<ng-container *ngIf="store.item$ | async; let item">
<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-[#0556B4] font-bold border-none outline-none rounded-t"
(click)="close.emit()"
>
{{ item?.product?.name }}
</button>
<h1>Empfehlungen für Sie</h1>
<p>Neben dem Titel "{{ item.product?.name }}" gibt es noch andere Artikel, die Sie interessieren könnten.</p>

View File

@@ -1,5 +1,5 @@
:host {
@apply flex flex-col bg-white h-[calc(100vh-16.5rem-3.75rem)] desktop-small:h-[calc(100vh-15.1rem-3.75rem)];
@apply flex flex-col bg-white h-split-screen-tablet desktop-small:h-split-screen-desktop;
box-shadow: 0px -2px 24px 0px #dce2e9;
}

View File

@@ -1,3 +1,3 @@
:host {
@apply flex flex-col w-full h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)] box-content relative;
@apply flex flex-col w-full h-split-screen-tablet desktop-small:h-split-screen-desktop box-content relative;
}

View File

@@ -52,11 +52,11 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
this._articleSearch.searchCompleted
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
.subscribe(([state, processId]) => {
if (state.searchState === '') {
const params = state.filter.getQueryParams();
if (state.hits === 1) {
const item = state.items.find((f) => f);
.subscribe(([searchCompleted, processId]) => {
if (searchCompleted.state.searchState === '') {
const params = searchCompleted.state.filter.getQueryParams();
if (searchCompleted.state.hits === 1) {
const item = searchCompleted.state.items.find((f) => f);
this._navigationService.navigateToDetails({
processId,
itemId: item.id,

View File

@@ -1,11 +1,10 @@
import { Injectable } from '@angular/core';
import { DomainCatalogService } from '@domain/catalog';
import { Observable, Subject } from 'rxjs';
import { debounceTime, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ItemDTO, QueryTokenDTO, UISettingsDTO } from '@swagger/cat';
import { ApplicationService } from '@core/application';
import { BranchDTO } from '@swagger/checkout';
import { Filter } from 'apps/shared/components/filter/src/lib';
@@ -17,6 +16,7 @@ export interface ArticleSearchState {
hits: number;
selectedBranch: BranchDTO;
selectedItemIds: number[];
scrollPosition: number;
defaultSettings?: UISettingsDTO;
}
@@ -44,6 +44,12 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
return this.get((s) => s.items);
}
scrollPosition$ = this.select((s) => s.scrollPosition);
get scrollPosition() {
return this.get((s) => s.scrollPosition);
}
selectedBranch$ = this.select((s) => s.selectedBranch);
get selectedBranch() {
@@ -61,7 +67,7 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
searchStarted = new Subject<{ clear?: boolean; reload?: boolean }>();
searchCompleted = new Subject<ArticleSearchState>();
searchCompleted = new Subject<{ state: ArticleSearchState; clear: boolean; orderBy: boolean }>();
searchboxHint$ = this.select((s) => (s.searchState === 'empty' ? 'Keine Suchergebnisse' : undefined));
@@ -77,7 +83,7 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
return this.get((s) => s.hits);
}
constructor(private catalog: DomainCatalogService, private _appService: ApplicationService) {
constructor(private catalog: DomainCatalogService) {
super({
filter: undefined,
hits: 0,
@@ -86,6 +92,7 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
searchState: '',
selectedItemIds: [],
selectedBranch: undefined,
scrollPosition: 0,
});
this.setDefaultFilter();
}
@@ -106,6 +113,10 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
this.patchState({ selectedBranch });
}
setScrollPosition(scrollPosition: number) {
this.patchState({ scrollPosition });
}
async setDefaultFilter(defaultQueryParams?: Record<string, string>) {
const defaultSettings = await this.catalog.getSettings().toPromise();
@@ -149,7 +160,7 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
}
}
search = this.effect((options$: Observable<{ clear?: boolean }>) =>
search = this.effect((options$: Observable<{ clear?: boolean; orderBy?: boolean }>) =>
options$.pipe(
tap((options) => {
this.searchStarted.next({ clear: options?.clear });
@@ -185,11 +196,11 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
searchState,
});
}
this.searchCompleted.next(this.get());
this.searchCompleted.next({ state: this.get(), clear: options?.clear, orderBy: options?.orderBy });
},
(err) => {
this.patchState({ hits: 0, searchState: 'error', items: [] });
this.searchCompleted.next(this.get());
this.searchCompleted.next({ state: this.get(), clear: options?.clear, orderBy: options?.orderBy });
}
)
);

View File

@@ -12,7 +12,7 @@
</a>
</div>
<div class="catalog-search-filter-content-main -mt-14 desktop-small:-mt-8 desktop:-mt-12">
<div class="catalog-search-filter-content-main -mt-14 desktop-small:-mt-8 desktop-large:-mt-12">
<h1 class="text-h3 text-[1.625rem] font-bold text-center pt-6 pb-10">Filter</h1>
<shared-filter
[filter]="filter"
@@ -28,7 +28,7 @@
Filter zurücksetzen
</button>
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="fetching$ | async">
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="(fetching$ | async) || !hasSelectedOptions(filter)">
<ui-spinner [show]="fetching$ | async">
Filter anwenden
</ui-spinner>

View File

@@ -1,5 +1,5 @@
:host {
@apply block bg-white h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
@apply block bg-white h-split-screen-tablet desktop-small:h-split-screen-desktop;
}
.catalog-search-filter-content {
@@ -13,7 +13,7 @@
}
.cta-wrapper {
@apply text-center whitespace-nowrap absolute bottom-8 left-0 w-full;
@apply text-center whitespace-nowrap absolute bottom-8 left-1/2 -translate-x-1/2;
}
.cta-reset-filter,
@@ -21,7 +21,7 @@
@apply text-lg font-bold px-6 py-[0.85rem] rounded-full border-solid border-2 border-brand outline-none mx-2;
&:disabled {
@apply bg-inactive-branch cursor-not-allowed border-none text-white;
@apply bg-inactive-branch border-inactive-branch cursor-not-allowed text-white;
}
}
@@ -34,5 +34,5 @@
}
::ng-deep page-article-search-filter shared-filter shared-filter-input-group-main {
@apply desktop:hidden px-16;
@apply desktop-large:hidden px-16;
}

View File

@@ -6,7 +6,7 @@ import { map, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { ActivatedRoute } from '@angular/router';
import { ProductCatalogNavigationService } from '@shared/services';
import { Filter, FilterComponent } from 'apps/shared/components/filter/src/lib';
import { Filter, FilterComponent, FilterInput } from 'apps/shared/components/filter/src/lib';
@Component({
selector: 'page-article-search-filter',
@@ -17,9 +17,9 @@ import { Filter, FilterComponent } from 'apps/shared/components/filter/src/lib';
export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
_processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId)));
fetching$: Observable<boolean>;
fetching$: Observable<boolean> = this.articleSearch.fetching$;
filter$: Observable<Filter>;
filter$: Observable<Filter> = this.articleSearch.filter$;
searchboxHint$ = this.articleSearch.searchboxHint$;
@@ -35,7 +35,7 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
}
get showFilterClose$() {
return this._environment.matchDesktop$.pipe(map((state) => !(state?.matches && this.leftOutlet === 'search')));
return this._environment.matchDesktopLarge$.pipe(map((matches) => !(matches && this.leftOutlet === 'search')));
}
get leftOutlet() {
@@ -45,7 +45,6 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
get mainOutlet() {
return this._navigationService.getOutletLocations(this._activatedRoute)?.main;
}
get closeFilterRoute() {
const processId = Number(this._activatedRoute?.parent?.snapshot?.data?.processId);
const itemId = this._navigationService.getOutletParams(this._activatedRoute)?.right?.id;
@@ -71,8 +70,9 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
) {}
ngOnInit() {
this.fetching$ = this.articleSearch.fetching$;
this.filter$ = this.articleSearch.filter$.pipe(map((filter) => Filter.create(filter)));
this._activatedRoute.queryParams
.pipe(takeUntil(this._onDestroy$))
.subscribe(async (queryParams) => await this.articleSearch.setDefaultFilter(queryParams));
// #4143 To make Splitscreen Search and Filter work combined
this.articleSearch.searchStarted.pipe(takeUntil(this._onDestroy$)).subscribe(async (_) => {
@@ -100,17 +100,17 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
this.articleSearch.search({ clear: true });
this.articleSearch.searchCompleted
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
.subscribe(([state, processId]) => {
if (state.searchState === '') {
.subscribe(([searchCompleted, processId]) => {
if (searchCompleted.state.searchState === '') {
// Check if desktop is necessary, otherwise it would trigger navigation twice (Inside Article-Search.component and here)
if (state.hits === 1 && !this.isDesktop) {
const item = state.items.find((f) => f);
if (searchCompleted.state.hits === 1 && !this.isDesktop) {
const item = searchCompleted.state.items.find((f) => f);
this._navigationService.navigateToDetails({
processId,
itemId: item.id,
});
} else if (!this.isDesktop) {
const params = state.filter.getQueryParams();
const params = searchCompleted.state.filter.getQueryParams();
this._navigationService.navigateToResults({
processId,
queryParams: params,
@@ -121,7 +121,16 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
}
clearFilter(value: Filter) {
value.unselectAll();
value.unselectAllFilterOptions();
}
hasSelectedOptions(filter: Filter) {
// Is Query available
const hasInputOptions = !!filter.input.find((input) => !!input.input.find((fi) => fi.hasFilterInputOptionsSelected()));
// Are filter or filterChips selected
const hasFilterOptions = !!filter.filter.find((input) => !!input.input.find((fi) => fi.hasFilterInputOptionsSelected()));
return hasInputOptions || hasFilterOptions;
}
resetFilter(value: Filter) {

View File

@@ -1,6 +1,4 @@
<div
class="bg-white rounded py-10 px-4 text-center shadow-[0_-2px_24px_0_#dce2e9] h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)]"
>
<div class="bg-white rounded py-10 px-4 text-center shadow-[0_-2px_24px_0_#dce2e9] h-full">
<h1 class="text-h3 text-[1.625rem] font-bold mb-[0.375rem]">Artikelsuche</h1>
<p class="text-lg mb-10">
Welchen Artikel suchen Sie?
@@ -11,9 +9,9 @@
*ngIf="!(isDesktop$ | async)"
[inputGroup]="filter?.filter | group: 'main'"
></shared-filter-filter-group-main>
<div class="flex flex-row px-12 justify-center desktop:px-0">
<div class="flex flex-row px-12 justify-center desktop-large:px-0">
<shared-filter-input-group-main
class="block w-full mr-3 desktop:mx-auto"
class="block w-full mr-3 desktop-large:mx-auto"
[hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
@@ -33,7 +31,7 @@
</a>
</div>
<div class="flex flex-col items-start ml-12 desktop:ml-8 py-6 bg-white overflow-hidden h-[calc(100%-13.5rem)]">
<div class="flex flex-col items-start ml-12 desktop-large:ml-8 py-6 bg-white overflow-hidden h-[calc(100%-13.5rem)]">
<h3 class="text-p3 font-bold mb-3">Deine letzten Suchanfragen</h3>
<ul class="flex flex-col justify-start overflow-hidden overflow-y-scroll items-start m-0 p-0 bg-white w-full">
<li class="list-none pb-3" *ngFor="let recentQuery of history$ | async">

View File

@@ -1,5 +1,5 @@
:host {
@apply flex flex-col box-border h-full;
@apply flex flex-col box-border h-split-screen-tablet desktop-small:h-split-screen-desktop;
}
.page-search-main__filter {

View File

@@ -39,10 +39,7 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
sharedFilterInputGroupMain: FilterInputGroupMainComponent;
get isDesktop$() {
return this._environment.matchDesktop$.pipe(
map((state) => state?.matches),
shareReplay()
);
return this._environment.matchDesktopLarge$;
}
get leftOutlet() {

View File

@@ -74,40 +74,36 @@
<input
*ngIf="selectable"
(click)="$event.stopPropagation()"
[ngModel]="selected$ | async"
[ngModel]="selected"
(ngModelChange)="setSelected()"
class="isa-select-bullet"
type="checkbox"
/>
</div>
<div
class="page-search-result-item__item-stock desktop-small:text-p3 font-bold z-dropdown justify-self-start"
<button
class="page-search-result-item__item-stock desktop-small:text-p3 font-bold z-dropdown justify-self-start flex flex-row items-center justify-center"
[class.justify-self-end]="!mainOutletActive"
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
type="button"
(click)="$event.stopPropagation(); $event.preventDefault(); showTooltip()"
>
<ui-icon class="mr-[0.125rem] -mt-[0.275rem]" icon="home" size="1rem"></ui-icon>
<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>
<span
*ngIf="inStock$ | async; let stock"
[class.skeleton]="stock.inStock === undefined"
class="min-w-[0.75rem] text-right inline-block"
>{{ stock?.inStock }}</span
>
</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>
<span class="min-w-[1rem] text-center inline-block">-</span>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
<span>x</span>
</button>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-12" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
@@ -115,10 +111,10 @@
class="page-search-result-item__item-ssc desktop-small:text-p3 w-full text-right overflow-hidden text-ellipsis whitespace-nowrap"
[class.page-search-result-item__item-ssc-main]="mainOutletActive"
>
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive">
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
</div>
<strong>{{ item?.catalogAvailability?.ssc }}</strong> - {{ item?.catalogAvailability?.sscText }}
<ng-container *ngIf="ssc$ | async; let ssc">
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive">{{ ssc?.ssc }} - {{ ssc?.sscText }}</div>
<strong>{{ ssc?.ssc }}</strong> - {{ ssc?.sscText }}
</ng-container>
</div>
</div>
</a>

View File

@@ -8,9 +8,10 @@ import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { isEqual } from 'lodash';
import { combineLatest } from 'rxjs';
import { debounceTime, switchMap, map, shareReplay, filter } from 'rxjs/operators';
import { debounceTime, switchMap, map, filter, first } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { ProductCatalogNavigationService } from '@shared/services';
import { Store } from '@ngrx/store';
export interface SearchResultItemComponentState {
item?: ItemDTO;
@@ -38,7 +39,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
readonly item$ = this.select((s) => s.item);
selected$ = this._articleSearchService.selectedItemIds$.pipe(map((selectedItemIds) => selectedItemIds.includes(this.item?.id)));
@Input() selected: boolean = false;
@Input()
get selectable() {
@@ -104,15 +105,8 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
map(([defaultBranch, selectedBranch]) => {
if (defaultBranch?.branchType === 4) {
if (!selectedBranch) {
return 'Wählen Sie eine Filiale aus, um den Bestand zu sehen.';
}
if (defaultBranch?.branchType !== 4 && selectedBranch && defaultBranch.id !== selectedBranch?.id) {
return 'Sie sehen den Bestand einer anderen Filiale.';
} else {
if (selectedBranch && defaultBranch.id !== selectedBranch?.id) {
return 'Sie sehen den Bestand einer anderen Filiale.';
}
}
return '';
})
@@ -126,6 +120,17 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
)
);
ssc$ = this._availability.sscsObs$.pipe(
debounceTime(100),
map((sscs) => {
const updatedSsc = sscs?.find((ssc) => this.item?.id === ssc?.itemId);
return {
ssc: updatedSsc?.ssc ?? this.item?.catalogAvailability?.ssc,
sscText: updatedSsc?.sscText ?? this.item?.catalogAvailability?.sscText,
};
})
);
constructor(
private _dateAdapter: DateAdapter,
private _datePipe: DatePipe,
@@ -135,7 +140,8 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
private _availability: DomainAvailabilityService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService,
private _elRef: ElementRef<HTMLElement>
private _elRef: ElementRef<HTMLElement>,
private _store: Store
) {
super({
selected: false,
@@ -157,6 +163,14 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
// }
}
async showTooltip() {
const text = await this.stockTooltipText$.pipe(first()).toPromise();
if (!text) {
// Show Tooltip attached to branch selector dropdown
this._store.dispatch({ type: 'OPEN_TOOLTIP_NO_BRANCH_SELECTED' });
}
}
@HostBinding('style') get class() {
return this.mainOutletActive ? { height: '6.125rem' } : '';
}

View File

@@ -3,15 +3,15 @@
[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)">
<div class="flex flex-row w-full desktop-small:w-min" [class.desktop-large: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)"
[class.desktop-large:w-full]="!(mainOutletActive$ | async)"
[hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search(filter)"
(search)="search({filter, clear: true})"
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
@@ -40,9 +40,8 @@
<div class="page-search-results__order-by" [class.page-search-results__order-by-main]="mainOutletActive$ | async">
<shared-order-by-filter
[groupBy]="(mainOutletActive$ | async) ? [2, 2, 2] : []"
[orderBy]="(filter$ | async)?.orderBy"
(selectedOrderByChange)="search(); updateBreadcrumbs()"
(selectedOrderByChange)="search({ clear: true, orderBy: true }); updateBreadcrumbs()"
>
</shared-order-by-filter>
</div>
@@ -61,6 +60,7 @@
[class.page-search-results__result-item-main]="mainOutletActive$ | async"
*cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId"
(selectedChange)="addToCart($event)"
[selected]="isSelected(item)"
[selectable]="isSelectable(item)"
[item]="item"
[mainOutletActive]="mainOutletActive$ | async"
@@ -81,6 +81,3 @@
</button>
</div>
</div>
<!-- #4135 Code auskommentiert bis zur Klärung -->
<!-- <div *ngIf="isTablet" class="actions z-fixed"> -->

View File

@@ -1,5 +1,5 @@
:host {
@apply box-border grid h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
@apply box-border grid h-split-screen-tablet desktop-small:h-split-screen-desktop;
grid-template-rows: auto auto 1fr;
}
@@ -16,11 +16,7 @@
}
.page-search-results__order-by {
@apply bg-white rounded px-6 desktop-small:px-8;
}
.page-search-results__order-by-main {
@apply pl-[4.9375rem] px-4;
@apply bg-white rounded px-4;
}
.page-search-results__filter {
@@ -30,10 +26,10 @@
}
.actions {
@apply flex sticky bottom-10 items-center justify-center;
@apply flex sticky bottom-12 items-center justify-center;
.cta-cart {
@apply border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap no-underline;
@apply border-2 border-solid border-brand rounded-full py-4 px-6 font-bold text-lg outline-none self-end whitespace-nowrap no-underline;
&:disabled {
@apply bg-inactive-branch border-inactive-branch text-white;
@@ -47,11 +43,6 @@
::ng-deep page-search-results .page-search-results__order-by-main shared-order-by-filter {
@apply grid grid-flow-col justify-items-start gap-x-4 justify-start;
grid-template-columns: 37.5% 32.5% 20% auto;
.group {
@apply desktop-small:justify-start;
}
.order-by-filter-button {
@apply ml-0 mr-7;

View File

@@ -27,6 +27,7 @@ import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-m
import { SearchResultItemComponent } from './search-result-item.component';
import { ProductCatalogNavigationService } from '@shared/services';
import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/filter/src/lib';
import { DomainAvailabilityService, ItemData } from '@domain/availability';
@Component({
selector: 'page-search-results',
@@ -88,7 +89,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
}
get mainOutletActive$() {
return this._environment.matchTablet$.pipe(map((state) => this._navigationService.mainOutletActive(this.route, state?.matches)));
return this._environment.matchTablet$.pipe(map((matches) => this._navigationService.mainOutletActive(this.route, matches)));
}
get rightOutletLocation() {
@@ -102,7 +103,8 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
map(([results, mainOutlet]) => {
if (!mainOutlet && results?.length > 0) {
// Splitscreen mode: Items Length * Item Pixel Height
return results.length * 181;
const maxBufferSize = results.length * 181;
return maxBufferSize >= 1200 ? maxBufferSize : 1200;
} else {
return 1200;
}
@@ -118,7 +120,8 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
private _uiModal: UiModalService,
private _checkoutService: DomainCheckoutService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService
private _navigationService: ProductCatalogNavigationService,
private _availability: DomainAvailabilityService
) {}
ngOnInit() {
@@ -157,17 +160,20 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
const cleanQueryParams = this.cleanupQueryParams(queryParams);
// Scroll to scroll_position in great result list
if (!!queryParams?.scroll_position && this._navigationService.mainOutletActive(this.route)) {
this.scrollTop(Number(queryParams.scroll_position ?? 0));
}
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
if (this.rightOutletLocation !== 'filter') {
await this.searchService.setDefaultFilter(queryParams);
}
await this.searchService.setDefaultFilter(queryParams);
const data = this.getCachedData(processId, queryParams, selectedBranch?.id);
if (data.items?.length > 0) {
this.searchService.setItems(data.items);
this.searchService.setHits(data.hits);
}
if (data.items?.length === 0 && this.rightOutletLocation !== 'filter') {
this.search();
this.search({ clear: true });
} else {
if (!this.isDesktop || this._navigationService.mainOutletActive(this.route)) {
this.scrollTop(Number(queryParams.scroll_position ?? 0));
@@ -189,43 +195,52 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
await this.createBreadcrumb(processId, queryParams);
}
if (this.isTablet || this.route?.outlet === 'main') {
if (!this.isDesktop || this.route?.outlet === 'main') {
await this.removeDetailsBreadcrumb(processId);
}
})
);
this.subscriptions.add(
this.searchService.searchCompleted.pipe(withLatestFrom(this.application.activatedProcessId$)).subscribe(([state, processId]) => {
if (state.searchState === '') {
const params = state.filter.getQueryParams();
if ((state.hits === 1 && this.isTablet) || (!this.isTablet && !this._navigationService.mainOutletActive(this.route))) {
const item = state.items.find((f) => f);
const ean = this.route?.snapshot?.params?.ean;
const itemId = this.route?.snapshot?.params?.id ? Number(this.route?.snapshot?.params?.id) : item.id; // Nicht zum ersten Item der Liste springen wenn bereits eines selektiert ist
this.searchService.searchCompleted
.pipe(withLatestFrom(this.application.activatedProcessId$))
.subscribe(async ([searchCompleted, processId]) => {
const params = searchCompleted.state.filter.getQueryParams();
if (searchCompleted.state.searchState === '') {
// Keine Navigation bei OrderBy
if (searchCompleted?.orderBy) {
return;
}
// Navigation from Cart uses ean
if (!!ean) {
this._navigationService.navigateToDetails({
// Navigation auf Details bzw. Results | Details wenn hits 1
// Navigation auf Results bei clear search, oder immer auf Tablet oder wenn große Resultliste aktiv ist
if (searchCompleted.state.hits === 1) {
const item = searchCompleted.state.items.find((f) => f);
const ean = this.route?.snapshot?.params?.ean;
const itemId = this.route?.snapshot?.params?.id ? Number(this.route?.snapshot?.params?.id) : item.id; // Nicht zum ersten Item der Liste springen wenn bereits eines selektiert ist
// Navigation from Cart uses ean
if (!!ean) {
await this._navigationService.navigateToDetails({
processId,
ean,
queryParams: this.isTablet ? undefined : params,
});
} else {
await this._navigationService.navigateToDetails({
processId,
itemId,
queryParams: this.isTablet ? undefined : params,
});
}
} else if (searchCompleted?.clear || this.isTablet || this._navigationService.mainOutletActive(this.route)) {
await this._navigationService.navigateToResults({
processId,
ean,
queryParams: this.isTablet ? undefined : params,
});
} else {
this._navigationService.navigateToDetails({
processId,
itemId,
queryParams: this.isTablet ? undefined : params,
queryParams: params,
});
}
} else if (this.isTablet || this._navigationService.mainOutletActive(this.route)) {
this._navigationService.navigateToResults({
processId,
queryParams: params,
});
}
}
})
})
);
// #4143 To make Splitscreen Search and Filter work combined
@@ -247,11 +262,12 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
this.scrollItemIntoView();
}
ngOnDestroy() {
async ngOnDestroy() {
this.subscriptions?.unsubscribe();
this.cacheCurrentData(this.searchService.processId, this.searchService.filter.getQueryParams(), this.searchService?.selectedBranch?.id);
this.updateBreadcrumbs(this.searchService.processId, this.searchService.filter.getQueryParams());
await this.updateBreadcrumbs(this.searchService.processId, this.searchService.filter.getQueryParams());
this.unselectAll();
}
@@ -269,12 +285,12 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
return clean;
}
search(filter?: Filter) {
search({ filter, clear = false, orderBy = false }: { filter?: Filter; clear?: boolean; orderBy?: boolean }) {
if (!!filter) {
this.sharedFilterInputGroupMain.cancelAutocomplete();
}
this.searchService.search({ clear: true });
this.searchService.search({ clear, orderBy });
}
scrollTop(scrollPos: number) {
@@ -296,8 +312,12 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
return;
}
if (!this.isDesktop || this._navigationService.mainOutletActive(this.route)) {
this.searchService.setScrollPosition(this.scrollContainer.measureScrollOffset('top'));
}
if (index >= results.length - 20 && results.length - 20 > 0) {
this.searchService.search({ clear: false });
this.search({ clear: false });
}
}
@@ -305,7 +325,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
processId: number = this.searchService.processId,
queryParams: Record<string, string> = this.searchService.filter?.getQueryParams()
) {
const scroll_position = this.scrollContainer.measureScrollOffset('top');
const scroll_position = this.searchService.scrollPosition;
const selected_item_ids = this.searchService?.selectedItemIds?.toString();
if (queryParams) {
@@ -390,6 +410,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
return !(isArchiv || isFortsetzung);
}
isSelected(item: ItemDTO) {
return this.searchService.selectedItemIds.includes(item.id);
}
unselectAll() {
this.listItems.forEach((listItem) => this.searchService.setSelected({ selected: false, itemId: listItem.item.id }));
this.searchService.patchState({ selectedItemIds: [] });
@@ -431,10 +455,31 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
const canAddItemsPayload = [];
for (const item of selectedItems) {
const shoppingCartItem = this._createShoppingCartItem(item);
const isDownload = item?.product?.format === 'EB' || item?.product?.format === 'DL';
const price = item?.catalogAvailability?.price;
const shoppingCartItem: AddToShoppingCartDTO = {
quantity: 1,
availability: {
availabilityType: item?.catalogAvailability?.status,
price,
supplierProductNumber: item?.ids?.dig ? String(item.ids?.dig) : item?.product?.supplierProductNumber,
},
product: {
catalogProductNumber: String(item?.id),
...item?.product,
},
itemType: item.type,
promotion: { points: item?.promoPoints },
};
if (isDownload) {
shoppingCartItem.destination = { data: { target: 16 } };
const itemId = item?.id;
const ean = item?.product?.ean;
const downloadItem: ItemData = { ean, itemId, price };
// #4180 Für Download Artikel muss hier immer zwingend der logistician gesetzt werden, da diese Artikel direkt zugeordnet dem Warenkorb hinzugefügt werden
const downloadAvailability = await this._availability.getDownloadAvailability({ item: downloadItem }).pipe(first()).toPromise();
shoppingCartItem.destination = { data: { target: 16, logistician: downloadAvailability?.logistician } };
canAddItemsPayload.push({
availabilities: [{ ...item.catalogAvailability, format: 'DL' }],
id: item.product.catalogProductNumber,

View File

@@ -1,5 +1,7 @@
<shared-breadcrumb class="my-4" [key]="activatedProcessId$ | async" [tags]="['catalog']">
<shared-breadcrumb class="mb-5 desktop-small:mb-9" [key]="activatedProcessId$ | async" [tags]="['catalog']">
<shared-branch-selector
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="stockTooltipDisabled"
[filterCurrentBranch]="!!auth.hasRole('Store')"
[orderBy]="auth.hasRole('Store') ? 'distance' : 'name'"
[branchType]="1"
@@ -7,6 +9,9 @@
(valueChange)="patchProcessData($event)"
>
</shared-branch-selector>
<ui-tooltip #tooltip yPosition="below" xPosition="after" [xOffset]="-263" [yOffset]="4" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
</shared-breadcrumb>
<ng-container *ngIf="routerEvents$ | async">
@@ -19,7 +24,7 @@
<router-outlet name="main"></router-outlet>
</ng-container>
<div class="grid grid-cols-[minmax(31rem,.5fr)_1fr] gap-6">
<div class="grid grid-cols-split-screen gap-split-screen">
<div *ngIf="showLeftOutlet$ | async" class="block">
<router-outlet name="left"></router-outlet>
</div>

View File

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

View File

@@ -6,9 +6,12 @@ import { EnvironmentService } from '@core/environment';
import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { BreadcrumbComponent } from '@shared/components/breadcrumb';
import { BranchDTO } from '@swagger/checkout';
import { UiOverlayTriggerDirective } from '@ui/common';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { fromEvent, Observable, Subject } from 'rxjs';
import { combineLatest, fromEvent, Observable, Subject } from 'rxjs';
import { first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ActionsSubject } from '@ngrx/store';
import { DomainAvailabilityService } from '@domain/availability';
@Component({
selector: 'page-catalog',
@@ -23,6 +26,8 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
activatedProcessId$: Observable<string>;
selectedBranch$: Observable<BranchDTO>;
@ViewChild(UiOverlayTriggerDirective) branchInputNoBranchSelectedTrigger: UiOverlayTriggerDirective;
get branchSelectorWidth() {
return `${this.breadcrumbRef?.nativeElement?.clientWidth}px`;
}
@@ -30,19 +35,11 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
_onDestroy$ = new Subject<boolean>();
get isTablet$() {
return this._environmentService.matchTablet$.pipe(
map((state) => state.matches),
shareReplay()
);
return this._environmentService.matchTablet$;
}
get isDesktop$() {
return this._environmentService.matchDesktop$.pipe(
map((state) => {
return state.matches;
}),
shareReplay()
);
return this._environmentService.matchDesktopLarge$;
}
routerEvents$ = this._router.events.pipe(shareReplay());
@@ -53,23 +50,52 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
showRightOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'right')));
defaultBranch$ = this._availability.getDefaultBranch();
stockTooltipText$: Observable<string>;
stockTooltipDisabled$: Observable<boolean>;
get stockTooltipDisabled() {
return this.branchInputNoBranchSelectedTrigger?.opened ? false : true;
}
constructor(
public application: ApplicationService,
private _availability: DomainAvailabilityService,
private _uiModal: UiModalService,
public auth: AuthService,
private _environmentService: EnvironmentService,
private _renderer: Renderer2,
private _activatedRoute: ActivatedRoute,
private _router: Router
private _router: Router,
private _actions: ActionsSubject
) {}
ngOnInit() {
this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId)));
this.selectedBranch$ = this.activatedProcessId$.pipe(switchMap((processId) => this.application.getSelectedBranch$(Number(processId))));
this.stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranch$]).pipe(
map(([defaultBranch, selectedBranch]) => {
if (defaultBranch?.branchType === 4 && !selectedBranch) {
return 'Bitte wählen Sie eine Filiale aus, um den Bestand zu sehen.';
} else if (defaultBranch?.branchType !== 4 && !selectedBranch) {
return 'Bitte wählen Sie eine Filiale aus, um den Bestand einer anderen Filiale zu sehen';
}
return '';
})
);
}
ngAfterViewInit(): void {
this._actions.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.stockTooltipText$)).subscribe(([action, text]) => {
if (action.type === 'OPEN_TOOLTIP_NO_BRANCH_SELECTED' && !!text) {
this.branchInputNoBranchSelectedTrigger.open();
}
});
fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.isTablet$))
.subscribe(([_, isTablet]) => {

View File

@@ -6,9 +6,20 @@ import { ArticleDetailsModule } from './article-details/article-details.module';
import { ArticleSearchModule } from './article-search/article-search.module';
import { PageCatalogRoutingModule } from './page-catalog-routing.module';
import { PageCatalogComponent } from './page-catalog.component';
import { UiCommonModule } from '@ui/common';
import { UiTooltipModule } from '@ui/tooltip';
@NgModule({
imports: [CommonModule, PageCatalogRoutingModule, ArticleSearchModule, ArticleDetailsModule, BreadcrumbModule, BranchSelectorComponent],
imports: [
CommonModule,
PageCatalogRoutingModule,
ArticleSearchModule,
ArticleDetailsModule,
BreadcrumbModule,
BranchSelectorComponent,
UiCommonModule,
UiTooltipModule,
],
exports: [],
declarations: [PageCatalogComponent],
})

View File

@@ -1,11 +1,12 @@
import { NgModule } from '@angular/core';
import { TrimPipe } from './trim.pipe';
import { VatPipe } from './vat.pipe';
@NgModule({
imports: [],
exports: [TrimPipe],
declarations: [TrimPipe],
exports: [TrimPipe, VatPipe],
declarations: [TrimPipe, VatPipe],
providers: [],
})
export class PipesModule {}

View File

@@ -0,0 +1,18 @@
import { Pipe, PipeTransform } from '@angular/core';
import { VATType } from '@swagger/cat';
@Pipe({
name: 'vat',
})
export class VatPipe implements PipeTransform {
transform(vatType: VATType, priceMaintained?: boolean, ...args: any[]): any {
const vatString = vatType === 1 ? '0%' : vatType === 2 ? '19%' : vatType === 8 ? '7%' : undefined;
if (!vatString) {
return;
}
if (priceMaintained) {
return `inkl. ${vatString} MwSt; Preisgebunden`;
}
return `inkl. ${vatString} MwSt`;
}
}

View File

@@ -1,5 +1,5 @@
<ng-container *ngIf="(groupedItems$ | async)?.length <= 0 && !(fetching$ | async); else shoppingCart">
<div class="card stretch card-empty">
<div class="card stretch">
<div class="empty-message">
<span class="cart-icon flex items-center justify-center">
<shared-icon icon="shopping-cart-bold" [size]="24"></shared-icon>
@@ -20,7 +20,7 @@
</div>
</ng-container>
<div class="flex items-center justify-center card stretch card-empty" *ngIf="fetching$ | async">
<div class="flex items-center justify-center card stretch" *ngIf="fetching$ | async">
<ui-spinner [show]="true"> </ui-spinner>
</div>
@@ -46,19 +46,7 @@
*ngIf="group.orderType !== 'Dummy'"
class="icon-order-type"
[size]="group.orderType === 'B2B-Versand' ? 36 : 24"
[icon]="
group.orderType === 'Abholung'
? 'isa-box-out'
: group.orderType === 'Versand'
? 'isa-truck'
: group.orderType === 'Rücklage'
? 'isa-shopping-bag'
: group.orderType === 'B2B-Versand'
? 'isa-b2b-truck'
: group.orderType === 'Download'
? 'isa-download'
: 'isa-truck'
"
[icon]="group.orderType"
></shared-icon>
<div class="label" [class.dummy]="group.orderType === 'Dummy'">
@@ -75,25 +63,18 @@
<div class="grow"></div>
<div class="pl-4" *ngIf="group.orderType !== 'Download' && group.orderType !== 'Dummy'">
<button class="cta-edit" (click)="showPurchasingListModal(group.items)">
Lieferung Ändern
Ändern
</button>
</div>
</div>
<hr *ngIf="group.orderType === 'Download'" />
</ng-container>
<ng-container *ngIf="group.orderType === 'Versand' || group.orderType === 'B2B-Versand' || group.orderType === 'DIG-Versand'">
<div class="flex flex-row items-center px-5 pt-0 pb-[0.875rem] -mt-2 bg-[#F5F7FA]">
<div class="text-p2">
{{ shippingAddress$ | async | shippingAddress }}
</div>
<div class="grow"></div>
<div class="pl-4">
<button (click)="changeAddress()" class="cta-edit">
Adresse Ändern
</button>
</div>
</div>
<hr />
<hr
*ngIf="
group.orderType === 'Download' ||
group.orderType === 'Versand' ||
group.orderType === 'B2B-Versand' ||
group.orderType === 'DIG-Versand'
"
/>
</ng-container>
<ng-container *ngFor="let item of group.items; let lastItem = last; let i = index">
<ng-container
@@ -145,7 +126,8 @@
[disabled]="
showOrderButtonSpinner ||
((primaryCtaLabel$ | async) === 'Bestellen' && !(checkNotificationChannelControl$ | async)) ||
notificationsControl?.invalid
notificationsControl?.invalid ||
((primaryCtaLabel$ | async) === 'Bestellen' && ((checkingOla$ | async) || (checkoutIsInValid$ | async)))
"
>
<ui-spinner [show]="showOrderButtonSpinner">

View File

@@ -1,9 +1,9 @@
:host {
@apply box-border relative block h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
@apply box-border relative block h-split-screen-tablet desktop-small:h-split-screen-desktop;
}
.stretch {
@apply overflow-scroll h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
@apply overflow-scroll h-split-screen-tablet desktop-small:h-split-screen-desktop;
}
button {
@@ -14,10 +14,6 @@ button {
@apply bg-white rounded shadow-card;
}
.card-empty {
max-height: calc(100vh - 250px);
}
.empty-message {
@apply flex flex-col justify-center h-full text-center ml-auto mr-auto;
width: fit-content;
@@ -38,7 +34,6 @@ button {
a {
@apply no-underline;
width: 60%;
}
.cart-icon {
@@ -78,12 +73,11 @@ button {
}
.header {
@apply text-center text-h2 desktop:pb-10 -mt-2;
@apply text-center text-h2 desktop-large:pb-10 -mt-2;
}
hr {
height: 2px;
@apply bg-[#EDEFF0];
@apply bg-[#EDEFF0] h-[0.125rem];
}
h1 {

View File

@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
@@ -6,8 +6,8 @@ import { DomainCheckoutService } from '@domain/checkout';
import { AvailabilityDTO, DestinationDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { PrintModalData, PrintModalComponent } from '@modal/printer';
import { first, map, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs';
import { catchError, debounceTime, delay, first, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { Subject, NEVER, combineLatest, BehaviorSubject, interval, of, merge } from 'rxjs';
import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainPrinterService } from '@domain/printer';
@@ -17,6 +17,8 @@ import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-mod
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
import { EnvironmentService } from '@core/environment';
import { CheckoutReviewStore } from './checkout-review.store';
import { ToasterService } from '@shared/shell';
import { ShoppingCartItemComponent } from './shopping-cart-item/shopping-cart-item.component';
@Component({
selector: 'page-checkout-review',
@@ -25,19 +27,18 @@ import { CheckoutReviewStore } from './checkout-review.store';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutReviewComponent implements OnInit, OnDestroy {
checkingOla$ = new BehaviorSubject<boolean>(false);
payer$ = this._store.payer$;
buyer$ = this._store.buyer$;
shoppingCart$ = this._store.shoppingCart$;
fetching$ = this._store.fetching$;
notificationsControl = this._store.notificationsControl;
shippingAddress$ = this.applicationService.activatedProcessId$.pipe(
takeUntil(this._store.orderCompleted),
switchMap((processId) => this.domainCheckoutService.getShippingAddress({ processId }))
);
shoppingCartItemsWithoutOrderType$ = this._store.shoppingCartItems$.pipe(
takeUntil(this._store.orderCompleted),
map((items) => items?.filter((item) => item?.features?.orderType === undefined))
@@ -125,12 +126,12 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
showQuantityControlSpinnerItemId: number;
quantityError$ = new BehaviorSubject<{ [key: string]: string }>({});
primaryCtaLabel$ = combineLatest([this.payer$, this.shoppingCartItemsWithoutOrderType$]).pipe(
map(([payer, shoppingCartItemsWithoutOrderType]) => {
primaryCtaLabel$ = combineLatest([this.payer$, this.buyer$, this.shoppingCartItemsWithoutOrderType$]).pipe(
map(([payer, buyer, shoppingCartItemsWithoutOrderType]) => {
if (shoppingCartItemsWithoutOrderType?.length > 0) {
return 'Kaufoptionen';
}
if (!payer) {
if (!(payer || buyer)) {
return 'Weiter';
}
return 'Bestellen';
@@ -152,21 +153,24 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
loadingOnQuantityChangeById$ = new Subject<number>();
showOrderButtonSpinner: boolean;
checkoutIsInValid$ = this.applicationService.activatedProcessId$.pipe(
switchMap((processId) => this.domainCheckoutService.checkoutIsValid({ processId })),
map((valid) => !valid)
);
get productSearchBasePath() {
return this._productNavigationService.getArticleSearchBasePath(this.applicationService.activatedProcessId);
}
get isDesktop$() {
return this._environmentService.matchDesktop$.pipe(
map((state) => {
return state.matches;
}),
shareReplay()
);
return this._environmentService.matchDesktopLarge$;
}
private _onDestroy$ = new Subject<void>();
@ViewChildren(ShoppingCartItemComponent)
private _shoppingCartItems: QueryList<ShoppingCartItemComponent>;
constructor(
private domainCheckoutService: DomainCheckoutService,
public applicationService: ApplicationService,
@@ -181,7 +185,8 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
private _productNavigationService: ProductCatalogNavigationService,
private _navigationService: CheckoutNavigationService,
private _environmentService: EnvironmentService,
private _store: CheckoutReviewStore
private _store: CheckoutReviewStore,
private _toaster: ToasterService
) {}
async ngOnInit() {
@@ -191,6 +196,12 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
await this.removeBreadcrumbs();
await this.updateBreadcrumb();
this.registerOlaCechk();
window['Checkout'] = {
refreshAvailabilities: this.refreshAvailabilities.bind(this),
};
}
ngOnDestroy(): void {
@@ -199,6 +210,32 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
this._onDestroy$.complete();
}
registerOlaCechk() {
this.applicationService.activatedProcessId$
.pipe(
takeUntil(this._onDestroy$),
switchMap((processId) =>
this.domainCheckoutService.validateOlaStatus({
processId,
})
)
)
.subscribe((result) => {
if (!result) {
this.refreshAvailabilities();
}
});
}
async refreshAvailabilities() {
this.checkingOla$.next(true);
for (let itemComp of this._shoppingCartItems.toArray()) {
await itemComp.refreshAvailability();
await new Promise((resolve) => setTimeout(resolve, 500));
}
this.checkingOla$.next(false);
}
async updateBreadcrumb() {
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
@@ -484,6 +521,8 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
title: 'Hinweis',
data: { message: message.trim() },
});
} else if (error) {
this.uiModal.error('Fehler beim abschließen der Bestellung', error);
}
if (error.status === 409) {

View File

@@ -20,6 +20,7 @@ import { CheckoutReviewDetailsComponent } from './details/checkout-review-detail
import { CheckoutReviewStore } from './checkout-review.store';
import { IconModule } from '@shared/components/icon';
import { TextFieldModule } from '@angular/cdk/text-field';
import { LoaderComponent } from '@shared/components/loader';
@NgModule({
imports: [
@@ -39,6 +40,7 @@ import { TextFieldModule } from '@angular/cdk/text-field';
UiCheckboxModule,
SharedNotificationChannelControlModule,
TextFieldModule,
LoaderComponent,
],
exports: [CheckoutReviewComponent, CheckoutReviewDetailsComponent],
declarations: [CheckoutReviewComponent, SpecialCommentComponent, ShoppingCartItemComponent, CheckoutReviewDetailsComponent],

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