Compare commits

...

49 Commits

Author SHA1 Message Date
Lorenz Hilpert
a608e4d02e chore: migrate packages to latest versions using Nx migrations
- Update Nx from 21.3.2 to 21.5.2
- Update Angular packages from 20.1.x to 20.2.4
- Update NgRx from 20.0.0 to 20.0.1
- Update development dependencies (@swc/core, @types/node, etc.)
- Resolve dependency conflicts with canvas and jsdom versions
- Apply Nx migrations for tsConfig options and Angular CLI updates
- Fix TypeScript error in customer-order-search.store.ts for tuple destructuring
- Update package overrides for compatibility with updated dependencies

All tests passing and build successful after migration.
2025-09-17 13:49:11 +02:00
Lorenz Hilpert
afc6351509 chore: disable markdown format on save in VSCode settings
Added formatOnSave: false for markdown files to prevent automatic formatting that may conflict with document structure preferences.
2025-09-17 11:50:03 +02:00
Lorenz Hilpert
c5d057e3a7 Merged PR 1950: 5343-Filter-NumberRange
Related work items: #5343
2025-09-16 09:54:29 +00:00
Nino Righi
707802ce0d Merged PR 1944: feat(checkout-reward): #5258
- feat(loyalty): add loyalty program feature with list and navigation
- fix(isa-app-side-menu): Update customer expand to signals
- feat(catalogue-data-access): add searchLoyaltyItems method with comprehensive test coverage
- feat(project-structure): migrate loyalty system to reward-based architecture
- feat(checkout-reward): add query settings resolver and catalog resource
- feat(swagger-cat-search-api): Swagger Update
- feat(checkout-reward): update API call and prepare filter integration

Refs: #5258
2025-09-12 10:44:42 +00:00
Lorenz Hilpert
e00de7598d feat(crm): add crm-data-access library with initial component and tests
- Introduced the crm-data-access library with a basic component.
- Added necessary configuration files, including ESLint and TypeScript settings.
- Implemented unit tests for the component to ensure functionality.

Refs: #5254
2025-09-11 20:13:56 +02:00
Lorenz Hilpert
516b7748c2 chore: update .gitignore and package-lock.json to include new files 2025-09-09 11:19:04 +02:00
Nino Righi
cffa7721bc Merged PR 1941: fix(oms-data-access): adjust tolino return eligibility logic for display damage
fix(oms-data-access): adjust tolino return eligibility logic for display damage

Update tolino return eligibility to check for display damage and refine
date range conditions. Returns are now only eligible if the receipt is
between 6-24 months old, the item was received damaged, and the display
is not damaged.

Ref: #5286
2025-09-04 15:12:44 +00:00
Nino Righi
066ab5d5be Merged PR 1940: feat(old-ui-tooltip): add pointer-events-auto to tooltip panel
feat(old-ui-tooltip): add pointer-events-auto to tooltip panel

Enable mouse interactions with tooltip content by adding pointer-events-auto
class to .ui-tooltip-panel. This allows users to interact with clickable
elements inside tooltips while maintaining proper tooltip positioning.

Ref: #5244
2025-09-04 14:11:49 +00:00
Nino Righi
3bbf79a3c3 Merged PR 1939: feat(remission-list, empty-state): add comprehensive empty state handling wit...
feat(remission-list, empty-state): add comprehensive empty state handling with new appearance types

Add dedicated empty state component for remission list with smart prioritization logic:
- Department selection required state (highest priority)
- All done state when list is processed and empty
- No search results state for filtered content

Enhance ui-empty-state component with new appearance types:
- AllDone: Trophy cup icon with animated steam effects
- SelectAction: Hand pointer with dropdown interface element
- Improved visual hierarchy and spacing for all states

Update remission list to use new empty state component with proper state detection
including search term validation, department filter checking, and reload detection.

Ref: #5317, #5290
2025-09-04 14:11:19 +00:00
Nino Righi
357485e32f Merged PR 1938: #5294 Small Adjustments
#5294 Small Adjustments
2025-09-04 14:10:55 +00:00
Nino Righi
39984342a6 Merged PR 1937: fix(ui-input-controls-dropdown): prevent multiple dropdowns from being open s...
fix(ui-input-controls-dropdown): prevent multiple dropdowns from being open simultaneously

Add DropdownService to manage global dropdown state and ensure only one
dropdown is open at any time. When a new dropdown opens, any previously
opened dropdown is automatically closed, improving user experience and
preventing UI conflicts.

Ref: #5298
2025-09-03 13:19:10 +00:00
Nino Righi
c52f18e979 Merged PR 1936: fix(remission): filter search results by stock availability and display stock...
fix(remission): filter search results by stock availability and display stock info

- Add stock resource integration to search item component
- Filter search results to only show items with available stock (> 0)
- Display current stock information in search result items
- Implement calculateAvailableStock utility for accurate stock calculation
- Add inStock input parameter to SearchItemToRemitComponent
- Create reusable instock.resource for stock data fetching

The search now only displays items that are actually available for remission,
improving user experience by preventing selection of out-of-stock items.

Ref: #5318
2025-09-03 13:18:47 +00:00
Nino Righi
e58ec93087 Merged PR 1935: fix(remission-list, remission-data-access): add impediment comment and remain...
fix(remission-list, remission-data-access): add impediment comment and remaining quantity handling for return suggestions

Add support for impedimentComment and remainingQuantity fields when adding return suggestion items. When quantity is less than available stock, automatically set impedimentComment to 'Restmenge' and calculate remainingQuantity as the difference between available stock and remitted quantity.

Changes:
- Add impedimentComment and remainingQuantity to AddReturnSuggestionItemSchema
- Update RemissionReturnReceiptService to handle new fields in addReturnSuggestionItem method
- Enhance RemissionListComponent to calculate and pass impediment data when remitting items
- Fix quantity calculation logic to properly handle partial remissions

Ref: #5322
2025-09-03 13:18:23 +00:00
Nino Righi
4e6204817d Merged PR 1934: feature(remission-list): temporarily disable remission-processed-hint component
feature(remission-list): temporarily disable remission-processed-hint component

Comment out remi-remission-processed-hint component in remission list template
and add TODO comments referencing the need to adjust code once ticket #5215
is implemented. This temporary fix prevents issues with the hint component
until the underlying changes are completed.

Ref: #5136
2025-09-03 13:16:35 +00:00
Nino Righi
c41355bcdf Merged PR 1933: fix(remission-data-access): replace hardcoded values with dynamic helper func...
fix(remission-data-access): replace hardcoded values with dynamic helper functions

Replace hardcoded assortment and retail price values in RemissionSearchService
with proper helper functions. Add getAssortmentFromItem and getRetailPriceFromItem
helpers to dynamically extract values from Item objects instead of using
static fallbacks.

Also fix potential undefined reference errors in remission list resource
by adding proper null checks for response merging operations.

Ref: #5321
2025-09-03 13:15:57 +00:00
Nino Righi
fa8e601660 Merged PR 1932: feat(remission): ensure package assignment before completing return receipts
feat(remission): ensure package assignment before completing return receipts

Add validation to check if a package is assigned to a return receipt before
allowing completion. When no package is assigned, automatically open the
package assignment dialog to let users scan/input a package number.

- Add hasAssignedPackage input to complete component and pass from parent
- Integrate RemissionStartService.assignPackage() in completion flow
- Add assignPackageOnly flag to conditionally hide step counter in dialog
- Update dialog data structure to support direct package assignment mode
- Enhance test coverage for new assignment scenarios

This ensures all completed return receipts have proper package tracking
and improves the user workflow by guiding them through required steps.

Ref: #5289
2025-09-03 13:15:32 +00:00
Nino Righi
708ec01704 Merged PR 1931: fix(remission-quantity-and-reason-item)
fix(remission-quantity-and-reason-item)
Ref: #5292
2025-09-02 15:20:44 +00:00
Nino Righi
332699ca74 Merged PR 1930: fix(remission-quantity-and-reason-item): correct quantity input binding and d...
fix(remission-quantity-and-reason-item): correct quantity input binding and default value

Fix quantity input field binding to use computed quantity signal instead of
direct quantityAndReason().quantity, ensuring proper display of undefined
values as empty field. Update initial quantity default from 1 to 0 to
prevent pre-filled values when creating new quantity/reason items.

Also improves placeholder text color contrast by changing from neutral-200
to neutral-500 for better accessibility.

Ref: #5292
2025-09-02 15:20:14 +00:00
Nino
3b0a63a53a fix(remission-data-access, remission-list, remission-add-item-flow): enforce mandatory list type for add-item flow
Remove addToDepartmentList method and ensure items added via search dialog
are always processed as mandatory remission items (ReturnItem) instead of
department suggestions (ReturnSuggestion). This prevents items from being
incorrectly added to department overflow lists when remission is already
started, maintaining data consistency in the WBS system.

Changes:
- Remove addToDepartmentList method from RemissionSearchService
- Update remitItems to use mandatory list type for add-item flow
- Simplify addToRemiList to only use mandatory remission endpoint
- Add addItemFlow parameter to control remission list type behavior

Refs: #4768, #5273, #5280
2025-09-02 14:40:26 +02:00
Nino Righi
327fdc745d Merged PR 1929: fix(remission-quantity-reason): correct dropdown placeholder and remove hardc...
fix(remission-quantity-reason): correct dropdown placeholder and remove hardcoded option

Change dropdown placeholder from "Rückgabegrund" to "Remigrund" for consistency
with application terminology. Remove hardcoded test option from reason dropdown
that was polluting the dropdown list. Extract initial item object to class
property for better maintainability and reusability.

Ref: #5293, #5299
2025-09-01 16:24:54 +00:00
Nino Righi
297ec9100d Merged PR 1928: fix(remission-filter-label): improve filter button label display and default text
fix(remission-filter-label): improve filter button label display and default text

Add default placeholder text "Abteilung auswählen" when no departments
are selected and implement dynamic label width to prevent text truncation.
The label now expands to full width when displaying placeholder text
and constrains to 8rem when showing selected values.

Ref: #5303
2025-09-01 16:24:23 +00:00
Nino Righi
298ab1acbe Merged PR 1927: fix(remission-data-access): remove automatic date defaulting in fetchRemissio...
fix(remission-data-access): remove automatic date defaulting in fetchRemissionReturnReceipts

Remove the automatic default of 7 days ago when no start date is provided
to the fetchRemissionReturnReceipts method. The service now passes the
start parameter directly to the API without modification, allowing the
API or schema to handle date defaults as intended.

This change improves the separation of concerns by moving date handling
logic out of the service layer and updates the corresponding test to
handle both defined and undefined start date scenarios.

Ref: #5256
2025-09-01 16:24:06 +00:00
Nino Righi
fe77a0ea8b Merged PR 1926: feat(libs-ui-dialog-feedback-dialog): add auto-close functionality with confi...
feat(libs-ui-dialog-feedback-dialog): add auto-close functionality with configurable delay

Implement automatic dialog closure after a configurable delay period.
The dialog now auto-closes by default after 1500ms, with options to
disable auto-close or customize the delay duration through the
FeedbackDialogData interface.

- Add autoClose and autoCloseDelay properties to FeedbackDialogData
- Implement auto-close logic using RxJS asapScheduler in constructor
- Add comprehensive test coverage for auto-close behavior
- Update JSDoc documentation for better clarity

Ref: #5297
2025-09-01 15:01:21 +00:00
Nino Righi
48f588f53b Merged PR 1925: fix(remission-shared-search-item-to-remit-dialog): display context-aware feed...
fix(remission-shared-search-item-to-remit-dialog): display context-aware feedback message

Update feedback dialog message to reflect the current remission state.
Shows "Wurde zum Warenbegleitschein hinzugefügt" when remission is already
started, otherwise shows "Wurde zur Remi Liste hinzugefügt".

This provides users with more accurate feedback about where their items
were added based on the current workflow state.

Ref: #5300
2025-09-01 15:00:54 +00:00
Nino Righi
7f4af304ac Merged PR 1924: feat(isa-app-shell): improve navigation link targeting for remission sub-routes
feat(isa-app-shell): improve navigation link targeting for remission sub-routes

Replace generic routerLinkActive with specific regex patterns for remission
navigation items to ensure accurate active state highlighting. This change:

- Uses sharedRegexRouterLinkActive for "Remission" sub-item to match specific routes
- Uses sharedRegexRouterLinkActive for "Warenbegleitscheine" sub-item
- Replaces broad routerLinkActive with precise regex patterns
- Ensures navigation accurately reflects current route state for remission workflows

The regex patterns specifically target `/[tabId]/remission/(mandatory|department)`
and `/[tabId]/remission/return-receipt` routes for better user experience.

Ref: #5304
2025-09-01 15:00:22 +00:00
Nino Righi
643b2b0e60 Merged PR 1923: feat(remission): remove Koerperlos remission list type
feat(remission): remove Koerperlos remission list type

Remove the 'Körperlose Remi' option from remission list types as it's no longer needed. This simplifies the remission type selection by:

- Removing Koerperlos from RemissionListType constant
- Eliminating disabled state logic in dropdown component
- Removing special handling in changeRemissionType method
- Fixing label text from 'Abteilungen' to 'Abteilung' for consistency

The dropdown now only shows the two active remission types: Pflichtremission and Abteilungsremission.

Ref: #5303
2025-09-01 14:59:57 +00:00
Nino Righi
cd1ff5f277 Merged PR 1922: feat(libs-shared-product-format): remove truncate class from format detail text
feat(libs-shared-product-format): remove truncate class from format detail text

Remove the 'truncate' class from the span element containing formatDetail()
to allow full text display without text clipping. This improves readability
when product format details contain longer descriptions.

Ref: #5301
2025-09-01 14:59:28 +00:00
Nino Righi
46c70cae3e Merged PR 1921: feat(remission-list-resource): remove client-side date sorting in favor of ba...
feat(remission-list-resource): remove client-side date sorting in favor of backend sorting

Remove commented-out date sorting logic from sortResponseResult function.
The sorting by creation date for Pflichtremission and by SORT number
for Abteilungsremission is now handled entirely by the backend,
eliminating the need for client-side date sorting operations.

This change improves performance by reducing client-side processing
and ensures consistent sorting behavior across the application.

Ref: #5295
2025-09-01 14:59:07 +00:00
Nino
d2dcf638e3 Merge tag '4.0-hotfix-release' 2025-08-14 16:47:48 +02:00
Nino
a4241cbd7a Merge branch 'release/4.0' 2025-08-14 16:40:46 +02:00
Nino Righi
dd3705f8bc Merged PR 1920: feat(remission-list): add navigation to default list and clear input on reload
feat(remission-list): add navigation to default list and clear input on reload

Add Router injection and injectTabId for navigation functionality.
Implement navigateToDefaultRemissionList method to redirect users
to the default remission list using the current activated tab ID.
Clear query input on reload trigger to prevent stale search results
from persisting across navigation.

Enhance emptySearchResultEffect to handle department list navigation
when no items are found and remission hasn't started yet.

Ref: #5273
2025-08-14 14:06:20 +00:00
Nino Righi
514715589b Merged PR 1919: feat(remission): add impediment management and UI enhancements for remission...
feat(remission): add impediment management and UI enhancements for remission list

Implement comprehensive impediment handling for return items and suggestions
with enhanced user interface components and improved data access layer.

Key additions:
- Add impediment update schema and validation for return items
- Implement RemissionReturnReceiptService with full CRUD operations
- Create RemissionListItemComponent with actions and selection capabilities
- Add ProductInfoComponent with responsive layout and labeling
- Enhance UI Dialog system with improved injection patterns and testing
- Add comprehensive test coverage for all new components and services
- Implement proper data attributes for E2E testing support

Technical improvements:
- Follow SOLID principles with clear separation of concerns
- Use OnPush change detection strategy for optimal performance
- Implement proper TypeScript typing with Zod schema validation
- Add comprehensive JSDoc documentation for all public APIs
- Use modern Angular signals and computed properties for state management

Refs: #5275, #5038
2025-08-14 14:05:01 +00:00
Nino Righi
0740273dbc Merged PR 1917: feat(remission-data-access): enhance stock calculation to handle zero predefi...
feat(remission-data-access): enhance stock calculation to handle zero predefined quantities

Improve calculateStockToRemit and getStockToRemit functions to properly distinguish
between undefined and zero predefined return quantities. When predefinedReturnQuantity
is undefined, the system now falls back to approximation calculation (availableStock
minus remainingQuantityInStock). When predefinedReturnQuantity is explicitly set to 0,
the system respects this backend-calculated value.

Add comprehensive test coverage for edge cases including:
- Zero predefined return quantities for both Pflicht and Abteilung types
- Negative approximation calculations (clamped to 0)
- Null/undefined remainingQuantityInStock handling
- Missing returnItem scenarios for Abteilung type

Ref: #5280
2025-08-13 13:39:18 +00:00
Nino Righi
bbb9c5d39c Merged PR 1918: feat(libs-ui-label, remission-shared-product, storybook): add UI label compon...
feat(libs-ui-label, remission-shared-product, storybook): add UI label component for remission tags

- Create new @isa/ui/label library with primary/secondary appearances
- Integrate label component into ProductInfoComponent to display remission tags (Prio 1, Prio 2, Pflicht)
- Add conditional rendering based on RemissionItemTags enum with proper appearance mapping
- Include comprehensive unit tests using Vitest and Angular Testing Utilities
- Add Storybook stories for both label component and updated product info component
- Import label styles in main tailwind.scss

Ref: #5268
2025-08-13 13:38:13 +00:00
Nino Righi
f0bd957a07 Merged PR 1914: Rückmerge Release/4.0 -> Develop
Rückmerge Release/4.0 -> Develop
2025-08-13 09:52:15 +00:00
Nino Righi
e4f289c67d Merged PR 1915: fix(remission-list-resource): only apply default sorting when no orderBy spec...
fix(remission-list-resource): only apply default sorting when no orderBy specified

Replace default sort mechanism to respect explicit orderBy from QueryToken.
Previously, the resource always applied manual sorting regardless of whether
explicit ordering was requested, causing conflicts with user-defined sorting.

Now checks if queryToken.orderBy exists and has items before applying the
default sort behavior (manually-added items first, then by created date).

Refs: #5276
2025-08-13 09:51:25 +00:00
Nino Righi
2af16d92ea Merged PR 1916: fix(remission-helpers, remission-list-item): fix predefinedReturnQuantity han...
fix(remission-helpers, remission-list-item): fix predefinedReturnQuantity handling and enhance stock validation

Fix issue where predefinedReturnQuantity value of 0 was being treated differently
from undefined in mandatory remission (Pflichtremission). Now both 0 and undefined
are handled consistently by changing the initial value to undefined and using
truthy check instead of strict undefined comparison.

Additionally enhance hasStockToRemit validation by requiring both availableStock
and stockToRemit to be greater than 0, preventing invalid remission states when
no stock is available.

Changes:
- Change predefinedReturnQuantity initial value from 0 to undefined in getStockToRemit
- Remove nullish coalescing operator that forced 0 default for predefinedReturnQuantity
- Update calculateStockToRemit to use truthy check (!predefinedReturnQuantity)
  instead of strict undefined comparison
- Enhance hasStockToRemit computed property to validate both availableStock > 0
  and stockToRemit > 0
- Add comprehensive test coverage for all hasStockToRemit edge cases including
  negative values and zero combinations

Ref: #5269
2025-08-13 09:50:18 +00:00
Nino Righi
99e8e7cfe0 Merged PR 1913: feat(remission): refactor return receipt details and extract shared actions
feat(remission): refactor return receipt details and extract shared actions

Refactor remission return receipt details to use return-based data flow
instead of individual receipt fetching. Extract reusable action components
for better code organization and consistency.

- Remove deprecated fetchRemissionReturnReceipt method and schema
- Add helper functions for extracting data from return objects
- Replace receipt-specific components with return-based equivalents
- Create shared return-receipt-actions library with reusable components
- Update components to use modern Angular patterns (signals, computed)
- Improve data flow consistency across remission features
- Add comprehensive test coverage for new components
- Update eager loading support in fetch return functionality

The new architecture provides better data consistency and reduces
code duplication by centralizing receipt actions and data extraction
logic into reusable components.

Refs: #5242, #5138, #5232, #5241
2025-08-12 13:32:57 +00:00
Nino Righi
ac728f2dd9 Merged PR 1912: hotfix(isa-app-ui/shared-searchbox): improve component initialization and met...
hotfix(isa-app-ui/shared-searchbox): improve component initialization and method safety

Enhance searchbox component reliability by addressing initialization
issues and improving method safety across both shared and ui implementations.

Key changes:
- Fix potential null reference errors in cancel search functionality
- Improve method parameter typing with explicit defaults
- Add proper initialization for ControlValueAccessor callbacks
- Enhance component property initialization with explicit types
- Add hintCleared output event for better hint management

These changes resolve runtime errors and improve type safety
for the searchbox components used throughout the application.

Refs: #5245
2025-08-07 17:55:25 +00:00
Nino
2e012a124a chore(package-lock): Update Package Lock JSON 2025-08-07 14:21:57 +02:00
Nino Righi
d22e320294 Merged PR 1910: feat(remission-list, ui-tooltip): add info tooltip with performance optimization
feat(remission-list, ui-tooltip): add info tooltip with performance optimization

Add tooltip to department capacity info button with enhanced trigger management.
Optimize department list fetching to only load when search input or department
filter is active, improving initial load performance.

- Add tooltip directive to info button showing capacity details
- Implement conditional department list fetching based on input/filter presence
- Enhance tooltip directive with improved trigger management and positioning
- Update tooltip component to use modern Angular control flow syntax
- Add proper show/hide logic with trigger-specific behavior

Refs: #5255
2025-08-06 16:02:27 +00:00
Nino Righi
a0f24aac17 Merged PR 1909: fix(remission-data-access, remission-product-stock-info): improve stock infor...
fix(remission-data-access, remission-product-stock-info): improve stock information display and data handling

Enhance product stock info component with proper loading states.

- Add stockFetching input to ProductStockInfoComponent for loading states
- Update remission list components to properly handle stock fetching state
- Enhance type safety and documentation for better maintainability

The RemissionSearchService now provides clear documentation for all
methods including fetchList, fetchQuerySettings, and capacity fetching
operations. The ProductStockInfoComponent now properly displays loading
states during stock data retrieval.

Ref: #5243
2025-08-06 16:01:10 +00:00
Nino Righi
7ae484fc83 Merged PR 1908: feat(remission-shared-dialog): add dynamic dropdown label for return reason s...
feat(remission-shared-dialog): add dynamic dropdown label for return reason selection

Implement computed property to show selected reason value or default placeholder
text in the dropdown label. This provides better UX by displaying the current
selection instead of a static label.

- Add dropdownLabel computed property that returns selected reason or fallback
- Update template to use dynamic label binding instead of hardcoded text
- Enhances user feedback when reason is selected vs. when no selection is made

Ref: #5253
2025-08-06 15:58:44 +00:00
Nino Righi
0dcb31973f Merged PR 1907: feat(remission-list-item, ui-dialog): enhance quantity dialog with original v...
feat(remission-list-item, ui-dialog): enhance quantity dialog with original value display

Add support for displaying original remission quantity in the quantity change dialog.
This provides better context for users when modifying remission quantities by showing
both the current input and the original calculated value.

Changes:
- Add subMessage and subMessageValue inputs to NumberInputComponent and dialog interfaces
- Update RemissionListItemActionsComponent to pass original quantity context to dialog
- Modify RemissionListItemComponent to track quantity differences and pass stockToRemit value
- Add selectedQuantityDiffersFromStockToRemit computed property for UI state management
- Update component templates to display contextual information in quantity dialogs

Ref: #5204
2025-08-06 15:58:10 +00:00
Nino Righi
c2f393d249 Merged PR 1911: hotfix(isa-app-store, core-storage): prevent caching of erroneous user state
hotfix(isa-app-store, core-storage): prevent caching of erroneous user state

Remove shareReplay(1) operator from user state observable to ensure
fresh state retrieval on each request. This prevents the system from
retaining and reusing failed or invalid state data across multiple
operations.

The current implementation now makes two API calls (GET + POST) per
set operation to guarantee the latest state is always used, trading
performance for reliability in error scenarios.

Refs: #5270, #5249
2025-08-06 15:47:49 +00:00
Nino Righi
2dbf7dda37 Merged PR 1906: feat(remission-data-access, remission-start-dialog): refactor remission workf...
feat(remission-data-access, remission-start-dialog): refactor remission workflow to use createRemission API

Replace the startRemission method with separate createRemission and assignPackage operations.
The new implementation improves error handling and provides better separation of concerns
between return creation and package assignment steps.

Key changes:
- Add CreateRemission interface to models with support for validation error properties
- Replace startRemission with createRemission method that handles return and receipt creation
- Update service methods to return ResponseArgs objects with proper error handling
- Enhance dialog components with reactive error handling using Angular effects
- Add comprehensive server-side validation error display in form controls
- Separate package assignment into dedicated step with individual loading states
- Improve test coverage with proper mocking of new service methods

The refactored workflow provides better user feedback for validation errors and maintains
the existing two-step process while improving maintainability and error handling.

Ref: #5251
2025-08-05 10:42:45 +00:00
Nino Righi
cce15a2137 Merged PR 1905: feat(remission-data-access, remission-list-item): add remission item source t...
feat(remission-data-access, remission-list-item): add remission item source tracking and delete functionality

Add comprehensive remission item source management with the ability to delete
manually added items from return receipts. Introduces new RemissionItemSource
model to track item origins and refactors remission list item components for
better action management.

Key changes:
- Add RemissionItemSource model with 'manually-added' and 'DisposalListModule' types
- Extend ReturnItem and ReturnSuggestion interfaces with source property
- Implement deleteReturnItem service method with comprehensive error handling
- Create RemissionListItemActionsComponent for managing item-specific actions
- Add conditional display logic for delete buttons based on item source
- Refactor RemissionListItemSelectComponent with hasStockToRemit input validation
- Add deleteRemissionListItemInProgress state management across components
- Include comprehensive test coverage for new delete functionality

This enhancement enables users to remove manually added items from remission
lists while preserving system-generated entries, improving workflow flexibility
and data integrity.

Ref: 5259
2025-08-04 11:31:05 +00:00
Nino Righi
14a5a67a1e Merged PR 1904: feat(utils-ean-validation, remission-list): add EAN validation library and im...
feat(utils-ean-validation, remission-list): add EAN validation library and implement exact search

Create new EAN validation utility library with validator function and isEan helper.
Implement exact search functionality for remission lists that bypasses filters
when scanning EAN codes or performing exact searches.

Changes:
- Add new utils/ean-validation library with EAN regex validation
- Export eanValidator for Angular reactive forms integration
- Export isEan utility function for EAN validation checks
- Configure library with Vitest for testing
- Update remission list resource to support exact search mode
- Clear filters and orderBy when performing EAN-based searches
- Add data attributes to product info component for E2E testing

Ref: #5128
2025-08-01 13:22:41 +00:00
Nino Righi
0addf392b6 Merged PR 1901: hotfix(return-summary): disable navigation during return processing
hotfix(return-summary): disable navigation during return processing

Replace Router navigation with Location.back() for better UX and add
disabled states to prevent user actions during pending operations.

Changes:
- Replace navigateBack() method with direct Location.back() calls
- Add returnItemsAndPrintReciptPending input to ReturnSummaryItemComponent
- Disable edit and back buttons when return operation is pending
- Update parent component to pass pending state to child components
- Fix template binding to use computed pending status signal

This prevents users from navigating away during critical return
operations and provides consistent disabled states across the UI.

Ref: #5257
2025-07-31 16:41:59 +00:00
369 changed files with 48856 additions and 39662 deletions

185
.github/prompts/plan.prompt.md vendored Normal file
View File

@@ -0,0 +1,185 @@
---
mode: agent
tools: [ 'edit', 'search', 'usages', 'vscodeAPI', 'problems', 'changes', 'fetch', 'githubRepo', 'Nx Mcp Server', 'context7' ]
description: Plan Mode - Research and create a detailed implementation plan before making any changes.
---
# Plan Mode
You are now operating in **Plan Mode** - a research and planning phase that ensures thorough analysis before implementation. Plan mode is **ALWAYS ACTIVE** when using this prompt. You must follow these strict guidelines for every request:
## Phase 1: Research & Analysis (MANDATORY)
### ALLOWED Operations:
- ✅ Read files using Read, Glob, Grep tools
- ✅ Search documentation and codebases
- ✅ Analyze existing patterns and structures
- ✅ Use WebFetch for documentation research
- ✅ List and explore project structure
- ✅ Use Nx/Angular/Context7 MCP tools for workspace analysis
- ✅ Review dependencies and configurations
### FORBIDDEN Operations:
-**NEVER** create, edit, or modify any files
-**NEVER** run commands that change system state
-**NEVER** make commits or push changes
-**NEVER** install packages or modify configurations
-**NEVER** run build/test commands during planning
## Phase 2: Plan Presentation (REQUIRED FORMAT)
After thorough research, present your plan using this exact structure:
```markdown
## 📋 Implementation Plan
### 🎯 Objective
[Clear statement of what will be accomplished]
### 🔍 Research Summary
- **Current State**: [What exists now]
- **Requirements**: [What needs to be built/changed]
- **Constraints**: [Limitations and considerations]
### 📁 Files to be Modified/Created
1. **File**: `path/to/file.ts`
- **Action**: Create/Modify/Delete
- **Purpose**: [Why this file needs changes]
- **Key Changes**: [Specific modifications planned]
2. **File**: `path/to/another-file.ts`
- **Action**: Create/Modify/Delete
- **Purpose**: [Why this file needs changes]
- **Key Changes**: [Specific modifications planned]
### 🏗️ Implementation Steps
1. **Step 1**: [Detailed description]
- Files affected: `file1.ts`, `file2.ts`
- Rationale: [Why this step is necessary]
2. **Step 2**: [Detailed description]
- Files affected: `file3.ts`
- Rationale: [Why this step is necessary]
3. **Step N**: [Continue numbering...]
### ⚠️ Risks & Considerations
- **Risk 1**: [Potential issue and mitigation]
- **Risk 2**: [Potential issue and mitigation]
### 🧪 Testing Strategy
- [How the changes will be tested]
- [Specific test files or approaches]
### 📚 Architecture Decisions
- **Pattern Used**: [Which architectural pattern will be followed]
- **Libraries/Dependencies**: [What will be used and why]
- **Integration Points**: [How this fits with existing code]
### ✅ Success Criteria
- [ ] Criterion 1
- [ ] Criterion 2
- [ ] All tests pass
- [ ] No lint errors
```
## Phase 3: Await Approval
After presenting the plan:
1. **STOP** all implementation activities
2. **WAIT** for explicit user approval
3. **DO NOT** proceed with any file changes
4. **RESPOND** to questions or plan modifications
5. **EXIT PLAN MODE** only when user explicitly says "execute", "implement", "go ahead", "approved", or similar approval language
## Phase 4: Implementation (After Exiting Plan Mode)
Once the user explicitly approves and you exit plan mode:
1. **PLAN MODE IS NOW DISABLED** - you can proceed with normal implementation
2. Use TodoWrite to create implementation todos
3. Follow the plan step-by-step
4. Update todos as you progress
5. Run tests and lint checks as specified
6. Provide progress updates
## Key Behavioral Rules
### Research Thoroughly
- Spend significant time understanding the codebase
- Look for existing patterns to follow
- Identify all dependencies and integration points
- Consider edge cases and error scenarios
### Be Comprehensive
- Plans should be detailed enough for another developer to implement
- Include all necessary file changes
- Consider testing, documentation, and deployment
- Address potential conflicts or breaking changes
### Show Your Work
- Explain reasoning behind architectural decisions
- Reference existing code patterns when applicable
- Cite documentation or best practices
- Provide alternatives when multiple approaches exist
### Safety First
- Never make changes during planning phase
- Always wait for explicit approval
- Flag potentially risky changes
- Suggest incremental implementation when complex
## Example Interactions
### Good Plan Mode Behavior:
```
User: "Add a dark mode toggle to the settings page"
Assistant: I'll research the current theming system and create a comprehensive plan for implementing dark mode.
[Extensive research using Read, Grep, Glob tools]
## 📋 Implementation Plan
[Follows complete format above]
Ready to proceed? Please approve this plan before I begin implementation.
```
### What NOT to do:
```
User: "Add a dark mode toggle"
Assistant: I'll add that right away!
[Immediately starts editing files - WRONG!]
```
## Integration with Existing Copilot Instructions
This plan mode respects all existing project patterns:
- Follows Angular + Nx workspace conventions
- Uses existing import path aliases
- Respects testing strategy (Jest/Vitest)
- Follows NgRx Signals patterns
- Adheres to logging and configuration patterns
- Maintains library conventions and file naming
Remember: **RESEARCH FIRST, PLAN THOROUGHLY, WAIT FOR APPROVAL, THEN IMPLEMENT**

1
.gitignore vendored
View File

@@ -75,3 +75,4 @@ vitest.config.*.timestamp*
.memory.json
nx.instructions.md
CLAUDE.md

192
.vscode/settings.json vendored
View File

@@ -1,92 +1,100 @@
{
"editor.accessibilitySupport": "off",
"typescript.tsdk": "node_modules/typescript/lib",
"exportall.config.exclude": [".test.", ".spec.", ".stories."],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.validate": [
"json"
],
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"exportall.config.folderListener": [
"/libs/oms/data-access/src/lib/models",
"/libs/oms/data-access/src/lib/schemas",
"/libs/catalogue/data-access/src/lib/models",
"/libs/common/data-access/src/lib/models",
"/libs/common/data-access/src/lib/error",
"/libs/oms/data-access/src/lib/errors/return-process"
],
"github.copilot.chat.commitMessageGeneration.instructions": [
{
"file": ".github/commit-instructions.md"
}
],
"github.copilot.chat.codeGeneration.instructions": [
{
"file": ".vscode/llms/angular.txt"
},
{
"file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"
},
{
"file": "docs/guidelines/project-structure.md"
},
{
"file": "docs/guidelines/state-management.md"
},
{
"file": "docs/guidelines/testing.md"
}
],
"github.copilot.chat.testGeneration.instructions": [
{
"file": ".github/testing-instructions.md"
},
{
"file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"
},
{
"file": "docs/guidelines/testing.md"
}
],
"github.copilot.chat.reviewSelection.instructions": [
{
"file": ".github/copilot-instructions.md"
},
{
"file": ".github/review-instructions.md"
},
{
"file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"
},
{
"file": "docs/guidelines/project-structure.md"
},
{
"file": "docs/guidelines/state-management.md"
},
{
"file": "docs/guidelines/testing.md"
}
],
"nxConsole.generateAiAgentRules": true,
"chat.mcp.enabled": true,
"chat.mcp.discovery.enabled": true
}
{
"editor.accessibilitySupport": "off",
"typescript.tsdk": "node_modules/typescript/lib",
"exportall.config.exclude": [".test.", ".spec.", ".stories."],
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"eslint.validate": [
"json"
],
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.formatOnSave": false
},
"exportall.config.folderListener": [
"/libs/oms/data-access/src/lib/models",
"/libs/oms/data-access/src/lib/schemas",
"/libs/catalogue/data-access/src/lib/models",
"/libs/common/data-access/src/lib/models",
"/libs/common/data-access/src/lib/error",
"/libs/oms/data-access/src/lib/errors/return-process"
],
"github.copilot.chat.commitMessageGeneration.instructions": [
{
"file": ".github/commit-instructions.md"
}
],
"github.copilot.chat.codeGeneration.instructions": [
{
"file": ".vscode/llms/angular.txt"
},
{
"file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"
},
{
"file": "docs/guidelines/project-structure.md"
},
{
"file": "docs/guidelines/state-management.md"
},
{
"file": "docs/guidelines/testing.md"
}
],
"github.copilot.chat.testGeneration.instructions": [
{
"file": ".github/testing-instructions.md"
},
{
"file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"
},
{
"file": "docs/guidelines/testing.md"
}
],
"github.copilot.chat.reviewSelection.instructions": [
{
"file": ".github/copilot-instructions.md"
},
{
"file": ".github/review-instructions.md"
},
{
"file": "docs/tech-stack.md"
},
{
"file": "docs/guidelines/code-style.md"
},
{
"file": "docs/guidelines/project-structure.md"
},
{
"file": "docs/guidelines/state-management.md"
},
{
"file": "docs/guidelines/testing.md"
}
],
"nxConsole.generateAiAgentRules": true,
"chat.mcp.discovery.enabled": {
"claude-desktop": true,
"windsurf": true,
"cursor-global": true,
"cursor-workspace": true
},
"chat.mcp.access": "all"
}

148
CLAUDE.md Normal file
View File

@@ -0,0 +1,148 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is an Angular monorepo managed by Nx. The main application is `isa-app`, which appears to be an inventory and returns management system for retail/e-commerce.
## Architecture
### Monorepo Structure
- **apps/isa-app**: Main Angular application
- **libs/**: Reusable libraries organized by domain and type
- **core/**: Core utilities (config, logging, storage, tabs)
- **common/**: Shared utilities (data-access, decorators, print)
- **ui/**: UI component libraries (buttons, dialogs, inputs, etc.)
- **shared/**: Shared domain components (filter, scanner, product components)
- **oms/**: Order Management System features and utilities
- **remission/**: Remission/returns management features
- **catalogue/**: Product catalogue functionality
- **utils/**: General utilities (validation, scroll position, parsing)
- **icons/**: Icon library
- **generated/swagger/**: Auto-generated API client code from OpenAPI specs
### Key Architectural Patterns
- **Standalone Components**: Project uses Angular standalone components
- **Feature Libraries**: Domain features organized as separate libraries (e.g., `oms-feature-return-search`)
- **Data Access Layer**: Separate data-access libraries for each domain (e.g., `oms-data-access`, `remission-data-access`)
- **Shared UI Components**: Reusable UI components in `libs/ui/`
- **Generated API Clients**: Swagger/OpenAPI clients auto-generated in `generated/swagger/`
## Common Development Commands
### Build Commands
```bash
# Build the main application (development)
npx nx build isa-app --configuration=development
# Build for production
npx nx build isa-app --configuration=production
# Serve the application with SSL
npx nx serve isa-app --ssl
```
### Testing Commands
```bash
# Run tests for a specific library (always use --skip-cache)
npx nx run <project-name>:test --skip-cache
# Example: npx nx run remission-data-access:test --skip-cache
# Run tests for all libraries except the main app
npx nx run-many -t test --exclude isa-app --skip-cache
# Run a single test file
npx nx run <project-name>:test --testFile=<path-to-test-file> --skip-cache
# Run tests with coverage
npx nx run <project-name>:test --code-coverage --skip-cache
# Run tests in watch mode
npx nx run <project-name>:test --watch
```
### Linting Commands
```bash
# Lint a specific project
npx nx lint <project-name>
# Example: npx nx lint remission-data-access
# Run linting for all projects
npx nx run-many -t lint
```
### Other Useful Commands
```bash
# Generate Swagger API clients
npm run generate:swagger
# Start Storybook
npx nx run isa-app:storybook
# Format code with Prettier
npm run prettier
# List all projects in the workspace
npx nx list
# Show project dependencies graph
npx nx graph
# Run affected tests (based on git changes)
npx nx affected:test
```
## Testing Framework
### Current Setup
- **Jest**: Primary test runner for existing libraries
- **Vitest**: Being adopted for new libraries (migration in progress)
- **Testing Utilities**:
- **Angular Testing Utilities** (TestBed, ComponentFixture): Use for new tests
- **Spectator**: Legacy testing utility for existing tests
- **ng-mocks**: For advanced mocking scenarios
### Test File Requirements
- Test files must end with `.spec.ts`
- Use AAA pattern (Arrange-Act-Assert)
- Include E2E testing attributes (`data-what`, `data-which`) in HTML templates
- Mock external dependencies and child components
## State Management
- **NgRx**: Store, Effects, Entity, Component Store, Signals
- **RxJS**: For reactive programming patterns
## Styling
- **Tailwind CSS**: Primary styling framework with custom configuration
- **SCSS**: For component-specific styles
- **Custom Tailwind plugins**: For buttons, inputs, menus, typography
## API Integration
- **Generated Swagger Clients**: Auto-generated TypeScript clients from OpenAPI specs
- **Available APIs**: availability, cat-search, checkout, crm, eis, inventory, isa, oms, print, wws
## Build Configuration
- **Angular 20.1.2**: Latest Angular version
- **TypeScript 5.8.3**: For type safety
- **Node.js >= 22.0.0**: Required Node version
- **npm >= 10.0.0**: Required npm version
## Important Conventions
- **Component Prefix**: Each library has its own prefix (e.g., `remi` for remission, `oms` for OMS)
- **Standalone Components**: All new components should be standalone
- **Path Aliases**: Use TypeScript path aliases defined in `tsconfig.base.json` (e.g., `@isa/core/config`)
- **Project Names**: Can be found in each library's `project.json` file
## Development Workflow Tips
- Always use `npx nx run` pattern for executing tasks
- Include `--skip-cache` flag when running tests to ensure fresh results
- Use Nx's affected commands to optimize CI/CD pipelines
- Project graph visualization helps understand dependencies: `npx nx graph`
## Development Notes
- Use start target to start the application. Only one project can be started: isa-app
- Make sure to have a look at @docs/guidelines/testing.md before writing tests
- Make sure to add e2e attributes to the html. Those are important for my colleagues writen e2e tests
- Guide for the e2e testing attributes can be found in the testing.md
- When reviewing code follow the instructions @.github/review-instructions.md

View File

@@ -100,7 +100,8 @@
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/isa-app/jest.config.ts"
"jestConfig": "apps/isa-app/jest.config.ts",
"tsConfig": "apps/isa-app/tsconfig.spec.json"
}
},
"serve-static": {

View File

@@ -185,6 +185,11 @@ const routes: Routes = [
resolve: { process: tabResolverFn, tab: tabResolverFn },
canActivate: [IsAuthenticatedGuard],
children: [
{
path: 'reward',
loadChildren: () =>
import('@isa/checkout/feature/reward-catalog').then((m) => m.routes),
},
{
path: 'return',
loadChildren: () =>

View File

@@ -1,18 +1,18 @@
import { Injectable } from '@angular/core';
import { Logger, LogLevel } from '@core/logger';
import { Store } from '@ngrx/store';
import { debounceTime, switchMap, takeUntil } from 'rxjs/operators';
import { RootState } from './root.state';
import packageInfo from 'packageJson';
import { environment } from '../../environments/environment';
import { Subject } from 'rxjs';
import { AuthService } from '@core/auth';
import { injectStorage, UserStorageProvider } from '@isa/core/storage';
import { isEqual } from 'lodash';
import { Injectable } from "@angular/core";
import { Logger, LogLevel } from "@core/logger";
import { Store } from "@ngrx/store";
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
import { RootState } from "./root.state";
import packageInfo from "packageJson";
import { environment } from "../../environments/environment";
import { Subject } from "rxjs";
import { AuthService } from "@core/auth";
import { injectStorage, UserStorageProvider } from "@isa/core/storage";
import { isEqual } from "lodash";
@Injectable({ providedIn: 'root' })
@Injectable({ providedIn: "root" })
export class RootStateService {
static LOCAL_STORAGE_KEY = 'ISA_APP_INITIALSTATE';
static LOCAL_STORAGE_KEY = "ISA_APP_INITIALSTATE";
#storage = injectStorage(UserStorageProvider);
@@ -29,14 +29,17 @@ export class RootStateService {
);
}
window['clearUserState'] = () => {
window["clearUserState"] = () => {
this.clear();
};
}
async init() {
await this.load();
this._store.dispatch({ type: 'HYDRATE', payload: RootStateService.LoadFromLocalStorage() });
this._store.dispatch({
type: "HYDRATE",
payload: RootStateService.LoadFromLocalStorage(),
});
this.initSave();
}
@@ -50,14 +53,10 @@ export class RootStateService {
const data = {
...state,
version: packageInfo.version,
sub: this._authService.getClaimByKey('sub'),
sub: this._authService.getClaimByKey("sub"),
};
RootStateService.SaveToLocalStorageRaw(JSON.stringify(data));
return this.#storage.set('state', {
...state,
version: packageInfo.version,
sub: this._authService.getClaimByKey('sub'),
});
return this.#storage.set("state", data);
}),
)
.subscribe();
@@ -68,7 +67,7 @@ export class RootStateService {
*/
async load(): Promise<boolean> {
try {
const res = await this.#storage.get('state');
const res = await this.#storage.get("state");
const storageContent = RootStateService.LoadFromLocalStorageRaw();
@@ -88,7 +87,7 @@ export class RootStateService {
async clear() {
try {
this._cancelSave.next();
await this.#storage.clear('state');
await this.#storage.clear("state");
await new Promise((resolve) => setTimeout(resolve, 100));
RootStateService.RemoveFromLocalStorage();
await new Promise((resolve) => setTimeout(resolve, 100));
@@ -112,7 +111,7 @@ export class RootStateService {
try {
return JSON.parse(raw);
} catch (error) {
console.error('Error parsing local storage:', error);
console.error("Error parsing local storage:", error);
this.RemoveFromLocalStorage();
}
}

View File

@@ -244,9 +244,15 @@ export class CustomerOrderSearchStore extends ComponentStore<CustomerOrderSearch
}),
// #4822 Quick Fix - HSC Bestellungen Suche wird nicht ausgelöst (Möglicherweise nicht die beste Lösung)
switchMap(
async ([options, results, filter, branch]): Promise<
[typeof options, typeof results, typeof filter, typeof branch, boolean]
async (params: [
{ clear?: boolean; siletReload?: boolean },
OrderItemListItemDTO[],
Filter,
BranchDTO
]): Promise<
[{ clear?: boolean; siletReload?: boolean }, OrderItemListItemDTO[], Filter, BranchDTO, boolean]
> => {
const [options, results, filter, branch] = params;
const queryToken = filter?.getQueryToken() ?? {};
const cached: boolean =
options?.siletReload &&

View File

@@ -16,20 +16,20 @@ import {
forwardRef,
Optional,
inject,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { UiAutocompleteComponent } from '@ui/autocomplete';
import { UiFormControlDirective } from '@ui/form-control';
import { containsElement } from '@utils/common';
import { Subscription } from 'rxjs';
import { ScanAdapterService } from '@adapter/scan';
import { injectCancelSearch } from '@shared/services/cancel-subject';
import { EnvironmentService } from '@core/environment';
} from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
import { UiAutocompleteComponent } from "@ui/autocomplete";
import { UiFormControlDirective } from "@ui/form-control";
import { containsElement } from "@utils/common";
import { Subscription } from "rxjs";
import { ScanAdapterService } from "@adapter/scan";
import { injectCancelSearch } from "@shared/services/cancel-subject";
import { EnvironmentService } from "@core/environment";
@Component({
selector: 'shared-searchbox',
templateUrl: 'searchbox.component.html',
styleUrls: ['searchbox.component.scss'],
selector: "shared-searchbox",
templateUrl: "searchbox.component.html",
styleUrls: ["searchbox.component.scss"],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@@ -49,9 +49,9 @@ export class SearchboxComponent
cancelSearch = injectCancelSearch({ optional: true });
disabled: boolean;
type = 'text';
type = "text";
@ViewChild('input', { read: ElementRef, static: true })
@ViewChild("input", { read: ElementRef, static: true })
input: ElementRef;
@ContentChild(UiAutocompleteComponent)
@@ -61,9 +61,9 @@ export class SearchboxComponent
focusAfterViewInit = true;
@Input()
placeholder = '';
placeholder = "";
private _query = '';
private _query = "";
@Input()
get query() {
@@ -94,7 +94,7 @@ export class SearchboxComponent
scanner = false;
@Input()
hint = '';
hint = "";
@Input()
autocompleteValueSelector: (item: any) => string = (item: any) => item;
@@ -104,11 +104,11 @@ export class SearchboxComponent
}
clear(): void {
this.setQuery('');
this.setQuery("");
this.cancelSearch();
}
@HostBinding('class.autocomplete-opend')
@HostBinding("class.autocomplete-opend")
get autocompleteOpen() {
return this.autocomplete?.opend;
}
@@ -213,13 +213,13 @@ export class SearchboxComponent
}
clearHint() {
this.hint = '';
this.hint = "";
this.focused.emit(true);
this.cdr.markForCheck();
}
onKeyup(event: KeyboardEvent) {
if (event.key === 'Enter') {
if (event.key === "Enter") {
if (this.autocomplete?.opend && this.autocomplete?.activeItem) {
this.setQuery(this.autocomplete?.activeItem?.item);
this.autocomplete?.close();
@@ -227,7 +227,7 @@ export class SearchboxComponent
this.search.emit(this.query);
event.preventDefault();
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
this.handleArrowUpDownEvent(event);
}
}
@@ -242,7 +242,7 @@ export class SearchboxComponent
}
}
@HostListener('window:click', ['$event'])
@HostListener("window:click", ["$event"])
focusLost(event: MouseEvent) {
if (
this.autocomplete?.opend &&
@@ -256,9 +256,11 @@ export class SearchboxComponent
this.search.emit(this.query);
}
@HostListener('focusout', ['$event'])
@HostListener("focusout", ["$event"])
onBlur() {
this.onTouched();
if (typeof this.onTouched === "function") {
this.onTouched();
}
this.focused.emit(false);
this.cdr.markForCheck();
}

View File

@@ -16,14 +16,16 @@
</a>
<div class="side-menu-group-sub-item-wrapper">
@if (customerSearchRoute$ | async; as customerSearchRoute) {
@if (
customerSearchRoute() || customerCreateRoute() || customerRewardRoute()
) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="customerSearchRoute.path"
[queryParams]="customerSearchRoute.queryParams"
[routerLink]="customerSearchRoute().path"
[queryParams]="customerSearchRoute().queryParams"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer"
sharedRegexRouterLinkActiveTest="^(\/kunde\/\d*\/customer|\/\d*\/reward)"
(isActiveChange)="customerActive($event); focusSearchBox()"
>
<span class="side-menu-group-item-icon">
@@ -32,11 +34,11 @@
<span class="side-menu-group-item-label">Kunden</span>
<button
class="side-menu-group-arrow"
[class.side-menu-item-rotate]="customerExpanded"
[class.side-menu-item-rotate]="customerExpanded()"
(click)="
$event.stopPropagation();
$event.preventDefault();
customerExpanded = !customerExpanded
toggleCustomerExpanded()
"
>
<shared-icon icon="keyboard-arrow-down"></shared-icon>
@@ -44,13 +46,16 @@
</a>
}
<div class="side-menu-group-sub-items" [class.hidden]="!customerExpanded">
@if (customerSearchRoute$ | async; as customerSearchRoute) {
<div
class="side-menu-group-sub-items"
[class.hidden]="!customerExpanded()"
>
@if (customerSearchRoute() || customerRewardRoute()) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="customerSearchRoute.path"
[queryParams]="customerSearchRoute.queryParams"
[routerLink]="customerSearchRoute().path"
[queryParams]="customerSearchRoute().queryParams"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(search|search)"
(isActiveChange)="focusSearchBox()"
@@ -59,12 +64,12 @@
<span class="side-menu-group-item-label">Suchen</span>
</a>
}
@if (customerCreateRoute$ | async; as customerCreateRoute) {
@if (customerCreateRoute() || customerRewardRoute()) {
<a
class="side-menu-group-item"
(click)="closeSideMenu()"
[routerLink]="customerCreateRoute.path"
[queryParams]="customerCreateRoute.queryParams"
[routerLink]="customerCreateRoute().path"
[queryParams]="customerCreateRoute().queryParams"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/kunde\/\d*\/customer\/(\(create|create)"
>
@@ -72,6 +77,19 @@
<span class="side-menu-group-item-label">Erfassen</span>
</a>
}
@if (customerRewardRoute()) {
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="customerRewardRoute()"
(isActiveChange)="focusSearchBox()"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/\d*\/reward"
>
<span class="side-menu-group-item-icon"> </span>
<span class="side-menu-group-item-label">Prämienshop</span>
</a>
}
</div>
</div>
@@ -95,7 +113,7 @@
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="[
'/',
processService.activatedTab()?.id || processService.nextId(),
tabService.activatedTab()?.id || tabService.nextId(),
'return',
]"
(isActiveChange)="focusSearchBox()"
@@ -289,7 +307,7 @@
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="[
'/',
processService.activatedTab()?.id || processService.nextId(),
tabService.activatedTab()?.id || tabService.nextId(),
'remission',
]"
(isActiveChange)="focusSearchBox(); remissionExpanded.set($event)"
@@ -319,11 +337,12 @@
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="[
'/',
processService.activatedTab()?.id || processService.nextId(),
tabService.activatedTab()?.id || tabService.nextId(),
'remission',
]"
(isActiveChange)="focusSearchBox()"
routerLinkActive="active"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/\d*\/remission\/(mandatory|department)"
>
<span class="side-menu-group-item-icon"> </span>
<span class="side-menu-group-item-label">Remission</span>
@@ -333,12 +352,13 @@
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="[
'/',
processService.activatedTab()?.id || processService.nextId(),
tabService.activatedTab()?.id || tabService.nextId(),
'remission',
'return-receipt',
]"
(isActiveChange)="focusSearchBox()"
routerLinkActive="active"
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/\d*\/remission\/return-receipt"
>
<span class="side-menu-group-item-icon"> </span>
<span class="side-menu-group-item-label">Warenbegleitscheine</span>

View File

@@ -1,7 +1,7 @@
import {
Component,
ChangeDetectionStrategy,
Inject,
computed,
ChangeDetectorRef,
inject,
DOCUMENT,
@@ -29,6 +29,7 @@ import {
PickUpShelfOutNavigationService,
ProductCatalogNavigationService,
} from '@shared/services/navigation';
import { toSignal } from '@angular/core/rxjs-interop';
import { TabService } from '@isa/core/tabs';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
@@ -68,7 +69,7 @@ export class ShellSideMenuComponent {
#pickUpShelfInNavigation = inject(PickupShelfInNavigationService);
#cdr = inject(ChangeDetectorRef);
#document = inject(DOCUMENT);
processService = inject(TabService);
tabService = inject(TabService);
branchKey$ = this.#stockService.StockCurrentBranch().pipe(
retry(3),
@@ -109,18 +110,32 @@ export class ShellSideMenuComponent {
}),
);
customerSearchRoute$ = this.getLastActivatedCustomerProcessId$().pipe(
map((processId) => {
return this.#customerSearchNavigation.defaultRoute({ processId });
}),
customerSearchRoute = toSignal(
this.getLastActivatedCustomerProcessId$().pipe(
map((processId) => {
return this.#customerSearchNavigation.defaultRoute({ processId });
}),
),
);
customerCreateRoute$ = this.getLastActivatedCustomerProcessId$().pipe(
map((processId) => {
return this.#customerCreateNavigation.defaultRoute({ processId });
}),
customerCreateRoute = toSignal(
this.getLastActivatedCustomerProcessId$().pipe(
map((processId) => {
return this.#customerCreateNavigation.defaultRoute({ processId });
}),
),
);
customerRewardRoute = computed(() => {
const routeName = 'reward';
const tabId = this.tabService.activatedTab()?.id;
return this.#router.createUrlTree([
'/',
tabId || this.tabService.nextId(),
routeName,
]);
});
pickUpShelfOutRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
map((processId) => {
if (processId) {
@@ -212,26 +227,25 @@ export class ShellSideMenuComponent {
}
shelfExpanded = false;
customerExpanded = false;
customerExpanded = signal(false);
remissionExpanded = signal(false);
customerActive(isActive: boolean) {
if (isActive) {
this.expandCustomer();
this.customerExpanded.set(true);
}
}
toggleCustomerExpanded() {
this.customerExpanded.set(!this.customerExpanded());
}
shelfActive(isActive: boolean) {
if (isActive) {
this.expandShelf();
}
}
expandCustomer() {
this.customerExpanded = true;
this.#cdr.markForCheck();
}
expandShelf() {
this.shelfExpanded = true;
this.#cdr.markForCheck();

View File

@@ -18,6 +18,7 @@
@import "../../../libs/ui/search-bar/src/search-bar.scss";
@import "../../../libs/ui/skeleton-loader/src/skeleton-loader.scss";
@import "../../../libs/ui/tooltip/src/tooltip.scss";
@import "../../../libs/ui/label/src/label.scss";
.input-control {
@apply rounded border border-solid border-[#AEB7C1] px-4 py-[1.125rem] outline-none;

View File

@@ -16,20 +16,20 @@ import {
forwardRef,
Optional,
inject,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { UiAutocompleteComponent } from '@ui/autocomplete';
import { UiFormControlDirective } from '@ui/form-control';
import { Subscription } from 'rxjs';
import { ScanAdapterService } from '@adapter/scan';
import { injectCancelSearch } from '@shared/services/cancel-subject';
import { containsElement } from '@utils/common';
import { EnvironmentService } from '@core/environment';
} from "@angular/core";
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms";
import { UiAutocompleteComponent } from "@ui/autocomplete";
import { UiFormControlDirective } from "@ui/form-control";
import { Subscription } from "rxjs";
import { ScanAdapterService } from "@adapter/scan";
import { injectCancelSearch } from "@shared/services/cancel-subject";
import { containsElement } from "@utils/common";
import { EnvironmentService } from "@core/environment";
@Component({
selector: 'ui-searchbox',
templateUrl: 'searchbox.component.html',
styleUrls: ['searchbox.component.scss'],
selector: "ui-searchbox",
templateUrl: "searchbox.component.html",
styleUrls: ["searchbox.component.scss"],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@@ -49,9 +49,9 @@ export class UiSearchboxNextComponent
private readonly _cancelSearch = injectCancelSearch({ optional: true });
disabled: boolean;
type = 'text';
type = "text";
@ViewChild('input', { read: ElementRef, static: true })
@ViewChild("input", { read: ElementRef, static: true })
input: ElementRef;
@ContentChild(UiAutocompleteComponent)
@@ -61,9 +61,9 @@ export class UiSearchboxNextComponent
focusAfterViewInit: boolean = true;
@Input()
placeholder: string = '';
placeholder: string = "";
private _query = '';
private _query = "";
@Input()
get query() {
@@ -94,7 +94,7 @@ export class UiSearchboxNextComponent
scanner = false;
@Input()
hint: string = '';
hint: string = "";
@Output()
hintCleared = new EventEmitter<void>();
@@ -107,11 +107,11 @@ export class UiSearchboxNextComponent
}
clear(): void {
this.setQuery('');
this.setQuery("");
this._cancelSearch();
}
@HostBinding('class.autocomplete-opend')
@HostBinding("class.autocomplete-opend")
get autocompleteOpen() {
return this.autocomplete?.opend;
}
@@ -212,14 +212,14 @@ export class UiSearchboxNextComponent
}
clearHint() {
this.hint = '';
this.hint = "";
this.focused.emit(true);
this.hintCleared.emit();
this.cdr.markForCheck();
}
onKeyup(event: KeyboardEvent) {
if (event.key === 'Enter') {
if (event.key === "Enter") {
if (this.autocomplete?.opend && this.autocomplete?.activeItem) {
this.setQuery(this.autocomplete?.activeItem?.item);
this.autocomplete?.close();
@@ -227,7 +227,7 @@ export class UiSearchboxNextComponent
this.search.emit(this.query);
event.preventDefault();
} else if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
} else if (event.key === "ArrowUp" || event.key === "ArrowDown") {
this.handleArrowUpDownEvent(event);
}
}
@@ -235,12 +235,14 @@ export class UiSearchboxNextComponent
handleArrowUpDownEvent(event: KeyboardEvent) {
this.autocomplete?.handleKeyboardEvent(event);
if (this.autocomplete?.activeItem) {
const query = this.autocompleteValueSelector(this.autocomplete.activeItem.item);
const query = this.autocompleteValueSelector(
this.autocomplete.activeItem.item,
);
this.setQuery(query, false, false);
}
}
@HostListener('window:click', ['$event'])
@HostListener("window:click", ["$event"])
focusLost(event: MouseEvent) {
if (
this.autocomplete?.opend &&
@@ -254,9 +256,11 @@ export class UiSearchboxNextComponent
this.search.emit(this.query);
}
@HostListener('focusout', ['$event'])
@HostListener("focusout", ["$event"])
onBlur() {
this.onTouched();
if (typeof this.onTouched === "function") {
this.onTouched();
}
this.focused.emit(false);
this.cdr.markForCheck();
}

View File

@@ -17,6 +17,8 @@
}
.ui-tooltip-panel {
@apply pointer-events-auto;
.triangle {
width: 30px;
polygon {

View File

@@ -17,6 +17,7 @@ import { provideRouter } from '@angular/router';
type ProductInfoInputs = {
item: ProductInfoItem;
orientation: ProductInfoOrientation;
innerGridClass: string;
};
const meta: Meta<ProductInfoInputs> = {
@@ -50,8 +51,10 @@ const meta: Meta<ProductInfoInputs> = {
value: 19.99,
},
},
tag: 'Prio 2',
},
orientation: 'horizontal',
innerGridClass: 'grid-cols-[minmax(20rem,1fr),auto]',
},
argTypes: {
item: {
@@ -68,6 +71,16 @@ const meta: Meta<ProductInfoInputs> = {
},
},
},
innerGridClass: {
control: 'text',
description:
'Custom CSS classes for the inner grid layout. (Applies on vertical layout only)',
table: {
defaultValue: {
summary: 'grid-cols-[minmax(20rem,1fr),auto]',
},
},
},
},
render: (args) => ({
props: args,
@@ -95,6 +108,7 @@ export const Default: Story = {
value: 29.99,
},
},
tag: 'Prio 2',
},
},
};

View File

@@ -0,0 +1,59 @@
import {
argsToTemplate,
moduleMetadata,
type Meta,
type StoryObj,
} from '@storybook/angular';
import {
InlineInputComponent,
InputControlDirective,
} from '@isa/ui/input-controls';
const meta: Meta<InlineInputComponent> = {
component: InlineInputComponent,
title: 'ui/input-controls/InlineInput',
argTypes: {
size: { control: 'select', options: ['small', 'medium'] },
},
args: {
size: 'medium',
},
decorators: [
moduleMetadata({
imports: [InputControlDirective],
}),
],
render: (args) => ({
props: args,
template: `
<ui-inline-input ${argsToTemplate(args)}>
<input type="text" placeholder="Enter inline text" />
</ui-inline-input>
`,
}),
};
export default meta;
type Story = StoryObj<InlineInputComponent>;
export const Primary: Story = {
args: {
size: 'medium',
},
};
export const WithLabel: Story = {
args: {
size: 'medium',
},
render: (args) => ({
props: args,
template: `
<ui-inline-input ${argsToTemplate(args)}>
<label>Label</label>
<input type="text" placeholder="Enter inline text" />
</ui-inline-input>
`,
}),
};

View File

@@ -0,0 +1,39 @@
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
import { Labeltype, LabelPriority, LabelComponent } from '@isa/ui/label';
type UiLabelInputs = {
type: Labeltype;
priority: LabelPriority;
};
const meta: Meta<UiLabelInputs> = {
component: LabelComponent,
title: 'ui/label/Label',
argTypes: {
type: {
control: { type: 'select' },
options: Object.values(Labeltype),
description: 'Determines the label type',
},
priority: {
control: { type: 'select' },
options: Object.values(LabelPriority),
description: 'Determines the label priority',
},
},
args: {
type: 'tag',
priority: 'high',
},
render: (args) => ({
props: args,
template: `<ui-label ${argsToTemplate(args)}>Prio 1</ui-label>`,
}),
};
export default meta;
type Story = StoryObj<LabelComponent>;
export const Default: Story = {
args: {},
};

View File

@@ -1,128 +1,136 @@
# Tech Stack Documentation
## Core Technologies
### Frontend Framework
- **[Angular](https://angular.dev/overview)** (Latest Version)
- Modern web framework for building scalable single-page applications
- Leverages TypeScript for enhanced type safety and developer experience
### State Management
- **[NgRx](https://ngrx.io/docs)**
- Redux-inspired state management for Angular applications
- Provides predictable state container and powerful dev tools
- Used for managing complex application state and side effects
### Styling
- **[Tailwind CSS](https://tailwindcss.com/)**
- Utility-first CSS framework
- Enables rapid UI development with pre-built classes
- Highly customizable through configuration
## Development Tools
### Language
- **[TypeScript](https://www.typescriptlang.org/docs/handbook/intro.html)**
- Strongly typed programming language
- Provides enhanced IDE support and compile-time error checking
- Used throughout the entire application
### Runtime
- **[Node.js](https://nodejs.org/docs/latest-v22.x/api/index.html)**
- JavaScript runtime environment
- Used for development server and build tools
- Required for running npm scripts and development tools
### Testing Framework
- **[Jest](https://jestjs.io/docs/getting-started)**
- Primary testing framework
- Used for unit and integration tests
- Provides snapshot testing capabilities
- **[Spectator](https://ngneat.github.io/spectator/)**
- Angular testing utility library
- Simplifies component testing
- Reduces boilerplate in test files
### UI Development
- **[Storybook](https://storybook.js.org/docs/get-started/frameworks/angular)**
- UI component development environment
- Enables isolated component development
- Serves as living documentation for components
### Utilities
- **[date-fns](https://date-fns.org/docs/Getting-Started)**
- Modern JavaScript date utility library
- Used for consistent date formatting and manipulation
- Tree-shakeable to optimize bundle size
- **[Lodash](https://lodash.com/)**
- Utility library for common JavaScript tasks
- **[UUID](https://www.npmjs.com/package/uuid)**
- Generates unique identifiers
- **[Zod](https://github.com/colinhacks/zod)**
- TypeScript-first schema validation library
## Additional Technical Areas
### Authentication & Authorization
- **[angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc)**
- Simplifies implementing OAuth2 and OIDC authentication in Angular.
- **[angular-oauth2-oidc-jwks](https://github.com/manfredsteyer/angular-oauth2-oidc)**
- Adds JWKS support for secure token management.
### Real-Time Communication
- **[@microsoft/signalr](https://www.npmjs.com/package/@microsoft/signalr)**
- Provides real-time communication between client and server components.
### Barcode Scanning
- **[Scandit Web Data Capture Barcode](https://www.scandit.com/documentation/web/)**
- Robust barcode scanning capabilities integrated into the application.
- **[Scandit Web Data Capture Core](https://www.scandit.com/documentation/web/)**
- Core library supporting the barcode scanning features.
### Tooling
- **[Nx](https://nx.dev/)**
- Powerful monorepo tool for Angular and other frontend applications.
- **[Husky](https://typicode.github.io/husky/#/)**
- Manages Git hooks for consistent developer workflows.
- **[ESLint](https://eslint.org/) & [Prettier](https://prettier.io/)**
- Linting and formatting tools to maintain consistent code quality.
- **[Storybook](https://storybook.js.org/)**
- Isolated component development and living documentation environment.
## Development Environment Setup
1. **Required Software**
- Node.js (Latest LTS version)
- npm (comes with Node.js)
- Git
2. **IDE Recommendations**
- Visual Studio Code with following extensions:
- Angular Language Service
- ESLint
- Prettier
- Tailwind CSS IntelliSense
3. **Getting Started**
```bash
npm install # Install dependencies
npm run start # Start development server
npm run test # Run tests
npm run storybook # Start Storybook
```
# Tech Stack Documentation
## Core Technologies
### Frontend Framework
- **[Angular](https://angular.dev/overview)** (Latest Version)
- Modern web framework for building scalable single-page applications
- Leverages TypeScript for enhanced type safety and developer experience
### State Management
- **[NgRx](https://ngrx.io/docs)**
- Redux-inspired state management for Angular applications
- Provides predictable state container and powerful dev tools
- Used for managing complex application state and side effects
### Styling
- **[Tailwind CSS](https://tailwindcss.com/)**
- Utility-first CSS framework
- Enables rapid UI development with pre-built classes
- Highly customizable through configuration
## Development Tools
### Language
- **[TypeScript](https://www.typescriptlang.org/docs/handbook/intro.html)**
- Strongly typed programming language
- Provides enhanced IDE support and compile-time error checking
- Used throughout the entire application
### Runtime
- **[Node.js](https://nodejs.org/docs/latest-v22.x/api/index.html)**
- JavaScript runtime environment
- Used for development server and build tools
- Required for running npm scripts and development tools
### Testing Framework
- **[Jest](https://jestjs.io/docs/getting-started)**
- Primary testing framework
- Used for unit and integration tests
- Provides snapshot testing capabilities
- **[Spectator](https://ngneat.github.io/spectator/)**
- Angular testing utility library
- Simplifies component testing
- Reduces boilerplate in test files
### UI Development
- **[Storybook](https://storybook.js.org/docs/get-started/frameworks/angular)**
- UI component development environment
- Enables isolated component development
- Serves as living documentation for components
### Utilities
- **[date-fns](https://date-fns.org/docs/Getting-Started)**
- Modern JavaScript date utility library
- Used for consistent date formatting and manipulation
- Tree-shakeable to optimize bundle size
- **[Lodash](https://lodash.com/)**
- Utility library for common JavaScript tasks
- **[UUID](https://www.npmjs.com/package/uuid)**
- Generates unique identifiers
- **[Zod](https://github.com/colinhacks/zod)**
- TypeScript-first schema validation library
## Additional Technical Areas
### Authentication & Authorization
- **[angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc)**
- Simplifies implementing OAuth2 and OIDC authentication in Angular.
- **[angular-oauth2-oidc-jwks](https://github.com/manfredsteyer/angular-oauth2-oidc)**
- Adds JWKS support for secure token management.
### Real-Time Communication
- **[@microsoft/signalr](https://www.npmjs.com/package/@microsoft/signalr)**
- Provides real-time communication between client and server components.
### Barcode Scanning
- **[Scandit Web Data Capture Barcode](https://www.scandit.com/documentation/web/)**
- Robust barcode scanning capabilities integrated into the application.
- **[Scandit Web Data Capture Core](https://www.scandit.com/documentation/web/)**
- Core library supporting the barcode scanning features.
### Tooling
- **[Nx](https://nx.dev/)**
- Powerful monorepo tool for Angular and other frontend applications.
- **[Husky](https://typicode.github.io/husky/#/)**
- Manages Git hooks for consistent developer workflows.
- **[ESLint](https://eslint.org/) & [Prettier](https://prettier.io/)**
- Linting and formatting tools to maintain consistent code quality.
- **[Storybook](https://storybook.js.org/)**
- Isolated component development and living documentation environment.
## Domain Libraries
### Customer Relationship Management (CRM)
- **`@isa/crm/data-access`**
- Handles data access logic for customer-related features.
- Contains services for fetching and managing customer data.
## Development Environment Setup
1. **Required Software**
- Node.js (Latest LTS version)
- npm (comes with Node.js)
- Git
2. **IDE Recommendations**
- Visual Studio Code with following extensions:
- Angular Language Service
- ESLint
- Prettier
- Tailwind CSS IntelliSense
3. **Getting Started**
```bash
npm install # Install dependencies
npm run start # Start development server
npm run test # Run tests
npm run storybook # Start Storybook
```

View File

@@ -31,8 +31,9 @@ const PARAMETER_CODEC = new ParameterCodec();
export class BaseService {
constructor(
protected config: CatConfiguration,
protected http: HttpClient,
) {}
protected http: HttpClient
) {
}
private _rootUrl: string = '';
@@ -56,7 +57,7 @@ export class BaseService {
*/
protected newParams(): HttpParams {
return new HttpParams({
encoder: PARAMETER_CODEC,
encoder: PARAMETER_CODEC
});
}
}

View File

@@ -4,6 +4,7 @@
* Auocomplete-Ergebnis
*/
export interface AutocompleteDTO {
/**
* Anzeige / Bezeichner
*/

View File

@@ -5,6 +5,7 @@ import { CatalogType } from './catalog-type';
* Suchabfrage
*/
export interface AutocompleteTokenDTO {
/**
* Katalogbereich
*/
@@ -13,7 +14,7 @@ export interface AutocompleteTokenDTO {
/**
* Filter
*/
filter?: { [key: string]: string };
filter?: {[key: string]: string};
/**
* Eingabe

View File

@@ -7,6 +7,7 @@ import { AvailabilityType } from './availability-type';
* Verfügbarkeit
*/
export interface AvailabilityDTO {
/**
* Voraussichtliches Lieferdatum
*/

View File

@@ -1,2 +1,2 @@
/* tslint:disable */
export type AvailabilityType = 0 | 1 | 2 | 32 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384;
export type AvailabilityType = 0 | 1 | 2 | 32 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384;

View File

@@ -1,2 +1,2 @@
/* tslint:disable */
export type Avoirdupois = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096;
export type Avoirdupois = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096;

View File

@@ -3,4 +3,4 @@
/**
* Katalogbereich
*/
export type CatalogType = 0 | 1 | 2 | 4;
export type CatalogType = 0 | 1 | 2 | 4;

View File

@@ -1,2 +1,2 @@
/* tslint:disable */
export type CRUDA = 0 | 1 | 2 | 4 | 8 | 16;
export type CRUDA = 0 | 1 | 2 | 4 | 8 | 16;

View File

@@ -1,2 +1,2 @@
/* tslint:disable */
export type DialogContentType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;
export type DialogContentType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;

View File

@@ -1,2 +1,2 @@
/* tslint:disable */
export type DialogSettings = 0 | 1 | 2 | 4;
export type DialogSettings = 0 | 1 | 2 | 4;

View File

@@ -2,7 +2,7 @@
import { TouchedBase } from './touched-base';
import { CRUDA } from './cruda';
import { EntityStatus } from './entity-status';
export interface EntityDTO extends TouchedBase {
export interface EntityDTO extends TouchedBase{
changed?: string;
created?: string;
cruda?: CRUDA;

View File

@@ -1,2 +1,2 @@
/* tslint:disable */
export type EntityStatus = 0 | 1 | 2 | 4 | 8;
export type EntityStatus = 0 | 1 | 2 | 4 | 8;

View File

@@ -4,6 +4,7 @@
* Bild
*/
export interface ImageDTO {
/**
* Copyright
*/

View File

@@ -1,2 +1,2 @@
/* tslint:disable */
export type InputType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 3072 | 4096 | 8192 | 12288;
export type InputType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 3072 | 4096 | 8192 | 12288;

View File

@@ -11,7 +11,8 @@ import { StockInfoDTO } from './stock-info-dto';
import { Successor } from './successor';
import { TextDTO } from './text-dto';
import { ItemType } from './item-type';
export interface ItemDTO extends EntityDTO {
export interface ItemDTO extends EntityDTO{
/**
* Verfügbarkeit laut Katalog
*/
@@ -30,7 +31,7 @@ export interface ItemDTO extends EntityDTO {
/**
* Weitere Artikel-IDs
*/
ids?: { [key: string]: number };
ids?: {[key: string]: number};
/**
* Primary image / Id des Hauptbilds
@@ -45,7 +46,7 @@ export interface ItemDTO extends EntityDTO {
/**
* Markierungen (Lesezeichen) wie (BOD, Prämie)
*/
labels?: { [key: string]: string };
labels?: {[key: string]: string};
/**
* Produkt-Stammdaten

View File

@@ -3,4 +3,4 @@
/**
* Artikel-/Produkttyp
*/
export type ItemType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384 | 32768 | 65536;
export type ItemType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384 | 32768 | 65536;

View File

@@ -1,5 +1,6 @@
/* tslint:disable */
export interface LesepunkteRequest {
/**
* Artikel ID
*/

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { ResponseArgsOfIEnumerableOfAutocompleteDTO } from './response-args-of-ienumerable-of-autocomplete-dto';
export interface ListResponseArgsOfAutocompleteDTO extends ResponseArgsOfIEnumerableOfAutocompleteDTO {
export interface ListResponseArgsOfAutocompleteDTO extends ResponseArgsOfIEnumerableOfAutocompleteDTO{
completed?: boolean;
hits?: number;
skip?: number;

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { ResponseArgsOfIEnumerableOfItemDTO } from './response-args-of-ienumerable-of-item-dto';
export interface ListResponseArgsOfItemDTO extends ResponseArgsOfIEnumerableOfItemDTO {
export interface ListResponseArgsOfItemDTO extends ResponseArgsOfIEnumerableOfItemDTO{
completed?: boolean;
hits?: number;
skip?: number;

View File

@@ -2,7 +2,7 @@
import { TouchedBase } from './touched-base';
import { PriceValueDTO } from './price-value-dto';
import { VATValueDTO } from './vatvalue-dto';
export interface PriceDTO extends TouchedBase {
export interface PriceDTO extends TouchedBase{
value?: PriceValueDTO;
vat?: VATValueDTO;
}

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { TouchedBase } from './touched-base';
export interface PriceValueDTO extends TouchedBase {
export interface PriceValueDTO extends TouchedBase{
currency?: string;
currencySymbol?: string;
value?: number;

View File

@@ -1,7 +1,7 @@
/* tslint:disable */
export interface ProblemDetails {
detail?: string;
extensions: { [key: string]: any };
extensions: {[key: string]: any};
instance?: string;
status?: number;
title?: string;

View File

@@ -2,7 +2,7 @@
import { TouchedBase } from './touched-base';
import { SizeOfString } from './size-of-string';
import { WeightOfAvoirdupois } from './weight-of-avoirdupois';
export interface ProductDTO extends TouchedBase {
export interface ProductDTO extends TouchedBase{
additionalName?: string;
catalogProductNumber?: string;
contributors?: string;

View File

@@ -1,7 +1,8 @@
/* tslint:disable */
import { QueryTokenDTO2 } from './query-token-dto2';
import { CatalogType } from './catalog-type';
export interface QueryTokenDTO extends QueryTokenDTO2 {
export interface QueryTokenDTO extends QueryTokenDTO2{
/**
* Katalogbereich
*/

View File

@@ -1,13 +1,13 @@
/* tslint:disable */
import { OrderByDTO } from './order-by-dto';
export interface QueryTokenDTO2 {
filter?: { [key: string]: string };
filter?: {[key: string]: string};
friendlyName?: string;
fuzzy?: number;
hitsOnly?: boolean;
ids?: Array<number>;
input?: { [key: string]: string };
options?: { [key: string]: string };
input?: {[key: string]: string};
options?: {[key: string]: string};
orderBy?: Array<OrderByDTO>;
skip?: number;
take?: number;

View File

@@ -1,5 +1,5 @@
/* tslint:disable */
import { ResponseArgs } from './response-args';
export interface ResponseArgsOfIDictionaryOfLongAndNullableInteger extends ResponseArgs {
result?: { [key: string]: number };
export interface ResponseArgsOfIDictionaryOfLongAndNullableInteger extends ResponseArgs{
result?: {[key: string]: number};
}

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { ResponseArgs } from './response-args';
import { AutocompleteDTO } from './autocomplete-dto';
export interface ResponseArgsOfIEnumerableOfAutocompleteDTO extends ResponseArgs {
export interface ResponseArgsOfIEnumerableOfAutocompleteDTO extends ResponseArgs{
result?: Array<AutocompleteDTO>;
}

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { ResponseArgs } from './response-args';
import { InputGroupDTO } from './input-group-dto';
export interface ResponseArgsOfIEnumerableOfInputGroupDTO extends ResponseArgs {
export interface ResponseArgsOfIEnumerableOfInputGroupDTO extends ResponseArgs{
result?: Array<InputGroupDTO>;
}

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { ResponseArgs } from './response-args';
import { ItemDTO } from './item-dto';
export interface ResponseArgsOfIEnumerableOfItemDTO extends ResponseArgs {
export interface ResponseArgsOfIEnumerableOfItemDTO extends ResponseArgs{
result?: Array<ItemDTO>;
}

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { ResponseArgs } from './response-args';
import { OrderByDTO } from './order-by-dto';
export interface ResponseArgsOfIEnumerableOfOrderByDTO extends ResponseArgs {
export interface ResponseArgsOfIEnumerableOfOrderByDTO extends ResponseArgs{
result?: Array<OrderByDTO>;
}

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { ResponseArgs } from './response-args';
import { QueryTokenDTO } from './query-token-dto';
export interface ResponseArgsOfIEnumerableOfQueryTokenDTO extends ResponseArgs {
export interface ResponseArgsOfIEnumerableOfQueryTokenDTO extends ResponseArgs{
result?: Array<QueryTokenDTO>;
}

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { ResponseArgs } from './response-args';
import { ItemDTO } from './item-dto';
export interface ResponseArgsOfItemDTO extends ResponseArgs {
export interface ResponseArgsOfItemDTO extends ResponseArgs{
result?: ItemDTO;
}

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { ResponseArgs } from './response-args';
import { UISettingsDTO } from './uisettings-dto';
export interface ResponseArgsOfUISettingsDTO extends ResponseArgs {
export interface ResponseArgsOfUISettingsDTO extends ResponseArgs{
result?: UISettingsDTO;
}

View File

@@ -3,7 +3,7 @@ import { DialogOfString } from './dialog-of-string';
export interface ResponseArgs {
dialog?: DialogOfString;
error: boolean;
invalidProperties?: { [key: string]: string };
invalidProperties?: {[key: string]: string};
message?: string;
requestId?: number;
}

View File

@@ -1,5 +1,6 @@
/* tslint:disable */
export interface ReviewDTO {
/**
* Autor
*/

View File

@@ -4,6 +4,7 @@
* Regalinfo
*/
export interface ShelfInfoDTO {
/**
* Sortiment
*/

View File

@@ -3,4 +3,5 @@
/**
* Shop
*/
export interface ShopDTO {}
export interface ShopDTO {
}

View File

@@ -4,6 +4,7 @@
* Eigenchaften
*/
export interface SpecDTO {
/**
* PK
*/

View File

@@ -5,6 +5,7 @@ import { StockStatus } from './stock-status';
* Bestandsinformation
*/
export interface StockInfoDTO {
/**
* Filiale PK
*/

View File

@@ -3,4 +3,4 @@
/**
* Dispositionsstatus
*/
export type StockStatus = 0 | 1 | 2 | 3 | 4;
export type StockStatus = 0 | 1 | 2 | 3 | 4;

View File

@@ -1,6 +1,7 @@
/* tslint:disable */
import { ProductDTO } from './product-dto';
export interface Successor extends ProductDTO {
export interface Successor extends ProductDTO{
/**
* PK
*/

View File

@@ -1,5 +1,6 @@
/* tslint:disable */
export interface TextDTO {
/**
* PK
*/

View File

@@ -1,2 +1,3 @@
/* tslint:disable */
export interface TouchedBase {}
export interface TouchedBase {
}

View File

@@ -1,5 +1,5 @@
/* tslint:disable */
export interface TranslationDTO {
target?: string;
values?: { [key: string]: string };
values?: {[key: string]: string};
}

View File

@@ -1,7 +1,8 @@
/* tslint:disable */
import { QuerySettingsDTO } from './query-settings-dto';
import { TranslationDTO } from './translation-dto';
export interface UISettingsDTO extends QuerySettingsDTO {
export interface UISettingsDTO extends QuerySettingsDTO{
/**
* Url Template für Detail-Bild
*/

View File

@@ -1,2 +1,2 @@
/* tslint:disable */
export type VATType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;
export type VATType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;

View File

@@ -1,7 +1,7 @@
/* tslint:disable */
import { TouchedBase } from './touched-base';
import { VATType } from './vattype';
export interface VATValueDTO extends TouchedBase {
export interface VATValueDTO extends TouchedBase{
inPercent?: number;
label?: string;
value?: number;

View File

@@ -16,7 +16,10 @@ class PromotionService extends __BaseService {
static readonly PromotionLesepunktePath = '/promotion/lesepunkte';
static readonly PromotionLesepunkte2Path = '/stock/{stockId}/promotion/lesepunkte';
constructor(config: __Configuration, http: HttpClient) {
constructor(
config: __Configuration,
http: HttpClient
) {
super(config, http);
}
@@ -24,24 +27,26 @@ class PromotionService extends __BaseService {
* Lesepunkte
* @param items Ids und Mengen
*/
PromotionLesepunkteResponse(
items: Array<LesepunkteRequest>,
): __Observable<__StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>> {
PromotionLesepunkteResponse(items: Array<LesepunkteRequest>): __Observable<__StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
__body = items;
let req = new HttpRequest<any>('POST', this.rootUrl + `/promotion/lesepunkte`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/promotion/lesepunkte`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>;
}),
})
);
}
/**
@@ -49,7 +54,9 @@ class PromotionService extends __BaseService {
* @param items Ids und Mengen
*/
PromotionLesepunkte(items: Array<LesepunkteRequest>): __Observable<ResponseArgsOfIDictionaryOfLongAndNullableInteger> {
return this.PromotionLesepunkteResponse(items).pipe(__map((_r) => _r.body as ResponseArgsOfIDictionaryOfLongAndNullableInteger));
return this.PromotionLesepunkteResponse(items).pipe(
__map(_r => _r.body as ResponseArgsOfIDictionaryOfLongAndNullableInteger)
);
}
/**
@@ -60,9 +67,7 @@ class PromotionService extends __BaseService {
*
* - `items`: Ids und Mengen
*/
PromotionLesepunkte2Response(
params: PromotionService.PromotionLesepunkte2Params,
): __Observable<__StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>> {
PromotionLesepunkte2Response(params: PromotionService.PromotionLesepunkte2Params): __Observable<__StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
@@ -75,15 +80,14 @@ class PromotionService extends __BaseService {
{
headers: __headers,
params: __params,
responseType: 'json',
},
);
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfIDictionaryOfLongAndNullableInteger>;
}),
})
);
}
/**
@@ -94,18 +98,20 @@ class PromotionService extends __BaseService {
*
* - `items`: Ids und Mengen
*/
PromotionLesepunkte2(
params: PromotionService.PromotionLesepunkte2Params,
): __Observable<ResponseArgsOfIDictionaryOfLongAndNullableInteger> {
return this.PromotionLesepunkte2Response(params).pipe(__map((_r) => _r.body as ResponseArgsOfIDictionaryOfLongAndNullableInteger));
PromotionLesepunkte2(params: PromotionService.PromotionLesepunkte2Params): __Observable<ResponseArgsOfIDictionaryOfLongAndNullableInteger> {
return this.PromotionLesepunkte2Response(params).pipe(
__map(_r => _r.body as ResponseArgsOfIDictionaryOfLongAndNullableInteger)
);
}
}
module PromotionService {
/**
* Parameters for PromotionLesepunkte2
*/
export interface PromotionLesepunkte2Params {
/**
* Lager PK (optional)
*/
@@ -118,4 +124,4 @@ module PromotionService {
}
}
export { PromotionService };
export { PromotionService }

View File

@@ -37,12 +37,16 @@ class SearchService extends __BaseService {
static readonly SearchDetailByEANPath = '/s/ean/{ean}';
static readonly SearchDetailByEAN2Path = '/stock/{stockId}/ean/{ean}';
static readonly SearchSettingsPath = '/s/settings';
static readonly SearchLoyaltySettingsPath = '/s/loyalty/settings';
static readonly SearchSearchFilterPath = '/s/filter';
static readonly SearchSearchSortPath = '/s/sort';
static readonly SearchHistoryPath = '/s/history';
static readonly SearchGetRecommendationsPath = '/s/recommendations/{digId}';
constructor(config: __Configuration, http: HttpClient) {
constructor(
config: __Configuration,
http: HttpClient
) {
super(config, http);
}
@@ -55,17 +59,21 @@ class SearchService extends __BaseService {
let __headers = new HttpHeaders();
let __body: any = null;
__body = queryToken;
let req = new HttpRequest<any>('POST', this.rootUrl + `/s/top`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/s/top`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -73,7 +81,9 @@ class SearchService extends __BaseService {
* @param queryToken Suchkriterien
*/
SearchTop(queryToken: QueryTokenDTO): __Observable<ListResponseArgsOfItemDTO> {
return this.SearchTopResponse(queryToken).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
return this.SearchTopResponse(queryToken).pipe(
__map(_r => _r.body as ListResponseArgsOfItemDTO)
);
}
/**
@@ -90,17 +100,21 @@ class SearchService extends __BaseService {
let __body: any = null;
__body = params.queryToken;
let req = new HttpRequest<any>('POST', this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/top`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/top`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -112,7 +126,9 @@ class SearchService extends __BaseService {
* - `queryToken`: Suchkriterien
*/
SearchTop2(params: SearchService.SearchTop2Params): __Observable<ListResponseArgsOfItemDTO> {
return this.SearchTop2Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
return this.SearchTop2Response(params).pipe(
__map(_r => _r.body as ListResponseArgsOfItemDTO)
);
}
/**
@@ -124,17 +140,21 @@ class SearchService extends __BaseService {
let __headers = new HttpHeaders();
let __body: any = null;
__body = queryToken;
let req = new HttpRequest<any>('POST', this.rootUrl + `/s`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/s`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -142,7 +162,9 @@ class SearchService extends __BaseService {
* @param queryToken Suchkriterien
*/
SearchSearch(queryToken: QueryTokenDTO): __Observable<ListResponseArgsOfItemDTO> {
return this.SearchSearchResponse(queryToken).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
return this.SearchSearchResponse(queryToken).pipe(
__map(_r => _r.body as ListResponseArgsOfItemDTO)
);
}
/**
@@ -159,17 +181,21 @@ class SearchService extends __BaseService {
let __body: any = null;
__body = params.queryToken;
let req = new HttpRequest<any>('POST', this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -181,7 +207,9 @@ class SearchService extends __BaseService {
* - `queryToken`: Suchkriterien
*/
SearchSearch2(params: SearchService.SearchSearch2Params): __Observable<ListResponseArgsOfItemDTO> {
return this.SearchSearch2Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
return this.SearchSearch2Response(params).pipe(
__map(_r => _r.body as ListResponseArgsOfItemDTO)
);
}
/**
@@ -193,17 +221,21 @@ class SearchService extends __BaseService {
let __headers = new HttpHeaders();
let __body: any = null;
__body = queryToken;
let req = new HttpRequest<any>('POST', this.rootUrl + `/s/complete`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/s/complete`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfAutocompleteDTO>;
}),
})
);
}
/**
@@ -211,7 +243,9 @@ class SearchService extends __BaseService {
* @param queryToken Suchbegriff
*/
SearchAutocomplete(queryToken: AutocompleteTokenDTO): __Observable<ListResponseArgsOfAutocompleteDTO> {
return this.SearchAutocompleteResponse(queryToken).pipe(__map((_r) => _r.body as ListResponseArgsOfAutocompleteDTO));
return this.SearchAutocompleteResponse(queryToken).pipe(
__map(_r => _r.body as ListResponseArgsOfAutocompleteDTO)
);
}
/**
@@ -222,25 +256,27 @@ class SearchService extends __BaseService {
*
* - `queryToken`: Suchbegriff
*/
SearchAutocomplete2Response(
params: SearchService.SearchAutocomplete2Params,
): __Observable<__StrictHttpResponse<ListResponseArgsOfAutocompleteDTO>> {
SearchAutocomplete2Response(params: SearchService.SearchAutocomplete2Params): __Observable<__StrictHttpResponse<ListResponseArgsOfAutocompleteDTO>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
__body = params.queryToken;
let req = new HttpRequest<any>('POST', this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/complete`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/complete`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfAutocompleteDTO>;
}),
})
);
}
/**
@@ -252,7 +288,9 @@ class SearchService extends __BaseService {
* - `queryToken`: Suchbegriff
*/
SearchAutocomplete2(params: SearchService.SearchAutocomplete2Params): __Observable<ListResponseArgsOfAutocompleteDTO> {
return this.SearchAutocomplete2Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfAutocompleteDTO));
return this.SearchAutocomplete2Response(params).pipe(
__map(_r => _r.body as ListResponseArgsOfAutocompleteDTO)
);
}
/**
@@ -264,17 +302,21 @@ class SearchService extends __BaseService {
let __headers = new HttpHeaders();
let __body: any = null;
__body = ids;
let req = new HttpRequest<any>('POST', this.rootUrl + `/s/byid`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/s/byid`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -282,7 +324,9 @@ class SearchService extends __BaseService {
* @param ids PKs
*/
SearchById(ids: Array<number>): __Observable<ListResponseArgsOfItemDTO> {
return this.SearchByIdResponse(ids).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
return this.SearchByIdResponse(ids).pipe(
__map(_r => _r.body as ListResponseArgsOfItemDTO)
);
}
/**
@@ -299,17 +343,21 @@ class SearchService extends __BaseService {
let __body: any = null;
__body = params.ids;
let req = new HttpRequest<any>('POST', this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/byid`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/byid`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -321,7 +369,9 @@ class SearchService extends __BaseService {
* - `ids`: PKs
*/
SearchById2(params: SearchService.SearchById2Params): __Observable<ListResponseArgsOfItemDTO> {
return this.SearchById2Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
return this.SearchById2Response(params).pipe(
__map(_r => _r.body as ListResponseArgsOfItemDTO)
);
}
/**
@@ -333,17 +383,21 @@ class SearchService extends __BaseService {
let __headers = new HttpHeaders();
let __body: any = null;
__body = eans;
let req = new HttpRequest<any>('POST', this.rootUrl + `/s/byean`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/s/byean`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -351,7 +405,9 @@ class SearchService extends __BaseService {
* @param eans EANs
*/
SearchByEAN(eans: Array<string>): __Observable<ListResponseArgsOfItemDTO> {
return this.SearchByEANResponse(eans).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
return this.SearchByEANResponse(eans).pipe(
__map(_r => _r.body as ListResponseArgsOfItemDTO)
);
}
/**
@@ -368,17 +424,21 @@ class SearchService extends __BaseService {
let __body: any = null;
__body = params.eans;
let req = new HttpRequest<any>('POST', this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/byean`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/s/byean`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -390,7 +450,9 @@ class SearchService extends __BaseService {
* - `eans`: EANs
*/
SearchByEAN2(params: SearchService.SearchByEAN2Params): __Observable<ListResponseArgsOfItemDTO> {
return this.SearchByEAN2Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
return this.SearchByEAN2Response(params).pipe(
__map(_r => _r.body as ListResponseArgsOfItemDTO)
);
}
/**
@@ -407,17 +469,21 @@ class SearchService extends __BaseService {
let __body: any = null;
__body = params.eans;
let req = new HttpRequest<any>('POST', this.rootUrl + `/branch/${encodeURIComponent(String(params.branchNumber))}/s/byean`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/branch/${encodeURIComponent(String(params.branchNumber))}/s/byean`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ListResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -429,7 +495,9 @@ class SearchService extends __BaseService {
* - `branchNumber`: Filiale-Nr (optional)
*/
SearchByEAN3(params: SearchService.SearchByEAN3Params): __Observable<ListResponseArgsOfItemDTO> {
return this.SearchByEAN3Response(params).pipe(__map((_r) => _r.body as ListResponseArgsOfItemDTO));
return this.SearchByEAN3Response(params).pipe(
__map(_r => _r.body as ListResponseArgsOfItemDTO)
);
}
/**
@@ -446,17 +514,21 @@ class SearchService extends __BaseService {
let __body: any = null;
if (params.doNotTrack != null) __params = __params.set('doNotTrack', params.doNotTrack.toString());
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/${encodeURIComponent(String(params.id))}`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'GET',
this.rootUrl + `/s/${encodeURIComponent(String(params.id))}`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -468,7 +540,9 @@ class SearchService extends __BaseService {
* - `doNotTrack`:
*/
SearchDetail(params: SearchService.SearchDetailParams): __Observable<ResponseArgsOfItemDTO> {
return this.SearchDetailResponse(params).pipe(__map((_r) => _r.body as ResponseArgsOfItemDTO));
return this.SearchDetailResponse(params).pipe(
__map(_r => _r.body as ResponseArgsOfItemDTO)
);
}
/**
@@ -486,6 +560,7 @@ class SearchService extends __BaseService {
let __headers = new HttpHeaders();
let __body: any = null;
if (params.doNotTrack != null) __params = __params.set('doNotTrack', params.doNotTrack.toString());
let req = new HttpRequest<any>(
'GET',
@@ -494,15 +569,14 @@ class SearchService extends __BaseService {
{
headers: __headers,
params: __params,
responseType: 'json',
},
);
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -516,7 +590,9 @@ class SearchService extends __BaseService {
* - `doNotTrack`:
*/
SearchDetail2(params: SearchService.SearchDetail2Params): __Observable<ResponseArgsOfItemDTO> {
return this.SearchDetail2Response(params).pipe(__map((_r) => _r.body as ResponseArgsOfItemDTO));
return this.SearchDetail2Response(params).pipe(
__map(_r => _r.body as ResponseArgsOfItemDTO)
);
}
/**
@@ -528,17 +604,21 @@ class SearchService extends __BaseService {
let __headers = new HttpHeaders();
let __body: any = null;
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/ean/${encodeURIComponent(String(ean))}`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'GET',
this.rootUrl + `/s/ean/${encodeURIComponent(String(ean))}`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -546,7 +626,9 @@ class SearchService extends __BaseService {
* @param ean EAN
*/
SearchDetailByEAN(ean: string): __Observable<ResponseArgsOfItemDTO> {
return this.SearchDetailByEANResponse(ean).pipe(__map((_r) => _r.body as ResponseArgsOfItemDTO));
return this.SearchDetailByEANResponse(ean).pipe(
__map(_r => _r.body as ResponseArgsOfItemDTO)
);
}
/**
@@ -562,6 +644,7 @@ class SearchService extends __BaseService {
let __headers = new HttpHeaders();
let __body: any = null;
let req = new HttpRequest<any>(
'GET',
this.rootUrl + `/stock/${encodeURIComponent(String(params.stockId))}/ean/${encodeURIComponent(String(params.ean))}`,
@@ -569,15 +652,14 @@ class SearchService extends __BaseService {
{
headers: __headers,
params: __params,
responseType: 'json',
},
);
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfItemDTO>;
}),
})
);
}
/**
@@ -589,7 +671,9 @@ class SearchService extends __BaseService {
* - `ean`: EAN
*/
SearchDetailByEAN2(params: SearchService.SearchDetailByEAN2Params): __Observable<ResponseArgsOfItemDTO> {
return this.SearchDetailByEAN2Response(params).pipe(__map((_r) => _r.body as ResponseArgsOfItemDTO));
return this.SearchDetailByEAN2Response(params).pipe(
__map(_r => _r.body as ResponseArgsOfItemDTO)
);
}
/**
@@ -599,24 +683,63 @@ class SearchService extends __BaseService {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/settings`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'GET',
this.rootUrl + `/s/settings`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfUISettingsDTO>;
}),
})
);
}
/**
* Settings
*/
SearchSettings(): __Observable<ResponseArgsOfUISettingsDTO> {
return this.SearchSettingsResponse().pipe(__map((_r) => _r.body as ResponseArgsOfUISettingsDTO));
return this.SearchSettingsResponse().pipe(
__map(_r => _r.body as ResponseArgsOfUISettingsDTO)
);
}
/**
* Settings
*/
SearchLoyaltySettingsResponse(): __Observable<__StrictHttpResponse<ResponseArgsOfUISettingsDTO>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
let req = new HttpRequest<any>(
'GET',
this.rootUrl + `/s/loyalty/settings`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfUISettingsDTO>;
})
);
}
/**
* Settings
*/
SearchLoyaltySettings(): __Observable<ResponseArgsOfUISettingsDTO> {
return this.SearchLoyaltySettingsResponse().pipe(
__map(_r => _r.body as ResponseArgsOfUISettingsDTO)
);
}
/**
@@ -626,24 +749,30 @@ class SearchService extends __BaseService {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/filter`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'GET',
this.rootUrl + `/s/filter`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfIEnumerableOfInputGroupDTO>;
}),
})
);
}
/**
* Filter
*/
SearchSearchFilter(): __Observable<ResponseArgsOfIEnumerableOfInputGroupDTO> {
return this.SearchSearchFilterResponse().pipe(__map((_r) => _r.body as ResponseArgsOfIEnumerableOfInputGroupDTO));
return this.SearchSearchFilterResponse().pipe(
__map(_r => _r.body as ResponseArgsOfIEnumerableOfInputGroupDTO)
);
}
/**
@@ -653,24 +782,30 @@ class SearchService extends __BaseService {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/sort`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'GET',
this.rootUrl + `/s/sort`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfIEnumerableOfOrderByDTO>;
}),
})
);
}
/**
* Filter
*/
SearchSearchSort(): __Observable<ResponseArgsOfIEnumerableOfOrderByDTO> {
return this.SearchSearchSortResponse().pipe(__map((_r) => _r.body as ResponseArgsOfIEnumerableOfOrderByDTO));
return this.SearchSearchSortResponse().pipe(
__map(_r => _r.body as ResponseArgsOfIEnumerableOfOrderByDTO)
);
}
/**
@@ -682,17 +817,21 @@ class SearchService extends __BaseService {
let __headers = new HttpHeaders();
let __body: any = null;
if (take != null) __params = __params.set('take', take.toString());
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/history`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'GET',
this.rootUrl + `/s/history`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfIEnumerableOfQueryTokenDTO>;
}),
})
);
}
/**
@@ -700,7 +839,9 @@ class SearchService extends __BaseService {
* @param take Take
*/
SearchHistory(take?: null | number): __Observable<ResponseArgsOfIEnumerableOfQueryTokenDTO> {
return this.SearchHistoryResponse(take).pipe(__map((_r) => _r.body as ResponseArgsOfIEnumerableOfQueryTokenDTO));
return this.SearchHistoryResponse(take).pipe(
__map(_r => _r.body as ResponseArgsOfIEnumerableOfQueryTokenDTO)
);
}
/**
@@ -713,25 +854,27 @@ class SearchService extends __BaseService {
*
* @return ResponseArgs of Recomendations
*/
SearchGetRecommendationsResponse(
params: SearchService.SearchGetRecommendationsParams,
): __Observable<__StrictHttpResponse<ResponseArgsOfIEnumerableOfItemDTO>> {
SearchGetRecommendationsResponse(params: SearchService.SearchGetRecommendationsParams): __Observable<__StrictHttpResponse<ResponseArgsOfIEnumerableOfItemDTO>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
if (params.sessionId != null) __params = __params.set('sessionId', params.sessionId.toString());
let req = new HttpRequest<any>('GET', this.rootUrl + `/s/recommendations/${encodeURIComponent(String(params.digId))}`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'GET',
this.rootUrl + `/s/recommendations/${encodeURIComponent(String(params.digId))}`,
__body,
{
headers: __headers,
params: __params,
responseType: 'json'
});
return this.http.request<any>(req).pipe(
__filter((_r) => _r instanceof HttpResponse),
__filter(_r => _r instanceof HttpResponse),
__map((_r) => {
return _r as __StrictHttpResponse<ResponseArgsOfIEnumerableOfItemDTO>;
}),
})
);
}
/**
@@ -745,15 +888,19 @@ class SearchService extends __BaseService {
* @return ResponseArgs of Recomendations
*/
SearchGetRecommendations(params: SearchService.SearchGetRecommendationsParams): __Observable<ResponseArgsOfIEnumerableOfItemDTO> {
return this.SearchGetRecommendationsResponse(params).pipe(__map((_r) => _r.body as ResponseArgsOfIEnumerableOfItemDTO));
return this.SearchGetRecommendationsResponse(params).pipe(
__map(_r => _r.body as ResponseArgsOfIEnumerableOfItemDTO)
);
}
}
module SearchService {
/**
* Parameters for SearchTop2
*/
export interface SearchTop2Params {
/**
* Lager PK (optional)
*/
@@ -769,6 +916,7 @@ module SearchService {
* Parameters for SearchSearch2
*/
export interface SearchSearch2Params {
/**
* Lager PK (optional)
*/
@@ -784,6 +932,7 @@ module SearchService {
* Parameters for SearchAutocomplete2
*/
export interface SearchAutocomplete2Params {
/**
* Lager PK (optional)
*/
@@ -799,6 +948,7 @@ module SearchService {
* Parameters for SearchById2
*/
export interface SearchById2Params {
/**
* Lager PK (optional)
*/
@@ -814,6 +964,7 @@ module SearchService {
* Parameters for SearchByEAN2
*/
export interface SearchByEAN2Params {
/**
* Lager PK (optional)
*/
@@ -829,6 +980,7 @@ module SearchService {
* Parameters for SearchByEAN3
*/
export interface SearchByEAN3Params {
/**
* EANs
*/
@@ -844,6 +996,7 @@ module SearchService {
* Parameters for SearchDetail
*/
export interface SearchDetailParams {
/**
* PK
*/
@@ -855,6 +1008,7 @@ module SearchService {
* Parameters for SearchDetail2
*/
export interface SearchDetail2Params {
/**
* Lager PK (optional)
*/
@@ -871,6 +1025,7 @@ module SearchService {
* Parameters for SearchDetailByEAN2
*/
export interface SearchDetailByEAN2Params {
/**
* Lager PK (optional)
*/
@@ -895,4 +1050,4 @@ module SearchService {
}
}
export { SearchService };
export { SearchService }

View File

@@ -6,4 +6,4 @@ import { HttpResponse } from '@angular/common/http';
*/
export type StrictHttpResponse<T> = HttpResponse<T> & {
readonly body: T;
};
}

View File

@@ -10,7 +10,8 @@
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/catalogue/data-access/jest.config.ts"
"jestConfig": "libs/catalogue/data-access/jest.config.ts",
"tsConfig": "libs/catalogue/data-access/tsconfig.spec.json"
}
},
"lint": {

View File

@@ -1 +1,2 @@
export * from './catalouge-search.schemas';
export * from './query-token.schema';

View File

@@ -0,0 +1,31 @@
import { z } from 'zod';
/**
* Schema for defining order-by parameters in queries.
* Used for sorting search results.
*/
export const OrderBySchema = z.object({
by: z.string(), // Field name to sort by
label: z.string(), // Display label for the sort option
desc: z.boolean(), // Whether sorting is descending
selected: z.boolean(), // Whether this sort option is currently selected
});
/**
* Schema for validating and parsing query tokens.
* Used for search operations to ensure consistent query structure.
*/
export const QueryTokenSchema = z.object({
filter: z.record(z.any()).default({}), // Filter criteria as key-value pairs
input: z.record(z.string()).default({}).optional(),
orderBy: z.array(OrderBySchema).default([]).optional(), // Sorting parameters
skip: z.number().int().min(0).default(0),
take: z.number().int().min(1).max(100).default(20),
});
/**
* Type representing the structure of a query token.
* Generated from the Zod schema for type safety.
*/
export type QueryToken = z.infer<typeof QueryTokenSchema>;
export type QueryTokenInput = z.input<typeof QueryTokenSchema>;

View File

@@ -4,6 +4,7 @@ import { CatalougeSearchService } from './catalouge-search.service';
import { SearchService } from '@generated/swagger/cat-search-api';
import { Item } from '../models';
import { SearchByTermInput } from '../schemas/catalouge-search.schemas';
import { QueryTokenInput } from '../schemas';
describe('CatalougeSearchService', () => {
let service: CatalougeSearchService;
@@ -23,7 +24,9 @@ describe('CatalougeSearchService', () => {
});
service = TestBed.inject(CatalougeSearchService);
searchServiceSpy = TestBed.inject(SearchService) as jest.Mocked<SearchService>;
searchServiceSpy = TestBed.inject(
SearchService,
) as jest.Mocked<SearchService>;
});
it('should be created', () => {
@@ -34,8 +37,16 @@ describe('CatalougeSearchService', () => {
it('should return items when search is successful', (done) => {
// Arrange
const mockItems: Item[] = [
{ id: 1, product: { name: 'Item 1' }, catalogAvailability: { available: true } } as unknown as Item,
{ id: 2, product: { name: 'Item 2' }, catalogAvailability: { available: true } } as unknown as Item,
{
id: 1,
product: { name: 'Item 1' },
catalogAvailability: { available: true },
} as unknown as Item,
{
id: 2,
product: { name: 'Item 2' },
catalogAvailability: { available: true },
} as unknown as Item,
];
const mockResponse = {
error: false,
@@ -48,7 +59,10 @@ describe('CatalougeSearchService', () => {
next: (result) => {
// Assert
expect(result).toEqual(mockItems);
expect(searchServiceSpy.SearchByEAN).toHaveBeenCalledWith(['123456789', '987654321']);
expect(searchServiceSpy.SearchByEAN).toHaveBeenCalledWith([
'123456789',
'987654321',
]);
done();
},
error: done.fail,
@@ -77,7 +91,13 @@ describe('CatalougeSearchService', () => {
it('should handle single EAN', (done) => {
// Arrange
const mockItems: Item[] = [{ id: 1, product: { name: 'Item 1' }, catalogAvailability: { available: true } } as unknown as Item];
const mockItems: Item[] = [
{
id: 1,
product: { name: 'Item 1' },
catalogAvailability: { available: true },
} as unknown as Item,
];
const mockResponse = {
error: false,
result: mockItems,
@@ -89,7 +109,9 @@ describe('CatalougeSearchService', () => {
next: (result) => {
// Assert
expect(result).toEqual(mockItems);
expect(searchServiceSpy.SearchByEAN).toHaveBeenCalledWith(['123456789']);
expect(searchServiceSpy.SearchByEAN).toHaveBeenCalledWith([
'123456789',
]);
done();
},
error: done.fail,
@@ -121,7 +143,11 @@ describe('CatalougeSearchService', () => {
it('should return search results when successful', async () => {
// Arrange
const mockItems: Item[] = [
{ id: 1, product: { name: 'Test Item' }, catalogAvailability: { available: true } } as unknown as Item,
{
id: 1,
product: { name: 'Test Item' },
catalogAvailability: { available: true },
} as unknown as Item,
];
const mockResponse = {
error: false,
@@ -166,9 +192,9 @@ describe('CatalougeSearchService', () => {
const abortController = new AbortController();
// Act & Assert
await expect(service.searchByTerm(params, abortController.signal))
.rejects
.toThrow('Search failed');
await expect(
service.searchByTerm(params, abortController.signal),
).rejects.toThrow('Search failed');
});
it('should handle abort signal', async () => {
@@ -219,4 +245,167 @@ describe('CatalougeSearchService', () => {
});
});
});
});
describe('searchLoyaltyItems', () => {
it('should return loyalty items when search is successful', async () => {
// Arrange
const mockItems: Item[] = [
{
id: 1,
product: { name: 'Loyalty Item 1' },
catalogAvailability: { available: true },
} as unknown as Item,
{
id: 2,
product: { name: 'Loyalty Item 2' },
catalogAvailability: { available: true },
} as unknown as Item,
];
const mockResponse = {
error: false,
result: mockItems,
total: 2,
};
searchServiceSpy.SearchSearch.mockReturnValue(of(mockResponse));
const params: QueryTokenInput = {
filter: { category: 'electronics' },
input: { brand: 'test-brand' },
orderBy: [{ by: 'name', label: 'Name', desc: false, selected: true }],
skip: 0,
take: 10,
};
const abortController = new AbortController();
// Act
const result = await service.searchLoyaltyItems(
params,
abortController.signal,
);
// Assert
expect(result).toEqual(mockResponse);
expect(searchServiceSpy.SearchSearch).toHaveBeenCalledWith({
filter: {
format: '!eb;!dl',
praemie: '1-',
category: 'electronics',
},
input: { brand: 'test-brand' },
orderBy: [{ by: 'name', label: 'Name', desc: false, selected: true }],
skip: 0,
take: 10,
});
});
it('should apply default loyalty filter when no custom filter provided', async () => {
// Arrange
const mockItems: Item[] = [
{
id: 1,
product: { name: 'Loyalty Item' },
catalogAvailability: { available: true },
} as unknown as Item,
];
const mockResponse = {
error: false,
result: mockItems,
total: 1,
};
searchServiceSpy.SearchSearch.mockReturnValue(of(mockResponse));
const params: QueryTokenInput = {
input: { search: 'test' },
};
const abortController = new AbortController();
// Act
const result = await service.searchLoyaltyItems(
params,
abortController.signal,
);
// Assert
expect(result).toEqual(mockResponse);
expect(searchServiceSpy.SearchSearch).toHaveBeenCalledWith({
filter: {
format: '!eb;!dl',
praemie: '1-',
},
input: { search: 'test' },
orderBy: undefined,
skip: 0,
take: 20,
});
});
it('should merge custom filter with loyalty filter', async () => {
// Arrange
const mockResponse = {
error: false,
result: [],
total: 0,
};
searchServiceSpy.SearchSearch.mockReturnValue(of(mockResponse));
const params: QueryTokenInput = {
filter: {
category: 'books',
format: 'custom-format', // This should override the default format
},
input: { title: 'test-book' },
skip: 5,
take: 15,
};
const abortController = new AbortController();
// Act
await service.searchLoyaltyItems(params, abortController.signal);
// Assert
expect(searchServiceSpy.SearchSearch).toHaveBeenCalledWith({
filter: {
format: 'custom-format', // Custom filter should override default
praemie: '1-',
category: 'books',
},
input: { title: 'test-book' },
orderBy: undefined,
skip: 5,
take: 15,
});
});
it('should handle minimal parameters with defaults', async () => {
// Arrange
const mockResponse = {
error: false,
result: [],
total: 0,
};
searchServiceSpy.SearchSearch.mockReturnValue(of(mockResponse));
const params: QueryTokenInput = {};
const abortController = new AbortController();
// Act
const result = await service.searchLoyaltyItems(
params,
abortController.signal,
);
// Assert
expect(result).toEqual(mockResponse);
expect(searchServiceSpy.SearchSearch).toHaveBeenCalledWith({
filter: {
format: '!eb;!dl',
praemie: '1-',
},
input: undefined,
orderBy: undefined,
skip: 0,
take: 20,
});
});
});
});

View File

@@ -8,6 +8,7 @@ import {
SearchByTermSchema,
} from '../schemas/catalouge-search.schemas';
import { ListResponseArgs } from '@isa/common/data-access';
import { QueryTokenInput, QueryTokenSchema } from '../schemas';
@Injectable({ providedIn: 'root' })
export class CatalougeSearchService {
@@ -50,4 +51,33 @@ export class CatalougeSearchService {
return res as ListResponseArgs<Item>;
}
async searchLoyaltyItems(params: QueryTokenInput, abortSignal: AbortSignal) {
const { filter, input, orderBy, skip, take } =
QueryTokenSchema.parse(params);
const loyaltyFilter = {
format: '!eb;!dl',
praemie: '1-',
...filter,
};
const req$ = this.#searchService
.SearchSearch({
filter: loyaltyFilter,
input,
orderBy,
skip,
take,
})
.pipe(takeUntilAborted(abortSignal));
const res = await firstValueFrom(req$);
if (res.error) {
throw new Error(res.message);
}
return res as ListResponseArgs<Item>;
}
}

View File

@@ -0,0 +1,7 @@
# data-access
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test data-access` to execute the unit tests.

View File

@@ -0,0 +1,34 @@
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: 'lib',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'lib',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];

View File

@@ -0,0 +1,20 @@
{
"name": "checkout-data-access",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/checkout/data-access/src",
"prefix": "lib",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../../coverage/libs/checkout/data-access"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -0,0 +1,2 @@
export * from './lib/services';
export * from './lib/models';

View File

@@ -0,0 +1 @@
export * from './query-settings';

View File

@@ -0,0 +1,3 @@
import { UISettingsDTO } from '@generated/swagger/cat-search-api';
export type QuerySettings = UISettingsDTO;

View File

@@ -0,0 +1 @@
export * from './reward-checkout.service';

View File

@@ -0,0 +1,28 @@
import { Injectable, inject } from '@angular/core';
import { logger } from '@isa/core/logging';
import { SearchService } from '@generated/swagger/cat-search-api';
import { firstValueFrom } from 'rxjs';
import { QuerySettings } from '../models';
@Injectable({ providedIn: 'root' })
export class RewardCheckoutService {
#searchService = inject(SearchService);
#logger = logger(() => ({ service: 'RewardCheckoutService' }));
async fetchQuerySettings(): Promise<QuerySettings> {
this.#logger.info('Fetching query settings from API');
const req$ = this.#searchService.SearchLoyaltySettings();
const res = await firstValueFrom(req$);
if (res.error || !res.result) {
const error = new Error(res.message || 'Failed to fetch Query Settings');
this.#logger.error('Failed to fetch query settings', error);
throw error;
}
this.#logger.debug('Successfully fetched query settings');
return res.result as QuerySettings;
}
}

View File

@@ -0,0 +1,13 @@
import '@angular/compiler';
import '@analogjs/vitest-angular/setup-zone';
import {
BrowserTestingModule,
platformBrowserTesting,
} from '@angular/platform-browser/testing';
import { getTestBed } from '@angular/core/testing';
getTestBed().initTestEnvironment(
BrowserTestingModule,
platformBrowserTesting(),
);

View File

@@ -0,0 +1,30 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"importHelpers": true,
"moduleResolution": "bundler",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": [
"src/**/*.spec.ts",
"src/test-setup.ts",
"jest.config.ts",
"src/**/*.test.ts",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": ["src/**/*.ts"]
}

View File

@@ -0,0 +1,29 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
]
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"files": ["src/test-setup.ts"]
}

View File

@@ -0,0 +1,27 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import angular from '@analogjs/vite-plugin-angular';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/checkout/data-access',
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
setupFiles: ['src/test-setup.ts'],
reporters: ['default'],
coverage: {
reportsDirectory: '../../../coverage/libs/checkout/data-access',
provider: 'v8' as const,
},
},
}));

View File

@@ -0,0 +1,7 @@
# reward-catalog
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test reward-catalog` to execute the unit tests.

View File

@@ -0,0 +1,34 @@
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: 'lib',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'lib',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];

View File

@@ -0,0 +1,20 @@
{
"name": "reward-catalog",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/checkout/feature/reward-catalog/src",
"prefix": "lib",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../../../coverage/libs/checkout/feature/reward-catalog"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -0,0 +1 @@
export * from './lib/routes';

View File

@@ -0,0 +1,9 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import {
QuerySettings,
RewardCheckoutService,
} from '@isa/checkout/data-access';
export const querySettingsResolverFn: ResolveFn<QuerySettings> = () =>
inject(RewardCheckoutService).fetchQuerySettings();

View File

@@ -0,0 +1 @@
export * from './reward-catalog.resource';

View File

@@ -0,0 +1,33 @@
import { inject, resource } from '@angular/core';
import {
CatalougeSearchService,
QueryTokenInput,
} from '@isa/catalogue/data-access';
import { ResponseArgsError } from '@isa/common/data-access';
import { SearchTrigger } from '@isa/shared/filter';
export const createRewardCatalogResource = (
params: () => {
queryToken: QueryTokenInput;
searchTrigger: SearchTrigger | 'reload' | 'initial';
},
) => {
const catalogSearchService = inject(CatalougeSearchService);
return resource({
params,
loader: async ({ abortSignal, params }) => {
const fetchCatalogResponse =
await catalogSearchService.searchLoyaltyItems(
params.queryToken,
abortSignal,
);
if (fetchCatalogResponse?.error) {
throw new ResponseArgsError(fetchCatalogResponse);
}
return fetchCatalogResponse;
},
});
};

View File

@@ -0,0 +1,9 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'reward-catalog-item',
templateUrl: './reward-catalog-item.component.html',
styleUrl: './reward-catalog-item.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RewardCatalogItemComponent {}

View File

@@ -0,0 +1,70 @@
<reward-start-card></reward-start-card>
<!-- <filter-controls-panel (triggerSearch)="search($event)"></filter-controls-panel> -->
<span
class="text-isa-neutral-900 isa-text-body-2-regular self-start"
data-what="result-count"
>
<!-- {{ hits() }} Einträge -->
XXX Einträge
</span>
<div class="flex flex-col gap-4 w-full items-center justify-center mb-24">
<!-- @if (hits()) {
<!-- @for (item of items(); track item.id) {
@defer (on viewport) {
<reward-catalog-item
#listElement
[item]="item"
[stock]="getStockForItem(item)"
[stockFetching]="inStockFetching()"
[productGroupValue]="getProductGroupValueForItem(item)"
(inProgressChange)="onListItemActionInProgress($event)"
></reward-catalog-item>
} @placeholder {
<div class="h-[7.75rem] w-full flex items-center justify-center">
<ui-icon-button
[pending]="true"
[color]="'tertiary'"
data-what="load-spinner"
data-which="item-placeholder"
></ui-icon-button>
</div>
}
}
} @else {
<ui-empty-state
class="w-full justify-self-center"
title="Keine Suchergebnisse"
description="Bitte prüfen Sie die Schreibweise oder ändern Sie die Filtereinstellungen."
>
</ui-empty-state>
} -->
<ui-empty-state
class="w-full justify-self-center"
title="Keine Suchergebnisse"
description="Bitte prüfen Sie die Schreibweise oder ändern Sie die Filtereinstellungen."
>
</ui-empty-state>
</div>
<!--
<ui-stateful-button
class="fixed right-6 bottom-6"
(clicked)="remitItems()"
(action)="remitItems()"
[(state)]="remitItemsState"
defaultContent="Prämie auswählen"
defaultWidth="13rem"
[errorContent]="remitItemsError()"
errorWidth="32rem"
errorAction="Erneut versuchen"
successContent="Hinzugefügt"
successWidth="20rem"
size="large"
color="brand"
[pending]="remitItemsInProgress()"
[disabled]="!hasSelectedItems() || listItemActionInProgress()"
>
</ui-stateful-button>} -->

View File

@@ -0,0 +1,131 @@
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
signal,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import {
provideFilter,
withQuerySettingsFactory,
withQueryParamsSync,
FilterControlsPanelComponent,
SearchTrigger,
FilterService,
} from '@isa/shared/filter';
import { IconButtonComponent } from '@isa/ui/buttons';
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
import { RewardStartCardComponent } from './reward-start-card/reward-start-card.component';
import { logger } from '@isa/core/logging';
import { createRewardCatalogResource } from './resources';
import { EmptyStateComponent } from '@isa/ui/empty-state';
import { RewardCatalogItemComponent } from './reward-catalog-item/reward-catalog-item.component';
/**
* Factory function to retrieve query settings from the activated route data.
* @returns The query settings from the activated route data.
*/
function querySettingsFactory() {
return inject(ActivatedRoute).snapshot.data['querySettings'];
}
@Component({
selector: 'reward-catalog',
templateUrl: './reward-catalog.component.html',
styleUrl: './reward-catalog.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
provideFilter(
withQuerySettingsFactory(querySettingsFactory),
withQueryParamsSync(),
),
],
imports: [
FilterControlsPanelComponent,
IconButtonComponent,
EmptyStateComponent,
RewardStartCardComponent,
RewardCatalogItemComponent,
],
host: {
'[class]':
'"w-full flex flex-col gap-4 mt-5 isa-desktop:mt-6 overflow-x-hidden"',
},
})
export class RewardCatalogComponent {
/**
* Signal to trigger searches in the reward catalog.
* Can be 'reload', 'initial', or a specific SearchTrigger value.
*/
searchTrigger = signal<SearchTrigger | 'reload' | 'initial'>('initial');
/**
* Logger instance for logging component events and errors.
* @private
*/
#logger = logger(() => ({
component: 'RewardCatalogComponent',
}));
/**
* FilterService instance for managing filter state and queries.
* @private
*/
// #filterService = inject(FilterService);
/**
* Restores scroll position when navigating back to this component.
*/
restoreScrollPosition = injectRestoreScrollPosition();
/**
* Resource for fetching and managing the reward catalog data.
* Uses the FilterService to get the current query token and reacts to search triggers.
* @private
* @returns The reward catalog resource.
*/
// rewardCatalogResource = createRewardCatalogResource(() => {
// return {
// queryToken: this.#filterService.query(),
// searchTrigger: this.searchTrigger(),
// };
// });
/**
* Computed signal for the current reward catalog response.
* @returns The latest reward catalog response or undefined.
*/
// listResponseValue = computed(() => this.rewardCatalogResource.value());
/**
* Computed signal indicating whether the reward catalog resource is currently fetching data.
* @returns True if fetching, false otherwise.
*/
// listFetching = computed(
// () => this.rewardCatalogResource.status() === 'loading',
// );
/**
* Computed signal for the reward catalog items to display.
* @returns Array of Item.
*/
// items = computed(() => {
// const value = this.listResponseValue();
// return value?.result ? value.result : [];
// });
/**
* Computed signal for the total number of hits in the reward catalog.
* @returns Number of hits, or 0 if unavailable.
*/
// hits = computed(() => {
// const value = this.listResponseValue();
// return value?.hits ? value.hits : 0;
// });
search(trigger: SearchTrigger): void {
// this.#filterService.commit();
this.searchTrigger.set(trigger);
}
}

View File

@@ -0,0 +1,11 @@
:host {
@apply w-full grid grid-cols-[1fr,auto] gap-6 rounded-2xl bg-isa-neutral-400 p-6 justify-between;
}
.reward-start-card__title-container {
@apply flex flex-col gap-4 text-isa-neutral-900;
}
.reward-start-card__select-cta {
@apply h-12 w-[13rem] justify-self-end;
}

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