Compare commits

...

46 Commits

Author SHA1 Message Date
Nino
888e505f1a Merge branch 'develop' into fix/5411-Reward-Crm-Change-Adress 2025-11-10 14:35:07 +01:00
Nino
b28f79cd1d fix(crm-data-access, customer-details, reward-shopping-cart): persist selected addresses across navigation flows
Implement address selection persistence using CRM tab metadata to ensure
selected shipping addresses and payers are retained throughout the customer
selection flow, particularly when navigating from Kundenkarte to reward cart.

Changes include:
- Create PayerResource and CustomerPayerAddressResource to load selected
  payer from tab metadata with fallback to customer as payer
- Create PayerService to fetch payer data from CRM API with proper error
  handling and abort signal support
- Update BillingAndShippingAddressCardComponent to prefer selected addresses
  from metadata over customer defaults, with computed loading state
- Refactor continue() flow in CustomerDetailsViewMainComponent to load
  selected addresses from metadata before setting in checkout service
- Add adapter logic to convert CRM payer/shipping address types to checkout
  types with proper type casting for incompatible enum types
- Implement fallback chain: metadata selection → component state → customer
  default for both payer and shipping address

This ensures address selections made in the address selection dialogs are
properly preserved and applied when completing the customer selection flow,
fixing the issue where addresses would revert to customer defaults.

Ref: #5411
2025-11-10 14:33:18 +01:00
Lorenz Hilpert
b984a2cac2 Merged PR 2014: fix(open-reward-tasks): filter out pickup-only reward orders
fix(open-reward-tasks): filter out pickup-only reward orders

Exclude reward orders with only pickup items (supplier_id: '16') from open tasks
as these cannot be completed in the reward shop.

Fixes BUG-5444
2025-11-10 13:11:49 +00:00
Lorenz Hilpert
b0afc80a26 Merged PR 2013: 🐛 fix(order-destination): display 'Hugendubel Digital' for download destinat...
🐛 fix(order-destination): display 'Hugendubel Digital' for download destinations

Fixes #5453

- Add handling for Download order type in name() computed signal to return 'Hugendubel Digital'
- Add handling for Download order type in address() computed signal to return undefined
- Update template to conditionally show pipe separator and address only when address exists
- Add isaDeliveryDownload icon for download order type
- Remove unused OrderType import

Related work items: #5453
2025-11-07 16:21:08 +00:00
Lorenz Hilpert
3bc6d47c31 Merged PR 2012: fix(purchase-options): resolve Lesepunkte delivery method change error
fix(purchase-options): resolve Lesepunkte delivery method change error

  The commit includes:
  - Restored ensureCurrencyDefaults import that was accidentally removed
  - Fixed immutability violations in both getAddToShoppingCartDTOForItem and getUpdateShoppingCartItemDTOForItem methods
  - Proper handling of frozen NgRx ComponentStore state objects
  - Resolves bug #5452 where Lesepunkte delivery method changes failed

  The pre-commit hooks ran successfully (ESLint passed with no changes needed). The fix is now ready to be pushed and tested.

Related work items: #5452
2025-11-07 15:56:50 +00:00
Lorenz Hilpert
e05deeb8bc Merged PR 2011: 🐛 fix(checkout): include download orders in destination update flow
🐛 fix(checkout): include download orders in destination update flow

Fixes #5448 - Prämienshop download orders now properly update destination
logistician before order creation. Previously, download orders were excluded
from the destination update step, causing null logistician errors during
order creation in the reward shop.

The fix adds hasDownload to the condition that determines whether to update
destination shipping addresses, ensuring download items (including those
purchased with loyalty points) get their logistician properly assigned.

Related work items: #5448
2025-11-07 15:07:49 +00:00
Nino Righi
11e2aaff8d Merged PR 2010: fix(reward-shopping-cart-item, filter-service): exclude downloads and text in...
fix(reward-shopping-cart-item, filter-service): exclude downloads and text inputs from counts

Hide low stock warning for download items in reward shopping cart, as
downloads don't have physical inventory constraints.

Exclude text/searchbox inputs from selected filter count calculation
to prevent search queries from inflating the filter badge counter.

Remove unused isIconButtonActive computed property in filter menu button.

Ref: #5441
2025-11-07 15:03:41 +00:00
Lorenz Hilpert
731df8414d Merged PR 2009: fix shopping cart - sync issues 2025-11-07 12:18:31 +00:00
Nino Righi
f04e36e710 Merged PR 2008: fix(reward-print, reward-popup, reward-destination): improve reward cart stab...
fix(reward-print, reward-popup, reward-destination): improve reward cart stability and UX

- fix: remove console.log statement from calculate-price-value helper
- fix: add loading/pending state to print button to prevent duplicate prints
- fix: debounce reward selection resource reloading to prevent race conditions
- fix: correct reward cart item destination-info alignment and flex behavior
- fix: support OrderType in OrderDestinationComponent alongside OrderTypeFeature
- fix: use unitPrice instead of total for price calculations in reward items
- refactor: update calculatePriceValue test descriptions for clarity
- fix: fallback to order.orderType when features don't contain orderType

The reward selection popup now properly waits for all resources to reload
before resolving, preventing timing issues with cart synchronization.
Print button shows pending state during print operations.
Destination info components now handle both legacy OrderType and new
OrderTypeFeature enums for better compatibility.

Ref: #5442, #5445
2025-11-06 16:32:10 +00:00
Lorenz Hilpert
af7bad03f5 💄 style: reformat crm-customer.service for improved readability
Apply consistent code formatting with multi-line function parameters and object properties throughout the CRM customer service. No functional changes.
2025-11-06 17:30:18 +01:00
Lorenz Hilpert
8e4d4ff804 🔄 chore: sync swagger API clients with backend updates
- Regenerated all Swagger API clients (availability, checkout, crm, isa, print, wws)
- Updated CRM loyalty card API endpoints (removed interests, added booking/bon management)
- Temporarily disabled interests form block functionality due to API changes
- Removed deprecated models (check-loyalty-card-result, loyalty-card-status, entity-key-value)
- Added new loyalty booking and bon management models and services
2025-11-06 17:03:37 +01:00
Lorenz Hilpert
89b3d9aa60 Merged PR 2000: open tasks
Related work items: #5309
2025-11-06 10:01:41 +00:00
Lorenz Hilpert
1d4c900d3a 🔧 chore: configure junit and cobertura test reporting
- Add junit reporter to vitest configs for CI/CD integration
- Enable cobertura coverage reports for test analytics
- Add @ts-expect-error comment for complex vitest reporter types
- Remove duplicate test target from reward-selection-dialog project.json
2025-11-06 10:46:38 +01:00
Nino Righi
a6f0aaf1cc Merged PR 2007: fix(filter-service, number-range-filter-input): resolve NumberRange state man...
fix(filter-service, number-range-filter-input): resolve NumberRange state management and reference issues

**Root Cause:**
Filter service was experiencing reference sharing between current state and
committed state due to shallow copying in commit(), causing filters to
incorrectly appear as "changed" when mixing NumberRange with other filter
types like Checkbox.

**Changes Made:**

1. **State Management (filter.service.ts):**
   - Use structuredClone() in commit() for deep copies to prevent reference sharing
   - Update clear() to preserve structural properties (options array references)
   - Refactor resetInput() to selectively copy only mutable properties while
     preserving structural ones for isEqual() comparisons
   - Simplify selectedFilterCount to use isDefaultFilterInput() consistently

2. **Default Values (number-range-filter-input.mapping.ts):**
   - Parse minValue/maxValue from config (e.g., "1-" → 1) as defaults
   - Use parsed defaults as initial min/max when no explicit value provided

**Impact:**
- NumberRange filters correctly display default values in UI
- Filters no longer incorrectly show as "changed" after multiple commits
- "Standardeinstellungen" works correctly when mixing NumberRange with other types
- selectedFilterCount accurately reflects changed filters including NumberRange

Ref: #5402
2025-11-05 20:17:52 +00:00
Nino Righi
b8e2d3f87b Merged PR 2006: feature(checkout-complete-order): Added Error Feedback to reward checkout
feature(checkout-complete-order): Added Error Feedback to reward checkout

Fixed Build Errors inside purchase-options

Ref: #5416
2025-11-05 20:17:26 +00:00
Nino
27aa694158 fix(chore): Updated Package-Lock to fix build related issues 2025-11-05 16:59:00 +01:00
Nino
196b9a237a Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2025-11-05 16:44:15 +01:00
Nino
6a2ba30a01 fix(chore): Build Errors Fix 2025-11-05 16:43:50 +01:00
Nino Righi
eb0d96698c Merged PR 2005: feat(shared-filter, ui-switch): add switch filter menu button for inline toggle filters
feat(shared-filter, ui-switch): add switch filter menu button for inline toggle filters

Add a new SwitchMenuButtonComponent that renders filter inputs as compact toggle switches
without an overlay menu. This provides a more streamlined UX for simple boolean/single-option
filters directly in the controls panel.

Key changes:
- Create new switch-menu module with button component and tests
- Extend FilterControlsPanelComponent to accept switchFilters input array
- Rename IconSwitchComponent to SwitchComponent for consistency
- Update filter actions to use 'target' property instead of 'group' for filtering
- Add isEmptyFilterInput support for NumberRange inputs
- Export switch-menu module from shared/filter public API

The switch button auto-commits on toggle and uses the checkbox filter model internally,
allowing simple configuration like:

switchFilters = [{ filter: stockFilter, icon: 'isaFiliale' }]

This implementation follows the existing filter architecture patterns and maintains
full accessibility support through ARIA attributes and keyboard navigation.

Ref: #5427
2025-11-05 15:31:13 +00:00
Lorenz Hilpert
a52928d212 🔧 chore: remove memory MCP server and context-manager documentation
Remove the memory MCP server configuration and all references to the context-manager agent with persistent memory capabilities from CLAUDE.md.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 14:04:52 +01:00
Lorenz Hilpert
d46bf462cb Merged PR 2004: 🐛 fix(checkout): remove manual cart counter updates (#5412)
🐛 fix(checkout): remove manual cart counter updates (#5412)

Remove updateProcessCount method and all manual calls to it.
The cart counter is now updated through reactive mechanisms,
ensuring it stays in sync when items are added or removed.

Fixes bug where reward cart icon counter was not updating
when items were removed from the cart.

Related work items: #5412
2025-11-04 16:46:19 +00:00
Lorenz Hilpert
a2833b669d Merged PR 2003: 🐛 fix(purchase-options): correct popup display for e-books and downloads
🐛 fix(purchase-options): correct popup display for e-books and downloads

- Check item formats (DL/EB) instead of backend availabilities to determine download-only items
- Display 'Derzeit nicht verfügbar' warning when download items lack availability
- Prevent 'Geringer Bestand' message from showing for:
  * Download/e-book items
  * Items with zero stock
  * Items with pending in-store availability requests

Fixes #5410

Related work items: #5410
2025-11-04 16:06:06 +00:00
Lorenz Hilpert
cc62441f58 Merged PR 2002: fix(checkout): resolve itemType validation error for download items
fix(checkout): resolve itemType validation error for download items

Updates ItemTypeSchema to accept bitwise flag combinations instead of
only individual enum values. The backend returns combined itemType
values (e.g., 20480 = ItemPrice | Download) which were causing Zod
validation errors when adding download/e-book items to cart.

Changes:
- Update ItemTypeSchema to use bitwise validation pattern
- Add comprehensive unit tests (24 tests) covering individual flags,
  combinations, and edge cases
- Follow same pattern as NotificationChannelSchema and CRUDASchema

Closes #5429

Related work items: #5429
2025-11-04 15:14:39 +00:00
Lorenz Hilpert
e1681d8867 Merged PR 2001: fix(auth): handle empty user state on login
fix(auth): handle empty user state on login

Resolves error when user state is empty during login process.

Refs #5431

Related work items: #5431
2025-11-04 15:13:21 +00:00
Lorenz Hilpert
ce86014300 ♻️ refactor(filter): replace group-based filtering with target-based filtering
Replace the `group` property with `target` property in BaseFilterInputSchema to explicitly distinguish between 'filter' and 'input' query parameters. This improves code clarity and provides better semantic meaning.

**Changes:**
- Add `target` property to BaseFilterInputSchema with type 'filter' | 'input' and default 'input'
- Update filter.service.ts to use `target` instead of `group` for filtering inputs
- Update all filter input mappings (checkbox, date-range, number-range, text) to include `target` property
- Update all affected unit tests (9 test files) to include `target` in mock data

**Tests:** All 128 unit tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 14:34:13 +01:00
Nino Righi
bdb8aac8df Merged PR 1999: fix(reward-list): Trigger Resource if reload gets triggered
fix(reward-list): Trigger Resource if reload gets triggered

Ref: #5423
2025-11-03 20:01:18 +00:00
Nino Righi
a49ea25fd0 Merged PR 1993: feat(action-handler, printing, schemas)
1 commit: Bestellbestätigung drucken
2. commit: Schemas
3. commit: Action/Command handler

feat(action-handler, printing, schemas): add handle command service for automated action execution

Implement HandleCommandService and facade to execute order actions automatically
after reward collection. Add action handler infrastructure with 23 handlers
(Accepted, Arrived, Assembled, etc.). Integrate automatic receipt fetching for
print commands. Add schema validation for command handling and receipt queries.
Update reward confirmation to trigger actions after successful collection.

- Add HandleCommandService with command orchestration
- Add HandleCommandFacade as public API layer
- Create schemas: HandleCommandSchema, FetchReceiptsByOrderItemSubsetIdsSchema
- Add helpers: getMainActions, buildItemQuantityMap
- Register 23 action handlers in reward confirmation routes
- Support PRINT_SHIPPINGNOTE and PRINT_SMALLAMOUNTINVOICE auto-fetching
- Update CoreCommandModule for forRoot/forChild patterns
- Add comprehensive unit tests for new services and helpers
- Apply prettier formatting to command and printing modules

Ref: #5394
2025-11-03 20:00:53 +00:00
Lorenz Hilpert
53a062dcde docs: fix and shorten skill descriptions
- Add missing frontmatter to html-template skill (name and description fields)
- Shorten 7 verbose skill descriptions from 300+ to ~150-180 characters
- Improve readability while preserving essential information

Skills updated:
- angular-template (363 → 174 chars)
- circular-dependency-resolver (326 → 173 chars)
- architecture-enforcer (319 → 178 chars)
- api-change-analyzer (319 → 178 chars)
- swagger-sync-manager (318 → 159 chars)
- library-scaffolder (314 → 146 chars)
- test-migration-specialist (308 → 162 chars)
- html-template (fixed missing frontmatter)
2025-11-03 11:56:28 +01:00
Nino Righi
32c7531d2b Merged PR 1998: fix(order-confirmation-item): Adjusted Layout Mobile and Desktop
fix(order-confirmation-item): Adjusted Layout Mobile and Desktop

Ref: #5424
2025-11-03 10:01:45 +00:00
Nino Righi
7894c7b768 Merged PR 1997: fix(filter-input.to-record-mapping): Removed " from InputType NumberRange map...
fix(filter-input.to-record-mapping): Removed " from InputType NumberRange mapping

Ref: #5425
2025-11-03 10:00:32 +00:00
Nino Righi
f175b5d2af Merged PR 1996: fix(crm): consolidate customer feature selection logic
fix(crm): consolidate customer feature selection logic

Introduce centralized `getEnabledCustomerFeature` helper to standardize
feature selection across components. Replaces inconsistent filtering
approaches with unified logic that prioritizes 'd-account' and
'd-no-account' features.

Changes:
- Add `getEnabledCustomerFeature` helper with unit tests
- Add `CustomerFeatureKey` and `CustomerFeatureGroup` enums
- Update customer-order-details-header component
- Update pickup-shelf-details-header component
- Update customer-result-list components
- Update order-details-main-view component

Ref: #5432
2025-11-03 10:00:02 +00:00
Nino Righi
7a04b828c3 Merged PR 1995: feature(reward-action): Added implementation for Stateful Button
feature(reward-action): Added implementation for Stateful Button

Ref: #5415
2025-11-03 09:56:39 +00:00
Nino Righi
fcda6b9a75 Merged PR 1994: feature(reward-list): Added Scroll to Top Button
feature(reward-list): Added Scroll to Top Button

Ref: #5414
2025-11-03 09:55:40 +00:00
Nino
27f4ef490f fix(process-bar): Added cart-checkout process type to filtered selection of processes
Ref: #5430
2025-10-31 14:40:16 +01:00
Nino
87f9044511 Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2025-10-30 15:24:06 +01:00
Nino
55219f125b fix(process-bar): Simulate "old tab logic" for reward release 2025-10-30 15:22:30 +01:00
Lorenz Hilpert
fd8e0194ac 🚚 refactor(skills): reorganize skill structure
- Rename logging-helper to logging for consistency
- Remove git-commit-helper (superseded by /commit command)
- Add git-workflow skill for Git Flow operations

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 13:39:14 +01:00
Lorenz Hilpert
c7fc8d8661 🚚 refactor(skills): rename tailwind-isa skill to tailwind
- Rename skill directory from tailwind-isa to tailwind
- Update skill name in frontmatter metadata
- Update all references in CLAUDE.md (2 locations)

Simplifies naming while maintaining clear purpose. The skill provides
Tailwind CSS utilities and design system guidance for the ISA project.
2025-10-29 13:37:56 +01:00
Lorenz Hilpert
bf30ec1213 feat(skills): create html-template skill for E2E and ARIA attributes
- Create comprehensive html-template skill with 3 reference files
  - E2E Testing Attributes (data-what, data-which patterns)
  - ARIA Accessibility Attributes (roles, properties, states, WCAG)
  - Combined Patterns (real-world examples with both)
- Move E2E attribute guidance from command to skill
- Add extensive ARIA accessibility documentation
- Update angular-template skill with cross-references
- Remove dev-add-e2e-attrs command (functionality now in skill)

The new skill provides 3,322 lines of comprehensive documentation
covering both testing and accessibility best practices for HTML
templates, with practical examples for forms, navigation, tables,
dialogs, and more.

Benefits:
- Clean separation: Angular syntax vs HTML attributes
- Reusable: html-template works with any HTML context
- Comprehensive: E2E + ARIA in one place
- Integrated: Works seamlessly with angular-template skill
2025-10-29 13:21:29 +01:00
Nino Righi
f87d3a35d9 Merged PR 1990: feat(crm-data-access,checkout): improve primary bonus card selection logic
feat(crm-data-access,checkout): improve primary bonus card selection logic

Enhance getPrimaryBonusCard helper to sort cards alphabetically by code
when multiple primary cards exist or when no primary card is designated.
This ensures deterministic card selection across the application.

Add comprehensive test coverage for edge cases including:
- Multiple primary cards (returns first alphabetically)
- No primary cards (returns first alphabetically)
- Empty bonus cards array (returns undefined)

Add TODO comments in ShoppingCartService for future refactoring of
cart handling logic to improve code organization and reusability.

Ref: #5407
2025-10-29 10:33:21 +00:00
Lorenz Hilpert
6db5f2afda 📝 docs: enhance context-manager with persistent memory and task management
- Add autonomous knowledge storage with MCP memory tools
- Implement task capture system for user-assigned TODOs
- Enable cross-session persistence of architectural decisions
- Add task status tracking (pending, in-progress, blocked, completed)
- Update CLAUDE.md with context-manager capabilities
- Configure memory MCP server in .mcp.json
- Document seven entity types for knowledge graph
- Add proactive task reminders at session start
2025-10-29 10:58:45 +01:00
Lorenz Hilpert
c2c40a44e8 📝 docs: initialize CHANGELOG.md with recent changes
Add changelog file following Keep a Changelog format.

Documents recent additions including checkout reward features, stock info
batching, CRM improvements, and Angular/Tailwind development skills.
2025-10-28 21:04:17 +01:00
Lorenz Hilpert
5e73fc1dab 🧑‍💻 chore: add EOD report and changelog generation slash commands
Add two new Claude Code slash commands for developer workflow:
- /eod-report: Generate daily work summaries from git commits
- /generate-changelog: Create changelog entries from git tags

Both commands support flexible date ranges and output formatting.
2025-10-28 21:04:04 +01:00
Lorenz Hilpert
9e5a1d2287 🙈 chore: add .vite and reports/ to gitignore
Exclude Vite build artifacts and generated EOD reports from version control.
2025-10-28 21:03:49 +01:00
Lorenz Hilpert
c769af7021 Merged PR 1987: Carousel Lobrary
Related work items: #5408
2025-10-28 10:34:57 +00:00
Lorenz Hilpert
bfd151dd84 Merged PR 1989: fix(checkout): resolve currency constraint violations in price handling
fix(checkout): resolve currency constraint violations in price handling

- Add ensureCurrencyDefaults() helper to normalize price objects with EUR defaults
- Fix currency constraint violation in shopping cart item additions (bug #5405)
- Apply price normalization across availability, checkout, and shopping cart services
- Update 8 locations: availability.adapter, checkout.service, shopping-cart.service,
  get-availability-params.adapter, availability-transformers, reward quantity control
- Refactor OrderType to @isa/common/data-access for cross-domain reusability
- Remove duplicate availability service from catalogue library
- Enhance PriceValue and VatValue schemas with proper currency defaults
- Add availability-transformers.spec.ts test coverage
- Fix QuantityControl fallback from 0 to 1 to prevent invalid state warnings

Resolves issue where POST requests to /checkout/v6/store/shoppingcart/{id}/item
were sending price objects without required currency/currencySymbol fields,
causing 400 Bad Request with 'Currency: Constraint violation: NotNull' error.

Related work items: #5405
2025-10-28 10:34:39 +00:00
638 changed files with 64077 additions and 49544 deletions

View File

@@ -1,65 +1,178 @@
---
name: context-manager
description: Context management specialist for multi-agent workflows and long-running tasks. Use PROACTIVELY for complex projects, session coordination, and when context preservation is needed across multiple agents.
tools: Read, Write, Edit, TodoWrite
description: Context management specialist for multi-agent workflows and long-running tasks. Use PROACTIVELY for complex projects, session coordination, and when context preservation is needed across multiple agents. AUTONOMOUSLY stores project knowledge in persistent memory.
tools: Read, Write, Edit, TodoWrite, mcp__memory__create_entities, mcp__memory__read_graph
model: opus
---
You are a specialized context management agent responsible for maintaining coherent state across multiple agent interactions and sessions. Your role is critical for complex, long-running projects.
**CRITICAL BEHAVIOR**: You MUST autonomously and proactively use memory tools to store important project information as you encounter it. DO NOT wait for explicit instructions to store information.
## Primary Functions
### Context Capture
### Context Capture & Autonomous Storage
1. Extract key decisions and rationale from agent outputs
2. Identify reusable patterns and solutions
3. Document integration points between components
4. Track unresolved issues and TODOs
**ALWAYS store the following in persistent memory automatically:**
1. **Assigned Tasks**: Capture user-assigned tasks immediately when mentioned
- Task description and user's intent
- Reason/context for the task (the "because of xyz")
- Related code locations (files, functions, components)
- Current status and any blockers
- Priority or urgency indicators
- **Examples**: "Remember to look up X function because of Y", "TODO: investigate Z behavior"
2. **Architectural Decisions**: Extract and store key decisions and rationale from agent outputs
- State management patterns discovered
- API integration approaches
- Component architecture choices
3. **Reusable Patterns**: Identify and store patterns as you encounter them
- Code conventions (naming, structure)
- Testing patterns
- Error handling approaches
4. **Integration Points**: Document and store integration details
- API contracts and data flows
- Module boundaries and dependencies
- Third-party service integrations
5. **Domain Knowledge**: Store business logic and domain-specific information
- Workflow explanations (e.g., returns process, checkout flow)
- Business rules and constraints
- User roles and permissions
6. **Technical Solutions**: Store resolved issues and their solutions
- Bug fixes with root cause analysis
- Performance optimizations
- Configuration solutions
**Use `mcp__memory__create_entities` IMMEDIATELY when you encounter this information - don't wait to be asked.**
### Context Distribution
1. Prepare minimal, relevant context for each agent
2. Create agent-specific briefings
3. Maintain a context index for quick retrieval
4. Prune outdated or irrelevant information
1. **ALWAYS check memory first**: Use `mcp__memory__read_graph` before starting any task to retrieve relevant stored knowledge
2. Prepare minimal, relevant context for each agent
3. Create agent-specific briefings enriched with stored memory
4. Maintain a context index for quick retrieval
5. Prune outdated or irrelevant information
### Memory Management
### Memory Management Strategy
- Store critical project decisions in memory
- Maintain a rolling summary of recent changes
- Index commonly accessed information
- Create context checkpoints at major milestones
**Persistent Memory (PRIORITY - use MCP tools)**:
- **CREATE**: Use `mcp__memory__create_entities` to store entities with relationships:
- Entity types: task, decision, pattern, integration, solution, convention, domain-knowledge
- Include observations (what was learned/assigned) and relations (how entities connect)
- **RETRIEVE**: Use `mcp__memory__read_graph` to query stored knowledge:
- Before starting new work (check for pending tasks, related patterns/decisions)
- When user asks "what was I working on?" (retrieve task history)
- When encountering similar problems (find previous solutions)
- When making architectural choices (review past decisions)
- At session start (remind user of pending/incomplete tasks)
**Ephemeral Memory (File-based - secondary)**:
- Maintain rolling summaries in temporary files
- Create session checkpoints
- Index recent activities
## Workflow Integration
When activated, you should:
**On every activation, you MUST:**
1. Review the current conversation and agent outputs
2. Extract and store important context
3. Create a summary for the next agent/session
4. Update the project's context index
5. Suggest when full context compression is needed
1. **Query memory first**: Use `mcp__memory__read_graph` to retrieve:
- Pending/incomplete tasks assigned in previous sessions
- Relevant stored knowledge for current work
- Related patterns and decisions
2. **Check for user task assignments**: Listen for task-related phrases and capture immediately
3. **Review current work**: Analyze conversation and agent outputs
4. **Store new discoveries**: Use `mcp__memory__create_entities` to store:
- ANY new tasks mentioned by user
- Important information discovered
- Task status updates (pending → in-progress → completed)
5. **Create summaries**: Prepare briefings enriched with memory context
6. **Update indexes**: Maintain project context index
7. **Suggest compression**: Recommend when full context compression is needed
**Key behaviors:**
- **TASK PRIORITY**: Capture and store user task assignments IMMEDIATELY when mentioned
- Store information PROACTIVELY without being asked
- Query memory BEFORE making recommendations
- Link new entities to existing ones for knowledge graph building
- Update existing entities when information evolves (especially task status)
- **Session Start**: Proactively remind user of pending/incomplete tasks from memory
## Context Formats
### Quick Context (< 500 tokens)
- Current task and immediate goals
- Recent decisions affecting current work
- Recent decisions affecting current work (query memory first)
- Active blockers or dependencies
- Relevant stored patterns from memory
### Full Context (< 2000 tokens)
- Project architecture overview
- Key design decisions
- Integration points and APIs
- Project architecture overview (enriched with stored decisions)
- Key design decisions (retrieved from memory)
- Integration points and APIs (from stored knowledge)
- Active work streams
### Archived Context (stored in memory)
### Persistent Context (stored in memory via MCP)
- Historical decisions with rationale
- Resolved issues and solutions
- Pattern library
- Performance benchmarks
**Store these entity types:**
- `task`: User-assigned tasks, reminders, TODOs with context and status
- `decision`: Architectural and design decisions with rationale
- `pattern`: Reusable code patterns and conventions
- `integration`: API contracts and integration points
- `solution`: Resolved issues with root cause and fix
- `convention`: Coding standards and project conventions
- `domain-knowledge`: Business logic and workflow explanations
Always optimize for relevance over completeness. Good context accelerates work; bad context creates confusion.
**Entity structure examples:**
**Task entity (NEW - PRIORITY):**
```json
{
"name": "investigate-checkout-pricing-calculation",
"entityType": "task",
"observations": [
"User requested: 'Remember to look up the pricing calculation function'",
"Reason: Pricing appears incorrect for bundle products in checkout",
"Located in: libs/checkout/feature-cart/src/lib/services/pricing.service.ts",
"Status: pending",
"Priority: high - affects production checkout",
"Related components: checkout-summary, cart-item-list"
],
"relations": [
{"type": "relates_to", "entity": "checkout-domain-knowledge"},
{"type": "blocks", "entity": "bundle-pricing-bug-fix"}
]
}
```
**Other entity types:**
```json
{
"name": "descriptive-entity-name",
"entityType": "decision|pattern|integration|solution|convention|domain-knowledge",
"observations": ["what was learned", "why it matters", "how it's used"],
"relations": [
{"type": "relates_to|depends_on|implements|solves|blocks", "entity": "other-entity-name"}
]
}
```
**Task Status Values**: `pending`, `in-progress`, `blocked`, `completed`, `cancelled`
**Task Capture Triggers**: Listen for phrases like:
- "Remember to..."
- "TODO: ..."
- "Don't forget..."
- "Look into..."
- "Investigate..."
- "Need to check..."
- "Follow up on..."
Always optimize for relevance over completeness. Good context accelerates work; bad context creates confusion. **Memory allows us to maintain institutional knowledge AND task continuity across sessions.**

View File

@@ -1,33 +0,0 @@
---
name: data-engineer
description: Data pipeline and analytics infrastructure specialist. Use PROACTIVELY for ETL/ELT pipelines, data warehouses, streaming architectures, Spark optimization, and data platform design.
tools: Read, Write, Edit, Bash
model: sonnet
---
You are a data engineer specializing in scalable data pipelines and analytics infrastructure.
## Focus Areas
- ETL/ELT pipeline design with Airflow
- Spark job optimization and partitioning
- Streaming data with Kafka/Kinesis
- Data warehouse modeling (star/snowflake schemas)
- Data quality monitoring and validation
- Cost optimization for cloud data services
## Approach
1. Schema-on-read vs schema-on-write tradeoffs
2. Incremental processing over full refreshes
3. Idempotent operations for reliability
4. Data lineage and documentation
5. Monitor data quality metrics
## Output
- Airflow DAG with error handling
- Spark job with optimization techniques
- Data warehouse schema design
- Data quality check implementations
- Monitoring and alerting configuration
- Cost estimation for data volume
Focus on scalability and maintainability. Include data governance considerations.

View File

@@ -1,590 +0,0 @@
---
name: database-architect
description: Database architecture and design specialist. Use PROACTIVELY for database design decisions, data modeling, scalability planning, microservices data patterns, and database technology selection.
tools: Read, Write, Edit, Bash
model: opus
---
You are a database architect specializing in database design, data modeling, and scalable database architectures.
## Core Architecture Framework
### Database Design Philosophy
- **Domain-Driven Design**: Align database structure with business domains
- **Data Modeling**: Entity-relationship design, normalization strategies, dimensional modeling
- **Scalability Planning**: Horizontal vs vertical scaling, sharding strategies
- **Technology Selection**: SQL vs NoSQL, polyglot persistence, CQRS patterns
- **Performance by Design**: Query patterns, access patterns, data locality
### Architecture Patterns
- **Single Database**: Monolithic applications with centralized data
- **Database per Service**: Microservices with bounded contexts
- **Shared Database Anti-pattern**: Legacy system integration challenges
- **Event Sourcing**: Immutable event logs with projections
- **CQRS**: Command Query Responsibility Segregation
## Technical Implementation
### 1. Data Modeling Framework
```sql
-- Example: E-commerce domain model with proper relationships
-- Core entities with business rules embedded
CREATE TABLE customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
encrypted_password VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
phone VARCHAR(20),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_active BOOLEAN DEFAULT true,
-- Add constraints for business rules
CONSTRAINT valid_email CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
CONSTRAINT valid_phone CHECK (phone IS NULL OR phone ~* '^\+?[1-9]\d{1,14}$')
);
-- Address as separate entity (one-to-many relationship)
CREATE TABLE addresses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
address_type address_type_enum NOT NULL DEFAULT 'shipping',
street_line1 VARCHAR(255) NOT NULL,
street_line2 VARCHAR(255),
city VARCHAR(100) NOT NULL,
state_province VARCHAR(100),
postal_code VARCHAR(20),
country_code CHAR(2) NOT NULL,
is_default BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Ensure only one default address per type per customer
UNIQUE(customer_id, address_type, is_default) WHERE is_default = true
);
-- Product catalog with hierarchical categories
CREATE TABLE categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
parent_id UUID REFERENCES categories(id),
name VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT true,
sort_order INTEGER DEFAULT 0,
-- Prevent self-referencing and circular references
CONSTRAINT no_self_reference CHECK (id != parent_id)
);
-- Products with versioning support
CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sku VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
category_id UUID REFERENCES categories(id),
base_price DECIMAL(10,2) NOT NULL CHECK (base_price >= 0),
inventory_count INTEGER NOT NULL DEFAULT 0 CHECK (inventory_count >= 0),
is_active BOOLEAN DEFAULT true,
version INTEGER DEFAULT 1,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Order management with state machine
CREATE TYPE order_status AS ENUM (
'pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'
);
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_number VARCHAR(50) UNIQUE NOT NULL,
customer_id UUID NOT NULL REFERENCES customers(id),
billing_address_id UUID NOT NULL REFERENCES addresses(id),
shipping_address_id UUID NOT NULL REFERENCES addresses(id),
status order_status NOT NULL DEFAULT 'pending',
subtotal DECIMAL(10,2) NOT NULL CHECK (subtotal >= 0),
tax_amount DECIMAL(10,2) NOT NULL DEFAULT 0 CHECK (tax_amount >= 0),
shipping_amount DECIMAL(10,2) NOT NULL DEFAULT 0 CHECK (shipping_amount >= 0),
total_amount DECIMAL(10,2) NOT NULL CHECK (total_amount >= 0),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- Ensure total calculation consistency
CONSTRAINT valid_total CHECK (total_amount = subtotal + tax_amount + shipping_amount)
);
-- Order items with audit trail
CREATE TABLE order_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL CHECK (quantity > 0),
unit_price DECIMAL(10,2) NOT NULL CHECK (unit_price >= 0),
total_price DECIMAL(10,2) NOT NULL CHECK (total_price >= 0),
-- Snapshot product details at time of order
product_name VARCHAR(255) NOT NULL,
product_sku VARCHAR(100) NOT NULL,
CONSTRAINT valid_item_total CHECK (total_price = quantity * unit_price)
);
```
### 2. Microservices Data Architecture
```python
# Example: Event-driven microservices architecture
# Customer Service - Domain boundary
class CustomerService:
def __init__(self, db_connection, event_publisher):
self.db = db_connection
self.event_publisher = event_publisher
async def create_customer(self, customer_data):
"""
Create customer with event publishing
"""
async with self.db.transaction():
# Create customer record
customer = await self.db.execute("""
INSERT INTO customers (email, encrypted_password, first_name, last_name, phone)
VALUES (%(email)s, %(password)s, %(first_name)s, %(last_name)s, %(phone)s)
RETURNING *
""", customer_data)
# Publish domain event
await self.event_publisher.publish({
'event_type': 'customer.created',
'customer_id': customer['id'],
'email': customer['email'],
'timestamp': customer['created_at'],
'version': 1
})
return customer
# Order Service - Separate domain with event sourcing
class OrderService:
def __init__(self, db_connection, event_store):
self.db = db_connection
self.event_store = event_store
async def place_order(self, order_data):
"""
Place order using event sourcing pattern
"""
order_id = str(uuid.uuid4())
# Event sourcing - store events, not state
events = [
{
'event_id': str(uuid.uuid4()),
'stream_id': order_id,
'event_type': 'order.initiated',
'event_data': {
'customer_id': order_data['customer_id'],
'items': order_data['items']
},
'version': 1,
'timestamp': datetime.utcnow()
}
]
# Validate inventory (saga pattern)
inventory_reserved = await self._reserve_inventory(order_data['items'])
if inventory_reserved:
events.append({
'event_id': str(uuid.uuid4()),
'stream_id': order_id,
'event_type': 'inventory.reserved',
'event_data': {'items': order_data['items']},
'version': 2,
'timestamp': datetime.utcnow()
})
# Process payment (saga pattern)
payment_processed = await self._process_payment(order_data['payment'])
if payment_processed:
events.append({
'event_id': str(uuid.uuid4()),
'stream_id': order_id,
'event_type': 'payment.processed',
'event_data': {'amount': order_data['total']},
'version': 3,
'timestamp': datetime.utcnow()
})
# Confirm order
events.append({
'event_id': str(uuid.uuid4()),
'stream_id': order_id,
'event_type': 'order.confirmed',
'event_data': {'order_id': order_id},
'version': 4,
'timestamp': datetime.utcnow()
})
# Store all events atomically
await self.event_store.append_events(order_id, events)
return order_id
```
### 3. Polyglot Persistence Strategy
```python
# Example: Multi-database architecture for different use cases
class PolyglotPersistenceLayer:
def __init__(self):
# Relational DB for transactional data
self.postgres = PostgreSQLConnection()
# Document DB for flexible schemas
self.mongodb = MongoDBConnection()
# Key-value store for caching
self.redis = RedisConnection()
# Search engine for full-text search
self.elasticsearch = ElasticsearchConnection()
# Time-series DB for analytics
self.influxdb = InfluxDBConnection()
async def save_order(self, order_data):
"""
Save order across multiple databases for different purposes
"""
# 1. Store transactional data in PostgreSQL
async with self.postgres.transaction():
order_id = await self.postgres.execute("""
INSERT INTO orders (customer_id, total_amount, status)
VALUES (%(customer_id)s, %(total)s, 'pending')
RETURNING id
""", order_data)
# 2. Store flexible document in MongoDB for analytics
await self.mongodb.orders.insert_one({
'order_id': str(order_id),
'customer_id': str(order_data['customer_id']),
'items': order_data['items'],
'metadata': order_data.get('metadata', {}),
'created_at': datetime.utcnow()
})
# 3. Cache order summary in Redis
await self.redis.setex(
f"order:{order_id}",
3600, # 1 hour TTL
json.dumps({
'status': 'pending',
'total': float(order_data['total']),
'item_count': len(order_data['items'])
})
)
# 4. Index for search in Elasticsearch
await self.elasticsearch.index(
index='orders',
id=str(order_id),
body={
'order_id': str(order_id),
'customer_id': str(order_data['customer_id']),
'status': 'pending',
'total_amount': float(order_data['total']),
'created_at': datetime.utcnow().isoformat()
}
)
# 5. Store metrics in InfluxDB for real-time analytics
await self.influxdb.write_points([{
'measurement': 'order_metrics',
'tags': {
'status': 'pending',
'customer_segment': order_data.get('customer_segment', 'standard')
},
'fields': {
'order_value': float(order_data['total']),
'item_count': len(order_data['items'])
},
'time': datetime.utcnow()
}])
return order_id
```
### 4. Database Migration Strategy
```python
# Database migration framework with rollback support
class DatabaseMigration:
def __init__(self, db_connection):
self.db = db_connection
self.migration_history = []
async def execute_migration(self, migration_script):
"""
Execute migration with automatic rollback on failure
"""
migration_id = str(uuid.uuid4())
checkpoint = await self._create_checkpoint()
try:
async with self.db.transaction():
# Execute migration steps
for step in migration_script['steps']:
await self.db.execute(step['sql'])
# Record each step for rollback
await self.db.execute("""
INSERT INTO migration_history
(migration_id, step_number, sql_executed, executed_at)
VALUES (%(migration_id)s, %(step)s, %(sql)s, %(timestamp)s)
""", {
'migration_id': migration_id,
'step': step['step_number'],
'sql': step['sql'],
'timestamp': datetime.utcnow()
})
# Mark migration as complete
await self.db.execute("""
INSERT INTO migrations
(id, name, version, executed_at, status)
VALUES (%(id)s, %(name)s, %(version)s, %(timestamp)s, 'completed')
""", {
'id': migration_id,
'name': migration_script['name'],
'version': migration_script['version'],
'timestamp': datetime.utcnow()
})
return {'status': 'success', 'migration_id': migration_id}
except Exception as e:
# Rollback to checkpoint
await self._rollback_to_checkpoint(checkpoint)
# Record failure
await self.db.execute("""
INSERT INTO migrations
(id, name, version, executed_at, status, error_message)
VALUES (%(id)s, %(name)s, %(version)s, %(timestamp)s, 'failed', %(error)s)
""", {
'id': migration_id,
'name': migration_script['name'],
'version': migration_script['version'],
'timestamp': datetime.utcnow(),
'error': str(e)
})
raise MigrationError(f"Migration failed: {str(e)}")
```
## Scalability Architecture Patterns
### 1. Read Replica Configuration
```sql
-- PostgreSQL read replica setup
-- Master database configuration
-- postgresql.conf
wal_level = replica
max_wal_senders = 3
wal_keep_segments = 32
archive_mode = on
archive_command = 'test ! -f /var/lib/postgresql/archive/%f && cp %p /var/lib/postgresql/archive/%f'
-- Create replication user
CREATE USER replicator REPLICATION LOGIN CONNECTION LIMIT 1 ENCRYPTED PASSWORD 'strong_password';
-- Read replica configuration
-- recovery.conf
standby_mode = 'on'
primary_conninfo = 'host=master.db.company.com port=5432 user=replicator password=strong_password'
restore_command = 'cp /var/lib/postgresql/archive/%f %p'
```
### 2. Horizontal Sharding Strategy
```python
# Application-level sharding implementation
class ShardManager:
def __init__(self, shard_config):
self.shards = {}
for shard_id, config in shard_config.items():
self.shards[shard_id] = DatabaseConnection(config)
def get_shard_for_customer(self, customer_id):
"""
Consistent hashing for customer data distribution
"""
hash_value = hashlib.md5(str(customer_id).encode()).hexdigest()
shard_number = int(hash_value[:8], 16) % len(self.shards)
return f"shard_{shard_number}"
async def get_customer_orders(self, customer_id):
"""
Retrieve customer orders from appropriate shard
"""
shard_key = self.get_shard_for_customer(customer_id)
shard_db = self.shards[shard_key]
return await shard_db.fetch_all("""
SELECT * FROM orders
WHERE customer_id = %(customer_id)s
ORDER BY created_at DESC
""", {'customer_id': customer_id})
async def cross_shard_analytics(self, query_template, params):
"""
Execute analytics queries across all shards
"""
results = []
# Execute query on all shards in parallel
tasks = []
for shard_key, shard_db in self.shards.items():
task = shard_db.fetch_all(query_template, params)
tasks.append(task)
shard_results = await asyncio.gather(*tasks)
# Aggregate results from all shards
for shard_result in shard_results:
results.extend(shard_result)
return results
```
## Architecture Decision Framework
### Database Technology Selection Matrix
```python
def recommend_database_technology(requirements):
"""
Database technology recommendation based on requirements
"""
recommendations = {
'relational': {
'use_cases': ['ACID transactions', 'complex relationships', 'reporting'],
'technologies': {
'PostgreSQL': 'Best for complex queries, JSON support, extensions',
'MySQL': 'High performance, wide ecosystem, simple setup',
'SQL Server': 'Enterprise features, Windows integration, BI tools'
}
},
'document': {
'use_cases': ['flexible schema', 'rapid development', 'JSON documents'],
'technologies': {
'MongoDB': 'Rich query language, horizontal scaling, aggregation',
'CouchDB': 'Eventual consistency, offline-first, HTTP API',
'Amazon DocumentDB': 'Managed MongoDB-compatible, AWS integration'
}
},
'key_value': {
'use_cases': ['caching', 'session storage', 'real-time features'],
'technologies': {
'Redis': 'In-memory, data structures, pub/sub, clustering',
'Amazon DynamoDB': 'Managed, serverless, predictable performance',
'Cassandra': 'Wide-column, high availability, linear scalability'
}
},
'search': {
'use_cases': ['full-text search', 'analytics', 'log analysis'],
'technologies': {
'Elasticsearch': 'Full-text search, analytics, REST API',
'Apache Solr': 'Enterprise search, faceting, highlighting',
'Amazon CloudSearch': 'Managed search, auto-scaling, simple setup'
}
},
'time_series': {
'use_cases': ['metrics', 'IoT data', 'monitoring', 'analytics'],
'technologies': {
'InfluxDB': 'Purpose-built for time series, SQL-like queries',
'TimescaleDB': 'PostgreSQL extension, SQL compatibility',
'Amazon Timestream': 'Managed, serverless, built-in analytics'
}
}
}
# Analyze requirements and return recommendations
recommended_stack = []
for requirement in requirements:
for category, info in recommendations.items():
if requirement in info['use_cases']:
recommended_stack.append({
'category': category,
'requirement': requirement,
'options': info['technologies']
})
return recommended_stack
```
## Performance and Monitoring
### Database Health Monitoring
```sql
-- PostgreSQL performance monitoring queries
-- Connection monitoring
SELECT
state,
COUNT(*) as connection_count,
AVG(EXTRACT(epoch FROM (now() - state_change))) as avg_duration_seconds
FROM pg_stat_activity
WHERE state IS NOT NULL
GROUP BY state;
-- Lock monitoring
SELECT
pg_class.relname,
pg_locks.mode,
COUNT(*) as lock_count
FROM pg_locks
JOIN pg_class ON pg_locks.relation = pg_class.oid
WHERE pg_locks.granted = true
GROUP BY pg_class.relname, pg_locks.mode
ORDER BY lock_count DESC;
-- Query performance analysis
SELECT
query,
calls,
total_time,
mean_time,
rows,
100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 20;
-- Index usage analysis
SELECT
schemaname,
tablename,
indexname,
idx_tup_read,
idx_tup_fetch,
idx_scan,
CASE
WHEN idx_scan = 0 THEN 'Unused'
WHEN idx_scan < 10 THEN 'Low Usage'
ELSE 'Active'
END as usage_status
FROM pg_stat_user_indexes
ORDER BY idx_scan DESC;
```
Your architecture decisions should prioritize:
1. **Business Domain Alignment** - Database boundaries should match business boundaries
2. **Scalability Path** - Plan for growth from day one, but start simple
3. **Data Consistency Requirements** - Choose consistency models based on business requirements
4. **Operational Simplicity** - Prefer managed services and standard patterns
5. **Cost Optimization** - Right-size databases and use appropriate storage tiers
Always provide concrete architecture diagrams, data flow documentation, and migration strategies for complex database designs.

View File

@@ -1,33 +0,0 @@
---
name: database-optimizer
description: SQL query optimization and database schema design specialist. Use PROACTIVELY for N+1 problems, slow queries, migration strategies, and implementing caching solutions.
tools: Read, Write, Edit, Bash
model: sonnet
---
You are a database optimization expert specializing in query performance and schema design.
## Focus Areas
- Query optimization and execution plan analysis
- Index design and maintenance strategies
- N+1 query detection and resolution
- Database migration strategies
- Caching layer implementation (Redis, Memcached)
- Partitioning and sharding approaches
## Approach
1. Measure first - use EXPLAIN ANALYZE
2. Index strategically - not every column needs one
3. Denormalize when justified by read patterns
4. Cache expensive computations
5. Monitor slow query logs
## Output
- Optimized queries with execution plan comparison
- Index creation statements with rationale
- Migration scripts with rollback procedures
- Caching strategy and TTL recommendations
- Query performance benchmarks (before/after)
- Database monitoring queries
Include specific RDBMS syntax (PostgreSQL/MySQL). Show query execution times.

View File

@@ -1,32 +0,0 @@
---
name: frontend-developer
description: Frontend development specialist for React applications and responsive design. Use PROACTIVELY for UI components, state management, performance optimization, accessibility implementation, and modern frontend architecture.
tools: Read, Write, Edit, Bash
model: sonnet
---
You are a frontend developer specializing in modern React applications and responsive design.
## Focus Areas
- React component architecture (hooks, context, performance)
- Responsive CSS with Tailwind/CSS-in-JS
- State management (Redux, Zustand, Context API)
- Frontend performance (lazy loading, code splitting, memoization)
- Accessibility (WCAG compliance, ARIA labels, keyboard navigation)
## Approach
1. Component-first thinking - reusable, composable UI pieces
2. Mobile-first responsive design
3. Performance budgets - aim for sub-3s load times
4. Semantic HTML and proper ARIA attributes
5. Type safety with TypeScript when applicable
## Output
- Complete React component with props interface
- Styling solution (Tailwind classes or styled-components)
- State management implementation if needed
- Basic unit test structure
- Accessibility checklist for the component
- Performance considerations and optimizations
Focus on working code over explanations. Include usage examples in comments.

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +0,0 @@
---
name: sql-pro
description: Write complex SQL queries, optimize execution plans, and design normalized schemas. Masters CTEs, window functions, and stored procedures. Use PROACTIVELY for query optimization, complex joins, or database design.
tools: Read, Write, Edit, Bash
model: sonnet
---
You are a SQL expert specializing in query optimization and database design.
## Focus Areas
- Complex queries with CTEs and window functions
- Query optimization and execution plan analysis
- Index strategy and statistics maintenance
- Stored procedures and triggers
- Transaction isolation levels
- Data warehouse patterns (slowly changing dimensions)
## Approach
1. Write readable SQL - CTEs over nested subqueries
2. EXPLAIN ANALYZE before optimizing
3. Indexes are not free - balance write/read performance
4. Use appropriate data types - save space and improve speed
5. Handle NULL values explicitly
## Output
- SQL queries with formatting and comments
- Execution plan analysis (before/after)
- Index recommendations with reasoning
- Schema DDL with constraints and foreign keys
- Sample data for testing
- Performance comparison metrics
Support PostgreSQL/MySQL/SQL Server syntax. Always specify which dialect.

View File

@@ -1,197 +0,0 @@
# /dev:add-e2e-attrs - Add E2E Test Attributes
Add required E2E test attributes (`data-what`, `data-which`, dynamic `data-*`) to component templates for QA automation.
## Parameters
- `component-path`: Path to component directory or HTML template file
## Required E2E Attributes
### Core Attributes (Required)
1. **`data-what`**: Semantic description of element's purpose
- Example: `data-what="submit-button"`, `data-what="search-input"`
2. **`data-which`**: Unique identifier for the specific instance
- Example: `data-which="primary"`, `data-which="customer-{{ customerId }}"`
### Dynamic Attributes (Contextual)
3. **`data-*`**: Additional context based on state/data
- Example: `data-status="active"`, `data-index="0"`
## Tasks
### 1. Analyze Component Template
- Read component HTML template
- Identify interactive elements that need E2E attributes:
- Buttons (`button`, `ui-button`)
- Inputs (`input`, `textarea`, `select`)
- Links (`a`, `routerLink`)
- Custom interactive components
- Form elements
- Clickable elements (`(click)` handlers)
### 2. Add Missing Attributes
**Buttons:**
```html
<!-- BEFORE -->
<button (click)="submit()">Submit</button>
<!-- AFTER -->
<button
(click)="submit()"
data-what="submit-button"
data-which="form-primary">
Submit
</button>
```
**Inputs:**
```html
<!-- BEFORE -->
<input [(ngModel)]="searchTerm" placeholder="Search..." />
<!-- AFTER -->
<input
[(ngModel)]="searchTerm"
placeholder="Search..."
data-what="search-input"
data-which="main-search" />
```
**Dynamic Lists:**
```html
<!-- BEFORE -->
@for (item of items; track item.id) {
<li (click)="selectItem(item)">{{ item.name }}</li>
}
<!-- AFTER -->
@for (item of items; track item.id) {
<li
(click)="selectItem(item)"
data-what="list-item"
[attr.data-which]="item.id"
[attr.data-status]="item.status">
{{ item.name }}
</li>
}
```
**Links:**
```html
<!-- BEFORE -->
<a routerLink="/orders/{{ orderId }}">View Order</a>
<!-- AFTER -->
<a
[routerLink]="['/orders', orderId]"
data-what="order-link"
[attr.data-which]="orderId">
View Order
</a>
```
**Custom Components:**
```html
<!-- BEFORE -->
<ui-button (click)="save()">Save</ui-button>
<!-- AFTER -->
<ui-button
(click)="save()"
data-what="save-button"
data-which="order-form">
Save
</ui-button>
```
### 3. Naming Conventions
**`data-what` Guidelines:**
- Use kebab-case
- Be descriptive but concise
- Common patterns:
- `*-button` (submit-button, cancel-button, delete-button)
- `*-input` (email-input, search-input, quantity-input)
- `*-link` (product-link, order-link, customer-link)
- `*-item` (list-item, menu-item, card-item)
- `*-dialog` (confirm-dialog, error-dialog)
- `*-dropdown` (status-dropdown, category-dropdown)
**`data-which` Guidelines:**
- Unique identifier for the instance
- Use dynamic binding for list items: `[attr.data-which]="item.id"`
- Static for unique elements: `data-which="primary"`
- Combine with context: `data-which="customer-{{ customerId }}-edit"`
### 4. Scan for Coverage
Check template coverage:
```bash
# Count interactive elements
grep -E '(click)=|routerLink|button|input|select|textarea' [template-file]
# Count elements with data-what
grep -c 'data-what=' [template-file]
# List elements missing E2E attributes
grep -E '(click)=|button' [template-file] | grep -v 'data-what='
```
### 5. Validate Attributes
- No duplicates in `data-which` within same view
- All interactive elements have both `data-what` and `data-which`
- Dynamic attributes use proper Angular binding: `[attr.data-*]`
- Attributes don't contain sensitive data (passwords, tokens)
### 6. Update Component Tests
Add E2E attribute selectors to tests:
```typescript
// Use E2E attributes for element selection
const submitButton = fixture.nativeElement.querySelector('[data-what="submit-button"][data-which="primary"]');
expect(submitButton).toBeTruthy();
```
### 7. Document Attributes
Add comment block at top of template:
```html
<!--
E2E Test Attributes:
- data-what="submit-button" data-which="primary" - Main form submission
- data-what="cancel-button" data-which="primary" - Cancel action
- data-what="search-input" data-which="main" - Product search field
-->
```
## Output
Provide summary:
- Template analyzed: [path]
- Interactive elements found: [count]
- Attributes added: [count]
- Coverage: [percentage]% (elements with E2E attrs / total interactive elements)
- List of added attributes with descriptions
- Validation status: ✅/❌
## Common Patterns by Component Type
**Form Components:**
- `data-what="[field]-input" data-which="[form-name]"`
- `data-what="submit-button" data-which="[form-name]"`
- `data-what="cancel-button" data-which="[form-name]"`
**List/Table Components:**
- `data-what="list-item" [attr.data-which]="item.id"`
- `data-what="edit-button" [attr.data-which]="item.id"`
- `data-what="delete-button" [attr.data-which]="item.id"`
**Navigation Components:**
- `data-what="nav-link" data-which="[destination]"`
- `data-what="breadcrumb" data-which="[level]"`
**Dialog Components:**
- `data-what="confirm-button" data-which="dialog"`
- `data-what="close-button" data-which="dialog"`
## References
- CLAUDE.md Code Quality section (E2E Testing Requirements)
- docs/guidelines/testing.md
- QA team E2E test documentation (if available)

View File

@@ -0,0 +1,434 @@
---
allowed-tools: Read, Write, Bash, Grep
argument-hint: [date] | --yesterday | --save-only
description: Generate End of Day report summarizing commits and work across all branches
---
# End of Day Report
Generate daily work summary: $ARGUMENTS
## Current State
- Current Date: !`date +%Y-%m-%d`
- Current Time: !`date +%H:%M`
- Current Branch: !`git branch --show-current`
- Git User: !`git config user.name`
- Git Email: !`git config user.email`
## Tasks
### 1. Determine Report Date and Scope
**Objective**: Identify the date range for the report
- [ ] Ask user for their work start time
- Use AskUserQuestion to ask: "What time did you start working today?"
- Provide options: "First commit time", "08:00", "09:00", "10:00", "Custom time"
- If "Custom time" selected, ask for specific time (HH:MM format)
- Default to first commit time if not specified
- Use this for accurate "Work Duration" calculation
- [ ] Check if date argument provided
- If `[date]` provided: Use specific date (format: YYYY-MM-DD)
- If `--yesterday` provided: Use yesterday's date
- Otherwise: Use today's date
```bash
# Get today's date
TODAY=$(date +%Y-%m-%d)
# Get yesterday's date
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)
# Get start of day
START_TIME="${TODAY} 00:00:00"
# Get end of day
END_TIME="${TODAY} 23:59:59"
```
- [ ] Set report scope
- Search across **all branches** (local and remote)
- Filter by git user name and email
- Include commits from start to end of specified day
### 2. Collect Commit Information
**Objective**: Gather all commits made by the user on the specified date (excluding merge commits)
- [ ] Fetch commits across all branches (non-merge commits only)
```bash
# Get all non-merge commits by current user today across all branches
git log --all \
--author="$(git config user.name)" \
--since="$START_TIME" \
--until="$END_TIME" \
--pretty=format:"%h|%ai|%s|%D" \
--no-merges
```
**Important**: Use `--no-merges` flag to exclude PR merge commits. These will be tracked separately in section 3.
- [ ] Extract commit details:
- Commit hash (short)
- Commit time
- Commit message
- Branch references (if any)
- [ ] Group commits by branch
- Parse branch references from commit output
- Identify which branch each commit belongs to
- Track branch switches during the day
- Exclude "Merged PR" commits from this section (they appear in Merge Activity instead)
**Example Output**:
```
c208327db|2025-10-28 14:23:45|feat(crm-data-access,checkout): improve primary bonus card selection logic|feature/5202-Praemie
9020cb305|2025-10-28 10:15:32|✨ feat(navigation): implement title management and enhance tab system|feature/5351-navigation
```
### 3. Identify PR and Merge Activity
**Objective**: Find pull requests created or merged today, distinguishing between PRs I merged vs PRs merged by colleagues
- [ ] Find ALL merge commits with "Merged PR" (check both author and committer)
```bash
# Get all PR merge activity with author and committer info
git log --all \
--since="$START_TIME" \
--until="$END_TIME" \
--grep="Merged PR" \
--pretty=format:"%h|%ai|%s|Author: %an <%ae>|Committer: %cn <%ce>"
```
- [ ] Categorize PR merges:
- **PRs I merged**: Where I am the COMMITTER (git config user.name matches committer name)
- **My PRs merged by colleagues**: Where I am the AUTHOR but someone else is the COMMITTER
- **Colleague PRs I merged**: Where someone else is the AUTHOR and I am the COMMITTER
- [ ] Parse PR numbers from commit messages
- Look for patterns: "Merged PR 1234:", "PR #1234", etc.
- Extract PR title/description
- Note which branch was merged
- Note who performed the merge (committer name)
- [ ] Identify branch merges
- Look for merge commits to develop/main
- Note feature branches merged
### 4. Analyze Branch Activity
**Objective**: Summarize branches worked on today
- [ ] List all branches with commits today
```bash
# Get unique branches with activity today
git log --all \
--author="$(git config user.name)" \
--since="$START_TIME" \
--until="$END_TIME" \
--pretty=format:"%D" | \
grep -v '^$' | \
tr ',' '\n' | \
sed 's/^ *//' | \
grep -E '^(origin/)?[a-zA-Z]' | \
sort -u
```
- [ ] Identify:
- Primary branch worked on (most commits)
- Other branches touched
- New branches created today
- Branches merged today
- [ ] Check current branch status
- Uncommitted changes
- Untracked files
- Ahead/behind develop
### 5. Generate Report Summary
**Objective**: Create formatted markdown report
- [ ] Build report structure:
```markdown
# End of Day Report - YYYY-MM-DD
**Developer**: [Name] <email>
**Date**: Day, Month DD, YYYY
**Time**: HH:MM
---
## 📊 Summary
- **Commits**: X commits across Y branches
- **PRs I Merged**: Z pull requests (as committer)
- **My PRs Merged by Colleagues**: W pull requests
- **Primary Branch**: branch-name
- **Work Duration**: Started at HH:MM, worked for Xh Ym
## 🔨 Commits Today
### Branch: feature/5351-navigation (5 commits)
- `9020cb3` (10:15) ✨ feat(navigation): implement title management and enhance tab system
- `abc1234` (11:30) fix(navigation): resolve routing edge case
- `def5678` (14:45) test(navigation): add comprehensive test coverage
- `ghi9012` (15:20) refactor(navigation): improve code organization
- `jkl3456` (16:00) docs(navigation): update README with usage examples
### Branch: feature/5202-Praemie (2 commits)
- `c208327` (14:23) feat(crm-data-access,checkout): improve primary bonus card selection logic
- `mno7890` (16:45) fix(checkout): handle edge case for bonus points
## 🔀 Merge Activity
### PRs I Merged (as committer)
- **PR #1990**: feat(ui): add new button variants → develop
- **PR #1991**: fix(api): resolve timeout issues → develop
### My PRs Merged by Colleagues
- **PR #1987**: Carousel Library → develop (merged by Nino Righi)
- **PR #1989**: fix(checkout): resolve currency constraint violations → develop (merged by Nino Righi)
### Branch Merges
- `feature/5202-Praemie-stock-info-request-batching``feature/5202-Praemie`
## 🌿 Branch Activity
**Primary Branch**: feature/5351-navigation (5 commits)
**Other Branches**:
- feature/5202-Praemie (2 commits)
- develop (merged 2 PRs)
**Current Branch**: feature/5351-navigation
**Status**: 3 files changed, 2 files staged, 1 file untracked
## 📝 Notes
[Optional section for manual notes - left empty by default]
---
_Report generated on YYYY-MM-DD at HH:MM_
```
**Formatting Rules**:
- Use emoji for section headers (📊 📝 🔨 🔀 🌿)
- Group commits by branch
- Show time for each commit in (HH:MM) format
- Include commit prefixes (feat:, fix:, docs:, etc.)
- Sort branches by number of commits (most active first)
- Highlight primary branch (most commits)
### 6. Save and Display Report
**Objective**: Output report to terminal and save to file
**Display to Terminal**:
- [ ] Print formatted report to stdout
- [ ] Use clear visual separators
- [ ] Ensure easy copy/paste to Slack/Teams/Email
**Save to File**:
- [ ] Create reports directory if it doesn't exist
```bash
mkdir -p reports/eod
```
- [ ] Determine filename
- Format: `reports/eod/YYYY-MM-DD.md`
- Example: `reports/eod/2025-10-28.md`
- [ ] Write report to file
```bash
# Save report
cat > "reports/eod/${TODAY}.md" << 'EOF'
[report content]
EOF
```
- [ ] Provide file location feedback
- Show absolute path to saved file
- Confirm successful save
**If `--save-only` flag**:
- [ ] Skip terminal display
- [ ] Only save to file
- [ ] Show success message with file path
### 7. Provide Summary Statistics
**Objective**: Show quick statistics and next steps
- [ ] Calculate and display:
- Total commits today (excluding PR merge commits)
- Number of branches worked on
- PRs I merged (as committer)
- My PRs merged by colleagues (authored by me, committed by others)
- Work duration (user-specified start time → last commit time)
- Lines of code changed (optional, if available)
- [ ] Suggest next steps:
- Commit uncommitted changes
- Push branches to remote
- Create PR for completed work
- Update task tracking system
## Output Format
### Standard Display
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 End of Day Report - 2025-10-28
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
**Developer**: Lorenz Hilpert <lorenz@example.com>
**Date**: Monday, October 28, 2025
**Time**: 17:30
---
## 📊 Summary
- **Commits**: 5 commits across 1 branch
- **PRs I Merged**: 2 pull requests (as committer)
- **My PRs Merged by Colleagues**: 0
- **Primary Branch**: feature/5351-navigation
- **Work Duration**: Started at 09:00, worked for 7h 45m (last commit at 16:45)
## 🔨 Commits Today
### Branch: feature/5351-navigation (5 commits)
- `9020cb3` (10:15) ✨ feat(navigation): implement title management and enhance tab system
- `abc1234` (11:30) 🐛 fix(navigation): resolve routing edge case
- `def5678` (14:45) ✅ test(navigation): add comprehensive test coverage
- `ghi9012` (15:20) ♻️ refactor(navigation): improve code organization
- `jkl3456` (16:00) 📝 docs(navigation): update README with usage examples
### Branch: feature/5202-Praemie (2 commits)
- `c208327` (14:23) ✨ feat(crm-data-access,checkout): improve primary bonus card selection logic
- `mno7890` (16:45) 🐛 fix(checkout): handle edge case for bonus points
## 🔀 Merge Activity
### PRs I Merged (as committer)
- **PR #1987**: Carousel Library → develop
- **PR #1989**: fix(checkout): resolve currency constraint violations → develop
### My PRs Merged by Colleagues
_None today_
## 🌿 Branch Activity
**Primary Branch**: feature/5351-navigation (5 commits)
**Other Branches**:
- feature/5202-Praemie (2 commits)
- develop (2 PR merges)
**Current Status**:
- Branch: feature/5351-navigation
- Changes: 3 files changed, 2 files staged, 1 file untracked
- Remote: 5 commits ahead of origin/feature/5351-navigation
## 📝 Notes
_No additional notes_
---
✅ Report saved to: /home/lorenz/Projects/ISA-Frontend/reports/eod/2025-10-28.md
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 Daily Statistics
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total Commits: 5 (excluding PR merges)
Branches: 1 active branch
PRs I Merged: 2
My PRs Merged by Colleagues: 0
Work Duration: 7h 45m (started at 09:00, last commit at 16:45)
📋 Next Steps
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. ✅ Push feature/5351-navigation to remote
2. ⚠️ Consider creating PR for completed work
3. 💾 1 untracked file - review and commit if needed
```
### No Activity Case
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 End of Day Report - 2025-10-28
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
**Developer**: Lorenz Hilpert <lorenz@example.com>
**Date**: Monday, October 28, 2025
**Time**: 17:30
---
## 📊 Summary
No commits found for today (2025-10-28).
**Possible Reasons**:
- No development work performed
- Working on uncommitted changes
- Using different git user configuration
**Current Branch**: feature/5351-navigation
**Uncommitted Changes**: 5 files modified, 2 files staged
---
💡 Tip: If you have uncommitted work, commit it before generating the report.
```
### Yesterday's Report
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 End of Day Report - 2025-10-27 (Yesterday)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[Report content for yesterday]
✅ Report saved to: /home/lorenz/Projects/ISA-Frontend/reports/eod/2025-10-27.md
```
## Usage Examples
```bash
# Generate today's EOD report
/eod-report
# Generate yesterday's report (if you forgot)
/eod-report --yesterday
# Generate report for specific date
/eod-report 2025-10-25
# Save to file only (no terminal output)
/eod-report --save-only
# Generate yesterday's report and save only
/eod-report --yesterday --save-only
```
## References
- Git Log Documentation: https://git-scm.com/docs/git-log
- Conventional Commits: https://www.conventionalcommits.org/
- Project Conventions: See CLAUDE.md for commit message standards
- Git Configuration: `git config user.name` and `git config user.email`

View File

@@ -0,0 +1,309 @@
---
allowed-tools: Read, Write, Edit, Bash, Grep
argument-hint: [version] | --since [tag] | --dry-run
description: Generate changelog entries from git tags using Keep a Changelog format
---
# Generate Changelog
Generate changelog entries from git commits between version tags: $ARGUMENTS
## Current State
- Latest Tag: !`git tag --sort=-creatordate | head -n 1`
- CHANGELOG.md: !`test -f CHANGELOG.md && echo "exists" || echo "does not exist"`
- Commits Since Last Tag: !`git log $(git tag --sort=-creatordate | head -n 1)..HEAD --oneline | wc -l`
- Current Branch: !`git branch --show-current`
## Tasks
### 1. Determine Version Range
**Objective**: Identify the commit range for changelog generation
- [ ] Check if version argument provided
- If `[version]` provided: Use as the new version number
- If `--since [tag]` provided: Use custom tag as starting point
- Otherwise: Use latest tag as starting point
```bash
# Find latest tag
LATEST_TAG=$(git tag --sort=-creatordate | head -n 1)
# Get commits since tag
git log ${LATEST_TAG}..HEAD --oneline
# If no tags exist, use entire history
if [ -z "$LATEST_TAG" ]; then
git log --oneline
fi
```
**Edge Cases**:
- No tags exist → Use entire commit history and suggest version 0.1.0
- No commits since last tag → Notify user, no changelog needed
- Invalid tag provided → Error with available tags list
### 2. Extract and Categorize Commits
**Objective**: Parse commit messages and group by Keep a Changelog categories
- [ ] Fetch commits with detailed information
```bash
# Get commits with format: hash | date | message
git log ${LATEST_TAG}..HEAD --pretty=format:"%h|%as|%s" --no-merges
```
- [ ] Parse conventional commit patterns and map to categories:
**Mapping Rules**:
- `feat:` or `feature:`**Added**
- `fix:` or `bugfix:`**Fixed**
- `refactor:`**Changed**
- `perf:` or `performance:`**Changed**
- `docs:`**Changed** (or skip if only documentation)
- `style:`**Changed**
- `test:` → (skip from changelog)
- `chore:` → (skip from changelog)
- `build:` or `ci:` → (skip from changelog)
- `revert:`**Changed** or **Fixed**
- `security:`**Security**
- `deprecate:` or `deprecated:`**Deprecated**
- `remove:` or `breaking:`**Removed**
- Non-conventional commits → **Changed** (default)
- [ ] Extract scope and description from commit messages
**Commit Pattern**: `type(scope): description`
Example:
```
feat(checkout): add reward delivery order support
fix(remission): resolve currency constraint violations
refactor(navigation): implement title management system
```
### 3. Generate Changelog Entry
**Objective**: Create properly formatted changelog section
- [ ] Determine version number
- Use provided `[version]` argument
- Or prompt for new version if not provided
- Format: `[X.Y.Z]` following semantic versioning
- [ ] Get current date in ISO format: `YYYY-MM-DD`
```bash
TODAY=$(date +%Y-%m-%d)
```
- [ ] Build changelog entry following Keep a Changelog format:
```markdown
## [VERSION] - YYYY-MM-DD
### Added
- New feature description from feat: commits
- Another feature
### Changed
- Refactored component description
- Performance improvements
### Deprecated
- Feature marked for removal
### Removed
- Deleted feature or breaking change
### Fixed
- Bug fix description
- Another fix
### Security
- Security improvement description
```
**Rules**:
- Only include sections that have entries
- Sort entries alphabetically within each section
- Use sentence case for descriptions
- Remove commit type prefix from descriptions
- Include scope in parentheses if present: `(scope) description`
- Add reference links to commits/PRs if available
### 4. Update or Preview CHANGELOG.md
**Objective**: Append new entry to changelog file or show preview
**If `--dry-run` flag provided**:
- [ ] Display generated changelog entry to stdout
- [ ] Show preview of where it would be inserted
- [ ] Do NOT modify CHANGELOG.md
- [ ] Exit with success
**Otherwise (append mode)**:
- [ ] Check if CHANGELOG.md exists
- If not, create with standard header:
```markdown
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
```
- [ ] Read existing CHANGELOG.md content
- [ ] Find insertion point (after "## [Unreleased]" section, or after main header)
- [ ] Insert new changelog entry
- [ ] Maintain reverse chronological order (newest first)
- [ ] Write updated content back to CHANGELOG.md
```bash
# Backup existing file
cp CHANGELOG.md CHANGELOG.md.bak
# Insert new entry
# (Implementation handled by Edit tool)
```
### 5. Validate and Report
**Objective**: Verify changelog quality and provide summary
- [ ] Validate generated entry:
- Version format is valid (X.Y.Z)
- Date is correct (YYYY-MM-DD)
- At least one category has entries
- No duplicate entries
- Proper markdown formatting
- [ ] Report statistics:
- Number of commits processed
- Entries per category
- Version number used
- File status (preview/updated)
- [ ] Show next steps:
- Review changelog entry
- Update version in package.json if needed
- Create git tag if appropriate
- Commit changelog changes
## Output Format
### Dry Run Preview
```
🔍 Changelog Preview (--dry-run mode)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
## [1.5.0] - 2025-10-28
### Added
- (checkout) Add reward delivery order support
- (navigation) Implement title management and tab system
### Changed
- (carousel) Update carousel library implementation
- (remission) Enhance returns processing workflow
### Fixed
- (checkout) Resolve currency constraint violations in price handling
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 Statistics
─────────────
Commits processed: 12
Added: 2 entries
Changed: 2 entries
Fixed: 1 entry
Version: 1.5.0
Date: 2025-10-28
⚠️ This is a preview. Run without --dry-run to update CHANGELOG.md
```
### Append Mode Success
```
✅ Changelog Updated Successfully
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
## [1.5.0] - 2025-10-28
### Added
- (checkout) Add reward delivery order support
- (navigation) Implement title management and tab system
### Changed
- (carousel) Update carousel library implementation
- (remission) Enhance returns processing workflow
### Fixed
- (checkout) Resolve currency constraint violations in price handling
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 Statistics
─────────────
Commits processed: 12
Added: 2 entries
Changed: 2 entries
Fixed: 1 entry
Version: 1.5.0
File: CHANGELOG.md (updated)
Backup: CHANGELOG.md.bak
📋 Next Steps
─────────────
1. Review the changelog entry in CHANGELOG.md
2. Update version in package.json: npm version 1.5.0
3. Commit the changelog: git add CHANGELOG.md && git commit -m "docs: update changelog for v1.5.0"
4. Create git tag: git tag -a v1.5.0 -m "Release v1.5.0"
5. Push changes: git push && git push --tags
```
### Error Cases
```
❌ No Changes Found
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
No commits found since last tag (v1.4.5).
Nothing to add to changelog.
```
```
❌ No Tags Found
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
No git tags found in this repository.
Suggestions:
- Create your first tag: git tag v0.1.0
- Or specify a commit range: /generate-changelog --since HEAD~10
- Or generate from all commits: /generate-changelog 0.1.0
```
```
⚠️ Invalid Version Format
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Version "1.5" is invalid.
Expected format: X.Y.Z (e.g., 1.5.0)
Please provide a valid semantic version.
```
## References
- Keep a Changelog: https://keepachangelog.com/
- Semantic Versioning: https://semver.org/
- Conventional Commits: https://www.conventionalcommits.org/
- Project Conventions: See CLAUDE.md for commit message standards

View File

@@ -15,6 +15,10 @@ Guide for modern Angular 20+ template patterns: control flow, lazy loading, proj
- Designing reusable components with `ng-content`
- Template performance optimization
**Related Skills:** These skills work together when writing Angular templates:
- **[html-template](../html-template/SKILL.md)** - E2E testing attributes (`data-what`, `data-which`) and ARIA accessibility
- **[tailwind](../tailwind/SKILL.md)** - ISA design system styling (colors, typography, spacing, layout)
## Control Flow (Angular 17+)
### @if / @else if / @else
@@ -211,7 +215,7 @@ Groups elements without DOM footprint:
1. **Use signals:** `isExpanded = signal(false)`
2. **Prefer control flow over directives:** Use `@if` not `*ngIf`
3. **Keep expressions simple:** Use `computed()` for complex logic
4. **E2E attributes:** Always add `[attr.data-what]` and `[attr.data-which]`
4. **Testing & Accessibility:** Always add E2E and ARIA attributes (see **[html-template](../html-template/SKILL.md)** skill)
5. **Track expressions:** Required in `@for`, use unique IDs
## Migration

View File

@@ -1,203 +0,0 @@
---
name: Git Commit Helper
description: Generate descriptive commit messages by analyzing git diffs. Use when the user asks for help writing commit messages or reviewing staged changes.
---
# Git Commit Helper
## Quick start
Analyze staged changes and generate commit message:
```bash
# View staged changes
git diff --staged
# Generate commit message based on changes
# (Claude will analyze the diff and suggest a message)
```
## Commit message format
Follow conventional commits format:
```
<type>(<scope>): <description>
[optional body]
[optional footer]
```
### Types
- **feat**: New feature
- **fix**: Bug fix
- **docs**: Documentation changes
- **style**: Code style changes (formatting, missing semicolons)
- **refactor**: Code refactoring
- **test**: Adding or updating tests
- **chore**: Maintenance tasks
### Examples
**Feature commit:**
```
feat(auth): add JWT authentication
Implement JWT-based authentication system with:
- Login endpoint with token generation
- Token validation middleware
- Refresh token support
```
**Bug fix:**
```
fix(api): handle null values in user profile
Prevent crashes when user profile fields are null.
Add null checks before accessing nested properties.
```
**Refactor:**
```
refactor(database): simplify query builder
Extract common query patterns into reusable functions.
Reduce code duplication in database layer.
```
## Analyzing changes
Review what's being committed:
```bash
# Show files changed
git status
# Show detailed changes
git diff --staged
# Show statistics
git diff --staged --stat
# Show changes for specific file
git diff --staged path/to/file
```
## Commit message guidelines
**DO:**
- Use imperative mood ("add feature" not "added feature")
- Keep first line under 50 characters
- Capitalize first letter
- No period at end of summary
- Explain WHY not just WHAT in body
**DON'T:**
- Use vague messages like "update" or "fix stuff"
- Include technical implementation details in summary
- Write paragraphs in summary line
- Use past tense
## Multi-file commits
When committing multiple related changes:
```
refactor(core): restructure authentication module
- Move auth logic from controllers to service layer
- Extract validation into separate validators
- Update tests to use new structure
- Add integration tests for auth flow
Breaking change: Auth service now requires config object
```
## Scope examples
**Frontend:**
- `feat(ui): add loading spinner to dashboard`
- `fix(form): validate email format`
**Backend:**
- `feat(api): add user profile endpoint`
- `fix(db): resolve connection pool leak`
**Infrastructure:**
- `chore(ci): update Node version to 20`
- `feat(docker): add multi-stage build`
## Breaking changes
Indicate breaking changes clearly:
```
feat(api)!: restructure API response format
BREAKING CHANGE: All API responses now follow JSON:API spec
Previous format:
{ "data": {...}, "status": "ok" }
New format:
{ "data": {...}, "meta": {...} }
Migration guide: Update client code to handle new response structure
```
## Template workflow
1. **Review changes**: `git diff --staged`
2. **Identify type**: Is it feat, fix, refactor, etc.?
3. **Determine scope**: What part of the codebase?
4. **Write summary**: Brief, imperative description
5. **Add body**: Explain why and what impact
6. **Note breaking changes**: If applicable
## Interactive commit helper
Use `git add -p` for selective staging:
```bash
# Stage changes interactively
git add -p
# Review what's staged
git diff --staged
# Commit with message
git commit -m "type(scope): description"
```
## Amending commits
Fix the last commit message:
```bash
# Amend commit message only
git commit --amend
# Amend and add more changes
git add forgotten-file.js
git commit --amend --no-edit
```
## Best practices
1. **Atomic commits** - One logical change per commit
2. **Test before commit** - Ensure code works
3. **Reference issues** - Include issue numbers if applicable
4. **Keep it focused** - Don't mix unrelated changes
5. **Write for humans** - Future you will read this
## Commit message checklist
- [ ] Type is appropriate (feat/fix/docs/etc.)
- [ ] Scope is specific and clear
- [ ] Summary is under 50 characters
- [ ] Summary uses imperative mood
- [ ] Body explains WHY not just WHAT
- [ ] Breaking changes are clearly marked
- [ ] Related issue numbers are included

View File

@@ -0,0 +1,352 @@
---
name: git-workflow
description: Enforces ISA-Frontend project Git workflow conventions including branch naming, conventional commits, and PR creation against develop branch
---
# Git Workflow Skill
Enforces Git workflow conventions specific to the ISA-Frontend project.
## When to Use
- Creating new branches for features or bugfixes
- Writing commit messages
- Creating pull requests
- Any Git operations requiring adherence to project conventions
## Core Principles
### 1. Default Branch is `develop` (NOT `main`)
- **All PRs target**: `develop` branch
- **Feature branches from**: `develop`
- **Never push directly to**: `develop` or `main`
### 2. Branch Naming Convention
**Format**: `<type>/{task-id}-{short-description}`
**Types**:
- `feature/` - New features or enhancements
- `bugfix/` - Bug fixes
- `hotfix/` - Emergency production fixes
**Rules**:
- Use English kebab-case for descriptions
- Start with task/issue ID (e.g., `5391`)
- Keep description concise - shorten if too long
- Use hyphens to separate words
**Examples**:
```bash
# Good
feature/5391-praemie-checkout-action-card-delivery-order
bugfix/6123-fix-login-redirect-loop
hotfix/7890-critical-payment-error
# Bad
feature/praemie-checkout # Missing task ID
feature/5391_praemie # Using underscores
feature-5391-very-long-description-that-goes-on-forever # Too long
```
### 3. Conventional Commits (WITHOUT Co-Author Tags)
**Format**: `<type>(<scope>): <description>`
**Types**:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation only
- `style`: Code style (formatting, missing semicolons)
- `refactor`: Code restructuring without feature changes
- `perf`: Performance improvements
- `test`: Adding or updating tests
- `build`: Build system or dependencies
- `ci`: CI configuration
- `chore`: Maintenance tasks
**Rules**:
-**NO** "Generated with Claude Code" tags
-**NO** "Co-Authored-By: Claude" tags
- ✅ Keep first line under 72 characters
- ✅ Use imperative mood ("add" not "added")
- ✅ Body optional but recommended for complex changes
**Examples**:
```bash
# Good
feat(checkout): add bonus card selection for delivery orders
fix(crm): resolve customer search filter reset issue
refactor(oms): extract return validation logic into service
# Bad
feat(checkout): add bonus card selection
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
# Also bad
Added new feature # Wrong tense
Fix bug # Missing scope
```
### 4. Pull Request Creation
**Target Branch**: Always `develop`
**PR Title Format**: Same as conventional commit
```
feat(domain): concise description of changes
```
**PR Body Structure**:
```markdown
## Summary
- Brief bullet points of changes
## Related Tasks
- Closes #{task-id}
- Refs #{related-task}
## Test Plan
- [ ] Unit tests added/updated
- [ ] E2E attributes added
- [ ] Manual testing completed
## Breaking Changes
None / List breaking changes
## Screenshots (if UI changes)
[Add screenshots]
```
## Common Workflows
### Creating a Feature Branch
```bash
# 1. Update develop
git checkout develop
git pull origin develop
# 2. Create feature branch
git checkout -b feature/5391-praemie-checkout-action-card
# 3. Work and commit
git add .
git commit -m "feat(checkout): add primary bonus card selection logic"
# 4. Push to remote
git push -u origin feature/5391-praemie-checkout-action-card
# 5. Create PR targeting develop (use gh CLI or web UI)
```
### Creating a Bugfix Branch
```bash
# From develop
git checkout develop
git pull origin develop
git checkout -b bugfix/6123-login-redirect-loop
# Commit
git commit -m "fix(auth): resolve infinite redirect on logout"
```
### Creating a Hotfix Branch
```bash
# From main (production)
git checkout main
git pull origin main
git checkout -b hotfix/7890-payment-processing-error
# Commit
git commit -m "fix(checkout): critical payment API timeout handling"
# Merge to both main and develop
```
## Commit Message Guidelines
### Good Commit Messages
```bash
feat(crm): add customer loyalty tier calculation
Implements three-tier loyalty system based on annual spend.
Includes migration for existing customer data.
Refs #5234
---
fix(oms): prevent duplicate return submissions
Adds debouncing to return form submission and validates
against existing returns in the last 60 seconds.
Closes #5891
---
refactor(catalogue): extract product search into dedicated service
Moves search logic from component to ProductSearchService
for better testability and reusability.
---
perf(remission): optimize remission list query with pagination
Reduces initial load time from 3s to 800ms by implementing
cursor-based pagination.
Closes #6234
```
### Bad Commit Messages
```bash
# Too vague
fix: bug fixes
# Missing scope
feat: new feature
# Wrong tense
fixed the login issue
# Including banned tags
feat(checkout): add feature
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
```
## Git Configuration Checks
### Verify Git Setup
```bash
# Check current branch
git branch --show-current
# Verify remote
git remote -v # Should show origin pointing to ISA-Frontend
# Check for uncommitted changes
git status
```
## Common Mistakes to Avoid
```bash
# ❌ Creating PR against main
gh pr create --base main # WRONG
# ✅ Always target develop
gh pr create --base develop # CORRECT
# ❌ Using underscores in branch names
git checkout -b feature/5391_my_feature # WRONG
# ✅ Use hyphens
git checkout -b feature/5391-my-feature # CORRECT
# ❌ Adding co-author tags
git commit -m "feat: something
Co-Authored-By: Claude <noreply@anthropic.com>" # WRONG
# ✅ Clean commit message
git commit -m "feat(scope): something" # CORRECT
# ❌ Forgetting task ID in branch name
git checkout -b feature/new-checkout-flow # WRONG
# ✅ Include task ID
git checkout -b feature/5391-new-checkout-flow # CORRECT
```
## Integration with Claude Code
When Claude Code creates commits or PRs:
### Commit Creation
```bash
# Claude uses conventional commits WITHOUT attribution
git commit -m "feat(checkout): implement bonus card selection
Adds logic for selecting primary bonus card during checkout
for delivery orders. Includes validation and error handling.
Refs #5391"
```
### PR Creation
```bash
# Target develop by default
gh pr create --base develop \
--title "feat(checkout): implement bonus card selection" \
--body "## Summary
- Add primary bonus card selection logic
- Implement validation for delivery orders
- Add error handling for API failures
## Related Tasks
- Closes #5391
## Test Plan
- [x] Unit tests added
- [x] E2E attributes added
- [x] Manual testing completed"
```
## Branch Cleanup
### After PR Merge
```bash
# Update develop
git checkout develop
git pull origin develop
# Delete local feature branch
git branch -d feature/5391-praemie-checkout
# Delete remote branch (usually done by PR merge)
git push origin --delete feature/5391-praemie-checkout
```
## Quick Reference
```bash
# Branch naming
feature/{task-id}-{description}
bugfix/{task-id}-{description}
hotfix/{task-id}-{description}
# Commit format
<type>(<scope>): <description>
# Common types
feat, fix, docs, style, refactor, perf, test, build, ci, chore
# PR target
Always: develop (NOT main)
# Banned in commits
- "Generated with Claude Code"
- "Co-Authored-By: Claude"
- Any AI attribution
```
## Resources
- [Conventional Commits](https://www.conventionalcommits.org/)
- Project PR template: `.github/pull_request_template.md`
- Code review standards: `.github/review-instructions.md`

View File

@@ -0,0 +1,298 @@
---
name: html-template
description: This skill should be used when writing or reviewing HTML templates to ensure proper E2E testing attributes (data-what, data-which) and ARIA accessibility attributes are included. Use when creating interactive elements like buttons, inputs, links, forms, dialogs, or any HTML markup requiring testing and accessibility compliance. Works seamlessly with the angular-template skill.
---
# HTML Template - Testing & Accessibility Attributes
This skill should be used when writing or reviewing HTML templates to ensure proper testing and accessibility attributes are included.
## When to Use This Skill
Use this skill when:
- Writing or modifying Angular component templates
- Creating any HTML templates or markup
- Reviewing code for testing and accessibility compliance
- Adding interactive elements (buttons, inputs, links, etc.)
- Implementing forms, lists, navigation, or dialogs
**Works seamlessly with:**
- **[angular-template](../angular-template/SKILL.md)** - Angular template syntax, control flow, and modern patterns
- **[tailwind](../tailwind/SKILL.md)** - ISA design system styling for visual design
## Overview
This skill provides comprehensive guidance for two critical HTML attribute categories:
### 1. E2E Testing Attributes
Enable automated end-to-end testing by providing stable selectors for QA automation:
- **`data-what`**: Semantic description of element's purpose
- **`data-which`**: Unique identifier for specific instances
- **`data-*`**: Additional contextual information
### 2. ARIA Accessibility Attributes
Ensure web applications are accessible to all users, including those using assistive technologies:
- **Roles**: Define element purpose (button, navigation, dialog, etc.)
- **Properties**: Provide additional context (aria-label, aria-describedby)
- **States**: Indicate dynamic states (aria-expanded, aria-disabled)
- **Live Regions**: Announce dynamic content changes
## Why Both Are Essential
- **E2E Attributes**: Enable reliable automated testing without brittle CSS or XPath selectors
- **ARIA Attributes**: Ensure compliance with WCAG standards and improve user experience for people with disabilities
- **Together**: Create robust, testable, and accessible web applications
## Quick Reference
### Button Example
```html
<button
type="button"
(click)="onSubmit()"
data-what="submit-button"
data-which="registration-form"
aria-label="Submit registration form">
Submit
</button>
```
### Input Example
```html
<input
type="text"
[(ngModel)]="email"
data-what="email-input"
data-which="registration-form"
aria-label="Email address"
aria-describedby="email-hint"
aria-required="true" />
<span id="email-hint">We'll never share your email</span>
```
### Dynamic List Example
```html
@for (item of items; track item.id) {
<li
(click)="selectItem(item)"
data-what="list-item"
[attr.data-which]="item.id"
[attr.data-status]="item.status"
[attr.aria-label]="'Select ' + item.name"
role="button"
tabindex="0">
{{ item.name }}
</li>
}
```
### Link Example
```html
<a
[routerLink]="['/orders', orderId]"
data-what="order-link"
[attr.data-which]="orderId"
[attr.aria-label]="'View order ' + orderNumber">
View Order #{{ orderNumber }}
</a>
```
### Dialog Example
```html
<div
class="dialog"
data-what="confirmation-dialog"
data-which="delete-item"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description">
<h2 id="dialog-title">Confirm Deletion</h2>
<p id="dialog-description">Are you sure you want to delete this item?</p>
<button
(click)="confirm()"
data-what="confirm-button"
data-which="delete-dialog"
aria-label="Confirm deletion">
Delete
</button>
<button
(click)="cancel()"
data-what="cancel-button"
data-which="delete-dialog"
aria-label="Cancel deletion">
Cancel
</button>
</div>
```
## Common Patterns by Element Type
### Interactive Elements That Need Attributes
**Required attributes for:**
- Buttons (`<button>`, `<ui-button>`, custom button components)
- Form inputs (`<input>`, `<textarea>`, `<select>`)
- Links (`<a>`, `[routerLink]`)
- Clickable elements (elements with `(click)` handlers)
- Custom interactive components
- List items in dynamic lists
- Navigation items
- Dialog/modal controls
### Naming Conventions
**E2E `data-what` patterns:**
- `*-button` (submit-button, cancel-button, delete-button)
- `*-input` (email-input, search-input, quantity-input)
- `*-link` (product-link, order-link, customer-link)
- `*-item` (list-item, menu-item, card-item)
- `*-dialog` (confirm-dialog, error-dialog, info-dialog)
- `*-dropdown` (status-dropdown, category-dropdown)
**E2E `data-which` guidelines:**
- Use unique identifiers: `data-which="primary"`, `data-which="customer-list"`
- Bind dynamically for lists: `[attr.data-which]="item.id"`
- Combine with context: `data-which="customer-{{ customerId }}-edit"`
**ARIA role patterns:**
- Interactive elements: `button`, `link`, `menuitem`
- Structural: `navigation`, `main`, `complementary`, `contentinfo`
- Widget: `dialog`, `alertdialog`, `tooltip`, `tablist`, `tab`
- Landmark: `banner`, `search`, `form`, `region`
## Best Practices
### E2E Attributes
1. ✅ Add to ALL interactive elements
2. ✅ Use kebab-case for `data-what` values
3. ✅ Ensure `data-which` is unique within the view
4. ✅ Use Angular binding for dynamic values: `[attr.data-*]`
5. ✅ Avoid including sensitive data in attributes
6. ✅ Document complex attribute patterns in template comments
### ARIA Attributes
1. ✅ Use semantic HTML first (use `<button>` instead of `<div role="button">`)
2. ✅ Provide text alternatives for all interactive elements
3. ✅ Ensure proper keyboard navigation (tabindex, focus management)
4. ✅ Use `aria-label` when visual label is missing
5. ✅ Use `aria-labelledby` to reference existing visible labels
6. ✅ Keep ARIA attributes in sync with visual states
7. ✅ Test with screen readers (NVDA, JAWS, VoiceOver)
### Combined Best Practices
1. ✅ Add both E2E and ARIA attributes to every interactive element
2. ✅ Keep attributes close together in the HTML for readability
3. ✅ Update tests to use `data-what` and `data-which` selectors
4. ✅ Validate coverage: all interactive elements should have both types
5. ✅ Review with QA and accessibility teams
## Detailed References
For comprehensive guides, examples, and patterns, see:
- **[E2E Testing Attributes](references/e2e-attributes.md)** - Complete E2E attribute patterns and conventions
- **[ARIA Accessibility Attributes](references/aria-attributes.md)** - Comprehensive ARIA guidance and WCAG compliance
- **[Combined Patterns](references/combined-patterns.md)** - Real-world examples with both attribute types
## Project-Specific Links
- **Testing Guidelines**: `docs/guidelines/testing.md` - Project testing standards including E2E attributes
- **CLAUDE.md**: Project conventions and requirements
- **Angular Template Skill**: `.claude/skills/angular-template` - For Angular-specific syntax
## Validation Checklist
Before considering template complete:
- [ ] All buttons have `data-what`, `data-which`, and `aria-label`
- [ ] All inputs have `data-what`, `data-which`, and appropriate ARIA attributes
- [ ] All links have `data-what`, `data-which`, and descriptive ARIA labels
- [ ] Dynamic lists use `[attr.data-*]` bindings with unique identifiers
- [ ] Dialogs have proper ARIA roles and relationships
- [ ] Forms have proper field associations and error announcements
- [ ] Interactive elements are keyboard accessible (tabindex where needed)
- [ ] No duplicate `data-which` values within the same view
- [ ] Screen reader testing completed (if applicable)
## Example: Complete Form
```html
<form
data-what="registration-form"
data-which="user-signup"
role="form"
aria-labelledby="form-title">
<h2 id="form-title">User Registration</h2>
<div class="form-field">
<label for="username-input">Username</label>
<input
id="username-input"
type="text"
[(ngModel)]="username"
data-what="username-input"
data-which="registration-form"
aria-required="true"
aria-describedby="username-hint" />
<span id="username-hint">Must be at least 3 characters</span>
</div>
<div class="form-field">
<label for="email-input">Email</label>
<input
id="email-input"
type="email"
[(ngModel)]="email"
data-what="email-input"
data-which="registration-form"
aria-required="true"
[attr.aria-invalid]="emailError ? 'true' : null"
aria-describedby="email-error" />
@if (emailError) {
<span
id="email-error"
role="alert"
aria-live="polite">
{{ emailError }}
</span>
}
</div>
<div class="form-actions">
<button
type="submit"
(click)="onSubmit()"
data-what="submit-button"
data-which="registration-form"
[attr.aria-disabled]="!isValid"
aria-label="Submit registration form">
Register
</button>
<button
type="button"
(click)="onCancel()"
data-what="cancel-button"
data-which="registration-form"
aria-label="Cancel registration">
Cancel
</button>
</div>
</form>
```
## Remember
- **Always use both E2E and ARIA attributes together**
- **E2E attributes enable automated testing** - your QA team relies on them
- **ARIA attributes enable accessibility** - legal requirement and right thing to do
- **Test with real users and assistive technologies** - automated checks aren't enough
- **Keep attributes up-to-date** - maintain as code changes
---
**This skill works automatically with Angular templates. Both E2E and ARIA attributes should be added to every interactive element.**

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,842 @@
# E2E Testing Attributes - Complete Reference
This reference provides comprehensive guidance for adding E2E (End-to-End) testing attributes to HTML templates for reliable automated testing.
## Table of Contents
- [Overview](#overview)
- [Core Attribute Types](#core-attribute-types)
- [Why E2E Attributes?](#why-e2e-attributes)
- [Naming Conventions](#naming-conventions)
- [Patterns by Element Type](#patterns-by-element-type)
- [Patterns by Component Type](#patterns-by-component-type)
- [Dynamic Attributes](#dynamic-attributes)
- [Best Practices](#best-practices)
- [Validation](#validation)
- [Testing Integration](#testing-integration)
## Overview
E2E testing attributes provide stable, semantic selectors for automated testing. They enable QA automation without relying on brittle CSS classes, IDs, or XPath selectors that frequently break when styling changes.
## Core Attribute Types
### 1. `data-what` (Required)
**Purpose**: Semantic description of the element's purpose or type
**Format**: kebab-case string
**Examples**:
- `data-what="submit-button"`
- `data-what="search-input"`
- `data-what="product-link"`
- `data-what="list-item"`
**Guidelines**:
- Describes WHAT the element is or does
- Should be consistent across similar elements
- Use descriptive, semantic names
- Keep it concise but clear
### 2. `data-which` (Required)
**Purpose**: Unique identifier for the specific instance of this element type
**Format**: kebab-case string or dynamic binding
**Examples**:
- `data-which="primary"` (static)
- `data-which="customer-form"` (static)
- `[attr.data-which]="item.id"` (dynamic)
- `[attr.data-which]="'customer-' + customerId"` (dynamic with context)
**Guidelines**:
- Identifies WHICH specific instance of this element type
- Must be unique within the same view/component
- Use dynamic binding for list items: `[attr.data-which]="item.id"`
- Can combine multiple identifiers: `data-which="customer-123-edit"`
### 3. `data-*` (Contextual)
**Purpose**: Additional contextual information about state, status, or data
**Format**: Custom attributes with kebab-case names
**Examples**:
- `data-status="active"`
- `data-index="0"`
- `data-role="admin"`
- `[attr.data-count]="items.length"`
**Guidelines**:
- Use for additional context that helps testing
- Avoid sensitive data (passwords, tokens, PII)
- Use Angular binding for dynamic values: `[attr.data-*]`
- Keep attribute names semantic and clear
## Why E2E Attributes?
### Problems with Traditional Selectors
**CSS Classes (Bad)**:
```html
<!-- Brittle - breaks when styling changes -->
<button class="btn btn-primary submit">Submit</button>
```
```javascript
// Test breaks when class names change
await page.click('.btn-primary.submit');
```
**XPath (Bad)**:
```javascript
// Brittle - breaks when structure changes
await page.click('//div[@class="form"]/button[2]');
```
**IDs (Better, but limited)**:
```html
<!-- IDs must be unique across entire page -->
<button id="submit-btn">Submit</button>
```
### Benefits of E2E Attributes
**Stable, Semantic Selectors (Good)**:
```html
<button
class="btn btn-primary"
data-what="submit-button"
data-which="registration-form">
Submit
</button>
```
```javascript
// Stable - survives styling and structure changes
await page.click('[data-what="submit-button"][data-which="registration-form"]');
```
**Advantages**:
- ✅ Decoupled from styling (CSS classes can change freely)
- ✅ Semantic and self-documenting
- ✅ Consistent across the application
- ✅ Easy to read and maintain
- ✅ Survives refactoring and restructuring
- ✅ QA and developers speak the same language
## Naming Conventions
### Common `data-what` Patterns
| Pattern | Use Case | Examples |
|---------|----------|----------|
| `*-button` | All button elements | `submit-button`, `cancel-button`, `delete-button`, `save-button` |
| `*-input` | Text inputs and textareas | `email-input`, `search-input`, `quantity-input`, `password-input` |
| `*-select` | Dropdown/select elements | `status-select`, `category-select`, `country-select` |
| `*-checkbox` | Checkbox inputs | `terms-checkbox`, `subscribe-checkbox`, `remember-checkbox` |
| `*-radio` | Radio button inputs | `payment-radio`, `shipping-radio` |
| `*-link` | Navigation links | `product-link`, `order-link`, `customer-link`, `home-link` |
| `*-item` | List/grid items | `list-item`, `menu-item`, `card-item`, `row-item` |
| `*-dialog` | Modals and dialogs | `confirm-dialog`, `error-dialog`, `info-dialog` |
| `*-dropdown` | Dropdown menus | `actions-dropdown`, `filter-dropdown` |
| `*-toggle` | Toggle switches | `theme-toggle`, `notifications-toggle` |
| `*-tab` | Tab navigation | `profile-tab`, `settings-tab` |
| `*-badge` | Status badges | `status-badge`, `count-badge` |
| `*-icon` | Interactive icons | `close-icon`, `menu-icon`, `search-icon` |
### `data-which` Naming Guidelines
**Static unique identifiers** (single instance):
- `data-which="primary"` - Primary action button
- `data-which="secondary"` - Secondary action button
- `data-which="main-search"` - Main search input
- `data-which="customer-form"` - Customer form context
**Dynamic identifiers** (multiple instances):
- `[attr.data-which]="item.id"` - List item by ID
- `[attr.data-which]="'product-' + product.id"` - Product item
- `[attr.data-which]="index"` - By array index (use sparingly)
**Contextual identifiers** (combine context):
- `data-which="customer-{{ customerId }}-edit"` - Edit button for specific customer
- `data-which="order-{{ orderId }}-cancel"` - Cancel button for specific order
## Patterns by Element Type
### Buttons
```html
<!-- Submit Button -->
<button
type="submit"
(click)="onSubmit()"
data-what="submit-button"
data-which="registration-form">
Submit
</button>
<!-- Cancel Button -->
<button
type="button"
(click)="onCancel()"
data-what="cancel-button"
data-which="registration-form">
Cancel
</button>
<!-- Delete Button with Confirmation -->
<button
(click)="onDelete(item)"
data-what="delete-button"
[attr.data-which]="item.id"
[attr.data-status]="item.canDelete ? 'enabled' : 'disabled'">
Delete
</button>
<!-- Icon Button -->
<button
(click)="toggleMenu()"
data-what="menu-button"
data-which="main-nav"
aria-label="Toggle menu">
<i class="icon-menu"></i>
</button>
<!-- Custom Button Component -->
<ui-button
(click)="save()"
data-what="save-button"
data-which="order-form">
Save Order
</ui-button>
```
### Inputs
```html
<!-- Text Input -->
<input
type="text"
[(ngModel)]="email"
placeholder="Email address"
data-what="email-input"
data-which="registration-form" />
<!-- Textarea -->
<textarea
[(ngModel)]="comments"
data-what="comments-textarea"
data-which="feedback-form"
rows="4"></textarea>
<!-- Number Input with State -->
<input
type="number"
[(ngModel)]="quantity"
data-what="quantity-input"
data-which="order-form"
[attr.data-min]="minQuantity"
[attr.data-max]="maxQuantity" />
<!-- Search Input -->
<input
type="search"
[(ngModel)]="searchTerm"
(input)="onSearch()"
placeholder="Search products..."
data-what="search-input"
data-which="product-catalog" />
<!-- Password Input -->
<input
type="password"
[(ngModel)]="password"
data-what="password-input"
data-which="login-form" />
```
### Select/Dropdown
```html
<!-- Basic Select -->
<select
[(ngModel)]="selectedStatus"
data-what="status-select"
data-which="order-filter">
<option value="">All Statuses</option>
<option value="pending">Pending</option>
<option value="completed">Completed</option>
</select>
<!-- Custom Dropdown Component -->
<ui-dropdown
[(value)]="selectedCategory"
data-what="category-dropdown"
data-which="product-filter">
</ui-dropdown>
```
### Checkboxes and Radios
```html
<!-- Checkbox -->
<label>
<input
type="checkbox"
[(ngModel)]="agreedToTerms"
data-what="terms-checkbox"
data-which="registration-form" />
I agree to the terms
</label>
<!-- Radio Group -->
<div data-what="payment-radio-group" data-which="checkout-form">
<label>
<input
type="radio"
name="payment"
value="credit"
[(ngModel)]="paymentMethod"
data-what="payment-radio"
data-which="credit-card" />
Credit Card
</label>
<label>
<input
type="radio"
name="payment"
value="paypal"
[(ngModel)]="paymentMethod"
data-what="payment-radio"
data-which="paypal" />
PayPal
</label>
</div>
```
### Links
```html
<!-- Static Link -->
<a
routerLink="/about"
data-what="nav-link"
data-which="about">
About Us
</a>
<!-- Dynamic Link with ID -->
<a
[routerLink]="['/products', product.id]"
data-what="product-link"
[attr.data-which]="product.id">
{{ product.name }}
</a>
<!-- External Link -->
<a
href="https://example.com"
target="_blank"
data-what="external-link"
data-which="documentation">
Documentation
</a>
<!-- Action Link (not navigation) -->
<a
(click)="downloadReport()"
data-what="download-link"
data-which="sales-report">
Download Report
</a>
```
### Lists and Tables
```html
<!-- Dynamic List with @for -->
<ul data-what="product-list" data-which="catalog">
@for (product of products; track product.id) {
<li
(click)="selectProduct(product)"
data-what="list-item"
[attr.data-which]="product.id"
[attr.data-status]="product.stock > 0 ? 'in-stock' : 'out-of-stock'">
{{ product.name }}
</li>
}
</ul>
<!-- Table Row -->
<table data-what="orders-table" data-which="customer-orders">
<tbody>
@for (order of orders; track order.id) {
<tr
data-what="table-row"
[attr.data-which]="order.id">
<td>{{ order.id }}</td>
<td>{{ order.date }}</td>
<td>
<button
data-what="view-button"
[attr.data-which]="order.id">
View
</button>
</td>
</tr>
}
</tbody>
</table>
```
### Dialogs and Modals
```html
<!-- Confirmation Dialog -->
<div
*ngIf="showDialog"
data-what="confirmation-dialog"
data-which="delete-item">
<h2>Confirm Deletion</h2>
<p>Are you sure you want to delete this item?</p>
<button
(click)="confirmDelete()"
data-what="confirm-button"
data-which="delete-dialog">
Delete
</button>
<button
(click)="cancelDelete()"
data-what="cancel-button"
data-which="delete-dialog">
Cancel
</button>
</div>
<!-- Info Dialog with Close -->
<div
data-what="info-dialog"
data-which="welcome-message">
<button
(click)="closeDialog()"
data-what="close-button"
data-which="dialog">
×
</button>
<div data-what="dialog-content" data-which="welcome">
<h2>Welcome!</h2>
<p>Thank you for joining us.</p>
</div>
</div>
```
## Patterns by Component Type
### Form Components
```html
<form data-what="user-form" data-which="registration">
<!-- Field inputs -->
<input
data-what="username-input"
data-which="registration-form"
type="text" />
<input
data-what="email-input"
data-which="registration-form"
type="email" />
<!-- Action buttons -->
<button
data-what="submit-button"
data-which="registration-form"
type="submit">
Submit
</button>
<button
data-what="cancel-button"
data-which="registration-form"
type="button">
Cancel
</button>
</form>
```
### List/Table Components
```html
<!-- Each item needs unique data-which -->
@for (item of items; track item.id) {
<div
data-what="list-item"
[attr.data-which]="item.id">
<span data-what="item-name" [attr.data-which]="item.id">
{{ item.name }}
</span>
<button
data-what="edit-button"
[attr.data-which]="item.id">
Edit
</button>
<button
data-what="delete-button"
[attr.data-which]="item.id">
Delete
</button>
</div>
}
```
### Navigation Components
```html
<nav data-what="main-navigation" data-which="header">
<a
routerLink="/dashboard"
data-what="nav-link"
data-which="dashboard">
Dashboard
</a>
<a
routerLink="/orders"
data-what="nav-link"
data-which="orders">
Orders
</a>
<a
routerLink="/customers"
data-what="nav-link"
data-which="customers">
Customers
</a>
</nav>
<!-- Breadcrumbs -->
<nav data-what="breadcrumb" data-which="page-navigation">
@for (crumb of breadcrumbs; track $index) {
<a
[routerLink]="crumb.url"
data-what="breadcrumb-link"
[attr.data-which]="crumb.id">
{{ crumb.label }}
</a>
}
</nav>
```
### Dialog/Modal Components
```html
<!-- All dialog buttons need clear identifiers -->
<div data-what="modal" data-which="user-settings">
<button
data-what="close-button"
data-which="modal">
Close
</button>
<button
data-what="save-button"
data-which="modal">
Save Changes
</button>
<button
data-what="reset-button"
data-which="modal">
Reset to Defaults
</button>
</div>
```
## Dynamic Attributes
### Using Angular Binding
When values need to be dynamic, use Angular's attribute binding:
```html
<!-- Static (simple values) -->
<button data-what="submit-button" data-which="form">
<!-- Dynamic (from component properties) -->
<button
data-what="submit-button"
[attr.data-which]="formId">
<!-- Dynamic (from loop variables) -->
@for (item of items; track item.id) {
<div
data-what="list-item"
[attr.data-which]="item.id"
[attr.data-status]="item.status"
[attr.data-index]="$index">
</div>
}
<!-- Dynamic (computed values) -->
<button
data-what="action-button"
[attr.data-which]="'customer-' + customerId + '-' + action">
</button>
```
### Loop Variables
Angular's `@for` provides special variables:
```html
@for (item of items; track item.id; let idx = $index; let isFirst = $first) {
<div
data-what="list-item"
[attr.data-which]="item.id"
[attr.data-index]="idx"
[attr.data-first]="isFirst">
{{ item.name }}
</div>
}
```
## Best Practices
### Do's ✅
1. **Add to ALL interactive elements**
- Buttons, inputs, links, clickable elements
- Custom components that handle user interaction
- Form controls and navigation items
2. **Use consistent naming**
- Follow the naming patterns (e.g., `*-button`, `*-input`)
- Use kebab-case consistently
- Be descriptive but concise
3. **Ensure uniqueness**
- `data-which` must be unique within the view
- Use item IDs for list items: `[attr.data-which]="item.id"`
- Combine context when needed: `data-which="form-primary-submit"`
4. **Use Angular binding for dynamic values**
- `[attr.data-which]="item.id"`
- `data-which="{{ item.id }}"` ❌ (avoid interpolation)
5. **Document complex patterns**
- Add comments for non-obvious attribute choices
- Document the expected test selectors
6. **Keep attributes updated**
- Update when element purpose changes
- Remove when elements are removed
- Maintain consistency across refactoring
### Don'ts ❌
1. **Don't include sensitive data**
-`data-which="password-{{ userPassword }}"`
-`data-token="{{ authToken }}"`
-`data-ssn="{{ socialSecurity }}"`
2. **Don't use generic values**
-`data-what="button"` (too generic)
-`data-what="submit-button"` (specific)
3. **Don't duplicate `data-which` in the same view**
- ❌ Two buttons with `data-which="primary"`
-`data-which="form-primary"` and `data-which="dialog-primary"`
4. **Don't rely only on index for lists**
-`[attr.data-which]="$index"` (changes when list reorders)
-`[attr.data-which]="item.id"` (stable identifier)
5. **Don't forget about custom components**
- Custom components need attributes too
- Attributes should be on the component tag, not just internal elements
## Validation
### Coverage Check
Ensure all interactive elements have E2E attributes:
```bash
# Count interactive elements
grep -E '\(click\)|routerLink|button|input|select|textarea' component.html | wc -l
# Count elements with data-what
grep -c 'data-what=' component.html
# Find elements missing E2E attributes
grep -E '\(click\)|button' component.html | grep -v 'data-what='
```
### Uniqueness Check
Verify no duplicate `data-which` values in the same template:
```typescript
// In component tests
it('should have unique data-which attributes', () => {
const elements = fixture.nativeElement.querySelectorAll('[data-which]');
const dataWhichValues = Array.from(elements).map(
(el: any) => el.getAttribute('data-which')
);
const uniqueValues = new Set(dataWhichValues);
expect(dataWhichValues.length).toBe(uniqueValues.size);
});
```
### Validation Checklist
- [ ] All buttons have `data-what` and `data-which`
- [ ] All inputs have `data-what` and `data-which`
- [ ] All links have `data-what` and `data-which`
- [ ] All clickable elements have attributes
- [ ] Dynamic lists use `[attr.data-which]="item.id"`
- [ ] No duplicate `data-which` values in the same view
- [ ] No sensitive data in attributes
- [ ] Custom components have attributes
- [ ] Attributes use kebab-case
- [ ] Coverage: 100% of interactive elements
## Testing Integration
### Using E2E Attributes in Tests
**Unit Tests (Angular Testing Utilities)**:
```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
describe('MyComponent', () => {
let fixture: ComponentFixture<MyComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyComponent],
}).compileComponents();
fixture = TestBed.createComponent(MyComponent);
fixture.detectChanges();
});
it('should have submit button with E2E attributes', () => {
const button = fixture.nativeElement.querySelector(
'[data-what="submit-button"][data-which="registration-form"]'
);
expect(button).toBeTruthy();
expect(button.textContent).toContain('Submit');
});
it('should have unique data-which for list items', () => {
const items = fixture.nativeElement.querySelectorAll('[data-what="list-item"]');
const dataWhichValues = Array.from(items).map(
(item: any) => item.getAttribute('data-which')
);
// All should have unique IDs
const uniqueValues = new Set(dataWhichValues);
expect(dataWhichValues.length).toBe(uniqueValues.size);
});
});
```
**E2E Tests (Playwright)**:
```typescript
import { test, expect } from '@playwright/test';
test('user registration flow', async ({ page }) => {
await page.goto('/register');
// Fill form using E2E attributes
await page.fill(
'[data-what="username-input"][data-which="registration-form"]',
'johndoe'
);
await page.fill(
'[data-what="email-input"][data-which="registration-form"]',
'john@example.com'
);
// Click submit using E2E attributes
await page.click(
'[data-what="submit-button"][data-which="registration-form"]'
);
// Verify success
await expect(page.locator('[data-what="success-message"]')).toBeVisible();
});
```
**E2E Tests (Cypress)**:
```typescript
describe('Order Management', () => {
it('should edit an order', () => {
cy.visit('/orders');
// Find specific order by ID using data-which
cy.get('[data-what="list-item"][data-which="order-123"]')
.should('be.visible');
// Click edit button for that specific order
cy.get('[data-what="edit-button"][data-which="order-123"]')
.click();
// Update quantity
cy.get('[data-what="quantity-input"][data-which="order-form"]')
.clear()
.type('5');
// Save changes
cy.get('[data-what="save-button"][data-which="order-form"]')
.click();
});
});
```
## Documentation in Templates
Add comment blocks to document E2E attributes:
```html
<!--
E2E Test Attributes:
- data-what="submit-button" data-which="registration-form" - Main form submission
- data-what="cancel-button" data-which="registration-form" - Cancel registration
- data-what="username-input" data-which="registration-form" - Username field
- data-what="email-input" data-which="registration-form" - Email field
- data-what="password-input" data-which="registration-form" - Password field
-->
<form data-what="registration-form" data-which="user-signup">
<!-- Form content -->
</form>
```
## Related Documentation
- **[ARIA Accessibility Attributes](aria-attributes.md)** - Accessibility guidance
- **[Combined Patterns](combined-patterns.md)** - Examples with E2E + ARIA together
- **Testing Guidelines**: `docs/guidelines/testing.md` - Project testing standards
- **CLAUDE.md**: Project code quality requirements
## Summary
E2E testing attributes are essential for:
- ✅ Stable, maintainable automated tests
- ✅ Clear communication between developers and QA
- ✅ Tests that survive styling and structural changes
- ✅ Self-documenting code that expresses intent
- ✅ Reliable CI/CD pipelines
**Always add `data-what` and `data-which` to every interactive element.**

View File

@@ -1,6 +1,6 @@
---
name: logging-helper
description: Ensures consistent usage of the @isa/core/logging library across the codebase with best practices for performance and maintainability
description: This skill should be used when working with Angular components, directives, services, pipes, guards, or TypeScript classes. Logging is MANDATORY in all Angular files. Implements @isa/core/logging with logger() factory pattern, appropriate log levels, lazy evaluation for performance, error handling, and avoids console.log and common mistakes.
---
# Logging Helper Skill

View File

@@ -1,5 +1,5 @@
---
name: tailwind-isa
name: tailwind
description: This skill should be used when working with Tailwind CSS styling in the ISA-Frontend project. Use it when writing component styles, choosing color values, applying typography, creating buttons, or determining appropriate spacing and layout utilities. Essential for maintaining design system consistency.
---
@@ -23,6 +23,15 @@ Invoke this skill when:
**Important**: This skill provides Tailwind utilities. Always prefer using components from `@isa/ui/*` libraries before applying custom Tailwind styles.
**Works together with:**
- **[angular-template](../angular-template/SKILL.md)** - Angular template syntax, control flow (@if, @for, @defer), and binding patterns
- **[html-template](../html-template/SKILL.md)** - E2E testing attributes (`data-what`, `data-which`) and ARIA accessibility
When building Angular components, these three skills work together:
1. Use **angular-template** for Angular syntax and control flow
2. Use **html-template** for `data-*` and ARIA attributes
3. Use **tailwind** (this skill) for styling with the ISA design system
## Core Design System Principles
### 0. Component Libraries First (Most Important)

2
.gitignore vendored
View File

@@ -78,3 +78,5 @@ vitest.config.*.timestamp*
nx.instructions.md
CLAUDE.md
*.pyc
.vite
reports/

View File

@@ -1,8 +1,8 @@
{
"mcpServers": {
"context7": {
"type": "sse",
"url": "https://mcp.context7.com/sse"
"type": "http",
"url": "https://mcp.context7.com/mcp"
},
"nx-mcp": {
"type": "stdio",

139
CHANGELOG.md Normal file
View File

@@ -0,0 +1,139 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- (checkout-reward) Disable and hide delivery options for reward feature purchases
- (purchase-options) Add disabledPurchaseOptions with flexible visibility control
- (reward-catalog) Pre-select in-store option for reward purchases
- (checkout) Complete reward order confirmation with reusable product info component
- (checkout) Implement reward order confirmation UI and confirmation list item action card component
- (checkout) Add reward order confirmation feature with schema migrations
- (stock-info) Implement request batching with BatchingResource
- (crm) Introduce PrimaryCustomerCardResource and format-name utility
- Angular template skill for modern template patterns
- Tailwind ISA design system skill
### Changed
- (checkout-reward) Implement hierarchical grouping on rewards order confirmation
- (checkout) Move reward selection helpers to data-access for reusability
- (common) Add validation for notification channel flag combinations
- (customer) Merge continueReward and continue methods into unified flow
- Comprehensive CLAUDE.md overhaul with library reference system
- Add Claude Code agents, commands, and skills infrastructure
### Fixed
- (checkout) Resolve currency constraint violations in price handling
- (checkout) Add complete price structure for reward delivery orders
- (checkout) Correct reward output desktop/mobile layout and add insufficient stock warnings
- (customer-card) Implement navigation flow from customer card to reward search
- (purchase-options) Correct customer features mapping
- (reward-order-confirmation) Group items by item-level delivery type
- (reward-order-confirmation) Correct typo and add loading state to collect button
- (reward-confirmation) Improve action card visibility and status messages
- (reward-selection-pop-up) Fix width issue
## [4.2] - 2025-10-23
### Added
- (checkout-reward) Add reward checkout feature (#5258)
- (crm) Add crm-data-access library with initial component and tests
- (shared-filter) Add canApply input to filter input menu components
- Architecture Decision Records (ADRs) documentation
- Error handling and validation infrastructure enhancements
### Changed
- (tabs) Implement backwards compatibility for Process → Tabs migration
- (notifications) Update remission path logic to use Date.now()
- (customer-card) Deactivate Create Customer with Card feature
- Update package.json and recreate package-lock.json for npm@11.6
- Disable markdown format on save in VSCode settings
### Fixed
- (process) Simulate "old tab logic" for compatibility
- (tabs) Correct singleton tabs interaction with new tab areas
- (remission-list) Prioritize reload trigger over exact search
- (remission-list-item, remission-list-empty-state) Improve empty state handling
## [4.1] - 2025-10-06
### Added
- (isa-app) Migrate remission navigation to tab-based routing system
- (utils) Add scroll-top button component
- (remission-list, empty-state) Add comprehensive empty state handling with user guidance
- (remission) Ensure package assignment before completing return receipts
- (libs-ui-dialog-feedback-dialog) Add auto-close functionality with configurable delay
- (old-ui-tooltip) Add pointer-events-auto to tooltip panel
### Changed
- (remission-list) Improve item update handling and UI feedback
- (remission-list, search-item-to-remit-dialog) Simplify dialog flow by removing intermediate steps
### Fixed
- (remission-list) Ensure list reload after search dialog closes
- (remission-list) Auto-select single search result when remission started
- (remission-list, remission-return-receipt-details, libs-dialog) Improve error handling with dedicated error dialog
- (remission-error) Simplify error handling in remission components
- (remission) Filter search results by stock availability and display stock info
- (remission-list, remission-data-access) Add impediment comment and remaining quantity tracking
- (remission-quantity-and-reason-item) Correct quantity input binding and dropdown behavior
- (remission-quantity-reason) Correct dropdown placeholder and remove hardcoded values
- (remission-filter-label) Improve filter button label display and default text
- (remission-data-access) Remove automatic date defaulting in fetchRemissions
- (remission-shared-search-item-to-remit-dialog) Display context-aware feedback on errors
- (isa-app-shell) Improve navigation link targeting for remission sub-routes
- (oms-data-access) Adjust tolino return eligibility logic for display damage
- (ui-input-controls-dropdown) Prevent multiple dropdowns from being open simultaneously
## [4.0] - 2025-07-23
### Added
- (oms-data-access) Initial implementation of OMS data access layer
- (oms-return-review) Implement return review feature
- (print-button) Implement reusable print button component with service integration
- (scanner) Add full-screen scanner styles and components
- (product-router-link) Add shared product router link directive and builder
- (tooltip) Add tooltip component and directive with customizable triggers
- (shared-scanner) Move scanner to shared/scanner location
- (common-data-access) Add takeUntil operators for keydown events
### Changed
- (oms-return-review, oms-return-summary) Fix return receipt mapping and ensure process completion
- (ui-tooltip) Remove native title attribute from tooltip icon host
- (oms-return-details) Improve layout and styling of order group item controls
- (searchbox) Improve formatting and add showScannerButton getter
- (libs-ui-item-rows) Improve data value wrapping and label sizing
- (shared-filter, search-bar, search-main) Add E2E data attributes for filtering and search
### Fixed
- (return-details) Update email validation and improve error handling
- (return-details) Correct storage key retrieval in ReturnDetailsStore
- (return-details) Small layout fix (#5171)
- (isa-app-moment-locale) Correct locale initialization for date formatting
- (oms-return-search) Fix display and logic issues in return search results
- (oms-return-search) Resolve issues in return search result item rendering
- (oms-task-list-item) Address styling and layout issues in return task list
- (ui-dropdown) Improve dropdown usability and conditional rendering
- (return-search) Correct typo in tooltip content
- (libs-shared-filter) Improve date range equality for default filter inputs
## [3.4] - 2025-02-10
_Earlier versions available in git history. Detailed changelog entries start from version 4.0._
### Historical Versions
Previous versions (3.3, 3.2, 3.1, 3.0, 2.x, 1.x) are available in the git repository.
For detailed information about changes in these versions, please refer to:
- Git tags: `git tag --sort=-creatordate`
- Commit history: `git log <tag-from>..<tag-to>`
- Pull requests in the repository
---
_This changelog was initially generated from git commit history. Future entries will be maintained manually following the Keep a Changelog format._

333
CLAUDE.md
View File

@@ -1,277 +1,30 @@
# CLAUDE.md
> **Last Updated:** 2025-10-22
> **Angular Version:** 20.1.2
> **Nx Version:** 21.3.2
> **Node.js:** ≥22.0.0
> **npm:** ≥10.0.0
This file contains meta-instructions for how Claude should work with the ISA-Frontend codebase.
## 🔴 CRITICAL: Mandatory Agent Usage
**You MUST use these subagents for ALL research tasks:**
**You MUST use these subagents for ALL research and knowledge management tasks:**
- **`docs-researcher`**: For ALL documentation (packages, libraries, READMEs)
- **`docs-researcher-advanced`**: Auto-escalate when docs-researcher fails
- **`Explore`**: For ALL code pattern searches and multi-file analysis
- **Direct tools (Read/Bash)**: ONLY for single specific files or commands
**Violations of this rule degrade performance and context quality. NO EXCEPTIONS.**
## Project Overview
## Communication Guidelines
This is a sophisticated Angular 20.1.2 monorepo managed by Nx 21.3.2. The main application is `isa-app`, a comprehensive inventory and returns management system for retail/e-commerce operations. The system handles complex workflows including order management (OMS), returns processing (remission), customer relationship management (CRM), product cataloging, and checkout/reward systems.
**Keep answers concise and focused:**
- Provide direct, actionable responses without unnecessary elaboration
- Skip verbose explanations unless specifically requested
- Focus on what the user needs to know, not everything you know
- Use bullet points and structured formatting for clarity
- Only provide detailed explanations when complexity requires it
## Architecture
## Researching and Investigating the Codebase
### Monorepo Structure
- **apps/isa-app**: Main Angular application
- **libs/**: Reusable libraries organized by domain and type
- **core/**: Core utilities (config, logging, storage, tabs, navigation)
- **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
**🔴 MANDATORY: You MUST use subagents for research. Direct file reading/searching.**
### Key Architectural Patterns
- **Domain-Driven Design**: Clear domain boundaries with dedicated modules (OMS, remission, CRM, catalogue, checkout)
- **Layered Architecture**: Strict dependency hierarchy (Feature → Shared/UI → Data Access → Infrastructure)
- **Standalone Components**: All new components use Angular standalone architecture with explicit imports
- **Feature Libraries**: Domain features organized as separate libraries (e.g., `oms-feature-return-search`, `remission-feature-remission-list`)
- **Data Access Layer**: Separate data-access libraries for each domain with NgRx Signals stores
- **Shared UI Components**: 17 dedicated UI component libraries with design system integration
- **Generated API Clients**: 10 auto-generated Swagger/OpenAPI clients with post-processing pipeline
- **Path Aliases**: Comprehensive TypeScript path mapping (`@isa/domain/layer/feature`)
- **Component Prefixes**: Domain-specific prefixes (OMS: `oms-feature-*`, Remission: `remi-*`, UI: `ui-*`)
- **Modern State Management**: NgRx Signals with entities, session persistence, and reactive patterns
## Common Development Commands
### Essential Commands (Project-Specific)
```bash
# Start development server with SSL (required for authentication flows)
npm start
# Run tests for all libraries (excludes main app)
npm test
# Build for development
npm run build
# Build for production
npm run build-prod
# Regenerate all API clients from Swagger/OpenAPI specs
npm run generate:swagger
# Regenerate library reference documentation
npm run docs:generate
# Format code with Prettier
npm run prettier
# Format only staged files (pre-commit hook)
npm run pretty-quick
# Run CI tests with coverage
npm run ci
```
### Standard Nx Commands
For complete command reference, see [Nx Documentation](https://nx.dev/reference/commands).
**Common patterns:**
```bash
# Test specific library (always use --skip-nx-cache)
npx nx test <project-name> --skip-nx-cache
# Lint a project
npx nx lint <project-name>
# Show project dependencies
npx nx graph
# Run tests for affected projects (CI/CD)
npx nx affected:test --skip-nx-cache
```
**Important:** Always use `--skip-nx-cache` flag when running tests to ensure fresh results.
## Testing Framework
> **Last Reviewed:** 2025-10-22
> **Status:** Migration in Progress (Jest → Vitest)
### Current Setup (Migration in Progress)
- **Jest**: 40 libraries (65.6% - legacy/existing code)
- **Vitest**: 21 libraries (34.4% - new standard)
- All formal libraries now have test executors configured
### Testing Strategy
- **New libraries**: Use Vitest + Angular Testing Utilities (TestBed, ComponentFixture)
- **Legacy libraries**: Continue with Jest + Spectator until migrated
- **Advanced mocking**: Use ng-mocks for complex scenarios
### Key Requirements
- Test files must end with `.spec.ts`
- Use AAA pattern (Arrange-Act-Assert)
- **Always include E2E attributes**: `data-what`, `data-which`, and dynamic `data-*` in HTML templates
- Mock external dependencies appropriately for your framework
**For detailed testing guidelines, framework comparison, and migration instructions, see [`docs/guidelines/testing.md`](docs/guidelines/testing.md).**
**References:**
- [Jest Documentation](https://jestjs.io/)
- [Vitest Documentation](https://vitest.dev/)
- [Angular Testing Guide](https://angular.io/guide/testing)
- [Spectator](https://ngneat.github.io/spectator/)
## State Management
- **NgRx Signals**: Primary state management with modern functional approach using `signalStore()`
- **Entity Management**: Uses `withEntities()` for normalized data storage
- **Session Persistence**: State persistence with `withStorage()` using SessionStorageProvider
- **Reactive Methods**: `rxMethod()` with `takeUntilKeydownEscape()` for user-cancellable operations
- **Custom RxJS Operators**: Specialized operators like `takeUntilAborted()`, `takeUntilKeydown()`
- **Error Handling**: `tapResponse()` for handling success/error states in stores
- **Lifecycle Hooks**: `withHooks()` for cleanup and initialization (e.g., orphaned entity cleanup)
- **Navigation State**: Use `@isa/core/navigation` for temporary navigation context (return URLs, wizard state) instead of query parameters
## Styling and Design System
- **Framework**: [Tailwind CSS](https://tailwindcss.com/docs) with extensive ISA-specific customization
- **Custom Breakpoints**: `isa-desktop` (1024px), `isa-desktop-l` (1440px), `isa-desktop-xl` (1920px)
- **Brand Color System**: `isa-*` color palette with semantic naming
- **Custom Tailwind Plugins** (7): button, typography, menu, label, input, section, select-bullet
- **Typography System**: 14 custom utilities (`.isa-text-heading-1-bold`, `.isa-text-body-2-regular`, etc.)
- **UI Component Libraries**: 17 specialized libraries with consistent APIs (see Library Reference)
- **Storybook**: Component documentation and development at `npm run storybook`
### Responsive Design with Breakpoint Service
Use `@isa/ui/layout` for reactive breakpoint detection instead of CSS-only solutions:
```typescript
import { breakpoint, Breakpoint } from '@isa/ui/layout';
// Detect screen size reactively
isDesktop = breakpoint([Breakpoint.Desktop, Breakpoint.DekstopL, Breakpoint.DekstopXL]);
```
**Available Breakpoints:**
- `Tablet`: max-width: 1279px (mobile/tablet)
- `Desktop`: 1280px - 1439px (standard desktop)
- `DekstopL`: 1440px - 1919px (large desktop)
- `DekstopXL`: 1920px+ (extra large)
**Template Usage:**
```html
@if (isDesktop) {
<!-- Desktop-specific content -->
}
```
**Why:** Prefer breakpoint service over CSS-only (hidden/flex) for SSR and maintainability.
## API Integration and Data Access
**Generated Swagger Clients:** 10 auto-generated TypeScript clients in `generated/swagger/`
- Available APIs: availability-api, cat-search-api, checkout-api, crm-api, eis-api, inventory-api, isa-api, oms-api, print-api, wws-api
- Tool: [ng-swagger-gen](https://www.npmjs.com/package/ng-swagger-gen) with custom per-API configuration
- Post-processing: Automatic Unicode cleanup via `tools/fix-files.js`
- Regenerate: `npm run generate:swagger`
**Architecture Pattern:**
- Business logic services wrap generated API clients
- Type safety: TypeScript + [Zod](https://zod.dev/) schema validation
- Error handling: Global HTTP interceptor with automatic re-authentication
- Modern injection: Uses `inject()` function with private field pattern
- Request cancellation: Built-in via AbortSignal and custom RxJS operators (`takeUntilAborted()`, `takeUntilKeydown()`)
**Data Access Libraries:** See Library Reference section for domain-specific implementations (`@isa/[domain]/data-access`).
## Build Configuration
- **Framework**: Angular with TypeScript (see `package.json` for current versions)
- **Requirements**:
- Node.js >= 22.0.0 (specified in package.json engines)
- npm >= 10.0.0 (specified in package.json engines)
- **Build System**: Nx monorepo with Vite for testing (Vitest)
- **Development Server**: Serves with SSL by default (required for authentication flows)
## Important Conventions and Patterns
### Library Organization
- **Naming Pattern**: `[domain]-[layer]-[feature]` (e.g., `oms-feature-return-search`, `ui-buttons`)
- **Path Aliases**: `@isa/[domain]/[layer]/[feature]` (e.g., `@isa/oms/data-access`, `@isa/ui/buttons`)
- **Project Names**: Found in each library's `project.json` file, following consistent naming
### Component Architecture
- **Standalone Components**: All new components must be standalone with explicit imports
- **Component Prefixes**: Domain-specific prefixes for clear identification
- OMS features: `oms-feature-*` (e.g., `oms-feature-return-search-main`)
- Remission features: `remi-*`
- UI components: `ui-*`
- Core utilities: `core-*`
- **Signal-based Inputs**: Use Angular signals (`input()`, `computed()`) for reactive properties
- **Host Binding**: Dynamic CSS classes via Angular host properties
### Dependency Rules
- **Unidirectional Dependencies**: Feature → Shared/UI → Data Access → Infrastructure
- **Import Boundaries**: Use path aliases, avoid relative imports across domain boundaries
- **Generated API Imports**: Import from `@generated/swagger/[api-name]` for API clients
### Code Quality
- **Modern Angular Patterns**: Prefer `inject()` over constructor injection
- **Type Safety**: Use Zod schemas for runtime validation alongside TypeScript
- **Error Handling**: Custom error classes with specific error codes
- **E2E Testing**: Always include `data-what` and `data-which` attributes in templates
## Development Workflow and Best Practices
### Project Conventions
- **Default Branch**: `develop` (not main) - Always create PRs against develop
- **Branch Naming**: When starting work on a new feature or bug, create a branch following this pattern:
- Format: `feature/{task-id}-{short-description}` or `bugfix/{task-id}-{short-description}`
- Use English kebab-case for the description
- Start with the task/issue ID (e.g., `5391`)
- Keep description concise - shorten if the full title is too long
- Example: For task "#5391 Prämie Checkout // Action Card - Versandbestellung"
- Branch: `feature/5391-praemie-checkout-action-card-delivery-order`
- **Commit Style**: [Conventional commits](https://www.conventionalcommits.org/) without co-author tags
- **Nx Cache**: Always use `--skip-nx-cache` for tests to ensure fresh results
- **Testing**: New libraries use Vitest + Angular Testing Utilities; legacy use Jest + Spectator
- **E2E Attributes**: Always include `data-what`, `data-which`, and dynamic `data-*` in templates
- **Design System**: Use ISA-specific Tailwind utilities (`isa-accent-*`, `isa-text-*`)
### Code Quality Tools
- **Linting**: [ESLint](https://eslint.org/) with Nx dependency checks
- **Formatting**: [Prettier](https://prettier.io/) with Husky + lint-staged pre-commit hooks
- **Type Safety**: [TypeScript](https://www.typescriptlang.org/) strict mode + [Zod](https://zod.dev/) validation
- **Bundle Size**: Monitor carefully (2MB warning, 5MB error for main bundle)
### Nx Workflow Tips
- Use `npx nx graph` to visualize dependencies
- Use `npx nx affected:test` for CI/CD optimization
- Reference: [Nx Documentation](https://nx.dev/getting-started/intro)
## Development Notes and Guidelines
### Getting Started
- **Application Startup**: Only `isa-app` can be started - it's the main application entry point
- **SSL Development**: The development server runs with SSL by default (`npm start`), which is crucial for production-like authentication flows
- **Node Requirements**: Ensure Node.js ≥22.0.0 and npm ≥10.0.0 before starting development
- **First-Time Setup**: After cloning, run `npm install` then `npm start` to verify everything works
### Essential Documentation References
- **Testing Guidelines**: Review `docs/guidelines/testing.md` before writing any tests - it covers the Jest→Vitest migration, Spectator→Angular Testing Utilities transition, and E2E attribute requirements
- **Code Review Standards**: Follow the structured review process in `.github/review-instructions.md` with categorized feedback (🚨 Critical, ❗ Minor, ⚠️ Warnings, ✅ Good Practices)
- **E2E Testing Requirements**: Always include `data-what`, `data-which`, and dynamic `data-*` attributes in HTML templates - these are essential for automated testing by QA colleagues
### Researching and Investigating the Codebase
**🔴 MANDATORY: You MUST use subagents for research. Direct file reading/searching is FORBIDDEN except for single specific files.**
#### Required Agent Usage
### Required Agent Usage
| Task Type | Required Agent | Escalation Path |
|-----------|---------------|-----------------|
@@ -281,7 +34,7 @@ isDesktop = breakpoint([Breakpoint.Desktop, Breakpoint.DekstopL, Breakpoint.Deks
| **Implementation Analysis** | `Explore` | Multiple file analysis |
| **Single Specific File** | Read tool directly | No agent needed |
#### Documentation Research System (Two-Tier)
### Documentation Research System (Two-Tier)
1. **ALWAYS start with `docs-researcher`** (Haiku, 30-120s) for any documentation need
2. **Auto-escalate to `docs-researcher-advanced`** (Sonnet, 2-7min) when:
@@ -290,7 +43,7 @@ isDesktop = breakpoint([Breakpoint.Desktop, Breakpoint.DekstopL, Breakpoint.Deks
- Need code inference
- Complex architectural questions
#### Enforcement Examples
### Enforcement Examples
```
❌ WRONG: Read libs/ui/buttons/README.md
@@ -304,61 +57,3 @@ isDesktop = breakpoint([Breakpoint.Desktop, Breakpoint.DekstopL, Breakpoint.Deks
```
**Remember: Using subagents is NOT optional - it's mandatory for maintaining context efficiency and search quality.**
#### Common Research Patterns
| Information Need | Required Approach |
|-----------------|-------------------|
| **Library documentation** | `docs-researcher` → Check library-reference.md → Escalate if needed |
| **Code patterns/examples** | `Explore` with "medium" or "very thorough" |
| **Architecture understanding** | `npx nx graph` + `Explore` for implementation |
| **Debugging/errors** | Direct tool use (Read specific error file, check console) |
#### Debugging Tips
- **TypeScript errors**: Follow error path to exact file:line
- **Test failures**: Use `--skip-nx-cache` for fresh output
- **Module resolution**: Check `tsconfig.base.json` path aliases
- **State issues**: Use Angular DevTools browser extension
### Library Development Patterns
- **Library Documentation**: Use `docs-researcher` for ALL library READMEs (mandatory for context management)
- **New Library Creation**: Use Nx generators with domain-specific naming (`[domain]-[layer]-[feature]`)
- **Standalone Components**: All new components must be standalone with explicit imports - no NgModules
- **Testing Framework**: New = Vitest + Angular Testing Utilities, Legacy = Jest + Spectator
- **Path Aliases**: Always use `@isa/[domain]/[layer]/[feature]` - avoid relative imports
#### Library Reference Guide
The monorepo contains **62 libraries** organized across 12 domains. For quick lookup, see **[`docs/library-reference.md`](docs/library-reference.md)**.
**Quick Overview by Domain:**
- Availability (1) | Catalogue (1) | Checkout (6) | Common (3) | Core (5) | CRM (1) | Icons (1)
- OMS (9) | Remission (8) | Shared Components (7) | UI Components (17) | Utilities (3)
### API Integration Workflow
- **Swagger Generation**: Run `npm run generate:swagger` to regenerate all 10 API clients when backend changes
- **Data Services**: Wrap generated API clients in domain-specific data-access services with proper error handling and Zod validation
- **State Management**: Use NgRx Signals with `signalStore()`, entity management, and session persistence for complex state
### Performance and Quality Considerations
- **Bundle Monitoring**: Watch bundle sizes (2MB warning, 5MB error for main bundle)
- **Testing Cache**: Always use `--skip-nx-cache` flag when running tests to ensure reliable results
- **Code Quality**: Pre-commit hooks enforce Prettier formatting and ESLint rules automatically
- **Memory Management**: Clean up subscriptions and use OnPush change detection for optimal performance
### Common Troubleshooting
- **Build Issues**: Check Node version and run `npm install` if encountering module resolution errors
- **Test Failures**: Use `--skip-nx-cache` flag and ensure test isolation (no shared state between tests)
- **Nx Cache Issues**: If you see `existing outputs match the cache, left as is` during build or testing:
- **Option 1**: Run `npx nx reset` to clear the Nx cache completely
- **Option 2**: Use `--skip-nx-cache` flag to bypass Nx cache for a specific command (e.g., `npx nx test <project> --skip-nx-cache`)
- **When to use**: Always use `--skip-nx-cache` when you need guaranteed fresh builds or test results
- **SSL Certificates**: Development server uses SSL - accept certificate warnings in browser for localhost
- **Import Errors**: Verify path aliases in `tsconfig.base.json` and use absolute imports for cross-library dependencies
### Domain-Specific Conventions
- **Component Prefixes**: Use `oms-feature-*` for OMS, `remi-*` for remission, `ui-*` for shared components
- **Git Workflow**: Default branch is `develop` (not main), use conventional commits without co-author tags
- **Design System**: Use ISA-specific Tailwind utilities (`isa-accent-*`, `isa-text-*`) and custom breakpoints (`isa-desktop-*`)
- **Logging**: Use centralized logging service (`@isa/core/logging`) with contextual information for debugging
- **Navigation State**: Use `@isa/core/navigation` for passing temporary state between routes (return URLs, form context) instead of query parameters - keeps URLs clean and state reliable

View File

@@ -1,4 +1,8 @@
import type { Preview } from '@storybook/angular';
import { registerLocaleData } from '@angular/common';
import localeDe from '@angular/common/locales/de';
registerLocaleData(localeDe);
const preview: Preview = {
tags: ['autodocs'],

View File

@@ -3,31 +3,49 @@ import { ActionHandler } from './action-handler.interface';
import { CommandService } from './command.service';
import { FEATURE_ACTION_HANDLERS, ROOT_ACTION_HANDLERS } from './tokens';
export function provideActionHandlers(actionHandlers: Type<ActionHandler>[]): Provider[] {
export function provideActionHandlers(
actionHandlers: Type<ActionHandler>[],
): Provider[] {
return [
CommandService,
actionHandlers.map((handler) => ({ provide: FEATURE_ACTION_HANDLERS, useClass: handler, multi: true })),
actionHandlers.map((handler) => ({
provide: FEATURE_ACTION_HANDLERS,
useClass: handler,
multi: true,
})),
];
}
@NgModule({})
export class CoreCommandModule {
static forRoot(actionHandlers: Type<ActionHandler>[]): ModuleWithProviders<CoreCommandModule> {
static forRoot(
actionHandlers: Type<ActionHandler>[],
): ModuleWithProviders<CoreCommandModule> {
return {
ngModule: CoreCommandModule,
providers: [
CommandService,
actionHandlers.map((handler) => ({ provide: ROOT_ACTION_HANDLERS, useClass: handler, multi: true })),
actionHandlers.map((handler) => ({
provide: ROOT_ACTION_HANDLERS,
useClass: handler,
multi: true,
})),
],
};
}
static forChild(actionHandlers: Type<ActionHandler>[]): ModuleWithProviders<CoreCommandModule> {
static forChild(
actionHandlers: Type<ActionHandler>[],
): ModuleWithProviders<CoreCommandModule> {
return {
ngModule: CoreCommandModule,
providers: [
CommandService,
actionHandlers.map((handler) => ({ provide: FEATURE_ACTION_HANDLERS, useClass: handler, multi: true })),
actionHandlers.map((handler) => ({
provide: FEATURE_ACTION_HANDLERS,
useClass: handler,
multi: true,
})),
],
};
}

View File

@@ -15,10 +15,13 @@ export class CommandService {
for (const action of actions) {
const handler = this.getActionHandler(action);
if (!handler) {
console.error('CommandService.handleCommand', 'Action Handler does not exist', { action });
console.error(
'CommandService.handleCommand',
'Action Handler does not exist',
{ action },
);
throw new Error('Action Handler does not exist');
}
data = await handler.handler(data, this);
}
return data;
@@ -29,10 +32,18 @@ export class CommandService {
}
getActionHandler(action: string): ActionHandler | undefined {
const featureActionHandlers: ActionHandler[] = this.injector.get(FEATURE_ACTION_HANDLERS, []);
const rootActionHandlers: ActionHandler[] = this.injector.get(ROOT_ACTION_HANDLERS, []);
const featureActionHandlers: ActionHandler[] = this.injector.get(
FEATURE_ACTION_HANDLERS,
[],
);
const rootActionHandlers: ActionHandler[] = this.injector.get(
ROOT_ACTION_HANDLERS,
[],
);
let handler = [...featureActionHandlers, ...rootActionHandlers].find((handler) => handler.action === action);
let handler = [...featureActionHandlers, ...rootActionHandlers].find(
(handler) => handler.action === action,
);
if (this._parent && !handler) {
handler = this._parent.getActionHandler(action);

View File

@@ -72,12 +72,82 @@ import { ApplicationService } from '@core/application';
import { CustomerDTO } from '@generated/swagger/crm-api';
import { Config } from '@core/config';
import parseDuration from 'parse-duration';
import { CheckoutMetadataService } from '@isa/checkout/data-access';
import {
CheckoutMetadataService,
ShoppingCart,
} from '@isa/checkout/data-access';
import {
ShoppingCartEvent,
ShoppingCartEvents,
} from '@isa/checkout/data-access';
/**
* Domain service for managing the complete checkout flow including shopping cart operations,
* checkout creation, buyer/payer management, payment processing, and order completion.
*
* This service orchestrates interactions between:
* - NgRx Store for state management
* - Multiple Swagger API clients (checkout, OMS, shopping cart, payment, buyer, payer, branch, kulturpass)
* - Shopping cart event system for cross-component synchronization
* - Availability service for real-time product availability checks
*
* @remarks
* **Process ID Pattern**: All methods require a `processId` (typically `Date.now()`) to isolate
* checkout sessions. Multiple concurrent checkout processes can run independently.
*
* **Observable-First Design**: Most methods return Observables for reactive composition. Consumers
* should use RxJS operators for transformation and error handling.
*
* **Auto-Creation**: Shopping carts auto-create if missing. The service uses `filter()` operators
* to trigger lazy initialization and prevent race conditions.
*
* **Event Sourcing**: Publishes shopping cart events (Created, ItemAdded, ItemUpdated, ItemRemoved)
* for synchronization across components. Subscribes to events from `ShoppingCartService` to maintain
* consistency.
*
* **OLA Management**: Tracks Order Level Agreement (OLA) expiration timestamps per item and order type.
* Validates availability freshness before checkout completion.
*
* @example
* ```typescript
* // Basic shopping cart flow
* const processId = Date.now();
*
* // Add items to cart
* this.checkoutService.addItemToShoppingCart({
* processId,
* items: [{ productId: 123, quantity: 2 }]
* }).subscribe();
*
* // Get cart (auto-creates if missing)
* this.checkoutService.getShoppingCart({ processId })
* .subscribe(cart => console.log(cart));
*
* // Complete checkout (orchestrates all steps)
* this.checkoutService.completeCheckout({ processId })
* .subscribe(orders => console.log('Orders created:', orders));
* ```
*
* @see DomainCheckoutSelectors For state selection patterns
* @see DomainCheckoutActions For available actions
* @see ShoppingCartEvents For event system integration
*/
@Injectable()
export class DomainCheckoutService {
/** Metadata service for shopping cart persistence */
#checkoutMetadataService = inject(CheckoutMetadataService);
/** Event bus for shopping cart synchronization across components */
#shoppingCartEvents = inject(ShoppingCartEvents);
/**
* Gets the OLA (Order Level Agreement) expiration duration in milliseconds.
*
* OLA expiration defines how long availability data remains valid before requiring refresh.
* Default is 5 minutes if not configured.
*
* @returns Duration in milliseconds
*/
get olaExpiration() {
const exp = this._config.get('@domain/checkout.olaExpiration') ?? '5m';
return parseDuration(exp);
@@ -96,9 +166,56 @@ export class DomainCheckoutService {
private _payerService: StoreCheckoutPayerService,
private _branchService: StoreCheckoutBranchService,
private _kulturpassService: KulturPassService,
) {}
) {
// Subscribe to shopping cart events from ShoppingCartService
this.#shoppingCartEvents.events$
.pipe(
// Only process events from ShoppingCartService to avoid circular updates
filter((payload) => payload.source === 'ShoppingCartService'),
)
.subscribe((payload) => {
// Update the store with the shopping cart from the event
this.store.dispatch(
DomainCheckoutActions.setShoppingCartByShoppingCartId({
shoppingCartId: payload.shoppingCart.id!,
shoppingCart: payload.shoppingCart as any,
}),
);
});
}
//#region shoppingcart
/**
* Retrieves the shopping cart for a given process ID. Auto-creates the cart if it doesn't exist.
*
* @remarks
* **Auto-Creation**: If no cart exists for the process ID, triggers `createShoppingCart()` automatically.
* The Observable filters out null/undefined and waits for cart creation to complete.
*
* **Latest Data**: Setting `latest: true` forces a refresh from the API instead of using cached state.
* This is useful before critical operations like checkout completion.
*
* **Memoization**: Uses `@memorize()` decorator to cache results by parameters, reducing duplicate calls.
*
* @param params - Parameters object
* @param params.processId - Unique process identifier (typically `Date.now()`)
* @param params.latest - If true, fetches fresh data from API; if false/undefined, uses store state
*
* @returns Observable of the shopping cart DTO. Never emits null/undefined.
*
* @example
* ```typescript
* // Get cart from store (creates if missing)
* this.checkoutService.getShoppingCart({ processId: 123 })
* .pipe(first())
* .subscribe(cart => console.log('Items:', cart.items));
*
* // Force refresh from API
* this.checkoutService.getShoppingCart({ processId: 123, latest: true })
* .pipe(first())
* .subscribe(cart => console.log('Fresh cart:', cart));
* ```
*/
@memorize()
getShoppingCart({
processId,
@@ -127,7 +244,6 @@ export class DomainCheckoutService {
.pipe(
map((response) => response.result),
tap((shoppingCart) => {
this.updateProcessCount(processId, shoppingCart);
this.store.dispatch(
DomainCheckoutActions.setShoppingCart({
processId,
@@ -144,6 +260,24 @@ export class DomainCheckoutService {
);
}
/**
* Reloads the shopping cart from the API and updates the store with fresh data.
*
* This is an async method that fetches the latest cart state from the backend
* and dispatches an action to update the NgRx store. Unlike `getShoppingCart({ latest: true })`,
* this method doesn't return the cart Observable - it's fire-and-forget.
*
* @param params - Parameters object
* @param params.processId - Process identifier
*
* @returns Promise that resolves when reload completes (or immediately if no cart exists)
*
* @example
* ```typescript
* await this.checkoutService.reloadShoppingCart({ processId: 123 });
* console.log('Cart reloaded');
* ```
*/
async reloadShoppingCart({ processId }: { processId: number }) {
const shoppingCart = await firstValueFrom(
this.store.select(DomainCheckoutSelectors.selectShoppingCartByProcessId, {
@@ -159,7 +293,6 @@ export class DomainCheckoutService {
}),
);
this.updateProcessCount(processId, cart.result);
this.store.dispatch(
DomainCheckoutActions.setShoppingCart({
processId,
@@ -168,6 +301,29 @@ export class DomainCheckoutService {
);
}
/**
* Creates a new shopping cart and associates it with the given process ID.
*
* @remarks
* **State Updates**:
* - Saves shopping cart ID to metadata service for persistence
* - Publishes `ShoppingCartEvent.Created` event for component synchronization
* - Dispatches `setShoppingCart` action to update NgRx store
*
* **Auto-Invocation**: Usually called automatically by `getShoppingCart()` when no cart exists.
* Rarely needs to be called directly.
*
* @param params - Parameters object
* @param params.processId - Process identifier to associate with the new cart
*
* @returns Observable of the newly created shopping cart DTO
*
* @example
* ```typescript
* this.checkoutService.createShoppingCart({ processId: Date.now() })
* .subscribe(cart => console.log('Cart created:', cart.id));
* ```
*/
createShoppingCart({
processId,
}: {
@@ -182,6 +338,11 @@ export class DomainCheckoutService {
processId,
shoppingCart.id,
);
this.#shoppingCartEvents.pub(
ShoppingCartEvent.Created,
shoppingCart as ShoppingCart,
'DomainCheckoutService',
);
this.store.dispatch(
DomainCheckoutActions.setShoppingCart({
processId,
@@ -192,6 +353,41 @@ export class DomainCheckoutService {
);
}
/**
* Adds one or more items to the shopping cart.
*
* @remarks
* **Process Flow**:
* 1. Retrieves existing shopping cart (creates if missing)
* 2. Calls API to add items
* 3. Publishes `ShoppingCartEvent.ItemAdded` event
* 4. Updates NgRx store with modified cart
*
* **Validation**: Ensure items are validated via `canAddItem()` or `canAddItems()` before calling
* this method to avoid API errors.
*
* @param params - Parameters object
* @param params.processId - Process identifier
* @param params.items - Array of items to add (product, quantity, availability, destination, etc.)
*
* @returns Observable of the updated shopping cart with new items
*
* @example
* ```typescript
* this.checkoutService.addItemToShoppingCart({
* processId: 123,
* items: [{
* product: { ean: '1234567890', catalogProductNumber: 456 },
* quantity: 2,
* availability: availabilityDto,
* destination: destinationDto
* }]
* }).subscribe(cart => console.log('Items:', cart.items.length));
* ```
*
* @see canAddItem For single item validation
* @see canAddItems For bulk item validation
*/
addItemToShoppingCart({
processId,
items,
@@ -210,7 +406,11 @@ export class DomainCheckoutService {
.pipe(
map((response) => response.result),
tap((shoppingCart) => {
this.updateProcessCount(processId, shoppingCart);
this.#shoppingCartEvents.pub(
ShoppingCartEvent.ItemAdded,
shoppingCart as ShoppingCart,
'DomainCheckoutService',
);
this.store.dispatch(
DomainCheckoutActions.setShoppingCart({
processId,
@@ -270,6 +470,37 @@ export class DomainCheckoutService {
);
}
/**
* Validates if a single item can be added to the shopping cart.
*
* Checks business rules, customer features, and cart compatibility before adding items.
* Use this before calling `addItemToShoppingCart()` to prevent API errors.
*
* @param params - Parameters object
* @param params.processId - Process identifier
* @param params.availability - OLA availability data for the item
* @param params.orderType - Order type (e.g., 'Abholung', 'Versand', 'Download', 'Rücklage')
*
* @returns Observable of `true` if item can be added, or error message string if not allowed
*
* @example
* ```typescript
* this.checkoutService.canAddItem({
* processId: 123,
* availability: olaAvailability,
* orderType: 'Versand'
* }).subscribe(result => {
* if (result === true) {
* // Proceed with adding item
* this.checkoutService.addItemToShoppingCart(...);
* } else {
* console.error('Cannot add item:', result);
* }
* });
* ```
*
* @see canAddItems For bulk validation
*/
canAddItem({
processId,
availability,
@@ -315,6 +546,38 @@ export class DomainCheckoutService {
.pipe(map((response) => response?.result));
}
/**
* Validates if multiple items can be added to the shopping cart in bulk.
*
* More efficient than calling `canAddItem()` multiple times. Returns validation
* results for each item in the payload.
*
* @param params - Parameters object
* @param params.processId - Process identifier
* @param params.payload - Array of item payloads to validate
* @param params.orderType - Order type for all items
*
* @returns Observable array of validation results (one per item). Each result contains
* `ok` flag and optional error messages.
*
* @example
* ```typescript
* this.checkoutService.canAddItems({
* processId: 123,
* payload: [
* { availabilities: [avail1], productId: 111, quantity: 2 },
* { availabilities: [avail2], productId: 222, quantity: 1 }
* ],
* orderType: 'Versand'
* }).subscribe(results => {
* results.forEach((result, index) => {
* console.log(`Item ${index}:`, result.ok ? 'Valid' : result.message);
* });
* });
* ```
*
* @see canAddItem For single item validation
*/
canAddItems({
processId,
payload,
@@ -386,6 +649,50 @@ export class DomainCheckoutService {
);
}
/**
* Updates an existing item in the shopping cart (quantity, availability, special comment, etc.).
*
* @remarks
* **Special Behavior**:
* - Setting `quantity: 0` removes the item and publishes `ItemRemoved` event instead of `ItemUpdated`
* - Always fetches latest cart state (`latest: true`) to avoid stale data conflicts
* - If availability is updated, adds timestamp to history for OLA validation
*
* **Event Publishing**:
* - Publishes `ShoppingCartEvent.ItemRemoved` if quantity is 0
* - Publishes `ShoppingCartEvent.ItemUpdated` for all other changes
*
* @param params - Parameters object
* @param params.processId - Process identifier
* @param params.shoppingCartItemId - ID of the cart item to update
* @param params.update - Fields to update (quantity, availability, specialComment, etc.)
*
* @returns Observable of the updated shopping cart
*
* @example
* ```typescript
* // Update quantity
* this.checkoutService.updateItemInShoppingCart({
* processId: 123,
* shoppingCartItemId: 456,
* update: { quantity: 5 }
* }).subscribe();
*
* // Remove item (quantity = 0)
* this.checkoutService.updateItemInShoppingCart({
* processId: 123,
* shoppingCartItemId: 456,
* update: { quantity: 0 }
* }).subscribe();
*
* // Update availability
* this.checkoutService.updateItemInShoppingCart({
* processId: 123,
* shoppingCartItemId: 456,
* update: { availability: newAvailabilityDto }
* }).subscribe();
* ```
*/
updateItemInShoppingCart({
processId,
shoppingCartItemId,
@@ -407,6 +714,17 @@ export class DomainCheckoutService {
.pipe(
map((response) => response.result),
tap((shoppingCart) => {
// Check if item was removed (quantity === 0)
const eventType =
update.quantity === 0
? ShoppingCartEvent.ItemRemoved
: ShoppingCartEvent.ItemUpdated;
this.#shoppingCartEvents.pub(
eventType,
shoppingCart as ShoppingCart,
'DomainCheckoutService',
);
this.store.dispatch(
DomainCheckoutActions.setShoppingCart({
processId,
@@ -425,8 +743,6 @@ export class DomainCheckoutService {
),
);
}
this.updateProcessCount(processId, shoppingCart);
}),
),
),
@@ -437,6 +753,34 @@ export class DomainCheckoutService {
//#region Checkout
/**
* Retrieves the checkout entity for a given process. Auto-creates if missing.
*
* @remarks
* **Auto-Creation**: Similar to `getShoppingCart()`, automatically triggers `createCheckout()`
* if no checkout exists for the process ID.
*
* **Refresh**: Setting `refresh: true` forces recreation of the checkout entity from the API.
*
* **Purpose**: The checkout entity aggregates buyer, payer, payment, destinations, and
* notification channels. It's required before order completion.
*
* @param params - Parameters object
* @param params.processId - Process identifier
* @param params.refresh - If true, recreates checkout from API; if false/undefined, uses store state
*
* @returns Observable of the checkout DTO. Never emits null/undefined.
*
* @example
* ```typescript
* this.checkoutService.getCheckout({ processId: 123 })
* .pipe(first())
* .subscribe(checkout => {
* console.log('Buyer:', checkout.buyer);
* console.log('Payment:', checkout.payment);
* });
* ```
*/
getCheckout({
processId,
refresh,
@@ -777,6 +1121,40 @@ export class DomainCheckoutService {
);
}
/**
* Refreshes the availability data for a single shopping cart item.
*
* Fetches fresh availability from the appropriate service based on order type
* (Abholung, Rücklage, Download, Versand, DIG-Versand, B2B-Versand) and updates
* the cart item.
*
* @remarks
* **Order Type Handling**:
* - **Abholung** (Pickup): Requires branch for availability check
* - **Rücklage** (TakeAway): Requires branch for availability check
* - **Download**: No additional parameters needed
* - **Versand** (Delivery): Standard delivery availability
* - **DIG-Versand** (Digital Delivery): Digital goods delivery
* - **B2B-Versand** (B2B Delivery): Business customer delivery
*
* **Updates**: Automatically calls `updateItemInShoppingCart()` with the new availability
* after fetching.
*
* @param params - Parameters object
* @param params.processId - Process identifier
* @param params.shoppingCartItemId - ID of the cart item to refresh
*
* @returns Promise of the refreshed availability DTO, or undefined if item not found
*
* @example
* ```typescript
* const availability = await this.checkoutService.refreshAvailability({
* processId: 123,
* shoppingCartItemId: 456
* });
* console.log('Updated availability:', availability);
* ```
*/
async refreshAvailability({
processId,
shoppingCartItemId,
@@ -881,9 +1259,41 @@ export class DomainCheckoutService {
}
/**
* Check if the availability of all items is valid
* @param param0 Process Id
* @returns true if the availability of all items is valid
* Validates if all shopping cart items have fresh availability data (OLA not expired).
*
* OLA (Order Level Agreement) defines how long availability data remains valid.
* This method polls the store at regular intervals and checks if the oldest
* availability timestamp is still within the expiration window.
*
* @remarks
* **Polling**: Uses `rxjsInterval()` to continuously check OLA status. The Observable emits
* `true` while all items are valid, `false` when any item expires.
*
* **Timestamp Tracking**: Tracks timestamps per `${itemId}_${orderType}` combination.
* Shipping types (Versand, DIG-Versand, B2B-Versand) fall back to generic 'Versand' timestamp.
*
* **Default Interval**: Polls every `olaExpiration / 10` milliseconds (default: 30 seconds for 5-minute expiration).
*
* @param params - Parameters object
* @param params.processId - Process identifier
* @param params.interval - Custom polling interval in milliseconds (optional)
*
* @returns Observable that emits `true` when OLA is valid, `false` when expired.
* Emits only on changes (`distinctUntilChanged()`).
*
* @example
* ```typescript
* this.checkoutService.validateOlaStatus({ processId: 123 })
* .subscribe(isValid => {
* if (!isValid) {
* console.warn('Availability data expired! Refresh required.');
* // Trigger availability refresh
* }
* });
* ```
*
* @see olaExpiration For expiration duration configuration
* @see checkoutIsValid For combined OLA + availability validation
*/
validateOlaStatus({
processId,
@@ -970,6 +1380,39 @@ export class DomainCheckoutService {
);
}
/**
* Validates if the checkout is ready for order completion.
*
* Combines OLA status validation with availability validation. Both must be true
* for checkout to proceed.
*
* @remarks
* **Validation Checks**:
* - OLA Status: All item availabilities are within expiration window
* - Availabilities: All items are marked as available (not out of stock)
*
* **Polling**: Uses fast polling (250ms) for OLA status to catch expiration quickly.
*
* @param params - Parameters object
* @param params.processId - Process identifier
*
* @returns Observable that emits `true` when checkout is valid, `false` otherwise.
* Emits on every change in either validation.
*
* @example
* ```typescript
* this.checkoutService.checkoutIsValid({ processId: 123 })
* .subscribe(isValid => {
* this.checkoutButton.disabled = !isValid;
* if (!isValid) {
* this.showWarning('Checkout unavailable: Availability expired');
* }
* });
* ```
*
* @see validateOlaStatus For OLA-only validation
* @see validateAvailabilities For availability-only validation
*/
checkoutIsValid({ processId }: { processId: number }): Observable<boolean> {
const olaStatus$ = this.validateOlaStatus({ processId, interval: 250 });
@@ -980,6 +1423,73 @@ export class DomainCheckoutService {
);
}
/**
* Orchestrates the complete checkout process from cart validation to order creation.
*
* This is the most complex method in the service, executing a 12-step sequence that
* validates data, updates entities, and creates orders. Each step must complete before
* the next begins.
*
* @remarks
* **Execution Sequence** (sequential, not parallel):
* 1. **Update Destination**: Sets shipping addresses on delivery destinations
* 2. **Refresh Shopping Cart**: Gets latest cart state from API
* 3. **Set Special Comments**: Applies agent comments to all cart items
* 4. **Refresh Checkout**: Recreates checkout entity from current state
* 5. **Check Availabilities**: Validates download items are available
* 6. **Update Availabilities**: Refreshes DIG-Versand and B2B-Versand prices
* 7. **Set Buyer**: Submits buyer information to checkout
* 8. **Set Notification Channels**: Configures email/SMS preferences
* 9. **Set Payer**: Submits payer information (if needed for order type)
* 10. **Set Payment Type**: Configures payment method (Rechnung/Bar)
* 11. **Set Destination**: Updates destinations with shipping addresses
* 12. **Complete Order**: Submits to OMS for order creation
*
* **Payment Type Logic**:
* - Download/Versand/DIG-Versand/B2B-Versand → Payment type 128 (Rechnung/Invoice)
* - Pickup/TakeAway only → Payment type 4 (Bar/Cash)
*
* **Payer Requirement**:
* - Required for B2B customers or Download/Delivery order types
* - Skipped for in-store pickup/takeaway only
*
* **Error Handling**:
* - HTTP 409 (Conflict): Order already exists - dispatches existing orders to store
* - Other errors propagate to consumer for handling
* - Failed availability checks throw error preventing order creation
*
* **Side Effects**:
* - Logs each step to console (for debugging)
* - Updates NgRx store at multiple points
* - Dispatches final orders to store on success
*
* @param params - Parameters object
* @param params.processId - Process identifier
*
* @returns Observable array of created orders (DisplayOrderDTO[])
*
* @throws Observable error if availability validation fails or API returns non-409 error
*
* @example
* ```typescript
* this.checkoutService.completeCheckout({ processId: 123 })
* .subscribe({
* next: (orders) => {
* console.log('Orders created:', orders);
* this.router.navigate(['/order-confirmation']);
* },
* error: (error) => {
* if (error.status === 409) {
* console.log('Order already exists');
* } else {
* console.error('Checkout failed:', error);
* }
* }
* });
* ```
*
* @see completeKulturpassOrder For Kulturpass-specific checkout flow
*/
completeCheckout({
processId,
}: {
@@ -1333,11 +1843,45 @@ export class DomainCheckoutService {
//#region Common
// Fix für Ticket #4619 Versand Artikel im Warenkob -> keine Änderung bei Kundendaten erfassen
// Auskommentiert, da dieser Aufruf oftmals mit gleichen Parametern aufgerufen wird (ohne ausgewählten Kunden nur ein leeres Objekt bei customerFeatures)
// memorize macht keinen deepCompare von Objekten und denkt hier, dass immer der gleiche Return Wert zurückkommt, allerdings ist das hier oft nicht der Fall
// und der Decorator memorized dann fälschlicherweise
// @memorize()
/**
* Validates if a customer can be set on the shopping cart based on cart contents and customer features.
*
* @remarks
* **Memoization Disabled**: The `@memorize()` decorator was intentionally disabled for this method
* due to shallow comparison issues. The decorator couldn't detect when `customerFeatures` object
* changed, causing stale cached results. See Ticket #4619.
*
* **Response Fields**:
* - `ok`: True if customer can be set without issues
* - `filter`: Customer search filters (e.g., `{ customertype: 'webshop;guest' }`)
* - `message`: Error message if validation fails
* - `create`: Options for creating new customer types (store, guest, webshop, b2b)
*
* **Use Cases**:
* - Determine which customer types are compatible with cart contents
* - Pre-filter customer search results
* - Enable/disable customer type creation buttons
*
* @param params - Parameters object
* @param params.processId - Process identifier
* @param params.customerFeatures - Customer feature flags (optional: webshop, guest, b2b, staff, etc.)
*
* @returns Observable with validation result containing ok, filter, message, and create options
*
* @example
* ```typescript
* this.checkoutService.canSetCustomer({
* processId: 123,
* customerFeatures: { webshop: 'true', guest: 'false' }
* }).subscribe(result => {
* if (result.ok) {
* console.log('Customer types allowed:', result.create.options.values);
* } else {
* console.error('Cannot set customer:', result.message);
* }
* });
* ```
*/
canSetCustomer({
processId,
customerFeatures,
@@ -1432,7 +1976,7 @@ export class DomainCheckoutService {
): Observable<{ [key: string]: boolean }> {
return this.canSetCustomer({ processId, customerFeatures: undefined }).pipe(
map((res) => {
let setableTypes: { [key: string]: boolean } = {
const setableTypes: { [key: string]: boolean } = {
store: false,
guest: false,
webshop: false,
@@ -1472,6 +2016,32 @@ export class DomainCheckoutService {
);
}
/**
* Retrieves all active, online branches that support shipping.
*
* @remarks
* **Filtering**: Returns only branches matching ALL criteria:
* - `status === 1` (Active)
* - `branchType === 1` (Standard branch type)
* - `isOnline === true` (Available online)
* - `isShippingEnabled === true` (Supports shipping)
*
* **Memoization**: Uses `@memorize()` decorator to cache results. Subsequent calls
* return cached data without API round-trip.
*
* **Pagination**: Fetches up to 999 branches (effectively all branches).
*
* @returns Observable array of filtered branch DTOs
*
* @example
* ```typescript
* this.checkoutService.getBranches()
* .subscribe(branches => {
* console.log('Available branches:', branches.length);
* this.branchDropdown.options = branches;
* });
* ```
*/
@memorize()
getBranches(): Observable<BranchDTO[]> {
return this._branchService
@@ -1552,6 +2122,21 @@ export class DomainCheckoutService {
);
}
/**
* Removes all checkout data for a process ID from the store.
*
* Cleans up shopping cart, checkout entity, buyer, payer, and all associated data.
* Call this when a checkout session is complete or cancelled.
*
* @param params - Parameters object
* @param params.processId - Process identifier to remove
*
* @example
* ```typescript
* // After successful order or when user cancels
* this.checkoutService.removeProcess({ processId: 123 });
* ```
*/
removeProcess({ processId }: { processId: number }) {
this.store.dispatch(
DomainCheckoutActions.removeCheckoutWithProcessId({ processId }),
@@ -1627,13 +2212,4 @@ export class DomainCheckoutService {
});
}
//#endregion
//#region Common
async updateProcessCount(processId: number, shoppingCart: ShoppingCartDTO) {
this.applicationService.patchProcessData(processId, {
count: shoppingCart.items?.length ?? 0,
});
}
//#endregion
}

View File

@@ -19,6 +19,11 @@ export const setShoppingCart = createAction(
props<{ processId: number; shoppingCart: ShoppingCartDTO }>(),
);
export const setShoppingCartByShoppingCartId = createAction(
`${prefix} Set Shopping Cart By Shopping Cart Id`,
props<{ shoppingCartId: number; shoppingCart: ShoppingCartDTO }>(),
);
export const setCheckout = createAction(
`${prefix} Set Checkout`,
props<{ processId: number; checkout: CheckoutDTO }>(),

View File

@@ -1,207 +1,311 @@
import { createReducer, on } from '@ngrx/store';
import { initialCheckoutState, storeCheckoutAdapter } from './domain-checkout.state';
import * as DomainCheckoutActions from './domain-checkout.actions';
import { Dictionary } from '@ngrx/entity';
import { CheckoutEntity } from './defs/checkout.entity';
import { isNullOrUndefined } from '@utils/common';
const _domainCheckoutReducer = createReducer(
initialCheckoutState,
on(DomainCheckoutActions.setShoppingCart, (s, { processId, shoppingCart }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
const addedShoppingCartItems =
shoppingCart?.items
?.filter((item) => !entity.shoppingCart?.items?.find((i) => i.id === item.id))
?.map((item) => item.data) ?? [];
entity.shoppingCart = shoppingCart;
entity.itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp ? { ...entity.itemAvailabilityTimestamp } : {};
const now = Date.now();
for (let shoppingCartItem of addedShoppingCartItems) {
if (shoppingCartItem.features?.orderType) {
entity.itemAvailabilityTimestamp[`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`] = now;
}
}
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.checkout = checkout;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setBuyerCommunicationDetails, (s, { processId, email, mobile }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
const communicationDetails = { ...entity.buyer.communicationDetails };
communicationDetails.email = email || communicationDetails.email;
communicationDetails.mobile = mobile || communicationDetails.mobile;
entity.buyer = {
...entity.buyer,
communicationDetails,
};
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setNotificationChannels, (s, { processId, notificationChannels }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
return storeCheckoutAdapter.setOne({ ...entity, notificationChannels }, s);
}),
on(DomainCheckoutActions.setCheckoutDestination, (s, { processId, destination }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.checkout = {
...entity.checkout,
destinations: entity.checkout.destinations.map((dest) => {
if (dest.id === destination.id) {
return { ...dest, ...destination };
}
return { ...dest };
}),
};
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setShippingAddress, (s, { processId, shippingAddress }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.shippingAddress = shippingAddress;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setBuyer, (s, { processId, buyer }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.buyer = buyer;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setPayer, (s, { processId, payer }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.payer = payer;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setSpecialComment, (s, { processId, agentComment }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.specialComment = agentComment;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.removeCheckoutWithProcessId, (s, { processId }) => {
return storeCheckoutAdapter.removeOne(processId, s);
}),
on(DomainCheckoutActions.setOrders, (s, { orders }) => ({ ...s, orders: [...s.orders, ...orders] })),
on(DomainCheckoutActions.updateOrderItem, (s, { item }) => {
const orders = [...s.orders];
const orderToUpdate = orders?.find((order) => order.items?.find((i) => i.id === item?.id));
const orderToUpdateIndex = orders?.indexOf(orderToUpdate);
const orderItemToUpdate = orderToUpdate?.items?.find((i) => i.id === item?.id);
const orderItemToUpdateIndex = orderToUpdate?.items?.indexOf(orderItemToUpdate);
const items = [...orderToUpdate?.items];
items[orderItemToUpdateIndex] = item;
orders[orderToUpdateIndex] = {
...orderToUpdate,
items: [...items],
};
return { ...s, orders: [...orders] };
}),
on(DomainCheckoutActions.removeAllOrders, (s) => ({
...s,
orders: [],
})),
on(DomainCheckoutActions.setOlaError, (s, { processId, olaErrorIds }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.olaErrorIds = olaErrorIds;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setCustomer, (s, { processId, customer }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.customer = customer;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory,
(s, { processId, shoppingCartItemId, availability }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
? { ...entity?.itemAvailabilityTimestamp }
: {};
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
if (!item?.features?.orderType) return s;
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
const entity = getCheckoutEntityByShoppingCartId({ shoppingCartId, entities: s.entities });
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
? { ...entity?.itemAvailabilityTimestamp }
: {};
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
if (!item?.features?.orderType) return s;
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
return storeCheckoutAdapter.setOne(entity, s);
},
),
);
export function domainCheckoutReducer(state, action) {
return _domainCheckoutReducer(state, action);
}
function getOrCreateCheckoutEntity({
entities,
processId,
}: {
entities: Dictionary<CheckoutEntity>;
processId: number;
}): CheckoutEntity {
let entity = entities[processId];
if (isNullOrUndefined(entity)) {
return {
processId,
checkout: undefined,
shoppingCart: undefined,
shippingAddress: undefined,
orders: [],
payer: undefined,
buyer: undefined,
specialComment: '',
notificationChannels: 0,
olaErrorIds: [],
customer: undefined,
// availabilityHistory: [],
itemAvailabilityTimestamp: {},
};
}
return { ...entity };
}
function getCheckoutEntityByShoppingCartId({
entities,
shoppingCartId,
}: {
entities: Dictionary<CheckoutEntity>;
shoppingCartId: number;
}): CheckoutEntity {
return Object.values(entities).find((entity) => entity.shoppingCart?.id === shoppingCartId);
}
import { createReducer, on } from '@ngrx/store';
import {
initialCheckoutState,
storeCheckoutAdapter,
} from './domain-checkout.state';
import * as DomainCheckoutActions from './domain-checkout.actions';
import { Dictionary } from '@ngrx/entity';
import { CheckoutEntity } from './defs/checkout.entity';
import { isNullOrUndefined } from '@utils/common';
const _domainCheckoutReducer = createReducer(
initialCheckoutState,
on(
DomainCheckoutActions.setShoppingCart,
(s, { processId, shoppingCart }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
const addedShoppingCartItems =
shoppingCart?.items
?.filter(
(item) =>
!entity.shoppingCart?.items?.find((i) => i.id === item.id),
)
?.map((item) => item.data) ?? [];
entity.shoppingCart = shoppingCart;
entity.itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp
? { ...entity.itemAvailabilityTimestamp }
: {};
const now = Date.now();
for (let shoppingCartItem of addedShoppingCartItems) {
if (shoppingCartItem.features?.orderType) {
entity.itemAvailabilityTimestamp[
`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`
] = now;
}
}
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(
DomainCheckoutActions.setShoppingCartByShoppingCartId,
(s, { shoppingCartId, shoppingCart }) => {
let entity = getCheckoutEntityByShoppingCartId({
shoppingCartId,
entities: s.entities,
});
if (!entity) {
// No entity found for this shoppingCartId, cannot update
return s;
}
entity = { ...entity, shoppingCart };
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.checkout = checkout;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(
DomainCheckoutActions.setBuyerCommunicationDetails,
(s, { processId, email, mobile }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
const communicationDetails = { ...entity.buyer.communicationDetails };
communicationDetails.email = email || communicationDetails.email;
communicationDetails.mobile = mobile || communicationDetails.mobile;
entity.buyer = {
...entity.buyer,
communicationDetails,
};
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(
DomainCheckoutActions.setNotificationChannels,
(s, { processId, notificationChannels }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
return storeCheckoutAdapter.setOne(
{ ...entity, notificationChannels },
s,
);
},
),
on(
DomainCheckoutActions.setCheckoutDestination,
(s, { processId, destination }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.checkout = {
...entity.checkout,
destinations: entity.checkout.destinations.map((dest) => {
if (dest.id === destination.id) {
return { ...dest, ...destination };
}
return { ...dest };
}),
};
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(
DomainCheckoutActions.setShippingAddress,
(s, { processId, shippingAddress }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.shippingAddress = shippingAddress;
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(DomainCheckoutActions.setBuyer, (s, { processId, buyer }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.buyer = buyer;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setPayer, (s, { processId, payer }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.payer = payer;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(
DomainCheckoutActions.setSpecialComment,
(s, { processId, agentComment }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.specialComment = agentComment;
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(DomainCheckoutActions.removeCheckoutWithProcessId, (s, { processId }) => {
return storeCheckoutAdapter.removeOne(processId, s);
}),
on(DomainCheckoutActions.setOrders, (s, { orders }) => ({
...s,
orders: [...s.orders, ...orders],
})),
on(DomainCheckoutActions.updateOrderItem, (s, { item }) => {
const orders = [...s.orders];
const orderToUpdate = orders?.find((order) =>
order.items?.find((i) => i.id === item?.id),
);
const orderToUpdateIndex = orders?.indexOf(orderToUpdate);
const orderItemToUpdate = orderToUpdate?.items?.find(
(i) => i.id === item?.id,
);
const orderItemToUpdateIndex =
orderToUpdate?.items?.indexOf(orderItemToUpdate);
const items = [...(orderToUpdate?.items ?? [])];
items[orderItemToUpdateIndex] = item;
orders[orderToUpdateIndex] = {
...orderToUpdate,
items: [...items],
};
return { ...s, orders: [...orders] };
}),
on(DomainCheckoutActions.removeAllOrders, (s) => ({
...s,
orders: [],
})),
on(DomainCheckoutActions.setOlaError, (s, { processId, olaErrorIds }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.olaErrorIds = olaErrorIds;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.setCustomer, (s, { processId, customer }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
entity.customer = customer;
return storeCheckoutAdapter.setOne(entity, s);
}),
on(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory,
(s, { processId, shoppingCartItemId, availability }) => {
const entity = getOrCreateCheckoutEntity({
processId,
entities: s.entities,
});
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
? { ...entity?.itemAvailabilityTimestamp }
: {};
const item = entity?.shoppingCart?.items?.find(
(i) => i.id === shoppingCartItemId,
)?.data;
if (!item?.features?.orderType) return s;
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] =
Date.now();
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
return storeCheckoutAdapter.setOne(entity, s);
},
),
on(
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
const entity = getCheckoutEntityByShoppingCartId({
shoppingCartId,
entities: s.entities,
});
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
? { ...entity?.itemAvailabilityTimestamp }
: {};
const item = entity?.shoppingCart?.items?.find(
(i) => i.id === shoppingCartItemId,
)?.data;
if (!item?.features?.orderType) return s;
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] =
Date.now();
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
return storeCheckoutAdapter.setOne(entity, s);
},
),
);
export function domainCheckoutReducer(state, action) {
return _domainCheckoutReducer(state, action);
}
function getOrCreateCheckoutEntity({
entities,
processId,
}: {
entities: Dictionary<CheckoutEntity>;
processId: number;
}): CheckoutEntity {
let entity = entities[processId];
if (isNullOrUndefined(entity)) {
return {
processId,
checkout: undefined,
shoppingCart: undefined,
shippingAddress: undefined,
orders: [],
payer: undefined,
buyer: undefined,
specialComment: '',
notificationChannels: 0,
olaErrorIds: [],
customer: undefined,
// availabilityHistory: [],
itemAvailabilityTimestamp: {},
};
}
return { ...entity };
}
function getCheckoutEntityByShoppingCartId({
entities,
shoppingCartId,
}: {
entities: Dictionary<CheckoutEntity>;
shoppingCartId: number;
}): CheckoutEntity {
return Object.values(entities).find(
(entity) => entity.shoppingCart?.id === shoppingCartId,
);
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -20,9 +20,15 @@ export class PrintShippingNoteActionHandler extends ActionHandler<OrderItemsCont
async printShippingNoteHelper(printer: string, receipts: ReceiptDTO[]) {
try {
for (const group of groupBy(receipts, (receipt) => receipt?.buyer?.buyerNumber)) {
for (const group of groupBy(
receipts,
(receipt) => receipt?.buyer?.buyerNumber,
)) {
await this.domainPrinterService
.printShippingNote({ printer, receipts: group?.items?.map((r) => r?.id) })
.printShippingNote({
printer,
receipts: group?.items?.map((r) => r?.id),
})
.toPromise();
}
return {
@@ -38,7 +44,9 @@ export class PrintShippingNoteActionHandler extends ActionHandler<OrderItemsCont
}
async handler(data: OrderItemsContext): Promise<OrderItemsContext> {
const printerList = await this.domainPrinterService.getAvailableLabelPrinters().toPromise();
const printerList = await this.domainPrinterService
.getAvailableLabelPrinters()
.toPromise();
const receipts = data?.receipts?.filter((r) => r?.receiptType & 1);
let printer: Printer;
@@ -53,7 +61,8 @@ export class PrintShippingNoteActionHandler extends ActionHandler<OrderItemsCont
data: {
printImmediately: !this._environmentSerivce.matchTablet(),
printerType: 'Label',
print: async (printer) => await this.printShippingNoteHelper(printer, receipts),
print: async (printer) =>
await this.printShippingNoteHelper(printer, receipts),
} as PrintModalData,
})
.afterClosed$.toPromise();

View File

@@ -5,7 +5,7 @@ import {
VATValueDTO,
} from '@generated/swagger/checkout-api';
import { PurchaseOption } from './store';
import { OrderType } from '@isa/checkout/data-access';
import { OrderTypeFeature } from '@isa/checkout/data-access';
export const PURCHASE_OPTIONS: PurchaseOption[] = [
'in-store',
@@ -23,7 +23,7 @@ export const DELIVERY_PURCHASE_OPTIONS: PurchaseOption[] = [
];
export const PURCHASE_OPTION_TO_ORDER_TYPE: {
[purchaseOption: string]: OrderType;
[purchaseOption: string]: OrderTypeFeature;
} = {
'in-store': 'Rücklage',
'pickup': 'Abholung',

View File

@@ -242,6 +242,14 @@
}
}
@if (showNoDownloadAvailability$ | async) {
<span
class="inline-block font-bold text-[#BE8100] mt-[14px] max-w-[19rem]"
>
Derzeit nicht verfügbar
</span>
}
@if (showMaxAvailableQuantity$ | async) {
<span class="font-bold text-[#BE8100] mt-[14px]">
{{ (availability$ | async)?.inStock }} Exemplare sofort lieferbar

View File

@@ -36,6 +36,7 @@ import { GIFT_CARD_MAX_PRICE, PRICE_PATTERN } from '../constants';
import {
Item,
PurchaseOptionsStore,
isDownload,
isItemDTO,
isShoppingCartItemDTO,
} from '../store';
@@ -222,13 +223,23 @@ export class PurchaseOptionsListItemComponent
}),
);
fetchingAvailabilities$ = this.item$
.pipe(
switchMap((item) =>
this._store.getFetchingAvailabilitiesForItem$(item.id),
),
)
.pipe(map((fetchingAvailabilities) => fetchingAvailabilities.length > 0));
fetchingAvailabilitiesArray$ = this.item$.pipe(
switchMap((item) => this._store.getFetchingAvailabilitiesForItem$(item.id)),
);
fetchingAvailabilities$ = this.fetchingAvailabilitiesArray$.pipe(
map((fetchingAvailabilities) => fetchingAvailabilities.length > 0),
);
fetchingInStoreAvailability$ = this.fetchingAvailabilitiesArray$.pipe(
map((fetchingAvailabilities) =>
fetchingAvailabilities.some((fa) => fa.purchaseOption === 'in-store'),
),
);
isFetchingInStore = toSignal(this.fetchingInStoreAvailability$, {
initialValue: false,
});
showNotAvailable$ = combineLatest([
this.availabilities$,
@@ -247,6 +258,35 @@ export class PurchaseOptionsListItemComponent
}),
);
isDownload$ = this.item$.pipe(map((item) => isDownload(item)));
isDownloadItem = toSignal(this.isDownload$, { initialValue: false });
showNoDownloadAvailability$ = combineLatest([
this.isDownload$,
this.availabilities$,
this.fetchingAvailabilities$,
]).pipe(
map(([isDownloadItem, availabilities, fetchingAvailabilities]) => {
// Only check for download items
if (!isDownloadItem) {
return false;
}
// Don't show error while loading
if (fetchingAvailabilities) {
return false;
}
// Check if download availability exists
const hasDownloadAvailability = availabilities.some(
(a) => a.purchaseOption === 'download',
);
return !hasDownloadAvailability;
}),
);
// Ticket #4813 Artikeldetailseite // EVT-Datum bei der Kaufoption Rücklage anzeigen
get isEVT() {
// Einstieg über Kaufoptionen - Hier wird die Katalogavailability verwendet die am ItemDTO hängt
@@ -279,10 +319,16 @@ export class PurchaseOptionsListItemComponent
});
showLowStockMessage = computed(() => {
const availability = this.availability();
const inStock = availability?.inStock ?? 0;
return (
this.useRedemptionPoints() &&
this.isReservePurchaseOption() &&
(!this.availability() || this.availability().inStock < 2)
!this.isDownloadItem() &&
!this.isFetchingInStore() &&
inStock > 0 &&
inStock < 2
);
});

View File

@@ -20,6 +20,7 @@ import {
PickupPurchaseOptionTileComponent,
} from './purchase-options-tile';
import {
isDownload,
isGiftCard,
Item,
PurchaseOption,
@@ -102,19 +103,16 @@ export class PurchaseOptionsModalComponent implements OnInit, OnDestroy {
purchasingOptions$ = this.store.getPurchaseOptionsInAvailabilities$;
isDownloadOnly$ = this.purchasingOptions$.pipe(
map(
(purchasingOptions) =>
purchasingOptions.length === 1 && purchasingOptions[0] === 'download',
),
isDownloadOnly$ = this.store.items$.pipe(
map((items) => items.length > 0 && items.every((item) => isDownload(item))),
);
isGiftCardOnly$ = this.store.items$.pipe(
map((items) => items.every((item) => isGiftCard(item, this.store.type))),
);
hasDownload$ = this.purchasingOptions$.pipe(
map((purchasingOptions) => purchasingOptions.includes('download')),
hasDownload$ = this.store.items$.pipe(
map((items) => items.some((item) => isDownload(item))),
);
canContinue$ = this.store.canContinue$;

View File

@@ -1,4 +1,4 @@
import { Injectable, inject } from '@angular/core';
import { Injectable, inject, untracked } from '@angular/core';
import { UiModalRef, UiModalService } from '@ui/modal';
import { PurchaseOptionsModalComponent } from './purchase-options-modal.component';
import {
@@ -10,6 +10,8 @@ import {
Customer,
CrmTabMetadataService,
} from '@isa/crm/data-access';
import { TabService } from '@isa/core/tabs';
import { BranchDTO } from '@generated/swagger/checkout-api';
/**
* Service for opening and managing the Purchase Options Modal.
@@ -35,6 +37,7 @@ import {
@Injectable({ providedIn: 'root' })
export class PurchaseOptionsModalService {
#uiModal = inject(UiModalService);
#tabService = inject(TabService);
#crmTabMetadataService = inject(CrmTabMetadataService);
#customerFacade = inject(CustomerFacade);
@@ -71,6 +74,7 @@ export class PurchaseOptionsModalService {
};
context.selectedCustomer = await this.#getSelectedCustomer(data);
context.selectedBranch = this.#getSelectedBranch(data.tabId);
return this.#uiModal.open<string, PurchaseOptionsModalContext>({
content: PurchaseOptionsModalComponent,
data: context,
@@ -90,4 +94,25 @@ export class PurchaseOptionsModalService {
return this.#customerFacade.fetchCustomer({ customerId });
}
#getSelectedBranch(tabId: number): BranchDTO | undefined {
const tab = untracked(() =>
this.#tabService.entities().find((t) => t.id === tabId),
);
if (!tab) {
return undefined;
}
const legacyProcessData = tab?.metadata?.process_data;
if (
typeof legacyProcessData === 'object' &&
'selectedBranch' in legacyProcessData
) {
return legacyProcessData.selectedBranch as BranchDTO;
}
return undefined;
}
}

View File

@@ -13,7 +13,7 @@ import {
ItemPayloadWithSourceId,
PurchaseOption,
} from './purchase-options.types';
import { OrderType } from '@isa/checkout/data-access';
import { OrderTypeFeature } from '@isa/checkout/data-access';
export function isItemDTO(item: any, type: ActionType): item is ItemDTO {
return type === 'add';
@@ -145,7 +145,7 @@ export function mapToOlaAvailability({
export function getOrderTypeForPurchaseOption(
purchaseOption: PurchaseOption,
): OrderType | undefined {
): OrderTypeFeature | undefined {
switch (purchaseOption) {
case 'delivery':
case 'dig-delivery':
@@ -163,7 +163,7 @@ export function getOrderTypeForPurchaseOption(
}
export function getPurchaseOptionForOrderType(
orderType: OrderType,
orderType: OrderTypeFeature,
): PurchaseOption | undefined {
switch (orderType) {
case 'Versand':

View File

@@ -17,7 +17,10 @@ import { memorize } from '@utils/common';
import { AuthService } from '@core/auth';
import { ApplicationService } from '@core/application';
import { DomainOmsService } from '@domain/oms';
import { OrderType, PurchaseOptionsFacade } from '@isa/checkout/data-access';
import {
OrderTypeFeature,
PurchaseOptionsFacade,
} from '@isa/checkout/data-access';
@Injectable({ providedIn: 'root' })
export class PurchaseOptionsService {
@@ -28,19 +31,12 @@ export class PurchaseOptionsService {
private _checkoutService: DomainCheckoutService,
private _omsService: DomainOmsService,
private _auth: AuthService,
private _app: ApplicationService,
) {}
getVats$() {
return this._omsService.getVATs();
}
getSelectedBranchForProcess(processId: number): Observable<Branch> {
return this._app
.getSelectedBranch$(processId)
.pipe(take(1), shareReplay(1));
}
getCustomerFeatures(processId: number): Observable<Record<string, string>> {
return this._checkoutService
.getCustomerFeatures({ processId })
@@ -122,7 +118,7 @@ export class PurchaseOptionsService {
fetchCanAdd(
shoppingCartId: number,
orderType: OrderType,
orderType: OrderTypeFeature,
payload: ItemPayload[],
customerFeatures: Record<string, string>,
): Promise<ItemsResult[]> {
@@ -185,10 +181,11 @@ export class PurchaseOptionsService {
items,
});
console.log('added item to cart', { shoppingCart });
this._checkoutService.updateProcessCount(
this._app.activatedProcessId,
shoppingCart,
);
// FIX BUILD ERRORS
// this._checkoutService.updateProcessCount(
// this._app.activatedProcessId,
// shoppingCart,
// );
return shoppingCart;
}
@@ -203,10 +200,11 @@ export class PurchaseOptionsService {
values: payload,
});
console.log('updated item in cart', { shoppingCart });
this._checkoutService.updateProcessCount(
this._app.activatedProcessId,
shoppingCart,
);
// FIX BUILD ERRORS
// this._checkoutService.updateProcessCount(
// this._app.activatedProcessId,
// shoppingCart,
// );
}
@memorize({ comparer: (_) => true })

View File

@@ -40,7 +40,12 @@ import { uniqueId } from 'lodash';
import { VATDTO } from '@generated/swagger/oms-api';
import { DomainCatalogService } from '@domain/catalog';
import { ItemDTO } from '@generated/swagger/cat-search-api';
import { Loyalty, OrderType, Promotion } from '@isa/checkout/data-access';
import {
Loyalty,
OrderTypeFeature,
Promotion,
} from '@isa/checkout/data-access';
import { ensureCurrencyDefaults } from '@isa/common/data-access';
@Injectable()
export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
@@ -314,11 +319,17 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
const itemData = mapToItemData(item, this.type);
if ((purchaseOption === 'in-store' || purchaseOption === undefined) && !this.isOptionDisabled('in-store')) {
if (
(purchaseOption === 'in-store' || purchaseOption === undefined) &&
!this.isOptionDisabled('in-store')
) {
promises.push(this._loadInStoreAvailability(itemData));
}
if ((purchaseOption === 'pickup' || purchaseOption === undefined) && !this.isOptionDisabled('pickup')) {
if (
(purchaseOption === 'pickup' || purchaseOption === undefined) &&
!this.isOptionDisabled('pickup')
) {
promises.push(this._loadPickupAvailability(itemData));
}
@@ -717,7 +728,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
try {
const res = await this._service.fetchCanAdd(
this.shoppingCartId,
key as OrderType,
key as OrderTypeFeature,
itemPayloads,
this.customerFeatures,
);
@@ -726,7 +737,9 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
this._addCanAddResult({
canAdd: canAdd.status === 0,
itemId: item.sourceId,
purchaseOption: getPurchaseOptionForOrderType(key as OrderType),
purchaseOption: getPurchaseOptionForOrderType(
key as OrderTypeFeature,
),
message: canAdd.message,
});
});
@@ -1056,7 +1069,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
throw new Error('Invalid item');
}
const price = this.getPriceForPurchaseOption(itemId, this.purchaseOption);
let price = this.getPriceForPurchaseOption(itemId, this.purchaseOption);
const availability = this.getAvailabilityWithPurchaseOption(
itemId,
purchaseOption,
@@ -1074,7 +1087,15 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
// Set loyalty points from item
loyalty = { value: redemptionPoints };
// Set price to 0
price.value.value = 0;
price = ensureCurrencyDefaults({
...price,
value: {
...price.value,
value: 0,
currency: 'EUR',
currencySymbol: '€',
},
});
}
let destination: EntityDTOContainerOfDestinationDTO;
@@ -1112,7 +1133,7 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
if (!isShoppingCartItemDTO(item, this.type)) {
throw new Error('Invalid item');
}
const price = this.getPriceForPurchaseOption(itemId, this.purchaseOption);
let price = this.getPriceForPurchaseOption(itemId, this.purchaseOption);
const availability = this.getAvailabilityWithPurchaseOption(
itemId,
purchaseOption,
@@ -1121,7 +1142,15 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
// If loyalty points is set we know it is a redemption item
// we need to make sure we don't update the price
if (this.useRedemptionPoints) {
price.value.value = 0;
price = ensureCurrencyDefaults({
...price,
value: {
...price.value,
value: 0,
currency: 'EUR',
currencySymbol: '€',
},
});
}
let destination: EntityDTOContainerOfDestinationDTO;

View File

@@ -55,8 +55,6 @@ import {
RewardSelectionPopUpService,
} from '@isa/checkout/shared/reward-selection-dialog';
import { injectConfirmationDialog, injectFeedbackDialog } from '@isa/ui/dialog';
@Component({
selector: 'page-article-details',
templateUrl: 'article-details.component.html',
@@ -210,7 +208,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
).path;
}
showMore: boolean = false;
showMore = false;
@ViewChild('detailsContainer', { read: ElementRef, static: false })
detailsContainer: ElementRef;
@@ -610,7 +608,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
async navigateToResultList() {
const processId = this.applicationService.activatedProcessId;
let crumbs = await this.breadcrumb
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, [
'catalog',
'details',

View File

@@ -1,53 +1,47 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ArticleDetailsComponent } from './article-details.component';
import { ProductImageModule } from '@cdn/product-image';
import { UiIconModule } from '@ui/icon';
import { RouterModule } from '@angular/router';
import { UiStarsModule } from '@ui/stars';
import { UiSliderModule } from '@ui/slider';
import { ArticleRecommendationsComponent } from './recommendations/article-recommendations.component';
import { PipesModule } from '../shared/pipes/pipes.module';
import { UiTooltipModule } from '@ui/tooltip';
import { UiCommonModule } from '@ui/common';
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
import { IconModule } from '@shared/components/icon';
import { ArticleDetailsTextComponent } from './article-details-text/article-details-text.component';
import { IconBadgeComponent } from 'apps/isa-app/src/shared/components/icon/badge/icon-badge.component';
import { MatomoModule } from 'ngx-matomo-client';
import {
SelectedRewardShoppingCartResource,
SelectedShoppingCartResource,
} from '@isa/checkout/data-access';
import {
RewardSelectionService,
RewardSelectionPopUpService,
} from '@isa/checkout/shared/reward-selection-dialog';
@NgModule({
imports: [
CommonModule,
ProductImageModule,
UiIconModule,
RouterModule,
UiStarsModule,
UiSliderModule,
UiCommonModule,
UiTooltipModule,
IconModule,
PipesModule,
OrderDeadlinePipeModule,
ArticleDetailsTextComponent,
IconBadgeComponent,
MatomoModule,
],
exports: [ArticleDetailsComponent, ArticleRecommendationsComponent],
declarations: [ArticleDetailsComponent, ArticleRecommendationsComponent],
providers: [
SelectedShoppingCartResource,
SelectedRewardShoppingCartResource,
RewardSelectionService,
RewardSelectionPopUpService,
],
})
export class ArticleDetailsModule {}
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ArticleDetailsComponent } from './article-details.component';
import { ProductImageModule } from '@cdn/product-image';
import { UiIconModule } from '@ui/icon';
import { RouterModule } from '@angular/router';
import { UiStarsModule } from '@ui/stars';
import { UiSliderModule } from '@ui/slider';
import { ArticleRecommendationsComponent } from './recommendations/article-recommendations.component';
import { PipesModule } from '../shared/pipes/pipes.module';
import { UiTooltipModule } from '@ui/tooltip';
import { UiCommonModule } from '@ui/common';
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
import { IconModule } from '@shared/components/icon';
import { ArticleDetailsTextComponent } from './article-details-text/article-details-text.component';
import { IconBadgeComponent } from 'apps/isa-app/src/shared/components/icon/badge/icon-badge.component';
import { MatomoModule } from 'ngx-matomo-client';
import {
RewardSelectionService,
RewardSelectionPopUpService,
} from '@isa/checkout/shared/reward-selection-dialog';
@NgModule({
imports: [
CommonModule,
ProductImageModule,
UiIconModule,
RouterModule,
UiStarsModule,
UiSliderModule,
UiCommonModule,
UiTooltipModule,
IconModule,
PipesModule,
OrderDeadlinePipeModule,
ArticleDetailsTextComponent,
IconBadgeComponent,
MatomoModule,
],
exports: [ArticleDetailsComponent, ArticleRecommendationsComponent],
declarations: [ArticleDetailsComponent, ArticleRecommendationsComponent],
providers: [
RewardSelectionService,
RewardSelectionPopUpService,
],
})
export class ArticleDetailsModule {}

View File

@@ -638,6 +638,8 @@ export class CheckoutReviewComponent
this.#checkoutService.reloadShoppingCart({
processId: this.applicationService.activatedProcessId,
});
this.refreshAvailabilities();
}
async changeAddress() {

View File

@@ -1,32 +1,31 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CheckoutSummaryComponent } from './checkout-summary.component';
import { PageCheckoutPipeModule } from '../pipes/page-checkout-pipe.module';
import { ProductImageModule } from '@cdn/product-image';
import { RouterModule } from '@angular/router';
import { UiCommonModule } from '@ui/common';
import { UiSpinnerModule } from '@ui/spinner';
import { UiDatepickerModule } from '@ui/datepicker';
import { IconModule } from '@shared/components/icon';
import { AuthModule } from '@core/auth';
import { SelectedRewardShoppingCartResource } from '@isa/checkout/data-access';
@NgModule({
imports: [
CommonModule,
RouterModule,
PageCheckoutPipeModule,
ProductImageModule,
IconModule,
UiCommonModule,
UiSpinnerModule,
UiDatepickerModule,
AuthModule,
UiSpinnerModule,
],
exports: [CheckoutSummaryComponent],
declarations: [CheckoutSummaryComponent],
providers: [SelectedRewardShoppingCartResource],
})
export class CheckoutSummaryModule {}
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CheckoutSummaryComponent } from './checkout-summary.component';
import { PageCheckoutPipeModule } from '../pipes/page-checkout-pipe.module';
import { ProductImageModule } from '@cdn/product-image';
import { RouterModule } from '@angular/router';
import { UiCommonModule } from '@ui/common';
import { UiSpinnerModule } from '@ui/spinner';
import { UiDatepickerModule } from '@ui/datepicker';
import { IconModule } from '@shared/components/icon';
import { AuthModule } from '@core/auth';
@NgModule({
imports: [
CommonModule,
RouterModule,
PageCheckoutPipeModule,
ProductImageModule,
IconModule,
UiCommonModule,
UiSpinnerModule,
UiDatepickerModule,
AuthModule,
UiSpinnerModule,
],
exports: [CheckoutSummaryComponent],
declarations: [CheckoutSummaryComponent],
providers: [],
})
export class CheckoutSummaryModule {}

View File

@@ -1,9 +1,14 @@
<ng-container *ngIf="orderItem$ | async; let orderItem">
<div class="grid grid-flow-row gap-px-2">
<div class="bg-[#F5F7FA] flex flex-row justify-between items-center p-4 rounded-t">
<div class="grid grid-flow-col gap-[0.4375rem] items-center" *ngIf="features$ | async; let features; else: featureLoading">
<shared-icon *ngIf="features?.length > 0" [size]="24" icon="person"></shared-icon>
<div class="grid grid-flow-col gap-2 items-center font-bold text-p2" *ngFor="let feature of features">
<div
class="bg-[#F5F7FA] flex flex-row justify-between items-center p-4 rounded-t"
>
<div
class="grid grid-flow-col gap-[0.4375rem] items-center"
*ngIf="customerFeature$ | async; let feature; else: featureLoading"
>
<shared-icon *ngIf="!!feature" [size]="24" icon="person"></shared-icon>
<div class="grid grid-flow-col gap-2 items-center font-bold text-p2">
{{ feature?.description }}
</div>
</div>
@@ -18,29 +23,54 @@
</button>
</div>
<div class="page-customer-order-details-header__details bg-white px-4 pt-4 pb-5">
<div
class="page-customer-order-details-header__details bg-white px-4 pt-4 pb-5"
>
<h2
class="page-customer-order-details-header__details-header items-center"
[class.mb-8]="!orderItem?.features?.paid && !isKulturpass"
>
<div class="text-h2">
{{ orderItem?.organisation }}
<ng-container *ngIf="!!orderItem?.organisation && (!!orderItem?.firstName || !!orderItem?.lastName)">-</ng-container>
<ng-container
*ngIf="
!!orderItem?.organisation &&
(!!orderItem?.firstName || !!orderItem?.lastName)
"
>-</ng-container
>
{{ orderItem?.lastName }}
{{ orderItem?.firstName }}
</div>
<div class="page-customer-order-details-header__header-compartment text-h3">
{{ orderItem?.compartmentCode }}{{ orderItem?.compartmentInfo && '_' + orderItem?.compartmentInfo }}
<div
class="page-customer-order-details-header__header-compartment text-h3"
>
{{ orderItem?.compartmentCode
}}{{ orderItem?.compartmentInfo && '_' + orderItem?.compartmentInfo }}
</div>
</h2>
<div class="page-customer-order-details-header__paid-marker mt-[0.375rem]" *ngIf="orderItem?.features?.paid && !isKulturpass">
<div class="font-bold w-fit desktop-small:text-p2 px-3 py-[0.125rem] rounded text-white bg-[#26830C]">
<div
class="page-customer-order-details-header__paid-marker mt-[0.375rem]"
*ngIf="orderItem?.features?.paid && !isKulturpass"
>
<div
class="font-bold w-fit desktop-small:text-p2 px-3 py-[0.125rem] rounded text-white bg-[#26830C]"
>
{{ orderItem?.features?.paid }}
</div>
</div>
<div class="page-customer-order-details-header__paid-marker mt-[0.375rem] text-[#26830C]" *ngIf="isKulturpass">
<svg class="fill-current mr-2" xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22">
<div
class="page-customer-order-details-header__paid-marker mt-[0.375rem] text-[#26830C]"
*ngIf="isKulturpass"
>
<svg
class="fill-current mr-2"
xmlns="http://www.w3.org/2000/svg"
height="22"
viewBox="0 -960 960 960"
width="22"
>
<path
d="M880-740v520q0 24-18 42t-42 18H140q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h680q24 0 42 18t18 42ZM140-631h680v-109H140v109Zm0 129v282h680v-282H140Zm0 282v-520 520Z"
/>
@@ -49,25 +79,49 @@
</div>
<div class="page-customer-order-details-header__details-wrapper -mt-3">
<div class="flex flex-row page-customer-order-details-header__buyer-number" data-detail-id="Kundennummer">
<div
class="flex flex-row page-customer-order-details-header__buyer-number"
data-detail-id="Kundennummer"
>
<div class="min-w-[9rem]">Kundennummer</div>
<div class="flex flex-row font-bold">{{ orderItem?.buyerNumber }}</div>
<div class="flex flex-row font-bold">
{{ orderItem?.buyerNumber }}
</div>
</div>
<div class="flex flex-row page-customer-order-details-header__order-number" data-detail-id="VorgangId">
<div
class="flex flex-row page-customer-order-details-header__order-number"
data-detail-id="VorgangId"
>
<div class="min-w-[9rem]">Vorgang-ID</div>
<div class="flex flex-row font-bold">{{ orderItem?.orderNumber }}</div>
<div class="flex flex-row font-bold">
{{ orderItem?.orderNumber }}
</div>
</div>
<div class="flex flex-row page-customer-order-details-header__order-date" data-detail-id="Bestelldatum">
<div
class="flex flex-row page-customer-order-details-header__order-date"
data-detail-id="Bestelldatum"
>
<div class="min-w-[9rem]">Bestelldatum</div>
<div class="flex flex-row font-bold">{{ orderItem?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
<div class="flex flex-row font-bold">
{{ orderItem?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr
</div>
</div>
<div class="flex flex-row page-customer-order-details-header__processing-status justify-between" data-detail-id="Status">
<div
class="flex flex-row page-customer-order-details-header__processing-status justify-between"
data-detail-id="Status"
>
<div class="min-w-[9rem]">Status</div>
<div *ngIf="!(changeStatusLoader$ | async)" class="flex flex-row font-bold -mr-[0.125rem]">
<div
*ngIf="!(changeStatusLoader$ | async)"
class="flex flex-row font-bold -mr-[0.125rem]"
>
<shared-icon
class="mr-2 text-black flex items-center justify-center"
[size]="16"
*ngIf="orderItem.processingStatus | processingStatus: 'icon'; let icon"
*ngIf="
orderItem.processingStatus | processingStatus: 'icon';
let icon
"
[icon]="icon"
></shared-icon>
@@ -91,18 +145,36 @@
icon="arrow-drop-down"
></shared-icon>
</button>
<ui-dropdown #statusDropdown yPosition="below" xPosition="after" [xOffset]="8">
<button uiDropdownItem *ngFor="let action of statusActions$ | async" (click)="handleActionClick(action)">
<ui-dropdown
#statusDropdown
yPosition="below"
xPosition="after"
[xOffset]="8"
>
<button
uiDropdownItem
*ngFor="let action of statusActions$ | async"
(click)="handleActionClick(action)"
>
{{ action.label }}
</button>
</ui-dropdown>
</ng-container>
</div>
<ui-spinner *ngIf="changeStatusLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
<ui-spinner
*ngIf="changeStatusLoader$ | async; let loader"
class="flex flex-row font-bold loader"
[show]="loader"
></ui-spinner>
</div>
<div class="flex flex-row page-customer-order-details-header__order-source" data-detail-id="Bestellkanal">
<div
class="flex flex-row page-customer-order-details-header__order-source"
data-detail-id="Bestellkanal"
>
<div class="min-w-[9rem]">Bestellkanal</div>
<div class="flex flex-row font-bold">{{ order?.features?.orderSource }}</div>
<div class="flex flex-row font-bold">
{{ order?.features?.orderSource }}
</div>
</div>
<div
class="flex flex-row page-customer-order-details-header__change-date justify-between"
@@ -124,26 +196,39 @@
<ng-template #changeDate>
<div class="min-w-[9rem]">Geändert</div>
<div class="flex flex-row font-bold">{{ orderItem?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
<div class="flex flex-row font-bold">
{{
orderItem?.processingStatusDate | date: 'dd.MM.yy | HH:mm'
}}
Uhr
</div>
</ng-template>
</div>
<div
class="flex flex-row page-customer-order-details-header__pick-up justify-between"
data-detail-id="Wunschdatum"
*ngIf="orderItem.orderType === 1 && (orderItem.processingStatus === 16 || orderItem.processingStatus === 8192)"
*ngIf="
orderItem.orderType === 1 &&
(orderItem.processingStatus === 16 ||
orderItem.processingStatus === 8192)
"
>
<ng-container *ngTemplateOutlet="preferredPickUpDate"></ng-container>
</div>
<div class="flex flex-col page-customer-order-details-header__dig-and-notification">
<div
class="flex flex-col page-customer-order-details-header__dig-and-notification"
>
<div
*ngIf="orderItem.orderType === 1"
class="flex flex-row page-customer-order-details-header__notification"
data-detail-id="Benachrichtigung"
>
<div class="min-w-[9rem]">Benachrichtigung</div>
<div class="flex flex-row font-bold">{{ (notificationsChannel | notificationsChannel) || '-' }}</div>
<div class="flex flex-row font-bold">
{{ (notificationsChannel | notificationsChannel) || '-' }}
</div>
</div>
<div
@@ -162,38 +247,50 @@
<div *ngIf="showFeature" class="flex flex-row items-center mr-3">
<ng-container [ngSwitch]="order.features.orderType">
<ng-container *ngSwitchCase="'Versand'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<div
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
>
<shared-icon [size]="24" icon="isa-truck"></shared-icon>
</div>
<p class="font-bold text-p1">Versand</p>
</ng-container>
<ng-container *ngSwitchCase="'DIG-Versand'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<div
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
>
<shared-icon [size]="24" icon="isa-truck"></shared-icon>
</div>
<p class="font-bold text-p1">Versand</p>
</ng-container>
<ng-container *ngSwitchCase="'B2B-Versand'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<div
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
>
<shared-icon [size]="24" icon="isa-b2b-truck"></shared-icon>
</div>
<p class="font-bold text-p1">B2B-Versand</p>
</ng-container>
<ng-container *ngSwitchCase="'Abholung'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<div
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
>
<shared-icon [size]="24" icon="isa-box-out"></shared-icon>
</div>
<p class="font-bold text-p1 mr-3">Abholung</p>
{{ orderItem.targetBranch }}
</ng-container>
<ng-container *ngSwitchCase="'Rücklage'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<div
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
>
<shared-icon [size]="24" icon="isa-shopping-bag"></shared-icon>
</div>
<p class="font-bold text-p1">Rücklage</p>
</ng-container>
<ng-container *ngSwitchCase="'Download'">
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
<div
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
>
<shared-icon [size]="24" icon="isa-download"></shared-icon>
</div>
<p class="font-bold text-p1">Download</p>
@@ -201,50 +298,93 @@
</ng-container>
</div>
<div class="page-customer-order-details-header__additional-addresses" *ngIf="showAddresses">
<div
class="page-customer-order-details-header__additional-addresses"
*ngIf="showAddresses"
>
<button (click)="openAddresses = !openAddresses" class="text-[#0556B4]">
Lieferadresse / Rechnungsadresse {{ openAddresses ? 'ausblenden' : 'anzeigen' }}
Lieferadresse / Rechnungsadresse
{{ openAddresses ? 'ausblenden' : 'anzeigen' }}
</button>
<div class="page-customer-order-details-header__addresses-popover" *ngIf="openAddresses">
<div
class="page-customer-order-details-header__addresses-popover"
*ngIf="openAddresses"
>
<button (click)="openAddresses = !openAddresses" class="close">
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
<div class="page-customer-order-details-header__addresses-popover-data">
<div *ngIf="order.shipping" class="page-customer-order-details-header__addresses-popover-delivery">
<div
class="page-customer-order-details-header__addresses-popover-data"
>
<div
*ngIf="order.shipping"
class="page-customer-order-details-header__addresses-popover-delivery"
>
<p>Lieferadresse</p>
<div class="page-customer-order-details-header__addresses-popover-delivery-data">
<div
class="page-customer-order-details-header__addresses-popover-delivery-data"
>
<ng-container *ngIf="order.shipping?.data?.organisation">
<p>{{ order.shipping?.data?.organisation?.name }}</p>
<p>{{ order.shipping?.data?.organisation?.department }}</p>
</ng-container>
<p>{{ order.shipping?.data?.firstName }} {{ order.shipping?.data?.lastName }}</p>
<p>
{{ order.shipping?.data?.firstName }}
{{ order.shipping?.data?.lastName }}
</p>
<p>{{ order.shipping?.data?.address?.info }}</p>
<p>{{ order.shipping?.data?.address?.street }} {{ order.shipping?.data?.address?.streetNumber }}</p>
<p>{{ order.shipping?.data?.address?.zipCode }} {{ order.shipping?.data?.address?.city }}</p>
<p>
{{ order.shipping?.data?.address?.street }}
{{ order.shipping?.data?.address?.streetNumber }}
</p>
<p>
{{ order.shipping?.data?.address?.zipCode }}
{{ order.shipping?.data?.address?.city }}
</p>
</div>
</div>
<div *ngIf="order.billing" class="page-customer-order-details-header__addresses-popover-billing">
<div
*ngIf="order.billing"
class="page-customer-order-details-header__addresses-popover-billing"
>
<p>Rechnungsadresse</p>
<div class="page-customer-order-details-header__addresses-popover-billing-data">
<div
class="page-customer-order-details-header__addresses-popover-billing-data"
>
<ng-container *ngIf="order.billing?.data?.organisation">
<p>{{ order.billing?.data?.organisation?.name }}</p>
<p>{{ order.billing?.data?.organisation?.department }}</p>
</ng-container>
<p>{{ order.billing?.data?.firstName }} {{ order.billing?.data?.lastName }}</p>
<p>
{{ order.billing?.data?.firstName }}
{{ order.billing?.data?.lastName }}
</p>
<p>{{ order.billing?.data?.address?.info }}</p>
<p>{{ order.billing?.data?.address?.street }} {{ order.billing?.data?.address?.streetNumber }}</p>
<p>{{ order.billing?.data?.address?.zipCode }} {{ order.billing?.data?.address?.city }}</p>
<p>
{{ order.billing?.data?.address?.street }}
{{ order.billing?.data?.address?.streetNumber }}
</p>
<p>
{{ order.billing?.data?.address?.zipCode }}
{{ order.billing?.data?.address?.city }}
</p>
</div>
</div>
</div>
</div>
</div>
<div class="page-customer-order-details-header__select grow" *ngIf="showMultiselect$ | async">
<button class="cta-select-all" (click)="selectAll()">Alle auswählen</button>
{{ selectedOrderItemCount$ | async }} von {{ orderItemCount$ | async }} Titeln
<div
class="page-customer-order-details-header__select grow"
*ngIf="showMultiselect$ | async"
>
<button class="cta-select-all" (click)="selectAll()">
Alle auswählen
</button>
{{ selectedOrderItemCount$ | async }} von
{{ orderItemCount$ | async }} Titeln
</div>
</div>
</div>
@@ -263,13 +403,20 @@
<button
[uiOverlayTrigger]="deadlineDatepicker"
#deadlineDatepickerTrigger="uiOverlayTrigger"
[disabled]="!isKulturpass && (!!orderItem?.features?.paid || (changeDateDisabled$ | async))"
[disabled]="
!isKulturpass &&
(!!orderItem?.features?.paid || (changeDateDisabled$ | async))
"
class="cta-pickup-deadline"
>
<strong class="border-r border-[#AEB7C1] pr-4">
{{ orderItem?.pickUpDeadline | date: 'dd.MM.yy' }}
</strong>
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
<shared-icon
class="text-[#596470] ml-4"
[size]="24"
icon="isa-calendar"
></shared-icon>
</button>
<ui-datepicker
#deadlineDatepicker
@@ -282,25 +429,46 @@
(save)="updatePickupDeadline($event)"
></ui-datepicker>
</div>
<ui-spinner *ngIf="changeDateLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
<ui-spinner
*ngIf="changeDateLoader$ | async; let loader"
class="flex flex-row font-bold loader"
[show]="loader"
></ui-spinner>
</ng-template>
<ng-template #preferredPickUpDate>
<div class="min-w-[9rem]">Zurücklegen bis</div>
<div *ngIf="!(changePreferredDateLoader$ | async)" class="flex flex-row font-bold">
<div
*ngIf="!(changePreferredDateLoader$ | async)"
class="flex flex-row font-bold"
>
<button
[uiOverlayTrigger]="preferredPickUpDatePicker"
#preferredPickUpDatePickerTrigger="uiOverlayTrigger"
[disabled]="(!isKulturpass && !!orderItem?.features?.paid) || (changeDateDisabled$ | async)"
[disabled]="
(!isKulturpass && !!orderItem?.features?.paid) ||
(changeDateDisabled$ | async)
"
class="cta-pickup-preferred"
>
<strong class="border-r border-[#AEB7C1] pr-4" *ngIf="preferredPickUpDate$ | async; let pickUpDate; else: selectTemplate">
<strong
class="border-r border-[#AEB7C1] pr-4"
*ngIf="
preferredPickUpDate$ | async;
let pickUpDate;
else: selectTemplate
"
>
{{ pickUpDate | date: 'dd.MM.yy' }}
</strong>
<ng-template #selectTemplate>
<strong class="border-r border-[#AEB7C1] pr-4">Auswählen</strong>
</ng-template>
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
<shared-icon
class="text-[#596470] ml-4"
[size]="24"
icon="isa-calendar"
></shared-icon>
</button>
<ui-datepicker
#preferredPickUpDatePicker
@@ -313,7 +481,11 @@
(save)="updatePreferredPickUpDate($event)"
></ui-datepicker>
</div>
<ui-spinner *ngIf="changePreferredDateLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
<ui-spinner
*ngIf="changePreferredDateLoader$ | async; let loader"
class="flex flex-row font-bold loader"
[show]="loader"
></ui-spinner>
</ng-template>
<ng-template #vslLieferdatum>
@@ -328,7 +500,11 @@
<span class="border-r border-[#AEB7C1] pr-4">
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
</span>
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
<shared-icon
class="text-[#596470] ml-4"
[size]="24"
icon="isa-calendar"
></shared-icon>
</button>
<ui-datepicker
#uiDatepicker
@@ -341,6 +517,10 @@
(save)="updateEstimatedShippingDate($event)"
></ui-datepicker>
</div>
<ui-spinner *ngIf="changeDateLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
<ui-spinner
*ngIf="changeDateLoader$ | async; let loader"
class="flex flex-row font-bold loader"
[show]="loader"
></ui-spinner>
</ng-template>
</ng-container>

View File

@@ -11,12 +11,17 @@ import {
import { CrmCustomerService } from '@domain/crm';
import { DomainOmsService } from '@domain/oms';
import { NotificationChannel } from '@generated/swagger/checkout-api';
import { KeyValueDTOOfStringAndString, OrderDTO, OrderItemListItemDTO } from '@generated/swagger/oms-api';
import {
KeyValueDTOOfStringAndString,
OrderDTO,
OrderItemListItemDTO,
} from '@generated/swagger/oms-api';
import { DateAdapter } from '@ui/common';
import { cloneDeep } from 'lodash';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
import { CustomerOrderDetailsStore } from '../customer-order-details.store';
import { getEnabledCustomerFeature } from '@isa/crm/data-access';
@Component({
selector: 'page-customer-order-details-header',
@@ -39,14 +44,21 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
return this.order?.features?.orderSource === 'KulturPass';
}
minDateDatepicker = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), -1);
minDateDatepicker = this.dateAdapter.addCalendarDays(
this.dateAdapter.today(),
-1,
);
today = this.dateAdapter.today();
selectedOrderItemCount$ = this._store.selectedeOrderItemSubsetIds$.pipe(map((ids) => ids?.length ?? 0));
selectedOrderItemCount$ = this._store.selectedeOrderItemSubsetIds$.pipe(
map((ids) => ids?.length ?? 0),
);
orderItemCount$ = this._store.items$.pipe(map((items) => items?.length ?? 0));
orderItem$ = this._store.items$.pipe(map((orderItems) => orderItems?.find((_) => true)));
orderItem$ = this._store.items$.pipe(
map((orderItems) => orderItems?.find((_) => true)),
);
preferredPickUpDate$ = new BehaviorSubject<Date>(undefined);
@@ -58,37 +70,57 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
changeStatusDisabled$ = this._store.changeActionDisabled$;
changeDateDisabled$ = this.changeStatusDisabled$;
features$ = this.orderItem$.pipe(
customerFeature$ = this.orderItem$.pipe(
filter((orderItem) => !!orderItem),
switchMap((orderItem) =>
this.customerService.getCustomers(orderItem.buyerNumber).pipe(
map((res) => res.result.find((c) => c.customerNumber === orderItem.buyerNumber)),
map((customer) => customer?.features || []),
map((features) => features.filter((f) => f.enabled && !!f.description)),
map((res) =>
res.result.find((c) => c.customerNumber === orderItem.buyerNumber),
),
map((customer) => getEnabledCustomerFeature(customer?.features)),
),
),
shareReplay(),
);
statusActions$ = this.orderItem$.pipe(
map((orderItem) => orderItem?.actions?.filter((action) => action.enabled === false)),
map((orderItem) =>
orderItem?.actions?.filter((action) => action.enabled === false),
),
);
showMultiselect$ = combineLatest([this._store.items$, this._store.fetchPartial$, this._store.itemsSelectable$]).pipe(
map(([orderItems, fetchPartial, multiSelect]) => multiSelect && fetchPartial && orderItems?.length > 1),
showMultiselect$ = combineLatest([
this._store.items$,
this._store.fetchPartial$,
this._store.itemsSelectable$,
]).pipe(
map(
([orderItems, fetchPartial, multiSelect]) =>
multiSelect && fetchPartial && orderItems?.length > 1,
),
);
crudaUpdate$ = this.orderItem$.pipe(map((orederItem) => !!(orederItem?.cruda & 4)));
crudaUpdate$ = this.orderItem$.pipe(
map((orederItem) => !!(orederItem?.cruda & 4)),
);
editButtonDisabled$ = combineLatest([this.changeStatusLoader$, this.crudaUpdate$]).pipe(
map(([changeStatusLoader, crudaUpdate]) => changeStatusLoader || !crudaUpdate),
editButtonDisabled$ = combineLatest([
this.changeStatusLoader$,
this.crudaUpdate$,
]).pipe(
map(
([changeStatusLoader, crudaUpdate]) => changeStatusLoader || !crudaUpdate,
),
);
canEditStatus$ = combineLatest([this.statusActions$, this.crudaUpdate$]).pipe(
map(([statusActions, crudaUpdate]) => statusActions?.length > 0 && crudaUpdate),
map(
([statusActions, crudaUpdate]) =>
statusActions?.length > 0 && crudaUpdate,
),
);
openAddresses: boolean = false;
openAddresses = false;
get digOrderNumber(): string {
return this.order?.linkedRecords?.find((_) => true)?.number;
@@ -96,7 +128,8 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
get showAddresses(): boolean {
return (
(this.order?.orderType === 2 || this.order?.orderType === 4) && (!!this.order?.shipping || !!this.order?.billing)
(this.order?.orderType === 2 || this.order?.orderType === 4) &&
(!!this.order?.shipping || !!this.order?.billing)
);
}
@@ -130,10 +163,20 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
this.changeStatusDisabled$.next(true);
const orderItems = cloneDeep(this._store.items);
for (const item of orderItems) {
if (this.dateAdapter.compareDate(deadline, new Date(item.pickUpDeadline)) !== 0) {
if (
this.dateAdapter.compareDate(
deadline,
new Date(item.pickUpDeadline),
) !== 0
) {
try {
const res = await this.omsService
.setPickUpDeadline(item.orderId, item.orderItemId, item.orderItemSubsetId, deadline?.toISOString())
.setPickUpDeadline(
item.orderId,
item.orderItemId,
item.orderItemSubsetId,
deadline?.toISOString(),
)
.pipe(first())
.toPromise();
item.pickUpDeadline = deadline.toISOString();
@@ -152,7 +195,12 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
this.changeStatusDisabled$.next(true);
const orderItems = cloneDeep(this._store.items);
for (const item of orderItems) {
if (this.dateAdapter.compareDate(estimatedShippingDate, new Date(item.pickUpDeadline)) !== 0) {
if (
this.dateAdapter.compareDate(
estimatedShippingDate,
new Date(item.pickUpDeadline),
) !== 0
) {
try {
const res = await this.omsService
.setEstimatedShippingDate(
@@ -198,7 +246,10 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
try {
await this.omsService.setPreferredPickUpDate({ data }).toPromise();
this.order.items.forEach((item) => {
item.data.subsetItems.forEach((subsetItem) => (subsetItem.data.preferredPickUpDate = date.toISOString()));
item.data.subsetItems.forEach(
(subsetItem) =>
(subsetItem.data.preferredPickUpDate = date.toISOString()),
);
});
this.findLatestPreferredPickUpDate();
} catch (error) {
@@ -218,7 +269,10 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
if (subsetItems?.length > 0) {
latestDate = new Date(
subsetItems?.reduce((a, b) => {
return new Date(a.data.preferredPickUpDate) > new Date(b.data.preferredPickUpDate) ? a : b;
return new Date(a.data.preferredPickUpDate) >
new Date(b.data.preferredPickUpDate)
? a
: b;
})?.data?.preferredPickUpDate,
);
}

View File

@@ -1,6 +1,7 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { CustomerInfoDTO } from '@generated/swagger/crm-api';
import { CustomerLabelColor, CustomerLabelTextColor } from '../../../constants';
import { getEnabledCustomerFeature } from '@isa/crm/data-access';
@Component({
selector: 'page-customer-result-list-item-full',
@@ -15,11 +16,9 @@ export class CustomerResultListItemFullComponent {
customerLabelTextColor = CustomerLabelTextColor;
get label() {
return this.customer?.features?.find((f) => f.enabled);
return getEnabledCustomerFeature(this.customer?.features);
}
@Input()
customer: CustomerInfoDTO;
constructor() {}
}

View File

@@ -1,6 +1,7 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { CustomerInfoDTO } from '@generated/swagger/crm-api';
import { CustomerLabelColor, CustomerLabelTextColor } from '../../../constants';
import { getEnabledCustomerFeature } from '@isa/crm/data-access';
@Component({
selector: 'page-customer-result-list-item',
@@ -15,11 +16,9 @@ export class CustomerResultListItemComponent {
customerLabelTextColor = CustomerLabelTextColor;
get label() {
return this.customer?.features?.find((f) => f.enabled);
return getEnabledCustomerFeature(this.customer?.features);
}
@Input()
customer: CustomerInfoDTO;
constructor() {}
}

View File

@@ -1,92 +1,99 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { FormBlock } from '../form-block';
import { InterestsFormBlockData } from './interests-form-block-data';
import { LoyaltyCardService } from '@generated/swagger/crm-api';
import { shareReplay } from 'rxjs/operators';
import { isEqual } from 'lodash';
import { memorize } from '@utils/common';
@Component({
selector: 'app-interests-form-block',
templateUrl: 'interests-form-block.component.html',
styleUrls: ['interests-form-block.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class InterestsFormBlockComponent extends FormBlock<InterestsFormBlockData, UntypedFormGroup> {
private _interests: Map<string, string>;
get interests(): Map<string, string> {
return this._interests;
}
set interests(value: Map<string, string>) {
if (!isEqual(this._interests, value)) {
this._interests = value;
if (this.control) {
this.updateInterestControls();
}
}
}
get tabIndexEnd() {
return this.tabIndexStart + this.interests?.keys.length;
}
constructor(
private _fb: UntypedFormBuilder,
private _LoyaltyCardService: LoyaltyCardService,
) {
super();
this.getInterests().subscribe({
next: (response) => {
const interests = new Map<string, string>();
response.result.forEach((preference) => {
interests.set(preference.key, preference.value);
});
this.interests = interests;
},
error: (error) => {
console.error(error);
},
});
}
@memorize({ ttl: 28800000 })
getInterests() {
return this._LoyaltyCardService.LoyaltyCardListInteressen().pipe(shareReplay(1));
}
updateInterestControls() {
const fData = this.data ?? {};
this.interests?.forEach((value, key) => {
if (!this.control.contains(key)) {
this.control.addControl(key, new UntypedFormControl(fData[key] ?? false));
}
});
Object.keys(this.control.controls).forEach((key) => {
if (!this.interests.has(key)) {
this.control.removeControl(key);
}
});
}
initializeControl(data?: InterestsFormBlockData): void {
const fData = data ?? {};
this.control = this._fb.group({});
this.interests?.forEach((value, key) => {
this.control.addControl(key, new UntypedFormControl(fData[key] ?? false));
});
}
_patchValue(update: { previous: InterestsFormBlockData; current: InterestsFormBlockData }): void {
const fData = update.current ?? {};
this.interests?.forEach((value, key) => {
this.control.get(key).patchValue(fData[key] ?? false);
});
}
}
import { Component, ChangeDetectionStrategy } from '@angular/core';
import {
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
} from '@angular/forms';
import { FormBlock } from '../form-block';
import { InterestsFormBlockData } from './interests-form-block-data';
import { isEqual } from 'lodash';
@Component({
selector: 'app-interests-form-block',
templateUrl: 'interests-form-block.component.html',
styleUrls: ['interests-form-block.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
})
export class InterestsFormBlockComponent extends FormBlock<
InterestsFormBlockData,
UntypedFormGroup
> {
private _interests: Map<string, string>;
get interests(): Map<string, string> {
return this._interests;
}
set interests(value: Map<string, string>) {
if (!isEqual(this._interests, value)) {
this._interests = value;
if (this.control) {
this.updateInterestControls();
}
}
}
get tabIndexEnd() {
return this.tabIndexStart + this.interests?.keys.length;
}
constructor(private _fb: UntypedFormBuilder) {
super();
// this.getInterests().subscribe({
// next: (response) => {
// const interests = new Map<string, string>();
// response.result.forEach((preference) => {
// interests.set(preference.key, preference.value);
// });
// this.interests = interests;
// },
// error: (error) => {
// console.error(error);
// },
// });
}
// @memorize({ ttl: 28800000 })
// getInterests() {
// return this._LoyaltyCardService.LoyaltyCardListInteressen().pipe(shareReplay(1));
// }
updateInterestControls() {
const fData = this.data ?? {};
this.interests?.forEach((value, key) => {
if (!this.control.contains(key)) {
this.control.addControl(
key,
new UntypedFormControl(fData[key] ?? false),
);
}
});
Object.keys(this.control.controls).forEach((key) => {
if (!this.interests.has(key)) {
this.control.removeControl(key);
}
});
}
initializeControl(data?: InterestsFormBlockData): void {
const fData = data ?? {};
this.control = this._fb.group({});
this.interests?.forEach((value, key) => {
this.control.addControl(key, new UntypedFormControl(fData[key] ?? false));
});
}
_patchValue(update: {
previous: InterestsFormBlockData;
current: InterestsFormBlockData;
}): void {
const fData = update.current ?? {};
this.interests?.forEach((value, key) => {
this.control.get(key).patchValue(fData[key] ?? false);
});
}
}

View File

@@ -1,47 +1,41 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomerSearchComponent } from './customer-search.component';
import { CustomerResultsSideViewModule } from './results-side-view/results-side-view.module';
import { RouterModule } from '@angular/router';
import { CustomerResultsMainViewModule } from './results-main-view/results-main-view.module';
import { CustomerDetailsMainViewModule } from './details-main-view/details-main-view.module';
import { CustomerHistoryMainViewModule } from './history-main-view/history-main-view.module';
import { CustomerFilterMainViewModule } from './filter-main-view/filter-main-view.module';
import { MainSideViewModule } from './main-side-view/main-side-view.module';
import { OrderDetailsSideViewComponent } from './order-details-side-view/order-details-side-view.component';
import { CustomerMainViewComponent } from './main-view/main-view.component';
import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
import {
SelectedShoppingCartResource,
SelectedRewardShoppingCartResource,
} from '@isa/checkout/data-access';
import {
RewardSelectionService,
RewardSelectionPopUpService,
} from '@isa/checkout/shared/reward-selection-dialog';
@NgModule({
imports: [
CommonModule,
RouterModule,
SharedSplitscreenComponent,
CustomerResultsSideViewModule,
CustomerResultsMainViewModule,
CustomerDetailsMainViewModule,
CustomerHistoryMainViewModule,
CustomerFilterMainViewModule,
MainSideViewModule,
OrderDetailsSideViewComponent,
CustomerMainViewComponent,
],
exports: [CustomerSearchComponent],
declarations: [CustomerSearchComponent],
providers: [
SelectedShoppingCartResource,
SelectedRewardShoppingCartResource,
RewardSelectionService,
RewardSelectionPopUpService,
],
})
export class CustomerSearchModule {}
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomerSearchComponent } from './customer-search.component';
import { CustomerResultsSideViewModule } from './results-side-view/results-side-view.module';
import { RouterModule } from '@angular/router';
import { CustomerResultsMainViewModule } from './results-main-view/results-main-view.module';
import { CustomerDetailsMainViewModule } from './details-main-view/details-main-view.module';
import { CustomerHistoryMainViewModule } from './history-main-view/history-main-view.module';
import { CustomerFilterMainViewModule } from './filter-main-view/filter-main-view.module';
import { MainSideViewModule } from './main-side-view/main-side-view.module';
import { OrderDetailsSideViewComponent } from './order-details-side-view/order-details-side-view.component';
import { CustomerMainViewComponent } from './main-view/main-view.component';
import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
import {
RewardSelectionService,
RewardSelectionPopUpService,
} from '@isa/checkout/shared/reward-selection-dialog';
@NgModule({
imports: [
CommonModule,
RouterModule,
SharedSplitscreenComponent,
CustomerResultsSideViewModule,
CustomerResultsMainViewModule,
CustomerDetailsMainViewModule,
CustomerHistoryMainViewModule,
CustomerFilterMainViewModule,
MainSideViewModule,
OrderDetailsSideViewComponent,
CustomerMainViewComponent,
],
exports: [CustomerSearchComponent],
declarations: [CustomerSearchComponent],
providers: [
RewardSelectionService,
RewardSelectionPopUpService,
],
})
export class CustomerSearchModule {}

View File

@@ -36,13 +36,21 @@ import { CrmCustomerService } from '@domain/crm';
import { MessageModalComponent, MessageModalData } from '@modal/message';
import { GenderSettingsService } from '@shared/services/gender';
import { toSignal } from '@angular/core/rxjs-interop';
import { CrmTabMetadataService, Customer } from '@isa/crm/data-access';
import { CustomerAdapter } from '@isa/checkout/data-access';
import {
CrmTabMetadataService,
Customer,
AssignedPayer,
} from '@isa/crm/data-access';
import {
CustomerAdapter,
ShippingAddressAdapter,
} from '@isa/checkout/data-access';
import {
NavigateAfterRewardSelection,
RewardSelectionPopUpService,
} from '@isa/checkout/shared/reward-selection-dialog';
import { NavigationStateService } from '@isa/core/navigation';
import { ShippingAddressDTO as CrmShippingAddressDTO } from '@generated/swagger/crm-api';
export interface CustomerDetailsViewMainState {
isBusy: boolean;
@@ -407,9 +415,9 @@ export class CustomerDetailsViewMainComponent
await this._updateNotifcationChannelsAsync(currentBuyer);
this._setPayer();
await this._setPayer();
this._setShippingAddress();
await this._setShippingAddress();
// #5262 Check for reward selection flow before navigation
if (this.hasReturnUrl()) {
@@ -631,8 +639,46 @@ export class CustomerDetailsViewMainComponent
}
}
@log
_setPayer() {
@logAsync
async _setPayer() {
// Check if there's a selected payer in metadata (from previous address selection)
const selectedPayerId = this.crmTabMetadataService.selectedPayerId(
this.processId,
);
if (selectedPayerId) {
// Load the selected payer from metadata
try {
const payerResponse = await this.customerService
.getPayer(selectedPayerId)
.toPromise();
if (payerResponse?.result) {
// Create AssignedPayer structure expected by adapter
// Type cast needed due to incompatible enum types between CRM and Checkout APIs
const assignedPayer = {
payer: {
id: selectedPayerId,
data: payerResponse.result,
},
} as AssignedPayer;
const payer = CustomerAdapter.toPayerFromAssignedPayer(assignedPayer);
if (payer) {
this._checkoutService.setPayer({
processId: this.processId,
payer,
});
return;
}
}
} catch (error) {
console.error('Failed to load selected payer from metadata', error);
}
}
// Fallback to current payer from component state
if (this.payer) {
this._checkoutService.setPayer({
processId: this.processId,
@@ -641,8 +687,41 @@ export class CustomerDetailsViewMainComponent
}
}
@log
_setShippingAddress() {
@logAsync
async _setShippingAddress() {
// Check if there's a selected shipping address in metadata (from previous address selection)
const selectedShippingAddressId =
this.crmTabMetadataService.selectedShippingAddressId(this.processId);
if (selectedShippingAddressId) {
// Load the selected shipping address from metadata
try {
const addressResponse = await this.customerService
.getShippingAddress(selectedShippingAddressId)
.toPromise();
if (addressResponse?.result) {
const shippingAddress = ShippingAddressAdapter.fromCrmShippingAddress(
addressResponse.result as CrmShippingAddressDTO,
);
if (shippingAddress) {
this._checkoutService.setShippingAddress({
processId: this.processId,
shippingAddress,
});
return;
}
}
} catch (error) {
console.error(
'Failed to load selected shipping address from metadata',
error,
);
}
}
// Fallback to current shipping address from component state
if (this.shippingAddress) {
this._checkoutService.setShippingAddress({
processId: this.processId,

View File

@@ -1,4 +1,10 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, inject } from '@angular/core';
import {
Component,
ChangeDetectionStrategy,
OnInit,
OnDestroy,
inject,
} from '@angular/core';
import { CustomerSearchStore } from '../store';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { map, takeUntil, tap } from 'rxjs/operators';
@@ -21,6 +27,7 @@ import { CustomerSearchNavigation } from '@shared/services/navigation';
import { CustomerOrderItemListItemComponent } from './order-item-list-item/order-item-list-item.component';
import { groupBy } from '@ui/common';
import { EnvironmentService } from '@core/environment';
import { getEnabledCustomerFeature } from '@isa/crm/data-access';
@Component({
selector: 'page-customer-order-details-main-view',
@@ -52,38 +59,58 @@ export class OrderDetailsMainViewComponent implements OnInit, OnDestroy {
private _navigation = inject(CustomerSearchNavigation);
private _env = inject(EnvironmentService);
orderId$ = this._activateRoute.params.pipe(map((params) => Number(params.orderId)));
orderId$ = this._activateRoute.params.pipe(
map((params) => Number(params.orderId)),
);
order$ = this._store.order$;
orderTargetBranch$ = this.order$.pipe(map((order) => order?.targetBranch?.id));
orderTargetBranch$ = this.order$.pipe(
map((order) => order?.targetBranch?.id),
);
orderShippingTarget$ = this.order$.pipe(map((order) => order?.shipping?.data));
orderShippingTarget$ = this.order$.pipe(
map((order) => order?.shipping?.data),
);
customerId$ = this._activateRoute.params.pipe(map((params) => Number(params.customerId)));
customerId$ = this._activateRoute.params.pipe(
map((params) => Number(params.customerId)),
);
customer$ = this._store.customer$;
accountType$ = this.customer$.pipe(
map((customer) => customer?.features?.find((feature) => feature.group === 'd-customertype')),
map((customer) => getEnabledCustomerFeature(customer?.features)),
);
accountTypeKey$ = this.accountType$.pipe(map((accountType) => accountType?.key));
accountTypeKey$ = this.accountType$.pipe(
map((accountType) => accountType?.key),
);
accountTypeDescription$ = this.accountType$.pipe(map((accountType) => accountType?.description));
accountTypeDescription$ = this.accountType$.pipe(
map((accountType) => accountType?.description),
);
orderItemId$ = this._activateRoute.params.pipe(map((params) => Number(params.orderItemId)));
orderItemId$ = this._activateRoute.params.pipe(
map((params) => Number(params.orderItemId)),
);
orderItems$ = this.order$.pipe(map((order) => order?.items?.map((i) => i?.data)));
orderItems$ = this.order$.pipe(
map((order) => order?.items?.map((i) => i?.data)),
);
selectedOrderItem$ = this._store.selectedOrderItem$;
selectedOrderItemOrderType$ = this.selectedOrderItem$.pipe(map((orderItem) => orderItem?.features?.orderType));
selectedOrderItemOrderType$ = this.selectedOrderItem$.pipe(
map((orderItem) => orderItem?.features?.orderType),
);
private _onDestroy = new Subject<void>();
ordersRoute$ = combineLatest([this.customerId$, this._store.processId$]).pipe(
map(([customerId, processId]) => this._navigation.ordersRoute({ processId, customerId })),
map(([customerId, processId]) =>
this._navigation.ordersRoute({ processId, customerId }),
),
);
orderDetailsHistoryRoute$ = combineLatest([
@@ -93,27 +120,38 @@ export class OrderDetailsMainViewComponent implements OnInit, OnDestroy {
this.orderItemId$,
]).pipe(
map(([customerId, processId, orderId, orderItemId]) =>
this._navigation.orderDetailsHistoryRoute({ processId, customerId, orderId, orderItemId }),
this._navigation.orderDetailsHistoryRoute({
processId,
customerId,
orderId,
orderItemId,
}),
),
);
groupedOrderItemsByOrderType$ = this.orderItems$.pipe(
map((orderItems) => groupBy(orderItems, (orderItem) => orderItem?.features?.orderType)),
map((orderItems) =>
groupBy(orderItems, (orderItem) => orderItem?.features?.orderType),
),
tap((groupedOrderItems) => console.log(groupedOrderItems)),
);
showSelectedItem$ = this._env.matchDesktopXLarge$;
showItemList$ = this.showSelectedItem$.pipe(map((showSelectedItem) => !showSelectedItem));
showItemList$ = this.showSelectedItem$.pipe(
map((showSelectedItem) => !showSelectedItem),
);
ngOnInit(): void {
this.orderId$.pipe(takeUntil(this._onDestroy)).subscribe((orderId) => {
this._store.selectOrder(orderId);
});
this.customerId$.pipe(takeUntil(this._onDestroy)).subscribe((customerId) => {
this._store.selectCustomer({ customerId });
});
this.customerId$
.pipe(takeUntil(this._onDestroy))
.subscribe((customerId) => {
this._store.selectCustomer({ customerId });
});
}
ngOnDestroy(): void {

View File

@@ -1,10 +1,19 @@
<ng-container *ngIf="orderItem$ | async; let orderItem">
<div class="grid grid-flow-row gap-px-2">
<div class="bg-[#F5F7FA] flex flex-row justify-between items-center p-4 rounded-t">
<div class="grid grid-flow-col gap-[0.4375rem] items-center" *ngIf="fetchingCustomerDone$ | async; else featureLoading">
<ng-container *ngIf="features$ | async; let features">
<shared-icon *ngIf="features?.length > 0" [size]="24" icon="person"></shared-icon>
<div class="grid grid-flow-col gap-2 items-center font-bold text-p2" *ngFor="let feature of features">
<div
class="bg-[#F5F7FA] flex flex-row justify-between items-center p-4 rounded-t"
>
<div
class="grid grid-flow-col gap-[0.4375rem] items-center"
*ngIf="fetchingCustomerDone$ | async; else featureLoading"
>
<ng-container *ngIf="customerFeature$ | async; let feature">
<shared-icon
*ngIf="!!feature"
[size]="24"
icon="person"
></shared-icon>
<div class="grid grid-flow-col gap-2 items-center font-bold text-p2">
{{ feature?.description }}
</div>
</ng-container>
@@ -27,30 +36,69 @@
<shared-skeleton-loader class="w-64 h-6"></shared-skeleton-loader>
</ng-template>
<div class="page-pickup-shelf-details-header__details bg-white px-4 pt-4 pb-5">
<div class="flex flex-row items-center" [class.mb-8]="!orderItem?.features?.paid && !isKulturpass">
<page-pickup-shelf-details-header-nav-menu class="mr-2" [customer]="customer$ | async"></page-pickup-shelf-details-header-nav-menu>
<h2 class="page-pickup-shelf-details-header__details-header items-center">
<div
class="page-pickup-shelf-details-header__details bg-white px-4 pt-4 pb-5"
>
<div
class="flex flex-row items-center"
[class.mb-8]="!orderItem?.features?.paid && !isKulturpass"
>
<page-pickup-shelf-details-header-nav-menu
class="mr-2"
[customer]="customer$ | async"
></page-pickup-shelf-details-header-nav-menu>
<h2
class="page-pickup-shelf-details-header__details-header items-center"
>
<div class="text-h2">
{{ orderItem?.organisation }}
<ng-container *ngIf="!!orderItem?.organisation && (!!orderItem?.firstName || !!orderItem?.lastName)">-</ng-container>
<ng-container
*ngIf="
!!orderItem?.organisation &&
(!!orderItem?.firstName || !!orderItem?.lastName)
"
>-</ng-container
>
{{ orderItem?.lastName }}
{{ orderItem?.firstName }}
</div>
<div class="page-pickup-shelf-details-header__header-compartment text-h3">
{{ orderItem?.compartmentCode }}{{ orderItem?.compartmentInfo && '_' + orderItem?.compartmentInfo }}
<div
class="page-pickup-shelf-details-header__header-compartment text-h3"
>
{{ orderItem?.compartmentCode
}}{{
orderItem?.compartmentInfo && '_' + orderItem?.compartmentInfo
}}
</div>
</h2>
</div>
<div class="page-pickup-shelf-details-header__paid-marker mt-[0.375rem]" *ngIf="orderItem?.features?.paid && !isKulturpass">
<div class="font-bold flex flex-row items-center justify-center text-p2 text-[#26830C]">
<shared-icon class="flex items-center justify-center mr-[0.375rem]" [size]="24" icon="credit-card"></shared-icon>
<div
class="page-pickup-shelf-details-header__paid-marker mt-[0.375rem]"
*ngIf="orderItem?.features?.paid && !isKulturpass"
>
<div
class="font-bold flex flex-row items-center justify-center text-p2 text-[#26830C]"
>
<shared-icon
class="flex items-center justify-center mr-[0.375rem]"
[size]="24"
icon="credit-card"
></shared-icon>
{{ orderItem?.features?.paid }}
</div>
</div>
<div class="page-pickup-shelf-details-header__paid-marker mt-[0.375rem] text-[#26830C]" *ngIf="isKulturpass">
<svg class="fill-current mr-2" xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22">
<div
class="page-pickup-shelf-details-header__paid-marker mt-[0.375rem] text-[#26830C]"
*ngIf="isKulturpass"
>
<svg
class="fill-current mr-2"
xmlns="http://www.w3.org/2000/svg"
height="22"
viewBox="0 -960 960 960"
width="22"
>
<path
d="M880-740v520q0 24-18 42t-42 18H140q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h680q24 0 42 18t18 42ZM140-631h680v-109H140v109Zm0 129v282h680v-282H140Zm0 282v-520 520Z"
/>
@@ -59,21 +107,42 @@
</div>
<div class="page-pickup-shelf-details-header__details-wrapper -mt-3">
<div class="flex flex-row page-pickup-shelf-details-header__buyer-number" data-detail-id="Kundennummer">
<div
class="flex flex-row page-pickup-shelf-details-header__buyer-number"
data-detail-id="Kundennummer"
>
<div class="min-w-[9rem]">Kundennummer</div>
<div class="flex flex-row font-bold">{{ orderItem?.buyerNumber }}</div>
<div class="flex flex-row font-bold">
{{ orderItem?.buyerNumber }}
</div>
</div>
<div class="flex flex-row page-pickup-shelf-details-header__order-number" data-detail-id="VorgangId">
<div
class="flex flex-row page-pickup-shelf-details-header__order-number"
data-detail-id="VorgangId"
>
<div class="min-w-[9rem]">Vorgang-ID</div>
<div class="flex flex-row font-bold">{{ orderItem?.orderNumber }}</div>
<div class="flex flex-row font-bold">
{{ orderItem?.orderNumber }}
</div>
</div>
<div class="flex flex-row page-pickup-shelf-details-header__order-date" data-detail-id="Bestelldatum">
<div
class="flex flex-row page-pickup-shelf-details-header__order-date"
data-detail-id="Bestelldatum"
>
<div class="min-w-[9rem]">Bestelldatum</div>
<div class="flex flex-row font-bold">{{ orderItem?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
<div class="flex flex-row font-bold">
{{ orderItem?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr
</div>
</div>
<div class="flex flex-row page-pickup-shelf-details-header__processing-status justify-between" data-detail-id="Status">
<div
class="flex flex-row page-pickup-shelf-details-header__processing-status justify-between"
data-detail-id="Status"
>
<div class="min-w-[9rem]">Status</div>
<div *ngIf="!(changeStatusLoader$ | async)" class="flex flex-row font-bold -mr-[0.125rem]">
<div
*ngIf="!(changeStatusLoader$ | async)"
class="flex flex-row font-bold -mr-[0.125rem]"
>
<span *ngIf="!(canEditStatus$ | async)">
{{ orderItem?.processingStatus | processingStatus }}
</span>
@@ -97,7 +166,12 @@
icon="arrow-drop-down"
></shared-icon>
</button>
<ui-dropdown #statusDropdown yPosition="below" xPosition="after" [xOffset]="8">
<ui-dropdown
#statusDropdown
yPosition="below"
xPosition="after"
[xOffset]="8"
>
<button
uiDropdownItem
*ngFor="let action of statusActions$ | async"
@@ -111,12 +185,22 @@
</ui-dropdown>
</ng-container>
</div>
<ui-spinner *ngIf="changeStatusLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
<ui-spinner
*ngIf="changeStatusLoader$ | async; let loader"
class="flex flex-row font-bold loader"
[show]="loader"
></ui-spinner>
</div>
<div class="flex flex-row page-pickup-shelf-details-header__order-source" data-detail-id="Bestellkanal">
<div
class="flex flex-row page-pickup-shelf-details-header__order-source"
data-detail-id="Bestellkanal"
>
<div class="min-w-[9rem]">Bestellkanal</div>
<div class="flex flex-row font-bold">
<shared-skeleton-loader class="w-32" *ngIf="fetchingOrder$ | async; else orderSourceTmpl"></shared-skeleton-loader>
<shared-skeleton-loader
class="w-32"
*ngIf="fetchingOrder$ | async; else orderSourceTmpl"
></shared-skeleton-loader>
<ng-template #orderSourceTmpl>
{{ order()?.features?.orderSource }}
</ng-template>
@@ -142,19 +226,30 @@
<ng-template #changeDate>
<div class="min-w-[9rem]">Geändert</div>
<div class="flex flex-row font-bold">{{ orderItem?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
<div class="flex flex-row font-bold">
{{
orderItem?.processingStatusDate | date: 'dd.MM.yy | HH:mm'
}}
Uhr
</div>
</ng-template>
</div>
<div
class="flex flex-row page-pickup-shelf-details-header__pick-up justify-between"
data-detail-id="Wunschdatum"
*ngIf="orderItem.orderType === 1 && (orderItem.processingStatus === 16 || orderItem.processingStatus === 8192)"
*ngIf="
orderItem.orderType === 1 &&
(orderItem.processingStatus === 16 ||
orderItem.processingStatus === 8192)
"
>
<ng-container *ngTemplateOutlet="preferredPickUpDate"></ng-container>
</div>
<div class="flex flex-col page-pickup-shelf-details-header__dig-and-notification">
<div
class="flex flex-col page-pickup-shelf-details-header__dig-and-notification"
>
<div
*ngIf="orderItem.orderType === 1"
class="flex flex-row page-pickup-shelf-details-header__notification"
@@ -162,9 +257,14 @@
>
<div class="min-w-[9rem]">Benachrichtigung</div>
<div class="flex flex-row font-bold">
<shared-skeleton-loader class="w-32" *ngIf="fetchingOrder$ | async; else notificationsChannelTpl"></shared-skeleton-loader>
<shared-skeleton-loader
class="w-32"
*ngIf="fetchingOrder$ | async; else notificationsChannelTpl"
></shared-skeleton-loader>
<ng-template #notificationsChannelTpl>
{{ (notificationsChannel$ | async | notificationsChannel) || '-' }}
{{
(notificationsChannel$ | async | notificationsChannel) || '-'
}}
</ng-template>
</div>
</div>
@@ -175,11 +275,17 @@
<ng-template #abholfrist>
<div class="min-w-[9rem]">Abholfrist</div>
<div *ngIf="!(orderItemSubsetLoading$ | async); else featureLoading" class="flex flex-row font-bold">
<div
*ngIf="!(orderItemSubsetLoading$ | async); else featureLoading"
class="flex flex-row font-bold"
>
<button
[uiOverlayTrigger]="deadlineDatepicker"
#deadlineDatepickerTrigger="uiOverlayTrigger"
[disabled]="!isKulturpass && (!!orderItem?.features?.paid || (changeDateDisabled$ | async))"
[disabled]="
!isKulturpass &&
(!!orderItem?.features?.paid || (changeDateDisabled$ | async))
"
class="cta-pickup-deadline"
matomoClickCategory="pickup-shelf-details-header"
matomoClickAction="click"
@@ -188,7 +294,11 @@
<strong class="border-r border-[#AEB7C1] pr-4">
{{ orderItem?.pickUpDeadline | date: 'dd.MM.yy' }}
</strong>
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
<shared-icon
class="text-[#596470] ml-4"
[size]="24"
icon="isa-calendar"
></shared-icon>
</button>
<ui-datepicker
#deadlineDatepicker
@@ -205,23 +315,40 @@
<ng-template #preferredPickUpDate>
<div class="min-w-[9rem]">Zurücklegen bis</div>
<div *ngIf="!(orderItemSubsetLoading$ | async); else featureLoading" class="flex flex-row font-bold">
<div
*ngIf="!(orderItemSubsetLoading$ | async); else featureLoading"
class="flex flex-row font-bold"
>
<button
[uiOverlayTrigger]="preferredPickUpDatePicker"
#preferredPickUpDatePickerTrigger="uiOverlayTrigger"
[disabled]="(!isKulturpass && !!orderItem?.features?.paid) || (changeDateDisabled$ | async)"
[disabled]="
(!isKulturpass && !!orderItem?.features?.paid) ||
(changeDateDisabled$ | async)
"
class="cta-pickup-preferred"
matomoClickCategory="pickup-shelf-details-header"
matomoClickAction="click"
matomoClickName="pickup-preferred"
>
<strong class="border-r border-[#AEB7C1] pr-4" *ngIf="findLatestPreferredPickUpDate$ | async; let pickUpDate; else: selectTemplate">
<strong
class="border-r border-[#AEB7C1] pr-4"
*ngIf="
findLatestPreferredPickUpDate$ | async;
let pickUpDate;
else: selectTemplate
"
>
{{ pickUpDate | date: 'dd.MM.yy' }}
</strong>
<ng-template #selectTemplate>
<strong class="border-r border-[#AEB7C1] pr-4">Auswählen</strong>
</ng-template>
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
<shared-icon
class="text-[#596470] ml-4"
[size]="24"
icon="isa-calendar"
></shared-icon>
</button>
<ui-datepicker
#preferredPickUpDatePicker
@@ -238,7 +365,10 @@
<ng-template #vslLieferdatum>
<div class="min-w-[9rem]">vsl. Lieferdatum</div>
<div *ngIf="!(orderItemSubsetLoading$ | async); else featureLoading" class="flex flex-row font-bold">
<div
*ngIf="!(orderItemSubsetLoading$ | async); else featureLoading"
class="flex flex-row font-bold"
>
<button
class="cta-datepicker"
[disabled]="changeDateDisabled$ | async"
@@ -251,7 +381,11 @@
<span class="border-r border-[#AEB7C1] pr-4">
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
</span>
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
<shared-icon
class="text-[#596470] ml-4"
[size]="24"
icon="isa-calendar"
></shared-icon>
</button>
<ui-datepicker
#uiDatepicker

View File

@@ -1,4 +1,12 @@
import { AsyncPipe, DatePipe, NgFor, NgIf, NgSwitch, NgSwitchCase, NgTemplateOutlet } from '@angular/common';
import {
AsyncPipe,
DatePipe,
NgFor,
NgIf,
NgSwitch,
NgSwitchCase,
NgTemplateOutlet,
} from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
@@ -28,6 +36,7 @@ import { PickUpShelfDetailsHeaderNavMenuComponent } from '../pickup-shelf-detail
import { SkeletonLoaderComponent } from '@shared/components/loader';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatomoModule } from 'ngx-matomo-client';
import { getEnabledCustomerFeature } from '@isa/crm/data-access';
@Component({
selector: 'page-pickup-shelf-details-header',
@@ -65,7 +74,10 @@ export class PickUpShelfDetailsHeaderComponent {
handleAction = new EventEmitter<KeyValueDTOOfStringAndString>();
@Output()
updateDate = new EventEmitter<{ date: Date; type?: 'delivery' | 'pickup' | 'preferred' }>();
updateDate = new EventEmitter<{
date: Date;
type?: 'delivery' | 'pickup' | 'preferred';
}>();
orderItemSubsetLoading$ = this._store.orderItemSubsetLoading$;
@@ -83,12 +95,19 @@ export class PickUpShelfDetailsHeaderComponent {
?.reduce((acc, item) => {
return [...acc, ...item.data.subsetItems];
}, [])
?.filter((a) => !!a.data.preferredPickUpDate && selectedOrderItemIds.find((id) => id === a.data.id));
?.filter(
(a) =>
!!a.data.preferredPickUpDate &&
selectedOrderItemIds.find((id) => id === a.data.id),
);
if (subsetItems?.length > 0) {
latestDate = new Date(
subsetItems?.reduce((a, b) => {
return new Date(a.data.preferredPickUpDate) > new Date(b.data.preferredPickUpDate) ? a : b;
return new Date(a.data.preferredPickUpDate) >
new Date(b.data.preferredPickUpDate)
? a
: b;
})?.data?.preferredPickUpDate,
);
}
@@ -108,14 +127,24 @@ export class PickUpShelfDetailsHeaderComponent {
return this.order()?.features?.orderSource === 'KulturPass';
}
minDateDatepicker = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), -1);
minDateDatepicker = this.dateAdapter.addCalendarDays(
this.dateAdapter.today(),
-1,
);
today = this.dateAdapter.today();
// Daten die im Header Angezeigt werden sollen
orderItem$ = combineLatest([
this._store.selectedOrderItem$, // Wenn man im Abholfach ist muss das ausgewählte OrderItem genommen werden
this._store.selectedOrderItems$.pipe(map((orderItems) => orderItems?.find((_) => true))), // Wenn man in der Warenausgabe ist muss man das erste OrderItem nehmen
]).pipe(map(([selectedOrderItem, selectedOrderItems]) => selectedOrderItem || selectedOrderItems));
this._store.selectedOrderItems$.pipe(
map((orderItems) => orderItems?.find((_) => true)),
), // Wenn man in der Warenausgabe ist muss man das erste OrderItem nehmen
]).pipe(
map(
([selectedOrderItem, selectedOrderItems]) =>
selectedOrderItem || selectedOrderItems,
),
);
changeDateLoader$ = new BehaviorSubject<boolean>(false);
changePreferredDateLoader$ = new BehaviorSubject<boolean>(false);
@@ -124,28 +153,41 @@ export class PickUpShelfDetailsHeaderComponent {
changeDateDisabled$ = this.changeStatusDisabled$;
fetchingCustomerDone$ = this._store.fetchingCustomer$.pipe(map((fetchingCustomer) => !fetchingCustomer));
fetchingCustomerDone$ = this._store.fetchingCustomer$.pipe(
map((fetchingCustomer) => !fetchingCustomer),
);
customer$ = this._store.customer$;
features$ = this.customer$.pipe(
map((customer) => customer?.features || []),
map((features) => features.filter((f) => f.enabled && !!f.description)),
customerFeature$ = this.customer$.pipe(
map((customer) => getEnabledCustomerFeature(customer?.features)),
shareReplay(),
);
statusActions$ = this.orderItem$.pipe(
map((orderItem) => orderItem?.actions?.filter((action) => action.enabled === false)),
map((orderItem) =>
orderItem?.actions?.filter((action) => action.enabled === false),
),
);
crudaUpdate$ = this.orderItem$.pipe(map((orederItem) => !!(orederItem?.cruda & 4)));
crudaUpdate$ = this.orderItem$.pipe(
map((orederItem) => !!(orederItem?.cruda & 4)),
);
editButtonDisabled$ = combineLatest([this.changeStatusLoader$, this.crudaUpdate$]).pipe(
map(([changeStatusLoader, crudaUpdate]) => changeStatusLoader || !crudaUpdate),
editButtonDisabled$ = combineLatest([
this.changeStatusLoader$,
this.crudaUpdate$,
]).pipe(
map(
([changeStatusLoader, crudaUpdate]) => changeStatusLoader || !crudaUpdate,
),
);
canEditStatus$ = combineLatest([this.statusActions$, this.crudaUpdate$]).pipe(
map(([statusActions, crudaUpdate]) => statusActions?.length > 0 && crudaUpdate),
map(
([statusActions, crudaUpdate]) =>
statusActions?.length > 0 && crudaUpdate,
),
);
constructor(

View File

@@ -21,7 +21,7 @@
(click)="$event?.preventDefault(); $event?.stopPropagation()"
>
<shared-icon icon="shopping-cart-bold" [size]="22"></shared-icon>
<span class="shopping-cart-count-label ml-2">{{ cartCount() }}</span>
<span class="shopping-cart-count-label ml-2">{{ itemCount() }}</span>
</button>
}
</a>

View File

@@ -12,6 +12,7 @@ import {
computed,
input,
effect,
untracked,
} from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
@@ -27,7 +28,10 @@ import {
} from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { TabService } from '@isa/core/tabs';
import { CheckoutMetadataService } from '@isa/checkout/data-access';
import {
CheckoutMetadataService,
ShoppingCartResource,
} from '@isa/checkout/data-access';
@Component({
selector: 'shell-process-bar-item',
@@ -35,12 +39,14 @@ import { CheckoutMetadataService } from '@isa/checkout/data-access';
styleUrls: ['process-bar-item.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: false,
providers: [ShoppingCartResource],
})
export class ShellProcessBarItemComponent
implements OnInit, OnDestroy, OnChanges
{
#tabService = inject(TabService);
#checkoutMetadataService = inject(CheckoutMetadataService);
#shoppingCartResource = inject(ShoppingCartResource);
tab = computed(() => this.#tabService.entityMap()[this.process().id]);
@@ -48,6 +54,18 @@ export class ShellProcessBarItemComponent
return this.#checkoutMetadataService.getShoppingCartId(this.process().id);
});
shoppingCartIdEffect = effect(() => {
const shoppingCartId = this.shoppingCartId();
untracked(() =>
this.#shoppingCartResource.setShoppingCartId(shoppingCartId),
);
});
itemCount = computed(() => {
const shoppingCart = this.#shoppingCartResource.resource.value();
return shoppingCart?.items?.length ?? 0;
});
private _process$ = new BehaviorSubject<ApplicationProcess>(undefined);
process$ = this._process$.asObservable();
@@ -58,23 +76,7 @@ export class ShellProcessBarItemComponent
closed = new EventEmitter();
showCart = computed(() => {
const tab = this.tab();
const pdata = tab.metadata?.process_data as { count?: number };
if (!pdata) {
return false;
}
return 'count' in pdata;
});
cartCount = computed(() => {
const tab = this.tab();
const pdata = tab.metadata?.process_data as { count?: number };
return pdata?.count ?? 0;
return true;
});
currentLocationUrlTree = computed(() => {

View File

@@ -1,72 +1,74 @@
<div
class="flex flex-row justify-start items-center h-full max-w-[1920px] desktop-xx-large:max-w-[2448px] relative"
(mouseenter)="hovered = true"
(mouseleave)="hovered = false"
>
@if (showScrollArrows) {
<button
class="scroll-button prev-button"
[class.invisible]="!this.hovered || showArrowLeft"
(click)="scrollLeft()"
>
<ui-icon icon="arrow_head" size="22px" rotate="180deg"></ui-icon>
</button>
}
<div
#processContainer
class="grid grid-flow-col max-w-[calc(100vw-9.5rem)] overflow-x-scroll"
(wheel)="onMouseWheel($event)"
(scroll)="checkScrollArrowVisibility()"
>
@for (process of processes$ | async; track trackByFn($index, process)) {
<shell-process-bar-item
[process]="process"
(closed)="checkScrollArrowVisibility()"
></shell-process-bar-item>
}
</div>
@if (showScrollArrows) {
<button
class="scroll-button next-button"
[class.invisible]="!this.hovered || showArrowRight"
(click)="scrollRight()"
>
<ui-icon icon="arrow_head" size="22px"></ui-icon>
</button>
}
<button
type="button"
class="grid px-3 shell-process-bar__create-process-btn-desktop start-process-btn grid-flow-col items-center justify-center gap-[0.625rem] grow-0 shrink-0"
(click)="createProcess('product')"
type="button"
>
<ng-container *ngTemplateOutlet="createProcessButtonContent"></ng-container>
</button>
<div class="grow"></div>
<button
type="button"
[disabled]="!(processes$ | async)?.length"
class="grow-0 shrink-0 px-3 mr-[.125rem] shell-process-bar__close-processes"
(click)="closeAllProcesses()"
>
<div
class="rounded border border-solid flex flex-row pl-3 pr-[0.625rem] py-[0.375rem]"
[class.text-brand]="(processes$ | async)?.length"
[class.border-brand]="(processes$ | async)?.length"
[class.text-[#AEB7C1]]="!(processes$ | async)?.length"
[class.border-[#AEB7C1]]="!(processes$ | async)?.length"
>
<span class="mr-1">{{ (processes$ | async)?.length }}</span>
<shared-icon icon="close"></shared-icon>
</div>
</button>
</div>
<ng-template #createProcessButtonContent>
<div class="bg-brand text-white w-[2.375rem] h-[2.375rem] rounded-full grid items-center justify-center mx-auto mb-1">
<shared-icon icon="add"></shared-icon>
</div>
@if (showStartProcessText$ | async) {
<span class="text-brand create-process-btn-text">Vorgang starten</span>
}
</ng-template>
<div
class="flex flex-row justify-start items-center h-full max-w-[1920px] desktop-xx-large:max-w-[2448px] relative"
(mouseenter)="hovered = true"
(mouseleave)="hovered = false"
>
@if (showScrollArrows) {
<button
class="scroll-button prev-button"
[class.invisible]="!this.hovered || showArrowLeft"
(click)="scrollLeft()"
>
<ui-icon icon="arrow_head" size="22px" rotate="180deg"></ui-icon>
</button>
}
<div
#processContainer
class="grid grid-flow-col max-w-[calc(100vw-9.5rem)] overflow-x-scroll"
(wheel)="onMouseWheel($event)"
(scroll)="checkScrollArrowVisibility()"
>
@for (process of processes$ | async; track process.id) {
<shell-process-bar-item
[process]="process"
(closed)="checkScrollArrowVisibility()"
></shell-process-bar-item>
}
</div>
@if (showScrollArrows) {
<button
class="scroll-button next-button"
[class.invisible]="!this.hovered || showArrowRight"
(click)="scrollRight()"
>
<ui-icon icon="arrow_head" size="22px"></ui-icon>
</button>
}
<button
type="button"
class="grid px-3 shell-process-bar__create-process-btn-desktop start-process-btn grid-flow-col items-center justify-center gap-[0.625rem] grow-0 shrink-0"
(click)="createProcess('product')"
type="button"
>
<ng-container *ngTemplateOutlet="createProcessButtonContent"></ng-container>
</button>
<div class="grow"></div>
<button
type="button"
[disabled]="!(processes$ | async)?.length"
class="grow-0 shrink-0 px-3 mr-[.125rem] shell-process-bar__close-processes"
(click)="closeAllProcesses()"
>
<div
class="rounded border border-solid flex flex-row pl-3 pr-[0.625rem] py-[0.375rem]"
[class.text-brand]="(processes$ | async)?.length"
[class.border-brand]="(processes$ | async)?.length"
[class.text-[#AEB7C1]]="!(processes$ | async)?.length"
[class.border-[#AEB7C1]]="!(processes$ | async)?.length"
>
<span class="mr-1">{{ (processes$ | async)?.length }}</span>
<shared-icon icon="close"></shared-icon>
</div>
</button>
</div>
<ng-template #createProcessButtonContent>
<div
class="bg-brand text-white w-[2.375rem] h-[2.375rem] rounded-full grid items-center justify-center mx-auto mb-1"
>
<shared-icon icon="add"></shared-icon>
</div>
@if (showStartProcessText$ | async) {
<span class="text-brand create-process-btn-text">Vorgang starten</span>
}
</ng-template>

View File

@@ -1,4 +1,3 @@
import { coerceArray } from '@angular/cdk/coercion';
import {
Component,
ChangeDetectionStrategy,
@@ -65,8 +64,16 @@ export class ShellProcessBarComponent implements OnInit {
}
initProcesses$() {
// TODO: Use implementation from develop
this.processes$ = this.section$.pipe(
switchMap((section) => this._app.getProcesses$(section)),
// TODO: Nach Prämie release kann der Filter rausgenommen werden
map((processes) =>
processes.filter(
(process) =>
process.type === 'cart' || process.type === 'cart-checkout',
),
),
);
}

View File

@@ -86,7 +86,15 @@
sharedRegexRouterLinkActive="active"
sharedRegexRouterLinkActiveTest="^\/\d*\/reward"
>
<span class="side-menu-group-item-icon"> </span>
<span class="side-menu-group-item-icon">
<shell-reward-shopping-cart-indicator />
@if (hasShoppingCartItems()) {
<span
class="w-2 h-2 bg-isa-accent-red rounded-full"
data-what="open-reward-tasks-indicator"
></span>
}
</span>
<span class="side-menu-group-item-label">Prämienshop</span>
</a>
}
@@ -272,11 +280,7 @@
<a
class="side-menu-group-item"
(click)="closeSideMenu(); focusSearchBox()"
[routerLink]="[
'/',
tabId(),
'remission',
]"
[routerLink]="['/', tabId(), 'remission']"
(isActiveChange)="focusSearchBox(); remissionExpanded.set($event)"
routerLinkActive="active"
#rlActive="routerLinkActive"

View File

@@ -35,6 +35,7 @@ import { TabService } from '@isa/core/tabs';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { isaNavigationRemission2, isaNavigationReturn } from '@isa/icons';
import z from 'zod';
import { SelectedRewardShoppingCartResource } from '@isa/checkout/data-access';
@Component({
selector: 'shell-side-menu',
@@ -71,6 +72,7 @@ export class ShellSideMenuComponent {
#cdr = inject(ChangeDetectorRef);
#document = inject(DOCUMENT);
tabService = inject(TabService);
#shoppingCartResource = inject(SelectedRewardShoppingCartResource);
staticTabIds = Object.values(
this.#config.get('process.ids', z.record(z.coerce.number())),
@@ -151,6 +153,10 @@ export class ShellSideMenuComponent {
return this.#router.createUrlTree(['/', tabId || this.nextId(), routeName]);
});
hasShoppingCartItems = computed(() => {
return this.#shoppingCartResource.resource.value()?.items?.length > 0;
});
pickUpShelfOutRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
map((processId) => {
if (processId) {

View File

@@ -1,9 +1,9 @@
import { NgModule } from '@angular/core';
import { ShellSideMenuComponent } from './side-menu.component';
@NgModule({
imports: [ShellSideMenuComponent],
exports: [ShellSideMenuComponent],
})
export class ShellSideMenuModule {}
import { NgModule } from '@angular/core';
import { ShellSideMenuComponent } from './side-menu.component';
@NgModule({
imports: [ShellSideMenuComponent],
exports: [ShellSideMenuComponent],
})
export class ShellSideMenuModule {}

View File

@@ -10,6 +10,7 @@
@layer components {
@import "../../../libs/ui/buttons/src/buttons.scss";
@import "../../../libs/ui/bullet-list/src/bullet-list.scss";
@import "../../../libs/ui/carousel/src/lib/_carousel.scss";
@import "../../../libs/ui/datepicker/src/datepicker.scss";
@import "../../../libs/ui/dialog/src/dialog.scss";
@import "../../../libs/ui/input-controls/src/input-controls.scss";
@@ -19,6 +20,7 @@
@import "../../../libs/ui/skeleton-loader/src/skeleton-loader.scss";
@import "../../../libs/ui/tooltip/src/tooltip.scss";
@import "../../../libs/ui/label/src/label.scss";
@import "../../../libs/ui/switch/src/switch.scss";
.input-control {
@apply rounded border border-solid border-[#AEB7C1] px-4 py-[1.125rem] outline-none;

View File

@@ -2,9 +2,9 @@ import {
type Meta,
type StoryObj,
applicationConfig,
argsToTemplate,
moduleMetadata,
} from '@storybook/angular';
import { provideHttpClient } from '@angular/common/http';
import { DestinationInfoComponent } from '@isa/checkout/shared/product-info';
import { ShippingTarget } from '@isa/checkout/data-access';
@@ -14,7 +14,7 @@ const meta: Meta<DestinationInfoComponent> = {
component: DestinationInfoComponent,
decorators: [
applicationConfig({
providers: [],
providers: [provideHttpClient()],
}),
moduleMetadata({
imports: [],
@@ -29,6 +29,7 @@ type Story = StoryObj<DestinationInfoComponent>;
export const Delivery: Story = {
args: {
underline: true,
shoppingCartItem: {
availability: {
estimatedDelivery: {
@@ -83,6 +84,12 @@ export const Pickup: Story = {
export const InStore: Story = {
args: {
shoppingCartItem: {
availability: {
estimatedDelivery: {
start: '2024-06-10T00:00:00+02:00',
stop: '2024-06-12T00:00:00+02:00',
},
},
destination: {
data: {
target: ShippingTarget.Branch,

View File

@@ -0,0 +1,141 @@
import { type Meta, type StoryObj, moduleMetadata } from '@storybook/angular';
import { CarouselComponent } from '@isa/ui/carousel';
import { QuoteCardComponent } from './quote-card.component';
// Collection of developer/inspirational quotes
const quotes = [
{ id: 1, text: 'Code is like humor. When you have to explain it, it\'s bad.', author: 'Cory House' },
{ id: 2, text: 'First, solve the problem. Then, write the code.', author: 'John Johnson' },
{ id: 3, text: 'Simplicity is the soul of efficiency.', author: 'Austin Freeman' },
{ id: 4, text: 'Make it work, make it right, make it fast.', author: 'Kent Beck' },
{ id: 5, text: 'Clean code always looks like it was written by someone who cares.', author: 'Robert C. Martin' },
{ id: 6, text: 'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.', author: 'Martin Fowler' },
{ id: 7, text: 'Experience is the name everyone gives to their mistakes.', author: 'Oscar Wilde' },
{ id: 8, text: 'In order to be irreplaceable, one must always be different.', author: 'Coco Chanel' },
{ id: 9, text: 'The best way to predict the future is to invent it.', author: 'Alan Kay' },
{ id: 10, text: 'Programs must be written for people to read, and only incidentally for machines to execute.', author: 'Harold Abelson' },
{ id: 11, text: 'Testing leads to failure, and failure leads to understanding.', author: 'Burt Rutan' },
{ id: 12, text: 'It\'s not a bug it\'s an undocumented feature.', author: 'Anonymous' },
{ id: 13, text: 'Software is a great combination between artistry and engineering.', author: 'Bill Gates' },
{ id: 14, text: 'Talk is cheap. Show me the code.', author: 'Linus Torvalds' },
{ id: 15, text: 'Sometimes it pays to stay in bed on Monday, rather than spending the rest of the week debugging Monday\'s code.', author: 'Dan Salomon' },
];
// Helper function to generate a specified number of quotes
function generateQuotes(count: number) {
const result = [];
for (let i = 0; i < count; i++) {
result.push(quotes[i % quotes.length]);
}
return result;
}
interface CarouselStoryProps {
gap: string;
arrowAutoHide: boolean;
itemCount: number;
}
const meta: Meta<CarouselStoryProps> = {
component: CarouselComponent,
title: 'ui/carousel/Carousel',
decorators: [
moduleMetadata({
imports: [QuoteCardComponent],
}),
],
argTypes: {
gap: {
control: 'text',
description: 'CSS gap value for spacing between carousel items',
},
arrowAutoHide: {
control: 'boolean',
description: 'Whether to auto-hide arrows until carousel is hovered or focused',
},
itemCount: {
control: { type: 'number', min: 3, max: 20 },
description: 'Number of quote cards to render',
},
},
args: {
gap: '1rem',
arrowAutoHide: true,
itemCount: 6,
},
render: (args) => ({
props: {
...args,
quotes: generateQuotes(args.itemCount),
},
template: `
<div style="padding: 2.5rem; background: #f5f5f5;">
<ui-carousel
[gap]="gap"
[arrowAutoHide]="arrowAutoHide"
>
@for (quote of quotes; track quote.id) {
<quote-card [quote]="quote.text" [author]="quote.author"></quote-card>
}
</ui-carousel>
</div>
`,
}),
};
export default meta;
type Story = StoryObj<CarouselStoryProps>;
/**
* Default carousel with 6 quote cards.
* Demonstrates basic horizontal scrolling with auto-hide arrows.
*/
export const Default: Story = {
args: {
itemCount: 6,
},
};
/**
* Carousel with many items (15 cards).
* Shows behavior with extensive scrolling and tests navigation performance.
*/
export const ManyItems: Story = {
args: {
itemCount: 15,
},
};
/**
* Carousel with few items (3 cards).
* Demonstrates behavior when content might not overflow.
* Arrows should disable if not scrollable.
*/
export const FewItems: Story = {
args: {
itemCount: 3,
},
};
/**
* Carousel with persistent arrows (no auto-hide).
* Arrows are always visible when content is scrollable.
*/
export const AlwaysShowArrows: Story = {
args: {
itemCount: 8,
arrowAutoHide: false,
},
};
/**
* Carousel with custom gap spacing (2rem).
* Demonstrates configurable spacing between items.
*/
export const CustomGap: Story = {
args: {
itemCount: 6,
gap: '2rem',
},
};

View File

@@ -0,0 +1,63 @@
import { Component, input, ChangeDetectionStrategy } from '@angular/core';
/**
* Quote card component for Storybook carousel examples.
* Displays a quote with author in a styled card matching the Figma design.
*/
@Component({
selector: 'quote-card',
template: `
<div class="quote-card">
<div class="quote-card__content">
<p class="quote-card__quote isa-text-body-1-regular text-isa-neutral-900">
"{{ quote() }}"
</p>
@if (author()) {
<p class="quote-card__author isa-text-body-2-bold text-isa-neutral-700">
— {{ author() }}
</p>
}
</div>
</div>
`,
styles: [`
.quote-card {
background: white;
border-radius: 16px;
padding: 20px 22px;
min-width: 334px;
max-width: 334px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
transition: box-shadow 0.2s ease;
}
.quote-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.quote-card__content {
display: flex;
flex-direction: column;
gap: 12px;
}
.quote-card__quote {
font-style: italic;
line-height: 1.5;
margin: 0;
}
.quote-card__author {
margin: 0;
font-style: normal;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
})
export class QuoteCardComponent {
quote = input.required<string>();
author = input<string>('');
}

View File

@@ -0,0 +1,104 @@
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
import { IconSwitchComponent, IconSwitchColor } from '@isa/ui/switch';
import { provideIcons } from '@ng-icons/core';
import { isaFiliale, IsaIcons, isaNavigationDashboard } from '@isa/icons';
type IconSwitchComponentInputs = {
icon: string;
checked: boolean;
color: IconSwitchColor;
disabled: boolean;
};
const meta: Meta<IconSwitchComponentInputs> = {
component: IconSwitchComponent,
title: 'ui/switch/IconSwitch',
decorators: [
(story) => ({
...story(),
applicationConfig: {
providers: [provideIcons(IsaIcons)],
},
}),
],
argTypes: {
icon: {
control: { type: 'select' },
options: Object.keys(IsaIcons),
description: 'The name of the icon to display in the switch',
},
checked: {
control: 'boolean',
description: 'Whether the switch is checked (on) or not (off)',
},
color: {
control: { type: 'select' },
options: Object.values(IconSwitchColor),
description: 'Determines the switch color theme',
},
disabled: {
control: 'boolean',
description: 'Disables the switch when true',
},
},
args: {
icon: 'isaFiliale',
checked: false,
color: 'primary',
disabled: false,
},
render: (args) => ({
props: args,
template: `<ui-icon-switch ${argsToTemplate(args)}></ui-icon-switch>`,
}),
};
export default meta;
type Story = StoryObj<IconSwitchComponent>;
export const Default: Story = {
args: {},
};
export const Enabled: Story = {
args: {
checked: true,
},
parameters: {
docs: {
description: {
story:
'The switch in its enabled/checked state with the primary color theme.',
},
},
},
};
export const Disabled: Story = {
args: {
checked: false,
disabled: true,
},
parameters: {
docs: {
description: {
story:
'The switch in a disabled state. User interactions are prevented.',
},
},
},
};
export const EnabledAndDisabled: Story = {
args: {
checked: true,
disabled: true,
},
parameters: {
docs: {
description: {
story: 'The switch in both enabled and disabled states simultaneously.',
},
},
},
};

287
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,287 @@
# ISA-Frontend Architecture Documentation
Complete architectural analysis of the ISA-Frontend monorepo with C4 models, dependency analysis, and implementation patterns.
## Documentation Files
### 1. **Architecture Analysis** (`architecture-analysis.md`) - 51 KB
Comprehensive architectural overview including:
- Complete project structure analysis
- All 63 libraries organized by domain
- Strict layered dependency model
- Technology stack (Angular 20.3.6, Nx 21.3.2, etc.)
- 6 primary domains (OMS, Remission, Checkout, Catalogue, Availability, CRM)
- Core infrastructure (5 libs), UI components (17 libs), shared components (7 libs)
- Modern architecture patterns (standalone components, NgRx Signals, responsive design)
- Complete C4 Model visualization (Levels 1-4)
- State management patterns with code examples
- Component architecture and dependency enforcement
### 2. **Dependency Hierarchy** (`dependency-hierarchy.md`) - 13 KB
Visual dependency organization:
- Layer-based dependency model (4 levels)
- Complete OMS domain dependency tree
- Remission domain dependency tree
- Checkout domain dependency tree
- Cross-domain dependency matrix
- Import path conventions and examples
- NO circular dependencies guarantee
- Bundle dependency impact analysis
- Development workflow for adding features
- Performance considerations (lazy loading, tree shaking)
- Quick reference lookup table
### 3. **Quick Reference** (`architecture-quick-reference.md`) - 15 KB
Developer quick reference guide:
- Project overview at a glance
- Domain summary with key libraries
- Architecture layers visualization
- State management pattern example
- Component structure template
- Common development patterns (search, dialogs, forms, responsive design)
- Essential command cheatsheet
- File organization by domain
- TypeScript path alias mapping
- Design system utilities (Tailwind ISA-specific)
- Testing approach (Vitest vs Jest)
- Troubleshooting guide
- Performance budgets
- Monorepo statistics
---
## Quick Navigation
### For Architecture Understanding
Start with **architecture-analysis.md** → C4 Model section
- Understand the 6 primary domains
- Learn the 4-layer dependency model
- See how all 63 libraries fit together
### For Dependency Details
Read **dependency-hierarchy.md** for:
- How libraries depend on each other
- Where to import from (path aliases)
- Why circular dependencies are prevented
- How to add new features without breaking the graph
### For Hands-On Development
Use **architecture-quick-reference.md** for:
- Quick lookup of library purposes
- Code patterns and examples
- Common commands
- File locations and conventions
- Troubleshooting tips
---
## Project Structure Overview
```
ISA-Frontend (Angular 20.3.6 Monorepo)
├── 63 Libraries organized by domain
│ ├── 6 Primary Domains (OMS, Remission, Checkout, Catalogue, Availability, CRM)
│ ├── 17 UI Component Libraries
│ ├── 7 Shared Component Libraries
│ ├── 5 Core Infrastructure Libraries
│ ├── 3 Common Utility Libraries
│ ├── 3 General Utility Libraries
│ ├── 1 Icon Library
│ └── 10 Auto-generated Swagger API Clients
├── 1 Main Application (isa-app)
└── Strict Layered Architecture (Feature → Shared → Data Access → Infrastructure)
```
---
## Key Architecture Principles
### 1. Domain-Driven Design
- 6 distinct business domains (OMS, Remission, Checkout, etc.)
- Each domain has data-access, feature, and shared components
- Clear domain boundaries prevent unnecessary coupling
### 2. Strict Layering
- **Feature Layer**: Route components, user interaction
- **Shared Layer**: Reusable UI and domain-specific components
- **Data Access Layer**: NgRx Signals stores and API services
- **Infrastructure Layer**: Core, common, and generated APIs
### 3. No Circular Dependencies
- Enforced by TypeScript path aliases
- Verified by ESLint nx plugin
- Enables scalability and maintainability
### 4. Modern Angular Patterns
- **Standalone Components**: All new components (no NgModule)
- **Signal-based State**: NgRx Signals with functional composition
- **Type Safety**: TypeScript strict mode + Zod validation
- **Responsive Design**: Breakpoint service instead of CSS media queries
---
## Statistics
| Metric | Count |
|--------|-------|
| Total Libraries | 63 |
| Primary Domains | 6 |
| UI Components | 17 |
| Feature Components | 20 |
| Data Access Stores | 6 |
| Core Infrastructure | 5 |
| Shared Components | 7 |
| Common Utilities | 3 |
| General Utilities | 3 |
| Generated APIs | 10 |
| Lines of Documentation | 2,165+ |
---
## Technology Stack
- **Framework**: Angular 20.3.6
- **Build Tool**: Nx 21.3.2
- **Language**: TypeScript 5.8.3
- **State Management**: NgRx Signals 20.0.0
- **Styling**: Tailwind CSS 3.4.14 + 7 custom plugins
- **Testing**: Jest (legacy) + Vitest (modern)
- **HTTP Client**: HttpClient 20.3.6
- **Validation**: Zod 3.24.2
- **API Generation**: ng-swagger-gen 2.3.1
- **Authentication**: OAuth2/OIDC via angular-oauth2-oidc
---
## Development Quick Start
```bash
# Install and start
npm install
npm start # Runs on https://localhost:4200
# Testing
npm test # All libraries
npx nx test [project] --skip-nx-cache # Specific library (fresh results)
# Building
npm run build # Development
npm run build-prod # Production
# Regenerate APIs
npm run generate:swagger # From OpenAPI specs
npm run fix:files:swagger # Unicode cleanup
# Analysis
npx nx graph # Visualize dependencies
npx nx show project [project] # Show project details
```
---
## Common Patterns
### Injecting and Using a Store
```typescript
export class MyComponent {
protected store = inject(omsStore);
onSearch(term: string) {
this.store.searchReceipts(term);
}
}
```
### Creating a New Feature
1. Create standalone component in `[domain]/feature/[feature-name]`
2. Import from data-access, shared, and UI libraries via path aliases
3. Inject store directly (don't inject individual services)
4. Use reactive template syntax (@if, @for, @switch)
### Adding E2E Attributes
```html
<button
data-what="submit"
data-which="primary-action"
[attr.data-order-id]="orderId"
>
Submit
</button>
```
---
## Architecture Decisions
### Why NgRx Signals?
- Modern, functional composition
- Signals for reactive properties
- Entity management for normalized state
- Auto-persistence with withStorage()
- Request cancellation support
### Why Standalone Components?
- No NgModule boilerplate
- Explicit dependencies (in imports)
- Tree-shakeable code
- Easier testing
### Why Path Aliases?
- Prevent relative imports across domains
- Enforce architectural boundaries
- Clear import intent (@isa/domain/layer/feature)
- Enable refactoring safety
### Why Strict Layering?
- Prevent circular dependencies
- Enable parallel development
- Facilitate testing
- Support scalability
---
## Getting Help
1. **Library Documentation**: Check `/libs/[domain]/[layer]/[feature]/README.md`
2. **Architecture Details**: See the appropriate documentation file above
3. **Code Examples**: Look for similar implementations in the codebase
4. **Nx Visualization**: Run `npx nx graph` to see dependency graph
5. **Project Configuration**: Review `nx.json` and `tsconfig.base.json`
---
## Related Documentation
- **Library Reference**: See `/docs/library-reference.md` for all 63 libraries
- **Testing Guidelines**: See `/docs/guidelines/testing.md` for testing patterns
- **Code Review Standards**: See `/.github/review-instructions.md` for review process
- **CLAUDE.md**: Project-specific conventions and best practices
---
## Maintenance
These documentation files are maintained manually. When:
- Adding new libraries: Update library reference and domain counts
- Changing architecture patterns: Update quick reference and analysis
- Adding new domains: Document domain structure and dependencies
- Migrating frameworks: Update testing approach sections
---
## Document Generation
To regenerate library reference:
```bash
npm run docs:generate
```
This updates `/docs/library-reference.md` automatically.
---
**Last Updated**: 2025-10-29
**Angular Version**: 20.3.6
**Nx Version**: 21.3.2
**Documentation Format**: Markdown (3 comprehensive files)

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,587 @@
# ISA-Frontend Architecture: Quick Reference Guide
## Project Overview at a Glance
| Aspect | Details |
|--------|---------|
| **Project Type** | Angular 20.3.6 Monorepo (Domain-Driven Design) |
| **Build Tool** | Nx 21.3.2 |
| **Total Libraries** | 63 (organized by domain + infrastructure) |
| **Main Application** | `isa-app` (only runnable app) |
| **Domains** | OMS, Remission, Checkout, Catalogue, Availability, CRM |
| **UI Components** | 17 specialized design system libraries |
| **Testing** | Jest (legacy) + Vitest (modern) - migration in progress |
| **State Management** | NgRx Signals with entity normalization |
| **API Clients** | 10 auto-generated from Swagger/OpenAPI specs |
| **Styling** | Tailwind CSS + 7 custom plugins |
| **Authentication** | OAuth2/OIDC via `angular-oauth2-oidc` |
| **Barcode Support** | Scandit Web Datacapture |
| **Analytics** | Matomo integration |
---
## Domain Summary
### 1. Order Management System (OMS) - 9 Libraries
**Focus:** Return workflows and receipt management
| Library | Purpose |
|---------|---------|
| `oms-data-access` | State + API integration |
| `oms-feature-return-search` | Receipt search interface |
| `oms-feature-return-details` | Item selection & configuration |
| `oms-feature-return-process` | Dynamic return questions |
| `oms-feature-return-summary` | Confirmation & printing |
| `oms-feature-return-review` | Completion review |
| `oms-shared-product-info` | Product display |
| `oms-shared-task-list` | Task management UI |
| `oms-utils-translation` | Receipt type labels |
**Key APIs:** oms-api, isa-api, print-api
---
### 2. Remission (Returns Management) - 8 Libraries
**Focus:** Warehouse return processing (mandatory + department)
| Library | Purpose |
|---------|---------|
| `remission-data-access` | State + API integration |
| `remission-feature-remission-list` | Main list view |
| `remission-feature-remission-return-receipt-list` | Receipt list |
| `remission-feature-remission-return-receipt-details` | Receipt details |
| `remission-shared-product` | Product components |
| `remission-shared-remission-start-dialog` | Start workflow |
| `remission-shared-return-receipt-actions` | Action buttons |
| `remission-shared-search-item-to-remit-dialog` | Item search |
**Key APIs:** Remission-specific via ISA backend
---
### 3. Checkout & Rewards - 6 Libraries
**Focus:** Shopping cart, orders, loyalty rewards
| Library | Purpose |
|---------|---------|
| `checkout-data-access` | Cart state + API |
| `checkout-feature-reward-catalog` | Reward browsing |
| `checkout-feature-reward-shopping-cart` | Cart with rewards |
| `checkout-feature-reward-order-confirmation` | Order confirmation |
| `checkout-shared-product-info` | Product display |
| `checkout-shared-reward-selection-dialog` | Reward selection |
**Key APIs:** checkout-api, crm-api (bonus cards)
---
### 4. Catalogue - 1 Library
**Focus:** Product search and discovery
| Library | Purpose |
|---------|---------|
| `catalogue-data-access` | Search + filtering |
**Key APIs:** cat-search-api, availability-api
---
### 5. Availability - 1 Library
**Focus:** Product stock checking
| Library | Purpose |
|---------|---------|
| `availability-data-access` | Stock queries |
**Key APIs:** availability-api
---
### 6. CRM - 1 Library
**Focus:** Customer data and bonus cards
| Library | Purpose |
|---------|---------|
| `crm-data-access` | Customer + bonus card state |
**Key APIs:** crm-api
---
## Architecture Layers
```
┌─────────────────────────────────┐
│ FEATURE LAYER (User-Facing) │
│ - Components with routes │
│ - User interactions │
│ - Navigation handlers │
└──────────────┬──────────────────┘
│ imports
┌──────────────▼──────────────────┐
│ SHARED LAYER (Reusable) │
│ - UI components (17 libs) │
│ - Shared components (7 libs) │
│ - Domain shared │
└──────────────┬──────────────────┘
│ imports
┌──────────────▼──────────────────┐
│ DATA ACCESS LAYER (State) │
│ - NgRx Signal Stores │
│ - API Services │
│ - Entity management │
└──────────────┬──────────────────┘
│ imports
┌──────────────▼──────────────────┐
│ INFRASTRUCTURE (Foundation) │
│ - Core libraries (5) │
│ - Common utilities (3) │
│ - Generated APIs (10) │
│ - Utilities (3) │
└─────────────────────────────────┘
```
---
## State Management Pattern
### NgRx Signals Store Structure
```typescript
export const orderStore = signalStore(
// 1. State definition
withState({
orders: [] as Order[],
selected: null as Order | null,
loading: false,
error: null as Error | null,
}),
// 2. Entity management (auto-normalization)
withEntities({ entity: type<Order>() }),
// 3. Computed values
withComputed((store) => ({
orderCount: computed(() => store.orders().length),
hasSelected: computed(() => store.selected() !== null),
})),
// 4. Methods for state mutations
withMethods((store, api = inject(OmsApiService)) => ({
load: rxMethod<void>(
pipe(
tapResponse(
(orders) => patchState(store, setAllEntities(orders)),
(error) => handleError(error)
)
)
),
select: (order: Order) => {
patchState(store, { selected: order });
},
})),
// 5. Auto persistence
withStorage({ key: 'orders' }),
// 6. Cleanup hooks
withHooks({
onInit: ({ load }) => load(),
onDestroy: () => console.log('Store destroyed'),
})
);
```
**Key Features:**
- Signals: Reactive properties
- Entity management: Auto-normalized state
- Methods: Encapsulated mutations
- Storage: Automatic persistence
- Hooks: Lifecycle management
---
## Component Structure
### Standalone Component Example
```typescript
@Component({
selector: 'oms-return-search',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
// Shared components
UiSearchBar,
UiButton,
OmsProductInfo,
UiEmptyState,
],
template: `
<div class="container">
<ui-search-bar (search)="onSearch($event)" />
@if (store.receipts(); as receipts) {
@if (receipts.length > 0) {
<oms-product-info
*ngFor="let receipt of receipts"
[receipt]="receipt"
/>
} @else {
<ui-empty-state title="Keine Belege" />
}
} @loading {
<ui-skeleton-loader />
}
</div>
`,
styles: [`...`],
})
export class OmsReturnSearchComponent {
protected store = inject(omsStore);
private api = inject(OmsApiService);
onSearch(term: string) {
this.store.searchReceipts(term);
}
}
```
**Best Practices:**
- ✅ Standalone components only
- ✅ Explicit imports
- ✅ Inject store, not services
- ✅ Use store methods directly
- ✅ Let control flow (@if, @for)
---
## Common Patterns
### 1. Search with Debouncing
```typescript
export class SearchComponent {
private searchTerm$ = new Subject<string>();
results$ = this.searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((term) => this.api.search(term)),
takeUntilKeydown('Escape')
);
onSearch(term: string) {
this.searchTerm$.next(term);
}
}
```
### 2. Modal/Dialog Handling
```typescript
export class DialogComponent {
private dialog = inject(DialogService);
openRewardSelection() {
this.dialog.open(RewardSelectionDialog, {
data: { cart: this.cart },
}).afterClosed$.subscribe((reward) => {
if (reward) {
this.store.selectReward(reward);
}
});
}
}
```
### 3. Form Validation
```typescript
export class ReturnProcessComponent {
form = new FormGroup({
reason: new FormControl('', [Validators.required]),
quantity: new FormControl(1, [Validators.min(1)]),
comments: new FormControl(''),
});
submit() {
if (this.form.valid) {
this.store.submitReturn(this.form.value);
}
}
}
```
### 4. Responsive Design
```typescript
export class ResponsiveComponent {
// Use breakpoint service instead of CSS-only
isDesktop = breakpoint([
Breakpoint.Desktop,
Breakpoint.DesktopL,
Breakpoint.DesktopXL,
]);
template = `
@if (isDesktop()) {
<desktop-layout />
} @else {
<mobile-layout />
}
`;
}
```
---
## Common Commands
### Development
```bash
npm start # Start with SSL
npm test # Test all libraries
npm run build # Dev build
npm run build-prod # Production build
```
### Testing
```bash
npx nx test oms-data-access --skip-nx-cache
npx nx affected:test --skip-nx-cache
npm run ci # CI with coverage
```
### Code Quality
```bash
npm run lint # ESLint
npm run prettier # Format code
npm run docs:generate # Update library ref
```
### API & Swagger
```bash
npm run generate:swagger # Regenerate all APIs
npm run fix:files:swagger # Unicode cleanup
```
### Dependency Analysis
```bash
npx nx graph # Visual dependency graph
npx nx show project oms-data-access --web false
npx nx affected:lint --skip-nx-cache
```
---
## File Organization by Domain
### OMS Domain Structure
```
libs/oms/
├── data-access/
│ └── src/
│ ├── index.ts
│ ├── stores/
│ │ ├── receipt.store.ts
│ │ └── return.store.ts
│ └── services/
│ ├── oms-api.service.ts
│ └── print.service.ts
├── feature/
│ ├── return-search/
│ ├── return-details/
│ ├── return-process/
│ ├── return-summary/
│ └── return-review/
├── shared/
│ ├── product-info/
│ └── task-list/
└── utils/
└── translation/
```
### UI Component Structure
```
libs/ui/buttons/
├── src/
│ ├── index.ts
│ ├── primary-button.component.ts
│ ├── secondary-button.component.ts
│ ├── ...
│ └── buttons.module.ts
├── README.md
├── project.json
└── ...
```
---
## TypeScript Path Aliases
```json
{
"paths": {
// Domain data-access
"@isa/oms/data-access": ["libs/oms/data-access/src/index.ts"],
"@isa/remission/data-access": ["libs/remission/data-access/src/index.ts"],
// UI components
"@isa/ui/buttons": ["libs/ui/buttons/src/index.ts"],
"@isa/ui/dialog": ["libs/ui/dialog/src/index.ts"],
// Core infrastructure
"@isa/core/logging": ["libs/core/logging/src/index.ts"],
"@isa/core/storage": ["libs/core/storage/src/index.ts"],
// Generated APIs
"@generated/swagger/oms-api": ["generated/swagger/oms-api/src/index.ts"]
}
}
```
---
## Styling & Design System
### Tailwind Utilities (ISA-Specific)
```html
<!-- Brand Colors -->
<div class="text-isa-accent-primary">Primary text</div>
<button class="bg-isa-accent-primary">Primary button</button>
<!-- Typography -->
<h1 class="isa-text-heading-1-bold">Large heading</h1>
<p class="isa-text-body-2-regular">Body text</p>
<!-- Custom Breakpoints -->
<div class="hidden isa-desktop:block">Desktop only</div>
<div class="block isa-desktop:hidden">Mobile only</div>
<!-- Custom Plugins -->
<button class="isa-button-primary">ISA Button</button>
<div class="isa-input-group">...</div>
```
### Custom Tailwind Plugins
1. button - Button styling
2. typography - Text utilities
3. menu - Menu styling
4. label - Label & tag styling
5. input - Input styling
6. section - Section containers
7. select-bullet - Select styling
---
## Testing Approach
### New Libraries (Vitest + Angular Testing Utils)
```typescript
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { OmsReturnSearchComponent } from './oms-return-search.component';
describe('OmsReturnSearchComponent', () => {
let component: OmsReturnSearchComponent;
let fixture: ComponentFixture<OmsReturnSearchComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [OmsReturnSearchComponent],
}).compileComponents();
fixture = TestBed.createComponent(OmsReturnSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
```
### E2E Attributes
All templates must include data attributes:
```html
<button
data-what="submit-return"
data-which="primary-action"
[attr.data-order-id]="orderId"
>
Submit Return
</button>
```
---
## Troubleshooting
| Issue | Solution |
|-------|----------|
| **Build cache stale** | `npx nx reset` or `--skip-nx-cache` |
| **Test failures** | Always use `--skip-nx-cache` |
| **Import not found** | Check `tsconfig.base.json` path alias |
| **Circular dependency** | Run `npx nx lint` to identify |
| **SSL certificate error** | Accept localhost certificate in browser |
| **State not persisting** | Check `withStorage()` in store |
| **API 401 Unauthorized** | Verify OAuth2 token in auth service |
---
## Quick Links
- **Library Reference:** `/docs/library-reference.md`
- **Architecture Analysis:** `/docs/architecture-analysis.md`
- **Dependency Hierarchy:** `/docs/dependency-hierarchy.md`
- **Testing Guidelines:** `/docs/guidelines/testing.md`
- **Nx Documentation:** https://nx.dev/
- **Angular Documentation:** https://angular.io/
- **NgRx Signals:** https://ngrx.io/guide/signals
---
## Getting Help
1. Check library README: `libs/[domain]/[layer]/[feature]/README.md`
2. Review existing examples in similar domains
3. Check `npx nx show project [project-name]`
4. Read CLAUDE.md for project-specific conventions
5. Review git history: `git log --oneline libs/[domain]`
---
## Performance Budgets
- **Main bundle:** 2MB warning, 5MB error (gzipped)
- **Initial load:** < 2s on 4G
- **Core (after auth):** < 5s
**Bundle Analysis:**
```bash
npx nx build isa-app --configuration=production --stats-json
webpack-bundle-analyzer dist/isa-app/browser/stats.json
```
---
## Monorepo Statistics
| Metric | Count |
|--------|-------|
| Total Libraries | 63 |
| Feature Components | 20 |
| UI Components | 17 |
| Data Access | 6 |
| Core Infrastructure | 5 |
| Shared Components | 7 |
| Utilities | 3 |
| Generated APIs | 10 |
| Lines of Code | ~500K+ |
| TypeScript Files | ~1,500 |
| Test Files | ~400 |
| Generated Test Coverage | Vitest: 34%, Jest: 65% |

View File

@@ -6,7 +6,7 @@
| Date | 29.09.2025 |
| Owners | Lorenz, Nino |
| Participants | N/A |
| Related ADRs | N/A |
| Related ADRs | [ADR-0002](./0002-models-schemas-dtos-architecture.md) |
| Tags | architecture, data-access, library, swagger |
---
@@ -35,9 +35,11 @@ Implement a **three-layer architecture** for all data-access libraries:
- Pattern: `<Operation>Schema` with `<Operation>` and `<Operation>Input` types
- Example: `SearchItemsSchema`, `SearchItems`, `SearchItemsInput`
2. **Model Layer** (`models/`): Domain-specific interfaces extending generated DTOs
- Pattern: `interface MyModel extends GeneratedDTO { ... }`
2. **Model Layer** (`models/`): Domain-specific types based on generated DTOs
- **Simple re-export pattern** (default): `export type Product = ProductDTO;`
- **Extension pattern** (when domain enhancements needed): `interface MyModel extends GeneratedDTO { ... }`
- Use `EntityContainer<T>` for lazy-loaded relationships
- **Rule**: Generated DTOs MUST NOT be imported outside data-access libraries (see ADR-0002)
3. **Service Layer** (`services/`): Injectable services integrating Swagger clients
- Pattern: Async methods with AbortSignal support
@@ -79,9 +81,12 @@ export * from './lib/services';
## Detailed Design Elements
### Schema Validation Pattern
**Structure:**
**Two schema patterns coexist:**
**Pattern A: Operation-based schemas** (for query/search operations)
```typescript
// Input validation schema
// Input validation schema for search operation
export const SearchByTermSchema = z.object({
searchTerm: z.string().min(1, 'Search term must not be empty'),
skip: z.number().int().min(0).default(0),
@@ -93,27 +98,67 @@ export type SearchByTerm = z.infer<typeof SearchByTermSchema>;
export type SearchByTermInput = z.input<typeof SearchByTermSchema>;
```
### Model Extension Pattern
**Generated DTO Extension:**
**Pattern B: Entity-based schemas** (for CRUD operations, see ADR-0002)
```typescript
// Full entity schema defining all fields
export const ProductSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(255),
contributor: z.string().optional(),
price: z.number().positive().optional(),
// ... all fields
});
// Derived validation schemas for specific operations
export const CreateProductSchema = ProductSchema.pick({ name: true });
export const UpdateProductSchema = ProductSchema.pick({ id: true, name: true }).required();
// Validate requests only, not responses
```
### Model Pattern
**Pattern A: Simple Re-export** (default, recommended - see ADR-0002)
```typescript
import { ProductDTO } from '@generated/swagger/catalogue-api';
/**
* Product model for catalogue domain.
* Simple re-export of generated DTO.
*/
export type Product = ProductDTO;
```
**Pattern B: Extension** (when domain-specific enhancements needed)
```typescript
import { ProductDTO } from '@generated/swagger/cat-search-api';
/**
* Enhanced product with computed/derived fields.
*/
export interface Product extends ProductDTO {
name: string;
contributors: string;
catalogProductNumber: string;
// Domain-specific enhancements
// Domain-specific computed fields
displayName: string;
formattedPrice: string;
}
```
**Entity Container Pattern:**
**Entity Container Pattern** (for lazy-loaded relationships)
```typescript
import { ReturnDTO } from '@generated/swagger/remission-api';
import { EntityContainer } from '@isa/common/data-access';
export interface Return extends ReturnDTO {
id: number;
receipts: EntityContainer<Receipt>[]; // Lazy-loaded relationships
}
```
**Important:** Generated DTOs (`@generated/swagger/*`) MUST NOT be imported directly in feature/UI libraries. Always import models from data-access.
### Service Implementation Pattern
**Standard service structure:**
```typescript
@@ -323,6 +368,7 @@ When and how this ADR will be re-evaluated (date, trigger conditions, metrics th
## Status Log
| Date | Change | Author |
|------|--------|--------|
| 2025-11-03 | Updated model/schema patterns to align with ADR-0002, added entity-based schemas, clarified DTO encapsulation | System |
| 2025-10-02 | Condensed for readability | Lorenz, Nino |
| 2025-09-29 | Created (Draft) | Lorenz |
| 2025-09-25 | Analysis completed, comprehensive patterns documented | Lorenz, Nino |
@@ -340,6 +386,9 @@ When and how this ADR will be re-evaluated (date, trigger conditions, metrics th
- `@isa/core/logging` - Structured logging infrastructure
- `@isa/common/data-access` - Shared utilities and types
**Related ADRs:**
- [ADR-0002: Models, Schemas, and DTOs Architecture](./0002-models-schemas-dtos-architecture.md) - Detailed guidance on model patterns, DTO encapsulation, and validation strategies
**Related Documentation:**
- ISA Frontend Copilot Instructions - Data-access patterns
- Tech Stack Documentation - Architecture overview

View File

@@ -0,0 +1,854 @@
# ADR 0002: Models, Schemas, and DTOs Architecture
| Field | Value |
|-------|-------|
| Status | Draft |
| Date | 2025-11-03 |
| Owners | TBD |
| Participants | TBD |
| Related ADRs | [ADR-0001](./0001-implement-data-access-api-requests.md) |
| Tags | architecture, data-access, models, schemas, dto, validation |
---
## Summary (Decision in One Sentence)
Encapsulate all generated Swagger DTOs within data-access libraries, use domain-specific models even when names collide, define full Zod schemas with partial validation at service-level for request data only, and export identical cross-domain models from common/data-access.
## Context & Problem Statement
**Current Issues:**
- Generated DTOs (`@generated/swagger/*`) directly imported in 50+ feature/UI files
- Same interface names (e.g., `PayerDTO`, `BranchDTO`) with different properties across 10 APIs
- Union type workarounds (`Product = CatProductDTO | CheckoutProductDTO | OmsProductDTO`) lose type safety
- Inconsistent Zod schema coverage - some types validated, others not
- Type compatibility issues between models and schemas with identical interfaces
- Component-local type redefinitions instead of shared models
- No clear pattern for partial validation (validate some fields, send all data)
**Example Conflicts:**
```typescript
// checkout-api: Minimal PayerDTO (3 properties)
export interface PayerDTO {
payerNumber?: string;
payerStatus?: PayerStatus;
payerType?: PayerType;
}
// crm-api: Full PayerDTO (17 properties)
export interface PayerDTO {
payerNumber?: string;
address?: AddressDTO;
communicationDetails?: CommunicationDetailsDTO;
// ... 14 more fields
}
// Feature components use aliasing as workaround
import { PayerDTO as CheckoutPayer } from '@generated/swagger/checkout-api';
import { PayerDTO as CrmPayer } from '@generated/swagger/crm-api';
```
**Goals:**
- Encapsulate generated code as implementation detail
- Eliminate type name conflicts across domains
- Standardize validation patterns
- Support partial validation while sending complete data
- Improve type safety and developer experience
**Constraints:**
- Must integrate with 10 existing Swagger generated clients
- Cannot break existing feature/UI components
- Must support domain-driven architecture
- Validation overhead must remain minimal
**Scope:**
- Model definitions and exports
- Schema architecture and validation strategy
- DTO encapsulation boundaries
- Common vs domain-specific type organization
## Decision
Implement a **four-layer type architecture** for all data-access libraries:
### 1. Generated Layer (Hidden)
- **Location:** `/generated/swagger/[api-name]/`
- **Visibility:** NEVER imported outside data-access libraries
- **Purpose:** Implementation detail, source of truth from backend
### 2. Model Layer (Public API)
- **Location:** `libs/[domain]/data-access/src/lib/models/`
- **Pattern:** Type aliases re-exporting generated DTOs
- **Naming:** Use domain context (e.g., `Product` in both catalogue and checkout)
- **Rule:** Each domain has its own models, even if names collide across domains
### 3. Schema Layer (Validation)
- **Location:** `libs/[domain]/data-access/src/lib/schemas/`
- **Pattern:** Full Zod schemas defining ALL fields
- **Validation:** Derive partial validation schemas using `.pick()` or `.partial()`
- **Purpose:** Runtime validation + type inference
### 4. Common Layer (Shared Types)
- **Location:** `libs/common/data-access/src/lib/models/` and `schemas/`
- **Rule:** Only for models **identical across all APIs** (same name, properties, types, optionality)
- **Examples:** `EntityStatus`, `NotificationChannel` (if truly identical)
### Validation Strategy
1. **Request Validation Only:** Validate data BEFORE sending to backend
2. **Service-Level Validation:** Perform validation in service methods
3. **Partial Validation:** Validate only required fields, send all data
4. **Full Schema Definition:** Define complete schemas even when partial validation used
5. **No Response Validation:** Trust backend responses without validation
### Export Rules
```typescript
// ✅ Data-access exports
export * from './lib/models'; // Type aliases over DTOs
export * from './lib/schemas'; // Zod schemas
export * from './lib/services'; // Business logic
// ❌ NEVER export generated code
// export * from '@generated/swagger/catalogue-api';
```
## Rationale
**Why Encapsulate Generated DTOs:**
- **Single Responsibility:** Data-access owns API integration details
- **Change Isolation:** API changes don't ripple through feature layers
- **Clear Boundaries:** Domain logic separated from transport layer
- **Migration Safety:** Can swap generated clients without breaking features
**Why Domain-Specific Models (Not Shared):**
- **Type Safety:** Each domain gets exact DTO shape from its API
- **No Name Conflicts:** `Payer` in checkout vs CRM have different meanings
- **Semantic Clarity:** Same name doesn't mean same concept across domains
- **Avoids Union Types:** Union types lose specificity and auto-completion
**Why Full Schemas with Partial Validation:**
- **Documentation:** Full schema serves as reference for all available fields
- **Flexibility:** Can derive different validation schemas (create vs update vs patch)
- **Type Safety:** `z.infer` provides complete type information
- **Reusability:** Pick different fields for different operations
- **Future-Proof:** New validations can be added without schema rewrites
**Why Service-Level Validation:**
- **Centralized Logic:** All API calls validated consistently
- **Early Failure:** Errors caught before network requests
- **Logged Context:** Validation failures logged with structured data
- **User Feedback:** Services can map validation errors to user messages
**Why No Response Validation:**
- **Performance:** No overhead on every API response
- **Backend Trust:** Backend is source of truth, already validated
- **Simpler Code:** Less boilerplate in services
- **Faster Development:** Focus on request contract, not response parsing
**Evidence Supporting Decision:**
- Analysis shows 50+ files importing generated DTOs (architecture violation)
- `BranchDTO` exists in 7 APIs with subtle differences
- Existing ADR-0001 establishes service patterns this extends
- Current union type patterns (`Product = A | B | C`) cause type narrowing issues
## Consequences
### Positive
- **Clear Architecture:** Generated code hidden behind stable public API
- **No Name Conflicts:** Domain models isolated by library boundaries
- **Type Safety:** Each domain gets precise types from its API
- **Validation Consistency:** All requests validated, responses trusted
- **Developer Experience:** Auto-completion works, no aliasing needed
- **Maintainability:** API changes isolated to data-access layer
- **Performance:** Minimal validation overhead, no response parsing
### Negative
- **Migration Effort:** 50+ files need import updates
- **Learning Curve:** Team must understand model vs DTO distinction
- **Schema Maintenance:** Every model needs corresponding full schema
- **Potential Duplication:** Similar models across domains (by design)
- **Validation Cost:** ~1-2ms overhead per validated request
### Neutral
- **Code Volume:** More files (models + schemas) but better organized
- **Common Models Rare:** Most types will be domain-specific, not common
### Risks & Mitigation
- **Risk:** Developers might accidentally import generated DTOs
- **Mitigation:** ESLint rule to prevent `@generated/swagger/*` imports outside data-access
- **Risk:** Unclear when model should be common vs domain-specific
- **Mitigation:** Decision tree in documentation (see below)
- **Risk:** Partial validation might miss critical fields
- **Mitigation:** Code review focus on validation schemas, tests for edge cases
## Detailed Design Elements
### Decision Tree: Where Should a Model Live?
```
┌─────────────────────────────────────────────────────────────┐
│ Is the DTO IDENTICAL in all generated APIs? │
│ (same name, properties, types, optional/required status) │
└──┬───────────────────────────────────────────────┬──────────┘
│ YES │ NO
│ │
▼ ▼
┌──────────────────────────────┐ ┌───────────────────────────────┐
│ libs/common/data-access/ │ │ Is it used in multiple │
│ models/[type].ts │ │ domains? │
│ │ └───┬───────────────────────┬───┘
│ Export once, import in all │ │ YES │ NO
│ domain data-access libs │ │ │
└──────────────────────────────┘ ▼ ▼
┌──────────────────┐ ┌─────────────────┐
│ Create separate │ │ Single domain's │
│ model in EACH │ │ data-access │
│ domain's │ │ library │
│ data-access │ └─────────────────┘
└──────────────────┘
Example: Product exists in
catalogue, checkout, oms
with different shapes
```
### Pattern 1: Domain-Specific Model (Most Common)
```typescript
// libs/catalogue/data-access/src/lib/models/product.ts
import { ProductDTO } from '@generated/swagger/catalogue-api';
/**
* Product model for catalogue domain.
*
* Represents a product in the product catalogue with full details
* including pricing, availability, and metadata.
*/
export type Product = ProductDTO;
```
```typescript
// libs/catalogue/data-access/src/lib/schemas/product.schema.ts
import { z } from 'zod';
/**
* Full Zod schema for Product entity.
* Defines all fields available in the Product model.
*/
export const ProductSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(255),
contributor: z.string().optional(),
price: z.number().positive().optional(),
description: z.string().max(2000).optional(),
categoryId: z.string().optional(),
stockQuantity: z.number().int().nonnegative().optional(),
imageUrl: z.string().url().optional(),
isActive: z.boolean().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
});
/**
* Validation schema for creating a product.
* Validates only required fields: name must be present and valid.
* Other fields are sent to API but not validated.
*/
export const CreateProductSchema = ProductSchema.pick({
name: true,
});
/**
* Validation schema for updating a product.
* Requires id and name, other fields optional.
*/
export const UpdateProductSchema = ProductSchema.pick({
id: true,
name: true,
}).required();
/**
* Inferred types from schemas
*/
export type Product = z.infer<typeof ProductSchema>;
export type CreateProductInput = z.input<typeof CreateProductSchema>;
export type UpdateProductInput = z.input<typeof UpdateProductSchema>;
```
```typescript
// libs/catalogue/data-access/src/lib/services/products.service.ts
import { inject, Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { ProductService, ProductDTO } from '@generated/swagger/catalogue-api';
import { CreateProductSchema, UpdateProductSchema } from '../schemas';
import { logger } from '@isa/core/logging';
@Injectable({ providedIn: 'root' })
export class ProductsService {
readonly #log = logger(ProductsService);
readonly #productService = inject(ProductService);
/**
* Creates a new product.
* Validates required fields before sending to API.
* Sends all product data (validated and unvalidated fields).
*/
async createProduct(product: ProductDTO): Promise<ProductDTO | null> {
// Validate only required fields (name)
const validationResult = CreateProductSchema.safeParse(product);
if (!validationResult.success) {
this.#log.error('Product validation failed', {
errors: validationResult.error.format(),
product
});
throw new Error(
`Invalid product data: ${validationResult.error.message}`
);
}
this.#log.debug('Creating product', { name: product.name });
// Send ALL product data to API (including unvalidated fields)
const response = await firstValueFrom(
this.#productService.createProduct(product)
);
// No response validation - trust backend
if (response.error) {
this.#log.error('Failed to create product', {
error: response.message
});
throw new Error(response.message);
}
return response.result ?? null;
}
/**
* Updates an existing product.
* Validates id and name are present.
*/
async updateProduct(
id: string,
product: ProductDTO
): Promise<ProductDTO | null> {
// Validate required fields for update (id + name)
const validationResult = UpdateProductSchema.safeParse({ id, ...product });
if (!validationResult.success) {
this.#log.error('Product update validation failed', {
errors: validationResult.error.format()
});
throw new Error(
`Invalid product update: ${validationResult.error.message}`
);
}
this.#log.debug('Updating product', { id, name: product.name });
const response = await firstValueFrom(
this.#productService.updateProduct(id, product)
);
if (response.error) {
this.#log.error('Failed to update product', {
error: response.message,
id
});
throw new Error(response.message);
}
return response.result ?? null;
}
/**
* Fetches a product by ID.
* No validation needed for GET requests.
*/
async getProduct(id: string): Promise<ProductDTO | null> {
this.#log.debug('Fetching product', { id });
const response = await firstValueFrom(
this.#productService.getProduct(id)
);
if (response.error) {
this.#log.error('Failed to fetch product', {
error: response.message,
id
});
throw new Error(response.message);
}
// No response validation
return response.result ?? null;
}
}
```
### Pattern 2: Common Model (Identical Across All APIs)
```typescript
// libs/common/data-access/src/lib/models/notification-channel.ts
import { NotificationChannel as CheckoutNotificationChannel } from '@generated/swagger/checkout-api';
import { NotificationChannel as CrmNotificationChannel } from '@generated/swagger/crm-api';
import { NotificationChannel as OmsNotificationChannel } from '@generated/swagger/oms-api';
/**
* NotificationChannel is identical across all APIs.
*
* Verification:
* - checkout-api: type NotificationChannel = 0 | 1 | 2 | 4
* - crm-api: type NotificationChannel = 0 | 1 | 2 | 4
* - oms-api: type NotificationChannel = 0 | 1 | 2 | 4
*
* All three definitions are identical, so we export once from common.
*/
export type NotificationChannel =
| CheckoutNotificationChannel
| CrmNotificationChannel
| OmsNotificationChannel;
// Alternative if truly identical (pick one as canonical):
// export type NotificationChannel = CheckoutNotificationChannel;
```
```typescript
// libs/common/data-access/src/lib/schemas/notification-channel.schema.ts
import { z } from 'zod';
/**
* Zod schema for NotificationChannel enum.
*
* Values:
* - 0: Email
* - 1: SMS
* - 2: Push Notification
* - 4: Phone Call
*/
export const NotificationChannelSchema = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(4),
]);
export type NotificationChannel = z.infer<typeof NotificationChannelSchema>;
```
```typescript
// Domain data-access libs re-export from common
// libs/checkout/data-access/src/lib/models/index.ts
export { NotificationChannel } from '@isa/common/data-access';
// libs/crm/data-access/src/lib/schemas/index.ts
export { NotificationChannelSchema } from '@isa/common/data-access';
```
### Pattern 3: Multiple Domain Models (Same Name, Different Structure)
When DTOs with the same name have different structures, keep them separate:
```typescript
// libs/checkout/data-access/src/lib/models/payer.ts
import { PayerDTO } from '@generated/swagger/checkout-api';
/**
* Payer model for checkout domain.
*
* Minimal payer information needed during checkout flow.
* Contains only basic identification and status.
*/
export type Payer = PayerDTO;
```
```typescript
// libs/crm/data-access/src/lib/models/payer.ts
import { PayerDTO } from '@generated/swagger/crm-api';
/**
* Payer model for CRM domain.
*
* Full payer entity with complete address, organization,
* communication details, and payment settings.
* Used for payer management and administration.
*/
export type Payer = PayerDTO;
```
**Components import from their respective domain:**
```typescript
// libs/checkout/feature/cart/src/lib/cart.component.ts
import { Payer } from '@isa/checkout/data-access'; // 3-field version
// libs/crm/feature/payers/src/lib/payer-details.component.ts
import { Payer } from '@isa/crm/data-access'; // 17-field version
```
### Pattern 4: Partial Validation with Full Schema
**Scenario:** Product has many fields, but only `name` is required for creation.
```typescript
// Full schema defines ALL fields
export const ProductSchema = z.object({
id: z.string().optional(),
name: z.string().min(1).max(255),
contributor: z.string().optional(),
price: z.number().positive().optional(),
description: z.string().optional(),
categoryId: z.string().optional(),
stockQuantity: z.number().int().nonnegative().optional(),
imageUrl: z.string().url().optional(),
isActive: z.boolean().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
// ... potentially 20+ more fields
});
// Validation schema picks only required field
export const CreateProductSchema = ProductSchema.pick({
name: true,
});
// Service validates partial, sends complete
async createProduct(product: ProductDTO): Promise<ProductDTO | null> {
// Validate: only name is checked
CreateProductSchema.parse(product);
// Send: all fields (name, contributor, price, description, etc.)
const response = await this.api.createProduct(product);
return response.result;
}
```
**Why not `.passthrough()`?**
```typescript
// ❌ Avoid this approach
const CreateProductSchema = z.object({
name: z.string().min(1),
}).passthrough();
// Type inference loses other fields
type Inferred = z.infer<typeof CreateProductSchema>;
// Result: { name: string } & { [key: string]: unknown }
// Lost: contributor, price, description types
// ✅ Prefer this approach
const CreateProductSchema = ProductSchema.pick({ name: true });
// Full ProductSchema defined elsewhere provides complete type
type Product = z.infer<typeof ProductSchema>;
// Result: { id?: string; name: string; contributor?: string; ... }
```
### Export Structure
```typescript
// libs/catalogue/data-access/src/index.ts
// Public API exports
export * from './lib/models'; // Type aliases over DTOs
export * from './lib/schemas'; // Zod schemas
export * from './lib/services'; // Business logic
export * from './lib/resources'; // Angular resources (optional)
export * from './lib/stores'; // State management (optional)
export * from './lib/helpers'; // Utilities (optional)
// ❌ NEVER export generated code
// This would break encapsulation:
// export * from '@generated/swagger/catalogue-api';
```
```typescript
// libs/catalogue/data-access/src/lib/models/index.ts
// Re-export all domain models
export * from './product';
export * from './category';
export * from './supplier';
export * from './inventory';
// May also re-export common models
export { EntityStatus, NotificationChannel } from '@isa/common/data-access';
```
```typescript
// libs/catalogue/data-access/src/lib/schemas/index.ts
// Re-export all domain schemas
export * from './product.schema';
export * from './category.schema';
export * from './supplier.schema';
export * from './inventory.schema';
// May also re-export common schemas
export { EntityStatusSchema } from '@isa/common/data-access';
```
## Code Examples
### Complete Example: Order in OMS Domain
```typescript
// libs/oms/data-access/src/lib/models/order.ts
import { OrderDTO } from '@generated/swagger/oms-api';
export type Order = OrderDTO;
```
```typescript
// libs/oms/data-access/src/lib/schemas/order.schema.ts
import { z } from 'zod';
import { OrderItemSchema } from './order-item.schema';
import { OrderStatusSchema } from '@isa/common/data-access';
export const OrderSchema = z.object({
id: z.string().optional(),
orderNumber: z.string().min(1),
customerId: z.string().uuid(),
items: z.array(OrderItemSchema).min(1),
status: OrderStatusSchema.optional(),
totalAmount: z.number().nonnegative().optional(),
shippingAddress: z.string().optional(),
billingAddress: z.string().optional(),
createdAt: z.string().datetime().optional(),
updatedAt: z.string().datetime().optional(),
});
export const CreateOrderSchema = OrderSchema.pick({
customerId: true,
items: true,
});
export const UpdateOrderStatusSchema = OrderSchema.pick({
id: true,
status: true,
}).required();
export type Order = z.infer<typeof OrderSchema>;
export type CreateOrderInput = z.input<typeof CreateOrderSchema>;
export type UpdateOrderStatusInput = z.input<typeof UpdateOrderStatusSchema>;
```
```typescript
// libs/oms/data-access/src/lib/services/orders.service.ts
import { inject, Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { OrderService, OrderDTO } from '@generated/swagger/oms-api';
import { CreateOrderSchema, UpdateOrderStatusSchema } from '../schemas';
import { logger } from '@isa/core/logging';
@Injectable({ providedIn: 'root' })
export class OrdersService {
readonly #log = logger(OrdersService);
readonly #orderService = inject(OrderService);
async createOrder(order: OrderDTO): Promise<OrderDTO | null> {
const validationResult = CreateOrderSchema.safeParse(order);
if (!validationResult.success) {
this.#log.error('Order validation failed', {
errors: validationResult.error.format()
});
throw new Error(
`Invalid order: ${validationResult.error.message}`
);
}
this.#log.debug('Creating order', {
customerId: order.customerId,
itemCount: order.items?.length
});
const response = await firstValueFrom(
this.#orderService.createOrder(order)
);
if (response.error) {
this.#log.error('Failed to create order', {
error: response.message
});
throw new Error(response.message);
}
return response.result ?? null;
}
async updateOrderStatus(
id: string,
status: string
): Promise<OrderDTO | null> {
UpdateOrderStatusSchema.parse({ id, status });
this.#log.debug('Updating order status', { id, status });
const response = await firstValueFrom(
this.#orderService.updateOrderStatus(id, status)
);
if (response.error) {
this.#log.error('Failed to update order status', {
error: response.message,
id
});
throw new Error(response.message);
}
return response.result ?? null;
}
}
```
### Usage in Feature Component
```typescript
// libs/oms/feature/orders/src/lib/create-order.component.ts
import { Component, inject, signal } from '@angular/core';
import { OrdersService, Order, CreateOrderInput } from '@isa/oms/data-access';
@Component({
selector: 'app-create-order',
template: `
<form (ngSubmit)="submit()">
<!-- Form fields -->
@if (error()) {
<div class="error">{{ error() }}</div>
}
<button type="submit">Create Order</button>
</form>
`
})
export class CreateOrderComponent {
readonly #ordersService = inject(OrdersService);
error = signal<string | null>(null);
async submit() {
try {
// Build order data
const orderInput: CreateOrderInput = {
customerId: this.customerId,
items: this.items,
// Optional fields can be included
shippingAddress: this.shippingAddress,
billingAddress: this.billingAddress,
};
// Service validates required fields, sends all data
const created = await this.#ordersService.createOrder(orderInput);
// Navigate to order details...
} catch (err) {
this.error.set(err instanceof Error ? err.message : 'Failed to create order');
}
}
}
```
## Migration Strategy
### Phase 1: New Development (Immediate)
- All new models follow this ADR
- All new schemas use full definition + partial validation
- All new services validate requests only
### Phase 2: Incremental Migration (Ongoing)
- When touching existing code, update imports
- Replace generated DTO imports with data-access model imports
- Add validation schemas for existing services
### Phase 3: Cleanup (Future)
- Add ESLint rule preventing `@generated/swagger/*` imports outside data-access
- Automated codemod to fix remaining violations
- Remove union type workarounds
### Migration Checklist (Per Domain)
- [ ] Create `models/` folder with type aliases over generated DTOs
- [ ] Create `schemas/` folder with full Zod schemas
- [ ] Add partial validation schemas (`.pick()` for required fields)
- [ ] Update services to validate before API calls
- [ ] Export models and schemas from data-access index
- [ ] Update feature components to import from data-access
- [ ] Remove direct `@generated/swagger/*` imports
- [ ] Verify no union types for different shapes
- [ ] Move truly identical models to common/data-access
## Open Questions / Follow-Ups
### For Team Discussion
1. **ESLint Rule Priority:** Should we add the ESLint rule immediately or after migration?
- Immediate: Prevents new violations
- After migration: Less friction during transition
2. **Validation Error Handling:** How should services communicate validation errors to UI?
- Throw generic Error (current approach)
- Custom ValidationError class with structured field errors
- Return Result<T, E> pattern instead of throwing
3. **Common Model Criteria:** Should we require 100% identical or allow minor differences?
- Strict: Must be byte-for-byte identical
- Lenient: Same semantic meaning, slight type differences OK
4. **Schema Generation:** Should we auto-generate Zod schemas from Swagger specs?
- Pro: Less manual work, stays in sync
- Con: Generated schemas might not match domain needs
5. **Response Validation:** Any exceptions where we SHOULD validate responses?
- Critical paths (payments, checkout)?
- External APIs (not our backend)?
### Dependent Decisions
- [ ] Define custom ValidationError class structure
- [ ] Decide on ESLint rule configuration
- [ ] Document common model approval process
- [ ] Create code generation tooling (if desired)
## Decision Review & Revalidation
**Review Triggers:**
- After 3 months of adoption (2025-02-03)
- When migration >50% complete
- If validation overhead becomes measurable performance issue
- If new backend API patterns emerge
**Success Metrics:**
- Zero `@generated/swagger/*` imports outside data-access (ESLint violations)
- 100% of services have request validation
- <5% of models in common/data-access (most are domain-specific)
- Developer survey shows improved clarity (>80% satisfaction)
**Failure Criteria (Revert Decision):**
- Validation overhead >10ms per request (current: ~1-2ms)
- Common models >30% (suggests wrong criteria)
- Excessive developer friction (>50% negative feedback)
## Status Log
| Date | Change | Author |
|------|--------|--------|
| 2025-11-03 | Created (Draft) | TBD |
## References
**Related ADRs:**
- [ADR-0001: Implement data-access API Requests](./0001-implement-data-access-api-requests.md) - Establishes service patterns this extends
**Existing Codebase:**
- `/generated/swagger/` - 10 generated API clients
- `libs/*/data-access/` - 7 existing data-access libraries
- `libs/common/data-access/` - Shared types and utilities
**External Documentation:**
- [Zod Documentation](https://zod.dev/) - Schema validation library
- [ng-swagger-gen](https://github.com/cyclosproject/ng-swagger-gen) - OpenAPI client generator
**Migration Resources:**
- Comprehensive guide: `/docs/architecture/models-schemas-dtos-guide.md`
- Example implementations in catalogue, oms, crm data-access libraries
---
> Document updates MUST reference this ADR number in commit messages: `ADR-0002:` prefix.
> Keep this document updated through all lifecycle stages.

View File

@@ -0,0 +1,458 @@
# ISA-Frontend: Dependency Hierarchy Diagram
## 1. Layer-Based Dependency Model
```
Level 4: Feature Components (Entry Points)
├── oms-feature-return-search
├── oms-feature-return-details
├── oms-feature-return-process
├── oms-feature-return-summary
├── oms-feature-return-review
├── remission-feature-remission-list
├── remission-feature-remission-return-receipt-list
├── remission-feature-remission-return-receipt-details
├── checkout-feature-reward-catalog
├── checkout-feature-reward-shopping-cart
└── checkout-feature-reward-order-confirmation
Level 3: Shared & UI Components
├── OMS Shared
│ ├── oms-shared-product-info
│ └── oms-shared-task-list
├── Remission Shared
│ ├── remission-shared-product
│ ├── remission-shared-remission-start-dialog
│ ├── remission-shared-return-receipt-actions
│ └── remission-shared-search-item-to-remit-dialog
├── Checkout Shared
│ ├── checkout-shared-product-info
│ └── checkout-shared-reward-selection-dialog
└── UI Component Library (17)
├── ui-buttons
├── ui-input-controls
├── ui-dialog
├── ui-datepicker
├── ui-layout
├── ui-menu
├── ui-toolbar
├── ui-search-bar
├── ui-expandable
├── ui-empty-state
├── ui-skeleton-loader
├── ui-carousel
├── ui-item-rows
├── ui-progress-bar
├── ui-tooltip
├── ui-label
└── ui-bullet-list
Level 2: Data Access Layer
├── oms-data-access
├── remission-data-access
├── checkout-data-access
├── catalogue-data-access
├── availability-data-access
└── crm-data-access
Level 1: Infrastructure & Core
├── Core Libraries (5)
│ ├── core-config
│ ├── core-logging
│ ├── core-navigation
│ ├── core-storage
│ └── core-tabs
├── Common Utilities (3)
│ ├── common-data-access
│ ├── common-decorators
│ └── common-print
├── Shared Components (7)
│ ├── shared-address
│ ├── shared-filter
│ ├── shared-product-image
│ ├── shared-product-format
│ ├── shared-product-router-link
│ ├── shared-quantity-control
│ └── shared-scanner
├── Generated APIs (10)
│ ├── @generated/swagger/oms-api
│ ├── @generated/swagger/checkout-api
│ ├── @generated/swagger/crm-api
│ ├── @generated/swagger/cat-search-api
│ ├── @generated/swagger/availability-api
│ ├── @generated/swagger/isa-api
│ ├── @generated/swagger/eis-api
│ ├── @generated/swagger/inventory-api
│ ├── @generated/swagger/print-api
│ └── @generated/swagger/wws-api
└── Utilities (3)
├── utils-ean-validation
├── utils-scroll-position
└── utils-z-safe-parse
```
---
## 2. OMS Domain Dependency Tree
```
oms-feature-return-search
├── oms-data-access
│ ├── @generated/swagger/oms-api
│ ├── @generated/swagger/print-api
│ ├── @isa/core/logging
│ └── @isa/common/data-access
├── oms-shared-product-info
│ ├── shared-product-image
│ ├── shared-product-format
│ ├── ui-item-rows
│ └── ui-label
└── ui-* (search-bar, buttons, empty-state, etc.)
oms-feature-return-details
├── oms-data-access (store)
├── oms-shared-product-info
├── ui-input-controls (quantity selector)
├── ui-buttons
└── shared-quantity-control
oms-feature-return-process
├── oms-data-access (update store)
├── ui-input-controls (forms)
├── ui-buttons
└── common-data-access (validation)
oms-feature-return-summary
├── oms-data-access (confirmation)
├── oms-shared-product-info
├── oms-shared-task-list
├── common-print (printing)
└── ui-buttons
oms-feature-return-review
├── oms-data-access (state)
├── oms-shared-product-info
├── common-print (reprint)
└── ui-empty-state
```
---
## 3. Remission Domain Dependency Tree
```
remission-feature-remission-list
├── remission-data-access
│ ├── @generated/swagger/remission-api
│ ├── @isa/core/logging
│ └── @isa/common/data-access
├── remission-shared-remission-start-dialog
├── ui-dialog
├── ui-buttons
└── shared-filter
remission-feature-remission-return-receipt-list
├── remission-data-access
├── remission-shared-search-item-to-remit-dialog
├── remission-shared-return-receipt-actions
├── ui-buttons
└── ui-empty-state
remission-feature-remission-return-receipt-details
├── remission-data-access
├── remission-shared-product
│ ├── shared-product-image
│ ├── shared-product-format
│ └── ui-item-rows
├── remission-shared-return-receipt-actions
├── ui-expandable
└── ui-buttons
```
---
## 4. Checkout Domain Dependency Tree
```
checkout-feature-reward-shopping-cart
├── checkout-data-access
│ ├── @generated/swagger/checkout-api
│ ├── @generated/swagger/crm-api
│ ├── @isa/core/logging
│ └── @isa/common/data-access
├── checkout-shared-product-info
│ ├── shared-product-image
│ └── ui-item-rows
├── checkout-shared-reward-selection-dialog
├── shared-quantity-control
├── ui-buttons
└── ui-empty-state
checkout-feature-reward-catalog
├── checkout-data-access
├── checkout-shared-product-info
├── shared-product-image
├── ui-buttons
├── ui-carousel
└── ui-skeleton-loader
checkout-feature-reward-order-confirmation
├── checkout-data-access
├── checkout-shared-product-info
├── ui-buttons
└── shared-address
```
---
## 5. Complete Cross-Domain Dependency Matrix
```
Domain → Depends On
────────────────────────────────
OMS Features → oms-data-access, oms-shared-*, ui-*, shared-*
OMS Data Access → @generated/swagger/*, core-*, common-*
Remission Features → remission-data-access, remission-shared-*, ui-*, shared-*
Remission D.A. → @generated/swagger/*, core-*, common-*
Checkout Features → checkout-data-access, checkout-shared-*, ui-*, shared-*
Checkout D.A. → @generated/swagger/*, core-*, common-*
Catalogue D.A. → @generated/swagger/*, core-*, common-*
Availability D.A. → @generated/swagger/*, core-*, common-*
CRM D.A. → @generated/swagger/crm-api, core-*, common-*
UI Components → core-config, common-*, no data-access deps
Shared Components → core-*, ui-*, no data-access deps
Core Libraries → No monorepo dependencies
Common Libraries → core-*, no domain deps
Generated APIs → External (backend)
Utilities → core-config, no domain deps
```
---
## 6. Import Path Conventions
All imports follow strict path aliases:
```typescript
// Domain-specific data-access
import { OrderStore, orderStore } from '@isa/oms/data-access';
import { ReturnStore, returnStore } from '@isa/remission/data-access';
import { CartStore, cartStore } from '@isa/checkout/data-access';
// Domain-specific shared components
import { OmsProductInfo } from '@isa/oms/shared/product-info';
import { RemissionProduct } from '@isa/remission/shared/product';
import { CheckoutProductInfo } from '@isa/checkout/shared/product-info';
// UI component library
import { UiButton, UiPrimaryButton } from '@isa/ui/buttons';
import { UiDialog } from '@isa/ui/dialog';
import { UiEmptyState } from '@isa/ui/empty-state';
// Shared components
import { SharedAddress } from '@isa/shared/address';
import { SharedFilter } from '@isa/shared/filter';
import { SharedProductImage } from '@isa/shared/product-image';
// Core infrastructure
import { Config } from '@isa/core/config';
import { logger } from '@isa/core/logging';
import { navigationState } from '@isa/core/navigation';
import { storageProvider } from '@isa/core/storage';
import { tabManager } from '@isa/core/tabs';
// Common utilities
import { tapResponse } from '@isa/common/data-access';
import { Cached, Debounce } from '@isa/common/decorators';
import { PrintService } from '@isa/common/print';
// General utilities
import { validateEan } from '@isa/utils/ean-validation';
import { restoreScrollPosition } from '@isa/utils/scroll-position';
import { safeParse } from '@isa/utils/z-safe-parse';
// Generated Swagger APIs
import { OmsApiClient } from '@generated/swagger/oms-api';
import { CheckoutApiClient } from '@generated/swagger/checkout-api';
import { CrmApiClient } from '@generated/swagger/crm-api';
```
---
## 7. NO Circular Dependencies
The architecture enforces strict acyclic dependencies:
```
Feature Layer (Level 4)
↓ (one-way only)
Shared/UI Layer (Level 3)
↓ (one-way only)
Data Access Layer (Level 2)
↓ (one-way only)
Infrastructure Layer (Level 1)
↓ (one-way only)
External APIs & Services (Backend)
```
**Verification Command:**
```bash
npx nx affected:lint --skip-nx-cache
```
---
## 8. Bundle Dependency Impact
### Smallest Dependencies (Infrastructure)
- `core-config` (~2KB)
- `core-logging` (~5KB)
- `utils-ean-validation` (~3KB)
### Medium Dependencies (Shared)
- `shared-product-image` (~8KB)
- `ui-buttons` (~12KB)
- `ui-input-controls` (~20KB)
### Larger Dependencies (Domain)
- `oms-data-access` (~25KB including API client)
- `checkout-data-access` (~30KB including API client)
- `remission-data-access` (~20KB including API client)
### Impact on Main Bundle
- All 63 libraries + isa-app = ~2MB (production gzipped)
- Tree-shaking removes unused code
- Lazy-loaded routes reduce initial load
---
## 9. Development Workflow
### Adding a New Feature
```
1. Create feature component
→ Depends on shared components
2. Create shared component (if needed)
→ Depends on UI & core libraries
3. Update data-access if needed
→ Depends on generated APIs & common
4. Import via path aliases
import { NewFeature } from '@isa/domain/feature/new-feature'
5. Verify no circular deps
npx nx affected:lint
```
### Dependency Investigation
```bash
# Show all dependencies of a library
npx nx show project oms-feature-return-search --web false
# Visualize dependency graph
npx nx graph --filter=oms-data-access
# Check for circular dependencies
npx nx lint
# View detailed dependency tree
npx nx show project oms-data-access --web false
```
---
## 10. Performance Considerations
### Lazy Loading
- Each feature module can be lazy-loaded via routing
- Reduces initial bundle size
- Loads on demand
### Tree Shaking
- Unused exports automatically removed
- All libraries use ES6 modules
- Named exports encouraged
### Code Splitting
- Generated APIs included only when imported
- UI components tree-shakeable
- Shared components bundled separately
### Module Boundaries
```
isa-app (main bundle)
├── core/ (always loaded)
├── routing (root)
└── route bundles (lazy)
├── oms/ (on route navigation)
├── remission/ (on route navigation)
└── checkout/ (on route navigation)
```
---
## 11. Breaking Changes Prevention
Strict dependency enforcement prevents:
- ✅ Feature importing data-access from other features
- ✅ UI components importing domain logic
- ✅ Core libraries importing domain logic
- ✅ Circular dependencies
- ✅ Implicit dependencies
Violations caught by:
1. ESLint nx plugin (import-rules)
2. TypeScript compiler (path alias validation)
3. Bundle analysis tools
4. Code review process
---
## 12. Updating Dependencies
### Safe Dependency Diagram Update
```
1. Update generated API
→ Automatically updates data-access
2. Update core library
→ Cascades to all dependent layers
3. Update UI component
→ Only affects features using it
4. Update data-access
→ Only affects features in domain
```
### Testing Strategy
- Update → Run affected tests → Deploy
- `npx nx affected:test --skip-nx-cache`
---
## Quick Reference: Which Library to Import
**I need a component for...**
| Use Case | Import From |
|----------|------------|
| Button styling | `@isa/ui/buttons` |
| Form inputs | `@isa/ui/input-controls` |
| Modal dialog | `@isa/ui/dialog` |
| Data fetching | `@isa/[domain]/data-access` |
| Product display | `@isa/shared/product-*` |
| Address display | `@isa/shared/address` |
| Logging | `@isa/core/logging` |
| Configuration | `@isa/core/config` |
| State storage | `@isa/core/storage` |
| Tab navigation | `@isa/core/tabs` |
| Context preservation | `@isa/core/navigation` |
| Printer management | `@isa/common/print` |
| EAN validation | `@isa/utils/ean-validation` |
| API client | `@generated/swagger/[api-name]` |

View File

@@ -1,11 +1,11 @@
# Library Reference Guide
> **Last Updated:** 2025-10-22
> **Last Updated:** 2025-10-27
> **Angular Version:** 20.1.2
> **Nx Version:** 21.3.2
> **Total Libraries:** 61
> **Total Libraries:** 62
All 61 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
All 62 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
**IMPORTANT: Always use the `docs-researcher` subagent** to retrieve and analyze library documentation. This keeps the main context clean and prevents pollution.
@@ -278,6 +278,11 @@ A comprehensive button component library for Angular applications providing five
**Location:** `libs/ui/buttons/`
### `@isa/ui/carousel`
A horizontal scroll container component with left/right navigation arrows, responsive behavior, keyboard support, and auto-hide functionality for Angular applications.
**Location:** `libs/ui/carousel/`
### `@isa/ui/datepicker`
A comprehensive date range picker component library for Angular applications with calendar and month/year selection views, form integration, and robust validation.

View File

@@ -8,7 +8,7 @@ import { Injectable } from '@angular/core';
providedIn: 'root',
})
export class AvConfiguration {
rootUrl: string = 'https://isa-test.paragon-data.net';
rootUrl: string = 'https://isa-test.paragon-data.net/ava/v6';
}
export interface AvConfigurationInterface {

View File

@@ -31,8 +31,9 @@ const PARAMETER_CODEC = new ParameterCodec();
export class BaseService {
constructor(
protected config: AvConfiguration,
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

@@ -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 AvailableFor = 0 | 1 | 2 | 4 | 8 | 16 | 32;
export type AvailableFor = 0 | 1 | 2 | 4 | 8 | 16 | 32;

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { TouchedBase } from './touched-base';
export interface DateRangeDTO extends TouchedBase {
export interface DateRangeDTO extends TouchedBase{
start?: string;
stop?: string;
}

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 { 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

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

View File

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

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,2 +1,3 @@
/* tslint:disable */
export interface TouchedBase {}
export interface TouchedBase {
}

View File

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

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

@@ -6,6 +6,7 @@ import { TrafficLightValue } from './traffic-light-value';
* Verfügbarkeit
*/
export interface WebshopAvailabilityDTO {
/**
* EAN
*/

View File

@@ -5,6 +5,7 @@ import { WebshopAvailabilityRequestItemDTO } from './webshop-availability-reques
* Webshop Availability Request DTO
*/
export interface WebshopAvailabilityRequestDTO {
/**
* Branch PKs
*/

View File

@@ -4,6 +4,7 @@
* Webshop Availability Request Item DTO
*/
export interface WebshopAvailabilityRequestItemDTO {
/**
* EAN
*/

View File

@@ -16,7 +16,10 @@ class AvailabilityService extends __BaseService {
static readonly AvailabilityStoreAvailabilityPath = '/availability/store';
static readonly AvailabilityShippingAvailabilityPath = '/availability/shipping';
constructor(config: __Configuration, http: HttpClient) {
constructor(
config: __Configuration,
http: HttpClient
) {
super(config, http);
}
@@ -25,24 +28,26 @@ class AvailabilityService extends __BaseService {
* Für jede AvailabilityRequestDTO müssen mindestens folgende Werte gesetzt sein: ItemId oder EAN, Qty, sowie ShopId oder BranchNumber
* @param request undefined
*/
AvailabilityStoreAvailabilityResponse(
request: Array<AvailabilityRequestDTO>,
): __Observable<__StrictHttpResponse<ResponseArgsOfIEnumerableOfAvailabilityDTO>> {
AvailabilityStoreAvailabilityResponse(request: Array<AvailabilityRequestDTO>): __Observable<__StrictHttpResponse<ResponseArgsOfIEnumerableOfAvailabilityDTO>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
__body = request;
let req = new HttpRequest<any>('POST', this.rootUrl + `/availability/store`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/availability/store`,
__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<ResponseArgsOfIEnumerableOfAvailabilityDTO>;
}),
})
);
}
/**
@@ -51,7 +56,9 @@ class AvailabilityService extends __BaseService {
* @param request undefined
*/
AvailabilityStoreAvailability(request: Array<AvailabilityRequestDTO>): __Observable<ResponseArgsOfIEnumerableOfAvailabilityDTO> {
return this.AvailabilityStoreAvailabilityResponse(request).pipe(__map((_r) => _r.body as ResponseArgsOfIEnumerableOfAvailabilityDTO));
return this.AvailabilityStoreAvailabilityResponse(request).pipe(
__map(_r => _r.body as ResponseArgsOfIEnumerableOfAvailabilityDTO)
);
}
/**
@@ -59,24 +66,26 @@ class AvailabilityService extends __BaseService {
* Für jede AvailabilityRequestDTO müssen mindestens folgende Werte gesetzt sein: ItemId oder EAN, Qty, sowie ShopId oder BranchNumber
* @param request undefined
*/
AvailabilityShippingAvailabilityResponse(
request: Array<AvailabilityRequestDTO>,
): __Observable<__StrictHttpResponse<ResponseArgsOfIEnumerableOfAvailabilityDTO>> {
AvailabilityShippingAvailabilityResponse(request: Array<AvailabilityRequestDTO>): __Observable<__StrictHttpResponse<ResponseArgsOfIEnumerableOfAvailabilityDTO>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
__body = request;
let req = new HttpRequest<any>('POST', this.rootUrl + `/availability/shipping`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/availability/shipping`,
__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<ResponseArgsOfIEnumerableOfAvailabilityDTO>;
}),
})
);
}
/**
@@ -86,11 +95,12 @@ class AvailabilityService extends __BaseService {
*/
AvailabilityShippingAvailability(request: Array<AvailabilityRequestDTO>): __Observable<ResponseArgsOfIEnumerableOfAvailabilityDTO> {
return this.AvailabilityShippingAvailabilityResponse(request).pipe(
__map((_r) => _r.body as ResponseArgsOfIEnumerableOfAvailabilityDTO),
__map(_r => _r.body as ResponseArgsOfIEnumerableOfAvailabilityDTO)
);
}
}
module AvailabilityService {}
module AvailabilityService {
}
export { AvailabilityService };
export { AvailabilityService }

View File

@@ -15,7 +15,10 @@ import { WebshopAvailabilityRequestDTO } from '../models/webshop-availability-re
class WebshopAvailabilityService extends __BaseService {
static readonly WebshopAvailabilityWebshopAvailabilityPath = '/availability/webshop';
constructor(config: __Configuration, http: HttpClient) {
constructor(
config: __Configuration,
http: HttpClient
) {
super(config, http);
}
@@ -23,39 +26,40 @@ class WebshopAvailabilityService extends __BaseService {
* Verfügbarkeit für Webshop
* @param payload undefined
*/
WebshopAvailabilityWebshopAvailabilityResponse(
payload: WebshopAvailabilityRequestDTO,
): __Observable<__StrictHttpResponse<ResponseArgsOfIEnumerableOfWebshopAvailabilityDTO>> {
WebshopAvailabilityWebshopAvailabilityResponse(payload: WebshopAvailabilityRequestDTO): __Observable<__StrictHttpResponse<ResponseArgsOfIEnumerableOfWebshopAvailabilityDTO>> {
let __params = this.newParams();
let __headers = new HttpHeaders();
let __body: any = null;
__body = payload;
let req = new HttpRequest<any>('POST', this.rootUrl + `/availability/webshop`, __body, {
headers: __headers,
params: __params,
responseType: 'json',
});
let req = new HttpRequest<any>(
'POST',
this.rootUrl + `/availability/webshop`,
__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<ResponseArgsOfIEnumerableOfWebshopAvailabilityDTO>;
}),
})
);
}
/**
* Verfügbarkeit für Webshop
* @param payload undefined
*/
WebshopAvailabilityWebshopAvailability(
payload: WebshopAvailabilityRequestDTO,
): __Observable<ResponseArgsOfIEnumerableOfWebshopAvailabilityDTO> {
WebshopAvailabilityWebshopAvailability(payload: WebshopAvailabilityRequestDTO): __Observable<ResponseArgsOfIEnumerableOfWebshopAvailabilityDTO> {
return this.WebshopAvailabilityWebshopAvailabilityResponse(payload).pipe(
__map((_r) => _r.body as ResponseArgsOfIEnumerableOfWebshopAvailabilityDTO),
__map(_r => _r.body as ResponseArgsOfIEnumerableOfWebshopAvailabilityDTO)
);
}
}
module WebshopAvailabilityService {}
module WebshopAvailabilityService {
}
export { WebshopAvailabilityService };
export { WebshopAvailabilityService }

View File

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

View File

File diff suppressed because it is too large Load Diff

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