Compare commits

...

82 Commits

Author SHA1 Message Date
Lorenz Hilpert
20003ab72a chore: update package.json dependencies and add snackbar library to tsconfig 2025-07-09 16:20:19 +02:00
Nino Righi
b7e7155577 Merged PR 1877: #4769, #5194 Remission List Item - StockInfos - ItemInfos
feat(remission-list, remission-shared-product-stock-info): implement product stock info display

Add product stock information to the remission list and shared product components.
This enhances user visibility into current stock levels directly within remission-related views,
improving workflow efficiency and reducing the need for context switching.

Ref: #4769, #5194
2025-07-02 14:23:04 +00:00
Lorenz Hilpert
b28c204f23 refactor(tabs): add metadata and navigation properties to Tab model 2025-07-01 12:00:12 +02:00
Lorenz Hilpert
e7a807cfbd refactor(tabs): enhance Tab interface with navigation and metadata 2025-07-01 11:46:30 +02:00
Lorenz Hilpert
344dc61a90 fix: resolve CSS cascade issue with UI components and Tailwind utilities
- Move UI component styles to @layer components in tailwind.scss
- Remove ui.scss and integrate imports directly into component layer
- Add SCSS files to Tailwind content config to prevent CSS class purging
- Update Angular project configuration to remove ui.scss references
- Ensure Tailwind utilities can override component styles properly

Refs: #5195
2025-06-30 23:01:58 +02:00
Lorenz Hilpert
8d063428fc Merge branch 'refactor/convert-buildable-libs-to-non-buildable' into develop
Resolved conflicts:
- .gitignore: Added .claude to ignored files
- nx.json: Kept HEAD version with extra eslint.config.js exclusion
- package.json: Merged dependencies, updated vitest to v3.1.1 for compatibility
- eslint config files: Fixed merge conflicts and accepted conversion from .mjs to .js
- Removed deleted files from refactor branch
- Regenerated package-lock.json with --legacy-peer-deps

Build and tests pass successfully.
2025-06-30 20:52:05 +02:00
Lorenz Hilpert
06b0c6264a chore: add .claude to .gitignore 2025-06-30 20:13:33 +02:00
Lorenz Hilpert
4fe633e973 chore: update package dependencies and remove unused shared imports in tsconfig 2025-06-30 20:13:19 +02:00
Lorenz Hilpert
2463a803ea Merged PR 1876: Fix Workspace Build Issues 2025-06-30 09:17:03 +00:00
Lorenz Hilpert
1663dcec73 test(search-item-to-remit-dialog): enhance unit tests for component behavior and signal integration 2025-06-30 11:00:00 +02:00
Lorenz Hilpert
827aa565c5 feat(tests): update test command to include tuiAutoExit and add unit tests for SearchItemToRemitDialogComponent 2025-06-27 17:34:13 +02:00
Lorenz Hilpert
39fc4ce1ce refactor(styles): update styles to use Tailwind CSS and clean up code 2025-06-27 16:45:47 +02:00
Lorenz Hilpert
4f4b072e25 refactor(sass): migrate @import to @use syntax
- Replace deprecated @import with modern @use in _components.scss
- Replace deprecated @import with modern @use in tailwind.scss
- Move @use statements before @tailwind directives per Sass requirements
- Eliminates all 5 Sass deprecation warnings from build
- Future-proofs codebase for Dart Sass 3.0.0
2025-06-27 16:42:49 +02:00
Lorenz Hilpert
9af4a72a76 fix: resolve build warnings and improve code quality
- Remove unused Angular component and pipe imports to eliminate TS-998113 warnings
- Fix Sass mixed declarations warnings by reordering CSS properties
- Remove empty ngOnInit method from preview component
- Clean up unused imports across customer search and OMS components
- Move animation/transition properties above nested rules in SCSS files

Reduces build warnings significantly and improves code maintainability.
2025-06-27 16:19:18 +02:00
Lorenz Hilpert
7a44101e90 refactor: convert buildable libraries to non-buildable and migrate eslint configs
- Convert eslint.config.mjs files to eslint.config.js format across workspace
- Remove build targets from remission libraries (data-access, feature/remission-list, helpers, shared)
- Remove build target from icons library
- Delete ng-package.json and tsconfig.lib.prod.json files from buildable libraries
- Update tsconfig.lib.json configurations to remove bundler moduleResolution
- Clean up build artifacts and simplify library configurations
- Libraries now compile on-demand during application build instead of pre-compilation
2025-06-27 15:44:34 +02:00
Lorenz Hilpert
5e6ee35d91 chore(eslint): remove eslint-plugin-prettier and update configurations 2025-06-26 22:48:56 +02:00
Lorenz Hilpert
15db63aa1a refactor(quantity-and-reason-item): improve stock fetching and caching logic
Enhance the fetchAssignedStock method to utilize memory storage for caching
assigned stock data. Update the resource loader to handle cached values and
set new stock data accordingly. Adjust the HTML button for better readability.
2025-06-26 22:34:03 +02:00
Lorenz Hilpert
998946157a chore: update dependencies and add vitest configuration
- Added @analogjs/vite-plugin-angular and @analogjs/vitest-angular to devDependencies.
- Updated @nx/vite to version 20.1.4.
- Added @vitest/coverage-v8 and @vitest/ui to devDependencies.
- Added jsdom to devDependencies.
- Added vite and vitest to devDependencies.
- Updated tsconfig.base.json to include new paths for shared libraries.
- Created vitest.workspace.ts for vitest configuration.

Refs: #5135
2025-06-26 22:09:21 +02:00
Nino Righi
11cfa4039f Merged PR 1875: feat(remission-list-item): Item View (Basic), Refs: #4769
feat(remission-list-item): Item View (Basic), Refs: #4769
2025-06-25 15:01:31 +00:00
Lorenz Hilpert
26fd5cb389 Merged PR 1874: Remi Add Item Dialog FLow
Related work items: #5135
2025-06-25 13:45:25 +00:00
Nino Righi
f34f2164fc Merged PR 1873: #4769 Remission List
- feat(remission-list): Zwischencommit
- feat(ui-input-controls): Adjusted Dropdown Styling and Added Droption Option Disable Class, Refs: #4769
- feat(remission): implement remission list feature shell and category select
- Merge branch 'develop' into feature/4769-Remission-Liste
- Merge branch 'develop' into feature/4769-Remission-Liste
- feat(remission-list, remission-data-access): implement new remission data access layer and update remission list integration
2025-06-25 13:38:22 +00:00
Lorenz Hilpert
a68f5b5347 chore(dependencies): add optional dependency for @esbuild/linux-x64 2025-06-25 11:50:45 +02:00
Nino Righi
d53540b8db Merged PR 1867: #4769 Remi 3.0 - Remission – Scannen und Suchen
- feat(remission-list): Zwischencommit
- feat(ui-input-controls): Adjusted Dropdown Styling and Added Droption Option Disable Class, Refs: #4769
- feat(remission): implement remission list feature shell and category select
- Merge branch 'develop' into feature/4769-Remission-Liste
2025-06-23 15:23:54 +00:00
Lorenz Hilpert
4cf0ce820e Merged PR 1866: Anlage Komponenten und Directives + Unit Tests und Stories
Related work items: #5175
2025-06-18 13:58:00 +00:00
Nino
b21ebac53f feat(remission-list): Init Routing to Remission List 2025-06-16 17:24:35 +02:00
Lorenz Hilpert
5a68adc87c chore: update editorconfig and add .prettierrc for code formatting 2025-06-16 17:06:34 +02:00
Nino
befdc9fa4d feat(core-tabs): Changes due to Renaming from Process to Tab and Unit Test Fixes 2025-06-16 16:37:48 +02:00
Nino
e41dbc2870 feat(core-tabs): Move Core-Process to Core-Tabs 2025-06-16 15:05:30 +02:00
Nino
083f75a395 feat(remission): Init Remission Feature/Shared/Helpers and Data-Access Libs
Refs: #4768, #4769, #4770, #4771
2025-06-16 13:51:43 +02:00
Lorenz Hilpert
7c8aef9a48 chore: update npm install command to include legacy-peer-deps flag 2025-06-16 12:06:10 +02:00
Lorenz Hilpert
ee841eba49 chore: update @nx/angular dependency to version ^20.1.4 2025-06-16 12:02:45 +02:00
Lorenz Hilpert
0560f18de3 Merge branch 'migration-angular-20' into develop 2025-06-16 11:55:58 +02:00
Lorenz Hilpert
d8c2ca9bdc Migration Angular v19 -> v20 2025-06-16 11:54:47 +02:00
Nino Righi
636e405927 Merged PR 1865: feat(oms-data-access, oms-return-review, oms-return-summary): fix return receipt mapping and ensure process completion
feat(oms-data-access, oms-return-review, oms-return-summary): fix return receipt mapping and ensure process completion

Corrects the mapping of return receipts in the return process flow to ensure that the correct receipt IDs are used when printing and finalizing returns. Updates the `finishProcess` method to associate return receipts with the correct entities and ensures the store is updated after process completion. This resolves issues where printed receipts or review steps could reference incorrect or missing data.

Ref: #5120
2025-06-14 14:19:02 +00:00
Nino Righi
159afa9356 Merged PR 1864: feat(ui-tooltip): remove native title attribute from tooltip icon host
- feat(ui-tooltip): remove native title attribute from tooltip icon host

Refs: #5163
2025-06-14 14:18:21 +00:00
Nino Righi
2088fd3191 Merged PR 1863: feat(oms-return-details): improve layout and styling of order group item controls
- feat(oms-return-details): improve layout and styling of order group item controls

Ref: #5171
2025-06-13 13:56:15 +00:00
Nino Righi
6f80159281 Merged PR 1860: #5157 Return Input Ean Validation
- feat(oms-return-review): implement return review feature
- Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop
- Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop
- feat(oms-return-process, ui-input-controls): improve error feedback for EAN input and adjust text field container spacing

Refs: #5157
2025-06-13 13:55:32 +00:00
Lorenz Hilpert
54664123fb chore: update dependencies to latest versions
- Upgraded @nx packages to version 21.2.0
- Updated Storybook packages to version 9.0.5 and core-server to 8.6.11
- Upgraded eslint-config-prettier to version 10.1.5
- Updated jest-preset-angular to version 14.6.0
- Upgraded nx to version 21.2.0
- Updated storybook test-runner to version 0.22.0
2025-06-13 15:54:23 +02:00
Lorenz Hilpert
0134f8dbf5 fix(return-search): correct typo in tooltip content 2025-06-12 18:53:41 +02:00
Lorenz Hilpert
1429ca37c6 feat(return-product-info): add ProductRouterLinkDirective to tests 2025-06-12 18:50:52 +02:00
Nino Righi
f5f8a7ae18 Merged PR 1862: #5168
- feat(oms-data-access): fix return process entity validation in startProcess

Refs: #5168
2025-06-12 16:28:31 +00:00
Lorenz Hilpert
3cf05f04ef feat(shared-scanner): Moved to shared/scanner
feat(common-data-access): takeUnitl operators for keydown

Refs: #5062
2025-06-12 16:34:21 +02:00
Lorenz Hilpert
055cfb67d3 Merged PR 1861: feat(product-router-link): add shared product router link directive and builder
feat(product-router-link): add shared product router link directive and builder

Ref: #5111 #5169
2025-06-12 14:28:12 +00:00
Lorenz Hilpert
53d8abd615 Merged PR 1859: feat(print-button): implement reusable print button component with service in...
feat(print-button): implement reusable print button component with service integration

Ref: #5146
2025-06-12 14:00:09 +00:00
Nino Righi
7323c67ba6 Merged PR 1857: feat(oms-return-review): implement return review feature
feat(oms-return-review): implement return review feature

Introduce the initial implementation of the return review feature in the OMS module. This includes core logic, UI components, and integration with existing state management. The feature enables users to review return processes, view item details, and confirm actions as part of the return workflow.

Ref: #5120
2025-06-12 12:16:30 +00:00
Lorenz Hilpert
1617533412 Merged PR 1858: feat(scanner): add full-screen scanner styles and components
feat(scanner): add full-screen scanner styles and components

Implemented full-screen scanner styles in styles.scss.
Added ScannerButtonComponent to trigger barcode scanning.
Created ScannerComponent for rendering camera view and processing scans.
Updated ScannerService to handle scanning operations and configuration.
Enhanced README.md with detailed library features and usage examples.
Refactored return process components to utilize new scanner button.
Updated search bar input to integrate scanner functionality.
Added tests for new components and services, ensuring proper functionality.
Improved UI button styles for better integration with scanner features.
Ref:
#5123
DS // Scanner Overlay
QA
#5056
Retoure // Scan-Button lösen Suche aus
QA
#5147

Related work items: #5147
2025-06-12 11:56:06 +00:00
Nino Righi
b589dc21cd Merged PR 1856: #5144 #5141 #5099
- feat(oms-data-access, oms-return-details): add processed quantity helper and refactor item controls
- feat(ui-input-controls, oms-return-details): add disabled styling and logic for dropdowns
- feat(oms-return-details): improve dropdown accessibility and disabled state handling

Refs: #5144 #5141 #5099
2025-06-11 19:56:16 +00:00
Lorenz Hilpert
80fb65ffc4 Merged PR 1855: 5000 Retoure // Info -Tooltip zur Suchseite hinzufügen
Related work items: #5000
2025-06-11 15:08:54 +00:00
Lorenz Hilpert
dbe0328eb7 Merged PR 1854: refactor(searchbox): improve formatting and add showScannerButton getter
refactor(searchbox): improve formatting and add showScannerButton getter

Ref: #5001
2025-06-10 14:57:14 +00:00
Lorenz Hilpert
61ce9940c9 Merged PR 1853: feat(return-process): add getReceiptItemQuantity helper and related tests
feat(return-process): add getReceiptItemQuantity helper and related tests

Ref: #5156
2025-06-10 14:56:34 +00:00
Nino Righi
a37201ef33 Merged PR 1849: feat(libs-ui-item-rows): improve data value wrapping and label sizing
feat(libs-ui-item-rows): improve data value wrapping and label sizing

- Add `break-all` to `.ui-item-row-data-value` for better handling of long or unbroken content, ensuring values do not overflow their containers
- Use Tailwind's `min-w-[6.5rem]` utility for `.ui-item-row-data-label` and `.ui-item-row-data-label` in both `item-row-data` and `client-row` components, standardizing minimum label width and improving layout consistency

Ref: #5074
2025-06-10 14:43:13 +00:00
Nino Righi
9857d86bdf Merged PR 1850: feat(libs-shared-filter): improve date range equality for default filter inpu...
feat(libs-shared-filter): improve date range equality for default filter input detection

Enhance the isDefaultFilterInput method to compare DateRangeFilterInput values by parsing ISO date strings to Date objects before comparison. This ensures that date ranges are considered equal even if their string representations differ in precision (e.g., "2023-06-05T22:00:00Z" vs. "2023-06-05T22:00:00.000Z"). This change improves filter reset and default state detection reliability for date range filters.

Ref: #5142
2025-06-10 13:57:09 +00:00
Nino Righi
7283caab15 Merged PR 1852: feat(shared-filter,search-bar,search-main): add E2E data attributes for filte...
feat(shared-filter,search-bar,search-main): add E2E data attributes for filter and search UI

Add standardized `data-which` and `data-what` attributes to filter input buttons, search bar input, search bar button, and clear search icon components. This improves end-to-end testability and aligns with project conventions for robust, maintainable UI automation. Updates affect filter menu, input menu button, search bar input, and search bar clear components, as well as the return search main feature.

Ref: #5060
2025-06-10 13:16:15 +00:00
Lorenz Hilpert
3eb6981e3a Merged PR 1851: Retoure // Mehrere Belege in der Retouren-Detailansicht anzeigen
Related work items: #5002, #5148
2025-06-06 15:34:33 +00:00
Lorenz Hilpert
dd598d100c Merged PR 1848: feat(tooltip): add tooltip component and directive with customizable triggers
feat(tooltip): add tooltip component and directive with customizable triggers

Introduce a new tooltip library for Angular applications, featuring a
flexible tooltip component that supports various trigger events
(click, hover, focus) and customizable content. Includes necessary
styles, tests, and documentation for usage and configuration.

Ref: #4992
2025-06-06 11:13:07 +00:00
Nino Righi
405bf5b463 Merged PR 1847: fix(shared-filter): add mapFilterInputToRecord util and refactor query mapping
fix(shared-filter): add mapFilterInputToRecord util and refactor query mapping

Introduce mapFilterInputToRecord utility for consistent mapping of filter inputs to query parameter records. Refactor FilterService.query to use this utility for both filter and input groups, ensuring DRY code and improved maintainability.

Add unit tests for the new mapping function and update the mappings index export.

Ref: #5105, #5106, #5143
2025-06-05 17:14:05 +00:00
Nino Righi
b261273228 Merged PR 1841: feat(ui-input-controls, oms-return-process): introduce text field container,...
feat(ui-input-controls, oms-return-process): introduce text field container, clear, and errors components

- Add `ui-text-field-container`, `ui-text-field-clear`, and `ui-text-field-errors` as standalone components for improved text field composition and error handling.
- Update SCSS to include new styles for container, clear, and errors components, ensuring visual consistency and error highlighting.
- Refactor `ReturnProcessProductQuestionComponent` to use the new containerized text field structure, improving template clarity and error display.
- Update Storybook story for `TextField` to demonstrate new composition and error handling.
- Export new components from the input-controls public API for external usage.

Ref: #4989, #5058
2025-06-05 17:12:28 +00:00
Lorenz Hilpert
f5507a874c Merge branch 'develop' of ssh.dev.azure.com:v3/hugendubel/ISA/ISA-Frontend into develop 2025-06-05 18:49:39 +02:00
Lorenz Hilpert
4478e1ce21 hotfix(checkout): Chaning the Quantity in the cart used a wrink branch for "Rücklage"
- When chaning the quantity, the current destination branch will be used to update the quantity in the cart
2025-06-05 18:35:23 +02:00
Lorenz Hilpert
ade6b7f845 Merged PR 1846: Navigation for to Customer Details from external Systems
Related work items: #5149
2025-06-05 16:09:13 +00:00
Nino Righi
7743150652 Merged PR 1845: feat(shared-product-info): add shared product info module and initial impleme...
feat(shared-product-info): add shared product info module and initial implementation

Introduce a new shared module for product information, providing reusable components and services for displaying product details across the application. This module is designed for consistency and maintainability, following project guidelines for modularization and type safety.

Ref: #5065
2025-06-04 19:42:14 +00:00
Nino Righi
543de57190 Merged PR 1844: feat(oms-data-access, oms-shared-task-list): add Tolino return receipt print support and improve task action typing
feat(oms-data-access, oms-shared-task-list): add Tolino return receipt print support and improve task action typing

- Add `PrintTolinoReturnReceiptService` to `oms-data-access` for printing Tolino return receipts via office printers, including direct integration with the OMS print API.
- Extend `TaskActionType` to include `receiptItemId` for more precise task identification and action handling.
- Update `return-task-list-item` and `return-task-list` components in `oms-shared-task-list` to support the new Tolino print action, including UI and logic for triggering the print dialog.
- Refactor print-related service and test code to use the new print API signature and improve type safety.
- Add and update unit tests to cover new print flows and ensure correct integration.

Ref: #5121
2025-06-03 22:17:29 +00:00
Nino Righi
bcd3c800b1 Merged PR 1843: feat(libs-shared-filter): show selected filter count on filter button
feat(libs-shared-filter): show selected filter count on filter button

- Display the number of selected filters as a badge on the filter menu button when filters are active.
- Add `.has-selected-filter` styling for visual emphasis when filters are selected.
- Update FilterService to provide a computed `selectedFilterCount` property, counting non-default filter inputs.
- Remove direct icon rendering from the button; icon is now handled by the button component.
- Update tests to mock and assert the new selected filter count logic.

Ref: #5070
2025-06-03 22:15:06 +00:00
Nino Righi
bd7faeb1b5 Merged PR 1842: #5139 minor fix
#5139 minor fix
2025-06-03 22:14:30 +00:00
Lorenz Hilpert
e60d74573c Merged PR 1840: feat(ui-menu): add ui-menu component and related directives
feat(ui-menu): add ui-menu component and related directives

Ref: #5103
2025-05-28 14:06:16 +00:00
Nino Righi
2f04b56f71 Merged PR 1838: feat(oms-return-search): add unit tests for ReturnSearchResultItemComponent a...
feat(oms-return-search): add unit tests for ReturnSearchResultItemComponent and fix address fallback

- Add comprehensive Spectator-based unit tests for ReturnSearchResultItemComponent, covering all computed properties and edge cases.
- Fix address computed property to correctly fall back to shipping address when billing address is missing, ensuring robust display logic.

Ref: #5113
2025-05-26 19:19:14 +00:00
Nino Righi
6e8df1c4ab Merged PR 1839: feat(oms-return-process): apply flex layout to select question description
feat(oms-return-process): apply flex layout to select question description

Update the return-process-select-question component to use the 'flex-1' Tailwind utility class on the description container. This ensures proper flexbox alignment and consistent layout within the parent flex context.

Ref: #5057
2025-05-26 19:19:00 +00:00
Lorenz Hilpert
94e1d729a0 fix(shared/filter): handle setTimeout in FilterMenuButton test
Fix failing test in FilterMenuButtonComponent that checks if rollback is called
when the menu is closed with rollbackOnClose=true. The test was failing because
the component uses setTimeout to schedule the rollback call, but the test was
asserting immediately without waiting for the timeout to complete.

Changes made:
- Modified the test to use jest.useFakeTimers() to control JavaScript timers
- Added jest.runAllTimers() to ensure the setTimeout callback executes
- Added cleanup with jest.useRealTimers() to prevent test pollution
- Made the test function async to properly handle asynchronous behavior

This change ensures that the test properly validates the component's
asynchronous behavior and makes the test suite more reliable.
2025-05-26 21:17:46 +02:00
Lorenz Hilpert
0d202ab97c Merged PR 1837: Fix - Filter Reset und Filter Sync - Removed unused code, logger performance
refactor: improve code formatting and readability in provide-filter.ts and filter-menu components

fix: delay filter rollback on close in FilterMenuButtonComponent

fix: update filter clear button text and method calls in filter-menu.component.html

chore: update package-lock.json to remove unnecessary dev flags and add new dependencies

Ref: #5125, #5076
2025-05-26 15:02:43 +00:00
Nino Righi
c322020c3f Merged PR 1835: feat(oms-data-access, oms-return-summary): unify return details mapping and serialization
Commit 8949c691: feat(oms-data-access, oms-return-summary): unify return details mapping and serialization

- Refactor `returnReceiptValuesMapping` to use `serializeReturnDetails` instead of `returnDetailsMapping` for the `returnDetails` field, ensuring consistent serialization of return details across the OMS data access layer.
- Move the string mapping logic for return details into a dedicated helper (`serializeReturnDetails`), and update all usages and tests accordingly.
- Update `ReturnSummaryItemComponent` to use the new `returnDetailsMapping` helper for rendering human-readable return details in the summary UI.
- Add and update comprehensive unit tests for both helpers and the mapping logic to ensure correct handling of edge cases and maintainability.

Ref: #5124
2025-05-26 14:00:21 +00:00
Nino Righi
bbcf84d357 Merged PR 1836: fix(oms-return-details): use 24-hour format for receipt and order dates
fix(oms-return-details): use 24-hour format for receipt and order dates

Update date formatting in return-details-order-group-data.component.html to use
'HH:mm' (24-hour format) instead of 'hh:mm' (12-hour format) for both receipt
and order dates. This ensures consistency with German locale expectations and
improves clarity for users.

Ref: #5040
2025-05-26 13:59:23 +00:00
Nino Righi
1ddc0a2767 Merged PR 1833: fix(oms-data-access, oms-return-details): remove obsolete 'Software' product...
fix(oms-data-access, oms-return-details): remove obsolete 'Software' product category and related logic

- Removed the 'Software' entry from the ProductCategory constant and its type in `constants.ts`.
- Removed all references to ProductCategory.Software in the category-question registry and eligibility logic.
- Updated the return-details-order-group-item-controls component template to ensure the product category dropdown and checkbox are only rendered when the item is returnable, improving UI consistency and preventing controls from appearing for non-returnable items.
- Added/extended unit tests to verify correct rendering and logic for canReturnReceiptItem and selectability.

This change ensures that only supported product categories are handled in the return process and that UI controls are displayed appropriately based on item eligibility.

Ref: #5100
2025-05-26 08:53:33 +00:00
Nino Righi
1ad6c41c25 Merged PR 1834: fix(oms-return-search): add missing name attribute to mobile sort button
fix(oms-return-search): add missing name attribute to mobile sort button

Adds the `name="isaActionSort"` attribute to the mobile sort button in the return search result component template. This ensures consistent accessibility and testability across platforms, aligning with project standards for semantic markup and E2E test selectors.

Ref: #5110
2025-05-26 08:52:03 +00:00
Nino
72bdf59b05 #5116 Quick Fix 2025-05-23 10:07:51 +02:00
Lorenz Hilpert
0a4eb9bb1c refactor(form-control): improve structure and formatting of component 2025-05-22 19:24:55 +02:00
Lorenz Hilpert
7c9839d93a refactor(scan): improve structure of init method and format code 2025-05-22 19:13:28 +02:00
Lorenz Hilpert
cfb8fb17d6 Merge branch 'develop' of ssh.dev.azure.com:v3/hugendubel/ISA/ISA-Frontend into develop 2025-05-22 19:07:31 +02:00
Lorenz Hilpert
cdd27aeeb0 refactor(print): update print dialog component to use new listbox directives
test(print): add unit tests for PrintService and PrintReceiptsService

feat(print): modify PrintService methods to return promises instead of observables

refactor(oms): rename return-print-receipts.service to print-receipts.service and update references

chore(ui): remove deprecated ui-list library and integrate listbox components

style(ui): add styles for listbox and listbox items

test(ui): implement unit tests for listbox directives

docs(ui): update README and remove unused files related to ui-list
2025-05-22 19:07:00 +02:00
Nino
2e3029daa2 feat(oms-return-details): extract item controls into dedicated component
Refactor the order group item controls into a separate component for improved maintainability and reusability. The new ReturnDetailsOrderGroupItemControlsComponent encapsulates dropdown and checkbox functionality that was previously embedded within the parent component.

This change improves code organization by applying the Single Responsibility Principle, making each component more focused and easier to test. Updated tests ensure proper functionality is maintained after extraction.

Ref: #5116
2025-05-22 17:30:35 +02:00
Nino
ec109f89ef Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2025-05-22 12:38:51 +02:00
Nino
f11567dd82 feat(oms-data-access): implement dynamic question flow for Tolino returns
Add conditional question flow logic for Tolino device returns based on previous answers.
The implementation creates a dedicated helper for Tolino questions that dynamically
determines the next question based on device power status and defect status.

This allows for a more tailored return process experience where:
- If device powers on and has no defects, show return reason
- If device doesn't power on and has no defects, ask about damage
- Otherwise check if case is damaged

Ref: #4944
2025-05-22 12:38:34 +02:00
Lorenz Hilpert
4bbdb870f8 Merge branch 'feature/5047-5053-Design-und-Funktionsweise-Drucker-Dialog' into develop 2025-05-21 21:11:22 +02:00
1019 changed files with 33033 additions and 24324 deletions

View File

@@ -2,6 +2,3 @@ last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 iOS major versions
safari > 11
Firefox ESR
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

View File

@@ -7,6 +7,7 @@ indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = crlf
[*.md]
max_line_length = off

View File

@@ -4,7 +4,7 @@ applyTo: '**'
// This file is automatically generated by Nx Console
You are in an nx workspace using Nx 20.4.6 and npm as the package manager.
You are in an nx workspace using Nx 21.2.1 and npm as the package manager.
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
@@ -27,6 +27,14 @@ If the user wants to generate something, use the following flow:
- wait for the user to finish the generator
- read the generator log file using the 'nx_read_generator_log' tool
- use the information provided in the log file to answer the user's question or continue with what they were doing
undefined
# Running Tasks Guidelines
If the user wants help with tasks or commands (which include keywords like "test", "build", "lint", or other similar actions), use the following flow:
- Use the 'nx_current_running_tasks_details' tool to get the list of tasks (this can include tasks that were completed, stopped or failed).
- If there are any tasks, ask the user if they would like help with a specific task then use the 'nx_current_running_task_output' tool to get the terminal output for that task/command
- Use the terminal output from 'nx_current_running_task_output' to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary
- If the user would like to rerun the task or command, always use `nx run <taskId>` to rerun in the terminal. This will ensure that the task will run in the nx context and will be run the same way it originally executed
- If the task was marked as "continuous" do not offer to rerun the task. This task is already running and the user can see the output in the terminal. You can use 'nx_current_running_task_output' to get the output of the task to verify the output.

7
.gitignore vendored
View File

@@ -25,7 +25,6 @@ speed-measure-plugin.json
*.launch
.settings/
*.sublime-workspace
.prettierrc
# IDE - VSCode
.vscode/*
@@ -59,9 +58,15 @@ libs/swagger/src/lib/*
.nx/cache
.nx/workspace-data
.angular
.claude
storybook-static
.cursor\rules\nx-rules.mdc
.github\instructions\nx.instructions.md
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
vite.config.*.timestamp*
vitest.config.*.timestamp*

View File

@@ -1,7 +1,7 @@
{
"*.ts": "npx eslint --fix --config eslint.config.mjs",
"*.tsx": "npx eslint --fix --config eslint.config.mjs",
"*.js": "npx eslint --fix --config eslint.config.mjs",
"*.jsx": "npx eslint --fix --config eslint.config.mjs",
"*.html": "npx eslint --fix --config eslint.config.mjs"
}
{
"*.ts": "npx eslint --fix --config eslint.config.js",
"*.tsx": "npx eslint --fix --config eslint.config.js",
"*.js": "npx eslint --fix --config eslint.config.js",
"*.jsx": "npx eslint --fix --config eslint.config.js",
"*.html": "npx eslint --fix --config eslint.config.js"
}

37
.prettierrc Normal file
View File

@@ -0,0 +1,37 @@
{
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"tabWidth": 2,
"bracketSpacing": true,
"printWidth": 80,
"endOfLine": "auto",
"arrowParens": "always",
"quoteProps": "consistent",
"overrides": [
{
"files": "*.html",
"options": {
"parser": "html"
}
},
{
"files": "*.component.html",
"options": {
"parser": "angular"
}
},
{
"files": "*.scss",
"options": {
"singleQuote": false
}
},
{
"files": "*.json",
"options": {
"printWidth": 80
}
}
]
}

View File

@@ -5,6 +5,7 @@
"angular.ng-template",
"nrwl.angular-console",
"dbaeumer.vscode-eslint",
"firsttris.vscode-jest-runner"
"firsttris.vscode-jest-runner",
"editorconfig.editorconfig"
]
}

View File

@@ -31,7 +31,7 @@
],
"github.copilot.chat.codeGeneration.instructions": [
{
"file": ".github/copilot-instructions.md"
"file": ".vscode/llms/angular.txt"
},
{
"file": "docs/tech-stack.md"
@@ -50,9 +50,6 @@
}
],
"github.copilot.chat.testGeneration.instructions": [
{
"file": ".github/copilot-instructions.md"
},
{
"file": ".github/testing-instructions.md"
},
@@ -90,4 +87,6 @@
}
],
"nxConsole.generateAiAgentRules": true,
"chat.mcp.enabled": true,
"chat.mcp.discovery.enabled": true
}

View File

@@ -8,7 +8,7 @@ WORKDIR /app
COPY . .
RUN umask 0022
RUN npm version ${SEMVERSION}
RUN npm install --foreground-scripts
RUN npm install --foreground-scripts --legacy-peer-deps
RUN if [ "${IS_PRODUCTION}" = "true" ] ; then npm run-script build-prod ; else npm run-script build ; fi
# stage final

View File

@@ -2,8 +2,11 @@ import type { StorybookConfig } from '@storybook/angular';
const config: StorybookConfig = {
stories: ['../stories/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
staticDirs: ['../src/assets'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-docs',
],
previewHead: (head) => `
${head}
<link href="/assets/fonts/fonts.css" rel="stylesheet" />

View File

@@ -1,55 +1,55 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../eslint.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'app',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'app',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.ts'],
rules: {
'@typescript-eslint/no-unused-expressions': 'warn',
'prefer-const': 'warn',
'@angular-eslint/contextual-lifecycle': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@angular-eslint/no-empty-lifecycle-method': 'warn',
'@typescript-eslint/no-inferrable-types': 'warn',
'@angular-eslint/component-selector': 'warn',
'@angular-eslint/prefer-standalone': 'warn',
'@typescript-eslint/no-inferrable-types': 'warn',
'no-empty-function': 'warn',
'@typescript-eslint/no-empty-function': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@angular-eslint/directive-selector': 'warn',
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {
'@angular-eslint/template/elements-content': 'warn',
'@angular-eslint/template/no-autofocus': 'warn',
},
},
];
const nx = require('@nx/eslint-plugin');
const baseConfig = require('../../eslint.config.js');
module.exports = [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'app',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'app',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.ts'],
rules: {
'@typescript-eslint/no-unused-expressions': 'warn',
'prefer-const': 'warn',
'@angular-eslint/contextual-lifecycle': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@angular-eslint/no-empty-lifecycle-method': 'warn',
'@typescript-eslint/no-inferrable-types': 'warn',
'@angular-eslint/component-selector': 'warn',
'@angular-eslint/prefer-standalone': 'warn',
'@typescript-eslint/no-inferrable-types': 'warn',
'no-empty-function': 'warn',
'@typescript-eslint/no-empty-function': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
'@angular-eslint/directive-selector': 'warn',
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {
'@angular-eslint/template/elements-content': 'warn',
'@angular-eslint/template/no-autofocus': 'warn',
},
},
];

View File

@@ -1,164 +1,162 @@
{
"name": "isa-app",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/isa-app/src",
"tags": [],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",
"options": {
"allowedCommonJsDependencies": [
"lodash",
"moment",
"jsrsasign",
"pdfjs-dist/build/pdf",
"pdfjs-dist/web/pdf_viewer",
"pdfjs-dist/es5/build/pdf",
"pdfjs-dist/es5/web/pdf_viewer"
],
"outputPath": "dist/isa-app",
"index": "apps/isa-app/src/index.html",
"browser": "apps/isa-app/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/isa-app/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"apps/isa-app/src/favicon.ico",
"apps/isa-app/src/assets",
"apps/isa-app/src/config",
"apps/isa-app/src/silent-refresh.html",
"apps/isa-app/src/manifest.webmanifest",
{
"glob": "**/*",
"input": "node_modules/scandit-web-datacapture-barcode/build/engine",
"output": "scandit"
}
],
"styles": [
"@angular/cdk/overlay-prebuilt.css",
"apps/isa-app/src/ui.scss",
"apps/isa-app/src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "25kb"
}
],
"fileReplacements": [
{
"replace": "apps/isa-app/src/environments/environment.ts",
"with": "apps/isa-app/src/environments/environment.prod.ts"
}
],
"outputHashing": "all",
"serviceWorker": "apps/isa-app/ngsw-config.json"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production",
"outputs": ["{options.outputPath}"]
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "isa-app:build:production"
},
"development": {
"buildTarget": "isa-app:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "isa-app:build"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/isa-app/jest.config.ts"
}
},
"serve-static": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "isa-app:build",
"staticFilePath": "dist/apps/isa-app/browser",
"spa": true
}
},
"storybook": {
"executor": "@storybook/angular:start-storybook",
"options": {
"port": 4400,
"configDir": "apps/isa-app/.storybook",
"browserTarget": "isa-app:build",
"compodoc": false,
"styles": ["apps/isa-app/src/ui.scss", "apps/isa-app/src/styles.scss"]
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@storybook/angular:build-storybook",
"outputs": ["{options.outputDir}"],
"options": {
"outputDir": "dist/storybook/isa-app",
"configDir": "apps/isa-app/.storybook",
"browserTarget": "isa-app:build",
"compodoc": false
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"test-storybook": {
"executor": "nx:run-commands",
"options": {
"command": "test-storybook -c apps/isa-app/.storybook --url=http://localhost:4400"
}
},
"static-storybook": {
"executor": "@nx/web:file-server",
"dependsOn": ["build-storybook"],
"options": {
"buildTarget": "isa-app:build-storybook",
"staticFilePath": "dist/storybook/isa-app",
"spa": true
},
"configurations": {
"ci": {
"buildTarget": "isa-app:build-storybook:ci"
}
}
}
}
}
{
"name": "isa-app",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/isa-app/src",
"tags": [],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",
"options": {
"allowedCommonJsDependencies": [
"lodash",
"moment",
"jsrsasign",
"pdfjs-dist/build/pdf",
"pdfjs-dist/web/pdf_viewer",
"pdfjs-dist/es5/build/pdf",
"pdfjs-dist/es5/web/pdf_viewer"
],
"outputPath": "dist/isa-app",
"index": "apps/isa-app/src/index.html",
"browser": "apps/isa-app/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/isa-app/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"apps/isa-app/src/favicon.ico",
"apps/isa-app/src/assets",
"apps/isa-app/src/config",
"apps/isa-app/src/silent-refresh.html",
"apps/isa-app/src/manifest.webmanifest",
{
"glob": "**/*",
"input": "node_modules/scandit-web-datacapture-barcode/build/engine",
"output": "scandit"
}
],
"styles": [
"@angular/cdk/overlay-prebuilt.css",
"apps/isa-app/src/tailwind.scss",
"apps/isa-app/src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "25kb"
}
],
"fileReplacements": [
{
"replace": "apps/isa-app/src/environments/environment.ts",
"with": "apps/isa-app/src/environments/environment.prod.ts"
}
],
"outputHashing": "all",
"serviceWorker": "apps/isa-app/ngsw-config.json"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production",
"outputs": ["{options.outputPath}"]
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "isa-app:build:production"
},
"development": {
"buildTarget": "isa-app:build:development"
}
},
"defaultConfiguration": "development",
"continuous": true
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "isa-app:build"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/isa-app/jest.config.ts"
}
},
"serve-static": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "isa-app:build",
"staticFilePath": "dist/apps/isa-app/browser",
"spa": true
}
},
"storybook": {
"executor": "@storybook/angular:start-storybook",
"options": {
"port": 4400,
"configDir": "apps/isa-app/.storybook",
"browserTarget": "isa-app:build",
"compodoc": false,
"open": false,
"assets": [
{
"glob": "**/*",
"input": "apps/isa-app/src/assets",
"output": "/assets"
}
],
"styles": [
"@angular/cdk/overlay-prebuilt.css",
"apps/isa-app/src/tailwind.scss",
"apps/isa-app/src/styles.scss"
]
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@storybook/angular:build-storybook",
"outputs": ["{options.outputDir}"],
"options": {
"outputDir": "dist/storybook/isa-app",
"configDir": "apps/isa-app/.storybook",
"browserTarget": "isa-app:build",
"compodoc": false,
"styles": [
"@angular/cdk/overlay-prebuilt.css",
"apps/isa-app/src/tailwind.scss",
"apps/isa-app/src/styles.scss"
]
},
"configurations": {
"ci": {
"quiet": true
}
}
}
}
}

View File

@@ -1,5 +1,9 @@
import { inject, Injectable } from '@angular/core';
import { PromptModalData, UiModalService, UiPromptModalComponent } from '@ui/modal';
import {
PromptModalData,
UiModalService,
UiPromptModalComponent,
} from '@ui/modal';
import { Observable } from 'rxjs';
import { ScanAdapter } from './scan-adapter';
import { Config } from '@core/config';
@@ -14,9 +18,14 @@ export class DevScanAdapter implements ScanAdapter {
private _config = inject(Config);
async init(): Promise<boolean> {
return new Promise((resolve, reject) => {
resolve(coerceBooleanProperty(this._config.get('dev-scanner')));
});
const enabled = localStorage.getItem('dev_scan_adapter_enabled') === 'true';
if (enabled) {
return new Promise((resolve, reject) => {
resolve(coerceBooleanProperty(this._config.get('dev-scanner')));
});
}
return false;
}
scan(): Observable<string> {
@@ -25,7 +34,8 @@ export class DevScanAdapter implements ScanAdapter {
content: UiPromptModalComponent,
title: 'Scannen',
data: {
message: 'Diese Eingabemaske dient nur zu Entwicklungs und Testzwecken.',
message:
'Diese Eingabemaske dient nur zu Entwicklungs und Testzwecken.',
placeholder: 'Scan Code',
confirmText: 'weiter',
cancelText: 'abbrechen',

View File

@@ -27,7 +27,10 @@ export class ScanditScanAdapter implements ScanAdapter {
) {}
async init(): Promise<boolean> {
if (this._environmentService.isTablet()) {
const enabled =
localStorage.getItem('scandit_scan_adapter_enabled') === 'true';
if (enabled || this._environmentService.isTablet()) {
try {
await configure({
licenseKey: this._config.get('licence.scandit'),
@@ -88,7 +91,11 @@ export class ScanditScanAdapter implements ScanAdapter {
createOverlay() {
const overlay = this._overlay.create({
positionStrategy: this._overlay.position().global().centerHorizontally().centerVertically(),
positionStrategy: this._overlay
.position()
.global()
.centerHorizontally()
.centerVertically(),
hasBackdrop: true,
});

View File

@@ -18,7 +18,11 @@ import { CanActivateAssortmentGuard } from './guards/can-activate-assortment.gua
import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
import { MainComponent } from './main.component';
import { PreviewComponent } from './preview';
import { BranchSectionResolver, CustomerSectionResolver, ProcessIdResolver } from './resolvers';
import {
BranchSectionResolver,
CustomerSectionResolver,
ProcessIdResolver,
} from './resolvers';
import { TokenLoginComponent, TokenLoginModule } from './token-login';
import { ProcessIdGuard } from './guards/process-id.guard';
import {
@@ -26,7 +30,7 @@ import {
ActivateProcessIdWithConfigKeyGuard,
} from './guards/activate-process-id.guard';
import { MatomoRouteData } from 'ngx-matomo-client';
import { processResolverFn } from '@isa/core/process';
import { tabResolverFn } from '@isa/core/tabs';
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
const routes: Routes = [
@@ -48,7 +52,8 @@ const routes: Routes = [
children: [
{
path: 'dashboard',
loadChildren: () => import('@page/dashboard').then((m) => m.DashboardModule),
loadChildren: () =>
import('@page/dashboard').then((m) => m.DashboardModule),
data: {
matomo: {
title: 'Dashboard',
@@ -57,57 +62,67 @@ const routes: Routes = [
},
{
path: 'product',
loadChildren: () => import('@page/catalog').then((m) => m.PageCatalogModule),
loadChildren: () =>
import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductGuard],
},
{
path: ':processId/product',
loadChildren: () => import('@page/catalog').then((m) => m.PageCatalogModule),
loadChildren: () =>
import('@page/catalog').then((m) => m.PageCatalogModule),
canActivate: [CanActivateProductWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'order',
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
loadChildren: () =>
import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersGuard],
},
{
path: ':processId/order',
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
loadChildren: () =>
import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'customer',
loadChildren: () => import('@page/customer').then((m) => m.CustomerModule),
loadChildren: () =>
import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerGuard],
},
{
path: ':processId/customer',
loadChildren: () => import('@page/customer').then((m) => m.CustomerModule),
loadChildren: () =>
import('@page/customer').then((m) => m.CustomerModule),
canActivate: [CanActivateCustomerWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'cart',
loadChildren: () => import('@page/checkout').then((m) => m.PageCheckoutModule),
loadChildren: () =>
import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartGuard],
},
{
path: ':processId/cart',
loadChildren: () => import('@page/checkout').then((m) => m.PageCheckoutModule),
loadChildren: () =>
import('@page/checkout').then((m) => m.PageCheckoutModule),
canActivate: [CanActivateCartWithProcessIdGuard],
},
{
path: 'pickup-shelf',
canActivate: [ProcessIdGuard],
// NOTE: This is a workaround for the canActivate guard not being called
loadChildren: () => import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
},
{
path: ':processId/pickup-shelf',
canActivate: [ActivateProcessIdGuard],
loadChildren: () => import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
},
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
],
@@ -119,34 +134,43 @@ const routes: Routes = [
children: [
{
path: 'task-calendar',
loadChildren: () => import('@page/task-calendar').then((m) => m.PageTaskCalendarModule),
loadChildren: () =>
import('@page/task-calendar').then(
(m) => m.PageTaskCalendarModule,
),
canActivate: [CanActivateTaskCalendarGuard],
},
{
path: 'pickup-shelf',
canActivate: [ActivateProcessIdWithConfigKeyGuard('pickupShelf')],
// NOTE: This is a workaround for the canActivate guard not being called
loadChildren: () => import('@page/pickup-shelf').then((m) => m.PickupShelfInModule),
loadChildren: () =>
import('@page/pickup-shelf').then((m) => m.PickupShelfInModule),
},
{
path: 'goods/in',
loadChildren: () => import('@page/goods-in').then((m) => m.GoodsInModule),
loadChildren: () =>
import('@page/goods-in').then((m) => m.GoodsInModule),
canActivate: [CanActivateGoodsInGuard],
},
{
path: 'remission',
loadChildren: () => import('@page/remission').then((m) => m.PageRemissionModule),
loadChildren: () =>
import('@page/remission').then((m) => m.PageRemissionModule),
canActivate: [CanActivateRemissionGuard],
},
{
path: 'package-inspection',
loadChildren: () =>
import('@page/package-inspection').then((m) => m.PackageInspectionModule),
import('@page/package-inspection').then(
(m) => m.PackageInspectionModule,
),
canActivate: [CanActivatePackageInspectionGuard],
},
{
path: 'assortment',
loadChildren: () => import('@page/assortment').then((m) => m.AssortmentModule),
loadChildren: () =>
import('@page/assortment').then((m) => m.AssortmentModule),
canActivate: [CanActivateAssortmentGuard],
},
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
@@ -156,14 +180,20 @@ const routes: Routes = [
],
},
{
path: ':processId',
path: ':tabId',
component: MainComponent,
resolve: { process: processResolverFn },
resolve: { process: tabResolverFn, tab: tabResolverFn },
canActivate: [IsAuthenticatedGuard],
children: [
{
path: 'return',
loadChildren: () => import('@isa/oms/feature/return-search').then((m) => m.routes),
loadChildren: () =>
import('@isa/oms/feature/return-search').then((m) => m.routes),
},
{
path: 'remission',
loadChildren: () =>
import('@isa/remission/feature/remission-list').then((m) => m.routes),
},
],
},

View File

@@ -1,4 +1,4 @@
import { DOCUMENT } from '@angular/common';
import {
Component,
effect,
@@ -10,6 +10,7 @@ import {
Renderer2,
signal,
untracked,
DOCUMENT
} from '@angular/core';
import { Title } from '@angular/platform-browser';
import { SwUpdate } from '@angular/service-worker';

View File

@@ -1,12 +1,22 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { CustomerSearchNavigation } from '@shared/services/navigation';
import { first } from 'rxjs/operators';
import { Injectable } from "@angular/core";
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot,
} from "@angular/router";
import { ApplicationProcess, ApplicationService } from "@core/application";
import { DomainCheckoutService } from "@domain/checkout";
import { logger } from "@isa/core/logging";
import { CustomerSearchNavigation } from "@shared/services/navigation";
import { first } from "rxjs/operators";
@Injectable({ providedIn: 'root' })
@Injectable({ providedIn: "root" })
export class CanActivateCustomerGuard {
#logger = logger(() => ({
context: "CanActivateCustomerGuard",
tags: ["guard", "customer", "navigation"],
}));
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
@@ -14,36 +24,77 @@ export class CanActivateCustomerGuard {
private readonly _navigation: CustomerSearchNavigation,
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
let lastActivatedProcessId = (
async canActivate(
route: ActivatedRouteSnapshot,
{ url }: RouterStateSnapshot,
) {
if (url.startsWith("/kunde/customer/search/")) {
const processId = Date.now(); // Generate a new process ID
// Extract parts before and after the pattern
const parts = url.split("/kunde/customer/");
if (parts.length === 2) {
const prefix = parts[0] + "/kunde/";
const suffix = "customer/" + parts[1];
// Construct the new URL with process ID inserted
const newUrl = `${prefix}${processId}/${suffix}`;
this.#logger.info("Redirecting to URL with process ID", () => ({
originalUrl: url,
newUrl,
processId,
}));
// Navigate to the new URL and prevent original navigation
this._router.navigateByUrl(newUrl);
return false;
}
}
const processes = await this._applicationService
.getProcesses$("customer")
.pipe(first())
.toPromise();
const lastActivatedProcessId = (
await this._applicationService
.getLastActivatedProcessWithSectionAndType$('customer', 'cart')
.getLastActivatedProcessWithSectionAndType$("customer", "cart")
.pipe(first())
.toPromise()
)?.id;
const lastActivatedCartCheckoutProcessId = (
await this._applicationService
.getLastActivatedProcessWithSectionAndType$('customer', 'cart-checkout')
.getLastActivatedProcessWithSectionAndType$("customer", "cart-checkout")
.pipe(first())
.toPromise()
)?.id;
const lastActivatedGoodsOutProcessId = (
await this._applicationService
.getLastActivatedProcessWithSectionAndType$('customer', 'goods-out')
.getLastActivatedProcessWithSectionAndType$("customer", "goods-out")
.pipe(first())
.toPromise()
)?.id;
const activatedProcessId = await this._applicationService.getActivatedProcessId$().pipe(first()).toPromise();
const activatedProcessId = await this._applicationService
.getActivatedProcessId$()
.pipe(first())
.toPromise();
// Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
if (!!lastActivatedCartCheckoutProcessId && lastActivatedCartCheckoutProcessId === activatedProcessId) {
await this.fromCartCheckoutProcess(processes, lastActivatedCartCheckoutProcessId);
if (
!!lastActivatedCartCheckoutProcessId &&
lastActivatedCartCheckoutProcessId === activatedProcessId
) {
await this.fromCartCheckoutProcess(
processes,
lastActivatedCartCheckoutProcessId,
);
return false;
} else if (!!lastActivatedGoodsOutProcessId && lastActivatedGoodsOutProcessId === activatedProcessId) {
} else if (
!!lastActivatedGoodsOutProcessId &&
lastActivatedGoodsOutProcessId === activatedProcessId
) {
await this.fromGoodsOutProcess(processes, lastActivatedGoodsOutProcessId);
return false;
}
@@ -68,25 +119,28 @@ export class CanActivateCustomerGuard {
const newProcessId = Date.now();
await this._applicationService.createProcess({
id: newProcessId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
type: "cart",
section: "customer",
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`,
});
await this.navigateToDefaultRoute(newProcessId);
}
// Bei offener Bestellbestätigung und Klick auf Footer Kundensuche
async fromCartCheckoutProcess(processes: ApplicationProcess[], processId: number) {
async fromCartCheckoutProcess(
processes: ApplicationProcess[],
processId: number,
) {
// Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
this._checkoutService.removeProcess({ processId });
// Ändere type cart-checkout zu cart
this._applicationService.patchProcess(processId, {
id: processId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
type: "cart",
section: "customer",
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`,
data: {},
});
@@ -95,22 +149,31 @@ export class CanActivateCustomerGuard {
}
// Bei offener Warenausgabe und Klick auf Footer Kundensuche
async fromGoodsOutProcess(processes: ApplicationProcess[], processId: number) {
const buyer = await this._checkoutService.getBuyer({ processId }).pipe(first()).toPromise();
const customerFeatures = await this._checkoutService.getCustomerFeatures({ processId }).pipe(first()).toPromise();
async fromGoodsOutProcess(
processes: ApplicationProcess[],
processId: number,
) {
const buyer = await this._checkoutService
.getBuyer({ processId })
.pipe(first())
.toPromise();
const customerFeatures = await this._checkoutService
.getCustomerFeatures({ processId })
.pipe(first())
.toPromise();
const name = buyer
? customerFeatures?.b2b
? buyer.organisation?.name
? buyer.organisation?.name
: buyer.lastName
: buyer.lastName
: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`;
: `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`;
// Ändere type goods-out zu cart
this._applicationService.patchProcess(processId, {
id: processId,
type: 'cart',
section: 'customer',
type: "cart",
section: "customer",
name,
});
@@ -119,12 +182,20 @@ export class CanActivateCustomerGuard {
}
processNumber(processes: ApplicationProcess[]) {
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
return !!processNumbers && processNumbers.length > 0 ? this.findMissingNumber(processNumbers) : 1;
const processNumbers = processes?.map((process) =>
Number(process?.name?.replace(/\D/g, "")),
);
return !!processNumbers && processNumbers.length > 0
? this.findMissingNumber(processNumbers)
: 1;
}
findMissingNumber(processNumbers: number[]) {
for (let missingNumber = 1; missingNumber < Math.max(...processNumbers); missingNumber++) {
for (
let missingNumber = 1;
missingNumber < Math.max(...processNumbers);
missingNumber++
) {
if (!processNumbers.find((number) => number === missingNumber)) {
return missingNumber;
}

View File

@@ -1,7 +1,6 @@
import { Platform, PlatformModule } from '@angular/cdk/platform';
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { Component } from '@angular/core';
import { BranchDTO } from '@generated/swagger/checkout-api';
import { BehaviorSubject } from 'rxjs';
@@ -9,9 +8,9 @@ import { BehaviorSubject } from 'rxjs';
selector: 'app-preview',
templateUrl: 'preview.component.html',
styleUrls: ['preview.component.css'],
imports: [CommonModule, BranchSelectorComponent, PlatformModule],
imports: [CommonModule, PlatformModule],
})
export class PreviewComponent implements OnInit {
export class PreviewComponent {
selectedBranch$ = new BehaviorSubject<BranchDTO>({});
get appVersion() {
@@ -24,7 +23,7 @@ export class PreviewComponent implements OnInit {
get navigator() {
const nav = {};
for (let i in window.navigator) nav[i] = navigator[i];
for (const i in window.navigator) nav[i] = navigator[i];
return nav;
}
@@ -51,8 +50,6 @@ export class PreviewComponent implements OnInit {
constructor(private readonly _platform: Platform) {}
ngOnInit() {}
setNewBranch(branch: BranchDTO) {
this.selectedBranch$.next(branch);
}

View File

@@ -1,13 +1,18 @@
import { coerceArray } from '@angular/cdk/coercion';
import { inject, Injectable } from '@angular/core';
import { Config } from '@core/config';
import { isNullOrUndefined } from '@utils/common';
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
import { BehaviorSubject } from 'rxjs';
import { coerceArray } from "@angular/cdk/coercion";
import { inject, Injectable } from "@angular/core";
import { Config } from "@core/config";
import { isNullOrUndefined } from "@utils/common";
import { AuthConfig, OAuthService } from "angular-oauth2-oidc";
import { JwksValidationHandler } from "angular-oauth2-oidc-jwks";
import { BehaviorSubject } from "rxjs";
/**
* Storage key for the URL to redirect to after login
*/
const REDIRECT_URL_KEY = "auth_redirect_url";
@Injectable({
providedIn: 'root',
providedIn: "root",
})
export class AuthService {
private readonly _initialized = new BehaviorSubject<boolean>(false);
@@ -16,28 +21,39 @@ export class AuthService {
}
private _authConfig: AuthConfig;
constructor(
private _config: Config,
private readonly _oAuthService: OAuthService,
) {
this._oAuthService.events?.subscribe((event) => {
if (event.type === 'token_received') {
console.log('SSO Token Expiration:', new Date(this._oAuthService.getAccessTokenExpiration()));
if (event.type === "token_received") {
console.log(
"SSO Token Expiration:",
new Date(this._oAuthService.getAccessTokenExpiration()),
);
// Handle redirect after successful authentication
setTimeout(() => {
const redirectUrl = this._getAndClearRedirectUrl();
if (redirectUrl) {
window.location.href = redirectUrl;
}
}, 100);
}
});
}
async init() {
if (this._initialized.getValue()) {
throw new Error('AuthService is already initialized');
throw new Error("AuthService is already initialized");
}
this._authConfig = this._config.get('@core/auth');
this._authConfig = this._config.get("@core/auth");
this._authConfig.redirectUri = window.location.origin;
this._authConfig.silentRefreshRedirectUri = window.location.origin + '/silent-refresh.html';
this._authConfig.silentRefreshRedirectUri =
window.location.origin + "/silent-refresh.html";
this._authConfig.useSilentRefresh = true;
this._oAuthService.configure(this._authConfig);
@@ -55,12 +71,18 @@ export class AuthService {
}
isIdTokenValid() {
console.log('ID Token Expiration:', new Date(this._oAuthService.getIdTokenExpiration()));
console.log(
"ID Token Expiration:",
new Date(this._oAuthService.getIdTokenExpiration()),
);
return this._oAuthService.hasValidIdToken();
}
isAccessTokenValid() {
console.log('ACCESS Token Expiration:', new Date(this._oAuthService.getAccessTokenExpiration()));
console.log(
"ACCESS Token Expiration:",
new Date(this._oAuthService.getAccessTokenExpiration()),
);
return this._oAuthService.hasValidAccessToken();
}
@@ -85,14 +107,31 @@ export class AuthService {
if (isNullOrUndefined(token)) {
return null;
}
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const base64Url = token.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const encoded = window.atob(base64);
return JSON.parse(encoded);
}
/**
* Saves the URL to redirect to after successful login
*/
_saveRedirectUrl(): void {
localStorage.setItem(REDIRECT_URL_KEY, window.location.href);
}
/**
* Gets and clears the saved redirect URL
*/
_getAndClearRedirectUrl(): string | null {
const url = localStorage.getItem(REDIRECT_URL_KEY);
localStorage.removeItem(REDIRECT_URL_KEY);
return url;
}
login() {
this._saveRedirectUrl();
this._oAuthService.initLoginFlow();
}
@@ -109,7 +148,7 @@ export class AuthService {
hasRole(role: string | string[]) {
const roles = coerceArray(role);
const userRoles = this.getClaimByKey('role');
const userRoles = this.getClaimByKey("role");
if (isNullOrUndefined(userRoles)) {
return false;
@@ -120,7 +159,10 @@ export class AuthService {
async refresh() {
try {
if (this._authConfig.responseType.includes('code') && this._authConfig.scope.includes('offline_access')) {
if (
this._authConfig.responseType.includes("code") &&
this._authConfig.scope.includes("offline_access")
) {
await this._oAuthService.refreshToken();
} else {
await this._oAuthService.silentRefresh();

View File

@@ -1,5 +1,9 @@
import { Injectable } from '@angular/core';
import { ItemDTO, ListResponseArgsOfItemDTO, SearchService } from '@generated/swagger/cat-search-api';
import {
ItemDTO,
ListResponseArgsOfItemDTO,
SearchService,
} from '@generated/swagger/cat-search-api';
import {
RemiService,
StockService,
@@ -18,7 +22,11 @@ import { memorize } from '@utils/common';
import { Observable, of, throwError } from 'rxjs';
import { catchError, map, shareReplay, switchMap } from 'rxjs/operators';
import { RemissionListItem } from './defs';
import { fromItemDto, mapFromReturnItemDTO, mapFromReturnSuggestionDTO } from './mappings';
import {
fromItemDto,
mapFromReturnItemDTO,
mapFromReturnSuggestionDTO,
} from './mappings';
import { Logger } from '@core/logger';
import { RemissionPlacementType } from '@domain/remission';
@@ -204,7 +212,10 @@ export class DomainRemissionService {
);
}
getStockInformation(items: RemissionListItem[], recalculate: boolean = false) {
getStockInformation(
items: RemissionListItem[],
recalculate: boolean = false,
) {
return this.getCurrentStock().pipe(
switchMap((stock) =>
this._stockService
@@ -218,7 +229,8 @@ export class DomainRemissionService {
map((res) => {
const o = items.map((item) => {
const stockInfo = res?.result?.find(
(stockInfo) => stockInfo.itemId === +item.dto.product.catalogProductNumber,
(stockInfo) =>
stockInfo.itemId === +item.dto.product.catalogProductNumber,
);
if (!stockInfo) {
@@ -231,7 +243,8 @@ export class DomainRemissionService {
return { ...item, ...defaultStockData };
}
const availableStock = stockInfo.inStock - stockInfo.removedFromStock;
const availableStock =
stockInfo.inStock - stockInfo.removedFromStock;
const inStock = availableStock < 0 ? 0 : availableStock;
let { remainingQuantity, remissionQuantity } = item;
@@ -249,7 +262,12 @@ export class DomainRemissionService {
}
}
return { ...item, remainingQuantity, remissionQuantity, inStock };
return {
...item,
remainingQuantity,
remissionQuantity,
inStock,
};
});
return o;
@@ -259,7 +277,10 @@ export class DomainRemissionService {
);
}
getRequiredCapacities(params: { departments?: string[]; supplierId: number }) {
getRequiredCapacities(params: {
departments?: string[];
supplierId: number;
}) {
return this.getCurrentStock().pipe(
switchMap((stock) =>
this._remiService
@@ -301,13 +322,18 @@ export class DomainRemissionService {
);
}
canAddReturnItem(item: ReturnItemDTO): Observable<BatchResponseArgsOfReturnItemDTOAndReturnItemDTO> {
canAddReturnItem(
item: ReturnItemDTO,
): Observable<BatchResponseArgsOfReturnItemDTOAndReturnItemDTO> {
return this._remiService.RemiCanAddReturnItem({
data: [item],
});
}
async createReturn(supplierId: number, returnGroup?: string): Promise<ReturnDTO> {
async createReturn(
supplierId: number,
returnGroup?: string,
): Promise<ReturnDTO> {
const response = await this._returnService
.ReturnCreateReturn({
data: {
@@ -343,7 +369,10 @@ export class DomainRemissionService {
.toPromise();
}
getReturns(params: { start?: Date; returncompleted: boolean }): Observable<ReturnDTO[]> {
getReturns(params: {
start?: Date;
returncompleted: boolean;
}): Observable<ReturnDTO[]> {
const queryToken: ReturnQueryTokenDTO = {
start: params.start?.toISOString(),
filter: {
@@ -360,13 +389,20 @@ export class DomainRemissionService {
});
return this.getCurrentStock().pipe(
switchMap((stock) => this._returnService.ReturnQueryReturns({ stockId: stock.id, queryToken })),
switchMap((stock) =>
this._returnService.ReturnQueryReturns({
stockId: stock.id,
queryToken,
}),
),
map((res) => res.result),
);
}
getReturn(returnId: number): Observable<ReturnDTO> {
return this._returnService.ReturnGetReturn({ returnId, eagerLoading: 3 }).pipe(map((res) => res.result));
return this._returnService
.ReturnGetReturn({ returnId, eagerLoading: 3 })
.pipe(map((res) => res.result));
}
async deleteReturn(returnId: number) {
@@ -393,7 +429,11 @@ export class DomainRemissionService {
inStock: number;
}) {
return this._returnService
.ReturnAddReturnItem({ returnId, receiptId, data: { returnItemId, quantity, placementType, inStock } })
.ReturnAddReturnItem({
returnId,
receiptId,
data: { returnItemId, quantity, placementType, inStock },
})
.pipe(map((r) => r.result));
}
@@ -420,7 +460,14 @@ export class DomainRemissionService {
.ReturnAddReturnSuggestion({
returnId,
receiptId,
data: { returnSuggestionId, quantity, placementType, inStock, impedimentComment, remainingQuantity },
data: {
returnSuggestionId,
quantity,
placementType,
inStock,
impedimentComment,
remainingQuantity,
},
})
.pipe(map((r) => r.result));
}
@@ -438,18 +485,28 @@ export class DomainRemissionService {
receiptId: number;
receiptItemId: number;
}) {
return this._returnService.ReturnRemoveReturnItem({ returnId, receiptItemId, receiptId });
return this._returnService.ReturnRemoveReturnItem({
returnId,
receiptItemId,
receiptId,
});
}
returnImpediment(itemId: number) {
return this._returnService
.ReturnReturnItemImpediment({ itemId, data: { comment: 'Produkt nicht gefunden' } })
.ReturnReturnItemImpediment({
itemId,
data: { comment: 'Produkt nicht gefunden' },
})
.pipe(map((r) => r.result));
}
returnSuggestion(itemId: number) {
return this._returnService
.ReturnReturnSuggestionImpediment({ itemId, data: { comment: 'Produkt nicht gefunden' } })
.ReturnReturnSuggestionImpediment({
itemId,
data: { comment: 'Produkt nicht gefunden' },
})
.pipe(map((r) => r.result));
}
@@ -459,7 +516,10 @@ export class DomainRemissionService {
* @param receiptNumber Receipt number
* @returns ReceiptDTO
*/
async createReceipt(returnDTO: ReturnDTO, receiptNumber?: string): Promise<ReceiptDTO> {
async createReceipt(
returnDTO: ReturnDTO,
receiptNumber?: string,
): Promise<ReceiptDTO> {
const stock = await this._getStock();
const response = await this._returnService
@@ -510,7 +570,10 @@ export class DomainRemissionService {
return receipt;
}
async completeReceipt(returnId: number, receiptId: number): Promise<ReceiptDTO> {
async completeReceipt(
returnId: number,
receiptId: number,
): Promise<ReceiptDTO> {
const res = await this._returnService
.ReturnFinalizeReceipt({
returnId,

View File

@@ -3,10 +3,12 @@
<p>Vorschläge:</p>
<ul class="content">
<li *ngFor="let item of ref?.data">
<span>{{ item.street }} {{ item.streetNumber }}, {{ item.zipCode }} {{ item.city }}</span>
<button (click)="ref.close(item)">Übernehmen</button>
</li>
@for (item of ref?.data; track item) {
<li>
<span>{{ item.street }} {{ item.streetNumber }}, {{ item.zipCode }} {{ item.city }}</span>
<button (click)="ref.close(item)">Übernehmen</button>
</li>
}
</ul>
<div class="center">

View File

@@ -6,13 +6,19 @@
uiSearchboxSearchButton
(click)="filter(input.value)"
[disabled]="branchesFetching$ | async"
>
<ui-icon class="spin" *ngIf="branchesFetching$ | async" icon="spinner" size="32px"></ui-icon>
<ui-icon *ngIf="!(branchesFetching$ | async)" icon="search" size="24px"></ui-icon>
</button>
<button *ngIf="input.value" type="reset" uiSearchboxClearButton (click)="filter(''); cancelSearch(); input.value = ''">
<ui-icon icon="close" size="22px"></ui-icon>
>
@if (branchesFetching$ | async) {
<ui-icon class="spin" icon="spinner" size="32px"></ui-icon>
}
@if (!(branchesFetching$ | async)) {
<ui-icon icon="search" size="24px"></ui-icon>
}
</button>
@if (input.value) {
<button type="reset" uiSearchboxClearButton (click)="filter(''); cancelSearch(); input.value = ''">
<ui-icon icon="close" size="22px"></ui-icon>
</button>
}
</ui-searchbox>
<p class="subtitle">
@@ -25,7 +31,7 @@
<hr />
<div class="branches">
<ng-container *ngFor="let branch of filteredBranches$ | async">
@for (branch of filteredBranches$ | async; track branch) {
<div class="branch">
<div class="branch-info">
<span class="branch-name">
@@ -36,25 +42,23 @@
{{ branch.address.city }}
</span>
</div>
<div class="branch-actions">
<button
*ngIf="(branch.id | stockInfo: (inStock$ | async))?.availableQuantity > 0 && branch?.isShippingEnabled"
class="cta-reserve"
(click)="reserve(branch)"
>
Reservieren
</button>
@if ((branch.id | stockInfo: (inStock$ | async))?.availableQuantity > 0 && branch?.isShippingEnabled) {
<button
class="cta-reserve"
(click)="reserve(branch)"
>
Reservieren
</button>
}
<ui-spinner [show]="stockFetching$ | async">
<span class="branch-stock">
<ui-icon icon="home" size="22px"></ui-icon>
<span>{{ branch.id | inStock: (inStock$ | async) }}x</span>
</span>
</ui-spinner>
</div>
</div>
<hr />
</ng-container>
}
</div>

View File

@@ -3,9 +3,11 @@
</div>
<div class="thumbnails-wrapper">
<button *ngFor="let image of images" (click)="activeImage = image" [class.selected]="activeImage.url === image.url">
<img class="thumbnail" [src]="image.thumbUrl" />
</button>
@for (image of images; track image) {
<button (click)="activeImage = image" [class.selected]="activeImage.url === image.url">
<img class="thumbnail" [src]="image.thumbUrl" />
</button>
}
</div>
<div class="controls">

View File

@@ -18,9 +18,11 @@
<ui-quantity-dropdown [quantity]="itemQuantity$ | async" (quantityChange)="onQuantityChange($event)"></ui-quantity-dropdown>
</div>
<div class="relative">
<div *ngIf="stockWarning$ | async" class="text-warning font-bold absolute right-0 top-0 whitespace-nowrap">
Es befinden sich {{ availableQuantity$ | async }} Exemplare in der Filiale
</div>
@if (stockWarning$ | async) {
<div class="text-warning font-bold absolute right-0 top-0 whitespace-nowrap">
Es befinden sich {{ availableQuantity$ | async }} Exemplare in der Filiale
</div>
}
</div>
</div>
</div>

View File

@@ -20,18 +20,21 @@
</div>
</div>
<div class="overflow-y-auto -mx-4 scroll-bar">
<div *ngIf="emptyShoppingCart$ | async" class="h-full grid items-center justify-center">
<h3 class="text-xl font-bold text-center text-gray-500">
Warenkorb ist leer, bitte suchen oder scannen
<br />
Sie Artikel um den Warenkob zu füllen.
</h3>
</div>
<shared-kulturpass-order-item
class="border-b border-solid border-[#EFF1F5]"
*ngFor="let item of items$ | async; trackBy: trackItemById"
[item]="item"
></shared-kulturpass-order-item>
@if (emptyShoppingCart$ | async) {
<div class="h-full grid items-center justify-center">
<h3 class="text-xl font-bold text-center text-gray-500">
Warenkorb ist leer, bitte suchen oder scannen
<br />
Sie Artikel um den Warenkob zu füllen.
</h3>
</div>
}
@for (item of items$ | async; track trackItemById($index, item)) {
<shared-kulturpass-order-item
class="border-b border-solid border-[#EFF1F5]"
[item]="item"
></shared-kulturpass-order-item>
}
</div>
<div class="flex flex-row justify-evenly items-stretch border-t border-solid border-[#EFF1F5] py-3 px-4 -mx-4">
<div class="grid grid-flow-row text-xl">
@@ -49,13 +52,15 @@
</div>
</div>
<div class="grid items-end justify-between">
<div *ngIf="negativeBalance$ | async" class="text-xl text-warning font-bold text-center">Der Betrag übersteigt ihr Guthaben</div>
@if (negativeBalance$ | async) {
<div class="text-xl text-warning font-bold text-center">Der Betrag übersteigt ihr Guthaben</div>
}
<button
type="button"
class="bg-brand text-white px-6 py-3 font-bold rounded-full disabled:bg-disabled-branch disabled:text-active-branch"
[disabled]="orderButtonDisabled$ | async"
(click)="order()"
>
>
<shared-loader [loading]="ordering$ | async" hideContent="true">Kauf abschließen und Rechnung drucken</shared-loader>
</button>
</div>

View File

@@ -2,19 +2,20 @@
{{ message }}
</p>
<div class="message-modal-actions grid grid-flow-col gap-4 items-center justify-center">
<button
*ngFor="let action of actions"
type="button"
class="btn rounded-full font-bold text-p1 border-[2px] border-solid border-brand px-6"
[class.bg-brand]="action.primary"
[class.hover:bg-brand]="action.primary"
[class.text-white]="action.primary"
[class.bg-white]="!action.primary"
[class.text-brand]="!action.primary"
[class.hover:bg-white]="!action.primary"
[class.hover:text-brand]="!action.primary"
(click)="clickAction(action)"
>
{{ action.label }}
</button>
@for (action of actions; track action) {
<button
type="button"
class="btn rounded-full font-bold text-p1 border-[2px] border-solid border-brand px-6"
[class.bg-brand]="action.primary"
[class.hover:bg-brand]="action.primary"
[class.text-white]="action.primary"
[class.bg-white]="!action.primary"
[class.text-brand]="!action.primary"
[class.hover:bg-white]="!action.primary"
[class.hover:text-brand]="!action.primary"
(click)="clickAction(action)"
>
{{ action.label }}
</button>
}
</div>

View File

@@ -1,4 +1,4 @@
import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { UiModalRef } from '@ui/modal';
import { MessageModalData } from './message-modal.data';
@@ -10,7 +10,7 @@ import { MessageModalAction } from './message-modal.action';
styleUrls: ['message-modal.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'shared-message-modal' },
imports: [CommonModule],
imports: [],
})
export class MessageModalComponent {
get message() {

View File

@@ -4,8 +4,10 @@
<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>
@if (editButton) {
<button class="notification-edit-cta text-brand font-bold text-lg px-4 py-3" (click)="itemSelected.emit(item)">
{{ editButtonLabel }}
</button>
}
</div>
</div>

View File

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

View File

@@ -1,8 +1,8 @@
<div class="notification-list scroll-bar">
<ng-container *ngFor="let notification of notifications">
@for (notification of notifications; track notification) {
<modal-notifications-list-item [item]="notification" (itemSelected)="itemSelected($event)"></modal-notifications-list-item>
<hr />
</ng-container>
}
</div>
<div class="actions">

View File

@@ -1,8 +1,8 @@
<div class="notification-list scroll-bar">
<ng-container *ngFor="let notification of notifications">
@for (notification of notifications; track notification) {
<modal-notifications-list-item [item]="notification" (itemSelected)="itemSelected($event)"></modal-notifications-list-item>
<hr />
</ng-container>
}
</div>
<div class="actions">

View File

@@ -1,8 +1,8 @@
<div class="notification-list scroll-bar">
<ng-container *ngFor="let notification of notifications">
@for (notification of notifications; track notification) {
<modal-notifications-list-item [item]="notification" (itemSelected)="itemSelected($event)"></modal-notifications-list-item>
<hr />
</ng-container>
}
</div>
<div class="actions">

View File

@@ -1,11 +1,11 @@
<div class="notification-list scroll-bar">
<ng-container *ngFor="let notification of notifications">
@for (notification of notifications; track notification) {
<div class="notification-headline">
<h1>{{ notification.headline }}</h1>
</div>
<div class="notification-text">{{ notification.text }}</div>
<hr />
</ng-container>
}
</div>
<div class="actions">

View File

@@ -1,6 +1,6 @@
<h1>Sie haben neue Nachrichten</h1>
<ng-container *ngFor="let notification of notifications$ | async | keyvalue">
@for (notification of notifications$ | async | keyvalue; track notification) {
<button type="button" class="notification-card" (click)="selectArea(notification.key)">
<div class="notification-icon">
<div class="notification-counter">{{ notification.value?.length }}</div>
@@ -9,30 +9,37 @@
<span>{{ notification.value?.[0]?.category }}</span>
</button>
<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>
@if (notification.key === selectedArea) {
@switch (notification.value?.[0]?.category) {
@case ('ISA-Update') {
<modal-notifications-update-group
[notifications]="notifications[selectedArea]"
></modal-notifications-update-group>
}
@case ('Reservierungsanfragen') {
<modal-notifications-reservation-group
[notifications]="notifications[selectedArea]"
(navigated)="close()"
></modal-notifications-reservation-group>
}
@case ('Remission') {
<modal-notifications-remission-group
[notifications]="notifications[selectedArea]"
(navigated)="close()"
></modal-notifications-remission-group>
}
@case ('Tätigkeitskalender') {
<modal-notifications-task-calendar-group
[notifications]="notifications[selectedArea]"
(navigated)="close()"
></modal-notifications-task-calendar-group>
}
@case ('Wareneingang Lagerware') {
<modal-notifications-package-inspection-group
[notifications]="notifications[selectedArea]"
(navigated)="close()"
></modal-notifications-package-inspection-group>
}
}
}
}

View File

@@ -2,13 +2,17 @@
<div class="header">
<h1>Wählen Sie einen Drucker aus</h1>
<span *ngIf="error" class="error-message">{{ errorMessage }}</span>
@if (error) {
<span class="error-message">{{ errorMessage }}</span>
}
</div>
<div class="body">
<ui-spinner [show]="!loaded">
<ui-select class="select" [(ngModel)]="selectedPrinterKey">
<ui-select-option *ngFor="let printer of printers$ | async" [label]="printer.description" [value]="printer.key"></ui-select-option>
@for (printer of printers$ | async; track printer) {
<ui-select-option [label]="printer.description" [value]="printer.key"></ui-select-option>
}
</ui-select>
</ui-spinner>
</div>

View File

@@ -1,5 +1,9 @@
<div class="flex flex-col text-right" [class.hidden]="hideHeader$ | async">
<button type="button" class="font-bold text-[#0556B4]" *ngIf="selectButton$ | async" (click)="selectAll()">Alle auswählen</button>
<button type="button" class="font-bold text-[#0556B4]" *ngIf="unselectButton$ | async" (click)="unselectAll()">Alle abwählen</button>
@if (selectButton$ | async) {
<button type="button" class="font-bold text-[#0556B4]" (click)="selectAll()">Alle auswählen</button>
}
@if (unselectButton$ | async) {
<button type="button" class="font-bold text-[#0556B4]" (click)="unselectAll()">Alle abwählen</button>
}
<span class="mt-2">{{ selectedItemsCount$ | async }} von {{ itemsCount$ | async }} Artikel</span>
</div>

View File

@@ -15,147 +15,171 @@
</div>
<div class="shared-purchase-options-list-item__manufacturer-and-ean">
{{ product?.manufacturer }}
<span *ngIf="product?.manufacturer && product?.ean">|</span>
@if (product?.manufacturer && product?.ean) {
<span>|</span>
}
{{ product?.ean }}
</div>
<div class="shared-purchase-options-list-item__volume-and-publication-date">
{{ product?.volume }}
<span *ngIf="product?.volume && product?.publicationDate">|</span>
@if (product?.volume && product?.publicationDate) {
<span>|</span>
}
{{ product?.publicationDate | date: 'dd. MMMM yyyy' }}
</div>
<div class="shared-purchase-options-list-item__availabilities mt-3 grid grid-flow-row gap-2 justify-start">
<div class="whitespace-nowrap self-center" *ngIf="(availabilities$ | async)?.length">Verfügbar als</div>
<div *ngFor="let availability of availabilities$ | async" class="grid grid-flow-col gap-4 justify-start">
<div
[ngSwitch]="availability.purchaseOption"
class="shared-purchase-options-list-item__availability grid grid-flow-col gap-2 items-center"
[attr.data-option]="availability.purchaseOption"
>
<ng-container *ngSwitchCase="'delivery'">
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
{{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
-
{{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
</ng-container>
<ng-container *ngSwitchCase="'dig-delivery'">
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
{{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
-
{{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
</ng-container>
<ng-container *ngSwitchCase="'b2b-delivery'">
<shared-icon icon="isa-b2b-truck" [size]="24"></shared-icon>
{{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
</ng-container>
<ng-container *ngSwitchCase="'pickup'">
<shared-icon
class="cursor-pointer"
#uiOverlayTrigger="uiOverlayTrigger"
[uiOverlayTrigger]="orderDeadlineTooltip"
[class.tooltip-active]="uiOverlayTrigger.opened"
icon="isa-box-out"
[size]="18"
></shared-icon>
{{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
<ui-tooltip
#orderDeadlineTooltip
yPosition="above"
xPosition="after"
[yOffset]="-12"
[xOffset]="4"
[warning]="true"
[closeable]="true"
@if ((availabilities$ | async)?.length) {
<div class="whitespace-nowrap self-center">Verfügbar als</div>
}
@for (availability of availabilities$ | async; track availability) {
<div class="grid grid-flow-col gap-4 justify-start">
<div
class="shared-purchase-options-list-item__availability grid grid-flow-col gap-2 items-center"
[attr.data-option]="availability.purchaseOption"
>
<b>{{ availability.data?.orderDeadline | orderDeadline }}</b>
</ui-tooltip>
</ng-container>
<ng-container *ngSwitchCase="'in-store'">
<shared-icon icon="isa-shopping-bag" [size]="18"></shared-icon>
{{ availability.data.inStock }}x
<ng-container *ngIf="isEVT; else noEVT">ab {{ isEVT | date: 'dd. MMMM yyyy' }}</ng-container>
<ng-template #noEVT>ab sofort</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'download'">
<shared-icon icon="isa-download" [size]="22"></shared-icon>
Download
</ng-container>
@switch (availability.purchaseOption) {
@case ('delivery') {
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
{{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
-
{{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
}
@case ('dig-delivery') {
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
{{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
-
{{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
}
@case ('b2b-delivery') {
<shared-icon icon="isa-b2b-truck" [size]="24"></shared-icon>
{{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
}
@case ('pickup') {
<shared-icon
class="cursor-pointer"
#uiOverlayTrigger="uiOverlayTrigger"
[uiOverlayTrigger]="orderDeadlineTooltip"
[class.tooltip-active]="uiOverlayTrigger.opened"
icon="isa-box-out"
[size]="18"
></shared-icon>
{{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
<ui-tooltip
#orderDeadlineTooltip
yPosition="above"
xPosition="after"
[yOffset]="-12"
[xOffset]="4"
[warning]="true"
[closeable]="true"
>
<b>{{ availability.data?.orderDeadline | orderDeadline }}</b>
</ui-tooltip>
}
@case ('in-store') {
<shared-icon icon="isa-shopping-bag" [size]="18"></shared-icon>
{{ availability.data.inStock }}x
@if (isEVT) {
ab {{ isEVT | date: 'dd. MMMM yyyy' }}
} @else {
ab sofort
}
}
@case ('download') {
<shared-icon icon="isa-download" [size]="22"></shared-icon>
Download
}
}
</div>
</div>
</div>
}
</div>
</div>
<div class="shared-purchase-options-list-item__price text-right ml-4 flex flex-col items-end">
<div class="shared-purchase-options-list-item__price-value font-bold text-xl flex flex-row items-center">
<div class="relative flex flex-row justify-end items-start">
<ui-select
*ngIf="canEditVat$ | async"
class="w-[6.5rem] min-h-[3.4375rem] p-4 rounded-card border border-solid border-[#AEB7C1] mr-4"
tabindex="-1"
[formControl]="manualVatFormControl"
[defaultLabel]="'MwSt'"
>
<ui-select-option *ngFor="let vat of vats$ | async" [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>
</ui-select>
<shared-input-control
[class.ml-6]="priceFormControl?.invalid && priceFormControl?.dirty"
*ngIf="canEditPrice$ | async; else priceTmpl"
>
<shared-input-control-indicator>
<shared-icon *ngIf="priceFormControl?.invalid && priceFormControl?.dirty" icon="mat-info"></shared-icon>
</shared-input-control-indicator>
<input
[uiOverlayTrigger]="giftCardTooltip"
triggerOn="none"
#quantityInput
#priceOverlayTrigger="uiOverlayTrigger"
sharedInputControlInput
type="string"
class="w-24"
[formControl]="priceFormControl"
placeholder="00,00"
(sharedOnInit)="onPriceInputInit(quantityInput, priceOverlayTrigger)"
sharedNumberValue
/>
<shared-input-control-suffix>EUR</shared-input-control-suffix>
<shared-input-control-error error="required">Preis ist ungültig</shared-input-control-error>
<shared-input-control-error error="pattern">Preis ist ungültig</shared-input-control-error>
<shared-input-control-error error="min">Preis ist ungültig</shared-input-control-error>
<shared-input-control-error error="max">Preis ist ungültig</shared-input-control-error>
</shared-input-control>
@if (canEditVat$ | async) {
<ui-select
class="w-[6.5rem] min-h-[3.4375rem] p-4 rounded-card border border-solid border-[#AEB7C1] mr-4"
tabindex="-1"
[formControl]="manualVatFormControl"
[defaultLabel]="'MwSt'"
>
@for (vat of vats$ | async; track vat) {
<ui-select-option [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>
}
</ui-select>
}
@if (canEditPrice$ | async) {
<shared-input-control
[class.ml-6]="priceFormControl?.invalid && priceFormControl?.dirty"
>
<shared-input-control-indicator>
@if (priceFormControl?.invalid && priceFormControl?.dirty) {
<shared-icon icon="mat-info"></shared-icon>
}
</shared-input-control-indicator>
<input
[uiOverlayTrigger]="giftCardTooltip"
triggerOn="none"
#quantityInput
#priceOverlayTrigger="uiOverlayTrigger"
sharedInputControlInput
type="string"
class="w-24"
[formControl]="priceFormControl"
placeholder="00,00"
(sharedOnInit)="onPriceInputInit(quantityInput, priceOverlayTrigger)"
sharedNumberValue
/>
<shared-input-control-suffix>EUR</shared-input-control-suffix>
<shared-input-control-error error="required">Preis ist ungültig</shared-input-control-error>
<shared-input-control-error error="pattern">Preis ist ungültig</shared-input-control-error>
<shared-input-control-error error="min">Preis ist ungültig</shared-input-control-error>
<shared-input-control-error error="max">Preis ist ungültig</shared-input-control-error>
</shared-input-control>
} @else {
{{ priceValue$ | async | currency: 'EUR' : 'code' }}
}
<ui-tooltip [warning]="true" xPosition="after" yPosition="below" [xOffset]="-55" [yOffset]="18" [closeable]="true" #giftCardTooltip>
Tragen Sie hier den
<br />
Gutscheinbetrag ein.
</ui-tooltip>
<ui-tooltip [warning]="true" xPosition="after" yPosition="below" [xOffset]="-55" [yOffset]="18" [closeable]="true" #giftCardTooltip>
Tragen Sie hier den
<br />
Gutscheinbetrag ein.
</ui-tooltip>
</div>
</div>
<ui-quantity-dropdown class="mt-2" [formControl]="quantityFormControl" [range]="maxSelectableQuantity$ | async"></ui-quantity-dropdown>
<div class="pt-7">
@if ((canAddResult$ | async)?.canAdd) {
<input
class="fancy-checkbox"
[class.checked]="selectedFormControl?.value"
[formControl]="selectedFormControl"
type="checkbox"
/>
}
</div>
<ng-template #priceTmpl>
{{ priceValue$ | async | currency: 'EUR' : 'code' }}
</ng-template>
</div>
<ui-quantity-dropdown class="mt-2" [formControl]="quantityFormControl" [range]="maxSelectableQuantity$ | async"></ui-quantity-dropdown>
<div class="pt-7">
<input
*ngIf="(canAddResult$ | async)?.canAdd"
class="fancy-checkbox"
[class.checked]="selectedFormControl?.value"
[formControl]="selectedFormControl"
type="checkbox"
/>
</div>
<ng-container *ngIf="canAddResult$ | async; let canAddResult">
<span *ngIf="!canAddResult.canAdd" class="inline-block font-bold text-[#BE8100] mt-[14px] max-w-[19rem]">
{{ canAddResult.message }}
</span>
</ng-container>
@if (canAddResult$ | async; as canAddResult) {
@if (!canAddResult.canAdd) {
<span class="inline-block font-bold text-[#BE8100] mt-[14px] max-w-[19rem]">
{{ canAddResult.message }}
</span>
}
}
<span *ngIf="showMaxAvailableQuantity$ | async" class="font-bold text-[#BE8100] mt-[14px]">
{{ (availability$ | async)?.inStock }} Exemplare sofort lieferbar
</span>
<span *ngIf="showNotAvailable$ | async" class="font-bold text-[#BE8100] mt-[14px]">Derzeit nicht bestellbar</span>
@if (showMaxAvailableQuantity$ | async) {
<span class="font-bold text-[#BE8100] mt-[14px]">
{{ (availability$ | async)?.inStock }} Exemplare sofort lieferbar
</span>
}
@if (showNotAvailable$ | async) {
<span class="font-bold text-[#BE8100] mt-[14px]">Derzeit nicht bestellbar</span>
}
</div>
</div>
<div class="flex flex-row">
<div class="w-16"></div>
<div class="grow shared-purchase-options-list-item__availabilities"></div>
</div>
</div>
<div class="flex flex-row">
<div class="w-16"></div>
<div class="grow shared-purchase-options-list-item__availabilities"></div>
</div>

View File

@@ -1,28 +1,37 @@
<h3 class="text-center font-bold text-h3">Lieferung auswählen</h3>
<p class="text-center font-2xl mt-4">Wie möchten Sie die Artikel erhalten?</p>
<div class="rounded p-4 shadow-card mt-4 grid grid-flow-col gap-4 justify-center items-center relative">
<ng-container *ngIf="!(isDownloadOnly$ | async)">
<ng-container *ngIf="!(isGiftCardOnly$ | async)">
<app-in-store-purchase-options-tile *ngIf="showOption('in-store')"></app-in-store-purchase-options-tile>
<app-pickup-purchase-options-tile *ngIf="showOption('pickup')"></app-pickup-purchase-options-tile>
</ng-container>
<app-delivery-purchase-options-tile *ngIf="showOption('delivery')"></app-delivery-purchase-options-tile>
</ng-container>
@if (!(isDownloadOnly$ | async)) {
@if (!(isGiftCardOnly$ | async)) {
@if (showOption('in-store')) {
<app-in-store-purchase-options-tile></app-in-store-purchase-options-tile>
}
@if (showOption('pickup')) {
<app-pickup-purchase-options-tile></app-pickup-purchase-options-tile>
}
}
@if (showOption('delivery')) {
<app-delivery-purchase-options-tile></app-delivery-purchase-options-tile>
}
}
<ng-container *ngIf="hasDownload$ | async">
<app-download-purchase-options-tile *ngIf="showOption('download')"></app-download-purchase-options-tile>
</ng-container>
@if (hasDownload$ | async) {
@if (showOption('download')) {
<app-download-purchase-options-tile></app-download-purchase-options-tile>
}
}
</div>
<shared-purchase-options-list-header></shared-purchase-options-list-header>
<div class="shared-purchase-options-modal__items -mx-4">
<shared-purchase-options-list-item
class="border-t border-gray-200 p-4 border-solid"
*ngFor="let item of items$ | async; trackBy: itemTrackBy"
[item]="item"
></shared-purchase-options-list-item>
@for (item of items$ | async; track itemTrackBy($index, item)) {
<shared-purchase-options-list-item
class="border-t border-gray-200 p-4 border-solid"
[item]="item"
></shared-purchase-options-list-item>
}
</div>
<div class="text-center -mx-4 border-t border-gray-200 p-4 border-solid">
<ng-container *ngIf="type === 'add'">
@if (type === 'add') {
<button type="button" class="isa-cta-button" [disabled]="!(canContinue$ | async) || saving" (click)="save('continue-shopping')">
Weiter einkaufen
</button>
@@ -31,18 +40,18 @@
class="ml-4 isa-cta-button isa-button-primary"
[disabled]="!(canContinue$ | async) || saving"
(click)="save('continue')"
>
>
Fortfahren
</button>
</ng-container>
<ng-container *ngIf="type === 'update'">
}
@if (type === 'update') {
<button
type="button"
class="ml-4 isa-cta-button isa-button-primary"
[disabled]="!(canContinue$ | async) || saving"
(click)="save('continue')"
>
>
Fortfahren
</button>
</ng-container>
}
</div>

View File

@@ -1,4 +1,4 @@
import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { PurchaseOptionsStore } from '../store';
import { BasePurchaseOptionDirective } from './base-purchase-option.directive';
@@ -9,7 +9,7 @@ import { IconComponent } from '@shared/components/icon';
templateUrl: 'download-purchase-options-tile.component.html',
styleUrls: ['purchase-options-tile.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, IconComponent],
imports: [IconComponent],
})
export class DownloadPurchaseOptionTileComponent extends BasePurchaseOptionDirective {
constructor(

View File

@@ -2,30 +2,31 @@
<hr />
<ng-container *ngIf="orderItem$ | async; let orderItem">
@if (orderItem$ | async; as orderItem) {
<div class="header">
<img
class="thumbnail"
loading="lazy"
*ngIf="orderItem?.product?.ean | productImage; let productImage"
[src]="productImage"
[alt]="orderItem?.product?.name"
/>
@if (orderItem?.product?.ean | productImage; as productImage) {
<img
class="thumbnail"
loading="lazy"
[src]="productImage"
[alt]="orderItem?.product?.name"
/>
}
<div class="details">
<div class="product-name">{{ orderItem.product?.name }}</div>
<div *ngIf="orderItem.product?.format && orderItem.product.formatDetail" class="product-format">
<img class="format-icon" [src]="'/assets/images/Icon_' + orderItem.product?.format + '.svg'" alt="format icon" />
{{ orderItem.product.formatDetail }}
</div>
@if (orderItem.product?.format && orderItem.product.formatDetail) {
<div class="product-format">
<img class="format-icon" [src]="'/assets/images/Icon_' + orderItem.product?.format + '.svg'" alt="format icon" />
{{ orderItem.product.formatDetail }}
</div>
}
<div class="product-ean">
{{ orderItem.product?.ean }}
</div>
<div class="quantity">{{ orderItem.quantity }}x</div>
</div>
</div>
<ng-container *ngIf="availabilities$ | async; let availabilities; else: showLoadingSpinner">
@if (availabilities$ | async; as availabilities) {
<div class="supplier-grid">
<span></span>
<span class="number">Bestand</span>
@@ -34,53 +35,56 @@
<span>vsl. Lieferdatum</span>
<span class="number">Preis</span>
<span></span>
<ng-container *ngFor="let availability of availabilities; let i = index">
<ng-container *ngIf="availability">
@for (availability of availabilities; track availability; let i = $index) {
@if (availability) {
<span class="first-cell">{{ availability.supplier | supplierName }}</span>
<span class="number">{{ availability.qty || 0 }}</span>
<span>{{ availability.ssc }}</span>
<span>
<ui-checkbox *ngIf="availability.supplier !== 'F'" [(ngModel)]="availability.isPrebooked"></ui-checkbox>
@if (availability.supplier !== 'F') {
<ui-checkbox [(ngModel)]="availability.isPrebooked"></ui-checkbox>
}
</span>
<span>{{ availability.at | date: 'dd.MM.yy' }}</span>
<span class="number">{{ availability.price?.value?.value | currency: 'EUR' : 'code' }}</span>
<span>
<ui-select-bullet
*ngIf="availability.supplier !== 'F' || availability.qty > 0"
[(ngModel)]="checkedSupplier"
[value]="availability.supplier"
(ngModelChange)="checked($event, availability)"
></ui-select-bullet>
@if (availability.supplier !== 'F' || availability.qty > 0) {
<ui-select-bullet
[(ngModel)]="checkedSupplier"
[value]="availability.supplier"
(ngModelChange)="checked($event, availability)"
></ui-select-bullet>
}
</span>
</ng-container>
</ng-container>
}
}
</div>
<ng-container *ngIf="storeAvailabilityError$ | async">
@if (storeAvailabilityError$ | async) {
<div class="availability-error">Lieferantenbestand nicht verfügbar</div>
<hr />
</ng-container>
<ng-container *ngIf="takeAwayAvailabilityError$ | async">
}
@if (takeAwayAvailabilityError$ | async) {
<div class="availability-error">Filialbestand nicht verfügbar</div>
<hr />
</ng-container>
<div class="reason" *ngIf="showReasons$ | async">
<button class="reason-dropdown" [uiOverlayTrigger]="statusDropdown" #dropdown="uiOverlayTrigger">
{{ selectedReason || 'Warum wird nachbestellt?' }}
<ui-icon [rotate]="dropdown.opened ? '270deg' : '90deg'" icon="arrow_head"></ui-icon>
</button>
<ui-dropdown #statusDropdown yPosition="above" xPosition="after" [xOffset]="8">
<button uiDropdownItem *ngFor="let reason of reorderReasons$ | async" (click)="selectedReason = reason.value; dropdown.close()">
{{ reason.value }}
}
@if (showReasons$ | async) {
<div class="reason">
<button class="reason-dropdown" [uiOverlayTrigger]="statusDropdown" #dropdown="uiOverlayTrigger">
{{ selectedReason || 'Warum wird nachbestellt?' }}
<ui-icon [rotate]="dropdown.opened ? '270deg' : '90deg'" icon="arrow_head"></ui-icon>
</button>
</ui-dropdown>
<span *ngIf="showReasonError$ | async" class="error">Bitte wählen Sie einen Grund für das nachbestellen</span>
</div>
<ui-dropdown #statusDropdown yPosition="above" xPosition="after" [xOffset]="8">
@for (reason of reorderReasons$ | async; track reason) {
<button uiDropdownItem (click)="selectedReason = reason.value; dropdown.close()">
{{ reason.value }}
</button>
}
</ui-dropdown>
@if (showReasonError$ | async) {
<span class="error">Bitte wählen Sie einen Grund für das nachbestellen</span>
}
</div>
}
<div class="actions">
<button class="cta-not-available cta-action-secondary" [disabled]="ctaDisabled$ | async" (click)="notAvailable()">
<ui-spinner [show]="ctaDisabled$ | async">Nicht lieferbar</ui-spinner>
@@ -89,9 +93,7 @@
<ui-spinner [show]="ctaDisabled$ | async">Bestellen</ui-spinner>
</button>
</div>
</ng-container>
<ng-template #showLoadingSpinner>
} @else {
<ui-spinner class="load-spinner" [show]="true"></ui-spinner>
</ng-template>
</ng-container>
}
}

View File

@@ -5,7 +5,7 @@
<div class="reviews">
<hr />
<ng-container *ngFor="let review of reviews">
@for (review of reviews; track review) {
<div class="review">
<div class="row">
<ui-stars [rating]="review.rating"></ui-stars>
@@ -16,21 +16,29 @@
</div>
<div class="row">
<span>
<span class="text" *ngIf="expandIds.indexOf(review.id) === -1">{{ review.text | substr: 150 }}</span>
<span class="text" *ngIf="expandIds.indexOf(review.id) > -1">{{ review.text }}</span>
@if (expandIds.indexOf(review.id) === -1) {
<span class="text">{{ review.text | substr: 150 }}</span>
}
@if (expandIds.indexOf(review.id) > -1) {
<span class="text">{{ review.text }}</span>
}
</span>
</div>
<div class="row right">
<button *ngIf="expandIds.indexOf(review.id) === -1" class="btn-expand" (click)="expand(review.id)">
Mehr
<ui-icon icon="arrow"></ui-icon>
</button>
<button *ngIf="expandIds.indexOf(review.id) > -1" class="btn-collapse" (click)="expand(review.id)">
<ui-icon icon="arrow" rotate="180deg"></ui-icon>
Weniger
</button>
@if (expandIds.indexOf(review.id) === -1) {
<button class="btn-expand" (click)="expand(review.id)">
Mehr
<ui-icon icon="arrow"></ui-icon>
</button>
}
@if (expandIds.indexOf(review.id) > -1) {
<button class="btn-collapse" (click)="expand(review.id)">
<ui-icon icon="arrow" rotate="180deg"></ui-icon>
Weniger
</button>
}
</div>
</div>
<hr />
</ng-container>
}
</div>

View File

@@ -1,6 +1,6 @@
<div
class="page-price-update-item__item-header flex flex-row w-full items-center justify-between bg-[rgba(0,128,121,0.15)] mb-px-2 px-5 h-[53px] rounded-t"
>
>
<p class="page-price-update-item__item-instruction font-bold text-lg">{{ item?.task?.instruction }}</p>
<p class="page-price-update-item__item-due-date text-p2">
gültig ab
@@ -10,13 +10,14 @@
<div class="page-price-update-item__item-card p-5 bg-white">
<div class="page-price-update-item__item-thumbnail text-center mr-4 w-[47px] h-[73px]">
<img
class="page-price-update-item__item-image w-[47px] max-h-[73px]"
loading="lazy"
*ngIf="item?.product?.ean | productImage; let productImage"
[src]="productImage"
[alt]="item?.product?.name"
/>
@if (item?.product?.ean | productImage; as productImage) {
<img
class="page-price-update-item__item-image w-[47px] max-h-[73px]"
loading="lazy"
[src]="productImage"
[alt]="item?.product?.name"
/>
}
</div>
<div class="page-price-update-item__item-details">
@@ -31,28 +32,33 @@
[class.text-md]="item?.product?.name?.length >= 50"
[class.text-p3]="item?.product?.name?.length >= 60"
[class.text-xs]="item?.product?.name?.length >= 100"
>
>
{{ item?.product?.name }}
</div>
<div class="page-price-update-item__item-format">
<div *ngIf="item?.product?.format && item?.product?.formatDetail" class="font-bold flex flex-row">
<img
class="mr-3"
*ngIf="item?.product?.format !== '--'"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ environment.isTablet() ? (item?.product?.formatDetail | substr: 25) : item?.product?.formatDetail }}
</div>
@if (item?.product?.format && item?.product?.formatDetail) {
<div class="font-bold flex flex-row">
@if (item?.product?.format !== '--') {
<img
class="mr-3"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
}
{{ environment.isTablet() ? (item?.product?.formatDetail | substr: 25) : item?.product?.formatDetail }}
</div>
}
</div>
<div class="page-price-update-item__item-misc">
{{ environment.isTablet() ? (item?.product?.manufacturer | substr: 18) : item?.product?.manufacturer }} | {{ item?.product?.ean }}
<br />
{{ item?.product?.volume }}
<span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
@if (item?.product?.volume && item?.product?.publicationDate) {
<span>|</span>
}
{{ publicationDate }}
</div>
</div>
@@ -69,18 +75,21 @@
</div>
<div class="page-price-update-item__item-select-bullet">
<input *ngIf="isSelectable" [ngModel]="selected" (ngModelChange)="setSelected()" class="isa-select-bullet" type="checkbox" />
@if (isSelectable) {
<input [ngModel]="selected" (ngModelChange)="setSelected()" class="isa-select-bullet" type="checkbox" />
}
</div>
<div class="page-price-update-item__item-stock flex flex-row font-bold">
<ui-icon class="mt-px-2 mr-1" 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>
@if (inStock$ | async; as stock) {
<span
[class.skeleton]="stock?.inStock === undefined"
class="min-w-[1rem] text-right inline-block"
>
{{ stock?.inStock }}
</span>
}
x
</div>
</div>

View File

@@ -4,20 +4,26 @@
(click)="print()"
type="button"
class="page-price-update-list__print-cta text-lg font-bold text-[#F70400] pr-5 mb-3"
>
>
Drucken
</button>
<div class="flex flex-row items-center justify-end">
<div *ngIf="getSelectableItems().length > 0" class="text-[#0556B4] font-bold text-p3 mr-5">
<ng-container *ngIf="selectedItemUids$ | async; let selectedItems">
<button class="page-price-update-list__cta-unselect-all" *ngIf="selectedItems?.length > 0" type="button" (click)="unselectAll()">
Alle entfernen ({{ selectedItems?.length }})
</button>
<button class="page-price-update-list__cta-select-all" type="button" (click)="selectAll()" *ngIf="selectedItems?.length === 0">
Alle auswählen ({{ getSelectableItems().length }})
</button>
</ng-container>
</div>
@if (getSelectableItems().length > 0) {
<div class="text-[#0556B4] font-bold text-p3 mr-5">
@if (selectedItemUids$ | async; as selectedItems) {
@if (selectedItems?.length > 0) {
<button class="page-price-update-list__cta-unselect-all" type="button" (click)="unselectAll()">
Alle entfernen ({{ selectedItems?.length }})
</button>
}
@if (selectedItems?.length === 0) {
<button class="page-price-update-list__cta-select-all" type="button" (click)="selectAll()">
Alle auswählen ({{ getSelectableItems().length }})
</button>
}
}
</div>
}
<div class="page-price-update-list__items-count inline-flex flex-row items-center pr-5 text-p3">
{{ items?.length ?? 0 }}
Titel
@@ -32,38 +38,41 @@
<div class="items scroll-bar">
@for (item of items; track item.uId; let first = $first) {
@defer (on viewport) {
<page-price-update-item [item]="item" [selected]="isSelected(item)" [class.mt-px-10]="!first"></page-price-update-item>
<page-price-update-item [item]="item" [selected]="isSelected(item)" [class.mt-px-10]="!first"></page-price-update-item>
} @placeholder {
<page-price-update-item-loader></page-price-update-item-loader>
}
<page-price-update-item-loader></page-price-update-item-loader>
}
}
<page-price-update-item-loader *ngIf="fetching"></page-price-update-item-loader>
<div class="h-28"></div>
@if (fetching) {
<page-price-update-item-loader></page-price-update-item-loader>
}
<div class="h-28"></div>
</div>
<!-- <cdk-virtual-scroll-viewport #scrollContainer [itemSize]="267" minBufferPx="1200" maxBufferPx="1200" class="scroll-bar">
<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>
<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>
<page-price-update-item-loader *ngIf="fetching"> </page-price-update-item-loader>
<page-price-update-item-loader *ngIf="fetching"> </page-price-update-item-loader>
<div class="h-28"></div>
<div class="h-28"></div>
</cdk-virtual-scroll-viewport> -->
<div class="page-price-update-list__action-wrapper">
<button
[@cta]
*ngIf="!fetching"
[disabled]="(selectedItemUids$ | async).length === 0 || (loading$ | async)"
class="page-price-update-list__complete-items isa-button isa-cta-button isa-button-primary px-11"
type="button"
(click)="onComplete()"
>
<ui-spinner [show]="loading$ | async">Erledigt</ui-spinner>
</button>
@if (!fetching) {
<button
[@cta]
[disabled]="(selectedItemUids$ | async).length === 0 || (loading$ | async)"
class="page-price-update-list__complete-items isa-button isa-cta-button isa-button-primary px-11"
type="button"
(click)="onComplete()"
>
<ui-spinner [show]="loading$ | async">Erledigt</ui-spinner>
</button>
}
</div>

View File

@@ -5,17 +5,20 @@
id="asortment-filter-button"
class="absolute right-0 top-0 h-14 rounded px-5 text-lg bg-cadet-blue flex flex-row flex-nowrap items-center justify-center"
type="button"
>
>
<shared-icon class="mr-2" icon="filter-variant"></shared-icon>
Filter
</button>
</div>
<page-price-update-list
*ngIf="showList$ | async; else noResults"
[items]="store.items$ | async"
[fetching]="store.fetching$ | async"
></page-price-update-list>
@if (showList$ | async) {
<page-price-update-list
[items]="store.items$ | async"
[fetching]="store.fetching$ | async"
></page-price-update-list>
} @else {
<div class="bg-white text-h3 text-center pt-10 font-bold rounded-b h-[calc(100vh_-_370px)]">Keine Preisänderungen vorhanden.</div>
}
<shell-filter-overlay #filterOverlay class="relative">
<div class="relative">
@@ -26,22 +29,23 @@
<h3 class="text-3xl text-center font-bold mt-8">Filter</h3>
<ui-filter
*ngIf="filterOverlay.isOpen"
#filter
class="mx-4"
[filter]="store.pendingFilter$ | async"
(search)="applyFilter()"
[loading]="store.fetching$ | async"
[hint]="hint$ | async"
></ui-filter>
@if (filterOverlay.isOpen) {
<ui-filter
#filter
class="mx-4"
[filter]="store.pendingFilter$ | async"
(search)="applyFilter()"
[loading]="store.fetching$ | async"
[hint]="hint$ | async"
></ui-filter>
}
<div class="absolute bottom-8 left-0 right-0 grid grid-flow-col gap-4 justify-center">
<button
type="button"
class="px-6 py-4 font-bold bg-white text-brand border-2 border-solid border-brand rounded-full"
(click)="store.resetPendingFilter()"
>
>
Filter zurücksetzen
</button>
<button
@@ -49,12 +53,9 @@
class="px-6 py-4 font-bold bg-brand text-white border-2 border-solid border-brand rounded-full disabled:bg-cadet-blue disabled:cursor-progress disabled:border-cadet-blue"
(click)="applyFilter()"
[disabled]="store.fetching$ | async"
>
>
<ui-spinner [show]="store.fetching$ | async">Filter anwenden</ui-spinner>
</button>
</div>
</shell-filter-overlay>
<ng-template #noResults>
<div class="bg-white text-h3 text-center pt-10 font-bold rounded-b h-[calc(100vh_-_370px)]">Keine Preisänderungen vorhanden.</div>
</ng-template>

View File

@@ -1,14 +1,16 @@
<ng-container *ngFor="let line of lines">
<ng-container [ngSwitch]="line | lineType">
<ng-container *ngSwitchCase="'reihe'">
<page-article-details-text-link *ngFor="let reihe of getReihen(line)" [route]="reihe | reiheRoute">
{{ reihe }}
</page-article-details-text-link>
@for (line of lines; track line) {
@switch (line | lineType) {
@case ('reihe') {
@for (reihe of getReihen(line); track reihe) {
<page-article-details-text-link [route]="reihe | reiheRoute">
{{ reihe }}
</page-article-details-text-link>
}
<br />
</ng-container>
<ng-container *ngSwitchDefault>
}
@default {
{{ line }}
<br />
</ng-container>
</ng-container>
</ng-container>
}
}
}

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { TextDTO } from '@generated/swagger/cat-search-api';
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';
@@ -13,13 +13,9 @@ import { ReiheRoutePipe } from './reihe-route.pipe';
host: { class: 'page-article-details-text' },
imports: [
ArticleDetailsTextLinkComponent,
NgFor,
NgSwitch,
NgSwitchCase,
NgSwitchDefault,
LineTypePipe,
ReiheRoutePipe,
],
ReiheRoutePipe
],
})
export class ArticleDetailsTextComponent {
@Input()

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,39 @@
<ng-container *ngIf="store.item$ | async; let item">
@if (store.item$ | async; as 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>
<div class="articles">
<span class="label mb-2">
<ui-icon icon="recommendation" size="20px"></ui-icon>
Artikel
</span>
<ng-container *ngIf="store.recommendations$ | async; let recommendations">
<span *ngIf="recommendations.length === 0" class="empty-message">Keine Empfehlungen verfügbar</span>
<ui-slider *ngIf="recommendations.length > 0" [scrollDistance]="210">
<a
class="article"
*ngFor="let recommendation of store.recommendations$ | async"
[routerLink]="getDetailsPath(recommendation.product.ean)"
[queryParams]="{ main_qs: recommendation.product.ean, filter_format: '' }"
(click)="close.emit()"
>
<img [src]="recommendation.product?.ean | productImage: 195 : 315 : true" alt="product-image" />
<div class="flex flex-col">
<span class="format">{{ recommendation.product?.formatDetail }}</span>
<span class="price">{{ recommendation.catalogAvailability?.price?.value?.value | currency: ' ' }} EUR</span>
</div>
</a>
</ui-slider>
</ng-container>
@if (store.recommendations$ | async; as recommendations) {
@if (recommendations.length === 0) {
<span class="empty-message">Keine Empfehlungen verfügbar</span>
}
@if (recommendations.length > 0) {
<ui-slider [scrollDistance]="210">
@for (recommendation of store.recommendations$ | async; track recommendation) {
<a
class="article"
[routerLink]="getDetailsPath(recommendation.product.ean)"
[queryParams]="{ main_qs: recommendation.product.ean, filter_format: '' }"
(click)="close.emit()"
>
<img [src]="recommendation.product?.ean | productImage: 195 : 315 : true" alt="product-image" />
<div class="flex flex-col">
<span class="format">{{ recommendation.product?.formatDetail }}</span>
<span class="price">{{ recommendation.catalogAvailability?.price?.value?.value | currency: ' ' }} EUR</span>
</div>
</a>
}
</ui-slider>
}
}
</div>
</ng-container>
}

View File

@@ -1,19 +1,19 @@
<div class="hidden desktop-large:block" [class.show-filter]="showFilter">
<ng-container *ngIf="filter$ | async; let filter">
@if (filter$ | async; as filter) {
<div class="catalog-search-filter-content">
<div class="w-full flex flex-row justify-end items-center">
<button (click)="clearFilter(filter)" class="text-[#0556B4] p-4">Alle Filter entfernen</button>
<a
*ngIf="showFilterClose$ | async"
class="text-black p-4 outline-none border-none bg-transparent"
[routerLink]="closeFilterRoute"
(click)="showFilter = false"
queryParamsHandling="preserve"
>
<shared-icon icon="close" [size]="25"></shared-icon>
</a>
@if (showFilterClose$ | async) {
<a
class="text-black p-4 outline-none border-none bg-transparent"
[routerLink]="closeFilterRoute"
(click)="showFilter = false"
queryParamsHandling="preserve"
>
<shared-icon icon="close" [size]="25"></shared-icon>
</a>
}
</div>
<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
@@ -24,16 +24,14 @@
[scanner]="true"
></shared-filter>
</div>
<div class="cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter(filter)" [disabled]="fetching$ | async">Filter zurücksetzen</button>
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="(fetching$ | async) || !hasSelectedOptions(filter)">
<ui-spinner [show]="fetching$ | async">Filter anwenden</ui-spinner>
</button>
</div>
</div>
</ng-container>
}
</div>
<div class="desktop-large:hidden" [class.hidden]="showFilter">
<page-article-search-main (showFilter)="showFilter = true"></page-article-search-main>

View File

@@ -1,12 +1,13 @@
<div class="bg-white rounded py-10 px-4 text-center shadow-[0_-2px_24px_0_#dce2e9] h-full">
<h1 class="text-h3 text-[1.625rem] font-bold mb-[0.375rem]">Artikelsuche</h1>
<p class="text-lg mb-10">Welchen Artikel suchen Sie?</p>
<ng-container *ngIf="filter$ | async; let filter">
<shared-filter-filter-group-main
class="mb-8 w-full"
*ngIf="!(isDesktop$ | async)"
[inputGroup]="filter?.filter | group: 'main'"
></shared-filter-filter-group-main>
@if (filter$ | async; as filter) {
@if (!(isDesktop$ | async)) {
<shared-filter-filter-group-main
class="mb-8 w-full"
[inputGroup]="filter?.filter | group: 'main'"
></shared-filter-filter-group-main>
}
<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-large:mx-auto"
@@ -17,38 +18,40 @@
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
<button
type="button"
*ngIf="!(isDesktop$ | async)"
(click)="showFilter.emit()"
class="page-search-main__filter w-[6.75rem] h-14 rounded font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
>
<shared-icon class="mr-2" icon="filter-variant"></shared-icon>
Filter
</button>
@if (!(isDesktop$ | async)) {
<button
type="button"
(click)="showFilter.emit()"
class="page-search-main__filter w-[6.75rem] h-14 rounded font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
>
<shared-icon class="mr-2" icon="filter-variant"></shared-icon>
Filter
</button>
}
</div>
<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">
<button
class="flex flex-row items-center outline-none border-none bg-white text-black text-p2 m-0 p-0"
(click)="setQueryHistory(filter, recentQuery.friendlyName)"
matomoClickCategory="search"
matomoClickAction="click"
matomoClickName="recent-search"
>
<shared-icon
class="flex w-8 h-8 justify-center items-center mr-3 rounded-full text-black bg-[#edeff0]"
icon="magnify"
[size]="20"
></shared-icon>
<p class="m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-[25rem]">{{ recentQuery.friendlyName }}</p>
</button>
</li>
@for (recentQuery of history$ | async; track recentQuery) {
<li class="list-none pb-3">
<button
class="flex flex-row items-center outline-none border-none bg-white text-black text-p2 m-0 p-0"
(click)="setQueryHistory(filter, recentQuery.friendlyName)"
matomoClickCategory="search"
matomoClickAction="click"
matomoClickName="recent-search"
>
<shared-icon
class="flex w-8 h-8 justify-center items-center mr-3 rounded-full text-black bg-[#edeff0]"
icon="magnify"
[size]="20"
></shared-icon>
<p class="m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-[25rem]">{{ recentQuery.friendlyName }}</p>
</button>
</li>
}
</ul>
</div>
</ng-container>
}
</div>

View File

@@ -1,4 +1,6 @@
<p class="can-add-message" *ngIf="ref.data.canAddMessage">{{ ref.data.canAddMessage }}</p>
@if (ref.data.canAddMessage) {
<p class="can-add-message">{{ ref.data.canAddMessage }}</p>
}
<div class="actions">
<button (click)="continue()" class="cta cta-action-secondary">Weiter Einkaufen</button>

View File

@@ -1,4 +1,4 @@
<ng-container *ngIf="!primaryOutletActive; else primaryOutlet">
@if (!primaryOutletActive) {
<div class="bg-ucla-blue rounded w-[4.375rem] h-[5.625rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-col flex-grow">
<div class="h-4 bg-ucla-blue ml-4 mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
@@ -16,9 +16,7 @@
<div class="h-4 bg-ucla-blue ml-4 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
</div>
</ng-container>
<ng-template #primaryOutlet>
} @else {
<div class="bg-ucla-blue rounded w-[3rem] h-[4.125rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-col ml-4 w-[36.6%]">
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
@@ -35,4 +33,5 @@
<div class="h-4 bg-ucla-blue mb-2 w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
<div class="h-4 bg-ucla-blue w-[8.8125rem] animate-[load_1s_linear_infinite]"></div>
</div>
</ng-template>
}

View File

@@ -2,32 +2,34 @@
class="page-search-result-item__item-card hover p-5 desktop-small:px-4 desktop-small:py-[0.625rem] h-[13.25rem] desktop-small:h-[11.3125rem] bg-white border border-solid border-transparent rounded"
[class.page-search-result-item__item-card-primary]="primaryOutletActive"
[class.active]="isActive"
>
>
<div class="page-search-result-item__item-thumbnail text-center mr-4 w-[3.125rem] h-[4.9375rem]">
<img
class="page-search-result-item__item-image w-[3.125rem] max-h-[4.9375rem]"
loading="lazy"
*ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl"
[src]="thumbnailUrl"
[alt]="item?.product?.name"
/>
@if (item?.imageId | thumbnailUrl; as thumbnailUrl) {
<img
class="page-search-result-item__item-image w-[3.125rem] max-h-[4.9375rem]"
loading="lazy"
[src]="thumbnailUrl"
[alt]="item?.product?.name"
/>
}
</div>
<div
class="page-search-result-item__item-grid-container"
[class.page-search-result-item__item-grid-container-primary]="primaryOutletActive"
>
>
<div
class="page-search-result-item__item-contributors desktop-small:text-p3 font-bold text-[#0556B4] text-ellipsis overflow-hidden max-w-[24rem] whitespace-nowrap"
>
<a
*ngFor="let contributor of contributors; let last = last"
[routerLink]="resultsPath"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
@for (contributor of contributors; track contributor; let last = $last) {
<a
[routerLink]="resultsPath"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
}
</div>
<div
@@ -37,21 +39,24 @@
[class.text-md]="item?.product?.name?.length >= 50 && isTablet"
[class.text-p3]="item?.product?.name?.length >= 60 || !isTablet"
[class.text-xs]="item?.product?.name?.length >= 100"
>
>
{{ item?.product?.name }}
</div>
<div class="page-search-result-item__item-format desktop-small:text-p3">
<div *ngIf="item?.product?.format && item?.product?.formatDetail" class="font-bold flex flex-row">
<img
class="mr-3"
*ngIf="item?.product?.format !== '--'"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail | substr: 30 }}
</div>
@if (item?.product?.format && item?.product?.formatDetail) {
<div class="font-bold flex flex-row">
@if (item?.product?.format !== '--') {
<img
class="mr-3"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
}
{{ item?.product?.formatDetail | substr: 30 }}
</div>
}
</div>
<div class="page-search-result-item__item-manufacturer desktop-small:text-p3">
@@ -60,31 +65,34 @@
<div class="page-search-result-item__item-misc desktop-small:text-p3">
{{ item?.product?.volume }}
<span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
@if (item?.product?.volume && item?.product?.publicationDate) {
<span>|</span>
}
{{ publicationDate }}
</div>
<div
class="page-search-result-item__item-price desktop-small:text-p3 font-bold justify-self-end"
[class.page-search-result-item__item-price-primary]="primaryOutletActive"
>
>
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR' : 'code' }}
</div>
<div class="page-search-result-item__item-select-bullet justify-self-end">
<input
*ngIf="selectable"
(click)="$event.stopPropagation()"
[ngModel]="selected"
@if (selectable) {
<input
(click)="$event.stopPropagation()"
[ngModel]="selected"
(ngModelChange)="
setSelected();
tracker.trackEvent({ category: 'Trefferliste', action: 'select', name: item.product.name, value: selected ? 1 : 0 })
"
class="isa-select-bullet"
type="checkbox"
matomoTracker
#tracker="matomo"
/>
class="isa-select-bullet"
type="checkbox"
matomoTracker
#tracker="matomo"
/>
}
</div>
<button
@@ -94,20 +102,21 @@
[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">
<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)">
@if (isOrderBranch$ | async) {
@if (inStock$ | async; as stock) {
<span
[class.skeleton]="stock.inStock === undefined"
class="min-w-[0.75rem] text-right inline-block"
>
{{ stock?.inStock }}
</span>
}
}
@if (!(isOrderBranch$ | async)) {
<span class="min-w-[1rem] text-center inline-block">-</span>
</ng-container>
}
<span>x</span>
</button>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-12" [closeable]="true">
@@ -117,14 +126,14 @@
<div
class="page-search-result-item__item-ssc desktop-small:text-p3 w-full text-right overflow-hidden text-ellipsis whitespace-nowrap"
[class.page-search-result-item__item-ssc-primary]="primaryOutletActive"
>
<ng-container *ngIf="ssc$ | async; let ssc">
>
@if (ssc$ | async; as ssc) {
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="primaryOutletActive">
{{ ssc?.ssc }} - {{ ssc?.sscText }}
</div>
<strong>{{ ssc?.ssc }}</strong>
- {{ ssc?.sscText }}
</ng-container>
}
</div>
</div>
</div>

View File

@@ -2,50 +2,53 @@
class="page-search-results__header bg-background-liste flex items-end justify-between"
[class.pb-4]="!(primaryOutletActive$ | async)"
[class.flex-col]="!(primaryOutletActive$ | async)"
>
>
<div class="flex flex-row w-full desktop:w-min" [class.desktop-large:w-full]="!(primaryOutletActive$ | async)">
<shared-filter-input-group-main
*ngIf="filter$ | async; let filter"
class="block mr-3 w-full desktop:w-[23.5rem]"
[class.desktop-large:w-full]="!(primaryOutletActive$ | async)"
[hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search({ filter, clear: true })"
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
@if (filter$ | async; as filter) {
<shared-filter-input-group-main
class="block mr-3 w-full desktop:w-[23.5rem]"
[class.desktop-large:w-full]="!(primaryOutletActive$ | async)"
[hint]="searchboxHint$ | async"
[loading]="fetching$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search({ filter, clear: true })"
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
}
<a
class="page-search-results__filter w-[6.75rem] h-14 rounded font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
[routerLink]="filterRoute"
[queryParams]="filterQueryParams"
>
>
<shared-icon class="mr-2" icon="filter-variant"></shared-icon>
Filter
</a>
</div>
<div
*ngIf="hits$ | async; let hits"
class="page-search-results__items-count inline-flex flex-row items-center pr-5 text-p3"
[class.mb-4]="primaryOutletActive$ | async"
>
{{ hits ?? 0 }}
Titel
</div>
@if (hits$ | async; as hits) {
<div
class="page-search-results__items-count inline-flex flex-row items-center pr-5 text-p3"
[class.mb-4]="primaryOutletActive$ | async"
>
{{ hits ?? 0 }}
Titel
</div>
}
</div>
<div class="page-search-results__order-by mb-[0.125rem]" [class.page-search-results__order-by-primary]="primaryOutletActive$ | async">
<shared-order-by-filter
*ngIf="filter$ | async; let filter"
[orderBy]="filter?.orderBy"
(selectedOrderByChange)="search({ filter, clear: true, orderBy: true }); updateBreadcrumbs()"
></shared-order-by-filter>
@if (filter$ | async; as filter) {
<shared-order-by-filter
[orderBy]="filter?.orderBy"
(selectedOrderByChange)="search({ filter, clear: true, orderBy: true }); updateBreadcrumbs()"
></shared-order-by-filter>
}
</div>
<ng-container *ngIf="primaryOutletActive$ | async; else sideOutlet">
@if (primaryOutletActive$ | async) {
<cdk-virtual-scroll-viewport class="product-list" [itemSize]="103 * (scale$ | async)" (scrolledIndexChange)="scrolledIndexChange($event)">
<a
*cdkVirtualFor="let item of results$ | async; let i = index; trackBy: trackByItemId"
@@ -54,7 +57,7 @@
#rla="routerLinkActive"
queryParamsHandling="preserve"
(click)="scrollToItem(i)"
>
>
<search-result-item
class="page-search-results__result-item page-search-results__result-item-primary"
(selectedChange)="addToCart($event)"
@@ -65,24 +68,25 @@
[isActive]="rla.isActive"
></search-result-item>
</a>
<page-search-result-item-loading [primaryOutletActive]="true" *ngIf="fetching$ | async"></page-search-result-item-loading>
@if (fetching$ | async) {
<page-search-result-item-loading [primaryOutletActive]="true"></page-search-result-item-loading>
}
</cdk-virtual-scroll-viewport>
<div class="actions z-sticky h-0">
<button
[disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0"
class="cta-cart cta-action-primary"
(click)="addToCart()"
matomoClickCategory="Trefferliste"
matomoClickAction="click"
matomoClickName="In den Warenkorb legen"
>
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button>
@if ((selectedItemIds$ | async)?.length > 0) {
<button
[disabled]="loading$ | async"
class="cta-cart cta-action-primary"
(click)="addToCart()"
matomoClickCategory="Trefferliste"
matomoClickAction="click"
matomoClickName="In den Warenkorb legen"
>
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button>
}
</div>
</ng-container>
<ng-template #sideOutlet>
} @else {
<cdk-virtual-scroll-viewport class="product-list" [itemSize]="191 * (scale$ | async)" (scrolledIndexChange)="scrolledIndexChange($event)">
<a
*cdkVirtualFor="let item of results$ | async; let i = index; trackBy: trackByItemId"
@@ -91,7 +95,7 @@
#rla="routerLinkActive"
queryParamsHandling="preserve"
(click)="scrollToItem(i)"
>
>
<search-result-item
class="page-search-results__result-item"
(selectedChange)="addToCart($event)"
@@ -102,16 +106,20 @@
[isActive]="rla.isActive"
></search-result-item>
</a>
<page-search-result-item-loading [primaryOutletActive]="false" *ngIf="fetching$ | async"></page-search-result-item-loading>
@if (fetching$ | async) {
<page-search-result-item-loading [primaryOutletActive]="false"></page-search-result-item-loading>
}
</cdk-virtual-scroll-viewport>
<div class="actions z-sticky h-0">
<button
[disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0"
class="cta-cart cta-action-primary"
(click)="addToCart()"
>
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button>
@if ((selectedItemIds$ | async)?.length > 0) {
<button
[disabled]="loading$ | async"
class="cta-cart cta-action-primary"
(click)="addToCart()"
>
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button>
}
</div>
</ng-template>
}

View File

@@ -9,87 +9,93 @@
</p>
</div>
</div>
<form *ngIf="control" [formGroup]="control" (submit)="submit()">
<ui-form-control class="searchbox-control" label="EAN/ISBN">
<ui-searchbox
formControlName="ean"
[query]="query$ | async"
(queryChange)="setQuery($event)"
(search)="search($event)"
(scan)="search($event)"
[loading]="loading$ | async"
[hint]="message$ | async"
tabindex="0"
[scanner]="true"
></ui-searchbox>
</ui-form-control>
<ui-form-control label="Titel" requiredMark="*">
<input tabindex="0" uiInput formControlName="name" />
</ui-form-control>
<div class="control-row">
<ui-form-control label="Menge" requiredMark="*">
<input tabindex="0" uiInput formControlName="quantity" />
@if (control) {
<form [formGroup]="control" (submit)="submit()">
<ui-form-control class="searchbox-control" label="EAN/ISBN">
<ui-searchbox
formControlName="ean"
[query]="query$ | async"
(queryChange)="setQuery($event)"
(search)="search($event)"
(scan)="search($event)"
[loading]="loading$ | async"
[hint]="message$ | async"
tabindex="0"
[scanner]="true"
></ui-searchbox>
</ui-form-control>
<ui-form-control class="datepicker" label="vsl. Lieferdatum" requiredMark="*">
<button
tabindex="-1"
class="date-btn"
type="button"
[class.content-selected]="!!(estimatedShippingDate$ | async)"
[uiOverlayTrigger]="uiDatepicker"
#datepicker="uiOverlayTrigger"
>
<strong>
{{ estimatedShippingDate$ | async | date: 'dd.MM.yy' }}
</strong>
<ui-icon icon="arrow_head" class="dp-button-icon" size="20px" [rotate]="datepicker.opened ? '270deg' : '90deg'"></ui-icon>
</button>
<ui-datepicker
formControlName="estimatedShippingDate"
#uiDatepicker
yPosition="below"
xPosition="after"
[min]="minDate"
[disabledDaysOfWeek]="[0]"
[selected]="estimatedShippingDate$ | async"
saveLabel="Übernehmen"
(save)="changeEstimatedShippingDate($event); uiDatepicker.close()"
></ui-datepicker>
<ui-form-control label="Titel" requiredMark="*">
<input tabindex="0" uiInput formControlName="name" />
</ui-form-control>
</div>
<ui-form-control label="Autor">
<input tabindex="0" uiInput formControlName="contributors" />
</ui-form-control>
<ui-form-control label="Verlag">
<input tabindex="0" uiInput formControlName="manufacturer" />
</ui-form-control>
<ui-form-control class="supplier-dropdown" label="Lieferant" requiredMark="*">
<ui-select tabindex="-1" formControlName="supplier">
<ui-select-option *ngFor="let supplier of suppliers$ | async" [label]="supplier.name" [value]="supplier.id"></ui-select-option>
</ui-select>
</ui-form-control>
<div class="control-row">
<ui-form-control class="price" label="Stückpreis" [suffix]="price.value ? '' : ''" requiredMark="*">
<input tabindex="0" #price uiInput formControlName="price" />
<div class="control-row">
<ui-form-control label="Menge" requiredMark="*">
<input tabindex="0" uiInput formControlName="quantity" />
</ui-form-control>
<ui-form-control class="datepicker" label="vsl. Lieferdatum" requiredMark="*">
<button
tabindex="-1"
class="date-btn"
type="button"
[class.content-selected]="!!(estimatedShippingDate$ | async)"
[uiOverlayTrigger]="uiDatepicker"
#datepicker="uiOverlayTrigger"
>
<strong>
{{ estimatedShippingDate$ | async | date: 'dd.MM.yy' }}
</strong>
<ui-icon icon="arrow_head" class="dp-button-icon" size="20px" [rotate]="datepicker.opened ? '270deg' : '90deg'"></ui-icon>
</button>
<ui-datepicker
formControlName="estimatedShippingDate"
#uiDatepicker
yPosition="below"
xPosition="after"
[min]="minDate"
[disabledDaysOfWeek]="[0]"
[selected]="estimatedShippingDate$ | async"
saveLabel="Übernehmen"
(save)="changeEstimatedShippingDate($event); uiDatepicker.close()"
></ui-datepicker>
</ui-form-control>
</div>
<ui-form-control label="Autor">
<input tabindex="0" uiInput formControlName="contributors" />
</ui-form-control>
<ui-form-control class="mwst-dropdown" label="MwSt" requiredMark="*">
<ui-select tabindex="-1" formControlName="vat">
<ui-select-option *ngFor="let vat of vats$ | async" [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>
<ui-form-control label="Verlag">
<input tabindex="0" uiInput formControlName="manufacturer" />
</ui-form-control>
<ui-form-control class="supplier-dropdown" label="Lieferant" requiredMark="*">
<ui-select tabindex="-1" formControlName="supplier">
@for (supplier of suppliers$ | async; track supplier) {
<ui-select-option [label]="supplier.name" [value]="supplier.id"></ui-select-option>
}
</ui-select>
</ui-form-control>
</div>
<div class="actions">
<button
class="cta-secondary"
(click)="nextItem()"
[disabled]="control.invalid || control.disabled || (loading$ | async)"
type="button"
>
Weitere Artikel hinzufügen
</button>
<button class="cta-primary" [disabled]="control.invalid || control.disabled || (loading$ | async)" type="submit">
Bestellung anlegen
</button>
</div>
</form>
<div class="control-row">
<ui-form-control class="price" label="Stückpreis" [suffix]="price.value ? '' : ''" requiredMark="*">
<input tabindex="0" #price uiInput formControlName="price" />
</ui-form-control>
<ui-form-control class="mwst-dropdown" label="MwSt" requiredMark="*">
<ui-select tabindex="-1" formControlName="vat">
@for (vat of vats$ | async; track vat) {
<ui-select-option [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>
}
</ui-select>
</ui-form-control>
</div>
<div class="actions">
<button
class="cta-secondary"
(click)="nextItem()"
[disabled]="control.invalid || control.disabled || (loading$ | async)"
type="button"
>
Weitere Artikel hinzufügen
</button>
<button class="cta-primary" [disabled]="control.invalid || control.disabled || (loading$ | async)" type="submit">
Bestellung anlegen
</button>
</div>
</form>
}
</div>

View File

@@ -1,10 +1,9 @@
<ng-container *ngIf="(groupedItems$ | async)?.length <= 0 && !(fetching$ | async); else shoppingCart">
@if ((groupedItems$ | async)?.length <= 0 && !(fetching$ | async)) {
<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>
</span>
<h1>Ihr Warenkorb ist leer.</h1>
<p>
Sie haben alle Artikel aus dem
@@ -13,84 +12,75 @@
<br />
keinen Artikel hinzugefügt.
</p>
<div class="btn-wrapper">
<a class="cta-primary" [routerLink]="productSearchBasePath">Artikel suchen</a>
<button class="cta-secondary" (click)="openDummyModal({})">Neuanlage</button>
</div>
</div>
</div>
</ng-container>
<div class="flex items-center justify-center card stretch" *ngIf="fetching$ | async">
<ui-spinner [show]="true"></ui-spinner>
</div>
<ng-template #shoppingCart>
<ng-container *ngIf="shoppingCart$ | async; let shoppingCart">
} @else {
@if (shoppingCart$ | async; as shoppingCart) {
<div class="card stretch">
<div class="cta-print-wrapper">
<button class="cta-print" (click)="openPrintModal()">Drucken</button>
</div>
<h1 class="header">Warenkorb</h1>
<ng-container *ngIf="!(isDesktop$ | async)">
@if (!(isDesktop$ | async)) {
<page-checkout-review-details></page-checkout-review-details>
</ng-container>
<ng-container *ngFor="let group of groupedItems$ | async; let lastGroup = last; trackBy: trackByGroupedItems">
<ng-container *ngIf="group?.orderType !== undefined">
}
@for (group of groupedItems$ | async; track trackByGroupedItems($index, group); let lastGroup = $last) {
@if (group?.orderType !== undefined) {
<hr />
<div class="row item-group-header bg-[#F5F7FA]">
<shared-icon
*ngIf="group.orderType !== 'Dummy'"
class="icon-order-type"
[size]="group.orderType === 'B2B-Versand' ? 36 : 24"
[icon]="group.orderType"
></shared-icon>
@if (group.orderType !== 'Dummy') {
<shared-icon
class="icon-order-type"
[size]="group.orderType === 'B2B-Versand' ? 36 : 24"
[icon]="group.orderType"
></shared-icon>
}
<div class="label" [class.dummy]="group.orderType === 'Dummy'">
{{ group.orderType !== 'Dummy' ? group.orderType : 'Manuelle Anlage / Dummy Bestellung' }}
<button
*ngIf="group.orderType === 'Dummy'"
class="text-brand border-none font-bold text-p1 outline-none pl-4"
(click)="openDummyModal({ changeDataFromCart: true })"
>
Hinzufügen
</button>
@if (group.orderType === 'Dummy') {
<button
class="text-brand border-none font-bold text-p1 outline-none pl-4"
(click)="openDummyModal({ changeDataFromCart: true })"
>
Hinzufügen
</button>
}
</div>
<div class="grow"></div>
<div class="pl-4" *ngIf="group.orderType !== 'Download' && group.orderType !== 'Dummy'">
<button class="cta-edit" (click)="showPurchasingListModal(group.items)">Ändern</button>
</div>
@if (group.orderType !== 'Download' && group.orderType !== 'Dummy') {
<div class="pl-4">
<button class="cta-edit" (click)="showPurchasingListModal(group.items)">Ändern</button>
</div>
}
</div>
<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; trackBy: trackByItemId">
<ng-container
*ngIf="group?.orderType !== undefined && (item.features?.orderType === 'Abholung' || item.features?.orderType === 'Rücklage')"
>
<ng-container *ngIf="item?.destination?.data?.targetBranch?.data; let targetBranch">
<ng-container *ngIf="i === 0 || checkIfMultipleDestinationsForOrderTypeExist(targetBranch, group, i)">
@if (
group.orderType === 'Download' ||
group.orderType === 'Versand' ||
group.orderType === 'B2B-Versand' ||
group.orderType === 'DIG-Versand'
) {
<hr
/>
}
}
@for (item of group.items; track trackByItemId(i, item); let lastItem = $last; let i = $index) {
@if (group?.orderType !== undefined && (item.features?.orderType === 'Abholung' || item.features?.orderType === 'Rücklage')) {
@if (item?.destination?.data?.targetBranch?.data; as targetBranch) {
@if (i === 0 || checkIfMultipleDestinationsForOrderTypeExist(targetBranch, group, i)) {
<div
class="flex flex-row items-center px-5 pt-0 pb-[0.875rem] -mt-2 bg-[#F5F7FA]"
[class.multiple-destinations]="checkIfMultipleDestinationsForOrderTypeExist(targetBranch, group, i)"
>
>
<span class="branch-name">{{ targetBranch?.name }} | {{ targetBranch | branchAddress }}</span>
</div>
<hr />
</ng-container>
</ng-container>
</ng-container>
}
}
}
<page-shopping-cart-item
(changeItem)="changeItem($event)"
(changeDummyItem)="changeDummyItem($event)"
@@ -101,19 +91,22 @@
[loadingOnItemChangeById]="loadingOnItemChangeById$ | async"
[loadingOnQuantityChangeById]="loadingOnQuantityChangeById$ | async"
></page-shopping-cart-item>
<hr *ngIf="!lastItem" />
</ng-container>
</ng-container>
@if (!lastItem) {
<hr />
}
}
}
<div class="h-[8.9375rem]"></div>
</div>
<div class="card footer flex flex-col justify-center items-center">
<div class="flex flex-row items-start justify-between w-full mb-1">
<ng-container *ngIf="totalItemCount$ | async; let totalItemCount">
<div *ngIf="totalReadingPoints$ | async; let totalReadingPoints" class="total-item-reading-points w-full">
{{ totalItemCount }} Artikel | {{ totalReadingPoints }} Lesepunkte
</div>
</ng-container>
@if (totalItemCount$ | async; as totalItemCount) {
@if (totalReadingPoints$ | async; as totalReadingPoints) {
<div class="total-item-reading-points w-full">
{{ totalItemCount }} Artikel | {{ totalReadingPoints }} Lesepunkte
</div>
}
}
<div class="flex flex-col w-full">
<strong class="total-value">
Zwischensumme {{ shoppingCart?.total?.value | currency: shoppingCart?.total?.currency : 'code' }}
@@ -130,11 +123,18 @@
notificationsControl?.invalid ||
((primaryCtaLabel$ | async) === 'Bestellen' && ((checkingOla$ | async) || (checkoutIsInValid$ | async)))
"
>
>
<ui-spinner [show]="showOrderButtonSpinner">
{{ primaryCtaLabel$ | async }}
</ui-spinner>
</button>
</div>
</ng-container>
</ng-template>
}
}
@if (fetching$ | async) {
<div class="flex items-center justify-center card stretch">
<ui-spinner [show]="true"></ui-spinner>
</div>
}

View File

@@ -9,37 +9,53 @@ import {
AfterViewInit,
TrackByFunction,
inject,
} from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import { AvailabilityDTO, BranchDTO, DestinationDTO, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { PrintModalData, PrintModalComponent } from '@modal/printer';
import { delay, first, map, switchMap, takeUntil, tap } from 'rxjs/operators';
import { Subject, NEVER, combineLatest, BehaviorSubject, Subscription } from 'rxjs';
import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainPrinterService } from '@domain/printer';
import { CheckoutDummyComponent } from '../checkout-dummy/checkout-dummy.component';
import { CheckoutDummyData } from '../checkout-dummy/checkout-dummy-data';
import { PurchaseOptionsModalService } from '@modal/purchase-options';
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services/navigation';
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';
import { CustomerSearchNavigation } from '@shared/services/navigation';
} from "@angular/core";
import { Router } from "@angular/router";
import { ApplicationService } from "@core/application";
import { DomainAvailabilityService } from "@domain/availability";
import { DomainCheckoutService } from "@domain/checkout";
import {
AvailabilityDTO,
BranchDTO,
DestinationDTO,
ShoppingCartItemDTO,
} from "@generated/swagger/checkout-api";
import { UiMessageModalComponent, UiModalService } from "@ui/modal";
import { PrintModalData, PrintModalComponent } from "@modal/printer";
import { delay, first, map, switchMap, takeUntil, tap } from "rxjs/operators";
import {
Subject,
NEVER,
combineLatest,
BehaviorSubject,
Subscription,
} from "rxjs";
import { DomainCatalogService } from "@domain/catalog";
import { BreadcrumbService } from "@core/breadcrumb";
import { DomainPrinterService } from "@domain/printer";
import { CheckoutDummyComponent } from "../checkout-dummy/checkout-dummy.component";
import { CheckoutDummyData } from "../checkout-dummy/checkout-dummy-data";
import { PurchaseOptionsModalService } from "@modal/purchase-options";
import {
CheckoutNavigationService,
ProductCatalogNavigationService,
} from "@shared/services/navigation";
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";
import { CustomerSearchNavigation } from "@shared/services/navigation";
@Component({
selector: 'page-checkout-review',
templateUrl: 'checkout-review.component.html',
styleUrls: ['checkout-review.component.scss'],
selector: "page-checkout-review",
templateUrl: "checkout-review.component.html",
styleUrls: ["checkout-review.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit {
export class CheckoutReviewComponent
implements OnInit, OnDestroy, AfterViewInit
{
private _onDestroy$ = new Subject<void>();
private _customerSearchNavigation = inject(CustomerSearchNavigation);
@@ -57,7 +73,9 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
shoppingCartItemsWithoutOrderType$ = this._store.shoppingCartItems$.pipe(
takeUntil(this._store.orderCompleted),
map((items) => items?.filter((item) => item?.features?.orderType === undefined)),
map((items) =>
items?.filter((item) => item?.features?.orderType === undefined),
),
);
trackByGroupedItems: TrackByFunction<{
@@ -71,11 +89,11 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
map((items) =>
items.reduce(
(grouped, item) => {
let index = grouped.findIndex((g) =>
item?.availability?.supplyChannel === 'MANUALLY'
? g?.orderType === 'Dummy'
: item?.features?.orderType === 'DIG-Versand'
? g?.orderType === 'Versand'
const index = grouped.findIndex((g) =>
item?.availability?.supplyChannel === "MANUALLY"
? g?.orderType === "Dummy"
: item?.features?.orderType === "DIG-Versand"
? g?.orderType === "Versand"
: g?.orderType === item?.features?.orderType,
);
@@ -83,10 +101,10 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
if (!group) {
group = {
orderType:
item?.availability?.supplyChannel === 'MANUALLY'
? 'Dummy'
: item?.features?.orderType === 'DIG-Versand'
? 'Versand'
item?.availability?.supplyChannel === "MANUALLY"
? "Dummy"
: item?.features?.orderType === "DIG-Versand"
? "Versand"
: item?.features?.orderType,
destination: item?.destination?.data,
items: [],
@@ -95,7 +113,8 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
group.items = [...group.items, item]?.sort(
(a, b) =>
a.destination?.data?.targetBranch?.id - b.destination?.data?.targetBranch?.id ||
a.destination?.data?.targetBranch?.id -
b.destination?.data?.targetBranch?.id ||
a.product?.name.localeCompare(b.product?.name),
);
@@ -105,9 +124,19 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
grouped.push(group);
}
return [...grouped].sort((a, b) => (a?.orderType === undefined ? -1 : b?.orderType === undefined ? 1 : 0));
return [...grouped].sort((a, b) =>
a?.orderType === undefined
? -1
: b?.orderType === undefined
? 1
: 0,
);
},
[] as { orderType: string; destination: DestinationDTO; items: ShoppingCartItemDTO[] }[],
[] as {
orderType: string;
destination: DestinationDTO;
items: ShoppingCartItemDTO[];
}[],
),
),
);
@@ -138,7 +167,14 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
.getPromotionPoints({
items,
})
.pipe(map((response) => Object.values(response.result).reduce((sum, points) => sum + points, 0)));
.pipe(
map((response) =>
Object.values(response.result).reduce(
(sum, points) => sum + points,
0,
),
),
);
} else {
return NEVER;
}
@@ -147,20 +183,25 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
customerFeatures$ = this._store.customerFeatures$;
checkNotificationChannelControl$ = this._store.checkNotificationChannelControl$;
checkNotificationChannelControl$ =
this._store.checkNotificationChannelControl$;
showQuantityControlSpinnerItemId: number;
quantityError$ = new BehaviorSubject<{ [key: string]: string }>({});
primaryCtaLabel$ = combineLatest([this.payer$, this.buyer$, this.shoppingCartItemsWithoutOrderType$]).pipe(
primaryCtaLabel$ = combineLatest([
this.payer$,
this.buyer$,
this.shoppingCartItemsWithoutOrderType$,
]).pipe(
map(([payer, buyer, shoppingCartItemsWithoutOrderType]) => {
if (shoppingCartItemsWithoutOrderType?.length > 0) {
return 'Kaufoptionen';
return "Kaufoptionen";
}
if (!(payer || buyer)) {
return 'Weiter';
return "Weiter";
}
return 'Bestellen';
return "Bestellen";
}),
);
@@ -181,12 +222,16 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
checkoutIsInValid$ = this.applicationService.activatedProcessId$.pipe(
takeUntil(this._onDestroy$),
switchMap((processId) => this.domainCheckoutService.checkoutIsValid({ processId })),
switchMap((processId) =>
this.domainCheckoutService.checkoutIsValid({ processId }),
),
map((valid) => !valid),
);
get productSearchBasePath() {
return this._productNavigationService.getArticleSearchBasePath(this.applicationService.activatedProcessId).path;
return this._productNavigationService.getArticleSearchBasePath(
this.applicationService.activatedProcessId,
).path;
}
get isDesktop$() {
@@ -219,14 +264,16 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
) {}
async ngOnInit() {
this.applicationService.activatedProcessId$.pipe(takeUntil(this._onDestroy$)).subscribe((_) => {
this._store.loadShoppingCart();
});
this.applicationService.activatedProcessId$
.pipe(takeUntil(this._onDestroy$))
.subscribe((_) => {
this._store.loadShoppingCart();
});
await this.removeBreadcrumbs();
await this.updateBreadcrumb();
window['Checkout'] = {
window["Checkout"] = {
refreshAvailabilities: this.refreshAvailabilities.bind(this),
};
}
@@ -267,13 +314,16 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
group: { items: ShoppingCartItemDTO[] },
i: number,
) {
return i === 0 ? false : targetBranch.id !== group.items[i - 1].destination?.data?.targetBranch?.data.id;
return i === 0
? false
: targetBranch.id !==
group.items[i - 1].destination?.data?.targetBranch?.data.id;
}
async refreshAvailabilities() {
this.checkingOla$.next(true);
for (let itemComp of this._shoppingCartItems.toArray()) {
for (const itemComp of this._shoppingCartItems.toArray()) {
await itemComp.refreshAvailability();
await new Promise((resolve) => setTimeout(resolve, 100));
}
@@ -283,16 +333,22 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
async updateBreadcrumb() {
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
name: 'Warenkorb',
path: this._navigationService.getCheckoutReviewPath(this.applicationService.activatedProcessId).path,
tags: ['checkout', 'cart'],
section: 'customer',
name: "Warenkorb",
path: this._navigationService.getCheckoutReviewPath(
this.applicationService.activatedProcessId,
).path,
tags: ["checkout", "cart"],
section: "customer",
});
}
async removeBreadcrumbs() {
const checkoutDummyCrumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['checkout', 'cart', 'dummy'])
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, [
"checkout",
"cart",
"dummy",
])
.pipe(first())
.toPromise();
checkoutDummyCrumbs.forEach(async (crumb) => {
@@ -304,32 +360,49 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
this._store.notificationsControl = undefined;
}
openDummyModal({ data, changeDataFromCart = false }: { data?: CheckoutDummyData; changeDataFromCart?: boolean }) {
openDummyModal({
data,
changeDataFromCart = false,
}: {
data?: CheckoutDummyData;
changeDataFromCart?: boolean;
}) {
this.uiModal.open({
content: CheckoutDummyComponent,
data: { ...data, changeDataFromCart },
});
}
changeDummyItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) {
changeDummyItem({
shoppingCartItem,
}: {
shoppingCartItem: ShoppingCartItemDTO;
}) {
this.openDummyModal({ data: shoppingCartItem, changeDataFromCart: true });
}
async changeItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) {
async changeItem({
shoppingCartItem,
}: {
shoppingCartItem: ShoppingCartItemDTO;
}) {
this._purchaseOptionsModalService.open({
processId: this.applicationService.activatedProcessId,
items: [shoppingCartItem],
type: 'update',
type: "update",
});
}
async openPrintModal() {
let shoppingCart = await this.shoppingCart$.pipe(first()).toPromise();
const shoppingCart = await this.shoppingCart$.pipe(first()).toPromise();
this.uiModal.open({
content: PrintModalComponent,
data: {
printerType: 'Label',
print: (printer) => this.domainPrinterService.printCart({ cartId: shoppingCart.id, printer }).toPromise(),
printerType: "Label",
print: (printer) =>
this.domainPrinterService
.printCart({ cartId: shoppingCart.id, printer })
.toPromise(),
} as PrintModalData,
config: {
panelClass: [],
@@ -351,7 +424,8 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
this.loadingOnQuantityChangeById$.next(shoppingCartItem.id);
const shoppingCartItemPrice = shoppingCartItem?.availability?.price?.value?.value;
const shoppingCartItemPrice =
shoppingCartItem?.availability?.price?.value?.value;
const orderType = shoppingCartItem?.features?.orderType;
let availability: AvailabilityDTO;
@@ -360,7 +434,7 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
if (orderType) {
switch (orderType) {
case 'Rücklage':
case "Rücklage":
availability = await this.availabilityService
.getTakeAwayAvailability({
item: {
@@ -369,12 +443,13 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
price: shoppingCartItem.availability.price,
},
quantity,
branch,
})
.toPromise();
// this.setQuantityError(shoppingCartItem, availability, availability?.inStock < quantity);
break;
case 'Abholung':
case "Abholung":
availability = await this.availabilityService
.getPickUpAvailability({
branch,
@@ -388,7 +463,7 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
.pipe(map((av) => av[0]))
.toPromise();
break;
case 'Versand':
case "Versand":
availability = await this.availabilityService
.getDeliveryAvailability({
item: {
@@ -400,7 +475,7 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
})
.toPromise();
break;
case 'DIG-Versand':
case "DIG-Versand":
availability = await this.availabilityService
.getDigDeliveryAvailability({
item: {
@@ -412,7 +487,7 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
})
.toPromise();
break;
case 'B2B-Versand':
case "B2B-Versand":
availability = await this.availabilityService
.getB2bDeliveryAvailability({
item: {
@@ -424,7 +499,7 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
})
.toPromise();
break;
case 'Download':
case "Download":
availability = await this.availabilityService
.getDownloadAvailability({
item: {
@@ -463,7 +538,11 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
shoppingCartItemId: shoppingCartItem.id,
update: {
quantity,
availability: this.compareDeliveryAndCatalogPrice(updateAvailability, orderType, shoppingCartItemPrice),
availability: this.compareDeliveryAndCatalogPrice(
updateAvailability,
orderType,
shoppingCartItemPrice,
),
},
})
.toPromise();
@@ -483,8 +562,15 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
}
// Bei unbekannten Kunden und DIG Bestellung findet ein Vergleich der Preise statt
compareDeliveryAndCatalogPrice(availability: AvailabilityDTO, orderType: string, shoppingCartItemPrice: number) {
if (['Versand', 'DIG-Versand'].includes(orderType) && shoppingCartItemPrice < availability?.price?.value?.value) {
compareDeliveryAndCatalogPrice(
availability: AvailabilityDTO,
orderType: string,
shoppingCartItemPrice: number,
) {
if (
["Versand", "DIG-Versand"].includes(orderType) &&
shoppingCartItemPrice < availability?.price?.value?.value
) {
return {
...availability,
price: {
@@ -507,7 +593,10 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
.pipe(
first(),
switchMap((customerFeatures) => {
return this.domainCheckoutService.canSetCustomer({ processId, customerFeatures });
return this.domainCheckoutService.canSetCustomer({
processId,
customerFeatures,
});
}),
)
.toPromise();
@@ -524,24 +613,31 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
this._purchaseOptionsModalService.open({
processId: this.applicationService.activatedProcessId,
items: shoppingCartItems,
type: 'update',
type: "update",
});
}
async changeAddress() {
const processId = this.applicationService.activatedProcessId;
const customer = await this.domainCheckoutService.getBuyer({ processId }).pipe(first()).toPromise();
const customer = await this.domainCheckoutService
.getBuyer({ processId })
.pipe(first())
.toPromise();
if (!customer) {
this.navigateToCustomerSearch(processId);
return;
}
const customerId = customer.source;
const nav = this._customerSearchNavigation.detailsRoute({ processId, customerId });
const nav = this._customerSearchNavigation.detailsRoute({
processId,
customerId,
});
this.router.navigate(nav.path);
}
async order() {
const shoppingCartItemsWithoutOrderType = await this.shoppingCartItemsWithoutOrderType$.pipe(first()).toPromise();
const shoppingCartItemsWithoutOrderType =
await this.shoppingCartItemsWithoutOrderType$.pipe(first()).toPromise();
if (shoppingCartItemsWithoutOrderType?.length > 0) {
this.showPurchasingListModal(shoppingCartItemsWithoutOrderType);
@@ -549,7 +645,10 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
}
const processId = this.applicationService.activatedProcessId;
const customer = await this.domainCheckoutService.getBuyer({ processId }).pipe(first()).toPromise();
const customer = await this.domainCheckoutService
.getBuyer({ processId })
.pipe(first())
.toPromise();
if (!customer) {
this.navigateToCustomerSearch(processId);
} else {
@@ -557,33 +656,42 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
this.showOrderButtonSpinner = true;
// Ticket #3287 Um nur E-Mail und SMS Benachrichtigungen zu setzen und um alle anderen Benachrichtigungskanäle wie z.B. Brief zu deaktivieren
await this._store.onNotificationChange();
const orders = await this.domainCheckoutService.completeCheckout({ processId }).toPromise();
const orderIds = orders.map((order) => order.id).join(',');
const orders = await this.domainCheckoutService
.completeCheckout({ processId })
.toPromise();
const orderIds = orders.map((order) => order.id).join(",");
this._store.orderCompleted.next();
await this.patchProcess(processId);
await this._navigationService.getCheckoutSummaryPath({ processId, orderIds }).navigate();
await this._navigationService
.getCheckoutSummaryPath({ processId, orderIds })
.navigate();
} catch (error) {
const response = error?.error;
let message: string = response?.message ?? '';
let message: string = response?.message ?? "";
if (response?.invalidProperties && Object.values(response?.invalidProperties)?.length) {
message += `\n${Object.values(response.invalidProperties).join('\n')}`;
if (
response?.invalidProperties &&
Object.values(response?.invalidProperties)?.length
) {
message += `\n${Object.values(response.invalidProperties).join("\n")}`;
}
if (message?.length) {
this.uiModal.open({
content: UiMessageModalComponent,
title: 'Hinweis',
title: "Hinweis",
data: { message: message.trim() },
});
} else if (error) {
this.uiModal.error('Fehler beim abschließen der Bestellung', error);
this.uiModal.error("Fehler beim abschließen der Bestellung", error);
}
if (error.status === 409) {
this._store.orderCompleted.next();
await this.patchProcess(processId);
await this._navigationService.getCheckoutSummaryPath({ processId }).navigate();
await this._navigationService
.getCheckoutSummaryPath({ processId })
.navigate();
}
} finally {
this.showOrderButtonSpinner = false;
@@ -593,11 +701,14 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit
}
async patchProcess(processId: number) {
const process = await this.applicationService.getProcessById$(processId).pipe(first()).toPromise();
const process = await this.applicationService
.getProcessById$(processId)
.pipe(first())
.toPromise();
if (process) {
this.applicationService.patchProcess(process.id, {
name: `${process.name} Bestellbestätigung`,
type: 'cart-checkout',
type: "cart-checkout",
});
}
}

View File

@@ -2,56 +2,61 @@
Überprüfen Sie die Details.
</h1>
<ng-container *ngIf="buyer$ | async; let buyer">
<div *ngIf="!(showAddresses$ | async)" class="flex flex-row items-start justify-between p-5">
<div class="flex flex-row flex-wrap pr-4">
<ng-container *ngIf="getNameFromBuyer(buyer); let name">
<div class="mr-3">{{ name.label }}</div>
<div class="font-bold">{{ name.value }}</div>
</ng-container>
</div>
<button (click)="changeAddress()" class="text-p1 font-bold text-[#F70400]">Ändern</button>
</div>
</ng-container>
<ng-container *ngIf="showNotificationChannels$ | async">
<form class="pb-4" *ngIf="control" [formGroup]="control">
<shared-notification-channel-control
[communicationDetails]="communicationDetails$ | async"
(channelActionEvent)="updateNotifications($event)"
[channelActionName]="'Speichern'"
[channelActionLoading]="notificationChannelLoading$ | async"
formGroupName="notificationChannel"
></shared-notification-channel-control>
</form>
</ng-container>
<ng-container *ngIf="payer$ | async; let payer">
<div *ngIf="showAddresses$ | async" class="flex flex-row items-start justify-between p-5 pt-0">
<div class="flex flex-row flex-wrap pr-4" data-address-type="Rechnungsadresse" data-which="Rechnungsadresse">
<div class="mr-3" data-what="title">Rechnungsadresse</div>
<div class="font-bold" data-what="address">
{{ payer | payerAddress }}
@if (buyer$ | async; as buyer) {
@if (!(showAddresses$ | async)) {
<div class="flex flex-row items-start justify-between p-5">
<div class="flex flex-row flex-wrap pr-4">
@if (getNameFromBuyer(buyer); as name) {
<div class="mr-3">{{ name.label }}</div>
<div class="font-bold">{{ name.value }}</div>
}
</div>
<button (click)="changeAddress()" class="text-p1 font-bold text-[#F70400]">Ändern</button>
</div>
}
}
<button (click)="changeAddress()" class="text-p1 font-bold text-[#F70400]">Ändern</button>
</div>
</ng-container>
@if (showNotificationChannels$ | async) {
@if (control) {
<form class="pb-4" [formGroup]="control">
<shared-notification-channel-control
[communicationDetails]="communicationDetails$ | async"
(channelActionEvent)="updateNotifications($event)"
[channelActionName]="'Speichern'"
[channelActionLoading]="notificationChannelLoading$ | async"
formGroupName="notificationChannel"
></shared-notification-channel-control>
</form>
}
}
<ng-container *ngIf="payer$ | async; let payer">
<div *ngIf="showAddresses$ | async" class="flex flex-row items-start justify-between px-5">
<div class="flex flex-row flex-wrap pr-4" data-address-type="Lieferadresse" data-which="Lieferadresse">
<div class="mr-3" data-what="title">Lieferadresse</div>
<div class="font-bold" data-what="address">
{{ shippingAddress$ | async | shippingAddress }}
@if (payer$ | async; as payer) {
@if (showAddresses$ | async) {
<div class="flex flex-row items-start justify-between p-5 pt-0">
<div class="flex flex-row flex-wrap pr-4" data-address-type="Rechnungsadresse" data-which="Rechnungsadresse">
<div class="mr-3" data-what="title">Rechnungsadresse</div>
<div class="font-bold" data-what="address">
{{ payer | payerAddress }}
</div>
</div>
<button (click)="changeAddress()" class="text-p1 font-bold text-[#F70400]">Ändern</button>
</div>
}
}
<button (click)="changeAddress()" class="text-p1 font-bold text-[#F70400]">Ändern</button>
</div>
</ng-container>
@if (payer$ | async; as payer) {
@if (showAddresses$ | async) {
<div class="flex flex-row items-start justify-between px-5">
<div class="flex flex-row flex-wrap pr-4" data-address-type="Lieferadresse" data-which="Lieferadresse">
<div class="mr-3" data-what="title">Lieferadresse</div>
<div class="font-bold" data-what="address">
{{ shippingAddress$ | async | shippingAddress }}
</div>
</div>
<button (click)="changeAddress()" class="text-p1 font-bold text-[#F70400]">Ändern</button>
</div>
}
}
<page-special-comment
class="mb-6 mt-4"

View File

@@ -1,18 +1,21 @@
<div class="item-thumbnail">
<a [routerLink]="productSearchDetailsPath" [queryParams]="{ main_qs: item?.product?.ean }">
<img loading="lazy" *ngIf="item?.product?.ean | productImage; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" />
@if (item?.product?.ean | productImage; as thumbnailUrl) {
<img loading="lazy" [src]="thumbnailUrl" [alt]="item?.product?.name" />
}
</a>
</div>
<div class="item-contributors">
<a
*ngFor="let contributor of contributors$ | async; let last = last"
[routerLink]="productSearchResultsPath"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
@for (contributor of contributors$ | async; track contributor; let last = $last) {
<a
[routerLink]="productSearchResultsPath"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
}
</div>
<div
@@ -21,86 +24,106 @@
[class.text-p1]="item?.product?.name?.length >= 50 || !isTablet"
[class.text-p2]="item?.product?.name?.length >= 60 && isTablet"
[class.text-p3]="item?.product?.name?.length >= 100"
>
>
<a [routerLink]="productSearchDetailsPath" [queryParams]="{ main_qs: item?.product?.ean }">{{ item?.product?.name }}</a>
</div>
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
</div>
@if (item?.product?.format && item?.product?.formatDetail) {
<div class="item-format">
@if (item?.product?.format !== '--') {
<img
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
}
{{ item?.product?.formatDetail }}
</div>
}
<div class="item-info text-p2">
<div class="mb-1">{{ item?.product?.manufacturer | substr: 25 }} | {{ item?.product?.ean }}</div>
<div class="mb-1">
{{ item?.product?.volume }}
<span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
@if (item?.product?.volume && item?.product?.publicationDate) {
<span>|</span>
}
{{ item?.product?.publicationDate | date }}
</div>
<div *ngIf="notAvailable$ | async">
<span class="text-brand item-date">Nicht verfügbar</span>
</div>
<shared-skeleton-loader class="w-40" *ngIf="refreshingAvailabilit$ | async; else avaTmplt"></shared-skeleton-loader>
<ng-template #avaTmplt>
<div class="item-date" [class.availability-changed]="estimatedShippingDateChanged$ | async" *ngIf="orderType === 'Abholung'">
Abholung ab {{ item?.availability?.estimatedShippingDate | date }}
@if (notAvailable$ | async) {
<div>
<span class="text-brand item-date">Nicht verfügbar</span>
</div>
<div
class="item-date"
[class.availability-changed]="estimatedShippingDateChanged$ | async"
*ngIf="orderType === 'Versand' || orderType === 'B2B-Versand' || orderType === 'DIG-Versand'"
>
<ng-container *ngIf="item?.availability?.estimatedDelivery; else estimatedShippingDate">
Zustellung zwischen {{ (item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }}
und
{{ (item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
</ng-container>
<ng-template #estimatedShippingDate>Versand {{ item?.availability?.estimatedShippingDate | date }}</ng-template>
</div>
</ng-template>
}
<div class="item-availability-message" *ngIf="olaError$ | async">Artikel nicht verfügbar</div>
@if (refreshingAvailabilit$ | async) {
<shared-skeleton-loader class="w-40"></shared-skeleton-loader>
} @else {
@if (orderType === 'Abholung') {
<div class="item-date" [class.availability-changed]="estimatedShippingDateChanged$ | async">
Abholung ab {{ item?.availability?.estimatedShippingDate | date }}
</div>
}
@if (orderType === 'Versand' || orderType === 'B2B-Versand' || orderType === 'DIG-Versand') {
<div
class="item-date"
[class.availability-changed]="estimatedShippingDateChanged$ | async"
>
@if (item?.availability?.estimatedDelivery) {
Zustellung zwischen {{ (item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }}
und
{{ (item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
} @else {
Versand {{ item?.availability?.estimatedShippingDate | date }}
}
</div>
}
}
@if (olaError$ | async) {
<div class="item-availability-message">Artikel nicht verfügbar</div>
}
</div>
<div class="item-price-stock flex flex-col">
<div class="text-p2 font-bold">{{ item?.availability?.price?.value?.value | currency: 'EUR' : 'code' }}</div>
<div class="text-p2 font-normal">
<ui-quantity-dropdown
*ngIf="!(isDummy$ | async); else quantityDummy"
[ngModel]="item?.quantity"
(ngModelChange)="onChangeQuantity($event)"
[showSpinner]="(loadingOnQuantityChangeById$ | async) === item?.id"
[disabled]="(loadingOnItemChangeById$ | async) === item?.id"
[range]="quantityRange$ | async"
></ui-quantity-dropdown>
<ng-template #quantityDummy>
@if (!(isDummy$ | async)) {
<ui-quantity-dropdown
[ngModel]="item?.quantity"
(ngModelChange)="onChangeQuantity($event)"
[showSpinner]="(loadingOnQuantityChangeById$ | async) === item?.id"
[disabled]="(loadingOnItemChangeById$ | async) === item?.id"
[range]="quantityRange$ | async"
></ui-quantity-dropdown>
} @else {
<div class="mt-2">{{ item?.quantity }}x</div>
</ng-template>
</div>
<div class="quantity-error" *ngIf="quantityError">
{{ quantityError }}
}
</div>
@if (quantityError) {
<div class="quantity-error">
{{ quantityError }}
</div>
}
</div>
<div class="actions" *ngIf="orderType !== 'Download'">
<button
[disabled]="(loadingOnQuantityChangeById$ | async) === item?.id || (loadingOnItemChangeById$ | async) === item?.id"
(click)="onChangeItem()"
*ngIf="!(hasOrderType$ | async)"
>
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">Lieferweg auswählen</ui-spinner>
</button>
<button
[disabled]="(loadingOnQuantityChangeById$ | async) === item?.id || (loadingOnItemChangeById$ | async) === item?.id"
(click)="onChangeItem()"
*ngIf="canEdit$ | async"
>
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">Lieferweg ändern</ui-spinner>
</button>
</div>
@if (orderType !== 'Download') {
<div class="actions">
@if (!(hasOrderType$ | async)) {
<button
[disabled]="(loadingOnQuantityChangeById$ | async) === item?.id || (loadingOnItemChangeById$ | async) === item?.id"
(click)="onChangeItem()"
>
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">Lieferweg auswählen</ui-spinner>
</button>
}
@if (canEdit$ | async) {
<button
[disabled]="(loadingOnQuantityChangeById$ | async) === item?.id || (loadingOnItemChangeById$ | async) === item?.id"
(click)="onChangeItem()"
>
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">Lieferweg ändern</ui-spinner>
</button>
}
</div>
}

View File

@@ -24,11 +24,15 @@
></textarea>
<div class="comment-actions py-4">
<button type="reset" class="clear pl-4" *ngIf="!disabled && !!value" (click)="clear(); triggerResize()">
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
@if (!disabled && !!value) {
<button type="reset" class="clear pl-4" (click)="clear(); triggerResize()">
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
}
</div>
</div>
<div *ngIf="!(hasPayer || hasBuyer)" class="text-p3">Zur Info: Sie haben dem Warenkorb noch keinen Kunden hinzugefügt.</div>
@if (!(hasPayer || hasBuyer)) {
<div class="text-p3">Zur Info: Sie haben dem Warenkorb noch keinen Kunden hinzugefügt.</div>
}
</div>

View File

@@ -7,66 +7,63 @@
<h1 class="text-center text-h2 my-1 font-bold">Bestellbestätigung</h1>
<p class="text-center text-p1 mb-10">Nachfolgend erhalten Sie die Übersicht Ihrer Bestellung.</p>
<ng-container *ngFor="let displayOrder of displayOrders$ | async; let i = index; let orderLast = last">
<ng-container *ngIf="i === 0">
@for (displayOrder of displayOrders$ | async; track displayOrder; let i = $index; let orderLast = $last) {
@if (i === 0) {
<div class="flex flex-row items-center bg-white shadow-card min-h-[3.3125rem]">
<div class="text-h3 font-bold px-5 py-[0.875rem]">
{{ displayOrder?.buyer | buyerName }}
</div>
</div>
<hr />
</ng-container>
}
<div class="flex flex-row items-center bg-[#F5F7FA] min-h-[3.3125rem]">
<div class="flex flex-row items-center justify-center px-5 py-[0.875rem]">
<shared-icon
*ngIf="(displayOrder?.items)[0]?.features?.orderType !== 'Dummy'"
class="mr-2"
[size]="(displayOrder?.items)[0]?.features?.orderType === 'B2B-Versand' ? 36 : 24"
[icon]="(displayOrder?.items)[0]?.features?.orderType"
></shared-icon>
@if ((displayOrder?.items)[0]?.features?.orderType !== 'Dummy') {
<shared-icon
class="mr-2"
[size]="(displayOrder?.items)[0]?.features?.orderType === 'B2B-Versand' ? 36 : 24"
[icon]="(displayOrder?.items)[0]?.features?.orderType"
></shared-icon>
}
<p class="text-p1 font-bold mr-3">{{ (displayOrder?.items)[0]?.features?.orderType }}</p>
<div
*ngIf="
(displayOrder?.items)[0]?.features?.orderType === 'Abholung' || (displayOrder?.items)[0]?.features?.orderType === 'Rücklage';
else shippingAddress
"
>
{{ displayOrder.targetBranch?.name }}, {{ displayOrder.targetBranch | branchAddress }}
</div>
<ng-template #shippingAddress>
@if (
(displayOrder?.items)[0]?.features?.orderType === 'Abholung' || (displayOrder?.items)[0]?.features?.orderType === 'Rücklage') {
<div
>
{{ displayOrder.targetBranch?.name }}, {{ displayOrder.targetBranch | branchAddress }}
</div>
} @else {
{{ displayOrder.shippingAddress | branchAddress }}
</ng-template>
<div *ngIf="(displayOrder?.items)[0]?.features?.orderType === 'Download'">
| {{ displayOrder.buyer?.communicationDetails?.email }}
</div>
}
@if ((displayOrder?.items)[0]?.features?.orderType === 'Download') {
<div>
| {{ displayOrder.buyer?.communicationDetails?.email }}
</div>
}
</div>
</div>
<hr />
<div class="flex flex-row justify-between items-center">
<div class="flex flex-col px-5 py-4 bg-white" [attr.data-order-type]="(displayOrder?.items)[0]?.features?.orderType">
<div class="flex flex-row justify-between items-center mb-[0.375rem]">
<div class="flex flex-row">
<span class="w-32">Vorgangs-ID</span>
<ng-container *ngIf="customer$ | async; let customer">
<a
data-which="Vorgangs-ID"
data-what="link"
*ngIf="customer$ | async; let customer"
class="font-bold text-[#0556B4] no-underline"
[routerLink]="['/kunde', processId, 'customer', 'search', customer?.id, 'orders', displayOrder.id]"
[queryParams]="{ main_qs: customer?.customerNumber, filter_customertype: '' }"
>
{{ displayOrder.orderNumber }}
</a>
</ng-container>
@if (customer$ | async; as customer) {
@if (customer$ | async; as customer) {
<a
data-which="Vorgangs-ID"
data-what="link"
class="font-bold text-[#0556B4] no-underline"
[routerLink]="['/kunde', processId, 'customer', 'search', customer?.id, 'orders', displayOrder.id]"
[queryParams]="{ main_qs: customer?.customerNumber, filter_customertype: '' }"
>
{{ displayOrder.orderNumber }}
</a>
}
}
<ui-spinner class="text-[#0556B4] h-4 w-4" [show]="!(customer$ | async)"></ui-spinner>
</div>
</div>
<div class="flex flex-row justify-between items-center">
<div class="flex flex-row">
<span class="w-32">Bestelldatum</span>
@@ -74,14 +71,13 @@
</div>
</div>
</div>
<div class="mr-4">
<button
(click)="expanded[i] = !expanded[i]"
type="button"
class="text-[#0556B4] font-bold flex flex-row items-center justify-center"
[class.flex-row-reverse]="!expanded[i]"
>
>
<shared-icon
class="mr-1"
icon="arrow-back"
@@ -93,135 +89,139 @@
</button>
</div>
</div>
<ng-container *ngFor="let order of displayOrder.items; let last = last">
<ng-container *ngIf="expanded[i]">
@for (order of displayOrder.items; track order; let last = $last) {
@if (expanded[i]) {
<div
class="page-checkout-summary__items-tablet px-5 pb-[1.875rem] bg-white"
[class.page-checkout-summary__items]="isDesktop$ | async"
[class.last]="last"
>
>
<div class="page-checkout-summary__items-thumbnail flex flex-row">
<a [routerLink]="getProductSearchDetailsPath(order?.product?.ean)" [queryParams]="getProductSearchDetailsQueryParams(order)">
<img class="w-[3.125rem] max-h-20 mr-2" [src]="order.product?.ean | productImage: 195 : 315 : true" />
</a>
</div>
<div class="page-checkout-summary__items-title whitespace-nowrap overflow-ellipsis overflow-hidden">
<a
class="font-bold no-underline text-[#0556B4]"
[routerLink]="getProductSearchDetailsPath(order?.product?.ean)"
[queryParams]="getProductSearchDetailsQueryParams(order)"
>
>
{{ order?.product?.name }}
</a>
</div>
<div class="page-checkout-summary__items-ssc" *ngIf="(order?.subsetItems)[0]; let subsetItem">
<span class="mr-2">{{ subsetItem.supplierName }}</span>
<span *ngIf="subsetItem?.ssc && subsetItem?.sscText" class="font-bold border-l border-black pl-2">
{{ subsetItem.ssc }} - {{ subsetItem.sscText }}
</span>
</div>
@if ((order?.subsetItems)[0]; as subsetItem) {
<div class="page-checkout-summary__items-ssc">
<span class="mr-2">{{ subsetItem.supplierName }}</span>
@if (subsetItem?.ssc && subsetItem?.sscText) {
<span class="font-bold border-l border-black pl-2">
{{ subsetItem.ssc }} - {{ subsetItem.sscText }}
</span>
}
</div>
}
<div class="page-checkout-summary__items-quantity font-bold justify-self-end">
<span>{{ order.quantity }}x</span>
</div>
<div class="page-checkout-summary__items-price font-bold justify-self-end">
<span>{{ order.price?.value?.value | currency: ' ' }} {{ order.price?.value?.currency }}</span>
</div>
<div class="page-checkout-summary__items-delivery product-details">
<div class="delivery-row" [ngSwitch]="order?.features?.orderType">
<ng-container *ngSwitchCase="'Abholung'">
<span class="order-type">
Abholung ab {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }}
<ng-container [ngTemplateOutlet]="abholfrist" [ngTemplateOutletContext]="{ order: order }"></ng-container>
</span>
</ng-container>
<ng-container *ngSwitchCase="'Rücklage'">
<span class="order-type">
{{ order?.features?.orderType }}
<ng-container [ngTemplateOutlet]="abholfrist" [ngTemplateOutletContext]="{ order: order }"></ng-container>
</span>
</ng-container>
<ng-container *ngSwitchCase="['Versand', 'B2B-Versand', 'DIG-Versand'].indexOf(order?.features?.orderType) > -1">
<ng-container *ngIf="(order?.subsetItems)[0]?.estimatedDelivery; else estimatedShippingDate">
<div class="delivery-row">
@switch (order?.features?.orderType) {
@case ('Abholung') {
<span class="order-type">
Zustellung zwischen
{{ ((order?.subsetItems)[0]?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
{{ ((order?.subsetItems)[0]?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
Abholung ab {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }}
<ng-container [ngTemplateOutlet]="abholfrist" [ngTemplateOutletContext]="{ order: order }"></ng-container>
</span>
</ng-container>
<ng-template #estimatedShippingDate>
<span class="order-type">Versanddatum {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }}</span>
</ng-template>
</ng-container>
<ng-container *ngSwitchDefault>
<span class="order-type">{{ order?.features?.orderType }}</span>
</ng-container>
}
@case ('Rücklage') {
<span class="order-type">
{{ order?.features?.orderType }}
<ng-container [ngTemplateOutlet]="abholfrist" [ngTemplateOutletContext]="{ order: order }"></ng-container>
</span>
}
@case (['Versand', 'B2B-Versand', 'DIG-Versand'].indexOf(order?.features?.orderType) > -1) {
@if ((order?.subsetItems)[0]?.estimatedDelivery) {
<span class="order-type">
Zustellung zwischen
{{ ((order?.subsetItems)[0]?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
{{ ((order?.subsetItems)[0]?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
</span>
} @else {
<span class="order-type">Versanddatum {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }}</span>
}
}
@default {
<span class="order-type">{{ order?.features?.orderType }}</span>
}
}
</div>
</div>
</div>
</ng-container>
<hr *ngIf="last" />
</ng-container>
<ng-container *ngIf="orderLast">
}
@if (last) {
<hr />
}
}
@if (orderLast) {
<div class="flex flex-row justify-between items-center min-h-[3.3125rem] bg-white px-5 py-4 rounded-b">
<span *ngIf="totalReadingPoints$ | async; let totalReadingPoints" class="text-p2 font-bold">
{{ totalItemCount$ | async }} Artikel | {{ totalReadingPoints }} Lesepunkte
</span>
@if (totalReadingPoints$ | async; as totalReadingPoints) {
<span class="text-p2 font-bold">
{{ totalItemCount$ | async }} Artikel | {{ totalReadingPoints }} Lesepunkte
</span>
}
<div class="flex flex-row items-center justify-center">
<div class="text-p1 font-bold flex flex-row items-center">
<div class="mr-1">Gesamtsumme {{ totalPrice$ | async | currency: ' ' }} {{ totalPriceCurrency$ | async }}</div>
</div>
<div
class="bg-brand text-white font-bold text-p1 outline-none border-none rounded-full px-6 py-3 ml-2"
*ngIf="(takeNowOrders$ | async)?.length === 1 && (isB2BCustomer$ | async)"
>
<button class="cta-goods-out" (click)="navigateToShelfOut()">Zur Warenausgabe</button>
</div>
@if ((takeNowOrders$ | async)?.length === 1 && (isB2BCustomer$ | async)) {
<div
class="bg-brand text-white font-bold text-p1 outline-none border-none rounded-full px-6 py-3 ml-2"
>
<button class="cta-goods-out" (click)="navigateToShelfOut()">Zur Warenausgabe</button>
</div>
}
</div>
</div>
</ng-container>
</ng-container>
}
}
</div>
</div>
<ng-template #abholfrist let-order="order">
<div *ngIf="!(updatingPreferredPickUpDate$ | async)[(order?.subsetItems)[0].id]" class="inline-flex">
<button [uiOverlayTrigger]="deadlineDatepicker" #deadlineDatepickerTrigger="uiOverlayTrigger" class="flex flex-row items-center">
<span class="mx-[0.625rem] font-normal">bis</span>
<strong class="border-r border-[#AEB7C1] pr-4">
{{ ((order?.subsetItems)[0]?.preferredPickUpDate | date: 'dd.MM.yy') || 'TT.MM.JJJJ' }}
</strong>
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
</button>
<ui-datepicker
#deadlineDatepicker
yPosition="below"
xPosition="after"
[xOffset]="8"
[min]="minDateDatepicker"
[disabledDaysOfWeek]="[0]"
[(selected)]="selectedDate"
>
<div #content class="grid grid-flow-row gap-2">
<button
class="rounded-full font-bold text-white bg-brand py-px-15 px-px-25"
(click)="updatePreferredPickUpDate(undefined, selectedDate); deadlineDatepickerTrigger.close()"
@if (!(updatingPreferredPickUpDate$ | async)[(order?.subsetItems)[0].id]) {
<div class="inline-flex">
<button [uiOverlayTrigger]="deadlineDatepicker" #deadlineDatepickerTrigger="uiOverlayTrigger" class="flex flex-row items-center">
<span class="mx-[0.625rem] font-normal">bis</span>
<strong class="border-r border-[#AEB7C1] pr-4">
{{ ((order?.subsetItems)[0]?.preferredPickUpDate | date: 'dd.MM.yy') || 'TT.MM.JJJJ' }}
</strong>
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
</button>
<ui-datepicker
#deadlineDatepicker
yPosition="below"
xPosition="after"
[xOffset]="8"
[min]="minDateDatepicker"
[disabledDaysOfWeek]="[0]"
[(selected)]="selectedDate"
>
Für den Warenkorb festlegen
</button>
</div>
</ui-datepicker>
</div>
<div class="fetching" *ngIf="!!(updatingPreferredPickUpDate$ | async)[(order?.subsetItems)[0].id]"></div>
<div #content class="grid grid-flow-row gap-2">
<button
class="rounded-full font-bold text-white bg-brand py-px-15 px-px-25"
(click)="updatePreferredPickUpDate(undefined, selectedDate); deadlineDatepickerTrigger.close()"
>
Für den Warenkorb festlegen
</button>
</div>
</ui-datepicker>
</div>
}
@if (!!(updatingPreferredPickUpDate$ | async)[(order?.subsetItems)[0].id]) {
<div class="fetching"></div>
}
</ng-template>
<div class="relative">
@@ -232,17 +232,18 @@
type="button"
class="px-6 py-2 rounded-full border-2 border-solid border-brand text-brand bg-white font-bold text-lg whitespace-nowrap h-14 flex flex-row items-center justify-center print-button"
(click)="printOrderConfirmation()"
>
>
<ui-spinner class="min-h-4 min-w-4" [show]="isPrinting$ | async">Bestellbestätigung drucken</ui-spinner>
</button>
<button
*ngIf="hasAbholung$ | async"
type="button"
class="px-6 py-2 rounded-full border-2 border-solid border-brand text-brand bg-white font-bold text-lg whitespace-nowrap h-14"
(click)="sendOrderConfirmation()"
>
Bestellbestätigung senden
</button>
@if (hasAbholung$ | async) {
<button
type="button"
class="px-6 py-2 rounded-full border-2 border-solid border-brand text-brand bg-white font-bold text-lg whitespace-nowrap h-14"
(click)="sendOrderConfirmation()"
>
Bestellbestätigung senden
</button>
}
</div>
</div>

View File

@@ -15,7 +15,11 @@ import { CrmCustomerService } from '@domain/crm';
import { ActivatedRoute, Router } from '@angular/router';
import { DomainOmsService } from '@domain/oms';
import { DomainCatalogService } from '@domain/catalog';
import { DisplayOrderDTO, DisplayOrderItemDTO, DisplayOrderItemSubsetDTO } from '@generated/swagger/oms-api';
import {
DisplayOrderDTO,
DisplayOrderItemDTO,
DisplayOrderItemSubsetDTO,
} from '@generated/swagger/oms-api';
import { BreadcrumbService } from '@core/breadcrumb';
import { ApplicationService } from '@core/application';
import { DomainPrinterService } from '@domain/printer';
@@ -49,45 +53,73 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
private _onDestroy$ = new Subject<void>();
processId = Date.now();
selectedDate = this.dateAdapter.today();
minDateDatepicker = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), -1);
minDateDatepicker = this.dateAdapter.addCalendarDays(
this.dateAdapter.today(),
-1,
);
updatingPreferredPickUpDate$ = new BehaviorSubject<Record<string, string>>({});
updatingPreferredPickUpDate$ = new BehaviorSubject<Record<string, string>>(
{},
);
displayOrders$ = combineLatest([this.domainCheckoutService.getOrders(), this._route.params]).pipe(
displayOrders$ = combineLatest([
this.domainCheckoutService.getOrders(),
this._route.params,
]).pipe(
map(([orders, params]) => {
let filteredOrders: DisplayOrderDTO[] = [];
if (params?.orderIds) {
const orderIds: string[] = params.orderIds.split(',');
filteredOrders = orders.filter((order) => orderIds.find((id) => Number(id) === order.id));
filteredOrders = orders.filter((order) =>
orderIds.find((id) => Number(id) === order.id),
);
} else {
return filteredOrders;
}
// Ticket #4228 Für die korrekte Gruppierung der Items bei gleichem Bestellziel (Aufsplitten von Abholung und Rücklage)
const ordersWithMultipleFeatures = filteredOrders.filter((order) =>
order.items.find((item) => item.features.orderType !== order.features.orderType),
order.items.find(
(item) => item.features.orderType !== order.features.orderType,
),
);
if (ordersWithMultipleFeatures) {
for (let orderWithMultipleFeatures of ordersWithMultipleFeatures) {
if (orderWithMultipleFeatures?.items?.length > 1) {
const itemsWithOrderFeature = orderWithMultipleFeatures.items.filter(
(item) => item.features.orderType === orderWithMultipleFeatures.features.orderType,
);
const itemsWithDifferentOrderFeature = orderWithMultipleFeatures.items.filter(
(item) => item.features.orderType !== orderWithMultipleFeatures.features.orderType,
);
const itemsWithOrderFeature =
orderWithMultipleFeatures.items.filter(
(item) =>
item.features.orderType ===
orderWithMultipleFeatures.features.orderType,
);
const itemsWithDifferentOrderFeature =
orderWithMultipleFeatures.items.filter(
(item) =>
item.features.orderType !==
orderWithMultipleFeatures.features.orderType,
);
filteredOrders = [...filteredOrders.filter((order) => order.id !== orderWithMultipleFeatures.id)];
filteredOrders = [
...filteredOrders.filter(
(order) => order.id !== orderWithMultipleFeatures.id,
),
];
if (itemsWithOrderFeature?.length > 0) {
filteredOrders = [...filteredOrders, { ...orderWithMultipleFeatures, items: itemsWithOrderFeature }];
filteredOrders = [
...filteredOrders,
{ ...orderWithMultipleFeatures, items: itemsWithOrderFeature },
];
}
if (itemsWithDifferentOrderFeature?.length > 0) {
filteredOrders = [
...filteredOrders,
{ ...orderWithMultipleFeatures, items: itemsWithDifferentOrderFeature },
{
...orderWithMultipleFeatures,
items: itemsWithDifferentOrderFeature,
},
];
}
}
@@ -97,7 +129,9 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
return filteredOrders?.map((order) => {
return {
...order,
items: [...order.items]?.sort((a, b) => a.product?.name.localeCompare(b.product?.name)),
items: [...order.items]?.sort((a, b) =>
a.product?.name.localeCompare(b.product?.name),
),
};
});
}),
@@ -105,14 +139,23 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
);
hasAbholung$ = this.displayOrders$.pipe(
map((displayOrders) => displayOrders.filter((order) => order.features?.orderType === 'Abholung')?.length > 0),
map(
(displayOrders) =>
displayOrders.filter(
(order) => order.features?.orderType === 'Abholung',
)?.length > 0,
),
);
totalItemCount$ = this.displayOrders$.pipe(
map((displayOrders) =>
displayOrders.reduce(
(total, displayOrder) =>
total + displayOrder?.items?.reduce((subTotal, order) => subTotal + order?.quantity, 0),
total +
displayOrder?.items?.reduce(
(subTotal, order) => subTotal + order?.quantity,
0,
),
0,
),
),
@@ -121,7 +164,10 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
totalReadingPoints$ = this.displayOrders$.pipe(
switchMap((displayOrders) => {
const items = displayOrders
.reduce<DisplayOrderItemDTO[]>((items, order) => [...items, ...order.items], [])
.reduce<DisplayOrderItemDTO[]>(
(items, order) => [...items, ...order.items],
[],
)
.map((i) => {
if (i?.product?.catalogProductNumber) {
return {
@@ -135,7 +181,14 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
if (items.length !== 0) {
return this.domainCatalogService
.getPromotionPoints({ items })
.pipe(map((response) => Object.values(response.result).reduce((sum, points) => sum + points, 0)));
.pipe(
map((response) =>
Object.values(response.result).reduce(
(sum, points) => sum + points,
0,
),
),
);
} else {
return NEVER;
}
@@ -147,7 +200,11 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
displayOrders.reduce(
(total, displayOrder) =>
total +
displayOrder?.items?.reduce((subTotal, order) => subTotal + order?.price?.value?.value * order.quantity, 0),
displayOrder?.items?.reduce(
(subTotal, order) =>
subTotal + order?.price?.value?.value * order.quantity,
0,
),
0,
),
),
@@ -162,22 +219,33 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
containsDeliveryOrder$ = this.displayOrders$.pipe(
map(
(displayOrders) =>
displayOrders.filter((o) => ['Versand', 'B2B-Versand', 'DIG-Versand'].indexOf(o.features?.orderType) > -1)
?.length > 0,
displayOrders.filter(
(o) =>
['Versand', 'B2B-Versand', 'DIG-Versand'].indexOf(
o.features?.orderType,
) > -1,
)?.length > 0,
),
);
customer$ = this.displayOrders$.pipe(
switchMap((o) => this.customerService.getCustomers(o[0].buyerNumber, { take: 5 })),
switchMap((o) =>
this.customerService.getCustomers(o[0].buyerNumber, { take: 5 }),
),
map((customers) => customers.result[0]),
shareReplay(),
);
isB2BCustomer$ = this.customer$.pipe(map((customer) => customer?.features?.find((f) => f.key === 'b2b') != null));
isB2BCustomer$ = this.customer$.pipe(
map((customer) => customer?.features?.find((f) => f.key === 'b2b') != null),
);
takeNowOrders$ = this.displayOrders$.pipe(
map((displayOrders) =>
displayOrders.filter((o) => o.items.find((oi) => oi.features?.orderType === 'Rücklage') != null),
displayOrders.filter(
(o) =>
o.items.find((oi) => oi.features?.orderType === 'Rücklage') != null,
),
),
);
@@ -210,7 +278,9 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
private _cdr: ChangeDetectorRef,
) {
this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['checkout'])
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, [
'checkout',
])
.pipe(first())
.subscribe(async (crumbs) => {
for await (const crumb of crumbs) {
@@ -264,7 +334,8 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
getProductSearchDetailsQueryParams(item: DisplayOrderItemDTO) {
return {
main_qs: item?.product?.ean,
filter_format: item?.features?.orderType === 'Download' ? 'eb;dl' : undefined,
filter_format:
item?.features?.orderType === 'Download' ? 'eb;dl' : undefined,
};
}
@@ -274,9 +345,14 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
try {
const items = item ? [item] : await this.getAllOrderItems();
const subsetItems = items
.filter((item) => ['Rücklage', 'Abholung'].includes(item.features.orderType))
.filter((item) =>
['Rücklage', 'Abholung'].includes(item.features.orderType),
)
// .flatMap((item) => item.subsetItems);
.reduce<DisplayOrderItemSubsetDTO[]>((acc, item) => [...acc, ...item.subsetItems], []);
.reduce<DisplayOrderItemSubsetDTO[]>(
(acc, item) => [...acc, ...item.subsetItems],
[],
);
subsetItems.forEach((item) => (data[`${item.id}`] = date?.toISOString()));
try {
@@ -310,7 +386,10 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
async getAllOrderItems() {
const orders = await this.displayOrders$.pipe(first()).toPromise();
return orders.reduce<DisplayOrderItemDTO[]>((agg, order) => [...agg, ...order.items], []);
return orders.reduce<DisplayOrderItemDTO[]>(
(agg, order) => [...agg, ...order.items],
[],
);
}
async updateDisplayOrderItem(item: DisplayOrderItemDTO) {
@@ -322,9 +401,16 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
if (takeNowOrders.length != 1) return;
try {
await this.router.navigate(this._shelfOutNavigationService.listRoute({ processId: Date.now() }).path, {
queryParams: { main_qs: takeNowOrders[0].orderNumber, filter_supplier_id: '16' },
});
await this.router.navigate(
this._shelfOutNavigationService.listRoute({ processId: Date.now() })
.path,
{
queryParams: {
main_qs: takeNowOrders[0].orderNumber,
filter_supplier_id: '16',
},
},
);
} catch (e) {
console.error(e);
}
@@ -344,7 +430,8 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
.pipe(
first(),
map((printers) => {
if (Array.isArray(printers)) return printers.find((printer) => printer.selected === true);
if (Array.isArray(printers))
return printers.find((printer) => printer.selected === true);
}),
)
.toPromise();
@@ -362,10 +449,16 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
const result = await this.domainPrinterService
.printOrder({ orderIds: orders.map((o) => o.id), printer })
.toPromise();
this._toaster.open({ type: 'success', message: 'Bestellbestätigung wurde gedruckt' });
this._toaster.open({
type: 'success',
message: 'Bestellbestätigung wurde gedruckt',
});
return result;
} catch (error) {
this._toaster.open({ type: 'danger', message: 'Fehler beim Drucken der Bestellbestätigung' });
this._toaster.open({
type: 'danger',
message: 'Fehler beim Drucken der Bestellbestätigung',
});
} finally {
this.isPrinting$.next(false);
}
@@ -381,12 +474,21 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
} else {
try {
const result = await this.domainPrinterService
.printOrder({ orderIds: orders.map((o) => o.id), printer: selectedPrinter.key })
.printOrder({
orderIds: orders.map((o) => o.id),
printer: selectedPrinter.key,
})
.toPromise();
this._toaster.open({ type: 'success', message: 'Bestellbestätigung wurde gedruckt' });
this._toaster.open({
type: 'success',
message: 'Bestellbestätigung wurde gedruckt',
});
return result;
} catch (error) {
this._toaster.open({ type: 'danger', message: 'Fehler beim Drucken der Bestellbestätigung' });
this._toaster.open({
type: 'danger',
message: 'Fehler beim Drucken der Bestellbestätigung',
});
} finally {
this.isPrinting$.next(false);
}

View File

@@ -1,35 +1,35 @@
<ng-container *ngIf="orderItem$ | async; let orderItem">
@if (orderItem$ | async; as orderItem) {
<div #features class="page-customer-order-details-item__features">
<ng-container *ngIf="orderItem?.features?.prebooked">
@if (orderItem?.features?.prebooked) {
<img [uiOverlayTrigger]="prebookedTooltip" src="/assets/images/tag_icon_preorder.svg" [alt]="orderItem?.features?.prebooked" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #prebookedTooltip [closeable]="true">
Artikel wird für Sie vorgemerkt.
</ui-tooltip>
</ng-container>
<ng-container *ngIf="notificationsSent$ | async; let notificationsSent">
<ng-container *ngIf="notificationsSent?.NOTIFICATION_EMAIL">
}
@if (notificationsSent$ | async; as notificationsSent) {
@if (notificationsSent?.NOTIFICATION_EMAIL) {
<img [uiOverlayTrigger]="emailTooltip" src="/assets/images/email_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #emailTooltip [closeable]="true">
Per E-Mail benachrichtigt
<br />
<ng-container *ngFor="let notification of notificationsSent?.NOTIFICATION_EMAIL">
@for (notification of notificationsSent?.NOTIFICATION_EMAIL; track notification) {
{{ notification | date: 'dd.MM.yyyy | HH:mm' }} Uhr
<br />
</ng-container>
}
</ui-tooltip>
</ng-container>
<ng-container *ngIf="notificationsSent?.NOTIFICATION_SMS">
}
@if (notificationsSent?.NOTIFICATION_SMS) {
<img [uiOverlayTrigger]="smsTooltip" src="/assets/images/sms_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #smsTooltip [closeable]="true">
Per SMS benachrichtigt
<br />
<ng-container *ngFor="let notification of notificationsSent?.NOTIFICATION_SMS">
@for (notification of notificationsSent?.NOTIFICATION_SMS; track notification) {
{{ notification | date: 'dd.MM.yyyy | HH:mm' }} Uhr
<br />
</ng-container>
}
</ui-tooltip>
</ng-container>
</ng-container>
}
}
</div>
<div class="page-customer-order-details-item__item-container">
<div class="page-customer-order-details-item__thumbnail">
@@ -42,169 +42,190 @@
#elementDistance="uiElementDistance"
[style.max-width.px]="elementDistance.distanceChange | async"
class="flex flex-col"
>
>
<div class="font-normal mb-[0.375rem]">{{ orderItem.product?.contributors }}</div>
<div>{{ orderItem.product?.name }}</div>
</h3>
<div class="history-wrapper flex flex-col items-end justify-center">
<button class="cta-history text-p1" (click)="historyClick.emit(orderItem)">Historie</button>
<input
*ngIf="selectable$ | async"
[ngModel]="selected$ | async"
(ngModelChange)="setSelected($event)"
class="isa-select-bullet mt-4"
type="checkbox"
/>
@if (selectable$ | async) {
<input
[ngModel]="selected$ | async"
(ngModelChange)="setSelected($event)"
class="isa-select-bullet mt-4"
type="checkbox"
/>
}
</div>
</div>
<div class="detail">
<div class="label">Menge</div>
<div class="value">
<ng-container *ngIf="!(canChangeQuantity$ | async)">{{ orderItem?.quantity }}x</ng-container>
<ui-quantity-dropdown
*ngIf="canChangeQuantity$ | async"
[showTrash]="false"
[range]="orderItem?.quantity"
[(ngModel)]="quantity"
[showSpinner]="false"
></ui-quantity-dropdown>
@if (!(canChangeQuantity$ | async)) {
{{ orderItem?.quantity }}x
}
@if (canChangeQuantity$ | async) {
<ui-quantity-dropdown
[showTrash]="false"
[range]="orderItem?.quantity"
[(ngModel)]="quantity"
[showSpinner]="false"
></ui-quantity-dropdown>
}
<span class="overall-quantity">(von {{ orderItem?.overallQuantity }})</span>
</div>
</div>
<div class="detail" *ngIf="!!orderItem.product?.formatDetail">
<div class="label">Format</div>
<div class="value">
<img
*ngIf="orderItem?.product?.format && orderItem?.product?.format !== 'UNKNOWN'"
class="format-icon"
[src]="'/assets/images/Icon_' + orderItem.product?.format + '.svg'"
alt="format icon"
/>
<span>{{ orderItem.product?.formatDetail }}</span>
@if (!!orderItem.product?.formatDetail) {
<div class="detail">
<div class="label">Format</div>
<div class="value">
@if (orderItem?.product?.format && orderItem?.product?.format !== 'UNKNOWN') {
<img
class="format-icon"
[src]="'/assets/images/Icon_' + orderItem.product?.format + '.svg'"
alt="format icon"
/>
}
<span>{{ orderItem.product?.formatDetail }}</span>
</div>
</div>
</div>
<div class="detail" *ngIf="!!orderItem.product?.ean">
<div class="label">ISBN/EAN</div>
<div class="value">{{ orderItem.product?.ean }}</div>
</div>
<div class="detail" *ngIf="orderItem.price !== undefined">
<div class="label">Preis</div>
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.retailPrice?.vat?.inPercent">
<div class="label">MwSt</div>
<div class="value">{{ orderItem.retailPrice?.vat?.inPercent }}%</div>
</div>
}
@if (!!orderItem.product?.ean) {
<div class="detail">
<div class="label">ISBN/EAN</div>
<div class="value">{{ orderItem.product?.ean }}</div>
</div>
}
@if (orderItem.price !== undefined) {
<div class="detail">
<div class="label">Preis</div>
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
</div>
}
@if (!!orderItem.retailPrice?.vat?.inPercent) {
<div class="detail">
<div class="label">MwSt</div>
<div class="value">{{ orderItem.retailPrice?.vat?.inPercent }}%</div>
</div>
}
<hr class="border-[#EDEFF0] border-t-2 my-4" />
<div class="detail" *ngIf="orderItem.supplier">
<div class="label">Lieferant</div>
<div class="value">{{ orderItem.supplier }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.ssc || !!orderItem.sscText">
<div class="label">Meldenummer</div>
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.targetBranch">
<div class="label">Zielfiliale</div>
<div class="value">{{ orderItem.targetBranch }}</div>
</div>
@if (orderItem.supplier) {
<div class="detail">
<div class="label">Lieferant</div>
<div class="value">{{ orderItem.supplier }}</div>
</div>
}
@if (!!orderItem.ssc || !!orderItem.sscText) {
<div class="detail">
<div class="label">Meldenummer</div>
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
</div>
}
@if (!!orderItem.targetBranch) {
<div class="detail">
<div class="label">Zielfiliale</div>
<div class="value">{{ orderItem.targetBranch }}</div>
</div>
}
<div class="detail">
<div class="label">
<ng-container
*ngIf="
orderItemFeature(orderItem) === 'Versand' ||
orderItemFeature(orderItem) === 'B2B-Versand' ||
orderItemFeature(orderItem) === 'DIG-Versand'
"
>
@if (
orderItemFeature(orderItem) === 'Versand' ||
orderItemFeature(orderItem) === 'B2B-Versand' ||
orderItemFeature(orderItem) === 'DIG-Versand'
) {
{{ orderItem?.estimatedDelivery ? 'Lieferung zwischen' : 'Lieferung ab' }}
</ng-container>
<ng-container *ngIf="orderItemFeature(orderItem) === 'Abholung' || orderItemFeature(orderItem) === 'Rücklage'">
}
@if (orderItemFeature(orderItem) === 'Abholung' || orderItemFeature(orderItem) === 'Rücklage') {
Abholung ab
</ng-container>
}
</div>
<ng-container *ngIf="!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate">
@if (!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate) {
<div class="value bg-[#D8DFE5] rounded w-max px-2">
<ng-container *ngIf="!!orderItem?.estimatedDelivery; else estimatedShippingDate">
@if (!!orderItem?.estimatedDelivery) {
{{ orderItem?.estimatedDelivery?.start | date: 'dd.MM.yy' }} und
{{ orderItem?.estimatedDelivery?.stop | date: 'dd.MM.yy' }}
</ng-container>
} @else {
@if (!!orderItem?.estimatedShippingDate) {
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
}
}
</div>
</ng-container>
<ng-template #estimatedShippingDate>
<ng-container *ngIf="!!orderItem?.estimatedShippingDate">
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
</ng-container>
</ng-template>
}
</div>
<div class="page-customer-order-details-item__tracking-details" *ngIf="getOrderItemTrackingData(orderItem); let trackingData">
<div class="label">{{ trackingData.length > 1 ? 'Sendungsnummern' : 'Sendungsnummer' }}</div>
<ng-container *ngFor="let tracking of trackingData">
<ng-container *ngIf="tracking.trackingProvider === 'DHL' && !isNative; else noTrackingLink">
<a class="value text-[#0556B4]" [href]="getTrackingNumberLink(tracking.trackingNumber)" target="_blank">
{{ tracking.trackingProvider }}: {{ tracking.trackingNumber }}
</a>
</ng-container>
<ng-template #noTrackingLink>
<p class="value">{{ tracking.trackingProvider }}: {{ tracking.trackingNumber }}</p>
</ng-template>
</ng-container>
</div>
@if (getOrderItemTrackingData(orderItem); as trackingData) {
<div class="page-customer-order-details-item__tracking-details">
<div class="label">{{ trackingData.length > 1 ? 'Sendungsnummern' : 'Sendungsnummer' }}</div>
@for (tracking of trackingData; track tracking) {
@if (tracking.trackingProvider === 'DHL' && !isNative) {
<a class="value text-[#0556B4]" [href]="getTrackingNumberLink(tracking.trackingNumber)" target="_blank">
{{ tracking.trackingProvider }}: {{ tracking.trackingNumber }}
</a>
} @else {
<p class="value">{{ tracking.trackingProvider }}: {{ tracking.trackingNumber }}</p>
}
}
</div>
}
<hr class="border-[#EDEFF0] border-t-2 my-4" />
<div class="detail" *ngIf="!!orderItem?.compartmentCode">
<div class="label">Abholfachnr.</div>
<div class="value">{{ orderItem?.compartmentCode }}</div>
</div>
@if (!!orderItem?.compartmentCode) {
<div class="detail">
<div class="label">Abholfachnr.</div>
<div class="value">{{ orderItem?.compartmentCode }}</div>
</div>
}
<div class="detail">
<div class="label">Vormerker</div>
<div class="value">{{ orderItem.isPrebooked ? 'Ja' : 'Nein' }}</div>
</div>
<hr class="border-[#EDEFF0] border-t-2 my-4" />
<div class="detail" *ngIf="!!orderItem.paymentProcessing">
<div class="label">Zahlungsweg</div>
<div class="value">{{ orderItem.paymentProcessing || '-' }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.paymentType">
<div class="label">Zahlungsart</div>
<div class="value">{{ orderItem.paymentType | paymentType }}</div>
</div>
<h4 class="receipt-header" *ngIf="receiptCount$ | async; let count">
{{ count > 1 ? 'Belege' : 'Beleg' }}
</h4>
<ng-container *ngFor="let receipt of receipts$ | async">
<div class="detail" *ngIf="!!receipt?.receiptNumber">
<div class="label">Belegnummer</div>
<div class="value">{{ receipt?.receiptNumber }}</div>
@if (!!orderItem.paymentProcessing) {
<div class="detail">
<div class="label">Zahlungsweg</div>
<div class="value">{{ orderItem.paymentProcessing || '-' }}</div>
</div>
<div class="detail" *ngIf="!!receipt?.printedDate">
<div class="label">Erstellt am</div>
<div class="value">{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
}
@if (!!orderItem.paymentType) {
<div class="detail">
<div class="label">Zahlungsart</div>
<div class="value">{{ orderItem.paymentType | paymentType }}</div>
</div>
<div class="detail" *ngIf="!!receipt?.receiptText">
<div class="label">Rechnungstext</div>
<div class="value">{{ receipt?.receiptText || '-' }}</div>
</div>
<div class="detail" *ngIf="!!receipt?.receiptType">
<div class="label">Belegart</div>
<div class="value">
{{ receipt?.receiptType === 1 ? 'Lieferschein' : receipt?.receiptType === 64 ? 'Zahlungsbeleg' : '-' }}
}
@if (receiptCount$ | async; as count) {
<h4 class="receipt-header">
{{ count > 1 ? 'Belege' : 'Beleg' }}
</h4>
}
@for (receipt of receipts$ | async; track receipt) {
@if (!!receipt?.receiptNumber) {
<div class="detail">
<div class="label">Belegnummer</div>
<div class="value">{{ receipt?.receiptNumber }}</div>
</div>
</div>
</ng-container>
}
@if (!!receipt?.printedDate) {
<div class="detail">
<div class="label">Erstellt am</div>
<div class="value">{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</div>
}
@if (!!receipt?.receiptText) {
<div class="detail">
<div class="label">Rechnungstext</div>
<div class="value">{{ receipt?.receiptText || '-' }}</div>
</div>
}
@if (!!receipt?.receiptType) {
<div class="detail">
<div class="label">Belegart</div>
<div class="value">
{{ receipt?.receiptType === 1 ? 'Lieferschein' : receipt?.receiptType === 64 ? 'Zahlungsbeleg' : '-' }}
</div>
</div>
}
}
<div class="page-customer-order-details-item__comment flex flex-col items-start mt-[1.625rem]">
<div class="label mb-[0.375rem]">Anmerkung</div>
<div class="flex flex-row w-full">
<textarea
matInput
@@ -222,27 +243,28 @@
[formControl]="specialCommentControl"
[class.inactive]="!specialCommentControl.dirty"
></textarea>
<div class="comment-actions">
<button
type="reset"
class="clear"
*ngIf="!!specialCommentControl.value?.length"
(click)="specialCommentControl.setValue(''); saveSpecialComment(); triggerResize()"
>
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
<button
class="cta-save"
type="submit"
*ngIf="specialCommentControl?.enabled && specialCommentControl.dirty"
(click)="saveSpecialComment()"
>
Speichern
</button>
@if (!!specialCommentControl.value?.length) {
<button
type="reset"
class="clear"
(click)="specialCommentControl.setValue(''); saveSpecialComment(); triggerResize()"
>
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
}
@if (specialCommentControl?.enabled && specialCommentControl.dirty) {
<button
class="cta-save"
type="submit"
(click)="saveSpecialComment()"
>
Speichern
</button>
}
</div>
</div>
</div>
</div>
</div>
</ng-container>
}

View File

@@ -1,19 +1,20 @@
<div class="page-customer-order-details-tags__wrapper">
<button
class="page-customer-order-details-tags__tag"
type="button"
[class.selected]="tag === (selected$ | async) && !inputFocus.focused"
*ngFor="let tag of defaultTags"
(click)="setCompartmentInfo(tag)"
>
{{ tag }}
</button>
@for (tag of defaultTags; track tag) {
<button
class="page-customer-order-details-tags__tag"
type="button"
[class.selected]="tag === (selected$ | async) && !inputFocus.focused"
(click)="setCompartmentInfo(tag)"
>
{{ tag }}
</button>
}
<button
(click)="inputFocus.focus()"
type="button"
class="page-customer-order-details-tags__tag"
[class.selected]="(inputValue$ | async) === (selected$ | async) && (inputValue$ | async)"
>
>
<input
#inputFocus="uiFocus"
uiFocus
@@ -23,6 +24,6 @@
placeholder="..."
[size]="controlSize$ | async"
maxlength="15"
/>
/>
</button>
</div>

View File

@@ -1,4 +1,4 @@
import { NgFor, AsyncPipe } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, forwardRef } from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { UiCommonModule } from '@ui/common';
@@ -14,7 +14,7 @@ import { PickUpShelfDetailsTagsComponent } from '../../../pickup-shelf/shared/pi
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-pickup-shelf-details-tags' },
standalone: true,
imports: [NgFor, UiCommonModule, FormsModule, AsyncPipe, MatomoModule],
imports: [UiCommonModule, FormsModule, AsyncPipe, MatomoModule],
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => PickUpShelfDetailsTagsComponent), multi: true },
],

View File

@@ -4,39 +4,44 @@
(handleAction)="handleAction($event)"
[order]="order$ | async"
></page-customer-order-details-header>
<page-customer-order-details-item
class="mb-px-2"
*ngFor="let item of items$ | async"
[orderItem]="item"
[order]="order$ | async"
[selected]="true"
(historyClick)="navigateToHistoryPage($event)"
(specialCommentChanged)="onSpecialCommentChange()"
></page-customer-order-details-item>
<page-customer-order-details-tags *ngIf="showTagsComponent$ | async"></page-customer-order-details-tags>
@for (item of items$ | async; track item) {
<page-customer-order-details-item
class="mb-px-2"
[orderItem]="item"
[order]="order$ | async"
[selected]="true"
(historyClick)="navigateToHistoryPage($event)"
(specialCommentChanged)="onSpecialCommentChange()"
></page-customer-order-details-item>
}
@if (showTagsComponent$ | async) {
<page-customer-order-details-tags></page-customer-order-details-tags>
}
</div>
<div class="page-customer-order-details__action-wrapper">
<button
[disabled]="addToPreviousCompartmentActionDisabled$ | async"
*ngIf="addToPreviousCompartmentAction$ | async; let action"
class="cta-action shadow-action"
[class.cta-action-primary]="action.selected"
[class.cta-action-secondary]="!action.selected"
(click)="handleAction(action, { compartmentCode: latestCompartmentCode, compartmentInfo: latestCompartmentInfo })"
>
<ui-spinner [show]="(changeActionLoader$ | async) === action.command">
{{ latestCompartmentCode$ | async | addToPreviousCompartmentCodeLabelPipe }} zubuchen
</ui-spinner>
</button>
@if (addToPreviousCompartmentAction$ | async; as action) {
<button
[disabled]="addToPreviousCompartmentActionDisabled$ | async"
class="cta-action shadow-action"
[class.cta-action-primary]="action.selected"
[class.cta-action-secondary]="!action.selected"
(click)="handleAction(action, { compartmentCode: latestCompartmentCode, compartmentInfo: latestCompartmentInfo })"
>
<ui-spinner [show]="(changeActionLoader$ | async) === action.command">
{{ latestCompartmentCode$ | async | addToPreviousCompartmentCodeLabelPipe }} zubuchen
</ui-spinner>
</button>
}
<button
[disabled]="actionsDisabled$ | async"
class="cta-action shadow-action"
[class.cta-action-primary]="action.selected"
[class.cta-action-secondary]="!action.selected"
*ngFor="let action of mainActions$ | async"
(click)="handleAction(action)"
>
<ui-spinner [show]="(changeActionLoader$ | async) === action.command">{{ action.label }}</ui-spinner>
</button>
@for (action of mainActions$ | async; track action) {
<button
[disabled]="actionsDisabled$ | async"
class="cta-action shadow-action"
[class.cta-action-primary]="action.selected"
[class.cta-action-secondary]="!action.selected"
(click)="handleAction(action)"
>
<ui-spinner [show]="(changeActionLoader$ | async) === action.command">{{ action.label }}</ui-spinner>
</button>
}
</div>

View File

@@ -1,3 +1,5 @@
<div *ngIf="items$ | async; let items">
<shared-goods-in-out-order-edit (navigation)="navigateToDetailsPage($event)" [items]="items"></shared-goods-in-out-order-edit>
</div>
@if (items$ | async; as items) {
<div>
<shared-goods-in-out-order-edit (navigation)="navigateToDetailsPage($event)" [items]="items"></shared-goods-in-out-order-edit>
</div>
}

View File

@@ -1,19 +1,19 @@
<div class="hidden desktop-large:block" [class.show-filter]="showFilter">
<ng-container *ngIf="filter$ | async; let filter">
@if (filter$ | async; as filter) {
<div class="customer-orders-search-filter-content">
<div class="w-full flex flex-row justify-end items-center">
<button (click)="clearFilter(filter)" class="text-[#0556B4] p-4">Alle Filter entfernen</button>
<a
*ngIf="showFilterClose$ | async"
class="text-black p-4 outline-none border-none bg-transparent"
[routerLink]="closeFilterRoute"
(click)="showFilter = false"
queryParamsHandling="preserve"
>
<shared-icon icon="close" [size]="25"></shared-icon>
</a>
@if (showFilterClose$ | async) {
<a
class="text-black p-4 outline-none border-none bg-transparent"
[routerLink]="closeFilterRoute"
(click)="showFilter = false"
queryParamsHandling="preserve"
>
<shared-icon icon="close" [size]="25"></shared-icon>
</a>
}
</div>
<div class="customer-orders-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
@@ -22,20 +22,18 @@
(search)="applyFilter(filter)"
[hint]="message$ | async"
[scanner]="true"
>
>
<page-order-branch-id-input *sharedFilterCustomInput="'order_branch_id'; let input" [input]="input"></page-order-branch-id-input>
</shared-filter>
</div>
<div class="cta-wrapper">
<button class="cta-reset-filter" (click)="resetFilter()" [disabled]="loading$ | async">Filter zurücksetzen</button>
<button class="cta-apply-filter" (click)="applyFilter(filter)" [disabled]="(loading$ | async) || !hasSelectedOptions(filter)">
<ui-spinner [show]="loading$ | async">Filter anwenden</ui-spinner>
</button>
</div>
</div>
</ng-container>
}
</div>
<div class="desktop-large:hidden" [class.hidden]="showFilter">
<page-customer-order-search-main (showFilter)="showFilter = true"></page-customer-order-search-main>

View File

@@ -11,53 +11,57 @@
<br />
oder scannen Sie die Kundenkarte.
</p>
<ng-container *ngIf="filter$ | async; let filter">
<shared-filter-filter-group-main
class="mb-8 w-full"
*ngIf="!(isDesktop$ | async)"
[inputGroup]="filter?.filter | group: 'main'"
></shared-filter-filter-group-main>
@if (filter$ | async; as filter) {
@if (!(isDesktop$ | async)) {
<shared-filter-filter-group-main
class="mb-8 w-full"
[inputGroup]="filter?.filter | group: 'main'"
></shared-filter-filter-group-main>
}
<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-large:mx-auto"
*ngIf="filter?.input | group: 'main'; let inputGroup"
[inputGroup]="inputGroup"
[loading]="loading$ | async"
(search)="search(filter)"
[hint]="message$ | async"
[scanner]="true"
></shared-filter-input-group-main>
<button
*ngIf="!(isDesktop$ | async)"
(click)="showFilter.emit()"
class="page-search-main__filter w-[6.75rem] h-14 rounded font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
type="button"
>
<shared-icon class="mr-2" icon="filter-variant"></shared-icon>
Filter
</button>
@if (filter?.input | group: 'main'; as inputGroup) {
<shared-filter-input-group-main
class="block w-full mr-3 desktop-large:mx-auto"
[inputGroup]="inputGroup"
[loading]="loading$ | async"
(search)="search(filter)"
[hint]="message$ | async"
[scanner]="true"
></shared-filter-input-group-main>
}
@if (!(isDesktop$ | async)) {
<button
(click)="showFilter.emit()"
class="page-search-main__filter w-[6.75rem] h-14 rounded font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
type="button"
>
<shared-icon class="mr-2" icon="filter-variant"></shared-icon>
Filter
</button>
}
</div>
<div
class="flex flex-col items-start ml-12 desktop-large:ml-8 py-6 bg-white overflow-hidden h-[calc(100%-21rem)] desktop-large:h-[calc(100%-15rem)]"
>
>
<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 query of history$ | async">
<button
class="flex flex-row items-center outline-none border-none bg-white text-black text-p2 m-0 p-0"
(click)="setQueryHistory(filter, query)"
>
<shared-icon
class="flex w-8 h-8 justify-center items-center mr-3 rounded-full text-black bg-[#edeff0]"
icon="magnify"
[size]="20"
></shared-icon>
<p class="m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-[25rem]">{{ query }}</p>
</button>
</li>
@for (query of history$ | async; track query) {
<li class="list-none pb-3">
<button
class="flex flex-row items-center outline-none border-none bg-white text-black text-p2 m-0 p-0"
(click)="setQueryHistory(filter, query)"
>
<shared-icon
class="flex w-8 h-8 justify-center items-center mr-3 rounded-full text-black bg-[#edeff0]"
icon="magnify"
[size]="20"
></shared-icon>
<p class="m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-[25rem]">{{ query }}</p>
</button>
</li>
}
</ul>
</div>
</ng-container>
}
</div>

View File

@@ -5,19 +5,20 @@
[routerLinkActive]="!isTablet && !primaryOutletActive ? 'active' : ''"
[queryParams]="queryParams"
(click)="isDesktopLarge ? scrollIntoView() : ''"
>
>
<div
class="page-customer-order-item__item-grid-container"
[class.page-customer-order-item__item-grid-container-main]="primaryOutletActive"
>
>
<div class="page-customer-order-item__item-thumbnail text-center mr-4 w-[3.125rem] h-[4.9375rem]">
<img
class="page-customer-order-item__item-image w-[3.125rem] max-h-[4.9375rem]"
loading="lazy"
*ngIf="item?.product?.ean | productImage; let productImage"
[src]="productImage"
[alt]="item?.product?.name"
/>
@if (item?.product?.ean | productImage; as productImage) {
<img
class="page-customer-order-item__item-image w-[3.125rem] max-h-[4.9375rem]"
loading="lazy"
[src]="productImage"
[alt]="item?.product?.name"
/>
}
</div>
<div
@@ -27,7 +28,7 @@
[class.text-p2]="item?.product?.name?.length >= 50 && isTablet"
[class.text-p3]="item?.product?.name?.length >= 60 || !isTablet"
[class.text-p4]="item?.product?.name?.length >= 100"
>
>
{{ item?.product?.name }}
</div>
@@ -35,88 +36,111 @@
{{ item?.specialComment }}
</div>
<div *ngIf="primaryOutletActive" class="page-customer-order-item__item-format desktop-small:text-p2">
<div *ngIf="item?.product?.format && item?.product?.formatDetail" class="font-bold flex flex-row">
<img
class="mr-3"
*ngIf="item?.product?.format !== '--'"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail | substr: 30 }}
@if (primaryOutletActive) {
<div class="page-customer-order-item__item-format desktop-small:text-p2">
@if (item?.product?.format && item?.product?.formatDetail) {
<div class="font-bold flex flex-row">
@if (item?.product?.format !== '--') {
<img
class="mr-3"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
}
{{ item?.product?.formatDetail | substr: 30 }}
</div>
}
</div>
</div>
}
<div *ngIf="primaryOutletActive" class="page-customer-order-item__item-ean desktop-small:text-p2">
{{ item?.product?.ean }}
</div>
@if (primaryOutletActive) {
<div class="page-customer-order-item__item-ean desktop-small:text-p2">
{{ item?.product?.ean }}
</div>
}
<div *ngIf="primaryOutletActive" class="page-customer-order-item__item-price desktop-small:text-p2 font-bold">
{{ item.price | currency: 'EUR' : 'code' }}
</div>
@if (primaryOutletActive) {
<div class="page-customer-order-item__item-price desktop-small:text-p2 font-bold">
{{ item.price | currency: 'EUR' : 'code' }}
</div>
}
<div *ngIf="primaryOutletActive" class="page-customer-order-item__item-changed desktop-small:text-p2">
<ng-container [ngSwitch]="showChangeDate$ | async">
<div class="flex flex-row" *ngSwitchCase="true">
<div class="min-w-[7.5rem]">Geändert</div>
<div class="font-bold">{{ item?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</div>
<div class="flex flex-row" *ngSwitchCase="false">
<div class="min-w-[7.5rem]">Bestelldatum</div>
<div class="font-bold">{{ item?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</div>
</ng-container>
</div>
@if (primaryOutletActive) {
<div class="page-customer-order-item__item-changed desktop-small:text-p2">
@switch (showChangeDate$ | async) {
@case (true) {
<div class="flex flex-row">
<div class="min-w-[7.5rem]">Geändert</div>
<div class="font-bold">{{ item?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</div>
}
@case (false) {
<div class="flex flex-row">
<div class="min-w-[7.5rem]">Bestelldatum</div>
<div class="font-bold">{{ item?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</div>
}
}
</div>
}
<div *ngIf="primaryOutletActive" class="page-customer-order-item__item-quantity flex flex-row desktop-small:text-p2">
<div class="min-w-[7.5rem]">Menge</div>
<div class="font-bold">{{ item.quantity }} x</div>
</div>
@if (primaryOutletActive) {
<div class="page-customer-order-item__item-quantity flex flex-row desktop-small:text-p2">
<div class="min-w-[7.5rem]">Menge</div>
<div class="font-bold">{{ item.quantity }} x</div>
</div>
}
<div *ngIf="primaryOutletActive" class="page-customer-order-item__item-target-branch flex flex-row desktop-small:text-p2">
<ng-container *ngIf="item.orderType === 1; else showDelivery">
<div class="min-w-[7.5rem]">Zielfiliale</div>
<div class="font-bold">{{ item.targetBranch }}</div>
</ng-container>
<ng-template #showDelivery>
<div class="min-w-[7.5rem]">Versanddatum</div>
<div class="font-bold">{{ item?.estimatedShippingDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</ng-template>
</div>
@if (primaryOutletActive) {
<div class="page-customer-order-item__item-target-branch flex flex-row desktop-small:text-p2">
@if (item.orderType === 1) {
<div class="min-w-[7.5rem]">Zielfiliale</div>
<div class="font-bold">{{ item.targetBranch }}</div>
} @else {
<div class="min-w-[7.5rem]">Versanddatum</div>
<div class="font-bold">{{ item?.estimatedShippingDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
}
</div>
}
<hr
*ngIf="!primaryOutletActive"
class="page-customer-order-item__separator border-[#EDEFF0] border-solid border-[1px] -mx-[0.875rem]"
/>
@if (!primaryOutletActive) {
<hr
class="page-customer-order-item__separator border-[#EDEFF0] border-solid border-[1px] -mx-[0.875rem]"
/>
}
<div
class="page-customer-order-item__item-order-number desktop-small:text-xl justify-self-end font-bold"
[class.page-customer-order-item__item-order-number-main]="!primaryOutletActive"
>
<ng-container *ngIf="item?.compartmentCode; else orderNumber">
>
@if (item?.compartmentCode) {
{{ item?.compartmentCode }}{{ item?.compartmentInfo && '_' + item?.compartmentInfo }}
</ng-container>
<ng-template #orderNumber>{{ item?.orderNumber }}</ng-template>
} @else {
{{ item?.orderNumber }}
}
</div>
<div
class="page-customer-order-item__item-processing-paid-status flex flex-col font-bold desktop-small:text-p2 justify-self-end self-center"
>
>
<div class="page-customer-order-item__item-processing-status flex flex-row mb-[0.375rem]">
<shared-icon
class="flex items-center justify-center mr-1"
[size]="16"
*ngIf="item.processingStatus | processingStatus: 'icon'; let icon"
[icon]="icon"
></shared-icon>
@if (item.processingStatus | processingStatus: 'icon'; as icon) {
<shared-icon
class="flex items-center justify-center mr-1"
[size]="16"
[icon]="icon"
></shared-icon>
}
{{ item.processingStatus | processingStatus }}
</div>
<div class="page-customer-order-item__item-paid flex flex-row self-end">
<div class="font-bold w-fit desktop-small:text-p2 px-3 py-[0.125rem] rounded text-white bg-[#26830C]" *ngIf="item.features?.paid">
{{ item.features?.paid }}
</div>
@if (item.features?.paid) {
<div class="font-bold w-fit desktop-small:text-p2 px-3 py-[0.125rem] rounded text-white bg-[#26830C]">
{{ item.features?.paid }}
</div>
}
</div>
</div>
</div>

View File

@@ -2,102 +2,111 @@
class="page-customer-order-search-results__header bg-background-liste flex items-end justify-between"
[class.pb-4]="!(primaryOutletActive$ | async)"
[class.flex-col]="!(primaryOutletActive$ | async)"
>
>
<div class="flex flex-row w-full desktop-small:w-min" [class.desktop-large:w-full]="!(primaryOutletActive$ | async)">
<shared-filter-input-group-main
*ngIf="filter$ | async; let filter"
class="block mr-3 w-full desktop-small:w-[23.5rem]"
[class.desktop-large:w-full]="!(primaryOutletActive$ | async)"
[hint]="message$ | async"
[loading]="loading$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search({ filter, clear: true })"
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
@if (filter$ | async; as filter) {
<shared-filter-input-group-main
class="block mr-3 w-full desktop-small:w-[23.5rem]"
[class.desktop-large:w-full]="!(primaryOutletActive$ | async)"
[hint]="message$ | async"
[loading]="loading$ | async"
[inputGroup]="filter?.input | group: 'main'"
(search)="search({ filter, clear: true })"
[showDescription]="false"
[scanner]="true"
></shared-filter-input-group-main>
}
<a
class="page-customer-orders-results__filter w-[6.75rem] h-14 rounded font-bold px-5 mb-4 text-lg bg-[#AEB7C1] flex flex-row flex-nowrap items-center justify-center"
[class.active]="hasFilter$ | async"
[routerLink]="filterRoute"
queryParamsHandling="preserve"
>
>
<shared-icon class="mr-2" icon="filter-variant"></shared-icon>
Filter
</a>
</div>
<div
*ngIf="hits$ | async; let hits"
class="page-customer-order-search-results__items-count inline-flex flex-row items-center pr-5 text-p3"
[class.mb-4]="primaryOutletActive$ | async"
>
{{ hits ?? 0 }}
Titel
</div>
@if (hits$ | async; as hits) {
<div
class="page-customer-order-search-results__items-count inline-flex flex-row items-center pr-5 text-p3"
[class.mb-4]="primaryOutletActive$ | async"
>
{{ hits ?? 0 }}
Titel
</div>
}
</div>
<ui-scroll-container
*ngIf="!(listEmpty$ | async); else emptyMessage"
class="page-customer-order-results__scroll-container m-0 p-0"
[showScrollbar]="false"
[showScrollArrow]="false"
(reachEnd)="loadMore()"
[deltaEnd]="150"
[itemLength]="itemLength$ | async"
[containerHeight]="25"
[showSpacer]="(primaryOutletActive$ | async) || (isTablet$ | async)"
>
<ng-container *ngIf="processId$ | async; let processId">
<div class="page-customer-order-results__items-list w-full" *ngFor="let bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn">
<ng-container *ngIf="bueryNumberGroup.items[0]; let firstItem">
<div
class="page-customer-order-search__item-header-group w-full grid grid-flow-col gap-x-4 items-center justify-between bg-white text-xl rounded-t px-4 py-[0.875rem] font-bold mb-px-2"
>
<h3 class="m-0 break-words" [class.w-72]="!(primaryOutletActive$ | async)">
{{ firstItem?.organisation }}
<ng-container *ngIf="!!firstItem?.organisation && (!!firstItem?.firstName || !!firstItem?.lastName)">-</ng-container>
{{ firstItem?.lastName }}
{{ firstItem?.firstName }}
</h3>
<h3 class="m-0 break-words text-right" [class.w-40]="!(primaryOutletActive$ | async)">{{ firstItem?.buyerNumber }}</h3>
@if (!(listEmpty$ | async)) {
<ui-scroll-container
class="page-customer-order-results__scroll-container m-0 p-0"
[showScrollbar]="false"
[showScrollArrow]="false"
(reachEnd)="loadMore()"
[deltaEnd]="150"
[itemLength]="itemLength$ | async"
[containerHeight]="25"
[showSpacer]="(primaryOutletActive$ | async) || (isTablet$ | async)"
>
@if (processId$ | async; as processId) {
@for (bueryNumberGroup of items$ | async | groupBy: byBuyerNumberFn; track bueryNumberGroup) {
<div class="page-customer-order-results__items-list w-full">
@if (bueryNumberGroup.items[0]; as firstItem) {
<div
class="page-customer-order-search__item-header-group w-full grid grid-flow-col gap-x-4 items-center justify-between bg-white text-xl rounded-t px-4 py-[0.875rem] font-bold mb-px-2"
>
<h3 class="m-0 break-words" [class.w-72]="!(primaryOutletActive$ | async)">
{{ firstItem?.organisation }}
@if (!!firstItem?.organisation && (!!firstItem?.firstName || !!firstItem?.lastName)) {
-
}
{{ firstItem?.lastName }}
{{ firstItem?.firstName }}
</h3>
<h3 class="m-0 break-words text-right" [class.w-40]="!(primaryOutletActive$ | async)">{{ firstItem?.buyerNumber }}</h3>
</div>
}
@for (orderNumberGroup of bueryNumberGroup.items | groupBy: byOrderNumberFn; track orderNumberGroup) {
@for (processingStatusGroup of orderNumberGroup.items | groupBy: byProcessingStatusFn; track processingStatusGroup) {
@for (compartmentCodeGroup of processingStatusGroup.items | groupBy: byCompartmentCodeFn; track compartmentCodeGroup) {
@for (item of compartmentCodeGroup.items; track trackByFn($index, item); let firstItem = $first) {
<page-customer-order-item
class="page-customer-orders-results__result-item mb-[0.625rem]"
[class.page-customer-orders-results__result-item-main]="primaryOutletActive$ | async"
[item]="item"
[primaryOutletActive]="primaryOutletActive$ | async"
></page-customer-order-item>
}
}
}
}
</div>
</ng-container>
<ng-container *ngFor="let orderNumberGroup of bueryNumberGroup.items | groupBy: byOrderNumberFn">
<ng-container *ngFor="let processingStatusGroup of orderNumberGroup.items | groupBy: byProcessingStatusFn">
<ng-container *ngFor="let compartmentCodeGroup of processingStatusGroup.items | groupBy: byCompartmentCodeFn">
<page-customer-order-item
*ngFor="let item of compartmentCodeGroup.items; let firstItem = first; trackBy: trackByFn"
class="page-customer-orders-results__result-item mb-[0.625rem]"
[class.page-customer-orders-results__result-item-main]="primaryOutletActive$ | async"
[item]="item"
[primaryOutletActive]="primaryOutletActive$ | async"
></page-customer-order-item>
</ng-container>
</ng-container>
</ng-container>
</div>
</ng-container>
</ui-scroll-container>
<ng-template #emptyMessage>
}
}
</ui-scroll-container>
} @else {
<div class="empty-message">
Es sind im Moment keine Bestellposten vorhanden,
<br />
die bearbeitet werden können.
</div>
</ng-template>
}
<div class="actions z-fixed" *ngIf="actions$ | async; let actions">
<button
[disabled]="(loadingFetchedActionButton$ | async) || (loading$ | async)"
class="cta-action"
*ngFor="let action of actions"
[class.cta-action-primary]="action.selected"
[class.cta-action-secondary]="!action.selected"
(click)="handleAction(action)"
>
<ui-spinner [show]="(loadingFetchedActionButton$ | async) || (loading$ | async)">{{ action.label }}</ui-spinner>
</button>
</div>
@if (actions$ | async; as actions) {
<div class="actions z-fixed">
@for (action of actions; track action) {
<button
[disabled]="(loadingFetchedActionButton$ | async) || (loading$ | async)"
class="cta-action"
[class.cta-action-primary]="action.selected"
[class.cta-action-secondary]="!action.selected"
(click)="handleAction(action)"
>
<ui-spinner [show]="(loadingFetchedActionButton$ | async) || (loading$ | async)">{{ action.label }}</ui-spinner>
</button>
}
</div>
}

View File

@@ -4,7 +4,7 @@
[cdkMenuTriggerFor]="navMenu"
#menuTrigger="cdkMenuTriggerFor"
[class.open]="menuTrigger.isOpen()"
>
>
<shared-icon icon="apps" [size]="24"></shared-icon>
<shared-icon [icon]="menuTrigger.isOpen() ? 'arrow-drop-up' : 'arrow-drop-down'" [size]="24"></shared-icon>
</button>
@@ -12,42 +12,46 @@
<ng-template #navMenu>
<div class="pt-1">
<shared-menu>
<a
sharedMenuItem
*ngIf="customerDetailsRoute$ | async; let customerDetailsRoute"
[routerLink]="customerDetailsRoute.path"
[queryParams]="customerDetailsRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Kundendetails
</a>
<a
sharedMenuItem
*ngIf="ordersRoute$ | async; let ordersRoute"
[routerLink]="ordersRoute.path"
[queryParams]="ordersRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Bestellungen
</a>
<a
sharedMenuItem
*ngIf="kundenkarteRoute$ | async; let kundenkarteRoute"
[routerLink]="kundenkarteRoute.path"
[queryParams]="kundenkarteRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Kundenkarte
</a>
<a
sharedMenuItem
*ngIf="historyRoute$ | async; let historyRoute"
[routerLink]="historyRoute.path"
[queryParams]="historyRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Historie
</a>
@if (customerDetailsRoute$ | async; as customerDetailsRoute) {
<a
sharedMenuItem
[routerLink]="customerDetailsRoute.path"
[queryParams]="customerDetailsRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Kundendetails
</a>
}
@if (ordersRoute$ | async; as ordersRoute) {
<a
sharedMenuItem
[routerLink]="ordersRoute.path"
[queryParams]="ordersRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Bestellungen
</a>
}
@if (kundenkarteRoute$ | async; as kundenkarteRoute) {
<a
sharedMenuItem
[routerLink]="kundenkarteRoute.path"
[queryParams]="kundenkarteRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Kundenkarte
</a>
}
@if (historyRoute$ | async; as historyRoute) {
<a
sharedMenuItem
[routerLink]="historyRoute.path"
[queryParams]="historyRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Historie
</a>
}
</shared-menu>
</div>
</ng-template>

View File

@@ -8,7 +8,7 @@ import { map } from 'rxjs/operators';
import { CustomerSearchNavigation } from '@shared/services/navigation';
import { ComponentStore } from '@ngrx/component-store';
import { RouterLink } from '@angular/router';
import { AsyncPipe, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
export interface CustomerMenuComponentState {
customerId?: number;
@@ -26,7 +26,7 @@ export interface CustomerMenuComponentState {
styleUrls: ['customer-menu.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-customer-menu' },
imports: [CdkMenuModule, SharedMenuModule, IconComponent, RouterLink, NgIf, AsyncPipe],
imports: [CdkMenuModule, SharedMenuModule, IconComponent, RouterLink, AsyncPipe],
})
export class CustomerMenuComponent extends ComponentStore<CustomerMenuComponentState> {
private _navigation = inject(CustomerSearchNavigation);

View File

@@ -1,59 +1,66 @@
<cdk-virtual-scroll-viewport
itemSize="100"
class="h-[calc(100vh-20.125rem)] desktop-small:h-[calc(100vh-18.625rem)]"
*ngIf="!compact"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<a
*cdkVirtualFor="let customer of customers; trackBy: trackByFn; let index = index"
[routerLink]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.path"
[queryParams]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.queryParams"
[queryParamsHandling]="'merge'"
(click)="scrolledIndexChange(index)"
routerLinkActive
#rla="routerLinkActive"
>
<page-customer-result-list-item-full [class.active]="rla.isActive" [customer]="customer"></page-customer-result-list-item-full>
</a>
<div class="h-[6.125rem] bg-white rounded px-4 py-3" *ngIf="hits === customers?.length && !fetching">
<ng-container *ngTemplateOutlet="customerNotFound"></ng-container>
</div>
</cdk-virtual-scroll-viewport>
@if (!compact) {
<cdk-virtual-scroll-viewport
itemSize="100"
class="h-[calc(100vh-20.125rem)] desktop-small:h-[calc(100vh-18.625rem)]"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<a
*cdkVirtualFor="let customer of customers; trackBy: trackByFn; let index = index"
[routerLink]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.path"
[queryParams]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.queryParams"
[queryParamsHandling]="'merge'"
(click)="scrolledIndexChange(index)"
routerLinkActive
#rla="routerLinkActive"
>
<page-customer-result-list-item-full [class.active]="rla.isActive" [customer]="customer"></page-customer-result-list-item-full>
</a>
@if (hits === customers?.length && !fetching) {
<div class="h-[6.125rem] bg-white rounded px-4 py-3">
<ng-container *ngTemplateOutlet="customerNotFound"></ng-container>
</div>
}
</cdk-virtual-scroll-viewport>
}
<cdk-virtual-scroll-viewport
itemSize="191"
class="h-[calc(100vh-20.75rem)]"
*ngIf="compact"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<a
*cdkVirtualFor="let customer of customers; trackBy: trackByFn; let index = index"
[routerLink]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.path"
[queryParams]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.queryParams"
[queryParamsHandling]="'merge'"
(click)="scrolledIndexChange(index)"
routerLinkActive
#rla="routerLinkActive"
>
<page-customer-result-list-item [class.active]="rla.isActive" [customer]="customer"></page-customer-result-list-item>
</a>
<div class="h-[11.3125rem] bg-white rounded px-4 py-3" *ngIf="hits === customers?.length && !fetching">
<ng-container *ngTemplateOutlet="customerNotFound"></ng-container>
</div>
</cdk-virtual-scroll-viewport>
@if (compact) {
<cdk-virtual-scroll-viewport
itemSize="191"
class="h-[calc(100vh-20.75rem)]"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<a
*cdkVirtualFor="let customer of customers; trackBy: trackByFn; let index = index"
[routerLink]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.path"
[queryParams]="customerSearchNavigation.detailsRoute({ processId: processId, customerId: customer.id })?.queryParams"
[queryParamsHandling]="'merge'"
(click)="scrolledIndexChange(index)"
routerLinkActive
#rla="routerLinkActive"
>
<page-customer-result-list-item [class.active]="rla.isActive" [customer]="customer"></page-customer-result-list-item>
</a>
@if (hits === customers?.length && !fetching) {
<div class="h-[11.3125rem] bg-white rounded px-4 py-3">
<ng-container *ngTemplateOutlet="customerNotFound"></ng-container>
</div>
}
</cdk-virtual-scroll-viewport>
}
<ng-template #customerNotFound>
<div class="text-sm">
Hinweis: Aus Datenschutzgründen werden nur Teilinformationen dargestellt. Tab auf einen Kunden um mehr zu erfahren.
</div>
<div class="font-bold text-lg mt-3">
<span>Kunden nicht gefunden?</span>
<a
*ngIf="customerCreateNavigation.defaultRoute({ processId: processId }); let route"
[routerLink]="route.path"
[queryParams]="route.queryParams"
class="text-brand"
>
Neue Kundendaten erfassen
</a>
@if (customerCreateNavigation.defaultRoute({ processId: processId }); as route) {
<a
[routerLink]="route.path"
[queryParams]="route.queryParams"
class="text-brand"
>
Neue Kundendaten erfassen
</a>
}
</div>
</ng-template>

View File

@@ -1,21 +1,23 @@
<ng-container *ifRole="'Store'">
<shared-checkbox
*ngIf="customerType !== 'b2b'"
[ngModel]="p4mUser"
(ngModelChange)="setValue({ p4mUser: !p4mUser })"
[disabled]="p4mReadonly || readonly"
>
Kundenkarte
</shared-checkbox>
</ng-container>
<ng-container *ngFor="let option of filteredOptions$ | async">
<shared-checkbox
*ngIf="option?.enabled !== false"
[ngModel]="option.value === customerType"
(ngModelChange)="setValue({ customerType: $event ? option.value : undefined })"
[disabled]="isOptionDisabled(option)"
[name]="option.value"
>
{{ option.label }}
</shared-checkbox>
@if (customerType !== 'b2b') {
<shared-checkbox
[ngModel]="p4mUser"
(ngModelChange)="setValue({ p4mUser: !p4mUser })"
[disabled]="p4mReadonly || readonly"
>
Kundenkarte
</shared-checkbox>
}
</ng-container>
@for (option of filteredOptions$ | async; track option) {
@if (option?.enabled !== false) {
<shared-checkbox
[ngModel]="option.value === customerType"
(ngModelChange)="setValue({ customerType: $event ? option.value : undefined })"
[disabled]="isOptionDisabled(option)"
[name]="option.value"
>
{{ option.label }}
</shared-checkbox>
}
}

View File

@@ -6,57 +6,59 @@
type="text"
formControlName="street"
[tabindex]="tabIndexStart"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="Hausnummer">
<input
placeholder="Hausnummer"
class="input-control"
type="text"
formControlName="streetNumber"
[tabindex]="tabIndexStart + 1"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="PLZ">
<input
placeholder="PLZ"
class="input-control"
type="text"
formControlName="zipCode"
[tabindex]="tabIndexStart + 2"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="Ort">
<input
placeholder="Ort"
class="input-control"
type="text"
formControlName="city"
[tabindex]="tabIndexStart + 3"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control class="col-span-2" label="Adresszusatz">
<input
placeholder="Adresszusatz"
class="input-control"
type="text"
formControlName="info"
[tabindex]="tabIndexStart + 4"
[readonly]="readonly"
/>
</shared-form-control>
/>
</shared-form-control>
<shared-form-control label="Hausnummer">
<input
placeholder="Hausnummer"
class="input-control"
type="text"
formControlName="streetNumber"
[tabindex]="tabIndexStart + 1"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="PLZ">
<input
placeholder="PLZ"
class="input-control"
type="text"
formControlName="zipCode"
[tabindex]="tabIndexStart + 2"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="Ort">
<input
placeholder="Ort"
class="input-control"
type="text"
formControlName="city"
[tabindex]="tabIndexStart + 3"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control class="col-span-2" label="Land">
<shared-select placeholder="Land" formControlName="country" [tabindex]="tabIndexStart + 5" [readonly]="readonly">
<shared-select-option *ngFor="let country of countries || (countries$ | async)" [value]="country.isO3166_A_3">
{{ country.name }}
</shared-select-option>
</shared-select>
</shared-form-control>
</ng-container>
<shared-form-control class="col-span-2" label="Adresszusatz">
<input
placeholder="Adresszusatz"
class="input-control"
type="text"
formControlName="info"
[tabindex]="tabIndexStart + 4"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control class="col-span-2" label="Land">
<shared-select placeholder="Land" formControlName="country" [tabindex]="tabIndexStart + 5" [readonly]="readonly">
@for (country of countries || (countries$ | async); track country) {
<shared-select-option [value]="country.isO3166_A_3">
{{ country.name }}
</shared-select-option>
}
</shared-select>
</shared-form-control>
</ng-container>

View File

@@ -4,59 +4,64 @@
[tabindex]="tabIndexStart"
[autofocus]="focusAfterInit"
[readonly]="readonly"
>
>
<ng-content></ng-content>
</shared-checkbox>
<div class="address-block" *ngIf="control.value.deviatingAddress">
<div class="wrapper">
<app-organisation-form-block
*ngIf="organisation"
[tabIndexStart]="tabIndexStart + 1"
#orgaBlock
(onInit)="addOrganisationGroup($event)"
(onDestroy)="removeOrganisationGroup()"
[data]="data?.organisation"
#nameFormBlock
[tabIndexStart]="tabIndexStart + 1"
[requiredMarks]="organisationRequiredMarks"
[validatorFns]="organisationValidatorFns"
[readonly]="readonly"
></app-organisation-form-block>
<app-name-form-block
(onInit)="addNameGroup($event)"
(onDestroy)="removeNameGroup()"
[data]="data?.name"
#nameFormBlock
[requiredMarks]="nameRequiredMarks"
[validatorFns]="nameValidatorFns"
[readonly]="readonly"
></app-name-form-block>
<app-address-form-block
#addressFormBlock
(onInit)="addAddressGroup($event)"
(onDestroy)="removeAddressGroup()"
[data]="data?.address"
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidatorFns"
[readonly]="readonly"
></app-address-form-block>
<app-email-form-block
*ngIf="email"
#emailFormBlock
(onInit)="addEmailGroup($event)"
(onDestroy)="removeEmailGroup()"
[data]="data?.email"
[requiredMark]="emailRequiredMark"
[validatorFns]="emailValidationFns"
[readonly]="readonly"
></app-email-form-block>
<app-phone-numbers-form-block
*ngIf="phoneNumbers"
(onInit)="addPhoneNumbersGroup($event)"
(onDestroy)="removePhoneNumbersGroup()"
[readonly]="readonly"
>
[tabIndexStart]="emailFormBlock?.tabIndexEnd+1" [requiredMarks]="phoneNumbersRequiredMarks" [validatorFns]="phoneNumbersValidatorFns">
</app-phone-numbers-form-block>
@if (control.value.deviatingAddress) {
<div class="address-block">
<div class="wrapper">
@if (organisation) {
<app-organisation-form-block
[tabIndexStart]="tabIndexStart + 1"
#orgaBlock
(onInit)="addOrganisationGroup($event)"
(onDestroy)="removeOrganisationGroup()"
[data]="data?.organisation"
#nameFormBlock
[tabIndexStart]="tabIndexStart + 1"
[requiredMarks]="organisationRequiredMarks"
[validatorFns]="organisationValidatorFns"
[readonly]="readonly"
></app-organisation-form-block>
}
<app-name-form-block
(onInit)="addNameGroup($event)"
(onDestroy)="removeNameGroup()"
[data]="data?.name"
#nameFormBlock
[requiredMarks]="nameRequiredMarks"
[validatorFns]="nameValidatorFns"
[readonly]="readonly"
></app-name-form-block>
<app-address-form-block
#addressFormBlock
(onInit)="addAddressGroup($event)"
(onDestroy)="removeAddressGroup()"
[data]="data?.address"
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidatorFns"
[readonly]="readonly"
></app-address-form-block>
@if (email) {
<app-email-form-block
#emailFormBlock
(onInit)="addEmailGroup($event)"
(onDestroy)="removeEmailGroup()"
[data]="data?.email"
[requiredMark]="emailRequiredMark"
[validatorFns]="emailValidationFns"
[readonly]="readonly"
></app-email-form-block>
}
@if (phoneNumbers) {
<app-phone-numbers-form-block
(onInit)="addPhoneNumbersGroup($event)"
(onDestroy)="removePhoneNumbersGroup()"
[readonly]="readonly"
>
[tabIndexStart]="emailFormBlock?.tabIndexEnd+1" [requiredMarks]="phoneNumbersRequiredMarks" [validatorFns]="phoneNumbersValidatorFns">
</app-phone-numbers-form-block>
}
</div>
</div>
</div>
}

View File

@@ -1,12 +1,13 @@
<div class="interests-description">Geben Sie Interessen an, um Ihre persönlichen Kontoangaben zu verfeinern.</div>
<div class="interests-wrapper" [formGroup]="control">
<shared-checkbox
*ngFor="let pair of interests | keyvalue; let idx = index"
[formControlName]="pair.key"
[tabindex]="tabIndexStart + idx"
[autofocus]="focusAfterInit"
[readonly]="readonly"
>
{{ pair.value }}
</shared-checkbox>
@for (pair of interests | keyvalue; track pair; let idx = $index) {
<shared-checkbox
[formControlName]="pair.key"
[tabindex]="tabIndexStart + idx"
[autofocus]="focusAfterInit"
[readonly]="readonly"
>
{{ pair.value }}
</shared-checkbox>
}
</div>

View File

@@ -6,8 +6,10 @@
[readonly]="readonly"
[tabindex]="tabIndexStart"
[autofocus]="focusAfterInit"
>
<shared-select-option *ngFor="let gender of genderSettings.genders" [value]="gender.value">{{ gender.label }}</shared-select-option>
>
@for (gender of genderSettings.genders; track gender) {
<shared-select-option [value]="gender.value">{{ gender.label }}</shared-select-option>
}
</shared-select>
</shared-form-control>
@@ -30,7 +32,7 @@
formControlName="lastName"
[tabindex]="tabIndexStart + 2"
[readonly]="readonly"
/>
/>
</shared-form-control>
<shared-form-control label="Vorname">
@@ -41,6 +43,6 @@
formControlName="firstName"
[tabindex]="tabIndexStart + 3"
[readonly]="readonly"
/>
/>
</shared-form-control>
</ng-container>

View File

@@ -6,30 +6,30 @@
type="text"
formControlName="name"
[tabindex]="tabIndexStart"
[readonly]="readonly"
/>
</shared-form-control>
<ng-container *ngIf="appearence === 'default'">
<shared-form-control label="Abteilung">
<input
placeholder="Abteilung"
class="input-control"
type="text"
formControlName="department"
[tabindex]="tabIndexStart + 1"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="USt-ID">
<input
placeholder="USt-ID"
class="input-control"
type="text"
formControlName="vatId"
[tabindex]="tabIndexStart + 2"
[readonly]="readonly"
/>
</shared-form-control>
</ng-container>
</ng-container>
@if (appearence === 'default') {
<shared-form-control label="Abteilung">
<input
placeholder="Abteilung"
class="input-control"
type="text"
formControlName="department"
[tabindex]="tabIndexStart + 1"
[readonly]="readonly"
/>
</shared-form-control>
<shared-form-control label="USt-ID">
<input
placeholder="USt-ID"
class="input-control"
type="text"
formControlName="vatId"
[tabindex]="tabIndexStart + 2"
[readonly]="readonly"
/>
</shared-form-control>
}
</ng-container>

View File

@@ -6,9 +6,11 @@
[formControl]="control"
[tabindex]="tabIndexStart"
[readonly]="readonly"
/>
</shared-form-control>
<button type="button" *ngIf="!readonly && canScan()" (click)="scan()">
<shared-icon icon="barcode-scan" [size]="32"></shared-icon>
</button>
/>
</shared-form-control>
@if (!readonly && canScan()) {
<button type="button" (click)="scan()">
<shared-icon icon="barcode-scan" [size]="32"></shared-icon>
</button>
}

View File

@@ -1,38 +1,62 @@
<div class="wrapper text-center" [@cardFlip]="state" (@cardFlip.done)="flipAnimationDone($event)">
<div *ngIf="cardDetails" class="card-main">
<div class="icons text-brand">
<button *ngIf="isCustomerCard && frontside" class="icon-barcode" (click)="flipCard()">
<shared-icon [size]="35" icon="barcode-scanner"></shared-icon>
</button>
<button *ngIf="isCustomerCard && !frontside" class="icon-back" (click)="flipCard()">
<shared-icon [size]="35" icon="refresh"></shared-icon>
</button>
<!-- <div *ngIf="!isCustomerCard" class="icon-delete"><ui-icon (click)="onDeletePartnerCard()" size="25px" icon="trash"></ui-icon></div> -->
</div>
<div class="headline">
<p *ngIf="isCustomerCard && frontside">Ihre Lesepunkte</p>
<p *ngIf="isCustomerCard && !frontside">Kartennummer</p>
<p *ngIf="!isCustomerCard">Partnerkartennummer</p>
</div>
<div class="mt-2">
<div *ngIf="!isCustomerCard || (isCustomerCard && !frontside)" class="card-number">{{ cardDetails.code }}</div>
<div *ngIf="isCustomerCard && frontside" class="points">{{ cardDetails.totalPoints | number }}</div>
</div>
<div class="barcode-button">
<div *ngIf="!isCustomerCard || (isCustomerCard && !frontside)" class="barcode-field">
<img class="barcode" src="/assets/images/barcode.png" alt="Barcode" />
@if (cardDetails) {
<div class="card-main">
<div class="icons text-brand">
@if (isCustomerCard && frontside) {
<button class="icon-barcode" (click)="flipCard()">
<shared-icon [size]="35" icon="barcode-scanner"></shared-icon>
</button>
}
@if (isCustomerCard && !frontside) {
<button class="icon-back" (click)="flipCard()">
<shared-icon [size]="35" icon="refresh"></shared-icon>
</button>
}
<!-- <div *ngIf="!isCustomerCard" class="icon-delete"><ui-icon (click)="onDeletePartnerCard()" size="25px" icon="trash"></ui-icon></div> -->
</div>
<div *ngIf="isCustomerCard && frontside">
<button class="button" (click)="onRewardShop()">Zum Prämienshop</button>
<div class="headline">
@if (isCustomerCard && frontside) {
<p>Ihre Lesepunkte</p>
}
@if (isCustomerCard && !frontside) {
<p>Kartennummer</p>
}
@if (!isCustomerCard) {
<p>Partnerkartennummer</p>
}
</div>
<div class="mt-2">
@if (!isCustomerCard || (isCustomerCard && !frontside)) {
<div class="card-number">{{ cardDetails.code }}</div>
}
@if (isCustomerCard && frontside) {
<div class="points">{{ cardDetails.totalPoints | number }}</div>
}
</div>
<div class="barcode-button">
@if (!isCustomerCard || (isCustomerCard && !frontside)) {
<div class="barcode-field">
<img class="barcode" src="/assets/images/barcode.png" alt="Barcode" />
</div>
}
@if (isCustomerCard && frontside) {
<div>
<button class="button" (click)="onRewardShop()">Zum Prämienshop</button>
</div>
}
</div>
</div>
</div>
}
<div class="card-bottom">
<div *ngIf="!isCustomerCard || (isCustomerCard && !frontside)" class="customer-name">
<p>{{ cardDetails.firstName }} {{ cardDetails.lastName }}</p>
</div>
<div *ngIf="isCustomerCard && frontside" class="logo ml-2">
<img class="logo-picture" src="/assets/images/Hugendubel_Logo.png" alt="Hugendubel Logo" />
</div>
@if (!isCustomerCard || (isCustomerCard && !frontside)) {
<div class="customer-name">
<p>{{ cardDetails.firstName }} {{ cardDetails.lastName }}</p>
</div>
}
@if (isCustomerCard && frontside) {
<div class="logo ml-2">
<img class="logo-picture" src="/assets/images/Hugendubel_Logo.png" alt="Hugendubel Logo" />
</div>
}
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { DecimalPipe, NgIf } from '@angular/common';
import { DecimalPipe } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core';
import { IconComponent } from '@shared/components/icon';
import { BonusCardInfoDTO } from '@generated/swagger/crm-api';
@@ -8,7 +8,7 @@ import { BonusCardInfoDTO } from '@generated/swagger/crm-api';
selector: 'page-customer-kundenkarte',
templateUrl: 'kundenkarte.component.html',
styleUrls: ['kundenkarte.component.scss'],
imports: [IconComponent, NgIf, DecimalPipe],
imports: [IconComponent, DecimalPipe],
animations: [
trigger('cardFlip', [
state(

View File

@@ -1,89 +1,83 @@
<form *ngIf="formData$ | async; let data" (keydown.enter)="$event.preventDefault()">
<h1 class="title">Kundendaten erfassen</h1>
<p class="description">
Um Ihnen den ausgewählten Service zu
<br />
ermöglichen, legen wir Ihnen gerne
<br />
ein Kundenkonto an.
</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="false"
customerType="b2b"
(valueChanges)="customerTypeChanged($event)"
></app-customer-type-selector>
<app-organisation-form-block
#orga
[tabIndexStart]="1"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
(onInit)="addFormBlock('organisation', $event)"
[requiredMarks]="organisationFormBlockRequiredMarks"
[validatorFns]="organisationFormBlockValidators"
></app-organisation-form-block>
<app-name-form-block
#name
[tabIndexStart]="orga.tabIndexEnd + 1"
[data]="data.name"
(dataChanges)="patchFormData('name', $event)"
(onInit)="addFormBlock('name', $event)"
></app-name-form-block>
<app-address-form-block
#address
[tabIndexStart]="name.tabIndexEnd + 1"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
(onInit)="addFormBlock('address', $event)"
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidators"
[defaults]="{ country: 'DEU' }"
></app-address-form-block>
<app-email-form-block
#email
[tabIndexStart]="address.tabIndexEnd + 1"
[data]="data.email"
(dataChanges)="patchFormData('email', $event)"
[validatorFns]="emailFormBlockValidators"
(onInit)="addFormBlock('email', $event)"
></app-email-form-block>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="email.tabIndexEnd + 1"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
(onInit)="addFormBlock('phoneNumbers', $event)"
></app-phone-numbers-form-block>
<app-deviating-address-form-block
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[nameRequiredMarks]="deviatingNameRequiredMarks"
[nameValidatorFns]="deviatingNameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidators"
[organisationRequiredMarks]="organisationFormBlockRequiredMarks"
[organisationValidatorFns]="organisationFormBlockValidators"
[defaults]="{ address: { country: 'DEU' } }"
[organisation]="true"
[email]="true"
[phoneNumbers]="true"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<div class="spacer"></div>
<div class="sticky w-full flex items-center justify-center">
<button class="cta-submit" type="button" (click)="save()" [disabled]="form.invalid || form.pending">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</div>
</form>
@if (formData$ | async; as data) {
<form (keydown.enter)="$event.preventDefault()">
<h1 class="title">Kundendaten erfassen</h1>
<p class="description">
Um Ihnen den ausgewählten Service zu
<br />
ermöglichen, legen wir Ihnen gerne
<br />
ein Kundenkonto an.
</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="false"
customerType="b2b"
(valueChanges)="customerTypeChanged($event)"
></app-customer-type-selector>
<app-organisation-form-block
#orga
[tabIndexStart]="1"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
(onInit)="addFormBlock('organisation', $event)"
[requiredMarks]="organisationFormBlockRequiredMarks"
[validatorFns]="organisationFormBlockValidators"
></app-organisation-form-block>
<app-name-form-block
#name
[tabIndexStart]="orga.tabIndexEnd + 1"
[data]="data.name"
(dataChanges)="patchFormData('name', $event)"
(onInit)="addFormBlock('name', $event)"
></app-name-form-block>
<app-address-form-block
#address
[tabIndexStart]="name.tabIndexEnd + 1"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
(onInit)="addFormBlock('address', $event)"
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidators"
[defaults]="{ country: 'DEU' }"
></app-address-form-block>
<app-email-form-block
#email
[tabIndexStart]="address.tabIndexEnd + 1"
[data]="data.email"
(dataChanges)="patchFormData('email', $event)"
[validatorFns]="emailFormBlockValidators"
(onInit)="addFormBlock('email', $event)"
></app-email-form-block>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="email.tabIndexEnd + 1"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
(onInit)="addFormBlock('phoneNumbers', $event)"
></app-phone-numbers-form-block>
<app-deviating-address-form-block
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[nameRequiredMarks]="deviatingNameRequiredMarks"
[nameValidatorFns]="deviatingNameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidators"
[organisationRequiredMarks]="organisationFormBlockRequiredMarks"
[organisationValidatorFns]="organisationFormBlockValidators"
[defaults]="{ address: { country: 'DEU' } }"
[organisation]="true"
[email]="true"
[phoneNumbers]="true"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<div class="spacer"></div>
<div class="sticky w-full flex items-center justify-center">
<button class="cta-submit" type="button" (click)="save()" [disabled]="form.invalid || form.pending">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</div>
</form>
}

View File

@@ -1,4 +1,4 @@
import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { ActivatedRoute, RouterModule } from '@angular/router';
import { CustomerCreateSideViewModule } from './customer-create-side-view';
@@ -10,7 +10,7 @@ import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
templateUrl: 'create-customer.component.html',
styleUrls: ['create-customer.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, RouterModule, SharedSplitscreenComponent, CustomerCreateSideViewModule],
imports: [RouterModule, SharedSplitscreenComponent, CustomerCreateSideViewModule],
})
export class CreateCustomerComponent {
processId$ = this._activatedRoute.parent.data.pipe(map((data) => data.processId));

View File

@@ -1,102 +1,94 @@
<form *ngIf="formData$ | async; let data" (keydown.enter)="$event.preventDefault()">
<h1 class="title">Kundendaten erfassen</h1>
<p class="description">
Um Ihnen den ausgewählten Service
<br />
zu ermöglichen, legen wir Ihnen
<br />
gerne ein Kundenkonto an.
<br />
</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="false"
customerType="guest"
(valueChanges)="customerTypeChanged($event)"
></app-customer-type-selector>
<app-name-form-block
#name
[tabIndexStart]="1"
[data]="data.name"
(dataChanges)="patchFormData('name', $event)"
(onInit)="addFormBlock('name', $event)"
[requiredMarks]="nameRequiredMarks"
[validatorFns]="nameValidationFns"
></app-name-form-block>
<p class="info">
Wir werden Ihnen Werbung zu ähnlichen Produkten oder Dienstleistungen aus unserem Sortiment per E-Mail zusenden. Sie können der
Verwendung Ihrer Daten jederzeit z.B. mittels der in den E-Mails enthaltenen Abmeldelinks widersprechen, ohne dass hierfür andere als
die Übermittlungskosten nach den Basistarifen entstehen.
</p>
<app-email-form-block
#email
[tabIndexStart]="name.tabIndexEnd + 1"
[data]="data.email"
[requiredMark]="true"
(dataChanges)="patchFormData('email', $event)"
[validatorFns]="emailFormBlockValidators"
(onInit)="addFormBlock('email', $event)"
></app-email-form-block>
<app-organisation-form-block
#orga
[tabIndexStart]="email.tabIndexStart + 1"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
(onInit)="addFormBlock('organisation', $event)"
appearence="compact"
></app-organisation-form-block>
<app-address-form-block
#address
[tabIndexStart]="orga.tabIndexEnd + 1"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
(onInit)="addFormBlock('address', $event)"
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidators"
[defaults]="{ country: 'DEU' }"
></app-address-form-block>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="address.tabIndexEnd + 1"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
(onInit)="addFormBlock('phoneNumbers', $event)"
></app-phone-numbers-form-block>
<app-birth-date-form-block
#birthDate
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
(onInit)="addFormBlock('birthDate', $event)"
[data]="data.birthDate"
(dataChanges)="patchFormData('birthDate', $event)"
></app-birth-date-form-block>
<app-deviating-address-form-block
[tabIndexStart]="birthDate.tabIndexEnd + 1"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[nameRequiredMarks]="deviatingNameRequiredMarks"
[nameValidatorFns]="deviatingNameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidators"
[defaults]="{ address: { country: 'DEU' } }"
[organisation]="true"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<div class="spacer"></div>
<div class="sticky w-full flex items-center justify-center">
<button class="cta-submit" type="button" (click)="save()" [disabled]="form.invalid || form.pending">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</div>
</form>
@if (formData$ | async; as data) {
<form (keydown.enter)="$event.preventDefault()">
<h1 class="title">Kundendaten erfassen</h1>
<p class="description">
Um Ihnen den ausgewählten Service
<br />
zu ermöglichen, legen wir Ihnen
<br />
gerne ein Kundenkonto an.
<br />
</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="false"
customerType="guest"
(valueChanges)="customerTypeChanged($event)"
></app-customer-type-selector>
<app-name-form-block
#name
[tabIndexStart]="1"
[data]="data.name"
(dataChanges)="patchFormData('name', $event)"
(onInit)="addFormBlock('name', $event)"
[requiredMarks]="nameRequiredMarks"
[validatorFns]="nameValidationFns"
></app-name-form-block>
<p class="info">
Wir werden Ihnen Werbung zu ähnlichen Produkten oder Dienstleistungen aus unserem Sortiment per E-Mail zusenden. Sie können der
Verwendung Ihrer Daten jederzeit z.B. mittels der in den E-Mails enthaltenen Abmeldelinks widersprechen, ohne dass hierfür andere als
die Übermittlungskosten nach den Basistarifen entstehen.
</p>
<app-email-form-block
#email
[tabIndexStart]="name.tabIndexEnd + 1"
[data]="data.email"
[requiredMark]="true"
(dataChanges)="patchFormData('email', $event)"
[validatorFns]="emailFormBlockValidators"
(onInit)="addFormBlock('email', $event)"
></app-email-form-block>
<app-organisation-form-block
#orga
[tabIndexStart]="email.tabIndexStart + 1"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
(onInit)="addFormBlock('organisation', $event)"
appearence="compact"
></app-organisation-form-block>
<app-address-form-block
#address
[tabIndexStart]="orga.tabIndexEnd + 1"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
(onInit)="addFormBlock('address', $event)"
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidators"
[defaults]="{ country: 'DEU' }"
></app-address-form-block>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="address.tabIndexEnd + 1"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
(onInit)="addFormBlock('phoneNumbers', $event)"
></app-phone-numbers-form-block>
<app-birth-date-form-block
#birthDate
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
(onInit)="addFormBlock('birthDate', $event)"
[data]="data.birthDate"
(dataChanges)="patchFormData('birthDate', $event)"
></app-birth-date-form-block>
<app-deviating-address-form-block
[tabIndexStart]="birthDate.tabIndexEnd + 1"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[nameRequiredMarks]="deviatingNameRequiredMarks"
[nameValidatorFns]="deviatingNameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidators"
[defaults]="{ address: { country: 'DEU' } }"
[organisation]="true"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<div class="spacer"></div>
<div class="sticky w-full flex items-center justify-center">
<button class="cta-submit" type="button" (click)="save()" [disabled]="form.invalid || form.pending">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</div>
</form>
}

View File

@@ -1,10 +1,10 @@
<form *ngIf="formData$ | async; let data" (keydown.enter)="$event.preventDefault()">
<h1 class="title flex flex-row items-center justify-center">
Kundendaten erfassen
<!-- <span
class="rounded-full ml-4 h-8 w-8 text-xl text-center border-2 border-solid border-brand text-brand">i</span> -->
@if (formData$ | async; as data) {
<form (keydown.enter)="$event.preventDefault()">
<h1 class="title flex flex-row items-center justify-center">
Kundendaten erfassen
<!-- <span
class="rounded-full ml-4 h-8 w-8 text-xl text-center border-2 border-solid border-brand text-brand">i</span> -->
</h1>
<p class="description">
Um Sie als Kunde beim nächsten
<br />
@@ -12,7 +12,6 @@
<br />
wir Ihnen gerne eine Kundenkarte an.
</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="true"
@@ -20,7 +19,6 @@
(valueChanges)="customerTypeChanged($event)"
[p4mReadonly]="data?._meta?.p4mRequired"
></app-customer-type-selector>
<app-p4m-number-form-block
#p4mBlock
[tabIndexStart]="1"
@@ -31,7 +29,6 @@
[focusAfterInit]="!data?._meta?.p4mRequired"
[asyncValidatorFns]="asyncLoyaltyCardValidatorFn"
></app-p4m-number-form-block>
<app-accept-agb-form-block
[tabIndexStart]="inBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('agb', $event)"
@@ -40,7 +37,6 @@
[requiredMark]="true"
[validatorFns]="agbValidatorFns"
></app-accept-agb-form-block>
<app-newsletter-form-block
class="mb-4"
#newsletterBlock
@@ -50,7 +46,6 @@
(dataChanges)="patchFormData('newsletter', $event)"
[focusAfterInit]="data?._meta?.p4mRequired"
></app-newsletter-form-block>
<app-name-form-block
#nameBlock
[tabIndexStart]="newsletterBlock.tabIndexEnd + 1"
@@ -60,13 +55,13 @@
[validatorFns]="nameValidationFns"
(dataChanges)="patchFormData('name', $event)"
></app-name-form-block>
<p class="info" *ngIf="customerType === 'webshop-p4m'">
Wir werden Ihnen Werbung zu ähnlichen Produkten oder Dienstleistungen aus unserem Sortiment per E-Mail zusenden. Sie können der
Verwendung Ihrer Daten jederzeit z.B. mittels der in den E-Mails enthaltenen Abmeldelinks widersprechen, ohne dass hierfür andere als
die Übermittlungskosten nach den Basistarifen entstehen.
</p>
@if (customerType === 'webshop-p4m') {
<p class="info">
Wir werden Ihnen Werbung zu ähnlichen Produkten oder Dienstleistungen aus unserem Sortiment per E-Mail zusenden. Sie können der
Verwendung Ihrer Daten jederzeit z.B. mittels der in den E-Mails enthaltenen Abmeldelinks widersprechen, ohne dass hierfür andere als
die Übermittlungskosten nach den Basistarifen entstehen.
</p>
}
<app-email-form-block
class="flex-grow"
#email
@@ -78,7 +73,6 @@
[validatorFns]="emailValidatorFn"
[asyncValidatorFns]="asyncEmailVlaidtorFn"
></app-email-form-block>
<app-organisation-form-block
#orgBlock
[tabIndexStart]="email.tabIndexEnd + 1"
@@ -87,7 +81,6 @@
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
></app-organisation-form-block>
<app-address-form-block
[defaults]="{ country: 'DEU' }"
#addressBlock
@@ -98,7 +91,6 @@
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidatorFns"
></app-address-form-block>
<app-deviating-address-form-block
#ddaBlock
[defaults]="{ address: { country: 'DEU' } }"
@@ -110,10 +102,9 @@
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="shippingAddressRequiredMarks"
[addressValidatorFns]="shippingAddressValidators"
>
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="ddaBlock.tabIndexEnd + 1"
@@ -121,7 +112,6 @@
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
></app-phone-numbers-form-block>
<app-birth-date-form-block
#bdBlock
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
@@ -131,7 +121,6 @@
[requiredMark]="true"
[validatorFns]="birthDateValidatorFns"
></app-birth-date-form-block>
<app-interests-form-block
#inBlock
[tabIndexStart]="bdBlock.tabIndexEnd + 1"
@@ -139,12 +128,11 @@
[data]="data.interests"
(dataChanges)="patchFormData('interests', $event)"
></app-interests-form-block>
<div class="spacer"></div>
<div class="sticky w-full flex items-center justify-center">
<button class="cta-submit" type="button" [disabled]="form.invalid || form.pending" (click)="save()">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</div>
</form>
}

View File

@@ -1,86 +1,78 @@
<form *ngIf="formData$ | async; let data" (keydown.enter)="$event.preventDefault()">
<h1 class="title">Kundendaten erfassen</h1>
<p class="description">Wir legen Ihnen gerne ein Kundenkonto an</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="false"
customerType="store"
(valueChanges)="customerTypeChanged($event)"
></app-customer-type-selector>
<app-name-form-block
#name
[tabIndexStart]="1"
(onInit)="addFormBlock('name', $event)"
[requiredMarks]="nameRequiredMarks"
[validatorFns]="nameValidationFns"
[data]="data.name"
(dataChanges)="patchFormData('name', $event)"
></app-name-form-block>
<app-email-form-block
#email
[tabIndexStart]="name.tabIndexEnd + 1"
[validatorFns]="emailFormBlockValidators"
(onInit)="addFormBlock('email', $event)"
[data]="data.email"
(dataChanges)="patchFormData('email', $event)"
></app-email-form-block>
<app-organisation-form-block
#orga
[tabIndexStart]="email.tabIndexEnd + 1"
appearence="compact"
(onInit)="addFormBlock('organisation', $event)"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
></app-organisation-form-block>
<app-address-form-block
#address
[tabIndexStart]="orga.tabIndexEnd + 1"
(onInit)="addFormBlock('address', $event)"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
></app-address-form-block>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="address.tabIndexEnd + 1"
(onInit)="addFormBlock('phoneNumbers', $event)"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
></app-phone-numbers-form-block>
<app-birth-date-form-block
#birthDate
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
(onInit)="addFormBlock('birthDate', $event)"
[data]="data.birthDate"
(dataChanges)="patchFormData('birthDate', $event)"
></app-birth-date-form-block>
<app-deviating-address-form-block
[tabIndexStart]="birthDate.tabIndexEnd + 1"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[organisation]="true"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="deviatingAddressValidators"
[defaults]="{ address: { country: 'DEU' } }"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<div class="spacer"></div>
<div class="sticky w-full flex items-center justify-center">
<button class="cta-submit" type="button" [disabled]="form.invalid || form.pending" (click)="save()">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</div>
</form>
@if (formData$ | async; as data) {
<form (keydown.enter)="$event.preventDefault()">
<h1 class="title">Kundendaten erfassen</h1>
<p class="description">Wir legen Ihnen gerne ein Kundenkonto an</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="false"
customerType="store"
(valueChanges)="customerTypeChanged($event)"
></app-customer-type-selector>
<app-name-form-block
#name
[tabIndexStart]="1"
(onInit)="addFormBlock('name', $event)"
[requiredMarks]="nameRequiredMarks"
[validatorFns]="nameValidationFns"
[data]="data.name"
(dataChanges)="patchFormData('name', $event)"
></app-name-form-block>
<app-email-form-block
#email
[tabIndexStart]="name.tabIndexEnd + 1"
[validatorFns]="emailFormBlockValidators"
(onInit)="addFormBlock('email', $event)"
[data]="data.email"
(dataChanges)="patchFormData('email', $event)"
></app-email-form-block>
<app-organisation-form-block
#orga
[tabIndexStart]="email.tabIndexEnd + 1"
appearence="compact"
(onInit)="addFormBlock('organisation', $event)"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
></app-organisation-form-block>
<app-address-form-block
#address
[tabIndexStart]="orga.tabIndexEnd + 1"
(onInit)="addFormBlock('address', $event)"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
></app-address-form-block>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="address.tabIndexEnd + 1"
(onInit)="addFormBlock('phoneNumbers', $event)"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
></app-phone-numbers-form-block>
<app-birth-date-form-block
#birthDate
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
(onInit)="addFormBlock('birthDate', $event)"
[data]="data.birthDate"
(dataChanges)="patchFormData('birthDate', $event)"
></app-birth-date-form-block>
<app-deviating-address-form-block
[tabIndexStart]="birthDate.tabIndexEnd + 1"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[organisation]="true"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="deviatingAddressValidators"
[defaults]="{ address: { country: 'DEU' } }"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<div class="spacer"></div>
<div class="sticky w-full flex items-center justify-center">
<button class="cta-submit" type="button" [disabled]="form.invalid || form.pending" (click)="save()">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</div>
</form>
}

View File

@@ -1,104 +1,95 @@
<form *ngIf="formData$ | async; let data" (keydown.enter)="$event.preventDefault()">
<h1 class="title">Kundendaten erfassen</h1>
<p class="description">
Wenn Sie möchten legen wir Ihnen
<br />
gerne ein Onlinekonto an. Dort können
<br />
Sie Ihre Bestellungen einsehen.
</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="false"
customerType="webshop"
(valueChanges)="customerTypeChanged($event)"
></app-customer-type-selector>
<app-name-form-block
#name
[tabIndexStart]="1"
(onInit)="addFormBlock('name', $event)"
[requiredMarks]="nameRequiredMarks"
[validatorFns]="nameValidationFns"
[data]="data.name"
(dataChanges)="patchFormData('name', $event)"
></app-name-form-block>
<p class="info">
Wir werden Ihnen Werbung zu ähnlichen Produkten oder Dienstleistungen aus unserem Sortiment per E-Mail zusenden. Sie können der
Verwendung Ihrer Daten jederzeit z.B. mittels der in den E-Mails enthaltenen Abmeldelinks widersprechen, ohne dass hierfür andere als
die Übermittlungskosten nach den Basistarifen entstehen.
</p>
<app-email-form-block
#email
[tabIndexStart]="name.tabIndexEnd + 1"
(onInit)="addFormBlock('email', $event)"
[requiredMark]="true"
[asyncValidatorFns]="asyncEmailValidatorFns"
[validatorFns]="emailValidatorFns"
[data]="data.email"
(dataChanges)="patchFormData('email', $event)"
></app-email-form-block>
<app-organisation-form-block
#org
[tabIndexStart]="email.tabIndexEnd + 1"
appearence="compact"
(onInit)="addFormBlock('organisation', $event)"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
></app-organisation-form-block>
<app-address-form-block
#address
[tabIndexStart]="org.tabIndexEnd + 1"
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidators"
(onInit)="addFormBlock('address', $event)"
[defaults]="{ country: 'DEU' }"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
></app-address-form-block>
<p class="info">Das Anlegen geht für Sie noch schneller, wenn wir Ihnen das initiale Passwort per SMS auf Ihr Mobilgerät schicken.</p>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="address.tabIndexEnd + 1"
(onInit)="addFormBlock('phoneNumbers', $event)"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
></app-phone-numbers-form-block>
<app-birth-date-form-block
#birthDate
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
(onInit)="addFormBlock('birthDate', $event)"
[data]="data.birthDate"
(dataChanges)="patchFormData('birthDate', $event)"
></app-birth-date-form-block>
<app-deviating-address-form-block
[tabIndexStart]="birthDate.tabIndexEnd + 1"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
appearence="b2b"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidators"
[defaults]="{ address: { country: 'DEU' } }"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<div class="spacer"></div>
<div class="sticky w-full flex items-center justify-center">
<button class="cta-submit" type="button" [disabled]="form.invalid || form.pending" (click)="save()">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</div>
</form>
@if (formData$ | async; as data) {
<form (keydown.enter)="$event.preventDefault()">
<h1 class="title">Kundendaten erfassen</h1>
<p class="description">
Wenn Sie möchten legen wir Ihnen
<br />
gerne ein Onlinekonto an. Dort können
<br />
Sie Ihre Bestellungen einsehen.
</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="false"
customerType="webshop"
(valueChanges)="customerTypeChanged($event)"
></app-customer-type-selector>
<app-name-form-block
#name
[tabIndexStart]="1"
(onInit)="addFormBlock('name', $event)"
[requiredMarks]="nameRequiredMarks"
[validatorFns]="nameValidationFns"
[data]="data.name"
(dataChanges)="patchFormData('name', $event)"
></app-name-form-block>
<p class="info">
Wir werden Ihnen Werbung zu ähnlichen Produkten oder Dienstleistungen aus unserem Sortiment per E-Mail zusenden. Sie können der
Verwendung Ihrer Daten jederzeit z.B. mittels der in den E-Mails enthaltenen Abmeldelinks widersprechen, ohne dass hierfür andere als
die Übermittlungskosten nach den Basistarifen entstehen.
</p>
<app-email-form-block
#email
[tabIndexStart]="name.tabIndexEnd + 1"
(onInit)="addFormBlock('email', $event)"
[requiredMark]="true"
[asyncValidatorFns]="asyncEmailValidatorFns"
[validatorFns]="emailValidatorFns"
[data]="data.email"
(dataChanges)="patchFormData('email', $event)"
></app-email-form-block>
<app-organisation-form-block
#org
[tabIndexStart]="email.tabIndexEnd + 1"
appearence="compact"
(onInit)="addFormBlock('organisation', $event)"
[data]="data.organisation"
(dataChanges)="patchFormData('organisation', $event)"
></app-organisation-form-block>
<app-address-form-block
#address
[tabIndexStart]="org.tabIndexEnd + 1"
[requiredMarks]="addressRequiredMarks"
[validatorFns]="addressValidators"
(onInit)="addFormBlock('address', $event)"
[defaults]="{ country: 'DEU' }"
[data]="data.address"
(dataChanges)="patchFormData('address', $event)"
></app-address-form-block>
<p class="info">Das Anlegen geht für Sie noch schneller, wenn wir Ihnen das initiale Passwort per SMS auf Ihr Mobilgerät schicken.</p>
<app-phone-numbers-form-block
#phoneNumbers
[tabIndexStart]="address.tabIndexEnd + 1"
(onInit)="addFormBlock('phoneNumbers', $event)"
[data]="data.phoneNumbers"
(dataChanges)="patchFormData('phoneNumbers', $event)"
></app-phone-numbers-form-block>
<app-birth-date-form-block
#birthDate
[tabIndexStart]="phoneNumbers.tabIndexEnd + 1"
(onInit)="addFormBlock('birthDate', $event)"
[data]="data.birthDate"
(dataChanges)="patchFormData('birthDate', $event)"
></app-birth-date-form-block>
<app-deviating-address-form-block
[tabIndexStart]="birthDate.tabIndexEnd + 1"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
appearence="b2b"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidators"
[defaults]="{ address: { country: 'DEU' } }"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
>
Die Lieferadresse weicht von der Rechnungsadresse ab
</app-deviating-address-form-block>
<div class="spacer"></div>
<div class="sticky w-full flex items-center justify-center">
<button class="cta-submit" type="button" [disabled]="form.invalid || form.pending" (click)="save()">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</div>
</form>
}

View File

@@ -1,4 +1,4 @@
import { CommonModule } from '@angular/common';
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { RouterModule } from '@angular/router';
@@ -7,7 +7,7 @@ import { RouterModule } from '@angular/router';
templateUrl: 'customer-create-side-view.component.html',
styleUrls: ['customer-create-side-view.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, RouterModule],
imports: [RouterModule],
})
export class CustomerCreateSideViewComponent {
constructor() {}

View File

@@ -1,121 +1,109 @@
<form *ngIf="formData$ | async; let data" (keydown.enter)="$event.preventDefault()">
<h1 class="title flex flex-row items-center justify-center">Kundenkartendaten erfasen</h1>
<p class="description">Bitte erfassen Sie die Kundenkarte</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="true"
customerType="webshop"
[readonly]="true"
></app-customer-type-selector>
<app-p4m-number-form-block
#p4mBlock
[tabIndexStart]="1"
(onInit)="addFormBlock('p4m', $event)"
[data]="data.p4m"
(dataChanges)="patchFormData('p4m', $event)"
[focusAfterInit]="!data?._meta?.p4mRequired"
[asyncValidatorFns]="asyncLoyaltyCardValidatorFn"
></app-p4m-number-form-block>
<app-accept-agb-form-block
[tabIndexStart]="inBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('agb', $event)"
[data]="data.agb"
(dataChanges)="patchFormData('agb', $event)"
[requiredMark]="true"
[validatorFns]="agbValidatorFns"
></app-accept-agb-form-block>
<app-newsletter-form-block
class="mb-4"
#newsletterBlock
[tabIndexStart]="p4mBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('newsletter', $event)"
[data]="data.newsletter"
(dataChanges)="patchFormData('newsletter', $event)"
[focusAfterInit]="data?._meta?.p4mRequired"
></app-newsletter-form-block>
<app-name-form-block (onInit)="addFormBlock('name', $event)" [data]="data.name" readonly></app-name-form-block>
<app-email-form-block (onInit)="addFormBlock('email', $event)" [data]="data.email" readonly></app-email-form-block>
<app-organisation-form-block
(onInit)="addFormBlock('organisation', $event)"
appearence="compact"
[data]="data.organisation"
readonly
></app-organisation-form-block>
<app-address-form-block [data]="data.address" readonly></app-address-form-block>
<div class="mt-8">
<h4 class="-mb-6">Rechnungsadresse</h4>
<ui-form-control class="-mb-5" [showHint]="false">
<input type="text" [value]="(billingAddress | address: true) ?? 'Keine Adresse vorhanden'" [readonly]="true" />
</ui-form-control>
<app-deviating-address-form-block
#dbaBlock
[defaults]="{ address: { country: 'DEU' } }"
[tabIndexStart]="newsletterBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('billingAddress', $event)"
[data]="data.billingAddress"
(dataChanges)="patchFormData('billingAddress', $event)"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidatorFns"
>
Abweichende Rechnungsadresse anlegen
</app-deviating-address-form-block>
</div>
<div class="mt-8">
<h4 class="-mb-6">Lieferadresse</h4>
<ui-form-control class="-mb-5" [showHint]="false">
<input type="text" [value]="(shippingAddress | address: true) ?? 'Keine Adresse vorhanden'" [readonly]="true" />
</ui-form-control>
<app-deviating-address-form-block
#ddaBlock
[defaults]="{ address: { country: 'DEU' } }"
[tabIndexStart]="dbaBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidatorFns"
>
Abweichende Lieferadresse anlegen
</app-deviating-address-form-block>
</div>
<app-birth-date-form-block
#bdBlock
[tabIndexStart]="ddaBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('birthDate', $event)"
[data]="data.birthDate"
(dataChanges)="patchFormData('birthDate', $event)"
[requiredMark]="true"
[validatorFns]="birthDateValidatorFns"
></app-birth-date-form-block>
<app-interests-form-block
#inBlock
[tabIndexStart]="bdBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('interests', $event)"
[data]="data.interests"
(dataChanges)="patchFormData('interests', $event)"
></app-interests-form-block>
<div class="spacer"></div>
<div class="sticky w-full flex items-center justify-center">
<button class="cta-submit" type="button" [disabled]="form.invalid || form.pending" (click)="save()">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</div>
</form>
@if (formData$ | async; as data) {
<form (keydown.enter)="$event.preventDefault()">
<h1 class="title flex flex-row items-center justify-center">Kundenkartendaten erfasen</h1>
<p class="description">Bitte erfassen Sie die Kundenkarte</p>
<app-customer-type-selector
[processId]="processId$ | async"
[p4mUser]="true"
customerType="webshop"
[readonly]="true"
></app-customer-type-selector>
<app-p4m-number-form-block
#p4mBlock
[tabIndexStart]="1"
(onInit)="addFormBlock('p4m', $event)"
[data]="data.p4m"
(dataChanges)="patchFormData('p4m', $event)"
[focusAfterInit]="!data?._meta?.p4mRequired"
[asyncValidatorFns]="asyncLoyaltyCardValidatorFn"
></app-p4m-number-form-block>
<app-accept-agb-form-block
[tabIndexStart]="inBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('agb', $event)"
[data]="data.agb"
(dataChanges)="patchFormData('agb', $event)"
[requiredMark]="true"
[validatorFns]="agbValidatorFns"
></app-accept-agb-form-block>
<app-newsletter-form-block
class="mb-4"
#newsletterBlock
[tabIndexStart]="p4mBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('newsletter', $event)"
[data]="data.newsletter"
(dataChanges)="patchFormData('newsletter', $event)"
[focusAfterInit]="data?._meta?.p4mRequired"
></app-newsletter-form-block>
<app-name-form-block (onInit)="addFormBlock('name', $event)" [data]="data.name" readonly></app-name-form-block>
<app-email-form-block (onInit)="addFormBlock('email', $event)" [data]="data.email" readonly></app-email-form-block>
<app-organisation-form-block
(onInit)="addFormBlock('organisation', $event)"
appearence="compact"
[data]="data.organisation"
readonly
></app-organisation-form-block>
<app-address-form-block [data]="data.address" readonly></app-address-form-block>
<div class="mt-8">
<h4 class="-mb-6">Rechnungsadresse</h4>
<ui-form-control class="-mb-5" [showHint]="false">
<input type="text" [value]="(billingAddress | address: true) ?? 'Keine Adresse vorhanden'" [readonly]="true" />
</ui-form-control>
<app-deviating-address-form-block
#dbaBlock
[defaults]="{ address: { country: 'DEU' } }"
[tabIndexStart]="newsletterBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('billingAddress', $event)"
[data]="data.billingAddress"
(dataChanges)="patchFormData('billingAddress', $event)"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidatorFns"
>
Abweichende Rechnungsadresse anlegen
</app-deviating-address-form-block>
</div>
<div class="mt-8">
<h4 class="-mb-6">Lieferadresse</h4>
<ui-form-control class="-mb-5" [showHint]="false">
<input type="text" [value]="(shippingAddress | address: true) ?? 'Keine Adresse vorhanden'" [readonly]="true" />
</ui-form-control>
<app-deviating-address-form-block
#ddaBlock
[defaults]="{ address: { country: 'DEU' } }"
[tabIndexStart]="dbaBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('deviatingDeliveryAddress', $event)"
[data]="data.deviatingDeliveryAddress"
(dataChanges)="patchFormData('deviatingDeliveryAddress', $event)"
[nameRequiredMarks]="nameRequiredMarks"
[nameValidatorFns]="nameValidationFns"
[addressRequiredMarks]="addressRequiredMarks"
[addressValidatorFns]="addressValidatorFns"
>
Abweichende Lieferadresse anlegen
</app-deviating-address-form-block>
</div>
<app-birth-date-form-block
#bdBlock
[tabIndexStart]="ddaBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('birthDate', $event)"
[data]="data.birthDate"
(dataChanges)="patchFormData('birthDate', $event)"
[requiredMark]="true"
[validatorFns]="birthDateValidatorFns"
></app-birth-date-form-block>
<app-interests-form-block
#inBlock
[tabIndexStart]="bdBlock.tabIndexEnd + 1"
(onInit)="addFormBlock('interests', $event)"
[data]="data.interests"
(dataChanges)="patchFormData('interests', $event)"
></app-interests-form-block>
<div class="spacer"></div>
<div class="sticky w-full flex items-center justify-center">
<button class="cta-submit" type="button" [disabled]="form.invalid || form.pending" (click)="save()">
<ui-spinner [show]="busy$ | async">Speichern</ui-spinner>
</button>
</div>
</form>
}

View File

@@ -1,19 +1,22 @@
<div class="flex flex-row justify-end -mt-4 -mr-2">
<a
*ngIf="detailsRoute$ | async; let detailsRoute"
[routerLink]="detailsRoute.path"
[queryParams]="detailsRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
>
<shared-icon icon="close" [size]="32"></shared-icon>
</a>
@if (detailsRoute$ | async; as detailsRoute) {
<a
[routerLink]="detailsRoute.path"
[queryParams]="detailsRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
>
<shared-icon icon="close" [size]="32"></shared-icon>
</a>
}
</div>
<h1 class="text-2xl text-center font-bold mb-6">Rechnungsadresse hinzufügen</h1>
<form [formGroup]="formGroup" (ngSubmit)="save()">
<shared-form-control label="Anrede">
<shared-select formControlName="gender" placeholder="Anrede" tabindex="1" [autofocus]="true">
<shared-select-option *ngFor="let gender of genderSettings.genders" [value]="gender.value">{{ gender.label }}</shared-select-option>
@for (gender of genderSettings.genders; track gender) {
<shared-select-option [value]="gender.value">{{ gender.label }}</shared-select-option>
}
</shared-select>
</shared-form-control>
@@ -62,9 +65,11 @@
<shared-form-control class="col-span-2" label="Land">
<shared-select placeholder="Land" formControlName="country" tabindex="11">
<shared-select-option *ngFor="let country of countries$ | async" [value]="country.isO3166_A_3">
{{ country.name }}
</shared-select-option>
@for (country of countries$ | async; track country) {
<shared-select-option [value]="country.isO3166_A_3">
{{ country.name }}
</shared-select-option>
}
</shared-select>
</shared-form-control>
@@ -76,7 +81,7 @@
[disabled]="formGroup.invalid || formGroup.disabled"
type="submit"
class="px-5 py-3 font-bold text-lg rounded-full bg-brand text-white"
>
>
Speichern
</button>
</div>

View File

@@ -6,7 +6,7 @@ import { FormControlComponent } from '@shared/components/form-control';
import { CrmCustomerService } from '@domain/crm';
import { AddressDTO, Gender, PayerDTO } from '@generated/swagger/crm-api';
import { map } from 'rxjs/operators';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { AddressSelectionModalService } from '@modal/address-selection';
import { CustomerSearchStore } from '../store';
import { CustomerSearchNavigation } from '@shared/services/navigation';
@@ -26,13 +26,11 @@ import { GenderSettingsService } from '@shared/services/gender';
IconComponent,
RouterLink,
AsyncPipe,
NgIf,
NgForOf,
ReactiveFormsModule,
SelectModule,
FormControlComponent,
CheckboxComponent,
],
CheckboxComponent
],
})
export class AddBillingAddressMainViewComponent {
private _customerService = inject(CrmCustomerService);

View File

@@ -1,181 +1,183 @@
<div class="flex flex-row justify-end -mt-4 -mr-2">
<a
*ngIf="detailsRoute$ | async; let detailsRoute"
[routerLink]="detailsRoute.path"
[queryParams]="detailsRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
>
<shared-icon icon="close" [size]="32"></shared-icon>
</a>
@if (detailsRoute$ | async; as detailsRoute) {
<a
[routerLink]="detailsRoute.path"
[queryParams]="detailsRoute.urlTree.queryParams"
[queryParamsHandling]="'merge'"
>
<shared-icon icon="close" [size]="32"></shared-icon>
</a>
}
</div>
<h1 class="text-2xl text-center font-bold mb-6">Lieferadresse hinzufügen</h1>
<form [formGroup]="formGroup" (ngSubmit)="save()">
<ng-container *ngIf="isBusinessKonto$ | async">
@if (isBusinessKonto$ | async) {
<shared-form-control
[label]="formGroup.controls.organisation.errors?.required ? 'Firma *' : 'Firma'"
class="col-span-2"
>
>
<input
class="input-control"
placeholder="Firma"
type="text"
formControlName="organisation"
tabindex="1"
/>
</shared-form-control>
/>
</shared-form-control>
<shared-form-control label="Abteilung">
<input
class="input-control"
placeholder="Abteilung"
type="text"
formControlName="department"
tabindex="2"
/>
</shared-form-control>
<shared-form-control label="USt-ID">
<input
class="input-control"
placeholder="Abteilung"
type="text"
formControlName="vatId"
tabindex="3"
/>
</shared-form-control>
}
<shared-form-control label="Abteilung">
<input
class="input-control"
placeholder="Abteilung"
type="text"
formControlName="department"
tabindex="2"
/>
</shared-form-control>
<shared-form-control label="Anrede">
<shared-select formControlName="gender" placeholder="Anrede" tabindex="4" [autofocus]="true">
@for (gender of genderSettings.genders; track gender) {
<shared-select-option [value]="gender.value">{{
gender.label
}}</shared-select-option>
}
</shared-select>
</shared-form-control>
<shared-form-control label="USt-ID">
<input
class="input-control"
placeholder="Abteilung"
type="text"
formControlName="vatId"
tabindex="3"
/>
</shared-form-control>
</ng-container>
<shared-form-control label="Titel">
<shared-select formControlName="title" placeholder="Titel" tabindex="5">
<shared-select-option value="Dipl.-Ing.">Dipl.-Ing.</shared-select-option>
<shared-select-option value="Dr.">Dr.</shared-select-option>
<shared-select-option value="Dr. med.">Dr. med.</shared-select-option>
<shared-select-option value="Prof.">Prof.</shared-select-option>
<shared-select-option value="Prof. Dr.">Prof. Dr.</shared-select-option>
<shared-select-option value="RA">RA</shared-select-option>
</shared-select>
</shared-form-control>
<shared-form-control label="Anrede">
<shared-select formControlName="gender" placeholder="Anrede" tabindex="4" [autofocus]="true">
<shared-select-option *ngFor="let gender of genderSettings.genders" [value]="gender.value">{{
gender.label
}}</shared-select-option>
</shared-select>
</shared-form-control>
<shared-form-control
[label]="formGroup.controls.organisation.errors?.required ? 'Nachname *' : 'Nachname'"
>
<input
class="input-control"
placeholder="Nachname"
type="text"
formControlName="lastName"
tabindex="6"
/>
</shared-form-control>
<shared-form-control label="Titel">
<shared-select formControlName="title" placeholder="Titel" tabindex="5">
<shared-select-option value="Dipl.-Ing.">Dipl.-Ing.</shared-select-option>
<shared-select-option value="Dr.">Dr.</shared-select-option>
<shared-select-option value="Dr. med.">Dr. med.</shared-select-option>
<shared-select-option value="Prof.">Prof.</shared-select-option>
<shared-select-option value="Prof. Dr.">Prof. Dr.</shared-select-option>
<shared-select-option value="RA">RA</shared-select-option>
</shared-select>
</shared-form-control>
<shared-form-control
[label]="formGroup.controls.organisation.errors?.required ? 'Vorname *' : 'Vorname'"
>
<input
class="input-control"
placeholder="Vorname"
type="text"
formControlName="firstName"
tabindex="7"
/>
</shared-form-control>
<shared-form-control
[label]="formGroup.controls.organisation.errors?.required ? 'Nachname *' : 'Nachname'"
>
<input
class="input-control"
placeholder="Nachname"
type="text"
formControlName="lastName"
tabindex="6"
/>
</shared-form-control>
@if (!(isBusinessKonto$ | async)) {
<shared-form-control label="Firma" class="col-span-2">
<input
class="input-control"
placeholder="Firma"
type="text"
formControlName="organisation"
tabindex="8"
/>
</shared-form-control>
}
<shared-form-control
[label]="formGroup.controls.organisation.errors?.required ? 'Vorname *' : 'Vorname'"
>
<input
class="input-control"
placeholder="Vorname"
type="text"
formControlName="firstName"
tabindex="7"
/>
</shared-form-control>
<shared-form-control label="Straße">
<input
class="input-control"
placeholder="Straße"
type="text"
formControlName="street"
tabindex="9"
/>
</shared-form-control>
<ng-container *ngIf="!(isBusinessKonto$ | async)">
<shared-form-control label="Firma" class="col-span-2">
<input
class="input-control"
placeholder="Firma"
type="text"
formControlName="organisation"
tabindex="8"
/>
</shared-form-control>
</ng-container>
<shared-form-control label="Hausnummer">
<input
class="input-control"
placeholder="Hausnummer"
type="text"
formControlName="streetNumber"
tabindex="10"
/>
</shared-form-control>
<shared-form-control label="Straße">
<input
class="input-control"
placeholder="Straße"
type="text"
formControlName="street"
tabindex="9"
/>
</shared-form-control>
<shared-form-control label="PLZ">
<input
class="input-control"
placeholder="PLZ"
type="text"
formControlName="zipCode"
tabindex="11"
/>
</shared-form-control>
<shared-form-control label="Hausnummer">
<input
class="input-control"
placeholder="Hausnummer"
type="text"
formControlName="streetNumber"
tabindex="10"
/>
</shared-form-control>
<shared-form-control label="Ort">
<input
class="input-control"
placeholder="Ort"
type="text"
formControlName="city"
tabindex="12"
/>
</shared-form-control>
<shared-form-control label="PLZ">
<input
class="input-control"
placeholder="PLZ"
type="text"
formControlName="zipCode"
tabindex="11"
/>
</shared-form-control>
<shared-form-control label="Adresszusatz" class="col-span-2">
<input
class="input-control"
placeholder="Adresszusatz"
type="text"
formControlName="info"
tabindex="13"
/>
</shared-form-control>
<shared-form-control label="Ort">
<input
class="input-control"
placeholder="Ort"
type="text"
formControlName="city"
tabindex="12"
/>
</shared-form-control>
<shared-form-control class="col-span-2" label="Land">
<shared-select placeholder="Land" formControlName="country" tabindex="14">
@for (country of countries$ | async; track country) {
<shared-select-option
[value]="country.isO3166_A_3"
>
{{ country.name }}
</shared-select-option>
}
</shared-select>
</shared-form-control>
<shared-form-control label="Adresszusatz" class="col-span-2">
<input
class="input-control"
placeholder="Adresszusatz"
type="text"
formControlName="info"
tabindex="13"
/>
</shared-form-control>
<shared-form-control class="col-span-2" label="Land">
<shared-select placeholder="Land" formControlName="country" tabindex="14">
<shared-select-option
*ngFor="let country of countries$ | async"
[value]="country.isO3166_A_3"
>
{{ country.name }}
</shared-select-option>
</shared-select>
</shared-form-control>
<div class="text-center col-span-2">
<shared-checkbox formControlName="isDefault"
>Diese Lieferadresse als Standard Adresse festlegen</shared-checkbox
>
</div>
<div class="mt-6 text-center col-span-2">
<button
[disabled]="formGroup.invalid || formGroup.disabled"
type="submit"
class="px-5 py-3 font-bold text-lg rounded-full bg-brand text-white"
tabindex="15"
>
Speichern
</button>
</div>
</form>
<div class="text-center col-span-2">
<shared-checkbox formControlName="isDefault"
>Diese Lieferadresse als Standard Adresse festlegen</shared-checkbox
>
</div>
<div class="mt-6 text-center col-span-2">
<button
[disabled]="formGroup.invalid || formGroup.disabled"
type="submit"
class="px-5 py-3 font-bold text-lg rounded-full bg-brand text-white"
tabindex="15"
>
Speichern
</button>
</div>
</form>

View File

@@ -6,7 +6,7 @@ import { FormControlComponent } from '@shared/components/form-control';
import { CrmCustomerService } from '@domain/crm';
import { AddressDTO, Gender, ShippingAddressDTO } from '@generated/swagger/crm-api';
import { map, takeUntil } from 'rxjs/operators';
import { AsyncPipe, NgForOf, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { AddressSelectionModalService } from '@modal/address-selection';
import { CustomerSearchStore } from '../store';
import { CustomerSearchNavigation } from '@shared/services/navigation';
@@ -27,13 +27,11 @@ import { validateCompanyOrPersonalInfoRequired } from '../../validators/gender-b
RouterLink,
IconComponent,
AsyncPipe,
NgIf,
NgForOf,
ReactiveFormsModule,
SelectModule,
FormControlComponent,
CheckboxComponent,
],
CheckboxComponent
],
})
export class AddShippingAddressMainViewComponent implements OnInit, OnDestroy {
private _customerService = inject(CrmCustomerService);

View File

@@ -1,71 +1,77 @@
<div class="grid grid-flow-col items-center justify-between px-4 py-2 border-t-2 border-solid border-surface-2">
<h3 class="font-bold text-xl">Rechnungsadresse</h3>
<a
*ngIf="addBillingAddressRoute$ | async; let addRoute"
type="button"
class="text-brand font-bold"
[routerLink]="addRoute.path"
[queryParams]="addRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Hinzufügen
</a>
@if (addBillingAddressRoute$ | async; as addRoute) {
<a
type="button"
class="text-brand font-bold"
[routerLink]="addRoute.path"
[queryParams]="addRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Hinzufügen
</a>
}
</div>
<div class="grid grid-flow-row">
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 flex flex-row justify-between items-center"
*ngIf="showCustomerAddress$ | async"
>
<input
*ngIf="isNotBusinessKonto$ | async"
name="assigned-payer"
type="radio"
[checked]="!(selectedPayer$ | async)"
(change)="selectCustomerAddress()"
/>
<div class="ml-2 flex flex-row justify-between items-start grow">
<span class="mr-4">
{{ customer$ | async | address }}
</span>
<a
*ngIf="editRoute$ | async; let editRoute"
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
class="text-brand font-bold"
type="button"
@if (showCustomerAddress$ | async) {
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 flex flex-row justify-between items-center"
>
Bearbeiten
</a>
</div>
</label>
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 cursor-pointer flex flex-row items-center justify-start"
*ngFor="let assignedPayer of assignedPayers$ | async"
>
<input
name="assigned-payer"
type="radio"
[value]="assignedPayer"
[checked]="(selectedPayer$ | async)?.payer.id === assignedPayer.payer.id"
(change)="selectPayer(assignedPayer)"
/>
<div class="ml-2 flex flex-row justify-between items-start grow">
<span class="mr-4">
{{ assignedPayer.payer.data | address }}
</span>
<ng-container *ngIf="canEditAddress$ | async">
<a
*ngIf="editRoute(assignedPayer.payer.id); let editRoute"
class="text-brand font-bold"
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Bearbeiten
</a>
</ng-container>
</div>
</label>
@if (isNotBusinessKonto$ | async) {
<input
name="assigned-payer"
type="radio"
[checked]="!(selectedPayer$ | async)"
(change)="selectCustomerAddress()"
/>
}
<div class="ml-2 flex flex-row justify-between items-start grow">
<span class="mr-4">
{{ customer$ | async | address }}
</span>
@if (editRoute$ | async; as editRoute) {
<a
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
class="text-brand font-bold"
type="button"
>
Bearbeiten
</a>
}
</div>
</label>
}
@for (assignedPayer of assignedPayers$ | async; track assignedPayer) {
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 cursor-pointer flex flex-row items-center justify-start"
>
<input
name="assigned-payer"
type="radio"
[value]="assignedPayer"
[checked]="(selectedPayer$ | async)?.payer.id === assignedPayer.payer.id"
(change)="selectPayer(assignedPayer)"
/>
<div class="ml-2 flex flex-row justify-between items-start grow">
<span class="mr-4">
{{ assignedPayer.payer.data | address }}
</span>
@if (canEditAddress$ | async) {
@if (editRoute(assignedPayer.payer.id); as editRoute) {
<a
class="text-brand font-bold"
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Bearbeiten
</a>
}
}
</div>
</label>
}
</div>

View File

@@ -4,7 +4,7 @@ import { CrmCustomerService } from '@domain/crm';
import { debounceTime, map, switchMap, takeUntil } from 'rxjs/operators';
import { Observable, Subject, combineLatest } from 'rxjs';
import { AssignedPayerDTO, CustomerDTO, ListResponseArgsOfAssignedPayerDTO } from '@generated/swagger/crm-api';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { CustomerPipesModule } from '@shared/pipes/customer';
import { ComponentStore } from '@ngrx/component-store';
import { tapResponse } from '@ngrx/operators';
@@ -26,7 +26,7 @@ interface DetailsMainViewBillingAddressesComponentState {
styleUrls: ['details-main-view-billing-addresses.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-details-main-view-billing-addresses' },
imports: [NgIf, NgFor, AsyncPipe, CustomerPipesModule, RouterLink],
imports: [AsyncPipe, CustomerPipesModule, RouterLink],
})
export class DetailsMainViewBillingAddressesComponent
extends ComponentStore<DetailsMainViewBillingAddressesComponentState>

View File

@@ -1,64 +1,69 @@
<div class="grid grid-flow-col items-center justify-between px-4 py-2 border-t-2 border-solid border-surface-2">
<h3 class="font-bold text-xl">Lieferadresse</h3>
<a
*ngIf="addShippingAddressRoute$ | async; let addRoute"
type="button"
class="text-brand font-bold"
[routerLink]="addRoute.path"
[queryParams]="addRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Hinzufügen
</a>
@if (addShippingAddressRoute$ | async; as addRoute) {
<a
type="button"
class="text-brand font-bold"
[routerLink]="addRoute.path"
[queryParams]="addRoute.queryParams"
[queryParamsHandling]="'merge'"
>
Hinzufügen
</a>
}
</div>
<div class="grid grid-flow-row">
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 cursor-pointer flex flex-row justify-start items-center"
*ngIf="showCustomerAddress$ | async"
>
<input name="shipping-address" type="radio" [checked]="!(selectedShippingAddress$ | async)" (change)="selectCustomerAddress()" />
<div class="ml-2 flex flex-row justify-between items-start grow">
<span class="mr-4">
{{ customer$ | async | address }}
</span>
<a
*ngIf="editRoute$ | async; let editRoute"
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
class="text-brand font-bold"
type="button"
@if (showCustomerAddress$ | async) {
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 cursor-pointer flex flex-row justify-start items-center"
>
Bearbeiten
</a>
</div>
</label>
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 cursor-pointer flex flex-row justify-start items-center"
*ngFor="let shippingAddress of shippingAddresses$ | async"
>
<input
name="shipping-address"
type="radio"
[value]="shippingAddress"
[checked]="(selectedShippingAddress$ | async)?.id === shippingAddress.id"
(change)="selectShippingAddress(shippingAddress)"
/>
<div class="ml-2 flex flex-row justify-between items-start grow">
<span class="mr-4">
{{ shippingAddress | address }}
</span>
<a
*ngIf="editShippingAddressRoute$(shippingAddress.id) | async; let route"
class="text-brand font-bold"
type="button"
[routerLink]="route?.path"
[queryParams]="route?.queryParams"
[queryParamsHandling]="'merge'"
<input name="shipping-address" type="radio" [checked]="!(selectedShippingAddress$ | async)" (change)="selectCustomerAddress()" />
<div class="ml-2 flex flex-row justify-between items-start grow">
<span class="mr-4">
{{ customer$ | async | address }}
</span>
@if (editRoute$ | async; as editRoute) {
<a
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
class="text-brand font-bold"
type="button"
>
Bearbeiten
</a>
}
</div>
</label>
}
@for (shippingAddress of shippingAddresses$ | async; track shippingAddress) {
<label
class="px-4 py-3 font-bold border-t-2 border-solid border-surface-2 cursor-pointer flex flex-row justify-start items-center"
>
Bearbeiten
</a>
</div>
</label>
<input
name="shipping-address"
type="radio"
[value]="shippingAddress"
[checked]="(selectedShippingAddress$ | async)?.id === shippingAddress.id"
(change)="selectShippingAddress(shippingAddress)"
/>
<div class="ml-2 flex flex-row justify-between items-start grow">
<span class="mr-4">
{{ shippingAddress | address }}
</span>
@if (editShippingAddressRoute$(shippingAddress.id) | async; as route) {
<a
class="text-brand font-bold"
type="button"
[routerLink]="route?.path"
[queryParams]="route?.queryParams"
[queryParamsHandling]="'merge'"
>
Bearbeiten
</a>
}
</div>
</label>
}
</div>

View File

@@ -4,7 +4,7 @@ import { CrmCustomerService } from '@domain/crm';
import { map, switchMap, takeUntil } from 'rxjs/operators';
import { Observable, Subject, combineLatest } from 'rxjs';
import { CustomerDTO, ListResponseArgsOfAssignedPayerDTO, ShippingAddressDTO } from '@generated/swagger/crm-api';
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import { AsyncPipe } from '@angular/common';
import { CustomerPipesModule } from '@shared/pipes/customer';
import { ComponentStore } from '@ngrx/component-store';
import { tapResponse } from '@ngrx/operators';
@@ -25,7 +25,7 @@ interface DetailsMainViewDeliveryAddressesComponentState {
styleUrls: ['details-main-view-delivery-addresses.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page-details-main-view-delivery-addresses' },
imports: [NgIf, NgFor, AsyncPipe, CustomerPipesModule, RouterLink],
imports: [AsyncPipe, CustomerPipesModule, RouterLink],
})
export class DetailsMainViewDeliveryAddressesComponent
extends ComponentStore<DetailsMainViewDeliveryAddressesComponentState>

View File

@@ -23,40 +23,47 @@
{{ customerType$ | async }}
</span>
</div>
<ng-container *ngIf="showEditButton$ | async">
<a
*ngIf="editRoute$ | async; let editRoute"
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
class="btn btn-label font-bold text-brand"
>
Bearbeiten
</a>
</ng-container>
@if (showEditButton$ | async) {
@if (editRoute$ | async; as editRoute) {
<a
[routerLink]="editRoute.path"
[queryParams]="editRoute.queryParams"
[queryParamsHandling]="'merge'"
class="btn btn-label font-bold text-brand"
>
Bearbeiten
</a>
}
}
</div>
<div class="customer-details-customer-main-data px-5 py-3 grid grid-flow-row gap-3">
<div class="flex flex-row">
<div class="data-label">Erstellungsdatum</div>
<div *ngIf="created$ | async; let created" class="data-value">
{{ created | date: 'dd.MM.yyyy' }} | {{ created | date: 'HH:mm' }} Uhr
</div>
@if (created$ | async; as created) {
<div class="data-value">
{{ created | date: 'dd.MM.yyyy' }} | {{ created | date: 'HH:mm' }} Uhr
</div>
}
</div>
<div class="flex flex-row">
<div class="data-label">Kundennummer</div>
<div class="data-value">{{ customerNumber$ | async }}</div>
</div>
<div class="flex flex-row" *ngIf="customerNumberDig$ | async; let customerNumberDig">
<div class="data-label">Kundennummer-DIG</div>
<div class="data-value">{{ customerNumberDig }}</div>
</div>
<div class="flex flex-row" *ngIf="customerNumberBeeline$ | async; let customerNumberBeeline">
<div class="data-label">Kundennummer-BEELINE</div>
<div class="data-value">{{ customerNumberBeeline }}</div>
</div>
@if (customerNumberDig$ | async; as customerNumberDig) {
<div class="flex flex-row">
<div class="data-label">Kundennummer-DIG</div>
<div class="data-value">{{ customerNumberDig }}</div>
</div>
}
@if (customerNumberBeeline$ | async; as customerNumberBeeline) {
<div class="flex flex-row">
<div class="data-label">Kundennummer-BEELINE</div>
<div class="data-value">{{ customerNumberBeeline }}</div>
</div>
}
</div>
<ng-container *ngIf="isBusinessKonto$ | async">
@if (isBusinessKonto$ | async) {
<div class="customer-details-customer-main-row">
<div class="data-label">Firmenname</div>
<div class="data-value">{{ organisationName$ | async }}</div>
@@ -69,7 +76,7 @@
<div class="data-label">USt-ID</div>
<div class="data-value">{{ vatId$ | async }}</div>
</div>
</ng-container>
}
<div class="customer-details-customer-main-row">
<div class="data-label">Anrede</div>
@@ -113,7 +120,9 @@
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Land</div>
<div *ngIf="country$ | async; let country" class="data-value">{{ country | country }}</div>
@if (country$ | async; as country) {
<div class="data-value">{{ country | country }}</div>
}
</div>
<div class="customer-details-customer-main-row">
<div class="data-label">Festnetznr.</div>
@@ -124,18 +133,18 @@
<div class="data-value">{{ mobile$ | async }}</div>
</div>
<ng-container *ngIf="!(isBusinessKonto$ | async)">
@if (!(isBusinessKonto$ | async)) {
<div class="customer-details-customer-main-row">
<div class="data-label">Geburtstag</div>
<div class="data-value">{{ dateOfBirth$ | async | date: 'dd.MM.yyyy' }}</div>
</div>
</ng-container>
<ng-container *ngIf="!(isBusinessKonto$ | async) && (organisationName$ | async)">
}
@if (!(isBusinessKonto$ | async) && (organisationName$ | async)) {
<div class="customer-details-customer-main-row">
<div class="data-label">Firmenname</div>
<div class="data-value">{{ organisationName$ | async }}</div>
</div>
<ng-container *ngIf="!(isOnlineOrCustomerCardUser$ | async)">
@if (!(isOnlineOrCustomerCardUser$ | async)) {
<div class="customer-details-customer-main-row">
<div class="data-label">Abteilung</div>
<div class="data-value">{{ department$ | async }}</div>
@@ -144,8 +153,8 @@
<div class="data-label">USt-ID</div>
<div class="data-value">{{ vatId$ | async }}</div>
</div>
</ng-container>
</ng-container>
}
}
<page-details-main-view-billing-addresses></page-details-main-view-billing-addresses>
<page-details-main-view-delivery-addresses></page-details-main-view-delivery-addresses>
@@ -153,22 +162,24 @@
</div>
</shared-loader>
<button
*ngIf="shoppingCartHasNoItems$ | async"
type="button"
(click)="continue()"
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
[disabled]="showLoader$ | async"
>
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">Weiter zur Artikelsuche</shared-loader>
</button>
@if (shoppingCartHasNoItems$ | async) {
<button
type="button"
(click)="continue()"
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
[disabled]="showLoader$ | async"
>
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">Weiter zur Artikelsuche</shared-loader>
</button>
}
<button
*ngIf="shoppingCartHasItems$ | async"
type="button"
(click)="continue()"
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
[disabled]="showLoader$ | async"
>
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">Weiter zum Warenkorb</shared-loader>
</button>
@if (shoppingCartHasItems$ | async) {
<button
type="button"
(click)="continue()"
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
[disabled]="showLoader$ | async"
>
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">Weiter zum Warenkorb</shared-loader>
</button>
}

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