mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Compare commits
36 Commits
release/4.
...
de3edaa0f9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de3edaa0f9 | ||
|
|
964a6026a0 | ||
|
|
83ad5f526e | ||
|
|
ccc5285602 | ||
|
|
7200eaefbf | ||
|
|
39e56a275e | ||
|
|
6c41214d69 | ||
|
|
6e55b7b0da | ||
|
|
5711a75188 | ||
|
|
3696fb5b2d | ||
|
|
7e7721b222 | ||
|
|
f10338a48b | ||
|
|
6cb9aea7d1 | ||
|
|
fdfb54a3a0 | ||
|
|
a3c865e39c | ||
|
|
68f50b911d | ||
|
|
0670dbfdb1 | ||
|
|
db4f30af86 | ||
|
|
39b945ae88 | ||
|
|
a2b29c0525 | ||
|
|
2c385210db | ||
|
|
46999cc04c | ||
|
|
5aded6ff8e | ||
|
|
3228abef44 | ||
|
|
c0cc0e1bbc | ||
|
|
41630d5d7c | ||
|
|
a5bb8b2895 | ||
|
|
7950994d66 | ||
|
|
4589146e31 | ||
|
|
98fb863fc7 | ||
|
|
6f13d48604 | ||
|
|
d4bba4075b | ||
|
|
1fae7df73e | ||
|
|
bc1f6a42e6 | ||
|
|
b93e39068c | ||
|
|
dc26c4de04 |
@@ -29,12 +29,33 @@ Create well-formatted commit: $ARGUMENTS
|
||||
6. If multiple distinct changes are detected, suggests breaking the commit into multiple smaller commits
|
||||
7. For each commit (or the single commit if not split), creates a commit message using emoji conventional commit format
|
||||
|
||||
## Determining the Scope
|
||||
|
||||
The scope in commit messages MUST be the `name` field from the affected library's `project.json`:
|
||||
|
||||
1. **Check the file path**: `libs/ui/label/src/...` → Look at `libs/ui/label/project.json`
|
||||
2. **Read the project name**: The `"name"` field (e.g., `"name": "ui-label"`)
|
||||
3. **Use that as scope**: `feat(ui-label): ...`
|
||||
|
||||
**Examples:**
|
||||
- File: `libs/remission/feature/remission-list/src/...` → Scope: `remission-feature-remission-list`
|
||||
- File: `libs/ui/notice/src/...` → Scope: `ui-notice`
|
||||
- File: `apps/isa-app/src/...` → Scope: `isa-app`
|
||||
|
||||
**Multi-project changes:**
|
||||
- If changes span 2-3 related projects, use the primary one or list them: `feat(ui-label, ui-notice): ...`
|
||||
- If changes span many unrelated projects, split into separate commits
|
||||
|
||||
## Best Practices for Commits
|
||||
|
||||
- **Verify before committing**: Ensure code is linted, builds correctly, and documentation is updated
|
||||
- **Atomic commits**: Each commit should contain related changes that serve a single purpose
|
||||
- **Split large changes**: If changes touch multiple concerns, split them into separate commits
|
||||
- **Conventional commit format**: Use the format `<type>: <description>` where type is one of:
|
||||
- **Conventional commit format**: Use the format `<type>(<scope>): <description>` where:
|
||||
- **scope**: The project name from `project.json` of the affected library (e.g., `ui-label`, `crm-feature-checkout`)
|
||||
- Determine the scope by checking which library/project the changes belong to
|
||||
- If changes span multiple projects, use the primary affected project or split into multiple commits
|
||||
- type is one of:
|
||||
- `feat`: A new feature
|
||||
- `fix`: A bug fix
|
||||
- `docs`: Documentation changes
|
||||
@@ -122,37 +143,73 @@ When analyzing the diff, consider splitting commits based on these criteria:
|
||||
|
||||
## Examples
|
||||
|
||||
Good commit messages:
|
||||
- ✨ feat: add user authentication system
|
||||
- 🐛 fix: resolve memory leak in rendering process
|
||||
- 📝 docs: update API documentation with new endpoints
|
||||
- ♻️ refactor: simplify error handling logic in parser
|
||||
- 🚨 fix: resolve linter warnings in component files
|
||||
- 🧑💻 chore: improve developer tooling setup process
|
||||
- 👔 feat: implement business logic for transaction validation
|
||||
- 🩹 fix: address minor styling inconsistency in header
|
||||
- 🚑️ fix: patch critical security vulnerability in auth flow
|
||||
- 🎨 style: reorganize component structure for better readability
|
||||
- 🔥 fix: remove deprecated legacy code
|
||||
- 🦺 feat: add input validation for user registration form
|
||||
- 💚 fix: resolve failing CI pipeline tests
|
||||
- 📈 feat: implement analytics tracking for user engagement
|
||||
- 🔒️ fix: strengthen authentication password requirements
|
||||
- ♿️ feat: improve form accessibility for screen readers
|
||||
Good commit messages (scope = project name from project.json):
|
||||
- ✨ feat(auth-feature-login): add user authentication system
|
||||
- 🐛 fix(ui-renderer): resolve memory leak in rendering process
|
||||
- 📝 docs(crm-api): update API documentation with new endpoints
|
||||
- ♻️ refactor(shared-utils): simplify error handling logic in parser
|
||||
- 🚨 fix(ui-label): resolve linter warnings in component files
|
||||
- 🧑💻 chore(dev-tools): improve developer tooling setup process
|
||||
- 👔 feat(checkout-feature): implement business logic for transaction validation
|
||||
- 🩹 fix(ui-header): address minor styling inconsistency in header
|
||||
- 🚑️ fix(auth-core): patch critical security vulnerability in auth flow
|
||||
- 🎨 style(ui-components): reorganize component structure for better readability
|
||||
- 🔥 fix(legacy-module): remove deprecated legacy code
|
||||
- 🦺 feat(user-registration): add input validation for user registration form
|
||||
- 💚 fix(ci-config): resolve failing CI pipeline tests
|
||||
- 📈 feat(analytics-feature): implement analytics tracking for user engagement
|
||||
- 🔒️ fix(auth-password): strengthen authentication password requirements
|
||||
- ♿️ feat(ui-forms): improve form accessibility for screen readers
|
||||
|
||||
Example of splitting commits:
|
||||
- First commit: ✨ feat: add new solc version type definitions
|
||||
- Second commit: 📝 docs: update documentation for new solc versions
|
||||
- Third commit: 🔧 chore: update package.json dependencies
|
||||
- Fourth commit: 🏷️ feat: add type definitions for new API endpoints
|
||||
- Fifth commit: 🧵 feat: improve concurrency handling in worker threads
|
||||
- Sixth commit: 🚨 fix: resolve linting issues in new code
|
||||
- Seventh commit: ✅ test: add unit tests for new solc version features
|
||||
- Eighth commit: 🔒️ fix: update dependencies with security vulnerabilities
|
||||
- First commit: ✨ feat(ui-label): add prio-label component with variant styles
|
||||
- Second commit: 📝 docs(ui-label): update README with usage examples
|
||||
- Third commit: 🔧 chore(ui-notice): scaffold new notice library
|
||||
- Fourth commit: 🏷️ feat(shared-types): add type definitions for new API endpoints
|
||||
- Fifth commit: ♻️ refactor(pickup-shelf): update components to use new label
|
||||
- Sixth commit: 🚨 fix(remission-list): resolve linting issues in new code
|
||||
- Seventh commit: ✅ test(ui-label): add unit tests for prio-label component
|
||||
- Eighth commit: 🔒️ fix(deps): update dependencies with security vulnerabilities
|
||||
|
||||
## Command Options
|
||||
|
||||
- `--no-verify`: Skip running the pre-commit checks (lint, build, generate:docs)
|
||||
- `--amend`: Amend the previous commit (RESTRICTED - see rules below)
|
||||
|
||||
## Amend Rules (CRITICAL)
|
||||
|
||||
**ONLY use `--amend` in these specific cases:**
|
||||
|
||||
1. **Adding pre-commit hook fixes**: If a pre-commit hook modified files (formatting, linting auto-fixes), you may amend to include those changes.
|
||||
|
||||
2. **Before amending, ALWAYS verify:**
|
||||
```bash
|
||||
# Check authorship - NEVER amend another developer's commit
|
||||
git log -1 --format='%an %ae'
|
||||
|
||||
# Check not pushed - NEVER amend pushed commits
|
||||
git status # Should show "Your branch is ahead of..."
|
||||
```
|
||||
|
||||
3. **If either check fails:**
|
||||
- Create a NEW commit instead
|
||||
- Never amend commits authored by others
|
||||
- Never amend commits already pushed to remote
|
||||
|
||||
**Example workflow for pre-commit hook changes:**
|
||||
```bash
|
||||
# 1. Initial commit triggers pre-commit hook
|
||||
git commit -m "feat(scope): add feature"
|
||||
# Hook modifies files...
|
||||
|
||||
# 2. Verify safe to amend
|
||||
git log -1 --format='%an %ae' # Your name/email
|
||||
git status # "Your branch is ahead..."
|
||||
|
||||
# 3. Stage hook changes and amend
|
||||
git add .
|
||||
git commit --amend --no-edit
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
allowed-tools: Read, Write, Edit, Bash
|
||||
argument-hint: [framework] | --c4-model | --arc42 | --adr | --plantuml | --full-suite
|
||||
description: Generate comprehensive architecture documentation with diagrams, ADRs, and interactive visualization
|
||||
---
|
||||
|
||||
# Architecture Documentation Generator
|
||||
|
||||
Generate comprehensive architecture documentation: $ARGUMENTS
|
||||
|
||||
## Current Architecture Context
|
||||
|
||||
- Project structure: !`find . -type f -name "*.json" -o -name "*.yaml" -o -name "*.toml" | head -5`
|
||||
- Documentation exists: @docs/ or @README.md (if exists)
|
||||
- Architecture files: !`find . -name "*architecture*" -o -name "*design*" -o -name "*.puml" | head -3`
|
||||
- Services/containers: @docker-compose.yml or @k8s/ (if exists)
|
||||
- API definitions: !`find . -name "*api*" -o -name "*openapi*" -o -name "*swagger*" | head -3`
|
||||
|
||||
## Task
|
||||
|
||||
Generate comprehensive architecture documentation with modern tooling and best practices:
|
||||
|
||||
1. **Architecture Analysis and Discovery**
|
||||
- Analyze current system architecture and component relationships
|
||||
- Identify key architectural patterns and design decisions
|
||||
- Document system boundaries, interfaces, and dependencies
|
||||
- Assess data flow and communication patterns
|
||||
- Identify architectural debt and improvement opportunities
|
||||
|
||||
2. **Architecture Documentation Framework**
|
||||
- Choose appropriate documentation framework and tools:
|
||||
- **C4 Model**: Context, Containers, Components, Code diagrams
|
||||
- **Arc42**: Comprehensive architecture documentation template
|
||||
- **Architecture Decision Records (ADRs)**: Decision documentation
|
||||
- **PlantUML/Mermaid**: Diagram-as-code documentation
|
||||
- **Structurizr**: C4 model tooling and visualization
|
||||
- **Draw.io/Lucidchart**: Visual diagramming tools
|
||||
|
||||
3. **System Context Documentation**
|
||||
- Create high-level system context diagrams
|
||||
- Document external systems and integrations
|
||||
- Define system boundaries and responsibilities
|
||||
- Document user personas and stakeholders
|
||||
- Create system landscape and ecosystem overview
|
||||
|
||||
4. **Container and Service Architecture**
|
||||
- Document container/service architecture and deployment view
|
||||
- Create service dependency maps and communication patterns
|
||||
- Document deployment architecture and infrastructure
|
||||
- Define service boundaries and API contracts
|
||||
- Document data persistence and storage architecture
|
||||
|
||||
5. **Component and Module Documentation**
|
||||
- Create detailed component architecture diagrams
|
||||
- Document internal module structure and relationships
|
||||
- Define component responsibilities and interfaces
|
||||
- Document design patterns and architectural styles
|
||||
- Create code organization and package structure documentation
|
||||
|
||||
6. **Data Architecture Documentation**
|
||||
- Document data models and database schemas
|
||||
- Create data flow diagrams and processing pipelines
|
||||
- Document data storage strategies and technologies
|
||||
- Define data governance and lifecycle management
|
||||
- Create data integration and synchronization documentation
|
||||
|
||||
7. **Security and Compliance Architecture**
|
||||
- Document security architecture and threat model
|
||||
- Create authentication and authorization flow diagrams
|
||||
- Document compliance requirements and controls
|
||||
- Define security boundaries and trust zones
|
||||
- Create incident response and security monitoring documentation
|
||||
|
||||
8. **Quality Attributes and Cross-Cutting Concerns**
|
||||
- Document performance characteristics and scalability patterns
|
||||
- Create reliability and availability architecture documentation
|
||||
- Document monitoring and observability architecture
|
||||
- Define maintainability and evolution strategies
|
||||
- Create disaster recovery and business continuity documentation
|
||||
|
||||
9. **Architecture Decision Records (ADRs)**
|
||||
- Create comprehensive ADR template and process
|
||||
- Document historical architectural decisions and rationale
|
||||
- Create decision tracking and review process
|
||||
- Document trade-offs and alternatives considered
|
||||
- Set up ADR maintenance and evolution procedures
|
||||
|
||||
10. **Documentation Automation and Maintenance**
|
||||
- Set up automated diagram generation from code annotations
|
||||
- Configure documentation pipeline and publishing automation
|
||||
- Set up documentation validation and consistency checking
|
||||
- Create documentation review and approval process
|
||||
- Train team on architecture documentation practices and tools
|
||||
- Set up documentation versioning and change management
|
||||
@@ -1,3 +1,9 @@
|
||||
---
|
||||
allowed-tools: Read, Write, Edit, Bash, Grep, Glob, Task
|
||||
argument-hint: [library-name]
|
||||
description: Generate or update README.md for a specific library with comprehensive documentation
|
||||
---
|
||||
|
||||
# /docs:library - Generate/Update Library README
|
||||
|
||||
Generate or update README.md for a specific library with comprehensive documentation.
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
---
|
||||
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
argument-hint: --dry-run | --force
|
||||
description: Regenerate library reference documentation (docs/library-reference.md) by scanning all monorepo libraries
|
||||
---
|
||||
|
||||
# /docs:refresh-reference - Regenerate Library Reference
|
||||
|
||||
Regenerate the library reference documentation (`docs/library-reference.md`) by scanning all libraries in the monorepo.
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
# /quality:bundle-analyze - Analyze Bundle Sizes
|
||||
|
||||
Analyze production bundle sizes and provide optimization recommendations. Project thresholds: 2MB warning, 5MB error.
|
||||
|
||||
## Tasks
|
||||
|
||||
### 1. Run Production Build
|
||||
```bash
|
||||
# Clean previous build
|
||||
rm -rf dist/
|
||||
|
||||
# Build for production
|
||||
npm run build-prod
|
||||
```
|
||||
|
||||
### 2. Analyze Bundle Output
|
||||
```bash
|
||||
# List bundle files with sizes
|
||||
ls -lh dist/apps/isa-app/browser/*.js | awk '{print $9, $5}'
|
||||
|
||||
# Get total bundle size
|
||||
du -sh dist/apps/isa-app/browser/
|
||||
```
|
||||
|
||||
### 3. Identify Large Files
|
||||
Parse build output and identify:
|
||||
- Main bundle size
|
||||
- Lazy-loaded chunk sizes
|
||||
- Vendor chunks
|
||||
- Files exceeding thresholds:
|
||||
- **Warning**: > 2MB
|
||||
- **Error**: > 5MB
|
||||
|
||||
### 4. Analyze Dependencies
|
||||
```bash
|
||||
# Check for duplicate dependencies
|
||||
npm ls --depth=0 | grep -E "UNMET|deduped"
|
||||
|
||||
# Show largest node_modules packages
|
||||
du -sh node_modules/* | sort -rh | head -20
|
||||
```
|
||||
|
||||
### 5. Source Map Analysis
|
||||
Use source maps to identify large contributors:
|
||||
```bash
|
||||
# Install source-map-explorer if needed
|
||||
npm install -g source-map-explorer
|
||||
|
||||
# Analyze main bundle
|
||||
source-map-explorer dist/apps/isa-app/browser/main.*.js
|
||||
```
|
||||
|
||||
### 6. Generate Recommendations
|
||||
Based on analysis, provide actionable recommendations:
|
||||
|
||||
**If bundle > 2MB:**
|
||||
- Identify heavy dependencies to replace or remove
|
||||
- Suggest lazy loading opportunities
|
||||
- Check for unused imports
|
||||
|
||||
**Code Splitting Opportunities:**
|
||||
- Large feature modules that could be lazy-loaded
|
||||
- Heavy libraries that could be dynamically imported
|
||||
- Vendor code that could be split into separate chunks
|
||||
|
||||
**Dependency Optimization:**
|
||||
- Replace large libraries with smaller alternatives
|
||||
- Remove unused dependencies
|
||||
- Use tree-shakeable imports
|
||||
|
||||
**Build Configuration:**
|
||||
- Enable/optimize compression
|
||||
- Check for source maps in production (should be disabled)
|
||||
- Verify optimization flags
|
||||
|
||||
### 7. Comparative Analysis
|
||||
If previous build data exists:
|
||||
```bash
|
||||
# Compare with previous build
|
||||
# (Requires manual tracking or CI/CD integration)
|
||||
echo "Current build: $(du -sh dist/apps/isa-app/browser/ | awk '{print $1}')"
|
||||
```
|
||||
|
||||
### 8. Generate Report
|
||||
Create formatted report with:
|
||||
- Total bundle size with threshold status (✅ < 2MB, ⚠️ 2-5MB, ❌ > 5MB)
|
||||
- Main bundle and largest chunks
|
||||
- Top 10 largest dependencies
|
||||
- Optimization recommendations prioritized by impact
|
||||
- Lazy loading opportunities
|
||||
|
||||
## Output Format
|
||||
```
|
||||
Bundle Analysis Report
|
||||
======================
|
||||
|
||||
Total Size: X.XX MB [STATUS]
|
||||
Main Bundle: X.XX MB
|
||||
Largest Chunks:
|
||||
- chunk-name.js: X.XX MB
|
||||
- ...
|
||||
|
||||
Largest Dependencies:
|
||||
1. dependency-name: X.XX MB
|
||||
...
|
||||
|
||||
Recommendations:
|
||||
🔴 Critical (> 5MB):
|
||||
- [Action items]
|
||||
|
||||
⚠️ Warning (2-5MB):
|
||||
- [Action items]
|
||||
|
||||
✅ Optimization Opportunities:
|
||||
- [Action items]
|
||||
|
||||
Lazy Loading Candidates:
|
||||
- [Feature modules]
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
- Build failures: Show error and suggest fixes
|
||||
- Missing tools: Offer to install (source-map-explorer)
|
||||
- No dist folder: Run build first
|
||||
|
||||
## References
|
||||
- CLAUDE.md Build Configuration section
|
||||
- Angular build optimization: https://angular.dev/tools/cli/build
|
||||
- package.json (build-prod script)
|
||||
@@ -1,201 +0,0 @@
|
||||
# /quality:coverage - Generate Test Coverage Report
|
||||
|
||||
Generate comprehensive test coverage report with recommendations for improving coverage.
|
||||
|
||||
## Parameters
|
||||
- `library-name` (optional): Specific library to analyze. If omitted, analyzes all libraries.
|
||||
|
||||
## Tasks
|
||||
|
||||
### 1. Run Coverage Analysis
|
||||
```bash
|
||||
# Single library
|
||||
npx nx test [library-name] --skip-nx-cache --coverage
|
||||
|
||||
# All libraries (if no library specified)
|
||||
npm run ci # Runs all tests with coverage
|
||||
```
|
||||
|
||||
### 2. Parse Coverage Report
|
||||
Coverage output typically in:
|
||||
- `coverage/libs/[domain]/[layer]/[name]/`
|
||||
- Look for `coverage-summary.json` or text output
|
||||
|
||||
Extract metrics:
|
||||
- **Line coverage**: % of executable lines tested
|
||||
- **Branch coverage**: % of conditional branches tested
|
||||
- **Function coverage**: % of functions called in tests
|
||||
- **Statement coverage**: % of statements executed
|
||||
|
||||
### 3. Identify Uncovered Code
|
||||
Parse coverage report to find:
|
||||
- **Uncovered files**: Files with 0% coverage
|
||||
- **Partially covered files**: < 80% coverage
|
||||
- **Uncovered lines**: Specific line numbers not tested
|
||||
- **Uncovered branches**: Conditional paths not tested
|
||||
|
||||
```bash
|
||||
# List files with coverage below 80%
|
||||
# (Parse from coverage JSON output)
|
||||
```
|
||||
|
||||
### 4. Categorize Coverage Gaps
|
||||
|
||||
**Critical (High Risk):**
|
||||
- Service methods handling business logic
|
||||
- Data transformation functions
|
||||
- Error handling code paths
|
||||
- Security-related functions
|
||||
- State management store actions
|
||||
|
||||
**Important (Medium Risk):**
|
||||
- Component public methods
|
||||
- Utility functions
|
||||
- Validators
|
||||
- Pipes and filters
|
||||
- Guard functions
|
||||
|
||||
**Low Priority:**
|
||||
- Getters/setters
|
||||
- Simple property assignments
|
||||
- Console logging
|
||||
- Type definitions
|
||||
|
||||
### 5. Generate Recommendations
|
||||
|
||||
For each coverage gap, provide:
|
||||
- **File and line numbers**
|
||||
- **Risk level** (Critical/Important/Low)
|
||||
- **Suggested test type** (unit/integration)
|
||||
- **Test approach** (example test scenario)
|
||||
|
||||
Example:
|
||||
```
|
||||
📍 libs/oms/data-access/src/lib/services/order.service.ts:45-52
|
||||
🔴 Critical - Business Logic
|
||||
❌ 0% coverage - Error handling path
|
||||
|
||||
Recommended test:
|
||||
it('should handle API error when fetching order', async () => {
|
||||
// Mock API to return error
|
||||
// Call method
|
||||
// Verify error handling
|
||||
});
|
||||
```
|
||||
|
||||
### 6. Calculate Coverage Trends
|
||||
If historical data available:
|
||||
- Compare with previous coverage percentage
|
||||
- Show improvement/regression
|
||||
- Identify files with declining coverage
|
||||
|
||||
### 7. Generate HTML Report
|
||||
```bash
|
||||
# Open coverage report in browser (if available)
|
||||
open coverage/libs/[domain]/[layer]/[name]/index.html
|
||||
```
|
||||
|
||||
### 8. Create Coverage Summary Report
|
||||
|
||||
**Overall Metrics:**
|
||||
```
|
||||
Coverage Summary for [library-name]
|
||||
====================================
|
||||
|
||||
Line Coverage: XX.X% (XXX/XXX lines)
|
||||
Branch Coverage: XX.X% (XXX/XXX branches)
|
||||
Function Coverage: XX.X% (XXX/XXX functions)
|
||||
Statement Coverage: XX.X% (XXX/XXX statements)
|
||||
|
||||
Target: 80% (Recommended minimum)
|
||||
Status: ✅ Met / ⚠️ Below Target / 🔴 Critical
|
||||
```
|
||||
|
||||
**Files Needing Attention:**
|
||||
```
|
||||
🔴 Critical (< 50% coverage):
|
||||
1. service-name.service.ts - 35% (business logic)
|
||||
2. data-processor.ts - 42% (transformations)
|
||||
|
||||
⚠️ Below Target (50-79% coverage):
|
||||
3. component-name.component.ts - 68%
|
||||
4. validator.ts - 72%
|
||||
|
||||
✅ Well Covered (≥ 80% coverage):
|
||||
- Other files maintaining good coverage
|
||||
```
|
||||
|
||||
**Top Priority Tests to Add:**
|
||||
1. [File:Line] - [Description] - [Risk Level]
|
||||
2. ...
|
||||
|
||||
### 9. Framework-Specific Notes
|
||||
|
||||
**Vitest:**
|
||||
- Coverage provider: v8 or istanbul
|
||||
- Config in `vitest.config.ts`
|
||||
- Coverage thresholds configurable
|
||||
|
||||
**Jest:**
|
||||
- Coverage collected via `--coverage` flag
|
||||
- Config in `jest.config.ts`
|
||||
- Coverage directory: `coverage/`
|
||||
|
||||
### 10. Set Coverage Thresholds (Optional)
|
||||
Suggest adding to test config:
|
||||
```typescript
|
||||
// vitest.config.ts
|
||||
coverage: {
|
||||
thresholds: {
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Output Format
|
||||
```
|
||||
Test Coverage Report
|
||||
====================
|
||||
|
||||
Library: [name]
|
||||
Test Framework: [Vitest/Jest]
|
||||
Generated: [timestamp]
|
||||
|
||||
📊 Coverage Metrics
|
||||
-------------------
|
||||
Lines: XX.X% ████████░░ (XXX/XXX)
|
||||
Branches: XX.X% ███████░░░ (XXX/XXX)
|
||||
Functions: XX.X% █████████░ (XXX/XXX)
|
||||
Statements: XX.X% ████████░░ (XXX/XXX)
|
||||
|
||||
🎯 Target: 80% | Status: [✅/⚠️/🔴]
|
||||
|
||||
🔍 Coverage Gaps
|
||||
----------------
|
||||
[Categorized list with priorities]
|
||||
|
||||
💡 Recommendations
|
||||
------------------
|
||||
[Prioritized list of tests to add]
|
||||
|
||||
📈 Next Steps
|
||||
-------------
|
||||
1. Focus on critical coverage gaps first
|
||||
2. Add tests for business logic in [files]
|
||||
3. Consider setting coverage thresholds
|
||||
4. Re-run: npx nx test [library-name] --skip-nx-cache --coverage
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
- No coverage data: Ensure `--coverage` flag used
|
||||
- Missing library: Verify library name is correct
|
||||
- Coverage tool not configured: Check test config for coverage setup
|
||||
|
||||
## References
|
||||
- docs/guidelines/testing.md
|
||||
- CLAUDE.md Testing Framework section
|
||||
- Vitest coverage: https://vitest.dev/guide/coverage
|
||||
- Jest coverage: https://jestjs.io/docs/configuration#collectcoverage-boolean
|
||||
188
.claude/commands/quality.md
Normal file
188
.claude/commands/quality.md
Normal file
@@ -0,0 +1,188 @@
|
||||
---
|
||||
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
|
||||
argument-hint: bundle | coverage [library-name] | --all
|
||||
description: Analyze code quality: bundle sizes, test coverage, with optimization recommendations
|
||||
---
|
||||
|
||||
# /quality - Code Quality Analysis
|
||||
|
||||
Comprehensive quality analysis including bundle sizes and test coverage.
|
||||
|
||||
## Subcommands
|
||||
|
||||
- `bundle` - Analyze production bundle sizes
|
||||
- `coverage [library-name]` - Test coverage analysis
|
||||
- No argument - Run both analyses
|
||||
- `[library-name]` - Coverage for specific library
|
||||
|
||||
## Bundle Analysis
|
||||
|
||||
### 1. Run Production Build
|
||||
```bash
|
||||
# Clean previous build
|
||||
rm -rf dist/
|
||||
|
||||
# Build for production
|
||||
npm run build-prod
|
||||
```
|
||||
|
||||
### 2. Analyze Bundle Output
|
||||
```bash
|
||||
# List bundle files with sizes
|
||||
ls -lh dist/apps/isa-app/browser/*.js | awk '{print $9, $5}'
|
||||
|
||||
# Get total bundle size
|
||||
du -sh dist/apps/isa-app/browser/
|
||||
```
|
||||
|
||||
### 3. Identify Large Files
|
||||
Parse build output and identify:
|
||||
- Main bundle size
|
||||
- Lazy-loaded chunk sizes
|
||||
- Vendor chunks
|
||||
- Files exceeding thresholds:
|
||||
- **Warning**: > 2MB
|
||||
- **Error**: > 5MB
|
||||
|
||||
### 4. Analyze Dependencies
|
||||
```bash
|
||||
# Check for duplicate dependencies
|
||||
npm ls --depth=0 | grep -E "UNMET|deduped"
|
||||
|
||||
# Show largest node_modules packages
|
||||
du -sh node_modules/* | sort -rh | head -20
|
||||
```
|
||||
|
||||
### 5. Source Map Analysis (Optional)
|
||||
```bash
|
||||
# Install source-map-explorer if needed
|
||||
npm install -g source-map-explorer
|
||||
|
||||
# Analyze main bundle
|
||||
source-map-explorer dist/apps/isa-app/browser/main.*.js
|
||||
```
|
||||
|
||||
### 6. Bundle Recommendations
|
||||
|
||||
**If bundle > 2MB:**
|
||||
- Identify heavy dependencies to replace or remove
|
||||
- Suggest lazy loading opportunities
|
||||
- Check for unused imports
|
||||
|
||||
**Code Splitting Opportunities:**
|
||||
- Large feature modules that could be lazy-loaded
|
||||
- Heavy libraries that could be dynamically imported
|
||||
|
||||
**Dependency Optimization:**
|
||||
- Replace large libraries with smaller alternatives
|
||||
- Remove unused dependencies
|
||||
- Use tree-shakeable imports
|
||||
|
||||
---
|
||||
|
||||
## Coverage Analysis
|
||||
|
||||
### 1. Run Coverage Analysis
|
||||
```bash
|
||||
# Single library
|
||||
npx nx test [library-name] --skip-nx-cache --coverage
|
||||
|
||||
# All libraries (if no library specified)
|
||||
npm run ci # Runs all tests with coverage
|
||||
```
|
||||
|
||||
### 2. Parse Coverage Report
|
||||
Coverage output in: `coverage/libs/[domain]/[layer]/[name]/`
|
||||
|
||||
Extract metrics:
|
||||
- **Line coverage**: % of executable lines tested
|
||||
- **Branch coverage**: % of conditional branches tested
|
||||
- **Function coverage**: % of functions called in tests
|
||||
- **Statement coverage**: % of statements executed
|
||||
|
||||
### 3. Identify Uncovered Code
|
||||
Parse coverage report to find:
|
||||
- **Uncovered files**: Files with 0% coverage
|
||||
- **Partially covered files**: < 80% coverage
|
||||
- **Uncovered lines**: Specific line numbers not tested
|
||||
- **Uncovered branches**: Conditional paths not tested
|
||||
|
||||
### 4. Categorize Coverage Gaps
|
||||
|
||||
**Critical (High Risk):**
|
||||
- Service methods handling business logic
|
||||
- Data transformation functions
|
||||
- Error handling code paths
|
||||
- Security-related functions
|
||||
- State management store actions
|
||||
|
||||
**Important (Medium Risk):**
|
||||
- Component public methods
|
||||
- Utility functions
|
||||
- Validators and guards
|
||||
|
||||
**Low Priority:**
|
||||
- Getters/setters
|
||||
- Simple property assignments
|
||||
|
||||
### 5. Generate Recommendations
|
||||
|
||||
For each coverage gap provide:
|
||||
- **File and line numbers**
|
||||
- **Risk level** (Critical/Important/Low)
|
||||
- **Suggested test type** (unit/integration)
|
||||
- **Test approach** (example test scenario)
|
||||
|
||||
---
|
||||
|
||||
## Output Formats
|
||||
|
||||
### Bundle Report
|
||||
```
|
||||
Bundle Analysis Report
|
||||
======================
|
||||
|
||||
Total Size: X.XX MB [STATUS]
|
||||
Main Bundle: X.XX MB
|
||||
Largest Chunks:
|
||||
- chunk-name.js: X.XX MB
|
||||
|
||||
Largest Dependencies:
|
||||
1. dependency-name: X.XX MB
|
||||
|
||||
Recommendations:
|
||||
- [Prioritized action items]
|
||||
```
|
||||
|
||||
### Coverage Report
|
||||
```
|
||||
Coverage Summary for [library-name]
|
||||
====================================
|
||||
|
||||
Line Coverage: XX.X% (XXX/XXX lines)
|
||||
Branch Coverage: XX.X% (XXX/XXX branches)
|
||||
Function Coverage: XX.X% (XXX/XXX functions)
|
||||
|
||||
Target: 80% (Recommended minimum)
|
||||
Status: [Met/Below Target/Critical]
|
||||
|
||||
Files Needing Attention:
|
||||
[Categorized list with priorities]
|
||||
|
||||
Top Priority Tests to Add:
|
||||
[Prioritized recommendations]
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- **Build failures**: Show error and suggest fixes
|
||||
- **Missing tools**: Offer to install (source-map-explorer)
|
||||
- **No coverage data**: Ensure `--coverage` flag used
|
||||
- **Missing library**: Verify library name is correct
|
||||
|
||||
## References
|
||||
|
||||
- CLAUDE.md Build Configuration section
|
||||
- docs/guidelines/testing.md
|
||||
- Angular build optimization: https://angular.dev/tools/cli/build
|
||||
- Vitest coverage: https://vitest.dev/guide/coverage
|
||||
@@ -18,6 +18,7 @@ Guide for modern Angular 20+ template patterns: control flow, lazy loading, proj
|
||||
**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)
|
||||
- **[logging](../logging/SKILL.md)** - MANDATORY logging in all Angular files using `@isa/core/logging`
|
||||
|
||||
## Control Flow (Angular 17+)
|
||||
|
||||
|
||||
171
.claude/skills/architecture-documentation/SKILL.md
Normal file
171
.claude/skills/architecture-documentation/SKILL.md
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
name: architecture-documentation
|
||||
description: Generate architecture documentation (C4, Arc42, ADRs, PlantUML). Auto-invoke when user mentions "architecture docs", "C4 model", "ADR", "document architecture", "system design", or "create architecture diagram".
|
||||
---
|
||||
|
||||
# Architecture Documentation Skill
|
||||
|
||||
Generate comprehensive architecture documentation using modern frameworks and best practices.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Creating or updating architecture documentation
|
||||
- Generating C4 model diagrams (Context, Container, Component, Code)
|
||||
- Writing Architecture Decision Records (ADRs)
|
||||
- Documenting system design and component relationships
|
||||
- Creating PlantUML or Mermaid diagrams
|
||||
|
||||
## Available Frameworks
|
||||
|
||||
### C4 Model
|
||||
Best for: Visualizing software architecture at different abstraction levels
|
||||
|
||||
Levels:
|
||||
1. **Context** - System landscape and external actors
|
||||
2. **Container** - High-level technology choices (apps, databases, etc.)
|
||||
3. **Component** - Internal structure of containers
|
||||
4. **Code** - Class/module level detail (optional)
|
||||
|
||||
See: `@references/c4-model.md` for patterns and examples
|
||||
|
||||
### Arc42 Template
|
||||
Best for: Comprehensive architecture documentation
|
||||
|
||||
Sections:
|
||||
1. Introduction and Goals
|
||||
2. Constraints
|
||||
3. Context and Scope
|
||||
4. Solution Strategy
|
||||
5. Building Block View
|
||||
6. Runtime View
|
||||
7. Deployment View
|
||||
8. Cross-cutting Concepts
|
||||
9. Architecture Decisions
|
||||
10. Quality Requirements
|
||||
11. Risks and Technical Debt
|
||||
12. Glossary
|
||||
|
||||
See: `@references/arc42.md` for template structure
|
||||
|
||||
### Architecture Decision Records (ADRs)
|
||||
Best for: Documenting individual architectural decisions
|
||||
|
||||
See: `@references/adr-template.md` for format and examples
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Discovery Phase
|
||||
```bash
|
||||
# Find existing architecture files
|
||||
find . -name "*architecture*" -o -name "*.puml" -o -name "*.mmd"
|
||||
|
||||
# Identify service boundaries
|
||||
cat nx.json docker-compose.yml
|
||||
|
||||
# Check for existing ADRs
|
||||
ls -la docs/adr/ docs/decisions/
|
||||
```
|
||||
|
||||
### 2. Analysis Phase
|
||||
- Analyze codebase structure (`libs/`, `apps/`)
|
||||
- Identify dependencies from `tsconfig.base.json` paths
|
||||
- Review service boundaries from `project.json` tags
|
||||
- Map data flow from API definitions
|
||||
|
||||
### 3. Documentation Phase
|
||||
Based on the request, create appropriate documentation:
|
||||
|
||||
**For C4 diagrams:**
|
||||
```
|
||||
docs/architecture/
|
||||
├── c4-context.puml
|
||||
├── c4-container.puml
|
||||
└── c4-component-[name].puml
|
||||
```
|
||||
|
||||
**For ADRs:**
|
||||
```
|
||||
docs/adr/
|
||||
├── 0001-record-architecture-decisions.md
|
||||
├── 0002-[decision-title].md
|
||||
└── template.md
|
||||
```
|
||||
|
||||
**For Arc42:**
|
||||
```
|
||||
docs/architecture/
|
||||
└── arc42/
|
||||
├── 01-introduction.md
|
||||
├── 02-constraints.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
## ISA-Frontend Specific Context
|
||||
|
||||
### Monorepo Structure
|
||||
- **apps/**: Angular applications
|
||||
- **libs/**: Shared libraries organized by domain
|
||||
- `libs/[domain]/feature/` - Feature modules
|
||||
- `libs/[domain]/data-access/` - State management
|
||||
- `libs/[domain]/ui/` - Presentational components
|
||||
- `libs/[domain]/util/` - Utilities
|
||||
|
||||
### Key Architectural Patterns
|
||||
- **Nx Monorepo** with strict module boundaries
|
||||
- **NgRx Signal Store** for state management
|
||||
- **Standalone Components** (Angular 20+)
|
||||
- **Domain-Driven Design** library organization
|
||||
|
||||
### Documentation Locations
|
||||
- ADRs: `docs/adr/`
|
||||
- Architecture diagrams: `docs/architecture/`
|
||||
- API documentation: Generated from Swagger/OpenAPI
|
||||
|
||||
## Output Standards
|
||||
|
||||
### PlantUML Format
|
||||
```plantuml
|
||||
@startuml C4_Context
|
||||
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
|
||||
|
||||
Person(user, "User", "System user")
|
||||
System(system, "ISA System", "Main application")
|
||||
System_Ext(external, "External API", "Third-party service")
|
||||
|
||||
Rel(user, system, "Uses")
|
||||
Rel(system, external, "Calls")
|
||||
@enduml
|
||||
```
|
||||
|
||||
### Mermaid Format
|
||||
```mermaid
|
||||
graph TD
|
||||
A[User] --> B[ISA App]
|
||||
B --> C[API Gateway]
|
||||
C --> D[Backend Services]
|
||||
```
|
||||
|
||||
### ADR Format
|
||||
```markdown
|
||||
# ADR-XXXX: [Title]
|
||||
|
||||
## Status
|
||||
[Proposed | Accepted | Deprecated | Superseded]
|
||||
|
||||
## Context
|
||||
[What is the issue?]
|
||||
|
||||
## Decision
|
||||
[What was decided?]
|
||||
|
||||
## Consequences
|
||||
[What are the results?]
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start with Context** - Always begin with C4 Level 1 (System Context)
|
||||
2. **Use Consistent Notation** - Stick to one diagramming tool/format
|
||||
3. **Keep ADRs Atomic** - One decision per ADR
|
||||
4. **Version Control** - Commit documentation with code changes
|
||||
5. **Review Regularly** - Architecture docs decay; schedule reviews
|
||||
@@ -0,0 +1,213 @@
|
||||
# Architecture Decision Record (ADR) Template
|
||||
|
||||
## Overview
|
||||
|
||||
Architecture Decision Records document significant architectural decisions along with their context and consequences.
|
||||
|
||||
## ADR Format
|
||||
|
||||
### Standard Template
|
||||
|
||||
```markdown
|
||||
# ADR-XXXX: [Short Title]
|
||||
|
||||
## Status
|
||||
[Proposed | Accepted | Deprecated | Superseded by ADR-YYYY]
|
||||
|
||||
## Date
|
||||
YYYY-MM-DD
|
||||
|
||||
## Context
|
||||
What is the issue that we're seeing that is motivating this decision or change?
|
||||
|
||||
## Decision
|
||||
What is the change that we're proposing and/or doing?
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Benefit 1
|
||||
- Benefit 2
|
||||
|
||||
### Negative
|
||||
- Drawback 1
|
||||
- Drawback 2
|
||||
|
||||
### Neutral
|
||||
- Side effect 1
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Option 1: [Name]
|
||||
- Pros: ...
|
||||
- Cons: ...
|
||||
- Why rejected: ...
|
||||
|
||||
### Option 2: [Name]
|
||||
- Pros: ...
|
||||
- Cons: ...
|
||||
- Why rejected: ...
|
||||
|
||||
## References
|
||||
- [Link to related documentation]
|
||||
- [Link to discussion thread]
|
||||
```
|
||||
|
||||
## Example ADRs
|
||||
|
||||
### ADR-0001: Use Nx Monorepo
|
||||
|
||||
```markdown
|
||||
# ADR-0001: Use Nx Monorepo for Project Organization
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
2024-01-15
|
||||
|
||||
## Context
|
||||
The ISA Frontend consists of multiple applications and shared libraries. We need a way to:
|
||||
- Share code between applications
|
||||
- Maintain consistent tooling and dependencies
|
||||
- Enable efficient CI/CD with affected-based testing
|
||||
- Enforce architectural boundaries
|
||||
|
||||
## Decision
|
||||
We will use Nx as our monorepo tool with the following structure:
|
||||
- `apps/` - Deployable applications
|
||||
- `libs/` - Shared libraries organized by domain and type
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Single version of dependencies across all projects
|
||||
- Affected-based testing reduces CI time
|
||||
- Consistent tooling (ESLint, Prettier, TypeScript)
|
||||
- Built-in dependency graph visualization
|
||||
|
||||
### Negative
|
||||
- Learning curve for team members new to Nx
|
||||
- More complex initial setup
|
||||
- All code in single repository increases clone time
|
||||
|
||||
### Neutral
|
||||
- Requires discipline in library boundaries
|
||||
- Need to maintain `project.json` and tags
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Polyrepo
|
||||
- Pros: Simpler individual repos, independent deployments
|
||||
- Cons: Dependency management nightmare, code duplication
|
||||
- Why rejected: Too much overhead for code sharing
|
||||
```
|
||||
|
||||
### ADR-0002: Adopt NgRx Signal Store
|
||||
|
||||
```markdown
|
||||
# ADR-0002: Adopt NgRx Signal Store for State Management
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
2024-03-01
|
||||
|
||||
## Context
|
||||
We need a state management solution that:
|
||||
- Integrates well with Angular signals
|
||||
- Provides predictable state updates
|
||||
- Supports devtools for debugging
|
||||
- Has good TypeScript support
|
||||
|
||||
## Decision
|
||||
We will use NgRx Signal Store for new features and gradually migrate existing stores.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Native signal integration
|
||||
- Simpler boilerplate than classic NgRx
|
||||
- Better performance with fine-grained reactivity
|
||||
- Excellent TypeScript inference
|
||||
|
||||
### Negative
|
||||
- Migration effort for existing NgRx stores
|
||||
- Different patterns from classic NgRx
|
||||
|
||||
### Neutral
|
||||
- Team needs to learn new API
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Classic NgRx (Store + Effects)
|
||||
- Pros: Mature, well-documented
|
||||
- Cons: Verbose boilerplate, doesn't leverage signals
|
||||
- Why rejected: Signal Store is the future direction
|
||||
|
||||
### Akita
|
||||
- Pros: Less boilerplate
|
||||
- Cons: Not Angular-native, less community support
|
||||
- Why rejected: NgRx has better Angular integration
|
||||
```
|
||||
|
||||
## ADR Naming Convention
|
||||
|
||||
```
|
||||
docs/adr/
|
||||
├── 0000-adr-template.md # Template file
|
||||
├── 0001-use-nx-monorepo.md
|
||||
├── 0002-adopt-ngrx-signal-store.md
|
||||
├── 0003-standalone-components.md
|
||||
└── README.md # Index of all ADRs
|
||||
```
|
||||
|
||||
## ADR Index Template
|
||||
|
||||
```markdown
|
||||
# Architecture Decision Records
|
||||
|
||||
This directory contains Architecture Decision Records (ADRs) for the ISA Frontend.
|
||||
|
||||
## Index
|
||||
|
||||
| ADR | Title | Status | Date |
|
||||
|-----|-------|--------|------|
|
||||
| [0001](0001-use-nx-monorepo.md) | Use Nx Monorepo | Accepted | 2024-01-15 |
|
||||
| [0002](0002-adopt-ngrx-signal-store.md) | Adopt NgRx Signal Store | Accepted | 2024-03-01 |
|
||||
| [0003](0003-standalone-components.md) | Migrate to Standalone Components | Accepted | 2024-04-01 |
|
||||
|
||||
## Process
|
||||
|
||||
1. Copy `0000-adr-template.md` to `XXXX-short-title.md`
|
||||
2. Fill in the template
|
||||
3. Submit PR for review
|
||||
4. Update status once decided
|
||||
5. Update this index
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **One decision per ADR** - Keep ADRs focused
|
||||
2. **Number sequentially** - Never reuse numbers
|
||||
3. **Record context** - Why was this needed?
|
||||
4. **Document alternatives** - Show what was considered
|
||||
5. **Keep concise** - 1-2 pages max
|
||||
6. **Update status** - Mark deprecated decisions
|
||||
7. **Link related ADRs** - Reference superseding decisions
|
||||
8. **Review regularly** - Quarterly ADR review meetings
|
||||
|
||||
## When to Write an ADR
|
||||
|
||||
Write an ADR when:
|
||||
- Choosing a framework or library
|
||||
- Defining code organization patterns
|
||||
- Setting up infrastructure
|
||||
- Establishing conventions
|
||||
- Making trade-offs that affect multiple teams
|
||||
|
||||
Don't write an ADR for:
|
||||
- Small implementation details
|
||||
- Obvious choices with no alternatives
|
||||
- Temporary solutions
|
||||
268
.claude/skills/architecture-documentation/references/arc42.md
Normal file
268
.claude/skills/architecture-documentation/references/arc42.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# Arc42 Architecture Documentation Template
|
||||
|
||||
## Overview
|
||||
|
||||
Arc42 is a template for architecture documentation with 12 sections covering all aspects of software architecture.
|
||||
|
||||
## Template Structure
|
||||
|
||||
### 1. Introduction and Goals
|
||||
|
||||
```markdown
|
||||
# 1. Introduction and Goals
|
||||
|
||||
## 1.1 Requirements Overview
|
||||
- Core business requirements driving the architecture
|
||||
- Key functional requirements
|
||||
- Quality goals and priorities
|
||||
|
||||
## 1.2 Quality Goals
|
||||
| Priority | Quality Goal | Description |
|
||||
|----------|-------------|-------------|
|
||||
| 1 | Performance | < 200ms response time |
|
||||
| 2 | Usability | Intuitive for store employees |
|
||||
| 3 | Reliability | 99.9% uptime during store hours |
|
||||
|
||||
## 1.3 Stakeholders
|
||||
| Role | Expectations |
|
||||
|------|-------------|
|
||||
| Store Employee | Fast, reliable daily operations |
|
||||
| IT Operations | Easy deployment and monitoring |
|
||||
| Development Team | Maintainable, testable code |
|
||||
```
|
||||
|
||||
### 2. Architecture Constraints
|
||||
|
||||
```markdown
|
||||
# 2. Architecture Constraints
|
||||
|
||||
## 2.1 Technical Constraints
|
||||
| Constraint | Background |
|
||||
|------------|------------|
|
||||
| Angular 20+ | Company standard frontend framework |
|
||||
| TypeScript strict | Type safety requirement |
|
||||
| Browser support | Chrome 90+, Edge 90+ |
|
||||
|
||||
## 2.2 Organizational Constraints
|
||||
| Constraint | Background |
|
||||
|------------|------------|
|
||||
| Monorepo | Nx-based shared codebase |
|
||||
| CI/CD | Azure DevOps pipelines |
|
||||
| Code review | All changes require PR approval |
|
||||
|
||||
## 2.3 Conventions
|
||||
- Conventional commits
|
||||
- ESLint/Prettier formatting
|
||||
- Component-driven development
|
||||
```
|
||||
|
||||
### 3. System Scope and Context
|
||||
|
||||
```markdown
|
||||
# 3. System Scope and Context
|
||||
|
||||
## 3.1 Business Context
|
||||
[C4 Level 1 - System Context Diagram]
|
||||
|
||||
| Neighbor | Description |
|
||||
|----------|-------------|
|
||||
| Store Employee | Primary user performing daily operations |
|
||||
| Backend APIs | Provides business logic and data |
|
||||
| Printer Service | Label and receipt printing |
|
||||
|
||||
## 3.2 Technical Context
|
||||
[Deployment/Network Diagram]
|
||||
|
||||
| Interface | Protocol | Description |
|
||||
|-----------|----------|-------------|
|
||||
| REST API | HTTPS/JSON | Backend communication |
|
||||
| WebSocket | WSS | Real-time updates |
|
||||
| OAuth2 | HTTPS | Authentication |
|
||||
```
|
||||
|
||||
### 4. Solution Strategy
|
||||
|
||||
```markdown
|
||||
# 4. Solution Strategy
|
||||
|
||||
## Key Architectural Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Angular SPA | Rich interactive UI, offline capability |
|
||||
| NgRx Signal Store | Predictable state management |
|
||||
| Nx Monorepo | Code sharing, consistent tooling |
|
||||
| Standalone Components | Better tree-shaking, simpler imports |
|
||||
|
||||
## Quality Achievement Strategies
|
||||
|
||||
| Quality Goal | Approach |
|
||||
|--------------|----------|
|
||||
| Performance | Lazy loading, caching, code splitting |
|
||||
| Maintainability | Domain-driven library structure |
|
||||
| Testability | Component isolation, dependency injection |
|
||||
```
|
||||
|
||||
### 5. Building Block View
|
||||
|
||||
```markdown
|
||||
# 5. Building Block View
|
||||
|
||||
## Level 1: Application Overview
|
||||
[C4 Container Diagram]
|
||||
|
||||
## Level 2: Domain Decomposition
|
||||
| Domain | Purpose | Libraries |
|
||||
|--------|---------|-----------|
|
||||
| CRM | Customer management | crm-feature-*, crm-data-access-* |
|
||||
| OMS | Order management | oms-feature-*, oms-data-access-* |
|
||||
| Checkout | Transactions | checkout-feature-*, checkout-data-access-* |
|
||||
|
||||
## Level 3: Feature Details
|
||||
[C4 Component Diagrams per domain]
|
||||
```
|
||||
|
||||
### 6. Runtime View
|
||||
|
||||
```markdown
|
||||
# 6. Runtime View
|
||||
|
||||
## Scenario 1: User Login
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
User->>App: Open application
|
||||
App->>Auth: Redirect to login
|
||||
Auth->>App: Return token
|
||||
App->>API: Fetch user profile
|
||||
API->>App: User data
|
||||
App->>User: Display dashboard
|
||||
```
|
||||
|
||||
## Scenario 2: Order Processing
|
||||
[Sequence diagram for order flow]
|
||||
```
|
||||
|
||||
### 7. Deployment View
|
||||
|
||||
```markdown
|
||||
# 7. Deployment View
|
||||
|
||||
## Infrastructure
|
||||
```mermaid
|
||||
graph TD
|
||||
CDN[CDN] --> Browser[User Browser]
|
||||
Browser --> LB[Load Balancer]
|
||||
LB --> API[API Gateway]
|
||||
API --> Services[Backend Services]
|
||||
```
|
||||
|
||||
## Environments
|
||||
| Environment | URL | Purpose |
|
||||
|-------------|-----|---------|
|
||||
| Development | dev.isa.local | Local development |
|
||||
| Staging | staging.isa.com | Integration testing |
|
||||
| Production | isa.com | Live system |
|
||||
```
|
||||
|
||||
### 8. Cross-cutting Concepts
|
||||
|
||||
```markdown
|
||||
# 8. Cross-cutting Concepts
|
||||
|
||||
## 8.1 Domain Model
|
||||
[Domain entity relationships]
|
||||
|
||||
## 8.2 Security
|
||||
- Authentication: OAuth2/OIDC
|
||||
- Authorization: Role-based access control
|
||||
- Data protection: HTTPS, encrypted storage
|
||||
|
||||
## 8.3 Error Handling
|
||||
- Global error interceptor
|
||||
- User-friendly error messages
|
||||
- Error logging to backend
|
||||
|
||||
## 8.4 Logging
|
||||
- @isa/core/logging library
|
||||
- Structured log format
|
||||
- Log levels: trace, debug, info, warn, error
|
||||
```
|
||||
|
||||
### 9. Architecture Decisions
|
||||
|
||||
```markdown
|
||||
# 9. Architecture Decisions
|
||||
|
||||
See [ADR folder](../adr/) for detailed decision records.
|
||||
|
||||
## Key Decisions
|
||||
- ADR-0001: Use Nx Monorepo
|
||||
- ADR-0002: Adopt NgRx Signal Store
|
||||
- ADR-0003: Migrate to Standalone Components
|
||||
```
|
||||
|
||||
### 10. Quality Requirements
|
||||
|
||||
```markdown
|
||||
# 10. Quality Requirements
|
||||
|
||||
## Quality Tree
|
||||
```
|
||||
Quality
|
||||
├── Performance
|
||||
│ ├── Response Time < 200ms
|
||||
│ └── Time to Interactive < 3s
|
||||
├── Reliability
|
||||
│ ├── Uptime 99.9%
|
||||
│ └── Graceful degradation
|
||||
└── Maintainability
|
||||
├── Test coverage > 80%
|
||||
└── Clear module boundaries
|
||||
```
|
||||
|
||||
## Quality Scenarios
|
||||
| Scenario | Measure | Target |
|
||||
|----------|---------|--------|
|
||||
| Page load | Time to interactive | < 3s |
|
||||
| API call | Response time | < 200ms |
|
||||
| Build | CI pipeline duration | < 10min |
|
||||
```
|
||||
|
||||
### 11. Risks and Technical Debt
|
||||
|
||||
```markdown
|
||||
# 11. Risks and Technical Debt
|
||||
|
||||
## Identified Risks
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|-------------|--------|------------|
|
||||
| Backend unavailable | Medium | High | Offline mode |
|
||||
| Performance degradation | Low | Medium | Monitoring |
|
||||
|
||||
## Technical Debt
|
||||
| Item | Priority | Effort |
|
||||
|------|----------|--------|
|
||||
| Legacy Jest tests | Medium | High |
|
||||
| Any types in codebase | High | Medium |
|
||||
```
|
||||
|
||||
### 12. Glossary
|
||||
|
||||
```markdown
|
||||
# 12. Glossary
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| ADR | Architecture Decision Record |
|
||||
| C4 | Context, Container, Component, Code model |
|
||||
| ISA | In-Store Application |
|
||||
| SPA | Single Page Application |
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start with sections 1-4** - Goals, constraints, context, strategy
|
||||
2. **Add diagrams to section 5** - Building block views
|
||||
3. **Document decisions in section 9** - Link to ADRs
|
||||
4. **Keep updated** - Review quarterly
|
||||
5. **Use templates** - Consistent formatting
|
||||
163
.claude/skills/architecture-documentation/references/c4-model.md
Normal file
163
.claude/skills/architecture-documentation/references/c4-model.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# C4 Model Reference
|
||||
|
||||
## Overview
|
||||
|
||||
The C4 model provides a way to visualize software architecture at four levels of abstraction:
|
||||
1. **Context** - System landscape
|
||||
2. **Container** - Applications and data stores
|
||||
3. **Component** - Internal structure
|
||||
4. **Code** - Class/module detail (optional)
|
||||
|
||||
## Level 1: System Context Diagram
|
||||
|
||||
Shows the system under design and its relationships with users and external systems.
|
||||
|
||||
### PlantUML Template
|
||||
```plantuml
|
||||
@startuml C4_Context
|
||||
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml
|
||||
|
||||
LAYOUT_WITH_LEGEND()
|
||||
|
||||
title System Context Diagram - ISA Frontend
|
||||
|
||||
Person(user, "Store Employee", "Uses the ISA application")
|
||||
Person(admin, "Administrator", "Manages system configuration")
|
||||
|
||||
System(isa, "ISA Frontend", "Angular application for in-store operations")
|
||||
|
||||
System_Ext(backend, "ISA Backend", "REST API services")
|
||||
System_Ext(auth, "Auth Provider", "Authentication service")
|
||||
System_Ext(printer, "Printer Service", "Receipt/label printing")
|
||||
|
||||
Rel(user, isa, "Uses", "Browser")
|
||||
Rel(admin, isa, "Configures", "Browser")
|
||||
Rel(isa, backend, "API calls", "HTTPS/JSON")
|
||||
Rel(isa, auth, "Authenticates", "OAuth2")
|
||||
Rel(isa, printer, "Prints", "WebSocket")
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
### Key Elements
|
||||
- **Person**: Human users of the system
|
||||
- **System**: The system being documented (highlighted)
|
||||
- **System_Ext**: External systems the system depends on
|
||||
- **Rel**: Relationships between elements
|
||||
|
||||
## Level 2: Container Diagram
|
||||
|
||||
Shows the high-level technology choices and how containers communicate.
|
||||
|
||||
### PlantUML Template
|
||||
```plantuml
|
||||
@startuml C4_Container
|
||||
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
|
||||
|
||||
LAYOUT_WITH_LEGEND()
|
||||
|
||||
title Container Diagram - ISA Frontend
|
||||
|
||||
Person(user, "Store Employee")
|
||||
|
||||
System_Boundary(isa, "ISA Frontend") {
|
||||
Container(spa, "SPA", "Angular 20", "Single-page application")
|
||||
Container(pwa, "Service Worker", "Workbox", "Offline capability")
|
||||
ContainerDb(storage, "Local Storage", "IndexedDB", "Offline data cache")
|
||||
}
|
||||
|
||||
System_Ext(api, "ISA API Gateway")
|
||||
System_Ext(cdn, "CDN", "Static assets")
|
||||
|
||||
Rel(user, spa, "Uses", "Browser")
|
||||
Rel(spa, pwa, "Registers")
|
||||
Rel(pwa, storage, "Caches data")
|
||||
Rel(spa, api, "API calls", "REST/JSON")
|
||||
Rel(spa, cdn, "Loads assets", "HTTPS")
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
### Container Types
|
||||
- **Container**: Application or service
|
||||
- **ContainerDb**: Database or data store
|
||||
- **ContainerQueue**: Message queue
|
||||
|
||||
## Level 3: Component Diagram
|
||||
|
||||
Shows the internal structure of a container.
|
||||
|
||||
### PlantUML Template
|
||||
```plantuml
|
||||
@startuml C4_Component
|
||||
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml
|
||||
|
||||
LAYOUT_WITH_LEGEND()
|
||||
|
||||
title Component Diagram - OMS Feature Module
|
||||
|
||||
Container_Boundary(oms, "OMS Feature") {
|
||||
Component(list, "Order List", "Angular Component", "Displays orders")
|
||||
Component(detail, "Order Detail", "Angular Component", "Order management")
|
||||
Component(store, "Order Store", "NgRx Signal Store", "State management")
|
||||
Component(api, "Order API Service", "Angular Service", "API communication")
|
||||
}
|
||||
|
||||
ContainerDb_Ext(backend, "OMS Backend API")
|
||||
|
||||
Rel(list, store, "Reads state")
|
||||
Rel(detail, store, "Reads/writes state")
|
||||
Rel(store, api, "Fetches data")
|
||||
Rel(api, backend, "HTTP requests")
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
## ISA-Frontend Domain Components
|
||||
|
||||
### Suggested Component Structure
|
||||
|
||||
```
|
||||
libs/[domain]/
|
||||
├── feature/ → Component diagrams
|
||||
│ └── [feature]/
|
||||
├── data-access/ → Store/API components
|
||||
│ └── [store]/
|
||||
├── ui/ → Presentational components
|
||||
│ └── [component]/
|
||||
└── util/ → Utility components
|
||||
└── [util]/
|
||||
```
|
||||
|
||||
### Domain Boundaries
|
||||
- **CRM**: Customer management, loyalty
|
||||
- **OMS**: Order management, returns
|
||||
- **Checkout**: Payment, transactions
|
||||
- **Remission**: Product returns processing
|
||||
- **Catalogue**: Product information
|
||||
|
||||
## Mermaid Alternative
|
||||
|
||||
```mermaid
|
||||
C4Context
|
||||
title System Context - ISA Frontend
|
||||
|
||||
Person(user, "Store Employee", "Daily operations")
|
||||
|
||||
System(isa, "ISA Frontend", "Angular SPA")
|
||||
|
||||
System_Ext(backend, "Backend Services")
|
||||
System_Ext(auth, "Auth Service")
|
||||
|
||||
Rel(user, isa, "Uses")
|
||||
Rel(isa, backend, "API calls")
|
||||
Rel(isa, auth, "Authenticates")
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **One diagram per level** - Don't mix abstraction levels
|
||||
2. **Consistent naming** - Use same names across diagrams
|
||||
3. **Show key relationships** - Not every possible connection
|
||||
4. **Include legends** - Explain notation
|
||||
5. **Keep it simple** - 5-20 elements per diagram max
|
||||
@@ -19,6 +19,7 @@ Use this skill when:
|
||||
**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
|
||||
- **[logging](../logging/SKILL.md)** - MANDATORY logging in all Angular components using `@isa/core/logging`
|
||||
|
||||
## Overview
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: logging-helper
|
||||
name: logging
|
||||
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.
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
# Jest to Vitest Migration Patterns
|
||||
|
||||
## Overview
|
||||
|
||||
This reference provides syntax mappings and patterns for migrating tests from Jest (with Spectator) to Vitest (with Angular Testing Library).
|
||||
|
||||
## Configuration Migration
|
||||
|
||||
### Jest Config → Vitest Config
|
||||
|
||||
**Before (jest.config.ts):**
|
||||
```typescript
|
||||
export default {
|
||||
displayName: 'my-lib',
|
||||
preset: '../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../coverage/libs/my-lib',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular',
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
**After (vitest.config.ts):**
|
||||
```typescript
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [angular()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
include: ['**/*.spec.ts'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Test Setup Migration
|
||||
|
||||
**Before (test-setup.ts - Jest):**
|
||||
```typescript
|
||||
import 'jest-preset-angular/setup-jest';
|
||||
```
|
||||
|
||||
**After (test-setup.ts - Vitest):**
|
||||
```typescript
|
||||
import '@analogjs/vitest-angular/setup-zone';
|
||||
```
|
||||
|
||||
## Import Changes
|
||||
|
||||
### Test Function Imports
|
||||
|
||||
**Before (Jest):**
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
|
||||
```
|
||||
|
||||
**After (Vitest):**
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
```
|
||||
|
||||
### Mock Imports
|
||||
|
||||
**Before (Jest):**
|
||||
```typescript
|
||||
jest.fn()
|
||||
jest.spyOn()
|
||||
jest.mock()
|
||||
jest.useFakeTimers()
|
||||
```
|
||||
|
||||
**After (Vitest):**
|
||||
```typescript
|
||||
vi.fn()
|
||||
vi.spyOn()
|
||||
vi.mock()
|
||||
vi.useFakeTimers()
|
||||
```
|
||||
|
||||
## Mock Migration Patterns
|
||||
|
||||
### Function Mocks
|
||||
|
||||
**Before (Jest):**
|
||||
```typescript
|
||||
const mockFn = jest.fn();
|
||||
const mockFnWithReturn = jest.fn().mockReturnValue('value');
|
||||
const mockFnWithAsync = jest.fn().mockResolvedValue('async value');
|
||||
```
|
||||
|
||||
**After (Vitest):**
|
||||
```typescript
|
||||
const mockFn = vi.fn();
|
||||
const mockFnWithReturn = vi.fn().mockReturnValue('value');
|
||||
const mockFnWithAsync = vi.fn().mockResolvedValue('async value');
|
||||
```
|
||||
|
||||
### Spy Migration
|
||||
|
||||
**Before (Jest):**
|
||||
```typescript
|
||||
const spy = jest.spyOn(service, 'method');
|
||||
spy.mockImplementation(() => 'mocked');
|
||||
```
|
||||
|
||||
**After (Vitest):**
|
||||
```typescript
|
||||
const spy = vi.spyOn(service, 'method');
|
||||
spy.mockImplementation(() => 'mocked');
|
||||
```
|
||||
|
||||
### Module Mocks
|
||||
|
||||
**Before (Jest):**
|
||||
```typescript
|
||||
jest.mock('@isa/core/logging', () => ({
|
||||
logger: jest.fn(() => ({
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
```
|
||||
|
||||
**After (Vitest):**
|
||||
```typescript
|
||||
vi.mock('@isa/core/logging', () => ({
|
||||
logger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
```
|
||||
|
||||
## Spectator → Angular Testing Library
|
||||
|
||||
### Component Testing
|
||||
|
||||
**Before (Spectator):**
|
||||
```typescript
|
||||
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
let spectator: Spectator<MyComponent>;
|
||||
const createComponent = createComponentFactory({
|
||||
component: MyComponent,
|
||||
imports: [CommonModule],
|
||||
providers: [
|
||||
{ provide: MyService, useValue: mockService },
|
||||
],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createComponent();
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
expect(spectator.query('.title')).toHaveText('Hello');
|
||||
});
|
||||
|
||||
it('should handle click', () => {
|
||||
spectator.click('.button');
|
||||
expect(mockService.doSomething).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**After (Angular Testing Library):**
|
||||
```typescript
|
||||
import { render, screen, fireEvent } from '@testing-library/angular';
|
||||
|
||||
describe('MyComponent', () => {
|
||||
it('should render title', async () => {
|
||||
await render(MyComponent, {
|
||||
imports: [CommonModule],
|
||||
providers: [
|
||||
{ provide: MyService, useValue: mockService },
|
||||
],
|
||||
});
|
||||
|
||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle click', async () => {
|
||||
await render(MyComponent, {
|
||||
providers: [{ provide: MyService, useValue: mockService }],
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button'));
|
||||
expect(mockService.doSomething).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Query Selectors
|
||||
|
||||
| Spectator | Angular Testing Library |
|
||||
|-----------|------------------------|
|
||||
| `spectator.query('.class')` | `screen.getByTestId()` or `screen.getByRole()` |
|
||||
| `spectator.queryAll('.class')` | `screen.getAllByRole()` |
|
||||
| `spectator.query('button')` | `screen.getByRole('button')` |
|
||||
| `spectator.query('[data-testid]')` | `screen.getByTestId()` |
|
||||
|
||||
### Events
|
||||
|
||||
| Spectator | Angular Testing Library |
|
||||
|-----------|------------------------|
|
||||
| `spectator.click(element)` | `fireEvent.click(element)` or `await userEvent.click(element)` |
|
||||
| `spectator.typeInElement(value, element)` | `await userEvent.type(element, value)` |
|
||||
| `spectator.blur(element)` | `fireEvent.blur(element)` |
|
||||
| `spectator.focus(element)` | `fireEvent.focus(element)` |
|
||||
|
||||
### Service Testing
|
||||
|
||||
**Before (Spectator):**
|
||||
```typescript
|
||||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
|
||||
|
||||
describe('MyService', () => {
|
||||
let spectator: SpectatorService<MyService>;
|
||||
const createService = createServiceFactory({
|
||||
service: MyService,
|
||||
providers: [
|
||||
{ provide: HttpClient, useValue: mockHttp },
|
||||
],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spectator = createService();
|
||||
});
|
||||
|
||||
it('should fetch data', () => {
|
||||
spectator.service.getData().subscribe(data => {
|
||||
expect(data).toEqual(expectedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**After (TestBed):**
|
||||
```typescript
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
describe('MyService', () => {
|
||||
let service: MyService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
MyService,
|
||||
{ provide: HttpClient, useValue: mockHttp },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(MyService);
|
||||
});
|
||||
|
||||
it('should fetch data', () => {
|
||||
service.getData().subscribe(data => {
|
||||
expect(data).toEqual(expectedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Async Testing
|
||||
|
||||
### Observable Testing
|
||||
|
||||
**Before (Jest):**
|
||||
```typescript
|
||||
it('should emit values', (done) => {
|
||||
service.data$.subscribe({
|
||||
next: (value) => {
|
||||
expect(value).toBe(expected);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**After (Vitest):**
|
||||
```typescript
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
it('should emit values', async () => {
|
||||
const value = await firstValueFrom(service.data$);
|
||||
expect(value).toBe(expected);
|
||||
});
|
||||
```
|
||||
|
||||
### Timer Mocks
|
||||
|
||||
**Before (Jest):**
|
||||
```typescript
|
||||
jest.useFakeTimers();
|
||||
service.startTimer();
|
||||
jest.advanceTimersByTime(1000);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
jest.useRealTimers();
|
||||
```
|
||||
|
||||
**After (Vitest):**
|
||||
```typescript
|
||||
vi.useFakeTimers();
|
||||
service.startTimer();
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
vi.useRealTimers();
|
||||
```
|
||||
|
||||
## Common Matchers
|
||||
|
||||
| Jest | Vitest |
|
||||
|------|--------|
|
||||
| `expect(x).toBe(y)` | Same |
|
||||
| `expect(x).toEqual(y)` | Same |
|
||||
| `expect(x).toHaveBeenCalled()` | Same |
|
||||
| `expect(x).toHaveBeenCalledWith(y)` | Same |
|
||||
| `expect(x).toMatchSnapshot()` | `expect(x).toMatchSnapshot()` |
|
||||
| `expect(x).toHaveText('text')` | `expect(x).toHaveTextContent('text')` (with jest-dom) |
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
1. [ ] Update `vitest.config.ts`
|
||||
2. [ ] Update `test-setup.ts`
|
||||
3. [ ] Replace `jest.fn()` with `vi.fn()`
|
||||
4. [ ] Replace `jest.spyOn()` with `vi.spyOn()`
|
||||
5. [ ] Replace `jest.mock()` with `vi.mock()`
|
||||
6. [ ] Replace Spectator with Angular Testing Library
|
||||
7. [ ] Update queries to use accessible selectors
|
||||
8. [ ] Update async patterns
|
||||
9. [ ] Run tests and fix any remaining issues
|
||||
10. [ ] Remove Jest dependencies from `package.json`
|
||||
293
.claude/skills/type-safety-engineer/references/zod-patterns.md
Normal file
293
.claude/skills/type-safety-engineer/references/zod-patterns.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# Zod Patterns Reference
|
||||
|
||||
## Overview
|
||||
|
||||
Zod is a TypeScript-first schema validation library. Use it for runtime validation at system boundaries (API responses, form inputs, external data).
|
||||
|
||||
## Basic Schema Patterns
|
||||
|
||||
### Primitive Types
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
// Basic types
|
||||
const stringSchema = z.string();
|
||||
const numberSchema = z.number();
|
||||
const booleanSchema = z.boolean();
|
||||
const dateSchema = z.date();
|
||||
|
||||
// With constraints
|
||||
const emailSchema = z.string().email();
|
||||
const positiveNumber = z.number().positive();
|
||||
const nonEmptyString = z.string().min(1);
|
||||
const optionalString = z.string().optional();
|
||||
const nullableString = z.string().nullable();
|
||||
```
|
||||
|
||||
### Object Schemas
|
||||
|
||||
```typescript
|
||||
// Basic object
|
||||
const userSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1),
|
||||
age: z.number().int().positive().optional(),
|
||||
role: z.enum(['admin', 'user', 'guest']),
|
||||
createdAt: z.string().datetime(),
|
||||
});
|
||||
|
||||
// Infer TypeScript type from schema
|
||||
type User = z.infer<typeof userSchema>;
|
||||
|
||||
// Partial and Pick utilities
|
||||
const partialUser = userSchema.partial(); // All fields optional
|
||||
const requiredUser = userSchema.required(); // All fields required
|
||||
const pickedUser = userSchema.pick({ email: true, name: true });
|
||||
const omittedUser = userSchema.omit({ createdAt: true });
|
||||
```
|
||||
|
||||
### Array Schemas
|
||||
|
||||
```typescript
|
||||
// Basic array
|
||||
const stringArray = z.array(z.string());
|
||||
|
||||
// With constraints
|
||||
const nonEmptyArray = z.array(z.string()).nonempty();
|
||||
const limitedArray = z.array(z.number()).min(1).max(10);
|
||||
|
||||
// Tuple (fixed length, different types)
|
||||
const coordinate = z.tuple([z.number(), z.number()]);
|
||||
```
|
||||
|
||||
## API Response Validation
|
||||
|
||||
### Pattern: Validate API Responses
|
||||
|
||||
```typescript
|
||||
// Define response schema
|
||||
const apiResponseSchema = z.object({
|
||||
data: z.object({
|
||||
items: z.array(userSchema),
|
||||
total: z.number(),
|
||||
page: z.number(),
|
||||
pageSize: z.number(),
|
||||
}),
|
||||
meta: z.object({
|
||||
timestamp: z.string().datetime(),
|
||||
requestId: z.string().uuid(),
|
||||
}),
|
||||
});
|
||||
|
||||
// In Angular service
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UserService {
|
||||
#http = inject(HttpClient);
|
||||
#logger = logger({ component: 'UserService' });
|
||||
|
||||
getUsers(): Observable<User[]> {
|
||||
return this.#http.get('/api/users').pipe(
|
||||
map((response) => {
|
||||
const result = apiResponseSchema.safeParse(response);
|
||||
if (!result.success) {
|
||||
this.#logger.error('Invalid API response', {
|
||||
errors: result.error.errors
|
||||
});
|
||||
throw new Error('Invalid API response');
|
||||
}
|
||||
return result.data.data.items;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Coerce Types
|
||||
|
||||
```typescript
|
||||
// API returns string IDs, coerce to number
|
||||
const productSchema = z.object({
|
||||
id: z.coerce.number(), // "123" -> 123
|
||||
price: z.coerce.number(), // "99.99" -> 99.99
|
||||
inStock: z.coerce.boolean(), // "true" -> true
|
||||
createdAt: z.coerce.date(), // "2024-01-01" -> Date
|
||||
});
|
||||
```
|
||||
|
||||
## Form Validation
|
||||
|
||||
### Pattern: Form Schema
|
||||
|
||||
```typescript
|
||||
// Define form schema with custom error messages
|
||||
const loginFormSchema = z.object({
|
||||
email: z.string()
|
||||
.email({ message: 'Invalid email address' }),
|
||||
password: z.string()
|
||||
.min(8, { message: 'Password must be at least 8 characters' })
|
||||
.regex(/[A-Z]/, { message: 'Must contain uppercase letter' })
|
||||
.regex(/[0-9]/, { message: 'Must contain number' }),
|
||||
rememberMe: z.boolean().default(false),
|
||||
});
|
||||
|
||||
// Use with Angular forms
|
||||
type LoginForm = z.infer<typeof loginFormSchema>;
|
||||
```
|
||||
|
||||
### Pattern: Cross-field Validation
|
||||
|
||||
```typescript
|
||||
const passwordFormSchema = z.object({
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string(),
|
||||
}).refine(
|
||||
(data) => data.password === data.confirmPassword,
|
||||
{
|
||||
message: "Passwords don't match",
|
||||
path: ['confirmPassword'], // Error path
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Type Guards
|
||||
|
||||
### Pattern: Create Type Guard from Schema
|
||||
|
||||
```typescript
|
||||
// Schema
|
||||
const customerSchema = z.object({
|
||||
type: z.literal('customer'),
|
||||
customerId: z.string(),
|
||||
loyaltyPoints: z.number(),
|
||||
});
|
||||
|
||||
// Type guard function
|
||||
function isCustomer(value: unknown): value is z.infer<typeof customerSchema> {
|
||||
return customerSchema.safeParse(value).success;
|
||||
}
|
||||
|
||||
// Usage
|
||||
if (isCustomer(data)) {
|
||||
console.log(data.loyaltyPoints); // Type-safe access
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Discriminated Unions
|
||||
|
||||
```typescript
|
||||
const customerSchema = z.object({
|
||||
type: z.literal('customer'),
|
||||
customerId: z.string(),
|
||||
});
|
||||
|
||||
const guestSchema = z.object({
|
||||
type: z.literal('guest'),
|
||||
sessionId: z.string(),
|
||||
});
|
||||
|
||||
const userSchema = z.discriminatedUnion('type', [
|
||||
customerSchema,
|
||||
guestSchema,
|
||||
]);
|
||||
|
||||
type User = z.infer<typeof userSchema>;
|
||||
// User = { type: 'customer'; customerId: string } | { type: 'guest'; sessionId: string }
|
||||
```
|
||||
|
||||
## Replacing `any` Types
|
||||
|
||||
### Before (unsafe)
|
||||
|
||||
```typescript
|
||||
function processOrder(order: any) {
|
||||
// No type safety
|
||||
console.log(order.items.length);
|
||||
console.log(order.customer.name);
|
||||
}
|
||||
```
|
||||
|
||||
### After (with Zod)
|
||||
|
||||
```typescript
|
||||
const orderSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
items: z.array(z.object({
|
||||
productId: z.string(),
|
||||
quantity: z.number().positive(),
|
||||
price: z.number().nonnegative(),
|
||||
})),
|
||||
customer: z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
}),
|
||||
status: z.enum(['pending', 'confirmed', 'shipped', 'delivered']),
|
||||
});
|
||||
|
||||
type Order = z.infer<typeof orderSchema>;
|
||||
|
||||
function processOrder(input: unknown): Order {
|
||||
const order = orderSchema.parse(input); // Throws if invalid
|
||||
console.log(order.items.length); // Type-safe
|
||||
console.log(order.customer.name); // Type-safe
|
||||
return order;
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Pattern: Structured Error Handling
|
||||
|
||||
```typescript
|
||||
const result = schema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
// Access formatted errors
|
||||
const formatted = result.error.format();
|
||||
|
||||
// Access flat error list
|
||||
const flat = result.error.flatten();
|
||||
|
||||
// Custom error mapping
|
||||
const errors = result.error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
code: err.code,
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
## Transform Patterns
|
||||
|
||||
### Pattern: Transform on Parse
|
||||
|
||||
```typescript
|
||||
const userInputSchema = z.object({
|
||||
email: z.string().email().transform(s => s.toLowerCase()),
|
||||
name: z.string().transform(s => s.trim()),
|
||||
tags: z.string().transform(s => s.split(',')),
|
||||
});
|
||||
|
||||
// Input: { email: "USER@EXAMPLE.COM", name: " John ", tags: "a,b,c" }
|
||||
// Output: { email: "user@example.com", name: "John", tags: ["a", "b", "c"] }
|
||||
```
|
||||
|
||||
### Pattern: Default Values
|
||||
|
||||
```typescript
|
||||
const configSchema = z.object({
|
||||
theme: z.enum(['light', 'dark']).default('light'),
|
||||
pageSize: z.number().default(20),
|
||||
features: z.array(z.string()).default([]),
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Define schemas at module boundaries** - API services, form handlers
|
||||
2. **Use `safeParse` for error handling** - Don't let validation throw unexpectedly
|
||||
3. **Infer types from schemas** - Single source of truth
|
||||
4. **Add meaningful error messages** - Help debugging and user feedback
|
||||
5. **Use transforms for normalization** - Clean data on parse
|
||||
6. **Keep schemas close to usage** - Colocate with services/components
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isDevMode, NgModule } from '@angular/core';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import {
|
||||
CanActivateCartGuard,
|
||||
@@ -7,23 +7,17 @@ import {
|
||||
CanActivateCustomerOrdersGuard,
|
||||
CanActivateCustomerOrdersWithProcessIdGuard,
|
||||
CanActivateCustomerWithProcessIdGuard,
|
||||
CanActivateGoodsInGuard,
|
||||
CanActivateProductGuard,
|
||||
CanActivateProductWithProcessIdGuard,
|
||||
CanActivateTaskCalendarGuard,
|
||||
IsAuthenticatedGuard,
|
||||
} from './guards';
|
||||
import { CanActivateAssortmentGuard } from './guards/can-activate-assortment.guard';
|
||||
import { CanActivatePackageInspectionGuard } from './guards/can-activate-package-inspection.guard';
|
||||
import { MainComponent } from './main.component';
|
||||
import { PreviewComponent } from './preview';
|
||||
import {
|
||||
BranchSectionResolver,
|
||||
CustomerSectionResolver,
|
||||
ProcessIdResolver,
|
||||
} from './resolvers';
|
||||
import { TokenLoginComponent, TokenLoginModule } from './token-login';
|
||||
import { ProcessIdGuard } from './guards/process-id.guard';
|
||||
import {
|
||||
ActivateProcessIdGuard,
|
||||
ActivateProcessIdWithConfigKeyGuard,
|
||||
@@ -74,8 +68,12 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('@page/catalog').then((m) => m.PageCatalogModule),
|
||||
canActivate: [CanActivateProductWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
// TODO: Check if order and :processId/order is still being used
|
||||
// If not, remove these routes and the related guards and resolvers
|
||||
{
|
||||
path: 'order',
|
||||
loadChildren: () =>
|
||||
@@ -87,7 +85,9 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('@page/customer-order').then((m) => m.CustomerOrderModule),
|
||||
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'customer',
|
||||
@@ -100,7 +100,9 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('@page/customer').then((m) => m.CustomerModule),
|
||||
canActivate: [CanActivateCustomerWithProcessIdGuard],
|
||||
resolve: { processId: ProcessIdResolver },
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'cart',
|
||||
@@ -113,10 +115,13 @@ const routes: Routes = [
|
||||
loadChildren: () =>
|
||||
import('@page/checkout').then((m) => m.PageCheckoutModule),
|
||||
canActivate: [CanActivateCartWithProcessIdGuard],
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'pickup-shelf',
|
||||
canActivate: [ProcessIdGuard],
|
||||
canActivate: [ActivateProcessIdGuard],
|
||||
// NOTE: This is a workaround for the canActivate guard not being called
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
|
||||
@@ -126,6 +131,9 @@ const routes: Routes = [
|
||||
canActivate: [ActivateProcessIdGuard],
|
||||
loadChildren: () =>
|
||||
import('@page/pickup-shelf').then((m) => m.PickupShelfOutModule),
|
||||
resolve: {
|
||||
processId: ProcessIdResolver,
|
||||
},
|
||||
},
|
||||
{ path: '**', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
],
|
||||
@@ -141,7 +149,7 @@ const routes: Routes = [
|
||||
import('@page/task-calendar').then(
|
||||
(m) => m.PageTaskCalendarModule,
|
||||
),
|
||||
canActivate: [CanActivateTaskCalendarGuard],
|
||||
canActivate: [ActivateProcessIdWithConfigKeyGuard('taskCalendar')],
|
||||
},
|
||||
{
|
||||
path: 'pickup-shelf',
|
||||
@@ -154,27 +162,23 @@ const routes: Routes = [
|
||||
path: 'goods/in',
|
||||
loadChildren: () =>
|
||||
import('@page/goods-in').then((m) => m.GoodsInModule),
|
||||
canActivate: [CanActivateGoodsInGuard],
|
||||
canActivate: [ActivateProcessIdWithConfigKeyGuard('goodsIn')],
|
||||
},
|
||||
// {
|
||||
// path: 'remission',
|
||||
// loadChildren: () =>
|
||||
// import('@page/remission').then((m) => m.PageRemissionModule),
|
||||
// canActivate: [CanActivateRemissionGuard],
|
||||
// },
|
||||
{
|
||||
path: 'package-inspection',
|
||||
loadChildren: () =>
|
||||
import('@page/package-inspection').then(
|
||||
(m) => m.PackageInspectionModule,
|
||||
),
|
||||
canActivate: [CanActivatePackageInspectionGuard],
|
||||
canActivate: [
|
||||
ActivateProcessIdWithConfigKeyGuard('packageInspection'),
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'assortment',
|
||||
loadChildren: () =>
|
||||
import('@page/assortment').then((m) => m.AssortmentModule),
|
||||
canActivate: [CanActivateAssortmentGuard],
|
||||
canActivate: [ActivateProcessIdWithConfigKeyGuard('assortment')],
|
||||
},
|
||||
{ path: '**', redirectTo: 'task-calendar', pathMatch: 'full' },
|
||||
],
|
||||
@@ -243,13 +247,6 @@ const routes: Routes = [
|
||||
},
|
||||
];
|
||||
|
||||
if (isDevMode()) {
|
||||
routes.unshift({
|
||||
path: 'preview',
|
||||
component: PreviewComponent,
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { version } from '../../../../package.json';
|
||||
import { IsaTitleStrategy } from '@isa/common/title-management';
|
||||
import {
|
||||
HTTP_INTERCEPTORS,
|
||||
provideHttpClient,
|
||||
@@ -56,7 +57,6 @@ import {
|
||||
ScanditScanAdapterModule,
|
||||
} from '@adapter/scan';
|
||||
import * as Commands from './commands';
|
||||
import { PreviewComponent } from './preview';
|
||||
import { NativeContainerService } from '@external/native-container';
|
||||
import { ShellModule } from '@shared/shell';
|
||||
import { MainComponent } from './main.component';
|
||||
@@ -87,6 +87,7 @@ import {
|
||||
import { Store } from '@ngrx/store';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import z from 'zod';
|
||||
import { TitleStrategy } from '@angular/router';
|
||||
import { TabNavigationService } from '@isa/core/tabs';
|
||||
|
||||
registerLocaleData(localeDe, localeDeExtra);
|
||||
@@ -270,7 +271,6 @@ const USER_SUB_FACTORY = () => {
|
||||
CoreCommandModule.forRoot(Object.values(Commands)),
|
||||
CoreLoggerModule.forRoot(),
|
||||
AppStoreModule,
|
||||
PreviewComponent,
|
||||
AuthModule.forRoot(),
|
||||
CoreApplicationModule.forRoot(),
|
||||
UiModalModule.forRoot(),
|
||||
@@ -330,6 +330,7 @@ const USER_SUB_FACTORY = () => {
|
||||
useValue: 'EUR',
|
||||
},
|
||||
provideUserSubFactory(USER_SUB_FACTORY),
|
||||
{ provide: TitleStrategy, useClass: IsaTitleStrategy },
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,47 +1,82 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
export const ActivateProcessIdGuard: CanActivateFn = async (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
) => {
|
||||
const application = inject(ApplicationService);
|
||||
|
||||
const processIdStr = route.params.processId;
|
||||
|
||||
if (!processIdStr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const processId = Number(processIdStr);
|
||||
|
||||
// Check if Process already exists
|
||||
const process = await application.getProcessById$(processId).pipe(take(1)).toPromise();
|
||||
|
||||
if (!process) {
|
||||
application.createCustomerProcess(processId);
|
||||
}
|
||||
|
||||
application.activateProcess(processId);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const ActivateProcessIdWithConfigKeyGuard: (key: string) => CanActivateFn =
|
||||
(key) => async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||
const application = inject(ApplicationService);
|
||||
const config = inject(Config);
|
||||
|
||||
const processId = config.get(`process.ids.${key}`);
|
||||
|
||||
if (isNaN(processId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
application.activateProcess(processId);
|
||||
|
||||
return true;
|
||||
};
|
||||
import { inject } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
CanActivateFn,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { take } from 'rxjs/operators';
|
||||
import z from 'zod';
|
||||
|
||||
export const ActivateProcessIdGuard: CanActivateFn = async (
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot,
|
||||
) => {
|
||||
const application = inject(ApplicationService);
|
||||
|
||||
const processIdStr = route.params.processId;
|
||||
|
||||
if (!processIdStr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const processId = Number(processIdStr);
|
||||
|
||||
// Check if Process already exists
|
||||
const process = await application
|
||||
.getProcessById$(processId)
|
||||
.pipe(take(1))
|
||||
.toPromise();
|
||||
|
||||
if (!process) {
|
||||
application.createProcess({
|
||||
id: processId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name: `Vorgang ${processId}`,
|
||||
});
|
||||
}
|
||||
|
||||
application.activateProcess(processId);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const ActivateProcessIdWithConfigKeyGuard: (
|
||||
key: string,
|
||||
) => CanActivateFn =
|
||||
(key) =>
|
||||
async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
|
||||
const application = inject(ApplicationService);
|
||||
const config = inject(Config);
|
||||
|
||||
const processId = config.get(`process.ids.${key}`, z.coerce.number());
|
||||
|
||||
if (typeof processId !== 'number' || isNaN(processId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const processTitle = config.get(
|
||||
`process.titles.${key}`,
|
||||
z.string().default(key),
|
||||
);
|
||||
|
||||
const process = await application
|
||||
.getProcessById$(processId)
|
||||
.pipe(take(1))
|
||||
.toPromise();
|
||||
|
||||
if (!process) {
|
||||
await application.createProcess({
|
||||
id: processId,
|
||||
type: key,
|
||||
section: 'customer', // Not important anymore
|
||||
name: processTitle,
|
||||
});
|
||||
}
|
||||
|
||||
application.activateProcess(processId);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { z } from 'zod';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateGoodsInGuard {
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _config: Config,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const pid = this._config.get('process.ids.goodsIn', z.number());
|
||||
const process = await this._applicationService
|
||||
.getProcessById$(pid)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (!process) {
|
||||
await this._applicationService.createProcess({
|
||||
id: this._config.get('process.ids.goodsIn'),
|
||||
type: 'goods-in',
|
||||
section: 'branch',
|
||||
name: '',
|
||||
});
|
||||
}
|
||||
this._applicationService.activateProcess(
|
||||
this._config.get('process.ids.goodsIn'),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { Config } from '@core/config';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { z } from 'zod';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateGoodsInGuard {
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _config: Config,
|
||||
) {}
|
||||
|
||||
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const pid = this._config.get('process.ids.goodsIn', z.number());
|
||||
const process = await this._applicationService
|
||||
.getProcessById$(pid)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (!process) {
|
||||
await this._applicationService.createProcess({
|
||||
id: this._config.get('process.ids.goodsIn'),
|
||||
type: 'goods-in',
|
||||
section: 'branch',
|
||||
name: 'Abholfach',
|
||||
});
|
||||
}
|
||||
this._applicationService.activateProcess(
|
||||
this._config.get('process.ids.goodsIn'),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// start:ng42.barrel
|
||||
export * from './preview.component';
|
||||
// end:ng42.barrel
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
@apply grid min-h-screen content-center justify-center;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<h1>Platform: {{ platform | json }}</h1>
|
||||
<br />
|
||||
<h1>{{ appVersion }}</h1>
|
||||
<br />
|
||||
<h1>{{ userAgent }}</h1>
|
||||
<br />
|
||||
<h1>Navigator: {{ navigator | json }}</h1>
|
||||
<br />
|
||||
<br />
|
||||
<h1>Device: {{ device }}</h1>
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Platform, PlatformModule } from '@angular/cdk/platform';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { BranchDTO } from '@generated/swagger/checkout-api';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-preview',
|
||||
templateUrl: 'preview.component.html',
|
||||
styleUrls: ['preview.component.css'],
|
||||
imports: [CommonModule, PlatformModule],
|
||||
})
|
||||
export class PreviewComponent {
|
||||
selectedBranch$ = new BehaviorSubject<BranchDTO>({});
|
||||
|
||||
get appVersion() {
|
||||
return 'App Version: ' + (window.navigator as any).appVersion;
|
||||
}
|
||||
|
||||
get userAgent() {
|
||||
return 'User Agent: ' + (window.navigator as any).userAgent;
|
||||
}
|
||||
|
||||
get navigator() {
|
||||
const nav = {};
|
||||
for (const i in window.navigator) nav[i] = navigator[i];
|
||||
return nav;
|
||||
}
|
||||
|
||||
get platform() {
|
||||
return this._platform;
|
||||
}
|
||||
|
||||
get device() {
|
||||
const isIpadNative = this._platform.IOS && !this._platform.SAFARI;
|
||||
const isIpadMini6Native = window?.navigator?.userAgent?.includes('Macintosh') && !this._platform.SAFARI;
|
||||
const isNative = isIpadNative || isIpadMini6Native;
|
||||
const isPWA = this._platform.IOS && this._platform.SAFARI;
|
||||
const isDesktop = !isNative && !isPWA;
|
||||
if (isNative) {
|
||||
if (isIpadMini6Native) {
|
||||
return 'IPAD mini 6 Native App';
|
||||
} else if (isIpadNative) {
|
||||
return 'IPAD mini 2 Native App or IPAD mini 5 Native App';
|
||||
}
|
||||
} else if (isPWA) {
|
||||
return 'IPAD Safari PWA';
|
||||
} else if (isDesktop) return 'Desktop or Macintosh';
|
||||
}
|
||||
|
||||
constructor(private readonly _platform: Platform) {}
|
||||
|
||||
setNewBranch(branch: BranchDTO) {
|
||||
this.selectedBranch$.next(branch);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,2 @@
|
||||
// start:ng42.barrel
|
||||
export * from './process-id.resolver';
|
||||
export * from './section.resolver';
|
||||
// end:ng42.barrel
|
||||
export * from './process-id.resolver';
|
||||
export * from './section.resolver';
|
||||
|
||||
@@ -1,87 +1,86 @@
|
||||
{
|
||||
"title": "ISA - Local",
|
||||
"silentRefresh": {
|
||||
"interval": 300000
|
||||
},
|
||||
"debug": true,
|
||||
"dev-scanner": true,
|
||||
"@cdn/product-image": {
|
||||
"url": "https://produktbilder.paragon-data.net"
|
||||
},
|
||||
"@core/auth": {
|
||||
"issuer": "https://sso-test.paragon-data.de",
|
||||
"clientId": "isa-client",
|
||||
"responseType": "code",
|
||||
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi",
|
||||
"showDebugInformation": true
|
||||
},
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
|
||||
},
|
||||
"@swagger/cat": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/catsearch/v6"
|
||||
},
|
||||
"@swagger/av": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/ava/v6"
|
||||
},
|
||||
"@swagger/checkout": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/checkout/v6"
|
||||
},
|
||||
"@swagger/crm": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/crm/v6"
|
||||
},
|
||||
"@swagger/oms": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/oms/v6"
|
||||
},
|
||||
"@swagger/print": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/print/v1"
|
||||
},
|
||||
"@swagger/eis": {
|
||||
"rootUrl": "https://filialinformationsystem-test.paragon-systems.de/eiswebapi/v1"
|
||||
},
|
||||
"@swagger/remi": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/inv/v6"
|
||||
},
|
||||
"@swagger/wws": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/wws/v1"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"hubs": {
|
||||
"notifications": {
|
||||
"url": "https://isa-test.paragon-data.net/isa/v1/rt",
|
||||
"enableAutomaticReconnect": false,
|
||||
"httpOptions": {
|
||||
"transport": 1,
|
||||
"logMessageContent": true,
|
||||
"skipNegotiation": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"ids": {
|
||||
"goodsOut": 1000,
|
||||
"goodsIn": 2000,
|
||||
"taskCalendar": 3000,
|
||||
"remission": 4000,
|
||||
"packageInspection": 5000,
|
||||
"assortment": 6000,
|
||||
"pickupShelf": 7000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
"licence": {
|
||||
"scandit": "Ae8F2Wx2RMq5Lvn7UUAlWzVFZTt2+ubMAF8XtDpmPlNkBeG/LWs1M7AbgDW0LQqYLnszClEENaEHS56/6Ts2vrJ1Ux03CXUjK3jUvZpF5OchXR1CpnmpepJ6WxPCd7LMVHUGG1BbwPLDTFjP3y8uT0caTSmmGrYQWAs4CZcEF+ZBabP0z7vfm+hCZF/ebj9qqCJZcW8nH/n19hohshllzYBjFXjh87P2lIh1s6yZS3OaQWWXo/o0AKdxx7T6CVyR0/G5zq6uYJWf6rs3euUBEhpzOZHbHZK86Lvy2AVBEyVkkcttlDW1J2fA4l1W1JV/Xibz8AQV6kG482EpGF42KEoK48paZgX3e1AQsqUtmqzw294dcP4zMVstnw5/WrwKKi/5E/nOOJT2txYP1ZufIjPrwNFsqTlv7xCQlHjMzFGYwT816yD5qLRLbwOtjrkUPXNZLZ06T4upvWwJDmm8XgdeoDqMjHdcO4lwji1bl9EiIYJ/2qnsk9yZ2FqSaHzn4cbiL0f5u2HFlNAP0GUujGRlthGhHi6o4dFU+WAxKsFMKVt+SfoQUazNKHFVQgiAklTIZxIc/HUVzRvOLMxf+wFDerraBtcqGJg+g/5mrWYqeDBGhCBHtKiYf6244IJ4afzNTiH1/30SJcRzXwbEa3A7q1fJTx9/nLTOfVPrJKBQs7f/OQs2dA7LDCel8mzXdbjvsNQaeU5+iCIAq6zbTNKy1xT8wwj+VZrQmtNJs+qeznD+u29nCM24h8xCmRpvNPo4/Mww/lrTNrrNwLBSn1pMIwsH7yS9hH0v0oNAM3A6bVtk1D9qEkbyw+xZa+MZGpMP0D0CdcsqHalPcm5r/Ik="
|
||||
},
|
||||
"gender": {
|
||||
"0": "Keine Anrede",
|
||||
"1": "Enby",
|
||||
"2": "Herr",
|
||||
"4": "Frau"
|
||||
},
|
||||
"@shared/icon": "/assets/icons.json"
|
||||
}
|
||||
{
|
||||
"title": "ISA - Local",
|
||||
"silentRefresh": {
|
||||
"interval": 300000
|
||||
},
|
||||
"debug": true,
|
||||
"dev-scanner": true,
|
||||
"@cdn/product-image": {
|
||||
"url": "https://produktbilder.paragon-data.net"
|
||||
},
|
||||
"@core/auth": {
|
||||
"issuer": "https://sso-test.paragon-data.de",
|
||||
"clientId": "isa-client",
|
||||
"responseType": "code",
|
||||
"scope": "openid profile cmf_user isa-isa-webapi isa-checkout-webapi isa-cat-webapi isa-ava-webapi isa-crm-webapi isa-review-webapi isa-kpi-webapi isa-oms-webapi isa-nbo-webapi isa-print-webapi eis-service isa-inv-webapi isa-wws-webapi",
|
||||
"showDebugInformation": true
|
||||
},
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
|
||||
},
|
||||
"@swagger/cat": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/catsearch/v6"
|
||||
},
|
||||
"@swagger/av": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/ava/v6"
|
||||
},
|
||||
"@swagger/checkout": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/checkout/v6"
|
||||
},
|
||||
"@swagger/crm": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/crm/v6"
|
||||
},
|
||||
"@swagger/oms": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/oms/v6"
|
||||
},
|
||||
"@swagger/print": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/print/v1"
|
||||
},
|
||||
"@swagger/eis": {
|
||||
"rootUrl": "https://filialinformationsystem-test.paragon-systems.de/eiswebapi/v1"
|
||||
},
|
||||
"@swagger/remi": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/inv/v6"
|
||||
},
|
||||
"@swagger/wws": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/wws/v1"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"hubs": {
|
||||
"notifications": {
|
||||
"url": "https://isa-test.paragon-data.net/isa/v1/rt",
|
||||
"enableAutomaticReconnect": false,
|
||||
"httpOptions": {
|
||||
"transport": 1,
|
||||
"logMessageContent": true,
|
||||
"skipNegotiation": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"process": {
|
||||
"ids": {
|
||||
"goodsOut": 1000,
|
||||
"goodsIn": 2000,
|
||||
"taskCalendar": 3000,
|
||||
"packageInspection": 5000,
|
||||
"assortment": 6000,
|
||||
"pickupShelf": 2000
|
||||
}
|
||||
},
|
||||
"checkForUpdates": 3600000,
|
||||
"licence": {
|
||||
"scandit": "Ae8F2Wx2RMq5Lvn7UUAlWzVFZTt2+ubMAF8XtDpmPlNkBeG/LWs1M7AbgDW0LQqYLnszClEENaEHS56/6Ts2vrJ1Ux03CXUjK3jUvZpF5OchXR1CpnmpepJ6WxPCd7LMVHUGG1BbwPLDTFjP3y8uT0caTSmmGrYQWAs4CZcEF+ZBabP0z7vfm+hCZF/ebj9qqCJZcW8nH/n19hohshllzYBjFXjh87P2lIh1s6yZS3OaQWWXo/o0AKdxx7T6CVyR0/G5zq6uYJWf6rs3euUBEhpzOZHbHZK86Lvy2AVBEyVkkcttlDW1J2fA4l1W1JV/Xibz8AQV6kG482EpGF42KEoK48paZgX3e1AQsqUtmqzw294dcP4zMVstnw5/WrwKKi/5E/nOOJT2txYP1ZufIjPrwNFsqTlv7xCQlHjMzFGYwT816yD5qLRLbwOtjrkUPXNZLZ06T4upvWwJDmm8XgdeoDqMjHdcO4lwji1bl9EiIYJ/2qnsk9yZ2FqSaHzn4cbiL0f5u2HFlNAP0GUujGRlthGhHi6o4dFU+WAxKsFMKVt+SfoQUazNKHFVQgiAklTIZxIc/HUVzRvOLMxf+wFDerraBtcqGJg+g/5mrWYqeDBGhCBHtKiYf6244IJ4afzNTiH1/30SJcRzXwbEa3A7q1fJTx9/nLTOfVPrJKBQs7f/OQs2dA7LDCel8mzXdbjvsNQaeU5+iCIAq6zbTNKy1xT8wwj+VZrQmtNJs+qeznD+u29nCM24h8xCmRpvNPo4/Mww/lrTNrrNwLBSn1pMIwsH7yS9hH0v0oNAM3A6bVtk1D9qEkbyw+xZa+MZGpMP0D0CdcsqHalPcm5r/Ik="
|
||||
},
|
||||
"gender": {
|
||||
"0": "Keine Anrede",
|
||||
"1": "Enby",
|
||||
"2": "Herr",
|
||||
"4": "Frau"
|
||||
},
|
||||
"@shared/icon": "/assets/icons.json"
|
||||
}
|
||||
|
||||
@@ -1,174 +1,196 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BranchDTO } from '@generated/swagger/checkout-api';
|
||||
import { isBoolean, isNumber } from '@utils/common';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { ApplicationProcess } from './defs';
|
||||
import {
|
||||
removeProcess,
|
||||
selectSection,
|
||||
selectProcesses,
|
||||
setSection,
|
||||
addProcess,
|
||||
setActivatedProcess,
|
||||
selectActivatedProcess,
|
||||
patchProcess,
|
||||
patchProcessData,
|
||||
selectTitle,
|
||||
setTitle,
|
||||
} from './store';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationService {
|
||||
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
|
||||
|
||||
get activatedProcessId() {
|
||||
return this.activatedProcessIdSubject.value;
|
||||
}
|
||||
|
||||
get activatedProcessId$() {
|
||||
return this.activatedProcessIdSubject.asObservable();
|
||||
}
|
||||
|
||||
constructor(private store: Store) {}
|
||||
|
||||
getProcesses$(section?: 'customer' | 'branch') {
|
||||
const processes$ = this.store.select(selectProcesses);
|
||||
return processes$.pipe(
|
||||
map((processes) => processes.filter((process) => (section ? process.section === section : true))),
|
||||
);
|
||||
}
|
||||
|
||||
getProcessById$(processId: number): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$().pipe(map((processes) => processes.find((process) => process.id === processId)));
|
||||
}
|
||||
|
||||
getSection$() {
|
||||
return this.store.select(selectSection);
|
||||
}
|
||||
|
||||
getTitle$() {
|
||||
return this.getSection$().pipe(
|
||||
map((section) => {
|
||||
return section === 'customer' ? 'Kundenbereich' : 'Filialbereich';
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
getActivatedProcessId$() {
|
||||
return this.store.select(selectActivatedProcess).pipe(map((process) => process?.id));
|
||||
}
|
||||
|
||||
activateProcess(activatedProcessId: number) {
|
||||
this.store.dispatch(setActivatedProcess({ activatedProcessId }));
|
||||
this.activatedProcessIdSubject.next(activatedProcessId);
|
||||
}
|
||||
|
||||
removeProcess(processId: number) {
|
||||
this.store.dispatch(removeProcess({ processId }));
|
||||
}
|
||||
|
||||
patchProcess(processId: number, changes: Partial<ApplicationProcess>) {
|
||||
this.store.dispatch(patchProcess({ processId, changes }));
|
||||
}
|
||||
|
||||
patchProcessData(processId: number, data: Record<string, any>) {
|
||||
this.store.dispatch(patchProcessData({ processId, data }));
|
||||
}
|
||||
|
||||
getSelectedBranch$(processId?: number): Observable<BranchDTO> {
|
||||
if (!processId) {
|
||||
return this.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch))),
|
||||
);
|
||||
}
|
||||
|
||||
return this.getProcessById$(processId).pipe(map((process) => process?.data?.selectedBranch));
|
||||
}
|
||||
|
||||
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
||||
|
||||
async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
|
||||
const processes = await this.getProcesses$('customer').pipe(first()).toPromise();
|
||||
|
||||
const processIds = processes.filter((x) => this.REGEX_PROCESS_NAME.test(x.name)).map((x) => +x.name.split(' ')[1]);
|
||||
|
||||
const maxId = processIds.length > 0 ? Math.max(...processIds) : 0;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: processId ?? Date.now(),
|
||||
type: 'cart',
|
||||
name: `Vorgang ${maxId + 1}`,
|
||||
section: 'customer',
|
||||
closeable: true,
|
||||
};
|
||||
|
||||
await this.createProcess(process);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
async createProcess(process: ApplicationProcess) {
|
||||
const existingProcess = await this.getProcessById$(process?.id).pipe(first()).toPromise();
|
||||
if (existingProcess?.id === process?.id) {
|
||||
throw new Error('Process Id existiert bereits');
|
||||
}
|
||||
|
||||
if (!isNumber(process.id)) {
|
||||
throw new Error('Process Id nicht gesetzt');
|
||||
}
|
||||
|
||||
if (!isBoolean(process.closeable)) {
|
||||
process.closeable = true;
|
||||
}
|
||||
|
||||
if (!isBoolean(process.confirmClosing)) {
|
||||
process.confirmClosing = true;
|
||||
}
|
||||
|
||||
process.created = this._createTimestamp();
|
||||
process.activated = 0;
|
||||
this.store.dispatch(addProcess({ process }));
|
||||
}
|
||||
|
||||
setSection(section: 'customer' | 'branch') {
|
||||
this.store.dispatch(setSection({ section }));
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSectionAndType$(
|
||||
section: 'customer' | 'branch',
|
||||
type: string,
|
||||
): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$(section).pipe(
|
||||
map((processes) =>
|
||||
processes
|
||||
?.filter((process) => process.type === type)
|
||||
?.reduce((latest, current) => {
|
||||
if (!latest) {
|
||||
return current;
|
||||
}
|
||||
return latest?.activated > current?.activated ? latest : current;
|
||||
}, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSection$(section: 'customer' | 'branch'): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$(section).pipe(
|
||||
map((processes) =>
|
||||
processes?.reduce((latest, current) => {
|
||||
if (!latest) {
|
||||
return current;
|
||||
}
|
||||
return latest?.activated > current?.activated ? latest : current;
|
||||
}, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private _createTimestamp() {
|
||||
return Date.now();
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { BranchDTO } from '@generated/swagger/checkout-api';
|
||||
import { isBoolean, isNumber } from '@utils/common';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { ApplicationProcess } from './defs';
|
||||
import {
|
||||
removeProcess,
|
||||
selectSection,
|
||||
selectProcesses,
|
||||
setSection,
|
||||
addProcess,
|
||||
setActivatedProcess,
|
||||
selectActivatedProcess,
|
||||
patchProcess,
|
||||
patchProcessData,
|
||||
selectTitle,
|
||||
setTitle,
|
||||
} from './store';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationService {
|
||||
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
|
||||
|
||||
get activatedProcessId() {
|
||||
return this.activatedProcessIdSubject.value;
|
||||
}
|
||||
|
||||
get activatedProcessId$() {
|
||||
return this.activatedProcessIdSubject.asObservable();
|
||||
}
|
||||
|
||||
constructor(private store: Store) {}
|
||||
|
||||
getProcesses$(section?: 'customer' | 'branch') {
|
||||
const processes$ = this.store.select(selectProcesses);
|
||||
return processes$.pipe(
|
||||
map((processes) =>
|
||||
processes.filter((process) =>
|
||||
section ? process.section === section : true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getProcessById$(processId: number): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$().pipe(
|
||||
map((processes) => processes.find((process) => process.id === processId)),
|
||||
);
|
||||
}
|
||||
|
||||
getSection$() {
|
||||
return this.store.select(selectSection);
|
||||
}
|
||||
|
||||
getTitle$() {
|
||||
return this.getSection$().pipe(
|
||||
map((section) => {
|
||||
return section === 'customer' ? 'Kundenbereich' : 'Filialbereich';
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
getActivatedProcessId$() {
|
||||
return this.store
|
||||
.select(selectActivatedProcess)
|
||||
.pipe(map((process) => process?.id));
|
||||
}
|
||||
|
||||
activateProcess(activatedProcessId: number) {
|
||||
this.store.dispatch(setActivatedProcess({ activatedProcessId }));
|
||||
this.activatedProcessIdSubject.next(activatedProcessId);
|
||||
}
|
||||
|
||||
removeProcess(processId: number) {
|
||||
this.store.dispatch(removeProcess({ processId }));
|
||||
}
|
||||
|
||||
patchProcess(processId: number, changes: Partial<ApplicationProcess>) {
|
||||
this.store.dispatch(patchProcess({ processId, changes }));
|
||||
}
|
||||
|
||||
patchProcessData(processId: number, data: Record<string, any>) {
|
||||
this.store.dispatch(patchProcessData({ processId, data }));
|
||||
}
|
||||
|
||||
getSelectedBranch$(processId?: number): Observable<BranchDTO> {
|
||||
if (!processId) {
|
||||
return this.activatedProcessId$.pipe(
|
||||
switchMap((processId) =>
|
||||
this.getProcessById$(processId).pipe(
|
||||
map((process) => process?.data?.selectedBranch),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return this.getProcessById$(processId).pipe(
|
||||
map((process) => process?.data?.selectedBranch),
|
||||
);
|
||||
}
|
||||
|
||||
readonly REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
||||
|
||||
async createCustomerProcess(processId?: number): Promise<ApplicationProcess> {
|
||||
const processes = await this.getProcesses$('customer')
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
const processIds = processes
|
||||
.filter((x) => this.REGEX_PROCESS_NAME.test(x.name))
|
||||
.map((x) => +x.name.split(' ')[1]);
|
||||
|
||||
const maxId = processIds.length > 0 ? Math.max(...processIds) : 0;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: processId ?? Date.now(),
|
||||
type: 'cart',
|
||||
name: `Vorgang ${maxId + 1}`,
|
||||
section: 'customer',
|
||||
closeable: true,
|
||||
};
|
||||
|
||||
await this.createProcess(process);
|
||||
|
||||
return process;
|
||||
}
|
||||
|
||||
async createProcess(process: ApplicationProcess) {
|
||||
const existingProcess = await this.getProcessById$(process?.id)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (existingProcess?.id === process?.id) {
|
||||
throw new Error('Process Id existiert bereits');
|
||||
}
|
||||
|
||||
if (!isNumber(process.id)) {
|
||||
throw new Error('Process Id nicht gesetzt');
|
||||
}
|
||||
|
||||
if (!isBoolean(process.closeable)) {
|
||||
process.closeable = true;
|
||||
}
|
||||
|
||||
if (!isBoolean(process.confirmClosing)) {
|
||||
process.confirmClosing = true;
|
||||
}
|
||||
|
||||
process.created = this._createTimestamp();
|
||||
process.activated = 0;
|
||||
this.store.dispatch(addProcess({ process }));
|
||||
}
|
||||
|
||||
setSection(section: 'customer' | 'branch') {
|
||||
this.store.dispatch(setSection({ section }));
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSectionAndType$(
|
||||
section: 'customer' | 'branch',
|
||||
type: string,
|
||||
): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$(section).pipe(
|
||||
map((processes) =>
|
||||
processes
|
||||
?.filter((process) => process.type === type)
|
||||
?.reduce((latest, current) => {
|
||||
if (!latest) {
|
||||
return current;
|
||||
}
|
||||
return latest?.activated > current?.activated ? latest : current;
|
||||
}, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getLastActivatedProcessWithSection$(
|
||||
section: 'customer' | 'branch',
|
||||
): Observable<ApplicationProcess> {
|
||||
return this.getProcesses$(section).pipe(
|
||||
map((processes) =>
|
||||
processes?.reduce((latest, current) => {
|
||||
if (!latest) {
|
||||
return current;
|
||||
}
|
||||
return latest?.activated > current?.activated ? latest : current;
|
||||
}, undefined),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private _createTimestamp() {
|
||||
return Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1251,7 +1251,12 @@ export class DomainCheckoutService {
|
||||
|
||||
await this.updateItemInShoppingCart({
|
||||
processId,
|
||||
update: { availability },
|
||||
update: {
|
||||
availability: {
|
||||
...availability,
|
||||
price: item?.availability?.price ?? availability?.price,
|
||||
}, // #5488 After Refreshing Availabilities in Cart make sure to keep the original selected price from purchasing options modal
|
||||
},
|
||||
shoppingCartItemId: item.id,
|
||||
}).toPromise();
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ const _domainCheckoutReducer = createReducer(
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (let shoppingCartItem of addedShoppingCartItems) {
|
||||
for (const shoppingCartItem of addedShoppingCartItems) {
|
||||
if (shoppingCartItem.features?.orderType) {
|
||||
entity.itemAvailabilityTimestamp[
|
||||
`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`
|
||||
@@ -275,7 +275,7 @@ function getOrCreateCheckoutEntity({
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
processId: number;
|
||||
}): CheckoutEntity {
|
||||
let entity = entities[processId];
|
||||
const entity = entities[processId];
|
||||
|
||||
if (isNullOrUndefined(entity)) {
|
||||
return {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@isa/crm/data-access';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { BranchDTO } from '@generated/swagger/checkout-api';
|
||||
import { CheckoutMetadataService } from '@isa/checkout/data-access';
|
||||
|
||||
/**
|
||||
* Service for opening and managing the Purchase Options Modal.
|
||||
@@ -39,6 +40,7 @@ export class PurchaseOptionsModalService {
|
||||
#uiModal = inject(UiModalService);
|
||||
#tabService = inject(TabService);
|
||||
#crmTabMetadataService = inject(CrmTabMetadataService);
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
#customerFacade = inject(CustomerFacade);
|
||||
|
||||
/**
|
||||
@@ -74,7 +76,10 @@ export class PurchaseOptionsModalService {
|
||||
};
|
||||
|
||||
context.selectedCustomer = await this.#getSelectedCustomer(data);
|
||||
context.selectedBranch = this.#getSelectedBranch(data.tabId);
|
||||
context.selectedBranch = this.#getSelectedBranch(
|
||||
data.tabId,
|
||||
data.useRedemptionPoints,
|
||||
);
|
||||
return this.#uiModal.open<string, PurchaseOptionsModalContext>({
|
||||
content: PurchaseOptionsModalComponent,
|
||||
data: context,
|
||||
@@ -95,7 +100,10 @@ export class PurchaseOptionsModalService {
|
||||
return this.#customerFacade.fetchCustomer({ customerId });
|
||||
}
|
||||
|
||||
#getSelectedBranch(tabId: number): BranchDTO | undefined {
|
||||
#getSelectedBranch(
|
||||
tabId: number,
|
||||
useRedemptionPoints: boolean,
|
||||
): BranchDTO | undefined {
|
||||
const tab = untracked(() =>
|
||||
this.#tabService.entities().find((t) => t.id === tabId),
|
||||
);
|
||||
@@ -104,6 +112,10 @@ export class PurchaseOptionsModalService {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (useRedemptionPoints) {
|
||||
return this.#checkoutMetadataService.getSelectedBranch(tabId);
|
||||
}
|
||||
|
||||
const legacyProcessData = tab?.metadata?.process_data;
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { AssortmentComponent } from './assortment.component';
|
||||
import { PriceUpdateComponent } from './price-update/price-update.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AssortmentComponent,
|
||||
children: [
|
||||
{ path: 'price-update', component: PriceUpdateComponent },
|
||||
{ path: '**', redirectTo: 'price-update' },
|
||||
],
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { AssortmentComponent } from './assortment.component';
|
||||
import { PriceUpdateComponent } from './price-update/price-update.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AssortmentComponent,
|
||||
title: 'Sortiment',
|
||||
children: [
|
||||
{
|
||||
path: 'price-update',
|
||||
component: PriceUpdateComponent,
|
||||
title: 'Sortiment - Preisänderung',
|
||||
},
|
||||
{ path: '**', redirectTo: 'price-update' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,125 +1,135 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { ArticleDetailsComponent } from './article-details/article-details.component';
|
||||
import { ArticleSearchComponent } from './article-search/article-search.component';
|
||||
import { ArticleSearchFilterComponent } from './article-search/search-filter/search-filter.component';
|
||||
import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component';
|
||||
import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component';
|
||||
import { PageCatalogComponent } from './page-catalog.component';
|
||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PageCatalogComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'filter',
|
||||
component: ArticleSearchFilterComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'filter/:id',
|
||||
component: ArticleSearchFilterComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
component: ArticleSearchComponent,
|
||||
outlet: 'side',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ArticleSearchMainComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results/:id',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results/:ean/ean',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'details/:id',
|
||||
component: ArticleDetailsComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (ID)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'details/:ean/ean',
|
||||
component: ArticleDetailsComponent,
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'filter',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class PageCatalogRoutingModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { ArticleDetailsComponent } from './article-details/article-details.component';
|
||||
import { ArticleSearchComponent } from './article-search/article-search.component';
|
||||
import { ArticleSearchFilterComponent } from './article-search/search-filter/search-filter.component';
|
||||
import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component';
|
||||
import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component';
|
||||
import { PageCatalogComponent } from './page-catalog.component';
|
||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PageCatalogComponent,
|
||||
title: 'Artikelsuche',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'filter',
|
||||
component: ArticleSearchFilterComponent,
|
||||
title: 'Artikelsuche - Filter',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'filter/:id',
|
||||
component: ArticleSearchFilterComponent,
|
||||
title: 'Artikelsuche - Filter',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
component: ArticleSearchComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: ArticleSearchMainComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results/:id',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche - Artikeldetails',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'results/:ean/ean',
|
||||
component: ArticleSearchResultsComponent,
|
||||
outlet: 'side',
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'details/:id',
|
||||
component: ArticleDetailsComponent,
|
||||
title: 'Artikelsuche - Artikeldetails (ID)',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (ID)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'details/:ean/ean',
|
||||
component: ArticleDetailsComponent,
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
data: {
|
||||
matomo: {
|
||||
title: 'Artikelsuche - Artikeldetails (EAN)',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'filter',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class PageCatalogRoutingModule {}
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
}
|
||||
}
|
||||
<page-shopping-cart-item
|
||||
(changeItem)="changeItem($event)"
|
||||
(changeItem)="showPurchasingListModal([$event.shoppingCartItem])"
|
||||
(changeDummyItem)="changeDummyItem($event)"
|
||||
(changeQuantity)="updateItemQuantity($event)"
|
||||
[quantityError]="
|
||||
|
||||
@@ -388,26 +388,6 @@ export class CheckoutReviewComponent
|
||||
this.openDummyModal({ data: shoppingCartItem, changeDataFromCart: true });
|
||||
}
|
||||
|
||||
async changeItem({
|
||||
shoppingCartItem,
|
||||
}: {
|
||||
shoppingCartItem: ShoppingCartItemDTO;
|
||||
}) {
|
||||
const shoppingCart = await firstValueFrom(this.shoppingCart$);
|
||||
const modalRef = await this._purchaseOptionsModalService.open({
|
||||
tabId: this.applicationService.activatedProcessId,
|
||||
shoppingCartId: shoppingCart.id,
|
||||
items: [shoppingCartItem],
|
||||
type: 'update',
|
||||
});
|
||||
|
||||
await firstValueFrom(modalRef.afterClosed$);
|
||||
// Reload Shopping Cart after modal is closed to get updated items
|
||||
this.#checkoutService.reloadShoppingCart({
|
||||
processId: this.applicationService.activatedProcessId,
|
||||
});
|
||||
}
|
||||
|
||||
async openPrintModal() {
|
||||
const shoppingCart = await this.shoppingCart$.pipe(first()).toPromise();
|
||||
this.uiModal.open({
|
||||
|
||||
@@ -14,20 +14,24 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'details',
|
||||
component: CheckoutReviewDetailsComponent,
|
||||
title: 'Bestelldetails',
|
||||
outlet: 'side',
|
||||
},
|
||||
{
|
||||
path: 'review',
|
||||
component: CheckoutReviewComponent,
|
||||
title: 'Bestellung überprüfen',
|
||||
},
|
||||
{
|
||||
path: 'summary',
|
||||
component: CheckoutSummaryComponent,
|
||||
title: 'Bestellübersicht',
|
||||
canDeactivate: [canDeactivateTabCleanup],
|
||||
},
|
||||
{
|
||||
path: 'summary/:orderIds',
|
||||
component: CheckoutSummaryComponent,
|
||||
title: 'Bestellübersicht',
|
||||
canDeactivate: [canDeactivateTabCleanup],
|
||||
},
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'review' },
|
||||
|
||||
@@ -1,29 +1,66 @@
|
||||
@if (orderItem$ | async; as orderItem) {
|
||||
<div #features class="page-customer-order-details-item__features">
|
||||
@if (orderItem?.features?.prebooked) {
|
||||
<img [uiOverlayTrigger]="prebookedTooltip" src="/assets/images/tag_icon_preorder.svg" [alt]="orderItem?.features?.prebooked" />
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #prebookedTooltip [closeable]="true">
|
||||
<img
|
||||
[uiOverlayTrigger]="prebookedTooltip"
|
||||
src="/assets/images/tag_icon_preorder.svg"
|
||||
[alt]="orderItem?.features?.prebooked"
|
||||
/>
|
||||
<ui-tooltip
|
||||
yPosition="above"
|
||||
xPosition="after"
|
||||
[yOffset]="-11"
|
||||
[xOffset]="-8"
|
||||
#prebookedTooltip
|
||||
[closeable]="true"
|
||||
>
|
||||
Artikel wird für Sie vorgemerkt.
|
||||
</ui-tooltip>
|
||||
}
|
||||
@if (notificationsSent$ | async; as notificationsSent) {
|
||||
@if (notificationsSent?.NOTIFICATION_EMAIL) {
|
||||
<img [uiOverlayTrigger]="emailTooltip" src="/assets/images/email_bookmark.svg" />
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #emailTooltip [closeable]="true">
|
||||
<img
|
||||
[uiOverlayTrigger]="emailTooltip"
|
||||
src="/assets/images/email_bookmark.svg"
|
||||
/>
|
||||
<ui-tooltip
|
||||
yPosition="above"
|
||||
xPosition="after"
|
||||
[yOffset]="-11"
|
||||
[xOffset]="-8"
|
||||
#emailTooltip
|
||||
[closeable]="true"
|
||||
>
|
||||
Per E-Mail benachrichtigt
|
||||
<br />
|
||||
@for (notification of notificationsSent?.NOTIFICATION_EMAIL; track notification) {
|
||||
@for (
|
||||
notification of notificationsSent?.NOTIFICATION_EMAIL;
|
||||
track notification
|
||||
) {
|
||||
{{ notification | date: 'dd.MM.yyyy | HH:mm' }} Uhr
|
||||
<br />
|
||||
}
|
||||
</ui-tooltip>
|
||||
}
|
||||
@if (notificationsSent?.NOTIFICATION_SMS) {
|
||||
<img [uiOverlayTrigger]="smsTooltip" src="/assets/images/sms_bookmark.svg" />
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #smsTooltip [closeable]="true">
|
||||
<img
|
||||
[uiOverlayTrigger]="smsTooltip"
|
||||
src="/assets/images/sms_bookmark.svg"
|
||||
/>
|
||||
<ui-tooltip
|
||||
yPosition="above"
|
||||
xPosition="after"
|
||||
[yOffset]="-11"
|
||||
[xOffset]="-8"
|
||||
#smsTooltip
|
||||
[closeable]="true"
|
||||
>
|
||||
Per SMS benachrichtigt
|
||||
<br />
|
||||
@for (notification of notificationsSent?.NOTIFICATION_SMS; track notification) {
|
||||
@for (
|
||||
notification of notificationsSent?.NOTIFICATION_SMS;
|
||||
track notification
|
||||
) {
|
||||
{{ notification | date: 'dd.MM.yyyy | HH:mm' }} Uhr
|
||||
<br />
|
||||
}
|
||||
@@ -33,7 +70,10 @@
|
||||
</div>
|
||||
<div class="page-customer-order-details-item__item-container">
|
||||
<div class="page-customer-order-details-item__thumbnail">
|
||||
<img [src]="orderItem.product?.ean | productImage" [alt]="orderItem.product?.name" />
|
||||
<img
|
||||
[src]="orderItem.product?.ean | productImage"
|
||||
[alt]="orderItem.product?.name"
|
||||
/>
|
||||
</div>
|
||||
<div class="page-customer-order-details-item__details">
|
||||
<div class="flex flex-row justify-between items-start mb-[1.3125rem]">
|
||||
@@ -42,19 +82,29 @@
|
||||
#elementDistance="uiElementDistance"
|
||||
[style.max-width.px]="elementDistance.distanceChange | async"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<div class="font-normal mb-[0.375rem]">{{ orderItem.product?.contributors }}</div>
|
||||
>
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<ui-label class="w-10 mb-2">Prämie</ui-label>
|
||||
}
|
||||
<div class="font-normal mb-[0.375rem]">
|
||||
{{ orderItem.product?.contributors }}
|
||||
</div>
|
||||
<div>{{ orderItem.product?.name }}</div>
|
||||
</h3>
|
||||
<div class="history-wrapper flex flex-col items-end justify-center">
|
||||
<button class="cta-history text-p1" (click)="historyClick.emit(orderItem)">Historie</button>
|
||||
<button
|
||||
class="cta-history text-p1"
|
||||
(click)="historyClick.emit(orderItem)"
|
||||
>
|
||||
Historie
|
||||
</button>
|
||||
@if (selectable$ | async) {
|
||||
<input
|
||||
[ngModel]="selected$ | async"
|
||||
(ngModelChange)="setSelected($event)"
|
||||
class="isa-select-bullet mt-4"
|
||||
type="checkbox"
|
||||
/>
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -72,19 +122,26 @@
|
||||
[showSpinner]="false"
|
||||
></ui-quantity-dropdown>
|
||||
}
|
||||
<span class="overall-quantity">(von {{ orderItem?.overallQuantity }})</span>
|
||||
<span class="overall-quantity"
|
||||
>(von {{ orderItem?.overallQuantity }})</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@if (!!orderItem.product?.formatDetail) {
|
||||
<div class="detail">
|
||||
<div class="label">Format</div>
|
||||
<div class="value">
|
||||
@if (orderItem?.product?.format && orderItem?.product?.format !== 'UNKNOWN') {
|
||||
@if (
|
||||
orderItem?.product?.format &&
|
||||
orderItem?.product?.format !== 'UNKNOWN'
|
||||
) {
|
||||
<img
|
||||
class="format-icon"
|
||||
[src]="'/assets/images/Icon_' + orderItem.product?.format + '.svg'"
|
||||
[src]="
|
||||
'/assets/images/Icon_' + orderItem.product?.format + '.svg'
|
||||
"
|
||||
alt="format icon"
|
||||
/>
|
||||
/>
|
||||
}
|
||||
<span>{{ orderItem.product?.formatDetail }}</span>
|
||||
</div>
|
||||
@@ -96,10 +153,17 @@
|
||||
<div class="value">{{ orderItem.product?.ean }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (orderItem.price !== undefined) {
|
||||
@if (orderItem.price !== undefined || (hasRewardPoints$ | async)) {
|
||||
<div class="detail">
|
||||
<div class="label">Preis</div>
|
||||
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<div class="label">Prämie</div>
|
||||
<div class="value">
|
||||
{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte
|
||||
</div>
|
||||
} @else {
|
||||
<div class="label">Preis</div>
|
||||
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!!orderItem.retailPrice?.vat?.inPercent) {
|
||||
@@ -133,14 +197,23 @@
|
||||
orderItemFeature(orderItem) === 'Versand' ||
|
||||
orderItemFeature(orderItem) === 'B2B-Versand' ||
|
||||
orderItemFeature(orderItem) === 'DIG-Versand'
|
||||
) {
|
||||
{{ orderItem?.estimatedDelivery ? 'Lieferung zwischen' : 'Lieferung ab' }}
|
||||
) {
|
||||
{{
|
||||
orderItem?.estimatedDelivery
|
||||
? 'Lieferung zwischen'
|
||||
: 'Lieferung ab'
|
||||
}}
|
||||
}
|
||||
@if (orderItemFeature(orderItem) === 'Abholung' || orderItemFeature(orderItem) === 'Rücklage') {
|
||||
@if (
|
||||
orderItemFeature(orderItem) === 'Abholung' ||
|
||||
orderItemFeature(orderItem) === 'Rücklage'
|
||||
) {
|
||||
Abholung ab
|
||||
}
|
||||
</div>
|
||||
@if (!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate) {
|
||||
@if (
|
||||
!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate
|
||||
) {
|
||||
<div class="value bg-[#D8DFE5] rounded w-max px-2">
|
||||
@if (!!orderItem?.estimatedDelivery) {
|
||||
{{ orderItem?.estimatedDelivery?.start | date: 'dd.MM.yy' }} und
|
||||
@@ -155,14 +228,22 @@
|
||||
</div>
|
||||
@if (getOrderItemTrackingData(orderItem); as trackingData) {
|
||||
<div class="page-customer-order-details-item__tracking-details">
|
||||
<div class="label">{{ trackingData.length > 1 ? 'Sendungsnummern' : 'Sendungsnummer' }}</div>
|
||||
<div class="label">
|
||||
{{ trackingData.length > 1 ? 'Sendungsnummern' : 'Sendungsnummer' }}
|
||||
</div>
|
||||
@for (tracking of trackingData; track tracking) {
|
||||
@if (tracking.trackingProvider === 'DHL' && !isNative) {
|
||||
<a class="value text-[#0556B4]" [href]="getTrackingNumberLink(tracking.trackingNumber)" target="_blank">
|
||||
<a
|
||||
class="value text-[#0556B4]"
|
||||
[href]="getTrackingNumberLink(tracking.trackingNumber)"
|
||||
target="_blank"
|
||||
>
|
||||
{{ tracking.trackingProvider }}: {{ tracking.trackingNumber }}
|
||||
</a>
|
||||
} @else {
|
||||
<p class="value">{{ tracking.trackingProvider }}: {{ tracking.trackingNumber }}</p>
|
||||
<p class="value">
|
||||
{{ tracking.trackingProvider }}: {{ tracking.trackingNumber }}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -206,7 +287,9 @@
|
||||
@if (!!receipt?.printedDate) {
|
||||
<div class="detail">
|
||||
<div class="label">Erstellt am</div>
|
||||
<div class="value">{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
|
||||
<div class="value">
|
||||
{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!!receipt?.receiptText) {
|
||||
@@ -219,12 +302,20 @@
|
||||
<div class="detail">
|
||||
<div class="label">Belegart</div>
|
||||
<div class="value">
|
||||
{{ receipt?.receiptType === 1 ? 'Lieferschein' : receipt?.receiptType === 64 ? 'Zahlungsbeleg' : '-' }}
|
||||
{{
|
||||
receipt?.receiptType === 1
|
||||
? 'Lieferschein'
|
||||
: receipt?.receiptType === 64
|
||||
? 'Zahlungsbeleg'
|
||||
: '-'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="page-customer-order-details-item__comment flex flex-col items-start mt-[1.625rem]">
|
||||
<div
|
||||
class="page-customer-order-details-item__comment flex flex-col items-start mt-[1.625rem]"
|
||||
>
|
||||
<div class="label mb-[0.375rem]">Anmerkung</div>
|
||||
<div class="flex flex-row w-full">
|
||||
<textarea
|
||||
@@ -248,17 +339,23 @@
|
||||
<button
|
||||
type="reset"
|
||||
class="clear"
|
||||
(click)="specialCommentControl.setValue(''); saveSpecialComment(); triggerResize()"
|
||||
>
|
||||
(click)="
|
||||
specialCommentControl.setValue('');
|
||||
saveSpecialComment();
|
||||
triggerResize()
|
||||
"
|
||||
>
|
||||
<shared-icon icon="close" [size]="24"></shared-icon>
|
||||
</button>
|
||||
}
|
||||
@if (specialCommentControl?.enabled && specialCommentControl.dirty) {
|
||||
@if (
|
||||
specialCommentControl?.enabled && specialCommentControl.dirty
|
||||
) {
|
||||
<button
|
||||
class="cta-save"
|
||||
type="submit"
|
||||
(click)="saveSpecialComment()"
|
||||
>
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -15,12 +15,25 @@ import { DomainOmsService, DomainReceiptService } from '@domain/oms';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { tapResponse } from '@ngrx/operators';
|
||||
|
||||
import { OrderDTO, OrderItemListItemDTO, ReceiptDTO, ReceiptType } from '@generated/swagger/oms-api';
|
||||
import {
|
||||
OrderDTO,
|
||||
OrderItemListItemDTO,
|
||||
ReceiptDTO,
|
||||
ReceiptType,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { isEqual } from 'lodash';
|
||||
import { combineLatest, NEVER, Subject, Observable } from 'rxjs';
|
||||
import { catchError, filter, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import {
|
||||
catchError,
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
import { CustomerOrderDetailsStore } from '../customer-order-details.store';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { getOrderItemRewardFeature } from '@isa/oms/data-access';
|
||||
|
||||
export interface CustomerOrderDetailsItemComponentState {
|
||||
orderItem?: OrderItemListItemDTO;
|
||||
@@ -59,7 +72,12 @@ export class CustomerOrderDetailsItemComponent
|
||||
// Remove Prev OrderItem from selected list
|
||||
this._store.selectOrderItem(this.orderItem, false);
|
||||
|
||||
this.patchState({ orderItem, quantity: orderItem?.quantity, receipts: [], more: false });
|
||||
this.patchState({
|
||||
orderItem,
|
||||
quantity: orderItem?.quantity,
|
||||
receipts: [],
|
||||
more: false,
|
||||
});
|
||||
this.specialCommentControl.reset(orderItem?.specialComment);
|
||||
|
||||
// Add New OrderItem to selected list if selected was set to true by its input
|
||||
@@ -94,8 +112,23 @@ export class CustomerOrderDetailsItemComponent
|
||||
),
|
||||
);
|
||||
|
||||
canChangeQuantity$ = combineLatest([this.orderItem$, this._store.fetchPartial$]).pipe(
|
||||
map(([item, partialPickup]) => ([16, 8192].includes(item?.processingStatus) || partialPickup) && item.quantity > 1),
|
||||
hasRewardPoints$ = this.orderItem$.pipe(
|
||||
map((orderItem) => getOrderItemRewardFeature(orderItem) !== undefined),
|
||||
);
|
||||
|
||||
rewardPoints$ = this.orderItem$.pipe(
|
||||
map((orderItem) => getOrderItemRewardFeature(orderItem)),
|
||||
);
|
||||
|
||||
canChangeQuantity$ = combineLatest([
|
||||
this.orderItem$,
|
||||
this._store.fetchPartial$,
|
||||
]).pipe(
|
||||
map(
|
||||
([item, partialPickup]) =>
|
||||
([16, 8192].includes(item?.processingStatus) || partialPickup) &&
|
||||
item.quantity > 1,
|
||||
),
|
||||
);
|
||||
|
||||
get quantity() {
|
||||
@@ -111,7 +144,9 @@ export class CustomerOrderDetailsItemComponent
|
||||
|
||||
@Input()
|
||||
get selected() {
|
||||
return this._store.selectedeOrderItemSubsetIds.includes(this.orderItem?.orderItemSubsetId);
|
||||
return this._store.selectedeOrderItemSubsetIds.includes(
|
||||
this.orderItem?.orderItemSubsetId,
|
||||
);
|
||||
}
|
||||
set selected(selected: boolean) {
|
||||
if (this.selected !== selected) {
|
||||
@@ -120,22 +155,36 @@ export class CustomerOrderDetailsItemComponent
|
||||
}
|
||||
}
|
||||
|
||||
readonly selected$ = combineLatest([this.orderItem$, this._store.selectedeOrderItemSubsetIds$]).pipe(
|
||||
map(([orderItem, selectedItems]) => selectedItems.includes(orderItem?.orderItemSubsetId)),
|
||||
readonly selected$ = combineLatest([
|
||||
this.orderItem$,
|
||||
this._store.selectedeOrderItemSubsetIds$,
|
||||
]).pipe(
|
||||
map(([orderItem, selectedItems]) =>
|
||||
selectedItems.includes(orderItem?.orderItemSubsetId),
|
||||
),
|
||||
);
|
||||
|
||||
@Output()
|
||||
selectedChange = new EventEmitter<boolean>();
|
||||
|
||||
get selectable() {
|
||||
return this._store.itemsSelectable && this._store.items.length > 1 && this._store.fetchPartial;
|
||||
return (
|
||||
this._store.itemsSelectable &&
|
||||
this._store.items.length > 1 &&
|
||||
this._store.fetchPartial
|
||||
);
|
||||
}
|
||||
|
||||
readonly selectable$ = combineLatest([
|
||||
this._store.items$,
|
||||
this._store.itemsSelectable$,
|
||||
this._store.fetchPartial$,
|
||||
]).pipe(map(([orderItems, selectable, fetchPartial]) => orderItems.length > 1 && selectable && fetchPartial));
|
||||
]).pipe(
|
||||
map(
|
||||
([orderItems, selectable, fetchPartial]) =>
|
||||
orderItems.length > 1 && selectable && fetchPartial,
|
||||
),
|
||||
);
|
||||
|
||||
get receipts() {
|
||||
return this.get((s) => s.receipts);
|
||||
@@ -173,6 +222,7 @@ export class CustomerOrderDetailsItemComponent
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
ngOnInit() {}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -182,37 +232,39 @@ export class CustomerOrderDetailsItemComponent
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
loadReceipts = this.effect((done$: Observable<(receipts: ReceiptDTO[]) => void | undefined>) =>
|
||||
done$.pipe(
|
||||
withLatestFrom(this.orderItem$),
|
||||
filter(([_, orderItem]) => !!orderItem),
|
||||
switchMap(([done, orderItem]) =>
|
||||
this._domainReceiptService
|
||||
.getReceipts({
|
||||
receiptType: 65 as ReceiptType,
|
||||
ids: [orderItem.orderItemSubsetId],
|
||||
eagerLoading: 1,
|
||||
})
|
||||
.pipe(
|
||||
tapResponse(
|
||||
(res) => {
|
||||
const receipts = res.result.map((r) => r.item3?.data).filter((f) => !!f);
|
||||
this.receipts = receipts;
|
||||
loadReceipts = this.effect(
|
||||
(done$: Observable<(receipts: ReceiptDTO[]) => void | undefined>) =>
|
||||
done$.pipe(
|
||||
withLatestFrom(this.orderItem$),
|
||||
filter(([, orderItem]) => !!orderItem),
|
||||
switchMap(([done, orderItem]) =>
|
||||
this._domainReceiptService
|
||||
.getReceipts({
|
||||
receiptType: 65 as ReceiptType,
|
||||
ids: [orderItem.orderItemSubsetId],
|
||||
eagerLoading: 1,
|
||||
})
|
||||
.pipe(
|
||||
tapResponse(
|
||||
(res) => {
|
||||
const receipts = res.result
|
||||
.map((r) => r.item3?.data)
|
||||
.filter((f) => !!f);
|
||||
this.receipts = receipts;
|
||||
|
||||
if (typeof done === 'function') {
|
||||
done?.(receipts);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
if (typeof done === 'function') {
|
||||
done?.([]);
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
if (typeof done === 'function') {
|
||||
done?.(receipts);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (typeof done === 'function') {
|
||||
done?.([]);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
async saveSpecialComment() {
|
||||
@@ -220,7 +272,7 @@ export class CustomerOrderDetailsItemComponent
|
||||
|
||||
try {
|
||||
this.specialCommentControl.reset(this.specialCommentControl.value);
|
||||
const res = await this._omsService
|
||||
await this._omsService
|
||||
.patchComment({
|
||||
orderId,
|
||||
orderItemId,
|
||||
@@ -230,7 +282,10 @@ export class CustomerOrderDetailsItemComponent
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
this.orderItem = { ...this.orderItem, specialComment: this.specialCommentControl.value ?? '' };
|
||||
this.orderItem = {
|
||||
...this.orderItem,
|
||||
specialComment: this.specialCommentControl.value ?? '',
|
||||
};
|
||||
this._store.updateOrderItems([this.orderItem]);
|
||||
this.specialCommentChanged.emit();
|
||||
} catch (error) {
|
||||
@@ -253,8 +308,9 @@ export class CustomerOrderDetailsItemComponent
|
||||
|
||||
orderItemFeature(orderItemListItem: OrderItemListItemDTO) {
|
||||
const orderItems = this.order?.items;
|
||||
return orderItems?.find((orderItem) => orderItem.data.id === orderItemListItem.orderItemId)?.data?.features
|
||||
?.orderType;
|
||||
return orderItems?.find(
|
||||
(orderItem) => orderItem.data.id === orderItemListItem.orderItemId,
|
||||
)?.data?.features?.orderType;
|
||||
}
|
||||
|
||||
getOrderItemTrackingData(
|
||||
@@ -263,15 +319,18 @@ export class CustomerOrderDetailsItemComponent
|
||||
const orderItems = this.order?.items;
|
||||
const completeTrackingInformation = orderItems
|
||||
?.find((orderItem) => orderItem.data.id === orderItemListItem.orderItemId)
|
||||
?.data?.subsetItems?.find((subsetItem) => subsetItem.id === orderItemListItem.orderItemSubsetId)
|
||||
?.data?.trackingNumber;
|
||||
?.data?.subsetItems?.find(
|
||||
(subsetItem) => subsetItem.id === orderItemListItem.orderItemSubsetId,
|
||||
)?.data?.trackingNumber;
|
||||
|
||||
if (!completeTrackingInformation) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Beispielnummer: 'DHL: 124124' - Bei mehreren Tracking-Informationen muss noch ein Splitter eingebaut werden, je nach dem welcher Trenner verwendet wird
|
||||
const trackingInformationPairs = completeTrackingInformation.split(':').map((obj) => obj.trim());
|
||||
const trackingInformationPairs = completeTrackingInformation
|
||||
.split(':')
|
||||
.map((obj) => obj.trim());
|
||||
return this._trackingTransformationHelper(trackingInformationPairs);
|
||||
}
|
||||
|
||||
@@ -282,7 +341,10 @@ export class CustomerOrderDetailsItemComponent
|
||||
return trackingInformationPairs.reduce(
|
||||
(acc, current, index, array) => {
|
||||
if (index % 2 === 0) {
|
||||
acc.push({ trackingProvider: current, trackingNumber: array[index + 1] });
|
||||
acc.push({
|
||||
trackingProvider: current,
|
||||
trackingNumber: array[index + 1],
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ProductImageModule } from '@cdn/product-image';
|
||||
import { CustomerOrderDetailsStore } from './customer-order-details.store';
|
||||
import { UiDatepickerModule } from '@ui/datepicker';
|
||||
import { UiDropdownModule } from '@ui/dropdown';
|
||||
import { LabelComponent } from '@isa/ui/label';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -34,6 +35,7 @@ import { UiDropdownModule } from '@ui/dropdown';
|
||||
CustomerOrderPipesModule,
|
||||
ProductImageModule,
|
||||
CustomerOrderDetailsTagsComponent,
|
||||
LabelComponent,
|
||||
],
|
||||
exports: [
|
||||
CustomerOrderDetailsComponent,
|
||||
@@ -41,7 +43,11 @@ import { UiDropdownModule } from '@ui/dropdown';
|
||||
CustomerOrderDetailsHeaderComponent,
|
||||
CustomerOrderDetailsTagsComponent,
|
||||
],
|
||||
declarations: [CustomerOrderDetailsComponent, CustomerOrderDetailsItemComponent, CustomerOrderDetailsHeaderComponent],
|
||||
declarations: [
|
||||
CustomerOrderDetailsComponent,
|
||||
CustomerOrderDetailsItemComponent,
|
||||
CustomerOrderDetailsHeaderComponent,
|
||||
],
|
||||
providers: [CustomerOrderDetailsStore],
|
||||
})
|
||||
export class CustomerOrderDetailsModule {}
|
||||
|
||||
@@ -9,8 +9,7 @@ import { DecimalPipe } from '@angular/common';
|
||||
import { Component, Input, OnInit, inject } from '@angular/core';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { BonusCardInfoDTO } from '@generated/swagger/crm-api';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
import { injectTabId, TabService } from '@isa/core/tabs';
|
||||
import { Router } from '@angular/router';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
|
||||
@@ -47,7 +46,7 @@ import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
export class KundenkarteComponent implements OnInit {
|
||||
#tabId = injectTabId();
|
||||
#router = inject(Router);
|
||||
#navigationState = inject(NavigationStateService);
|
||||
#tabService = inject(TabService);
|
||||
#customerNavigationService = inject(CustomerSearchNavigation);
|
||||
|
||||
@Input() cardDetails: BonusCardInfoDTO;
|
||||
@@ -69,13 +68,12 @@ export class KundenkarteComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#navigationState.preserveContext(
|
||||
{
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
'select-customer': {
|
||||
returnUrl: `/${tabId}/reward`,
|
||||
autoTriggerContinueFn: true,
|
||||
},
|
||||
'select-customer',
|
||||
);
|
||||
});
|
||||
|
||||
await this.#router.navigate(
|
||||
this.#customerNavigationService.detailsRoute({
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
@@ -16,13 +16,16 @@ import { map } from 'rxjs/operators';
|
||||
standalone: false,
|
||||
})
|
||||
export class CustomerComponent implements OnDestroy {
|
||||
private _navigationState = inject(NavigationStateService);
|
||||
private _tabService = inject(TabService);
|
||||
processId$ = this._activatedRoute.data.pipe(map((data) => data.processId));
|
||||
|
||||
constructor(private _activatedRoute: ActivatedRoute) {}
|
||||
|
||||
async ngOnDestroy() {
|
||||
ngOnDestroy() {
|
||||
const tab = this._tabService.activatedTab();
|
||||
// #5512 Always clear preserved select-customer context if navigating out of customer area
|
||||
await this._navigationState.clearPreservedContext('select-customer');
|
||||
this._tabService.patchTabMetadata(tab.id, {
|
||||
'select-customer': null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +49,14 @@ import {
|
||||
NavigateAfterRewardSelection,
|
||||
RewardSelectionPopUpService,
|
||||
} from '@isa/checkout/shared/reward-selection-dialog';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { ShippingAddressDTO as CrmShippingAddressDTO } from '@generated/swagger/crm-api';
|
||||
|
||||
interface SelectCustomerContext {
|
||||
returnUrl?: string;
|
||||
autoTriggerContinueFn?: boolean;
|
||||
}
|
||||
|
||||
export interface CustomerDetailsViewMainState {
|
||||
isBusy: boolean;
|
||||
shoppingCart: ShoppingCartDTO;
|
||||
@@ -80,7 +85,7 @@ export class CustomerDetailsViewMainComponent
|
||||
private _router = inject(Router);
|
||||
private _activatedRoute = inject(ActivatedRoute);
|
||||
private _genderSettings = inject(GenderSettingsService);
|
||||
private _navigationState = inject(NavigationStateService);
|
||||
private _tabService = inject(TabService);
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
customerService = inject(CrmCustomerService);
|
||||
@@ -97,18 +102,19 @@ export class CustomerDetailsViewMainComponent
|
||||
map(([fetchingCustomer, fetchingList]) => fetchingCustomer || fetchingList),
|
||||
);
|
||||
|
||||
async getReturnUrlFromContext(): Promise<string | null> {
|
||||
// Get from preserved context (survives intermediate navigations, auto-scoped to tab)
|
||||
const context = await this._navigationState.restoreContext<{
|
||||
returnUrl?: string;
|
||||
}>('select-customer');
|
||||
getReturnUrlFromContext(): string | null {
|
||||
// Get from preserved context (survives intermediate navigations, scoped to tab)
|
||||
const context = this._tabService.activatedTab()?.metadata?.[
|
||||
'select-customer'
|
||||
] as SelectCustomerContext | undefined;
|
||||
|
||||
return context?.returnUrl ?? null;
|
||||
}
|
||||
|
||||
async checkHasReturnUrl(): Promise<void> {
|
||||
const hasContext =
|
||||
await this._navigationState.hasPreservedContext('select-customer');
|
||||
checkHasReturnUrl(): void {
|
||||
const hasContext = !!this._tabService.activatedTab()?.metadata?.[
|
||||
'select-customer'
|
||||
];
|
||||
this.hasReturnUrl.set(hasContext);
|
||||
}
|
||||
|
||||
@@ -321,24 +327,23 @@ export class CustomerDetailsViewMainComponent
|
||||
|
||||
ngOnInit() {
|
||||
// Check if we have a return URL context
|
||||
this.checkHasReturnUrl().then(async () => {
|
||||
// Check if we should auto-trigger continue() (only from Kundenkarte)
|
||||
const context = await this._navigationState.restoreContext<{
|
||||
returnUrl?: string;
|
||||
autoTriggerContinueFn?: boolean;
|
||||
}>('select-customer');
|
||||
this.checkHasReturnUrl();
|
||||
|
||||
if (context?.autoTriggerContinueFn) {
|
||||
// Clear the autoTriggerContinueFn flag immediately (preserves returnUrl automatically)
|
||||
await this._navigationState.patchContext(
|
||||
{ autoTriggerContinueFn: undefined },
|
||||
'select-customer',
|
||||
);
|
||||
// Check if we should auto-trigger continue() (only from Kundenkarte)
|
||||
const tab = this._tabService.activatedTab();
|
||||
const context = tab?.metadata?.['select-customer'] as
|
||||
| SelectCustomerContext
|
||||
| undefined;
|
||||
|
||||
// Auto-trigger continue() ONLY when coming from Kundenkarte
|
||||
this.continue();
|
||||
}
|
||||
});
|
||||
if (context?.autoTriggerContinueFn && tab) {
|
||||
// Clear the autoTriggerContinueFn flag immediately (preserves returnUrl)
|
||||
this._tabService.patchTabMetadata(tab.id, {
|
||||
'select-customer': { ...context, autoTriggerContinueFn: undefined },
|
||||
});
|
||||
|
||||
// Auto-trigger continue() ONLY when coming from Kundenkarte
|
||||
this.continue();
|
||||
}
|
||||
|
||||
this.processId$
|
||||
.pipe(
|
||||
@@ -436,10 +441,18 @@ export class CustomerDetailsViewMainComponent
|
||||
|
||||
// #5262 Check for reward selection flow before navigation
|
||||
if (this.hasReturnUrl()) {
|
||||
// Restore from preserved context (auto-scoped to current tab) and clean up
|
||||
const context = await this._navigationState.restoreAndClearContext<{
|
||||
returnUrl?: string;
|
||||
}>('select-customer');
|
||||
// Restore from preserved context (scoped to current tab) and clean up
|
||||
const tab = this._tabService.activatedTab();
|
||||
const context = tab?.metadata?.['select-customer'] as
|
||||
| SelectCustomerContext
|
||||
| undefined;
|
||||
|
||||
// Clear the context
|
||||
if (tab) {
|
||||
this._tabService.patchTabMetadata(tab.id, {
|
||||
'select-customer': null,
|
||||
});
|
||||
}
|
||||
|
||||
if (context?.returnUrl) {
|
||||
await this._router.navigateByUrl(context.returnUrl);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { CustomerSearchStore } from '../store';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { CustomerMenuComponent } from '../../components/customer-menu';
|
||||
@@ -51,7 +51,7 @@ export class KundenkarteMainViewComponent implements OnDestroy {
|
||||
#cardTransactionsResource = inject(CustomerCardTransactionsResource);
|
||||
elementRef = inject(ElementRef);
|
||||
#router = inject(Router);
|
||||
#navigationState = inject(NavigationStateService);
|
||||
#tabService = inject(TabService);
|
||||
#customerNavigationService = inject(CustomerSearchNavigation);
|
||||
|
||||
/**
|
||||
@@ -120,13 +120,12 @@ export class KundenkarteMainViewComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
// Preserve context for auto-triggering continue() in details view
|
||||
this.#navigationState.preserveContext(
|
||||
{
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
'select-customer': {
|
||||
returnUrl: `/${tabId}/reward`,
|
||||
autoTriggerContinueFn: true,
|
||||
},
|
||||
'select-customer',
|
||||
);
|
||||
});
|
||||
|
||||
// Navigate to customer details - will auto-trigger continue()
|
||||
await this.#router.navigate(
|
||||
|
||||
@@ -11,13 +11,11 @@
|
||||
[alt]="name"
|
||||
/>
|
||||
}
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.High">
|
||||
Prämie
|
||||
</ui-label>
|
||||
}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<ui-label class="w-10">Prämie</ui-label>
|
||||
}
|
||||
<div class="grid grid-flow-col justify-between items-end">
|
||||
<span>{{ orderItem.product?.contributors }}</span>
|
||||
@if (orderDetailsHistoryRoute$ | async; as orderDetailsHistoryRoute) {
|
||||
@@ -25,7 +23,7 @@
|
||||
[routerLink]="orderDetailsHistoryRoute.path"
|
||||
[queryParams]="orderDetailsHistoryRoute.urlTree.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
class="text-brand font-bold text-xl"
|
||||
class="text-brand font-bold text-xl relative -top-8"
|
||||
>
|
||||
Historie
|
||||
</a>
|
||||
@@ -64,7 +62,9 @@
|
||||
<div class="col-data">
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<div class="col-label">Prämie</div>
|
||||
<div class="col-value">{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte</div>
|
||||
<div class="col-value">
|
||||
{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte
|
||||
</div>
|
||||
} @else {
|
||||
<div class="col-label">Preis</div>
|
||||
<div class="col-value">
|
||||
|
||||
@@ -21,7 +21,7 @@ import { map, takeUntil } from 'rxjs/operators';
|
||||
import { CustomerSearchStore } from '../../store';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { PaymentTypePipe } from '@shared/pipes/customer';
|
||||
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
|
||||
import { LabelComponent } from '@isa/ui/label';
|
||||
import { getOrderItemRewardFeature } from '@isa/oms/data-access';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
|
||||
@@ -113,9 +113,6 @@ export class CustomerOrderItemListItemComponent implements OnInit, OnDestroy {
|
||||
map((orderItem) => getOrderItemRewardFeature(orderItem)),
|
||||
);
|
||||
|
||||
Labeltype = Labeltype;
|
||||
LabelPriority = LabelPriority;
|
||||
|
||||
ngOnInit() {
|
||||
this.customerId$
|
||||
.pipe(takeUntil(this._onDestroy))
|
||||
|
||||
@@ -1,170 +1,187 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { CustomerComponent } from './customer-page.component';
|
||||
import { CustomerSearchComponent } from './customer-search/customer-search.component';
|
||||
import { CustomerResultsMainViewComponent } from './customer-search/results-main-view/results-main-view.component';
|
||||
import { CustomerDetailsViewMainComponent } from './customer-search/details-main-view/details-main-view.component';
|
||||
import { CustomerHistoryMainViewComponent } from './customer-search/history-main-view/history-main-view.component';
|
||||
import { CustomerFilterMainViewComponent } from './customer-search/filter-main-view/filter-main-view.component';
|
||||
import { CustomerCreateGuard } from './guards/customer-create.guard';
|
||||
import {
|
||||
CreateB2BCustomerComponent,
|
||||
CreateGuestCustomerComponent,
|
||||
// CreateP4MCustomerComponent,
|
||||
CreateStoreCustomerComponent,
|
||||
CreateWebshopCustomerComponent,
|
||||
} from './create-customer';
|
||||
// import { UpdateP4MWebshopCustomerComponent } from './create-customer/update-p4m-webshop-customer';
|
||||
import { CreateCustomerComponent } from './create-customer/create-customer.component';
|
||||
import { CustomerDataEditB2BComponent } from './customer-search/edit-main-view/customer-data-edit-b2b.component';
|
||||
import { CustomerDataEditB2CComponent } from './customer-search/edit-main-view/customer-data-edit-b2c.component';
|
||||
import { AddBillingAddressMainViewComponent } from './customer-search/add-billing-address-main-view/add-billing-address-main-view.component';
|
||||
import { AddShippingAddressMainViewComponent } from './customer-search/add-shipping-address-main-view/add-shipping-address-main-view.component';
|
||||
import { EditBillingAddressMainViewComponent } from './customer-search/edit-billing-address-main-view/edit-billing-address-main-view.component';
|
||||
import { EditShippingAddressMainViewComponent } from './customer-search/edit-shipping-address-main-view/edit-shipping-address-main-view.component';
|
||||
import { CustomerOrdersMainViewComponent } from './customer-search/orders-main-view/orders-main-view.component';
|
||||
import { OrderDetailsMainViewComponent } from './customer-search/order-details-main-view/order-details-main-view.component';
|
||||
import { KundenkarteMainViewComponent } from './customer-search/kundenkarte-main-view/kundenkarte-main-view.component';
|
||||
import { CustomerOrderDetailsHistoryMainViewComponent } from './customer-search/order-details-history-main-view/order-details-history-main-view.component';
|
||||
import { CustomerMainViewComponent } from './customer-search/main-view/main-view.component';
|
||||
import { MainSideViewComponent } from './customer-search/main-side-view/main-side-view.component';
|
||||
import { CustomerResultsSideViewComponent } from './customer-search/results-side-view/results-side-view.component';
|
||||
import { OrderDetailsSideViewComponent } from './customer-search/order-details-side-view/order-details-side-view.component';
|
||||
import { CustomerCreateSideViewComponent } from './create-customer/customer-create-side-view';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CustomerComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: CustomerSearchComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'search',
|
||||
component: CustomerMainViewComponent,
|
||||
data: { side: 'main', breadcrumb: 'main' },
|
||||
},
|
||||
{
|
||||
path: 'search/list',
|
||||
component: CustomerResultsMainViewComponent,
|
||||
data: { breadcrumb: 'search' },
|
||||
},
|
||||
{
|
||||
path: 'search/filter',
|
||||
component: CustomerFilterMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'filter' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId',
|
||||
component: CustomerDetailsViewMainComponent,
|
||||
data: { side: 'results', breadcrumb: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/history',
|
||||
component: CustomerHistoryMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/kundenkarte',
|
||||
component: KundenkarteMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'kundenkarte' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders',
|
||||
component: CustomerOrdersMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'orders' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId',
|
||||
component: OrderDetailsMainViewComponent,
|
||||
data: { side: 'order-details', breadcrumb: 'order-details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId/:orderItemId',
|
||||
component: OrderDetailsMainViewComponent,
|
||||
data: { side: 'order-details', breadcrumb: 'order-details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId/:orderItemId/history',
|
||||
component: CustomerOrderDetailsHistoryMainViewComponent,
|
||||
data: {
|
||||
side: 'order-details',
|
||||
breadcrumb: 'order-details-history',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/edit/b2b',
|
||||
component: CustomerDataEditB2BComponent,
|
||||
data: { side: 'results', breadcrumb: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/edit',
|
||||
component: CustomerDataEditB2CComponent,
|
||||
data: { side: 'results', breadcrumb: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/billingaddress/add',
|
||||
component: AddBillingAddressMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'add-billing-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/billingaddress/:payerId/edit',
|
||||
component: EditBillingAddressMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'edit-billing-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/shippingaddress/add',
|
||||
component: AddShippingAddressMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'add-shipping-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/shippingaddress/:shippingAddressId/edit',
|
||||
component: EditShippingAddressMainViewComponent,
|
||||
data: { side: 'results', breadcrumb: 'edit-shipping-address' },
|
||||
},
|
||||
{
|
||||
path: 'search-customer-main',
|
||||
outlet: 'side',
|
||||
component: MainSideViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
outlet: 'side',
|
||||
component: CustomerResultsSideViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'order-details',
|
||||
outlet: 'side',
|
||||
component: OrderDetailsSideViewComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: CreateCustomerComponent,
|
||||
canActivate: [CustomerCreateGuard],
|
||||
canActivateChild: [CustomerCreateGuard],
|
||||
children: [
|
||||
{ path: 'create', component: CreateStoreCustomerComponent },
|
||||
{ path: 'create/store', component: CreateStoreCustomerComponent },
|
||||
{ path: 'create/webshop', component: CreateWebshopCustomerComponent },
|
||||
{ path: 'create/b2b', component: CreateB2BCustomerComponent },
|
||||
{ path: 'create/guest', component: CreateGuestCustomerComponent },
|
||||
// { path: 'create/webshop-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'webshop' } },
|
||||
// { path: 'create/store-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'store' } },
|
||||
// {
|
||||
// path: 'create/webshop-p4m/update',
|
||||
// component: UpdateP4MWebshopCustomerComponent,
|
||||
// data: { customerType: 'webshop' },
|
||||
// },
|
||||
{
|
||||
path: 'create-customer-main',
|
||||
outlet: 'side',
|
||||
component: CustomerCreateSideViewComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { CustomerComponent } from './customer-page.component';
|
||||
import { CustomerSearchComponent } from './customer-search/customer-search.component';
|
||||
import { CustomerResultsMainViewComponent } from './customer-search/results-main-view/results-main-view.component';
|
||||
import { CustomerDetailsViewMainComponent } from './customer-search/details-main-view/details-main-view.component';
|
||||
import { CustomerHistoryMainViewComponent } from './customer-search/history-main-view/history-main-view.component';
|
||||
import { CustomerFilterMainViewComponent } from './customer-search/filter-main-view/filter-main-view.component';
|
||||
import { CustomerCreateGuard } from './guards/customer-create.guard';
|
||||
import {
|
||||
CreateB2BCustomerComponent,
|
||||
CreateGuestCustomerComponent,
|
||||
// CreateP4MCustomerComponent,
|
||||
CreateStoreCustomerComponent,
|
||||
CreateWebshopCustomerComponent,
|
||||
} from './create-customer';
|
||||
// import { UpdateP4MWebshopCustomerComponent } from './create-customer/update-p4m-webshop-customer';
|
||||
import { CreateCustomerComponent } from './create-customer/create-customer.component';
|
||||
import { CustomerDataEditB2BComponent } from './customer-search/edit-main-view/customer-data-edit-b2b.component';
|
||||
import { CustomerDataEditB2CComponent } from './customer-search/edit-main-view/customer-data-edit-b2c.component';
|
||||
import { AddBillingAddressMainViewComponent } from './customer-search/add-billing-address-main-view/add-billing-address-main-view.component';
|
||||
import { AddShippingAddressMainViewComponent } from './customer-search/add-shipping-address-main-view/add-shipping-address-main-view.component';
|
||||
import { EditBillingAddressMainViewComponent } from './customer-search/edit-billing-address-main-view/edit-billing-address-main-view.component';
|
||||
import { EditShippingAddressMainViewComponent } from './customer-search/edit-shipping-address-main-view/edit-shipping-address-main-view.component';
|
||||
import { CustomerOrdersMainViewComponent } from './customer-search/orders-main-view/orders-main-view.component';
|
||||
import { OrderDetailsMainViewComponent } from './customer-search/order-details-main-view/order-details-main-view.component';
|
||||
import { KundenkarteMainViewComponent } from './customer-search/kundenkarte-main-view/kundenkarte-main-view.component';
|
||||
import { CustomerOrderDetailsHistoryMainViewComponent } from './customer-search/order-details-history-main-view/order-details-history-main-view.component';
|
||||
import { CustomerMainViewComponent } from './customer-search/main-view/main-view.component';
|
||||
import { MainSideViewComponent } from './customer-search/main-side-view/main-side-view.component';
|
||||
import { CustomerResultsSideViewComponent } from './customer-search/results-side-view/results-side-view.component';
|
||||
import { OrderDetailsSideViewComponent } from './customer-search/order-details-side-view/order-details-side-view.component';
|
||||
import { CustomerCreateSideViewComponent } from './create-customer/customer-create-side-view';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CustomerComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: CustomerSearchComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'search',
|
||||
component: CustomerMainViewComponent,
|
||||
title: 'Kundensuche',
|
||||
data: { side: 'main', breadcrumb: 'main' },
|
||||
},
|
||||
{
|
||||
path: 'search/list',
|
||||
component: CustomerResultsMainViewComponent,
|
||||
title: 'Kundensuche - Trefferliste',
|
||||
data: { breadcrumb: 'search' },
|
||||
},
|
||||
{
|
||||
path: 'search/filter',
|
||||
component: CustomerFilterMainViewComponent,
|
||||
title: 'Kundensuche - Filter',
|
||||
data: { side: 'results', breadcrumb: 'filter' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId',
|
||||
component: CustomerDetailsViewMainComponent,
|
||||
title: 'Kundendetails',
|
||||
data: { side: 'results', breadcrumb: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/history',
|
||||
component: CustomerHistoryMainViewComponent,
|
||||
title: 'Kundendetails - Verlauf',
|
||||
data: { side: 'results', breadcrumb: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/kundenkarte',
|
||||
component: KundenkarteMainViewComponent,
|
||||
title: 'Kundendetails - Kundenkarte',
|
||||
data: { side: 'results', breadcrumb: 'kundenkarte' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders',
|
||||
component: CustomerOrdersMainViewComponent,
|
||||
title: 'Kundendetails - Bestellungen',
|
||||
data: { side: 'results', breadcrumb: 'orders' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId',
|
||||
component: OrderDetailsMainViewComponent,
|
||||
title: 'Kundendetails - Bestelldetails',
|
||||
data: { side: 'order-details', breadcrumb: 'order-details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId/:orderItemId',
|
||||
component: OrderDetailsMainViewComponent,
|
||||
title: 'Kundendetails - Bestelldetails',
|
||||
data: { side: 'order-details', breadcrumb: 'order-details' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/orders/:orderId/:orderItemId/history',
|
||||
component: CustomerOrderDetailsHistoryMainViewComponent,
|
||||
title: 'Kundendetails - Bestelldetails Verlauf',
|
||||
data: {
|
||||
side: 'order-details',
|
||||
breadcrumb: 'order-details-history',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/edit/b2b',
|
||||
component: CustomerDataEditB2BComponent,
|
||||
title: 'Kundendetails - Bearbeiten (B2B)',
|
||||
data: { side: 'results', breadcrumb: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/edit',
|
||||
component: CustomerDataEditB2CComponent,
|
||||
title: 'Kundendetails - Bearbeiten',
|
||||
data: { side: 'results', breadcrumb: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/billingaddress/add',
|
||||
component: AddBillingAddressMainViewComponent,
|
||||
title: 'Kundendetails - Neue Rechnungsadresse',
|
||||
data: { side: 'results', breadcrumb: 'add-billing-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/billingaddress/:payerId/edit',
|
||||
component: EditBillingAddressMainViewComponent,
|
||||
title: 'Kundendetails - Rechnungsadresse bearbeiten',
|
||||
data: { side: 'results', breadcrumb: 'edit-billing-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/shippingaddress/add',
|
||||
component: AddShippingAddressMainViewComponent,
|
||||
title: 'Kundendetails - Neue Lieferadresse',
|
||||
data: { side: 'results', breadcrumb: 'add-shipping-address' },
|
||||
},
|
||||
{
|
||||
path: 'search/:customerId/shippingaddress/:shippingAddressId/edit',
|
||||
component: EditShippingAddressMainViewComponent,
|
||||
title: 'Kundendetails - Lieferadresse bearbeiten',
|
||||
data: { side: 'results', breadcrumb: 'edit-shipping-address' },
|
||||
},
|
||||
{
|
||||
path: 'search-customer-main',
|
||||
outlet: 'side',
|
||||
component: MainSideViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'results',
|
||||
outlet: 'side',
|
||||
component: CustomerResultsSideViewComponent,
|
||||
},
|
||||
{
|
||||
path: 'order-details',
|
||||
outlet: 'side',
|
||||
component: OrderDetailsSideViewComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
component: CreateCustomerComponent,
|
||||
canActivate: [CustomerCreateGuard],
|
||||
canActivateChild: [CustomerCreateGuard],
|
||||
title: 'Kundendaten erfassen',
|
||||
children: [
|
||||
{ path: 'create', component: CreateStoreCustomerComponent },
|
||||
{ path: 'create/store', component: CreateStoreCustomerComponent },
|
||||
{ path: 'create/webshop', component: CreateWebshopCustomerComponent },
|
||||
{ path: 'create/b2b', component: CreateB2BCustomerComponent },
|
||||
{ path: 'create/guest', component: CreateGuestCustomerComponent },
|
||||
// { path: 'create/webshop-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'webshop' } },
|
||||
// { path: 'create/store-p4m', component: CreateP4MCustomerComponent, data: { customerType: 'store' } },
|
||||
// {
|
||||
// path: 'create/webshop-p4m/update',
|
||||
// component: UpdateP4MWebshopCustomerComponent,
|
||||
// data: { customerType: 'webshop' },
|
||||
// },
|
||||
{
|
||||
path: 'create-customer-main',
|
||||
outlet: 'side',
|
||||
component: CustomerCreateSideViewComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: DashboardComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class DashboardRoutingModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: DashboardComponent,
|
||||
title: 'Dashboard',
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class DashboardRoutingModule {}
|
||||
|
||||
@@ -1,35 +1,51 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { GoodsInCleanupListComponent } from './goods-in-cleanup-list/goods-in-cleanup-list.component';
|
||||
import { GoodsInCleanupListModule } from './goods-in-cleanup-list/goods-in-cleanup-list.module';
|
||||
import { GoodsInListComponent } from './goods-in-list/goods-in-list.component';
|
||||
import { GoodsInListModule } from './goods-in-list/goods-in-list.module';
|
||||
import { GoodsInRemissionPreviewComponent } from './goods-in-remission-preview/goods-in-remission-preview.component';
|
||||
import { GoodsInRemissionPreviewModule } from './goods-in-remission-preview/goods-in-remission-preview.module';
|
||||
import { GoodsInReservationComponent } from './goods-in-reservation/goods-in-reservation.component';
|
||||
import { GoodsInReservationModule } from './goods-in-reservation/goods-in-reservation.module';
|
||||
import { GoodsInComponent } from './goods-in.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: GoodsInComponent,
|
||||
children: [
|
||||
{ path: 'list', component: GoodsInListComponent },
|
||||
{ path: 'reservation', component: GoodsInReservationComponent },
|
||||
{ path: 'cleanup', component: GoodsInCleanupListComponent },
|
||||
{ path: 'preview', component: GoodsInRemissionPreviewComponent },
|
||||
],
|
||||
},
|
||||
];
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
GoodsInListModule,
|
||||
GoodsInCleanupListModule,
|
||||
GoodsInReservationModule,
|
||||
GoodsInRemissionPreviewModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class GoodsInRoutingModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { GoodsInCleanupListComponent } from './goods-in-cleanup-list/goods-in-cleanup-list.component';
|
||||
import { GoodsInCleanupListModule } from './goods-in-cleanup-list/goods-in-cleanup-list.module';
|
||||
import { GoodsInListComponent } from './goods-in-list/goods-in-list.component';
|
||||
import { GoodsInListModule } from './goods-in-list/goods-in-list.module';
|
||||
import { GoodsInRemissionPreviewComponent } from './goods-in-remission-preview/goods-in-remission-preview.component';
|
||||
import { GoodsInRemissionPreviewModule } from './goods-in-remission-preview/goods-in-remission-preview.module';
|
||||
import { GoodsInReservationComponent } from './goods-in-reservation/goods-in-reservation.component';
|
||||
import { GoodsInReservationModule } from './goods-in-reservation/goods-in-reservation.module';
|
||||
import { GoodsInComponent } from './goods-in.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: GoodsInComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
component: GoodsInListComponent,
|
||||
title: 'Abholfach - Fehlende',
|
||||
},
|
||||
{
|
||||
path: 'reservation',
|
||||
component: GoodsInReservationComponent,
|
||||
title: 'Abholfach - Reservierung',
|
||||
},
|
||||
{
|
||||
path: 'cleanup',
|
||||
component: GoodsInCleanupListComponent,
|
||||
title: 'Abholfach - Ausräumen',
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
component: GoodsInRemissionPreviewComponent,
|
||||
title: 'Abholfach - Vorschau',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
GoodsInListModule,
|
||||
GoodsInCleanupListModule,
|
||||
GoodsInReservationModule,
|
||||
GoodsInRemissionPreviewModule,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class GoodsInRoutingModule {}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { PackageDetailsComponent } from './package-details';
|
||||
import { PackageInspectionComponent } from './package-inspection.component';
|
||||
import { PackageResultComponent } from './package-result';
|
||||
|
||||
export const packageInspectionRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PackageInspectionComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'packages',
|
||||
component: PackageResultComponent,
|
||||
},
|
||||
{
|
||||
path: 'packages/:id',
|
||||
component: PackageDetailsComponent,
|
||||
},
|
||||
{ path: '**', redirectTo: 'packages' },
|
||||
],
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { PackageDetailsComponent } from './package-details';
|
||||
import { PackageInspectionComponent } from './package-inspection.component';
|
||||
import { PackageResultComponent } from './package-result';
|
||||
|
||||
export const packageInspectionRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PackageInspectionComponent,
|
||||
title: 'Packstück-Prüfung',
|
||||
children: [
|
||||
{
|
||||
path: 'packages',
|
||||
component: PackageResultComponent,
|
||||
},
|
||||
{
|
||||
path: 'packages/:id',
|
||||
component: PackageDetailsComponent,
|
||||
title: 'Packstück-Prüfung - Details',
|
||||
},
|
||||
{ path: '**', redirectTo: 'packages' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,146 +1,168 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { PickupShelfInComponent } from './pickup-shelf-in.component';
|
||||
import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
|
||||
import { PickupShelfInDetailsComponent } from './pickup-shelf-in-details/pickup-shelf-in-details.component';
|
||||
import { viewResolver } from '../resolvers/view.resolver';
|
||||
import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
|
||||
import { PickUpShelfInMainSideViewComponent } from './pickup-shelf-in-main-side-view/pickup-shelf-in-main-side-view.component';
|
||||
import { PickUpShelfInMainComponent } from './pickup-shelf-in-main/pickup-shelf-in-main.component';
|
||||
import { PickUpShelfInListComponent } from './pickup-shelf-in-list/pickup-shelf-in-list.component';
|
||||
import { PickupShelfInEditComponent } from './pickup-shelf-in-edit/pickup-shelf-in-edit.component';
|
||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PickupShelfInComponent,
|
||||
resolve: {
|
||||
view: viewResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [
|
||||
{
|
||||
path: 'main',
|
||||
component: PickUpShelfInMainComponent,
|
||||
data: {
|
||||
view: 'main',
|
||||
matomo: {
|
||||
title: 'Abholfach',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: PickUpShelfInListComponent,
|
||||
data: {
|
||||
view: 'list',
|
||||
matomo: {
|
||||
title: 'Abholfach - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'list/filter',
|
||||
component: PickupShelfFilterComponent,
|
||||
data: {
|
||||
view: 'filter',
|
||||
matomo: {
|
||||
title: 'Abholfach - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{ path: 'search', component: PickUpShelfInMainSideViewComponent, outlet: 'side' },
|
||||
{ path: 'list', component: PickUpShelfInListComponent, data: { view: 'list' }, outlet: 'side' },
|
||||
],
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { PickupShelfInComponent } from './pickup-shelf-in.component';
|
||||
import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
|
||||
import { PickupShelfInDetailsComponent } from './pickup-shelf-in-details/pickup-shelf-in-details.component';
|
||||
import { viewResolver } from '../resolvers/view.resolver';
|
||||
import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
|
||||
import { PickUpShelfInMainSideViewComponent } from './pickup-shelf-in-main-side-view/pickup-shelf-in-main-side-view.component';
|
||||
import { PickUpShelfInMainComponent } from './pickup-shelf-in-main/pickup-shelf-in-main.component';
|
||||
import { PickUpShelfInListComponent } from './pickup-shelf-in-list/pickup-shelf-in-list.component';
|
||||
import { PickupShelfInEditComponent } from './pickup-shelf-in-edit/pickup-shelf-in-edit.component';
|
||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PickupShelfInComponent,
|
||||
resolve: {
|
||||
view: viewResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
title: 'Abholfach - Einbuchen',
|
||||
children: [
|
||||
{
|
||||
path: 'main',
|
||||
component: PickUpShelfInMainComponent,
|
||||
data: {
|
||||
view: 'main',
|
||||
matomo: {
|
||||
title: 'Abholfach',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: PickUpShelfInListComponent,
|
||||
title: 'Abholfach - Trefferliste',
|
||||
data: {
|
||||
view: 'list',
|
||||
matomo: {
|
||||
title: 'Abholfach - Trefferliste',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'list/filter',
|
||||
component: PickupShelfFilterComponent,
|
||||
title: 'Abholfach - Filter',
|
||||
data: {
|
||||
view: 'filter',
|
||||
matomo: {
|
||||
title: 'Abholfach - Filter',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/edit',
|
||||
component: PickupShelfInEditComponent,
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
data: {
|
||||
view: 'edit',
|
||||
matomo: {
|
||||
title: 'Abholfach - Bearbeiten',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Abholfach - Historie',
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Abholfach - Historie',
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Abholfach - Historie',
|
||||
data: {
|
||||
view: 'history',
|
||||
matomo: {
|
||||
title: 'Abholfach - Historie',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
title: 'Abholfach - Details',
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
title: 'Abholfach - Details',
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/:orderItemSubsetId',
|
||||
component: PickupShelfInDetailsComponent,
|
||||
title: 'Abholfach - Details',
|
||||
data: {
|
||||
view: 'details',
|
||||
matomo: {
|
||||
title: 'Abholfach - Details',
|
||||
} as MatomoRouteData,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
component: PickUpShelfInMainSideViewComponent,
|
||||
|
||||
outlet: 'side',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: PickUpShelfInListComponent,
|
||||
data: { view: 'list' },
|
||||
outlet: 'side',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,75 +1,107 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { PickupShelfOutComponent } from './pickup-shelf-out.component';
|
||||
import { PickupShelfOutMainComponent } from './pickup-shelf-out-main/pickup-shelf-out-main.component';
|
||||
|
||||
import { PickupShelfOutListComponent } from './pickup-shelf-out-list/pickup-shelf-out-list.component';
|
||||
import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
|
||||
import { PickupShelfOutDetailsComponent } from './pickup-shelf-out-details/pickup-shelf-out-details.component';
|
||||
import { PickupShelfOutMainSideViewComponent } from './pickup-shelf-out-main-side-view/pickup-shelf-out-main-side-view.component';
|
||||
import { viewResolver } from '../resolvers/view.resolver';
|
||||
import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
|
||||
import { PickupShelfOutEditComponent } from './pickup-shelf-out-edit/pickup-shelf-out-edit.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PickupShelfOutComponent,
|
||||
resolve: {
|
||||
view: viewResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
children: [
|
||||
{ path: 'main', component: PickupShelfOutMainComponent, data: { view: 'main' } },
|
||||
{ path: 'list', component: PickupShelfOutListComponent, data: { view: 'list' } },
|
||||
{ path: 'list/filter', component: PickupShelfFilterComponent, data: { view: 'filter' } },
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{ path: 'search', component: PickupShelfOutMainSideViewComponent, outlet: 'side' },
|
||||
{ path: 'list', component: PickupShelfOutListComponent, data: { view: 'list' }, outlet: 'side' },
|
||||
],
|
||||
},
|
||||
];
|
||||
import { Routes } from '@angular/router';
|
||||
import { PickupShelfOutComponent } from './pickup-shelf-out.component';
|
||||
import { PickupShelfOutMainComponent } from './pickup-shelf-out-main/pickup-shelf-out-main.component';
|
||||
|
||||
import { PickupShelfOutListComponent } from './pickup-shelf-out-list/pickup-shelf-out-list.component';
|
||||
import { PickupShelfFilterComponent } from '../shared/pickup-shelf-filter/pickup-shelf-filter.component';
|
||||
import { PickupShelfOutDetailsComponent } from './pickup-shelf-out-details/pickup-shelf-out-details.component';
|
||||
import { PickupShelfOutMainSideViewComponent } from './pickup-shelf-out-main-side-view/pickup-shelf-out-main-side-view.component';
|
||||
import { viewResolver } from '../resolvers/view.resolver';
|
||||
import { PickUpShelfHistoryComponent } from '../shared/pickup-shelf-history/pickup-shelf-history.component';
|
||||
import { PickupShelfOutEditComponent } from './pickup-shelf-out-edit/pickup-shelf-out-edit.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PickupShelfOutComponent,
|
||||
resolve: {
|
||||
view: viewResolver,
|
||||
},
|
||||
runGuardsAndResolvers: 'always',
|
||||
title: 'Warenausgabe',
|
||||
children: [
|
||||
{
|
||||
path: 'main',
|
||||
component: PickupShelfOutMainComponent,
|
||||
data: { view: 'main' },
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: PickupShelfOutListComponent,
|
||||
data: { view: 'list' },
|
||||
},
|
||||
{
|
||||
path: 'list/filter',
|
||||
component: PickupShelfFilterComponent,
|
||||
data: { view: 'filter' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/edit',
|
||||
component: PickupShelfOutEditComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'edit' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Warenausgabe - Verlauf',
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Warenausgabe - Verlauf',
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus/history',
|
||||
component: PickUpShelfHistoryComponent,
|
||||
title: 'Warenausgabe - Verlauf',
|
||||
data: { view: 'history' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/:orderNumber/item/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'order/:orderId/compartment/:compartmentCode/:compartmentInfo/status/:orderItemProcessingStatus',
|
||||
component: PickupShelfOutDetailsComponent,
|
||||
title: 'Warenausgabe - Details',
|
||||
data: { view: 'details' },
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
component: PickupShelfOutMainSideViewComponent,
|
||||
outlet: 'side',
|
||||
data: { view: 'search' },
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: PickupShelfOutListComponent,
|
||||
data: { view: 'list' },
|
||||
outlet: 'side',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,31 +1,75 @@
|
||||
@if (orderItem) {
|
||||
<div #features class="page-pickup-shelf-details-item__features">
|
||||
@if (orderItem?.features?.prebooked) {
|
||||
<img [uiOverlayTrigger]="prebookedTooltip" src="/assets/images/tag_icon_preorder.svg" [alt]="orderItem?.features?.prebooked" />
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #prebookedTooltip [closeable]="true">
|
||||
<img
|
||||
[uiOverlayTrigger]="prebookedTooltip"
|
||||
src="/assets/images/tag_icon_preorder.svg"
|
||||
[alt]="orderItem?.features?.prebooked"
|
||||
/>
|
||||
<ui-tooltip
|
||||
yPosition="above"
|
||||
xPosition="after"
|
||||
[yOffset]="-11"
|
||||
[xOffset]="-8"
|
||||
#prebookedTooltip
|
||||
[closeable]="true"
|
||||
>
|
||||
Artikel wird für Sie vorgemerkt.
|
||||
</ui-tooltip>
|
||||
}
|
||||
@if (hasEmailNotification$ | async) {
|
||||
<img [uiOverlayTrigger]="emailTooltip" src="/assets/images/email_bookmark.svg" />
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #emailTooltip [closeable]="true">
|
||||
<img
|
||||
[uiOverlayTrigger]="emailTooltip"
|
||||
src="/assets/images/email_bookmark.svg"
|
||||
/>
|
||||
<ui-tooltip
|
||||
yPosition="above"
|
||||
xPosition="after"
|
||||
[yOffset]="-11"
|
||||
[xOffset]="-8"
|
||||
#emailTooltip
|
||||
[closeable]="true"
|
||||
>
|
||||
Per E-Mail benachrichtigt
|
||||
<br />
|
||||
@for (notifications of emailNotificationDates$ | async; track notifications) {
|
||||
@for (notificationDate of notifications.dates; track notificationDate) {
|
||||
{{ notifications.type | notificationType }} {{ notificationDate | date: 'dd.MM.yyyy | HH:mm' }} Uhr
|
||||
@for (
|
||||
notifications of emailNotificationDates$ | async;
|
||||
track notifications
|
||||
) {
|
||||
@for (
|
||||
notificationDate of notifications.dates;
|
||||
track notificationDate
|
||||
) {
|
||||
{{ notifications.type | notificationType }}
|
||||
{{ notificationDate | date: 'dd.MM.yyyy | HH:mm' }} Uhr
|
||||
<br />
|
||||
}
|
||||
}
|
||||
</ui-tooltip>
|
||||
}
|
||||
@if (hasSmsNotification$ | async) {
|
||||
<img [uiOverlayTrigger]="smsTooltip" src="/assets/images/sms_bookmark.svg" />
|
||||
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #smsTooltip [closeable]="true">
|
||||
<img
|
||||
[uiOverlayTrigger]="smsTooltip"
|
||||
src="/assets/images/sms_bookmark.svg"
|
||||
/>
|
||||
<ui-tooltip
|
||||
yPosition="above"
|
||||
xPosition="after"
|
||||
[yOffset]="-11"
|
||||
[xOffset]="-8"
|
||||
#smsTooltip
|
||||
[closeable]="true"
|
||||
>
|
||||
Per SMS benachrichtigt
|
||||
<br />
|
||||
@for (notifications of smsNotificationDates$ | async; track notifications) {
|
||||
@for (notificationDate of notifications.dates; track notificationDate) {
|
||||
@for (
|
||||
notifications of smsNotificationDates$ | async;
|
||||
track notifications
|
||||
) {
|
||||
@for (
|
||||
notificationDate of notifications.dates;
|
||||
track notificationDate
|
||||
) {
|
||||
{{ notificationDate | date: 'dd.MM.yyyy | HH:mm' }} Uhr
|
||||
<br />
|
||||
}
|
||||
@@ -39,280 +83,336 @@
|
||||
[productImageNavigation]="orderItem?.product?.ean"
|
||||
[src]="orderItem.product?.ean | productImage"
|
||||
[alt]="orderItem.product?.name"
|
||||
/>
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.High">
|
||||
Prämie
|
||||
</ui-label>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div class="page-pickup-shelf-details-item__details">
|
||||
<div class="flex flex-row justify-between items-start mb-[1.3125rem]">
|
||||
<h3
|
||||
[uiElementDistance]="features"
|
||||
#elementDistance="uiElementDistance"
|
||||
[style.max-width.px]="elementDistance.distanceChange | async"
|
||||
class="flex flex-col"
|
||||
>
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<ui-label class="w-10 mb-2">Prämie</ui-label>
|
||||
}
|
||||
<div class="font-normal mb-[0.375rem]">
|
||||
{{ orderItem.product?.contributors }}
|
||||
</div>
|
||||
<div>{{ orderItem.product?.name }}</div>
|
||||
</h3>
|
||||
<div class="history-wrapper flex flex-col items-end justify-center">
|
||||
<button
|
||||
class="cta-history text-p1"
|
||||
(click)="historyClick.emit(orderItem)"
|
||||
>
|
||||
Historie
|
||||
</button>
|
||||
@if (selectable$ | async) {
|
||||
<input
|
||||
[ngModel]="selected$ | async"
|
||||
(ngModelChange)="
|
||||
setSelected($event);
|
||||
tracker.trackEvent({
|
||||
category: 'pickup-shelf-list-item',
|
||||
action: 'select',
|
||||
name: orderItem?.product?.name,
|
||||
value: $event ? 1 : 0,
|
||||
})
|
||||
"
|
||||
class="isa-select-bullet mt-4"
|
||||
type="checkbox"
|
||||
matomoTracker
|
||||
#tracker="matomo"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-pickup-shelf-details-item__details">
|
||||
<div class="flex flex-row justify-between items-start mb-[1.3125rem]">
|
||||
<h3
|
||||
[uiElementDistance]="features"
|
||||
#elementDistance="uiElementDistance"
|
||||
[style.max-width.px]="elementDistance.distanceChange | async"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<div class="font-normal mb-[0.375rem]">{{ orderItem.product?.contributors }}</div>
|
||||
<div>{{ orderItem.product?.name }}</div>
|
||||
</h3>
|
||||
<div class="history-wrapper flex flex-col items-end justify-center">
|
||||
<button class="cta-history text-p1" (click)="historyClick.emit(orderItem)">Historie</button>
|
||||
@if (selectable$ | async) {
|
||||
<input
|
||||
[ngModel]="selected$ | async"
|
||||
(ngModelChange)="
|
||||
setSelected($event);
|
||||
tracker.trackEvent({
|
||||
category: 'pickup-shelf-list-item',
|
||||
action: 'select',
|
||||
name: orderItem?.product?.name,
|
||||
value: $event ? 1 : 0,
|
||||
})
|
||||
"
|
||||
class="isa-select-bullet mt-4"
|
||||
type="checkbox"
|
||||
matomoTracker
|
||||
#tracker="matomo"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div class="detail">
|
||||
<div class="label">Menge</div>
|
||||
<div class="value">
|
||||
@if (!(canChangeQuantity$ | async)) {
|
||||
{{ orderItem?.quantity }}x
|
||||
}
|
||||
@if (canChangeQuantity$ | async) {
|
||||
<ui-quantity-dropdown
|
||||
[showTrash]="false"
|
||||
[range]="orderItem?.quantity"
|
||||
[(ngModel)]="quantity"
|
||||
(ngModelChange)="
|
||||
tracker.trackEvent({
|
||||
category: 'pickup-shelf-list-item',
|
||||
action: 'quantity',
|
||||
name: orderItem?.product?.name,
|
||||
value: $event,
|
||||
})
|
||||
"
|
||||
[showSpinner]="false"
|
||||
matomoTracker
|
||||
#tracker="matomo"
|
||||
></ui-quantity-dropdown>
|
||||
}
|
||||
<span class="overall-quantity"
|
||||
>(von {{ orderItem?.overallQuantity }})</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@if (!!orderItem.product?.formatDetail) {
|
||||
<div class="detail">
|
||||
<div class="label">Menge</div>
|
||||
<div class="label">Format</div>
|
||||
<div class="value">
|
||||
@if (!(canChangeQuantity$ | async)) {
|
||||
{{ orderItem?.quantity }}x
|
||||
@if (
|
||||
orderItem?.product?.format &&
|
||||
orderItem?.product?.format !== 'UNKNOWN'
|
||||
) {
|
||||
<img
|
||||
class="format-icon"
|
||||
[src]="
|
||||
'/assets/images/Icon_' + orderItem.product?.format + '.svg'
|
||||
"
|
||||
alt="format icon"
|
||||
/>
|
||||
}
|
||||
@if (canChangeQuantity$ | async) {
|
||||
<ui-quantity-dropdown
|
||||
[showTrash]="false"
|
||||
[range]="orderItem?.quantity"
|
||||
[(ngModel)]="quantity"
|
||||
(ngModelChange)="
|
||||
tracker.trackEvent({ category: 'pickup-shelf-list-item', action: 'quantity', name: orderItem?.product?.name, value: $event })
|
||||
"
|
||||
[showSpinner]="false"
|
||||
matomoTracker
|
||||
#tracker="matomo"
|
||||
></ui-quantity-dropdown>
|
||||
}
|
||||
<span class="overall-quantity">(von {{ orderItem?.overallQuantity }})</span>
|
||||
<span>{{ orderItem.product?.formatDetail }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (!!orderItem.product?.formatDetail) {
|
||||
<div class="detail">
|
||||
<div class="label">Format</div>
|
||||
}
|
||||
@if (!!orderItem.product?.ean) {
|
||||
<div class="detail">
|
||||
<div class="label">ISBN/EAN</div>
|
||||
<div class="value">{{ orderItem.product?.ean }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (orderItem.price !== undefined || (hasRewardPoints$ | async)) {
|
||||
<div class="detail">
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<div class="label">Prämie</div>
|
||||
<div class="value">
|
||||
@if (orderItem?.product?.format && orderItem?.product?.format !== 'UNKNOWN') {
|
||||
<img
|
||||
class="format-icon"
|
||||
[src]="'/assets/images/Icon_' + orderItem.product?.format + '.svg'"
|
||||
alt="format icon"
|
||||
/>
|
||||
}
|
||||
<span>{{ orderItem.product?.formatDetail }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!!orderItem.product?.ean) {
|
||||
<div class="detail">
|
||||
<div class="label">ISBN/EAN</div>
|
||||
<div class="value">{{ orderItem.product?.ean }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (orderItem.price !== undefined || (hasRewardPoints$ | async)) {
|
||||
<div class="detail">
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<div class="label">Prämie</div>
|
||||
<div class="value">{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte</div>
|
||||
} @else {
|
||||
<div class="label">Preis</div>
|
||||
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!!orderItem.retailPrice?.vat?.inPercent) {
|
||||
<div class="detail">
|
||||
<div class="label">MwSt</div>
|
||||
<div class="value">{{ orderItem.retailPrice?.vat?.inPercent }}%</div>
|
||||
</div>
|
||||
}
|
||||
<hr class="border-[#EDEFF0] border-t-2 my-4" />
|
||||
@if (orderItem.supplier) {
|
||||
<div class="detail">
|
||||
<div class="label">Lieferant</div>
|
||||
<div class="value">{{ orderItem.supplier }}</div>
|
||||
@if (!expanded) {
|
||||
<button
|
||||
(click)="expanded = !expanded"
|
||||
type="button"
|
||||
class="page-pickup-shelf-details-item__more text-[#0556B4] font-bold flex flex-row items-center justify-center"
|
||||
[class.flex-row-reverse]="!expanded"
|
||||
>
|
||||
<shared-icon class="mr-1" icon="arrow-back" [size]="20" [class.ml-1]="!expanded" [class.rotate-180]="!expanded"></shared-icon>
|
||||
{{ expanded ? 'Weniger' : 'Mehr' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!!orderItem.ssc || !!orderItem.sscText) {
|
||||
<div class="detail">
|
||||
<div class="label">Meldenummer</div>
|
||||
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (expanded) {
|
||||
@if (!!orderItem.targetBranch) {
|
||||
<div class="detail">
|
||||
<div class="label">Zielfiliale</div>
|
||||
<div class="value">{{ orderItem.targetBranch }}</div>
|
||||
{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte
|
||||
</div>
|
||||
} @else {
|
||||
<div class="label">Preis</div>
|
||||
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
|
||||
}
|
||||
<div class="detail">
|
||||
<div class="label">
|
||||
@if (
|
||||
orderItemFeature(orderItem) === 'Versand' ||
|
||||
orderItemFeature(orderItem) === 'B2B-Versand' ||
|
||||
orderItemFeature(orderItem) === 'DIG-Versand'
|
||||
) {
|
||||
{{ orderItem?.estimatedDelivery ? 'Lieferung zwischen' : 'Lieferung ab' }}
|
||||
}
|
||||
@if (orderItemFeature(orderItem) === 'Abholung' || orderItemFeature(orderItem) === 'Rücklage') {
|
||||
Abholung ab
|
||||
}
|
||||
</div>
|
||||
@if (!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate) {
|
||||
<div class="value bg-[#D8DFE5] rounded w-max px-2">
|
||||
@if (!!orderItem?.estimatedDelivery) {
|
||||
{{ orderItem?.estimatedDelivery?.start | date: 'dd.MM.yy' }} und
|
||||
{{ orderItem?.estimatedDelivery?.stop | date: 'dd.MM.yy' }}
|
||||
} @else {
|
||||
@if (!!orderItem?.estimatedShippingDate) {
|
||||
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<hr class="border-[#EDEFF0] border-t-2 my-4" />
|
||||
@if (!!orderItem?.compartmentCode) {
|
||||
<div class="detail">
|
||||
<div class="label">Abholfachnr.</div>
|
||||
<div class="value">{{ orderItem?.compartmentCode }}</div>
|
||||
</div>
|
||||
}
|
||||
<div class="detail">
|
||||
<div class="label">Vormerker</div>
|
||||
<div class="value">{{ orderItem.isPrebooked ? 'Ja' : 'Nein' }}</div>
|
||||
</div>
|
||||
<hr class="border-[#EDEFF0] border-t-2 my-4" />
|
||||
@if (!!orderItem.paymentProcessing) {
|
||||
<div class="detail">
|
||||
<div class="label">Zahlungsweg</div>
|
||||
<div class="value">{{ orderItem.paymentProcessing || '-' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (!!orderItem.paymentType) {
|
||||
<div class="detail">
|
||||
<div class="label">Zahlungsart</div>
|
||||
<div class="value">{{ orderItem.paymentType | paymentType }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (receiptCount$ | async; as count) {
|
||||
<h4 class="receipt-header">
|
||||
{{ count > 1 ? 'Belege' : 'Beleg' }}
|
||||
</h4>
|
||||
}
|
||||
@for (receipt of receipts$ | async; track receipt) {
|
||||
@if (!!receipt?.receiptNumber) {
|
||||
<div class="detail">
|
||||
<div class="label">Belegnummer</div>
|
||||
<div class="value">{{ receipt?.receiptNumber }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (!!receipt?.printedDate) {
|
||||
<div class="detail">
|
||||
<div class="label">Erstellt am</div>
|
||||
<div class="value">{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
|
||||
</div>
|
||||
}
|
||||
@if (!!receipt?.receiptText) {
|
||||
<div class="detail">
|
||||
<div class="label">Rechnungstext</div>
|
||||
<div class="value">{{ receipt?.receiptText || '-' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (!!receipt?.receiptType) {
|
||||
<div class="detail">
|
||||
<div class="label">Belegart</div>
|
||||
<div class="value">
|
||||
{{ receipt?.receiptType === 1 ? 'Lieferschein' : receipt?.receiptType === 64 ? 'Zahlungsbeleg' : '-' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (!!orderItem.paymentProcessing || !!orderItem.paymentType || !!(receiptCount$ | async)) {
|
||||
<hr
|
||||
class="border-[#EDEFF0] border-t-2 my-4"
|
||||
/>
|
||||
}
|
||||
@if (expanded) {
|
||||
</div>
|
||||
}
|
||||
@if (!!orderItem.retailPrice?.vat?.inPercent) {
|
||||
<div class="detail">
|
||||
<div class="label">MwSt</div>
|
||||
<div class="value">{{ orderItem.retailPrice?.vat?.inPercent }}%</div>
|
||||
</div>
|
||||
}
|
||||
<hr class="border-[#EDEFF0] border-t-2 my-4" />
|
||||
@if (orderItem.supplier) {
|
||||
<div class="detail">
|
||||
<div class="label">Lieferant</div>
|
||||
<div class="value">{{ orderItem.supplier }}</div>
|
||||
@if (!expanded) {
|
||||
<button
|
||||
(click)="expanded = !expanded"
|
||||
type="button"
|
||||
class="page-pickup-shelf-details-item__less text-[#0556B4] font-bold flex flex-row items-center justify-center"
|
||||
>
|
||||
<shared-icon class="mr-1" icon="arrow-back" [size]="20"></shared-icon>
|
||||
Weniger
|
||||
class="page-pickup-shelf-details-item__more text-[#0556B4] font-bold flex flex-row items-center justify-center"
|
||||
[class.flex-row-reverse]="!expanded"
|
||||
>
|
||||
<shared-icon
|
||||
class="mr-1"
|
||||
icon="arrow-back"
|
||||
[size]="20"
|
||||
[class.ml-1]="!expanded"
|
||||
[class.rotate-180]="!expanded"
|
||||
></shared-icon>
|
||||
{{ expanded ? 'Weniger' : 'Mehr' }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!!orderItem.ssc || !!orderItem.sscText) {
|
||||
<div class="detail">
|
||||
<div class="label">Meldenummer</div>
|
||||
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (expanded) {
|
||||
@if (!!orderItem.targetBranch) {
|
||||
<div class="detail">
|
||||
<div class="label">Zielfiliale</div>
|
||||
<div class="value">{{ orderItem.targetBranch }}</div>
|
||||
</div>
|
||||
}
|
||||
<div class="page-pickup-shelf-details-item__comment flex flex-col items-start mt-[1.625rem]">
|
||||
<div class="label mb-[0.375rem]">Anmerkung</div>
|
||||
<div class="flex flex-row w-full">
|
||||
<textarea
|
||||
matInput
|
||||
cdkTextareaAutosize
|
||||
#autosize="cdkTextareaAutosize"
|
||||
cdkAutosizeMinRows="1"
|
||||
cdkAutosizeMaxRows="5"
|
||||
maxlength="200"
|
||||
#specialCommentInput
|
||||
(keydown.delete)="triggerResize()"
|
||||
(keydown.backspace)="triggerResize()"
|
||||
type="text"
|
||||
name="comment"
|
||||
placeholder="Eine Anmerkung hinzufügen"
|
||||
[formControl]="specialCommentControl"
|
||||
[class.inactive]="!specialCommentControl.dirty"
|
||||
></textarea>
|
||||
<div class="comment-actions">
|
||||
@if (!!specialCommentControl.value?.length) {
|
||||
<button
|
||||
type="reset"
|
||||
class="clear"
|
||||
(click)="specialCommentControl.setValue(''); saveSpecialComment(); triggerResize()"
|
||||
>
|
||||
<shared-icon icon="close" [size]="24"></shared-icon>
|
||||
</button>
|
||||
}
|
||||
@if (specialCommentControl?.enabled && specialCommentControl.dirty) {
|
||||
<button
|
||||
class="cta-save"
|
||||
type="submit"
|
||||
(click)="saveSpecialComment()"
|
||||
matomoClickCategory="pickup-shelf-details-item"
|
||||
matomoClickAction="save"
|
||||
matomoClickName="special-comment"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
<div class="detail">
|
||||
<div class="label">
|
||||
@if (
|
||||
orderItemFeature(orderItem) === 'Versand' ||
|
||||
orderItemFeature(orderItem) === 'B2B-Versand' ||
|
||||
orderItemFeature(orderItem) === 'DIG-Versand'
|
||||
) {
|
||||
{{
|
||||
orderItem?.estimatedDelivery
|
||||
? 'Lieferung zwischen'
|
||||
: 'Lieferung ab'
|
||||
}}
|
||||
}
|
||||
@if (
|
||||
orderItemFeature(orderItem) === 'Abholung' ||
|
||||
orderItemFeature(orderItem) === 'Rücklage'
|
||||
) {
|
||||
Abholung ab
|
||||
}
|
||||
</div>
|
||||
@if (
|
||||
!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate
|
||||
) {
|
||||
<div class="value bg-[#D8DFE5] rounded w-max px-2">
|
||||
@if (!!orderItem?.estimatedDelivery) {
|
||||
{{ orderItem?.estimatedDelivery?.start | date: 'dd.MM.yy' }} und
|
||||
{{ orderItem?.estimatedDelivery?.stop | date: 'dd.MM.yy' }}
|
||||
} @else {
|
||||
@if (!!orderItem?.estimatedShippingDate) {
|
||||
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<hr class="border-[#EDEFF0] border-t-2 my-4" />
|
||||
@if (!!orderItem?.compartmentCode) {
|
||||
<div class="detail">
|
||||
<div class="label">Abholfachnr.</div>
|
||||
<div class="value">{{ orderItem?.compartmentCode }}</div>
|
||||
</div>
|
||||
}
|
||||
<div class="detail">
|
||||
<div class="label">Vormerker</div>
|
||||
<div class="value">{{ orderItem.isPrebooked ? 'Ja' : 'Nein' }}</div>
|
||||
</div>
|
||||
<hr class="border-[#EDEFF0] border-t-2 my-4" />
|
||||
@if (!!orderItem.paymentProcessing) {
|
||||
<div class="detail">
|
||||
<div class="label">Zahlungsweg</div>
|
||||
<div class="value">{{ orderItem.paymentProcessing || '-' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (!!orderItem.paymentType) {
|
||||
<div class="detail">
|
||||
<div class="label">Zahlungsart</div>
|
||||
<div class="value">{{ orderItem.paymentType | paymentType }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (receiptCount$ | async; as count) {
|
||||
<h4 class="receipt-header">
|
||||
{{ count > 1 ? 'Belege' : 'Beleg' }}
|
||||
</h4>
|
||||
}
|
||||
@for (receipt of receipts$ | async; track receipt) {
|
||||
@if (!!receipt?.receiptNumber) {
|
||||
<div class="detail">
|
||||
<div class="label">Belegnummer</div>
|
||||
<div class="value">{{ receipt?.receiptNumber }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (!!receipt?.printedDate) {
|
||||
<div class="detail">
|
||||
<div class="label">Erstellt am</div>
|
||||
<div class="value">
|
||||
{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!!receipt?.receiptText) {
|
||||
<div class="detail">
|
||||
<div class="label">Rechnungstext</div>
|
||||
<div class="value">{{ receipt?.receiptText || '-' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (!!receipt?.receiptType) {
|
||||
<div class="detail">
|
||||
<div class="label">Belegart</div>
|
||||
<div class="value">
|
||||
{{
|
||||
receipt?.receiptType === 1
|
||||
? 'Lieferschein'
|
||||
: receipt?.receiptType === 64
|
||||
? 'Zahlungsbeleg'
|
||||
: '-'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (
|
||||
!!orderItem.paymentProcessing ||
|
||||
!!orderItem.paymentType ||
|
||||
!!(receiptCount$ | async)
|
||||
) {
|
||||
<hr class="border-[#EDEFF0] border-t-2 my-4" />
|
||||
}
|
||||
@if (expanded) {
|
||||
<button
|
||||
(click)="expanded = !expanded"
|
||||
type="button"
|
||||
class="page-pickup-shelf-details-item__less text-[#0556B4] font-bold flex flex-row items-center justify-center"
|
||||
>
|
||||
<shared-icon
|
||||
class="mr-1"
|
||||
icon="arrow-back"
|
||||
[size]="20"
|
||||
></shared-icon>
|
||||
Weniger
|
||||
</button>
|
||||
}
|
||||
}
|
||||
<div
|
||||
class="page-pickup-shelf-details-item__comment flex flex-col items-start mt-[1.625rem]"
|
||||
>
|
||||
<div class="label mb-[0.375rem]">Anmerkung</div>
|
||||
<div class="flex flex-row w-full">
|
||||
<textarea
|
||||
matInput
|
||||
cdkTextareaAutosize
|
||||
#autosize="cdkTextareaAutosize"
|
||||
cdkAutosizeMinRows="1"
|
||||
cdkAutosizeMaxRows="5"
|
||||
maxlength="200"
|
||||
#specialCommentInput
|
||||
(keydown.delete)="triggerResize()"
|
||||
(keydown.backspace)="triggerResize()"
|
||||
type="text"
|
||||
name="comment"
|
||||
placeholder="Eine Anmerkung hinzufügen"
|
||||
[formControl]="specialCommentControl"
|
||||
[class.inactive]="!specialCommentControl.dirty"
|
||||
></textarea>
|
||||
<div class="comment-actions">
|
||||
@if (!!specialCommentControl.value?.length) {
|
||||
<button
|
||||
type="reset"
|
||||
class="clear"
|
||||
(click)="
|
||||
specialCommentControl.setValue('');
|
||||
saveSpecialComment();
|
||||
triggerResize()
|
||||
"
|
||||
>
|
||||
<shared-icon icon="close" [size]="24"></shared-icon>
|
||||
</button>
|
||||
}
|
||||
@if (
|
||||
specialCommentControl?.enabled && specialCommentControl.dirty
|
||||
) {
|
||||
<button
|
||||
class="cta-save"
|
||||
type="submit"
|
||||
(click)="saveSpecialComment()"
|
||||
matomoClickCategory="pickup-shelf-details-item"
|
||||
matomoClickAction="save"
|
||||
matomoClickName="special-comment"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { CdkTextareaAutosize, TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { AsyncPipe, CurrencyPipe, DatePipe, DecimalPipe } from '@angular/common';
|
||||
import {
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
DatePipe,
|
||||
DecimalPipe,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
@@ -11,12 +16,23 @@ import {
|
||||
inject,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
|
||||
import { NavigateOnClickDirective, ProductImageModule } from '@cdn/product-image';
|
||||
import { DBHOrderItemListItemDTO, OrderDTO, ReceiptDTO } from '@generated/swagger/oms-api';
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
UntypedFormControl,
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
NavigateOnClickDirective,
|
||||
ProductImageModule,
|
||||
} from '@cdn/product-image';
|
||||
import {
|
||||
DBHOrderItemListItemDTO,
|
||||
OrderDTO,
|
||||
ReceiptDTO,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { getOrderItemRewardFeature } from '@isa/oms/data-access';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
|
||||
import { LabelComponent } from '@isa/ui/label';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { PickupShelfPaymentTypePipe } from '../pipes/payment-type.pipe';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
@@ -60,8 +76,8 @@ export interface PickUpShelfDetailsItemComponentState {
|
||||
NotificationTypePipe,
|
||||
NavigateOnClickDirective,
|
||||
MatomoModule,
|
||||
LabelComponent
|
||||
],
|
||||
LabelComponent,
|
||||
],
|
||||
})
|
||||
export class PickUpShelfDetailsItemComponent
|
||||
extends ComponentStore<PickUpShelfDetailsItemComponentState>
|
||||
@@ -88,7 +104,11 @@ export class PickUpShelfDetailsItemComponent
|
||||
this._store.selectOrderItem(this.orderItem, false);
|
||||
}
|
||||
|
||||
this.patchState({ orderItem, quantity: orderItem?.quantity, more: false });
|
||||
this.patchState({
|
||||
orderItem,
|
||||
quantity: orderItem?.quantity,
|
||||
more: false,
|
||||
});
|
||||
this.specialCommentControl.reset(orderItem?.specialComment);
|
||||
// Add New OrderItem to selected list if selected was set to true by its input
|
||||
if (this.get((s) => s.selected)) {
|
||||
@@ -110,16 +130,24 @@ export class PickUpShelfDetailsItemComponent
|
||||
readonly orderItem$ = this.select((s) => s.orderItem);
|
||||
|
||||
emailNotificationDates$ = this.orderItem$.pipe(
|
||||
switchMap((orderItem) => this._store.getEmailNotificationDate$(orderItem?.orderItemSubsetId)),
|
||||
switchMap((orderItem) =>
|
||||
this._store.getEmailNotificationDate$(orderItem?.orderItemSubsetId),
|
||||
),
|
||||
);
|
||||
|
||||
hasEmailNotification$ = this.emailNotificationDates$.pipe(map((dates) => dates?.length > 0));
|
||||
hasEmailNotification$ = this.emailNotificationDates$.pipe(
|
||||
map((dates) => dates?.length > 0),
|
||||
);
|
||||
|
||||
smsNotificationDates$ = this.orderItem$.pipe(
|
||||
switchMap((orderItem) => this._store.getSmsNotificationDate$(orderItem?.orderItemSubsetId)),
|
||||
switchMap((orderItem) =>
|
||||
this._store.getSmsNotificationDate$(orderItem?.orderItemSubsetId),
|
||||
),
|
||||
);
|
||||
|
||||
hasSmsNotification$ = this.smsNotificationDates$.pipe(map((dates) => dates?.length > 0));
|
||||
hasSmsNotification$ = this.smsNotificationDates$.pipe(
|
||||
map((dates) => dates?.length > 0),
|
||||
);
|
||||
|
||||
/**
|
||||
* Observable that indicates whether the order item has reward points (Lesepunkte).
|
||||
@@ -137,8 +165,15 @@ export class PickUpShelfDetailsItemComponent
|
||||
map((orderItem) => getOrderItemRewardFeature(orderItem)),
|
||||
);
|
||||
|
||||
canChangeQuantity$ = combineLatest([this.orderItem$, this._store.fetchPartial$]).pipe(
|
||||
map(([item, partialPickup]) => ([16, 8192].includes(item?.processingStatus) || partialPickup) && item.quantity > 1),
|
||||
canChangeQuantity$ = combineLatest([
|
||||
this.orderItem$,
|
||||
this._store.fetchPartial$,
|
||||
]).pipe(
|
||||
map(
|
||||
([item, partialPickup]) =>
|
||||
([16, 8192].includes(item?.processingStatus) || partialPickup) &&
|
||||
item.quantity > 1,
|
||||
),
|
||||
);
|
||||
|
||||
get quantity() {
|
||||
@@ -147,7 +182,10 @@ export class PickUpShelfDetailsItemComponent
|
||||
set quantity(quantity: number) {
|
||||
if (this.quantity !== quantity) {
|
||||
this.patchState({ quantity });
|
||||
this._store.setSelectedOrderItemQuantity({ orderItemSubsetId: this.orderItem.orderItemSubsetId, quantity });
|
||||
this._store.setSelectedOrderItemQuantity({
|
||||
orderItemSubsetId: this.orderItem.orderItemSubsetId,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +193,9 @@ export class PickUpShelfDetailsItemComponent
|
||||
|
||||
@Input()
|
||||
get selected() {
|
||||
return this._store.selectedOrderItemIds.includes(this.orderItem?.orderItemSubsetId);
|
||||
return this._store.selectedOrderItemIds.includes(
|
||||
this.orderItem?.orderItemSubsetId,
|
||||
);
|
||||
}
|
||||
set selected(selected: boolean) {
|
||||
if (this.selected !== selected) {
|
||||
@@ -164,23 +204,40 @@ export class PickUpShelfDetailsItemComponent
|
||||
}
|
||||
}
|
||||
|
||||
readonly selected$ = combineLatest([this.orderItem$, this._store.selectedOrderItemIds$]).pipe(
|
||||
map(([orderItem, selectedItems]) => selectedItems.includes(orderItem?.orderItemSubsetId)),
|
||||
readonly selected$ = combineLatest([
|
||||
this.orderItem$,
|
||||
this._store.selectedOrderItemIds$,
|
||||
]).pipe(
|
||||
map(([orderItem, selectedItems]) =>
|
||||
selectedItems.includes(orderItem?.orderItemSubsetId),
|
||||
),
|
||||
);
|
||||
|
||||
@Output()
|
||||
selectedChange = new EventEmitter<boolean>();
|
||||
|
||||
get isItemSelectable() {
|
||||
return this._store.orderItems?.some((item) => !!item?.actions && item?.actions?.length > 0);
|
||||
return this._store.orderItems?.some(
|
||||
(item) => !!item?.actions && item?.actions?.length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
get selectable() {
|
||||
return this.isItemSelectable && this._store.orderItems.length > 1 && this._store.fetchPartial;
|
||||
return (
|
||||
this.isItemSelectable &&
|
||||
this._store.orderItems.length > 1 &&
|
||||
this._store.fetchPartial
|
||||
);
|
||||
}
|
||||
|
||||
readonly selectable$ = combineLatest([this._store.orderItems$, this._store.fetchPartial$]).pipe(
|
||||
map(([orderItems, fetchPartial]) => orderItems.length > 1 && this.isItemSelectable && fetchPartial),
|
||||
readonly selectable$ = combineLatest([
|
||||
this._store.orderItems$,
|
||||
this._store.fetchPartial$,
|
||||
]).pipe(
|
||||
map(
|
||||
([orderItems, fetchPartial]) =>
|
||||
orderItems.length > 1 && this.isItemSelectable && fetchPartial,
|
||||
),
|
||||
);
|
||||
|
||||
get receipts() {
|
||||
@@ -193,7 +250,9 @@ export class PickUpShelfDetailsItemComponent
|
||||
|
||||
readonly receipts$ = this._store.receipts$;
|
||||
|
||||
readonly receiptCount$ = this.receipts$.pipe(map((receipts) => receipts?.length));
|
||||
readonly receiptCount$ = this.receipts$.pipe(
|
||||
map((receipts) => receipts?.length),
|
||||
);
|
||||
|
||||
specialCommentControl = new UntypedFormControl();
|
||||
|
||||
@@ -203,10 +262,6 @@ export class PickUpShelfDetailsItemComponent
|
||||
|
||||
expanded = false;
|
||||
|
||||
// Expose to template
|
||||
Labeltype = Labeltype;
|
||||
LabelPriority = LabelPriority;
|
||||
|
||||
constructor(private _cdr: ChangeDetectorRef) {
|
||||
super({
|
||||
more: false,
|
||||
@@ -239,8 +294,9 @@ export class PickUpShelfDetailsItemComponent
|
||||
|
||||
orderItemFeature(orderItemListItem: DBHOrderItemListItemDTO) {
|
||||
const orderItems = this.order?.items;
|
||||
return orderItems?.find((orderItem) => orderItem.data.id === orderItemListItem.orderItemId)?.data?.features
|
||||
?.orderType;
|
||||
return orderItems?.find(
|
||||
(orderItem) => orderItem.data.id === orderItemListItem.orderItemId,
|
||||
)?.data?.features?.orderType;
|
||||
}
|
||||
|
||||
triggerResize() {
|
||||
|
||||
@@ -4,12 +4,16 @@
|
||||
[routerLinkActive]="!isTablet && !primaryOutletActive ? 'active' : ''"
|
||||
queryParamsHandling="preserve"
|
||||
(click)="onDetailsClick()"
|
||||
>
|
||||
>
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-grid-container"
|
||||
[class.page-pickup-shelf-list-item__item-grid-container-main]="primaryOutletActive"
|
||||
[class.page-pickup-shelf-list-item__item-grid-container-secondary]="primaryOutletActive && isItemSelectable === undefined"
|
||||
>
|
||||
[class.page-pickup-shelf-list-item__item-grid-container-main]="
|
||||
primaryOutletActive
|
||||
"
|
||||
[class.page-pickup-shelf-list-item__item-grid-container-secondary]="
|
||||
primaryOutletActive && isItemSelectable === undefined
|
||||
"
|
||||
>
|
||||
<div class="page-pickup-shelf-list-item__item-thumbnail text-center">
|
||||
@if (item?.product?.ean | productImage; as productImage) {
|
||||
<img
|
||||
@@ -18,88 +22,126 @@
|
||||
[productImageNavigation]="item?.product?.ean"
|
||||
[src]="productImage"
|
||||
[alt]="item?.product?.name"
|
||||
/>
|
||||
}
|
||||
@if (hasRewardPoints) {
|
||||
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.High">
|
||||
Prämie
|
||||
</ui-label>
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-title-contributors flex flex-col"
|
||||
[class.mr-32]="showCompartmentCode && item.features?.paid && (isTablet || isDesktopSmall || primaryOutletActive)"
|
||||
>
|
||||
[class.mr-32]="
|
||||
showCompartmentCode &&
|
||||
item.features?.paid &&
|
||||
(isTablet || isDesktopSmall || primaryOutletActive)
|
||||
"
|
||||
>
|
||||
@if (hasRewardPoints) {
|
||||
<ui-label class="w-10 mb-2">Prämie</ui-label>
|
||||
}
|
||||
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-contributors text-p2 font-normal text-ellipsis overflow-hidden max-w-[24rem] whitespace-nowrap mb-[0.375rem]"
|
||||
>
|
||||
@for (contributor of contributors; track contributor; let last = $last) {
|
||||
>
|
||||
@for (
|
||||
contributor of contributors;
|
||||
track contributor;
|
||||
let last = $last
|
||||
) {
|
||||
{{ contributor }}{{ last ? '' : ';' }}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-title font-bold text-p1 desktop-small:text-p2"
|
||||
[class.page-pickup-shelf-list-item__item-title-bigger-text-size]="!primaryOutletActive"
|
||||
>
|
||||
[class.page-pickup-shelf-list-item__item-title-bigger-text-size]="
|
||||
!primaryOutletActive
|
||||
"
|
||||
>
|
||||
{{ item?.product?.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-pickup-shelf-list-item__item-ean-quantity-changed flex flex-col">
|
||||
<div class="page-pickup-shelf-list-item__item-ean text-p2 flex flex-row mb-[0.375rem]" [attr.data-ean]="item?.product?.ean">
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-ean-quantity-changed flex flex-col"
|
||||
>
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-ean text-p2 flex flex-row mb-[0.375rem]"
|
||||
[attr.data-ean]="item?.product?.ean"
|
||||
>
|
||||
<div class="min-w-[7.5rem]">EAN</div>
|
||||
<div class="font-bold">{{ item?.product?.ean }}</div>
|
||||
</div>
|
||||
|
||||
<div class="page-pickup-shelf-list-item__item-quantity flex flex-row text-p2 mb-[0.375rem]" [attr.data-menge]="item.quantity">
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-quantity flex flex-row text-p2 mb-[0.375rem]"
|
||||
[attr.data-menge]="item.quantity"
|
||||
>
|
||||
<div class="min-w-[7.5rem]">Menge</div>
|
||||
<div class="font-bold">{{ item.quantity }} x</div>
|
||||
</div>
|
||||
|
||||
<div class="page-pickup-shelf-list-item__item-changed text-p2" [attr.data-geaendert]="item?.processingStatusDate">
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-changed text-p2"
|
||||
[attr.data-geaendert]="item?.processingStatusDate"
|
||||
>
|
||||
@if (showChangeDate) {
|
||||
<div class="flex flex-row">
|
||||
<div class="min-w-[7.5rem]">Geändert</div>
|
||||
<div class="font-bold">{{ item?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
|
||||
<div class="font-bold">
|
||||
{{ item?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="flex flex-row" [attr.data-bestelldatum]="item?.orderDate">
|
||||
<div class="min-w-[7.5rem]">Bestelldatum</div>
|
||||
<div class="font-bold">{{ item?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
|
||||
<div class="font-bold">
|
||||
{{ item?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-pickup-shelf-list-item__item-order-number-processing-status-paid flex flex-col">
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-order-number-processing-status-paid flex flex-col"
|
||||
>
|
||||
@if (showCompartmentCode) {
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-order-number text-h3 mb-[0.375rem] self-end font-bold break-all text-right"
|
||||
[attr.data-compartment-code]="item?.compartmentCode"
|
||||
[attr.data-compartment-info]="item?.compartmentInfo"
|
||||
>
|
||||
{{ item?.compartmentCode }}{{ item?.compartmentInfo && '_' + item?.compartmentInfo }}
|
||||
>
|
||||
{{ item?.compartmentCode
|
||||
}}{{ item?.compartmentInfo && '_' + item?.compartmentInfo }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="page-pickup-shelf-list-item__item-processing-paid-status flex flex-col font-bold self-end text-p2 mb-[0.375rem]">
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-processing-paid-status flex flex-col font-bold self-end text-p2 mb-[0.375rem]"
|
||||
>
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-processing-status flex flex-row mb-[0.375rem] rounded p-3 py-[0.125rem] text-white"
|
||||
[style]="processingStatusColor"
|
||||
[attr.data-processing-status]="item.processingStatus"
|
||||
>
|
||||
>
|
||||
{{ item.processingStatus | processingStatus }}
|
||||
</div>
|
||||
|
||||
<div class="page-pickup-shelf-list-item__item-paid self-end flex flex-row">
|
||||
@if (item.features?.paid && (isTablet || isDesktopSmall || primaryOutletActive)) {
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-paid self-end flex flex-row"
|
||||
>
|
||||
@if (
|
||||
item.features?.paid &&
|
||||
(isTablet || isDesktopSmall || primaryOutletActive)
|
||||
) {
|
||||
<div
|
||||
class="font-bold flex flex-row items-center justify-center text-p2 text-[#26830C]"
|
||||
[attr.data-paid]="item.features?.paid"
|
||||
>
|
||||
<shared-icon class="flex items-center justify-center mr-[0.375rem]" [size]="24" icon="credit-card"></shared-icon>
|
||||
>
|
||||
<shared-icon
|
||||
class="flex items-center justify-center mr-[0.375rem]"
|
||||
[size]="24"
|
||||
icon="credit-card"
|
||||
></shared-icon>
|
||||
{{ item.features?.paid }}
|
||||
</div>
|
||||
}
|
||||
@@ -110,28 +152,35 @@
|
||||
@if (isItemSelectable) {
|
||||
<div
|
||||
class="page-pickup-shelf-list-item__item-select-bullet justify-self-end self-center mb-2"
|
||||
[class.page-pickup-shelf-list-item__item-select-bullet-primary]="primaryOutletActive"
|
||||
>
|
||||
[class.page-pickup-shelf-list-item__item-select-bullet-primary]="
|
||||
primaryOutletActive
|
||||
"
|
||||
>
|
||||
<input
|
||||
(click)="$event.stopPropagation()"
|
||||
[ngModel]="selectedItem"
|
||||
(ngModelChange)="
|
||||
setSelected();
|
||||
tracker.trackEvent({ category: 'pickup-shelf-list-item', action: 'select', name: item?.product?.name, value: $event ? 1 : 0 })
|
||||
"
|
||||
(ngModelChange)="
|
||||
setSelected();
|
||||
tracker.trackEvent({
|
||||
category: 'pickup-shelf-list-item',
|
||||
action: 'select',
|
||||
name: item?.product?.name,
|
||||
value: $event ? 1 : 0,
|
||||
})
|
||||
"
|
||||
class="isa-select-bullet"
|
||||
type="checkbox"
|
||||
matomoTracker
|
||||
#tracker="matomo"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div
|
||||
[attr.data-special-comment]="item?.specialComment"
|
||||
class="page-pickup-shelf-list-item__item-special-comment break-words font-bold text-p2 mt-[0.375rem] text-[#996900]"
|
||||
>
|
||||
{{ item?.specialComment }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div
|
||||
[attr.data-special-comment]="item?.specialComment"
|
||||
class="page-pickup-shelf-list-item__item-special-comment break-words font-bold text-p2 mt-[0.375rem] text-[#996900]"
|
||||
>
|
||||
{{ item?.specialComment }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, ElementRef, Input, inject } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { NavigateOnClickDirective, ProductImageModule } from '@cdn/product-image';
|
||||
import {
|
||||
NavigateOnClickDirective,
|
||||
ProductImageModule,
|
||||
} from '@cdn/product-image';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { DBHOrderItemListItemDTO } from '@generated/swagger/oms-api';
|
||||
import { getOrderItemRewardFeature } from '@isa/oms/data-access';
|
||||
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
|
||||
import { LabelComponent } from '@isa/ui/label';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { PickupShelfProcessingStatusPipe } from '../pipes/processing-status.pipe';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@@ -32,8 +41,8 @@ import { MatomoModule } from 'ngx-matomo-client';
|
||||
PickupShelfProcessingStatusPipe,
|
||||
NavigateOnClickDirective,
|
||||
MatomoModule,
|
||||
LabelComponent
|
||||
],
|
||||
LabelComponent,
|
||||
],
|
||||
providers: [PickupShelfProcessingStatusPipe],
|
||||
})
|
||||
export class PickUpShelfListItemComponent {
|
||||
@@ -45,6 +54,7 @@ export class PickUpShelfListItemComponent {
|
||||
|
||||
@Input() primaryOutletActive = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@Input() itemDetailsLink: any[] = [];
|
||||
|
||||
@Input() isItemSelectable?: boolean = undefined;
|
||||
@@ -64,7 +74,9 @@ export class PickUpShelfListItemComponent {
|
||||
}
|
||||
|
||||
get contributors() {
|
||||
return this.item?.product?.contributors?.split(';').map((val) => val.trim());
|
||||
return this.item?.product?.contributors
|
||||
?.split(';')
|
||||
.map((val) => val.trim());
|
||||
}
|
||||
|
||||
get showChangeDate() {
|
||||
@@ -73,11 +85,19 @@ export class PickUpShelfListItemComponent {
|
||||
|
||||
// Zeige nur CompartmentCode an wenn verfügbar
|
||||
get showCompartmentCode() {
|
||||
return !!this.item?.compartmentCode && (this.isTablet || this.isDesktopSmall || this.primaryOutletActive);
|
||||
return (
|
||||
!!this.item?.compartmentCode &&
|
||||
(this.isTablet || this.isDesktopSmall || this.primaryOutletActive)
|
||||
);
|
||||
}
|
||||
|
||||
get processingStatusColor() {
|
||||
return { 'background-color': this._processingStatusPipe.transform(this.item?.processingStatus, true) };
|
||||
return {
|
||||
'background-color': this._processingStatusPipe.transform(
|
||||
this.item?.processingStatus,
|
||||
true,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,14 +110,12 @@ export class PickUpShelfListItemComponent {
|
||||
|
||||
selected$ = this.store.selectedListItems$.pipe(
|
||||
map((selectedListItems) =>
|
||||
selectedListItems?.find((item) => item?.orderItemSubsetId === this.item?.orderItemSubsetId),
|
||||
selectedListItems?.find(
|
||||
(item) => item?.orderItemSubsetId === this.item?.orderItemSubsetId,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Expose to template
|
||||
Labeltype = Labeltype;
|
||||
LabelPriority = LabelPriority;
|
||||
|
||||
constructor(
|
||||
private _elRef: ElementRef,
|
||||
private _environment: EnvironmentService,
|
||||
@@ -125,7 +143,9 @@ export class PickUpShelfListItemComponent {
|
||||
store: 'PickupShelfDetailsStore',
|
||||
})) ?? [];
|
||||
|
||||
return items.some((i) => i.orderItemSubsetId === this.item.orderItemSubsetId);
|
||||
return items.some(
|
||||
(i) => i.orderItemSubsetId === this.item.orderItemSubsetId,
|
||||
);
|
||||
}
|
||||
|
||||
addOrderItemIntoCache() {
|
||||
@@ -144,7 +164,10 @@ export class PickUpShelfListItemComponent {
|
||||
}
|
||||
|
||||
scrollIntoView() {
|
||||
this._elRef.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
this._elRef.nativeElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
}
|
||||
|
||||
setSelected() {
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { PageTaskCalendarComponent } from './page-task-calendar.component';
|
||||
import { CalendarComponent } from './pages/calendar/calendar.component';
|
||||
import { CalendarModule } from './pages/calendar/calendar.module';
|
||||
import { TaskSearchComponent } from './pages/task-search';
|
||||
import { TasksComponent } from './pages/tasks/tasks.component';
|
||||
import { TasksModule } from './pages/tasks/tasks.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PageTaskCalendarComponent,
|
||||
children: [
|
||||
{ path: 'calendar', component: CalendarComponent },
|
||||
{ path: 'tasks', component: TasksComponent },
|
||||
{ path: 'search', component: TaskSearchComponent },
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'tasks' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes), CalendarModule, TasksModule],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class PageTaskCalendarRoutingModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { PageTaskCalendarComponent } from './page-task-calendar.component';
|
||||
import { CalendarComponent } from './pages/calendar/calendar.component';
|
||||
import { CalendarModule } from './pages/calendar/calendar.module';
|
||||
import { TaskSearchComponent } from './pages/task-search';
|
||||
import { TasksComponent } from './pages/tasks/tasks.component';
|
||||
import { TasksModule } from './pages/tasks/tasks.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: PageTaskCalendarComponent,
|
||||
title: 'Tätigkeitskalender',
|
||||
children: [
|
||||
{ path: 'calendar', component: CalendarComponent },
|
||||
{
|
||||
path: 'tasks',
|
||||
title: 'Tätigkeitskalender - Aufgaben',
|
||||
component: TasksComponent,
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
title: 'Tätigkeitskalender - Suche',
|
||||
component: TaskSearchComponent,
|
||||
},
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'tasks' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes), CalendarModule, TasksModule],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class PageTaskCalendarRoutingModule {}
|
||||
|
||||
@@ -20,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/notice/src/notice.scss";
|
||||
@import "../../../libs/ui/switch/src/switch.scss";
|
||||
|
||||
.input-control {
|
||||
|
||||
@@ -1,51 +1,106 @@
|
||||
import {
|
||||
DropdownAppearance,
|
||||
DropdownButtonComponent,
|
||||
DropdownOptionComponent,
|
||||
} from '@isa/ui/input-controls';
|
||||
import { type Meta, type StoryObj, argsToTemplate, moduleMetadata } from '@storybook/angular';
|
||||
|
||||
type DropdownInputProps = {
|
||||
value: string;
|
||||
label: string;
|
||||
appearance: DropdownAppearance;
|
||||
};
|
||||
|
||||
const meta: Meta<DropdownInputProps> = {
|
||||
title: 'ui/input-controls/Dropdown',
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [DropdownButtonComponent, DropdownOptionComponent],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
value: { control: 'text' },
|
||||
label: { control: 'text' },
|
||||
appearance: {
|
||||
control: 'select',
|
||||
options: Object.values(DropdownAppearance),
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<ui-dropdown ${argsToTemplate(args)}>
|
||||
<ui-dropdown-option value="">Select an option</ui-dropdown-option>
|
||||
<ui-dropdown-option value="1">Option 1</ui-dropdown-option>
|
||||
<ui-dropdown-option value="2">Option 2</ui-dropdown-option>
|
||||
<ui-dropdown-option value="3">Option 3</ui-dropdown-option>
|
||||
</ui-dropdown>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<DropdownInputProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: undefined,
|
||||
label: 'Label',
|
||||
},
|
||||
};
|
||||
import {
|
||||
DropdownAppearance,
|
||||
DropdownButtonComponent,
|
||||
DropdownFilterComponent,
|
||||
DropdownOptionComponent,
|
||||
} from '@isa/ui/input-controls';
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
argsToTemplate,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
|
||||
type DropdownInputProps = {
|
||||
value: string;
|
||||
label: string;
|
||||
appearance: DropdownAppearance;
|
||||
};
|
||||
|
||||
const meta: Meta<DropdownInputProps> = {
|
||||
title: 'ui/input-controls/Dropdown',
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [
|
||||
DropdownButtonComponent,
|
||||
DropdownFilterComponent,
|
||||
DropdownOptionComponent,
|
||||
],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
value: { control: 'text' },
|
||||
label: { control: 'text' },
|
||||
appearance: {
|
||||
control: 'select',
|
||||
options: Object.values(DropdownAppearance),
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<ui-dropdown ${argsToTemplate(args)}>
|
||||
<ui-dropdown-option value="">Select an option</ui-dropdown-option>
|
||||
<ui-dropdown-option value="1">Option 1</ui-dropdown-option>
|
||||
<ui-dropdown-option value="2">Option 2</ui-dropdown-option>
|
||||
<ui-dropdown-option value="3">Option 3</ui-dropdown-option>
|
||||
</ui-dropdown>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<DropdownInputProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: undefined,
|
||||
label: 'Label',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithFilter: Story = {
|
||||
args: {
|
||||
value: undefined,
|
||||
label: 'Select a country',
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<ui-dropdown ${argsToTemplate(args)}>
|
||||
<ui-dropdown-filter placeholder="Search countries..."></ui-dropdown-filter>
|
||||
<ui-dropdown-option value="">Select a country</ui-dropdown-option>
|
||||
<ui-dropdown-option value="de">Germany</ui-dropdown-option>
|
||||
<ui-dropdown-option value="at">Austria</ui-dropdown-option>
|
||||
<ui-dropdown-option value="ch">Switzerland</ui-dropdown-option>
|
||||
<ui-dropdown-option value="fr">France</ui-dropdown-option>
|
||||
<ui-dropdown-option value="it">Italy</ui-dropdown-option>
|
||||
<ui-dropdown-option value="es">Spain</ui-dropdown-option>
|
||||
<ui-dropdown-option value="nl">Netherlands</ui-dropdown-option>
|
||||
<ui-dropdown-option value="be">Belgium</ui-dropdown-option>
|
||||
<ui-dropdown-option value="pl">Poland</ui-dropdown-option>
|
||||
<ui-dropdown-option value="cz">Czech Republic</ui-dropdown-option>
|
||||
</ui-dropdown>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const GreyAppearance: Story = {
|
||||
args: {
|
||||
value: undefined,
|
||||
label: 'Filter',
|
||||
appearance: DropdownAppearance.Grey,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
render: () => ({
|
||||
template: `
|
||||
<ui-dropdown label="Disabled dropdown" [disabled]="true">
|
||||
<ui-dropdown-option value="1">Option 1</ui-dropdown-option>
|
||||
<ui-dropdown-option value="2">Option 2</ui-dropdown-option>
|
||||
</ui-dropdown>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
|
||||
import { Labeltype, LabelPriority, LabelComponent } from '@isa/ui/label';
|
||||
import { LabelComponent } from '@isa/ui/label';
|
||||
|
||||
type UiLabelInputs = {
|
||||
type: Labeltype;
|
||||
priority: LabelPriority;
|
||||
type LabelInputs = {
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
const meta: Meta<UiLabelInputs> = {
|
||||
const meta: Meta<LabelInputs> = {
|
||||
component: LabelComponent,
|
||||
title: 'ui/label/Label',
|
||||
argTypes: {
|
||||
type: {
|
||||
control: { type: 'select' },
|
||||
options: Object.values(Labeltype),
|
||||
description: 'Determines the label type',
|
||||
},
|
||||
priority: {
|
||||
control: { type: 'select' },
|
||||
options: Object.values(LabelPriority),
|
||||
description: 'Determines the label priority',
|
||||
active: {
|
||||
control: { type: 'boolean' },
|
||||
description: 'Determines if the label is active (hover/pressed state)',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
type: 'tag',
|
||||
priority: 'high',
|
||||
active: false,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<ui-label ${argsToTemplate(args)}>Prio 1</ui-label>`,
|
||||
template: `<ui-label ${argsToTemplate(args)}>Prämie</ui-label>`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
@@ -37,3 +29,21 @@ type Story = StoryObj<LabelComponent>;
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Active: Story = {
|
||||
args: {
|
||||
active: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const RewardExample: Story = {
|
||||
args: {},
|
||||
render: () => ({
|
||||
template: `
|
||||
<div style="display: flex; gap: 1rem; align-items: center;">
|
||||
<ui-label>Prämie</ui-label>
|
||||
<ui-label [active]="true">Active</ui-label>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
59
apps/isa-app/stories/ui/label/ui-prio-label.stories.ts
Normal file
59
apps/isa-app/stories/ui/label/ui-prio-label.stories.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
|
||||
import { PrioLabelComponent } from '@isa/ui/label';
|
||||
|
||||
type PrioLabelInputs = {
|
||||
priority: 1 | 2;
|
||||
};
|
||||
|
||||
const meta: Meta<PrioLabelInputs> = {
|
||||
component: PrioLabelComponent,
|
||||
title: 'ui/label/PrioLabel',
|
||||
argTypes: {
|
||||
priority: {
|
||||
control: { type: 'select' },
|
||||
options: [1, 2],
|
||||
description: 'The priority level (1 = high, 2 = low)',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
priority: 1,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<ui-prio-label ${argsToTemplate(args)}>Pflicht</ui-prio-label>`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<PrioLabelComponent>;
|
||||
|
||||
export const Priority1: Story = {
|
||||
args: {
|
||||
priority: 1,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<ui-prio-label [priority]="priority">Pflicht</ui-prio-label>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Priority2: Story = {
|
||||
args: {
|
||||
priority: 2,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<ui-prio-label [priority]="priority">Prio 2</ui-prio-label>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const AllPriorities: Story = {
|
||||
render: () => ({
|
||||
template: `
|
||||
<div style="display: flex; gap: 1rem; align-items: center;">
|
||||
<ui-prio-label [priority]="1">Pflicht</ui-prio-label>
|
||||
<ui-prio-label [priority]="2">Prio 2</ui-prio-label>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
70
apps/isa-app/stories/ui/notice/ui-notice.stories.ts
Normal file
70
apps/isa-app/stories/ui/notice/ui-notice.stories.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
|
||||
import { NoticeComponent, NoticePriority } from '@isa/ui/notice';
|
||||
|
||||
type NoticeInputs = {
|
||||
priority: NoticePriority;
|
||||
};
|
||||
|
||||
const meta: Meta<NoticeInputs> = {
|
||||
component: NoticeComponent,
|
||||
title: 'ui/notice/Notice',
|
||||
argTypes: {
|
||||
priority: {
|
||||
control: { type: 'select' },
|
||||
options: Object.values(NoticePriority),
|
||||
description: 'The priority level (high, medium, low)',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
priority: NoticePriority.High,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<ui-notice ${argsToTemplate(args)}>Important message</ui-notice>`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<NoticeComponent>;
|
||||
|
||||
export const High: Story = {
|
||||
args: {
|
||||
priority: NoticePriority.High,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<ui-notice [priority]="priority">Action Required</ui-notice>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Medium: Story = {
|
||||
args: {
|
||||
priority: NoticePriority.Medium,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<ui-notice [priority]="priority">Secondary Information</ui-notice>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const Low: Story = {
|
||||
args: {
|
||||
priority: NoticePriority.Low,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<ui-notice [priority]="priority">Info Message</ui-notice>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const AllPriorities: Story = {
|
||||
render: () => ({
|
||||
template: `
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<ui-notice priority="high">High Priority</ui-notice>
|
||||
<ui-notice priority="medium">Medium Priority</ui-notice>
|
||||
<ui-notice priority="low">Low Priority</ui-notice>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
# Library Reference Guide
|
||||
|
||||
> **Last Updated:** 2025-11-25
|
||||
> **Last Updated:** 2025-11-28
|
||||
> **Angular Version:** 20.3.6
|
||||
> **Nx Version:** 21.3.2
|
||||
> **Total Libraries:** 72
|
||||
> **Total Libraries:** 74
|
||||
|
||||
All 72 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
|
||||
All 74 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.
|
||||
|
||||
@@ -29,7 +29,7 @@ A comprehensive product catalogue search service for Angular applications, provi
|
||||
|
||||
---
|
||||
|
||||
## Checkout Domain (6 libraries)
|
||||
## Checkout Domain (7 libraries)
|
||||
|
||||
### `@isa/checkout/data-access`
|
||||
A comprehensive checkout and shopping cart management library for Angular applications supporting multiple order types, reward redemption, and complex multi-step checkout workflows across retail and e-commerce operations.
|
||||
@@ -37,7 +37,7 @@ A comprehensive checkout and shopping cart management library for Angular applic
|
||||
**Location:** `libs/checkout/data-access/`
|
||||
|
||||
### `@isa/checkout/feature/reward-order-confirmation`
|
||||
## Overview
|
||||
A feature library providing a comprehensive order confirmation page for reward orders with support for printing, address display, and loyalty reward collection.
|
||||
|
||||
**Location:** `libs/checkout/feature/reward-order-confirmation/`
|
||||
|
||||
@@ -46,6 +46,11 @@ A comprehensive reward shopping cart feature for Angular applications supporting
|
||||
|
||||
**Location:** `libs/checkout/feature/reward-shopping-cart/`
|
||||
|
||||
### `@isa/checkout/feature/select-branch-dropdown`
|
||||
Branch selection dropdown components for the checkout domain, enabling users to select a branch for their checkout session.
|
||||
|
||||
**Location:** `libs/checkout/feature/select-branch-dropdown/`
|
||||
|
||||
### `@isa/checkout/shared/product-info`
|
||||
A comprehensive collection of presentation components for displaying product information, destination details, and stock availability in checkout and rewards workflows.
|
||||
|
||||
@@ -120,22 +125,22 @@ A sophisticated tab management system for Angular applications providing browser
|
||||
**Location:** `libs/crm/data-access/`
|
||||
|
||||
### `@isa/crm/feature/customer-bon-redemption`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A feature library providing customer loyalty receipt (Bon) redemption functionality with validation and point allocation capabilities.
|
||||
|
||||
**Location:** `libs/crm/feature/customer-bon-redemption/`
|
||||
|
||||
### `@isa/crm/feature/customer-booking`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A standalone Angular component that enables customer loyalty card point booking and reversal with configurable booking reasons.
|
||||
|
||||
**Location:** `libs/crm/feature/customer-booking/`
|
||||
|
||||
### `@isa/crm/feature/customer-card-transactions`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A standalone Angular component that displays customer loyalty card transaction history with automatic data loading and refresh capabilities.
|
||||
|
||||
**Location:** `libs/crm/feature/customer-card-transactions/`
|
||||
|
||||
### `@isa/crm/feature/customer-loyalty-cards`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
Customer loyalty cards feature module for displaying and managing customer bonus cards (Kundenkarten) with points tracking and card management actions.
|
||||
|
||||
**Location:** `libs/crm/feature/customer-loyalty-cards/`
|
||||
|
||||
@@ -144,7 +149,7 @@ This library was generated with [Nx](https://nx.dev).
|
||||
## Icons (1 library)
|
||||
|
||||
### `@isa/icons`
|
||||
## Overview
|
||||
A centralized icon library for the ISA-Frontend monorepo providing inline SVG icons as string constants.
|
||||
|
||||
**Location:** `libs/icons/`
|
||||
|
||||
@@ -256,7 +261,7 @@ Angular library for generating Code 128 barcodes using [JsBarcode](https://githu
|
||||
**Location:** `libs/shared/barcode/`
|
||||
|
||||
### `@isa/shared/delivery`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A reusable Angular component library for displaying order destination information with support for multiple delivery types (shipping, pickup, in-store, and digital downloads).
|
||||
|
||||
**Location:** `libs/shared/delivery/`
|
||||
|
||||
@@ -286,18 +291,13 @@ An accessible, feature-rich Angular quantity selector component with dropdown pr
|
||||
**Location:** `libs/shared/quantity-control/`
|
||||
|
||||
### `@isa/shared/scanner`
|
||||
## Overview
|
||||
Enterprise-grade barcode scanning library for ISA-Frontend using the Scandit SDK, providing mobile barcode scanning capabilities for iOS and Android platforms.
|
||||
|
||||
**Location:** `libs/shared/scanner/`
|
||||
|
||||
---
|
||||
|
||||
## UI Component Libraries (18 libraries)
|
||||
|
||||
### `@isa/ui/label`
|
||||
A flexible label component for displaying tags and notices with configurable priority levels across Angular applications.
|
||||
|
||||
**Location:** `libs/ui/label/`
|
||||
## UI Component Libraries (19 libraries)
|
||||
|
||||
### `@isa/ui/bullet-list`
|
||||
A lightweight bullet list component system for Angular applications supporting customizable icons and hierarchical content presentation.
|
||||
@@ -344,6 +344,11 @@ A collection of reusable row components for displaying structured data with cons
|
||||
|
||||
**Location:** `libs/ui/item-rows/`
|
||||
|
||||
### `@isa/ui/label`
|
||||
Label components for displaying tags, categories, and priority indicators.
|
||||
|
||||
**Location:** `libs/ui/label/`
|
||||
|
||||
### `@isa/ui/layout`
|
||||
This library provides utilities and directives for responsive design and viewport behavior in Angular applications.
|
||||
|
||||
@@ -354,6 +359,11 @@ A lightweight Angular component library providing accessible menu components bui
|
||||
|
||||
**Location:** `libs/ui/menu/`
|
||||
|
||||
### `@isa/ui/notice`
|
||||
A notice component for displaying prominent notifications and alerts with configurable priority levels.
|
||||
|
||||
**Location:** `libs/ui/notice/`
|
||||
|
||||
### `@isa/ui/progress-bar`
|
||||
A lightweight Angular progress bar component supporting both determinate and indeterminate modes.
|
||||
|
||||
@@ -394,17 +404,17 @@ Lightweight Angular utility library for validating EAN (European Article Number)
|
||||
**Location:** `libs/utils/ean-validation/`
|
||||
|
||||
### `@isa/utils/format-name`
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
A utility library for consistently formatting person and organisation names across the ISA-Frontend application.
|
||||
|
||||
**Location:** `libs/utils/format-name/`
|
||||
|
||||
### `@isa/utils/positive-integer-input`
|
||||
An Angular directive that ensures only positive integers can be entered into number input fields.
|
||||
An Angular directive that ensures only positive integers can be entered into number input fields through keyboard blocking, paste sanitization, and input validation.
|
||||
|
||||
**Location:** `libs/utils/positive-integer-input/`
|
||||
|
||||
### `@isa/utils/scroll-position`
|
||||
## Overview
|
||||
Utility library providing scroll position restoration and scroll-to-top functionality for Angular applications.
|
||||
|
||||
**Location:** `libs/utils/scroll-position/`
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* tslint:disable */
|
||||
import { EntityDTOBaseOfDisplayOrderItemDTOAndIOrderItem } from './entity-dtobase-of-display-order-item-dtoand-iorder-item';
|
||||
import { KeyValueDTOOfStringAndString } from './key-value-dtoof-string-and-string';
|
||||
import { LoyaltyDTO } from './loyalty-dto';
|
||||
import { DisplayOrderDTO } from './display-order-dto';
|
||||
import { PriceDTO } from './price-dto';
|
||||
@@ -9,6 +10,11 @@ import { QuantityUnitType } from './quantity-unit-type';
|
||||
import { DisplayOrderItemSubsetDTO } from './display-order-item-subset-dto';
|
||||
export interface DisplayOrderItemDTO extends EntityDTOBaseOfDisplayOrderItemDTOAndIOrderItem{
|
||||
|
||||
/**
|
||||
* Mögliche Aktionen
|
||||
*/
|
||||
actions?: Array<KeyValueDTOOfStringAndString>;
|
||||
|
||||
/**
|
||||
* Bemerkung des Auftraggebers
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
/* tslint:disable */
|
||||
import { EntityDTOBaseOfDisplayOrderItemSubsetDTOAndIOrderItemStatus } from './entity-dtobase-of-display-order-item-subset-dtoand-iorder-item-status';
|
||||
import { KeyValueDTOOfStringAndString } from './key-value-dtoof-string-and-string';
|
||||
import { DateRangeDTO } from './date-range-dto';
|
||||
import { DisplayOrderItemDTO } from './display-order-item-dto';
|
||||
import { OrderItemProcessingStatusValue } from './order-item-processing-status-value';
|
||||
export interface DisplayOrderItemSubsetDTO extends EntityDTOBaseOfDisplayOrderItemSubsetDTOAndIOrderItemStatus{
|
||||
|
||||
/**
|
||||
* Mögliche Aktionen
|
||||
*/
|
||||
actions?: Array<KeyValueDTOOfStringAndString>;
|
||||
|
||||
/**
|
||||
* Abholfachnummer
|
||||
*/
|
||||
@@ -40,6 +46,11 @@ export interface DisplayOrderItemSubsetDTO extends EntityDTOBaseOfDisplayOrderIt
|
||||
*/
|
||||
estimatedShippingDate?: string;
|
||||
|
||||
/**
|
||||
* Zusätzliche Markierungen (z.B. Abo, ...)
|
||||
*/
|
||||
features?: {[key: string]: string};
|
||||
|
||||
/**
|
||||
* Bestellposten
|
||||
*/
|
||||
|
||||
@@ -118,6 +118,11 @@ export interface ReceiptDTO extends EntityDTOBaseOfReceiptDTOAndIReceipt{
|
||||
*/
|
||||
receiptNumber?: string;
|
||||
|
||||
/**
|
||||
* Subtype of the receipt / Beleg-Unterart
|
||||
*/
|
||||
receiptSubType?: string;
|
||||
|
||||
/**
|
||||
* Belegtext
|
||||
*/
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { CheckoutMetadataService } from '../services/checkout-metadata.service';
|
||||
import { Branch } from '../schemas/branch.schema';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BranchFacade {
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
|
||||
getSelectedBranchId(tabId: number): number | undefined {
|
||||
return this.#checkoutMetadataService.getSelectedBranchId(tabId);
|
||||
getSelectedBranch(tabId: number): Branch | undefined {
|
||||
return this.#checkoutMetadataService.getSelectedBranch(tabId);
|
||||
}
|
||||
|
||||
setSelectedBranchId(tabId: number, branchId: number | undefined): void {
|
||||
this.#checkoutMetadataService.setSelectedBranchId(tabId, branchId);
|
||||
setSelectedBranch(tabId: number, branch: Branch | undefined): void {
|
||||
this.#checkoutMetadataService.setSelectedBranch(tabId, branch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ import {
|
||||
} from '@isa/checkout/data-access';
|
||||
|
||||
/**
|
||||
* Creates a unique key for an item based on EAN, destination, and orderItemType.
|
||||
* Creates a unique key for an item based on EAN, targetBranchId, and orderItemType.
|
||||
* Items are only considered identical if all three match.
|
||||
*/
|
||||
export const getItemKey = (item: ShoppingCartItem): string => {
|
||||
const ean = item.product.ean ?? 'no-ean';
|
||||
const destinationId = item.destination?.data?.id ?? 'no-destination';
|
||||
const targetBranchId =
|
||||
item.destination?.data?.targetBranch?.id ?? 'no-target-branch-id';
|
||||
const orderType = getOrderTypeFeature(item.features) ?? 'no-orderType';
|
||||
return `${ean}|${destinationId}|${orderType}`;
|
||||
return `${ean}|${targetBranchId}|${orderType}`;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { inject, Injectable, resource, signal } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { BranchService } from '../services';
|
||||
|
||||
@Injectable()
|
||||
export class BranchResource {
|
||||
#logger = logger({ service: 'BranchResource' });
|
||||
#branchService = inject(BranchService);
|
||||
|
||||
#params = signal<
|
||||
@@ -24,17 +26,20 @@ export class BranchResource {
|
||||
if ('branchId' in params && !params.branchId) {
|
||||
return null;
|
||||
}
|
||||
this.#logger.debug('Loading branch', () => params);
|
||||
const res = await this.#branchService.fetchBranches(abortSignal);
|
||||
return res.find((b) => {
|
||||
const branch = res.find((b) => {
|
||||
if ('branchId' in params) {
|
||||
return b.id === params.branchId;
|
||||
} else {
|
||||
return b.branchNumber === params.branchNumber;
|
||||
}
|
||||
});
|
||||
this.#logger.debug('Branch loaded', () => ({
|
||||
found: !!branch,
|
||||
branchId: branch?.id,
|
||||
}));
|
||||
return branch;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AssignedBranchResource {}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { inject, Injectable, resource } from '@angular/core';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { BranchService } from '../services';
|
||||
|
||||
@Injectable()
|
||||
export class BranchesResource {
|
||||
#logger = logger({ service: 'BranchesResource' });
|
||||
#branchService = inject(BranchService);
|
||||
|
||||
readonly resource = resource({
|
||||
loader: async ({ abortSignal }) => {
|
||||
this.#logger.debug('Loading all branches');
|
||||
const branches = await this.#branchService.fetchBranches(abortSignal);
|
||||
this.#logger.debug('Branches loaded', () => ({ count: branches.length }));
|
||||
return branches
|
||||
.filter(
|
||||
(branch) =>
|
||||
branch.isOnline &&
|
||||
branch.isShippingEnabled &&
|
||||
branch.isOrderingEnabled &&
|
||||
branch.name !== undefined,
|
||||
)
|
||||
.sort((a, b) => a.name!.localeCompare(b.name!));
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './branch.resource';
|
||||
export * from './branches.resource';
|
||||
export * from './shopping-cart.resource';
|
||||
|
||||
@@ -4,27 +4,26 @@ import {
|
||||
CHECKOUT_REWARD_SELECTION_POPUP_OPENED_STATE_KEY,
|
||||
CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY,
|
||||
CHECKOUT_SHOPPING_CART_ID_METADATA_KEY,
|
||||
COMPLETED_SHOPPING_CARTS_METADATA_KEY,
|
||||
SELECTED_BRANCH_METADATA_KEY,
|
||||
} from '../constants';
|
||||
import z from 'zod';
|
||||
import { ShoppingCart } from '../models';
|
||||
import { Branch, BranchSchema } from '../schemas/branch.schema';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CheckoutMetadataService {
|
||||
#tabService = inject(TabService);
|
||||
|
||||
setSelectedBranchId(tabId: number, branchId: number | undefined) {
|
||||
setSelectedBranch(tabId: number, branch: Branch | undefined) {
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
[SELECTED_BRANCH_METADATA_KEY]: branchId,
|
||||
[SELECTED_BRANCH_METADATA_KEY]: branch,
|
||||
});
|
||||
}
|
||||
|
||||
getSelectedBranchId(tabId: number): number | undefined {
|
||||
getSelectedBranch(tabId: number): Branch | undefined {
|
||||
return getMetadataHelper(
|
||||
tabId,
|
||||
SELECTED_BRANCH_METADATA_KEY,
|
||||
z.number().optional(),
|
||||
BranchSchema.optional(),
|
||||
this.#tabService.entityMap(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import { OpenRewardTasksResource } from '@isa/oms/data-access';
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
import { OpenTaskCardComponent } from './open-task-card.component';
|
||||
@@ -32,7 +39,7 @@ import { OpenTaskCardComponent } from './open-task-card.component';
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OpenTasksCarouselComponent {
|
||||
export class OpenTasksCarouselComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Global resource managing open reward tasks data
|
||||
*/
|
||||
@@ -48,7 +55,7 @@ export class OpenTasksCarouselComponent {
|
||||
const tasks = this.openTasksResource.tasks();
|
||||
const seenOrderIds = new Set<number>();
|
||||
|
||||
return tasks.filter(task => {
|
||||
return tasks.filter((task) => {
|
||||
if (!task.orderId || seenOrderIds.has(task.orderId)) {
|
||||
return false;
|
||||
}
|
||||
@@ -56,4 +63,21 @@ export class OpenTasksCarouselComponent {
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Initializes the component by loading open tasks and starting auto-refresh.
|
||||
* Ensures the carousel displays current data when mounted.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.openTasksResource.refresh();
|
||||
this.openTasksResource.startAutoRefresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup lifecycle hook that stops auto-refresh polling.
|
||||
* Prevents memory leaks by clearing the refresh interval.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.openTasksResource.stopAutoRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,14 +11,13 @@ import {
|
||||
ShoppingCartFacade,
|
||||
SelectedRewardShoppingCartResource,
|
||||
} from '@isa/checkout/data-access';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { injectTabId, TabService } from '@isa/core/tabs';
|
||||
import { StatefulButtonComponent, StatefulButtonState } from '@isa/ui/buttons';
|
||||
import { PurchaseOptionsModalService } from '@modal/purchase-options';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
import { getRouteToCustomer } from '../helpers';
|
||||
import { PrimaryCustomerCardResource } from '@isa/crm/data-access';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
|
||||
@Component({
|
||||
selector: 'reward-action',
|
||||
@@ -32,7 +31,7 @@ export class RewardActionComponent {
|
||||
#store = inject(RewardCatalogStore);
|
||||
#tabId = injectTabId();
|
||||
|
||||
#navigationState = inject(NavigationStateService);
|
||||
#tabService = inject(TabService);
|
||||
#purchasingOptionsModal = inject(PurchaseOptionsModalService);
|
||||
#shoppingCartFacade = inject(ShoppingCartFacade);
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
@@ -122,12 +121,9 @@ export class RewardActionComponent {
|
||||
const route = getRouteToCustomer(tabId);
|
||||
|
||||
// Preserve context: Store current reward page URL to return to after customer selection
|
||||
await this.#navigationState.preserveContext(
|
||||
{
|
||||
returnUrl: this.#router.url,
|
||||
},
|
||||
'select-customer',
|
||||
);
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
'select-customer': { returnUrl: this.#router.url },
|
||||
});
|
||||
|
||||
await this.#router.navigate(route.path, {
|
||||
queryParams: route.queryParams,
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<reward-catalog-open-tasks-carousel></reward-catalog-open-tasks-carousel>
|
||||
<reward-header></reward-header>
|
||||
<filter-controls-panel
|
||||
[switchFilters]="displayStockFilterSwitch()"
|
||||
(triggerSearch)="search($event)"
|
||||
></filter-controls-panel>
|
||||
<reward-list
|
||||
[searchTrigger]="searchTrigger()"
|
||||
(searchTriggerChange)="searchTrigger.set($event)"
|
||||
></reward-list>
|
||||
<reward-action></reward-action>
|
||||
@let tId = tabId();
|
||||
<reward-catalog-open-tasks-carousel></reward-catalog-open-tasks-carousel>
|
||||
<reward-header></reward-header>
|
||||
<filter-controls-panel
|
||||
[forceMobileLayout]="isCallCenter"
|
||||
[switchFilters]="displayStockFilterSwitch()"
|
||||
(triggerSearch)="search($event)"
|
||||
>
|
||||
@if (isCallCenter && tId) {
|
||||
<checkout-selected-branch-dropdown [tabId]="tId" />
|
||||
}
|
||||
</filter-controls-panel>
|
||||
<reward-list
|
||||
[searchTrigger]="searchTrigger()"
|
||||
(searchTriggerChange)="searchTrigger.set($event)"
|
||||
></reward-list>
|
||||
<reward-action></reward-action>
|
||||
|
||||
@@ -14,14 +14,18 @@ import {
|
||||
SearchTrigger,
|
||||
FilterService,
|
||||
FilterInput,
|
||||
SwitchMenuButtonComponent,
|
||||
} from '@isa/shared/filter';
|
||||
import { RewardHeaderComponent } from './reward-header/reward-header.component';
|
||||
import { RewardListComponent } from './reward-list/reward-list.component';
|
||||
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
|
||||
import { RewardActionComponent } from './reward-action/reward-action.component';
|
||||
import { SelectedRewardShoppingCartResource } from '@isa/checkout/data-access';
|
||||
import { SelectedBranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';
|
||||
import { SelectedCustomerResource } from '@isa/crm/data-access';
|
||||
import { OpenTasksCarouselComponent } from './open-tasks-carousel/open-tasks-carousel.component';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { Role, RoleService } from '@isa/core/auth';
|
||||
|
||||
/**
|
||||
* Factory function to retrieve query settings from the activated route data.
|
||||
@@ -50,6 +54,7 @@ function querySettingsFactory() {
|
||||
RewardHeaderComponent,
|
||||
RewardListComponent,
|
||||
RewardActionComponent,
|
||||
SelectedBranchDropdownComponent,
|
||||
],
|
||||
host: {
|
||||
'[class]':
|
||||
@@ -57,6 +62,10 @@ function querySettingsFactory() {
|
||||
},
|
||||
})
|
||||
export class RewardCatalogComponent {
|
||||
readonly tabId = injectTabId();
|
||||
|
||||
readonly isCallCenter = inject(RoleService).hasRole(Role.CallCenter);
|
||||
|
||||
restoreScrollPosition = injectRestoreScrollPosition();
|
||||
|
||||
searchTrigger = signal<SearchTrigger | 'reload' | 'initial'>('initial');
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { injectTabId, TabService } from '@isa/core/tabs';
|
||||
import { Router } from '@angular/router';
|
||||
import { getRouteToCustomer } from '../../helpers';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
|
||||
@Component({
|
||||
selector: 'reward-start-card',
|
||||
@@ -13,7 +12,7 @@ import { NavigationStateService } from '@isa/core/navigation';
|
||||
imports: [ButtonComponent],
|
||||
})
|
||||
export class RewardStartCardComponent {
|
||||
readonly #navigationState = inject(NavigationStateService);
|
||||
readonly #tabService = inject(TabService);
|
||||
readonly #router = inject(Router);
|
||||
|
||||
tabId = injectTabId();
|
||||
@@ -22,19 +21,19 @@ export class RewardStartCardComponent {
|
||||
* Called when "Kund*in auswählen" button is clicked.
|
||||
* Preserves the current URL as returnUrl before navigating to customer search.
|
||||
*/
|
||||
async onSelectCustomer() {
|
||||
onSelectCustomer() {
|
||||
const customerRoute = getRouteToCustomer(this.tabId());
|
||||
const tabId = this.#tabService.activatedTabId();
|
||||
|
||||
// Preserve context: Store current reward page URL to return to after customer selection
|
||||
await this.#navigationState.preserveContext(
|
||||
{
|
||||
returnUrl: this.#router.url,
|
||||
},
|
||||
'select-customer',
|
||||
);
|
||||
if (tabId) {
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
'select-customer': { returnUrl: this.#router.url },
|
||||
});
|
||||
}
|
||||
|
||||
// Navigate to customer search
|
||||
await this.#router.navigate(customerRoute.path, {
|
||||
this.#router.navigate(customerRoute.path, {
|
||||
queryParams: customerRoute.queryParams,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
@let i = item();
|
||||
<ui-client-row data-what="reward-list-item" [attr.data-which]="i.id">
|
||||
<ui-client-row-content
|
||||
class="flex-grow"
|
||||
[class.row-start-1]="!desktopBreakpoint()"
|
||||
>
|
||||
<checkout-product-info-redemption
|
||||
[item]="i"
|
||||
[orientation]="productInfoOrientation()"
|
||||
></checkout-product-info-redemption>
|
||||
</ui-client-row-content>
|
||||
<ui-item-row-data
|
||||
class="flex-grow"
|
||||
[class.stock-row-tablet]="!desktopBreakpoint()"
|
||||
>
|
||||
<checkout-stock-info [item]="i"></checkout-stock-info>
|
||||
</ui-item-row-data>
|
||||
|
||||
<ui-item-row-data
|
||||
class="justify-center"
|
||||
[class.select-row-tablet]="!desktopBreakpoint()"
|
||||
>
|
||||
<reward-list-item-select
|
||||
class="self-end"
|
||||
[item]="i"
|
||||
></reward-list-item-select>
|
||||
</ui-item-row-data>
|
||||
</ui-client-row>
|
||||
@let i = item();
|
||||
<ui-client-row data-what="reward-list-item" [attr.data-which]="i.id">
|
||||
<ui-client-row-content
|
||||
class="flex-grow"
|
||||
[class.row-start-1]="!desktopBreakpoint()"
|
||||
>
|
||||
<checkout-product-info-redemption
|
||||
[item]="i"
|
||||
[orientation]="productInfoOrientation()"
|
||||
></checkout-product-info-redemption>
|
||||
</ui-client-row-content>
|
||||
<ui-item-row-data
|
||||
class="flex-grow"
|
||||
[class.stock-row-tablet]="!desktopBreakpoint()"
|
||||
>
|
||||
<checkout-stock-info
|
||||
[item]="i"
|
||||
[branchId]="selectedBranchId()"
|
||||
></checkout-stock-info>
|
||||
</ui-item-row-data>
|
||||
|
||||
<ui-item-row-data
|
||||
class="justify-center"
|
||||
[class.select-row-tablet]="!desktopBreakpoint()"
|
||||
>
|
||||
<reward-list-item-select
|
||||
class="self-end"
|
||||
[item]="i"
|
||||
></reward-list-item-select>
|
||||
</ui-item-row-data>
|
||||
</ui-client-row>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
input,
|
||||
linkedSignal,
|
||||
computed,
|
||||
} from '@angular/core';
|
||||
import { Item } from '@isa/catalogue/data-access';
|
||||
import { ClientRowImports, ItemRowDataImports } from '@isa/ui/item-rows';
|
||||
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||
import { CheckoutMetadataService } from '@isa/checkout/data-access';
|
||||
import { ProductInfoRedemptionComponent } from '@isa/checkout/shared/product-info';
|
||||
import { StockInfoComponent } from '@isa/checkout/shared/product-info';
|
||||
import { RewardListItemSelectComponent } from './reward-list-item-select/reward-list-item-select.component';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
|
||||
@Component({
|
||||
selector: 'reward-list-item',
|
||||
@@ -25,6 +29,18 @@ import { RewardListItemSelectComponent } from './reward-list-item-select/reward-
|
||||
],
|
||||
})
|
||||
export class RewardListItemComponent {
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
|
||||
tabId = injectTabId();
|
||||
|
||||
selectedBranchId = computed(() => {
|
||||
const tabid = this.tabId();
|
||||
if (!tabid) {
|
||||
return null;
|
||||
}
|
||||
return this.#checkoutMetadataService.getSelectedBranch(tabid)?.id;
|
||||
});
|
||||
|
||||
item = input.required<Item>();
|
||||
|
||||
desktopBreakpoint = breakpoint([
|
||||
|
||||
@@ -6,6 +6,7 @@ export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: RewardCatalogComponent,
|
||||
title: 'Prämienshop',
|
||||
resolve: { querySettings: querySettingsResolverFn },
|
||||
data: {
|
||||
scrollPositionRestoration: true,
|
||||
|
||||
@@ -1,135 +1,249 @@
|
||||
# checkout-feature-reward-order-confirmation
|
||||
# @isa/checkout/feature/reward-order-confirmation
|
||||
|
||||
A feature library providing a comprehensive order confirmation page for reward orders with support for printing, address display, and loyalty reward collection.
|
||||
|
||||
## Overview
|
||||
|
||||
The `@isa/checkout/feature/reward-order-confirmation` library provides the **reward order confirmation screen** displayed after customers complete a loyalty reward redemption purchase. It shows a comprehensive summary of completed reward orders, including shipping/billing addresses, product details, loyalty points used, and delivery method information.
|
||||
This feature provides a complete order confirmation experience after a reward order has been placed. It displays order details grouped by delivery destination, shows payer and shipping information, and enables users with appropriate permissions to print order confirmations. For items with the 'Rücklage' (layaway) feature, it provides an action card that allows staff to collect loyalty rewards in various ways (collect, donate, or cancel).
|
||||
|
||||
**Type:** Routed feature library with lazy-loaded components
|
||||
The library uses NgRx Signal Store for reactive state management, integrating with the OMS (Order Management System) API to load and display order data. It supports multiple orders being displayed simultaneously by accepting comma-separated display order IDs in the route parameter.
|
||||
|
||||
## Features
|
||||
## Architecture
|
||||
|
||||
- **Multi-Order Support**: Display multiple orders from a single shopping cart session
|
||||
- **Order Type Awareness**: Conditional display based on delivery method (Delivery, Pickup, In-Store)
|
||||
- **Address Deduplication**: Smart handling of duplicate addresses across multiple orders
|
||||
- **Loyalty Points Display**: Shows loyalty points (Lesepunkte) used for each reward item
|
||||
- **Product Information**: Comprehensive product details including name, contributors, EAN, and quantity
|
||||
- **Destination Information**: Delivery/pickup destination details
|
||||
### State Management
|
||||
|
||||
## Main Components
|
||||
The library uses a **signalStore** (`OrderConfiramtionStore`) that:
|
||||
- Loads display orders via `DisplayOrdersResource`
|
||||
- Computes derived state (payers, shipping addresses, target branches)
|
||||
- Determines which order type features are present (delivery, pickup, in-store)
|
||||
- Provides reactive data to all child components
|
||||
|
||||
### RewardOrderConfirmationComponent (Main Container)
|
||||
- **Selector:** `checkout-reward-order-confirmation`
|
||||
- **Route:** `/:orderIds` (accepts multiple order IDs separated by `+`)
|
||||
- Orchestrates the entire confirmation view
|
||||
- Manages state via `OrderConfiramtionStore`
|
||||
### Route Configuration
|
||||
|
||||
The feature is accessible via the route pattern: `:displayOrderIds`
|
||||
|
||||
Example: `/order-confirmation/1234,5678` displays orders with display IDs 1234 and 5678.
|
||||
|
||||
The route includes:
|
||||
- OMS action handlers for command execution
|
||||
- Tab cleanup on deactivation
|
||||
- Lazy-loaded main component
|
||||
|
||||
## Components
|
||||
|
||||
### RewardOrderConfirmationComponent
|
||||
|
||||
**Selector:** `checkout-reward-order-confirmation`
|
||||
|
||||
Main container component that orchestrates the order confirmation page.
|
||||
|
||||
**Key Responsibilities:**
|
||||
- Parses display order IDs from route parameters (comma-separated)
|
||||
- Initializes the store with tab ID and order IDs
|
||||
- Triggers display order loading via `DisplayOrdersResource`
|
||||
- Composes child components (header, addresses, item list)
|
||||
|
||||
**Effects:**
|
||||
- Updates store state when route parameters or tab ID change
|
||||
- Loads display orders when order IDs change
|
||||
|
||||
### OrderConfirmationHeaderComponent
|
||||
- **Selector:** `checkout-order-confirmation-header`
|
||||
- Displays header: "Prämienausgabe abgeschlossen" (Reward distribution completed)
|
||||
|
||||
**Selector:** `checkout-order-confirmation-header`
|
||||
|
||||
Displays the page header with print functionality.
|
||||
|
||||
**Features:**
|
||||
- Print button that triggers `CheckoutPrintFacade.printOrderConfirmation()`
|
||||
- Role-based access control (print feature restricted by role)
|
||||
- Integrates with the ISA printing system
|
||||
|
||||
**Dependencies:**
|
||||
- `@isa/common/print` - Print button and printer selection
|
||||
- `@isa/core/auth` - Role-based directive (`*ifRole`)
|
||||
|
||||
### OrderConfirmationAddressesComponent
|
||||
- **Selector:** `checkout-order-confirmation-addresses`
|
||||
- Displays billing addresses, shipping addresses, and pickup branch information
|
||||
- Conditionally shows sections based on order type
|
||||
|
||||
**Selector:** `checkout-order-confirmation-addresses`
|
||||
|
||||
Displays payer and destination information based on order types.
|
||||
|
||||
**Displayed Information:**
|
||||
- **Payers:** Deduplicated list of buyers across all orders
|
||||
- **Shipping Addresses:** Shown only if orders have delivery features (Delivery, DigitalShipping, B2BShipping)
|
||||
- **Target Branches:** Shown only if orders have in-store features (InStore, Pickup)
|
||||
|
||||
**Dependencies:**
|
||||
- `@isa/shared/address` - Address display component
|
||||
- `@isa/crm/data-access` - Customer name formatting and address deduplication
|
||||
|
||||
### OrderConfirmationItemListComponent
|
||||
- **Selector:** `checkout-order-confirmation-item-list`
|
||||
- Displays order type badge with icon (Delivery/Pickup/In-Store)
|
||||
- Renders list of order items for each order
|
||||
|
||||
**Selector:** `checkout-order-confirmation-item-list`
|
||||
|
||||
Groups and displays order items by delivery destination and type.
|
||||
|
||||
**Key Features:**
|
||||
- Groups items using `groupItemsByDeliveryDestination()` helper
|
||||
- Displays delivery type icons (Versand, Rücklage, B2B Versand)
|
||||
- Optimized rendering with `trackBy` function for item groups
|
||||
|
||||
**Grouping Logic:**
|
||||
Items are grouped by:
|
||||
1. Order type (e.g., Delivery, Pickup, InStore)
|
||||
2. Shipping address (for delivery orders)
|
||||
3. Target branch (for pickup/in-store orders)
|
||||
|
||||
### OrderConfirmationItemListItemComponent
|
||||
- **Selector:** `checkout-order-confirmation-item-list-item`
|
||||
- Displays individual product information, quantity, loyalty points, and destination info
|
||||
|
||||
**Selector:** `checkout-order-confirmation-item-list-item`
|
||||
|
||||
Displays individual order item details within a group.
|
||||
|
||||
**Inputs:**
|
||||
- `item` (required): `DisplayOrderItemDTO` - The order item to display
|
||||
- `order` (required): `OrderItemGroup` - The group containing delivery type and destination
|
||||
|
||||
**Features:**
|
||||
- Product information display via `ProductInfoComponent`
|
||||
- Destination information via `DisplayOrderDestinationInfoComponent`
|
||||
- Action card for loyalty reward collection (conditional)
|
||||
- Loyalty points display
|
||||
|
||||
### ConfirmationListItemActionCardComponent
|
||||
- **Selector:** `checkout-confirmation-list-item-action-card`
|
||||
- Placeholder component for future action functionality
|
||||
|
||||
## State Management
|
||||
**Selector:** `checkout-confirmation-list-item-action-card`
|
||||
|
||||
**OrderConfiramtionStore** (NgRx Signals)
|
||||
Provides loyalty reward collection interface for layaway items.
|
||||
|
||||
**State:**
|
||||
- `tabId`: Current tab identifier
|
||||
- `orderIds`: Array of order IDs to display
|
||||
**Inputs:**
|
||||
- `item` (required): `DisplayOrderItem` - The order item with loyalty information
|
||||
|
||||
**Computed Properties:**
|
||||
- `orders`: Filtered display orders from OMS metadata service
|
||||
- `shoppingCart`: Associated completed shopping cart
|
||||
- `payers`: Deduplicated billing addressees
|
||||
- `shippingAddresses`: Deduplicated shipping addressees
|
||||
- `targetBranches`: Deduplicated pickup branches
|
||||
- `hasDeliveryOrderTypeFeature`: Boolean indicating delivery orders
|
||||
- `hasTargetBranchFeature`: Boolean indicating pickup/in-store orders
|
||||
**Display Conditions:**
|
||||
The action card is displayed when ALL of the following are met:
|
||||
- Item has the 'Rücklage' (layaway) order type feature
|
||||
- AND either:
|
||||
- Item has a loyalty collect command available, OR
|
||||
- Item processing is already complete
|
||||
|
||||
**Features:**
|
||||
- **Action Selection:** Dropdown to choose collection type:
|
||||
- `Collect` - Collect loyalty points for customer
|
||||
- `Donate` - Donate points to charity
|
||||
- `Cancel` - Cancel the reward collection
|
||||
- **Processing States:** Shows current processing status (Ordered, InProgress, Complete)
|
||||
- **Command Execution:** Integrates with OMS command system for reward collection
|
||||
- **Loading States:** Displays loading indicators during API calls
|
||||
- **Completion Status:** Shows check icon when processing is complete
|
||||
|
||||
**Role-Based Access:** Collection functionality is restricted by role permissions.
|
||||
|
||||
**Dependencies:**
|
||||
- `@isa/oms/data-access` - Order reward collection facade and command handling
|
||||
- `@isa/ui/buttons` - Button and dropdown components
|
||||
- `@isa/checkout/data-access` - Order type feature detection and item utilities
|
||||
|
||||
## Routes
|
||||
|
||||
```typescript
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: ':displayOrderIds',
|
||||
loadComponent: () => import('./reward-order-confirmation.component'),
|
||||
canDeactivate: [canDeactivateTabCleanup],
|
||||
providers: [CoreCommandModule.forChild(OMS_ACTION_HANDLERS)]
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
**Route Parameters:**
|
||||
- `displayOrderIds` - Comma-separated list of display order IDs (e.g., "1234,5678")
|
||||
|
||||
## Usage
|
||||
|
||||
### Routing Integration
|
||||
### Importing Routes
|
||||
|
||||
```typescript
|
||||
// In parent routing module
|
||||
{
|
||||
path: 'reward-confirmation',
|
||||
loadChildren: () => import('@isa/checkout/feature/reward-order-confirmation')
|
||||
.then(m => m.routes)
|
||||
}
|
||||
import { routes as rewardOrderConfirmationRoutes } from '@isa/checkout/feature/reward-order-confirmation';
|
||||
|
||||
const appRoutes: Routes = [
|
||||
{
|
||||
path: 'order-confirmation',
|
||||
children: rewardOrderConfirmationRoutes
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### URL Structure
|
||||
### Navigation Example
|
||||
|
||||
```
|
||||
/reward-confirmation/123+456+789
|
||||
```
|
||||
```typescript
|
||||
// Navigate to confirmation for single order
|
||||
router.navigate(['/order-confirmation', '1234']);
|
||||
|
||||
This displays confirmation for orders with IDs 123, 456, and 789.
|
||||
// Navigate to confirmation for multiple orders
|
||||
router.navigate(['/order-confirmation', '1234,5678,9012']);
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Data Access:**
|
||||
- `@isa/oms/data-access` - Order metadata and models
|
||||
- `@isa/checkout/data-access` - Checkout metadata and order type utilities
|
||||
- `@isa/crm/data-access` - Address deduplication utilities
|
||||
### Core Angular & NgRx
|
||||
- `@angular/core` - Angular framework
|
||||
- `@angular/router` - Routing
|
||||
- `@ngrx/signals` - Signal-based state management
|
||||
|
||||
**Core:**
|
||||
- `@isa/core/tabs` - Tab context management
|
||||
### ISA Libraries
|
||||
|
||||
**Shared Components:**
|
||||
- `@isa/shared/address` - Address rendering
|
||||
- `@isa/checkout/shared/product-info` - Product and destination information
|
||||
#### Data Access
|
||||
- `@isa/checkout/data-access` - Order type features, grouping utilities
|
||||
- `@isa/oms/data-access` - Display orders resource, reward collection, command handling
|
||||
- `@isa/crm/data-access` - Customer name formatting, address/branch deduplication
|
||||
|
||||
**UI:**
|
||||
- `@isa/icons` - Delivery method icons
|
||||
#### Shared Components
|
||||
- `@isa/checkout/shared/product-info` - Product display components
|
||||
- `@isa/shared/address` - Address display component
|
||||
- `@isa/common/print` - Print button and printer integration
|
||||
- `@isa/ui/buttons` - Button components
|
||||
- `@isa/ui/input-controls` - Dropdown components
|
||||
|
||||
## Data Flow
|
||||
#### Core Services
|
||||
- `@isa/core/tabs` - Tab service and cleanup guard
|
||||
- `@isa/core/auth` - Role-based access control
|
||||
- `@isa/core/command` - Command module integration
|
||||
|
||||
1. Route parameter (`orderIds`) is parsed into array of integers
|
||||
2. Store receives `tabId` from `TabService` and `orderIds` from route
|
||||
3. Store fetches orders from `OmsMetadataService` filtered by IDs
|
||||
4. Store fetches completed shopping cart from `CheckoutMetadataService`
|
||||
5. Components consume computed properties from store (addresses, order items, points)
|
||||
#### Icons
|
||||
- `@isa/icons` - ISA custom icon set
|
||||
- `@ng-icons/core` - Icon component
|
||||
|
||||
## Order Type Support
|
||||
|
||||
The library supports three delivery methods with specific icons:
|
||||
|
||||
- **Delivery** (`isaDeliveryVersand`): Shows shipping addresses
|
||||
- **Pickup** (`isaDeliveryRuecklage2`): Shows pickup branch
|
||||
- **In-Store** (`isaDeliveryRuecklage1`): Shows store branch
|
||||
### Generated APIs
|
||||
- `@generated/swagger/oms-api` - OMS API types (DisplayOrderItemDTO)
|
||||
|
||||
## Testing
|
||||
|
||||
Run tests with Vitest:
|
||||
The library uses Vitest for testing with comprehensive coverage of:
|
||||
- Component rendering and interactions
|
||||
- State management and computed signals
|
||||
- Route parameter parsing
|
||||
- Reward collection workflows
|
||||
- Conditional display logic
|
||||
|
||||
Run tests:
|
||||
```bash
|
||||
npx nx test checkout-feature-reward-order-confirmation --skip-nx-cache
|
||||
nx test checkout-feature-reward-order-confirmation
|
||||
```
|
||||
|
||||
**Test Framework:** Vitest
|
||||
**Coverage Output:** `coverage/libs/checkout/feature/reward-order-confirmation`
|
||||
## Key Features Summary
|
||||
|
||||
## Architecture Notes
|
||||
1. **Multi-Order Support:** Display confirmation for multiple orders simultaneously
|
||||
2. **Smart Grouping:** Items grouped by delivery destination for clarity
|
||||
3. **Conditional Displays:** Address sections shown based on order type features
|
||||
4. **Print Integration:** Role-based printing with printer selection
|
||||
5. **Loyalty Rewards:** In-page reward collection for layaway items
|
||||
6. **Reactive State:** Signal-based state management with computed values
|
||||
7. **Tab Integration:** Proper cleanup on tab deactivation
|
||||
8. **Role-Based Access:** Permissions enforced for sensitive operations
|
||||
|
||||
- **Component Prefix:** `checkout`
|
||||
- **Change Detection:** All components use `OnPush` strategy
|
||||
- **Standalone Architecture:** All components are standalone with explicit imports
|
||||
- **Project Name:** `checkout-feature-reward-order-confirmation`
|
||||
## Related Libraries
|
||||
|
||||
- `@isa/checkout/feature/checkout-process` - Main checkout flow
|
||||
- `@isa/oms/data-access` - Order management system integration
|
||||
- `@isa/checkout/data-access` - Checkout domain logic and utilities
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
:host {
|
||||
@apply w-full flex flex-row items-center justify-between;
|
||||
}
|
||||
|
||||
ui-item-row-data-label {
|
||||
width: 12.4rem;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
<h1 class="text-isa-neutral-900 isa-text-subtitle-1-regular">
|
||||
Prämienausgabe abgeschlossen
|
||||
</h1>
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-isa-neutral-900 isa-text-subtitle-1-regular">
|
||||
Prämienausgabe abgeschlossen
|
||||
</h1>
|
||||
<ui-item-row-data>
|
||||
<ui-item-row-data-row>
|
||||
<ui-item-row-data-label>Vorgangs-ID</ui-item-row-data-label>
|
||||
<ui-item-row-data-value>{{ orderNumbers() }}</ui-item-row-data-value>
|
||||
</ui-item-row-data-row>
|
||||
<ui-item-row-data-row>
|
||||
<ui-item-row-data-label>Bestelldatum</ui-item-row-data-label>
|
||||
<ui-item-row-data-value>{{ orderDates() }}</ui-item-row-data-value>
|
||||
</ui-item-row-data-row>
|
||||
</ui-item-row-data>
|
||||
</div>
|
||||
|
||||
<common-print-button printerType="label" [printFn]="printFn"
|
||||
<common-print-button
|
||||
class="self-start"
|
||||
*ifNotRole="Role.CallCenter"
|
||||
printerType="label"
|
||||
[printFn]="printFn"
|
||||
>Prämienbeleg drucken</common-print-button
|
||||
>
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
computed,
|
||||
} from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { CheckoutPrintFacade } from '@isa/checkout/data-access';
|
||||
import { PrintButtonComponent, Printer } from '@isa/common/print';
|
||||
import { OrderConfiramtionStore } from '../reward-order-confirmation.store';
|
||||
import { IfRoleDirective, Role } from '@isa/core/auth';
|
||||
import { ItemRowDataImports } from '@isa/ui/item-rows';
|
||||
|
||||
@Component({
|
||||
selector: 'checkout-order-confirmation-header',
|
||||
templateUrl: './order-confirmation-header.component.html',
|
||||
styleUrls: ['./order-confirmation-header.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [PrintButtonComponent],
|
||||
imports: [PrintButtonComponent, IfRoleDirective, ItemRowDataImports],
|
||||
})
|
||||
export class OrderConfirmationHeaderComponent {
|
||||
protected readonly Role = Role;
|
||||
#checkoutPrintFacade = inject(CheckoutPrintFacade);
|
||||
#store = inject(OrderConfiramtionStore);
|
||||
#datePipe = new DatePipe('de-DE');
|
||||
|
||||
orderIds = this.#store.orderIds;
|
||||
|
||||
@@ -22,4 +32,36 @@ export class OrderConfirmationHeaderComponent {
|
||||
data: this.orderIds() ?? [],
|
||||
});
|
||||
};
|
||||
|
||||
orderNumbers = computed(() => {
|
||||
const orders = this.#store.orders();
|
||||
if (!orders || orders.length === 0) {
|
||||
return '';
|
||||
}
|
||||
return orders
|
||||
.map((order) => order.orderNumber)
|
||||
.filter(Boolean)
|
||||
.join('; ');
|
||||
});
|
||||
|
||||
orderDates = computed(() => {
|
||||
const orders = this.#store.orders();
|
||||
if (!orders || orders.length === 0) {
|
||||
return '';
|
||||
}
|
||||
const formattedDates = orders
|
||||
.map((order) => {
|
||||
if (!order.orderDate) {
|
||||
return null;
|
||||
}
|
||||
const formatted = this.#datePipe.transform(
|
||||
order.orderDate,
|
||||
'dd.MM.yyyy | HH:mm',
|
||||
);
|
||||
return formatted ? `${formatted} Uhr` : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return [...new Set(formattedDates)].join('; ');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
HandleCommandService,
|
||||
HandleCommand,
|
||||
getMainActions,
|
||||
DisplayOrdersResource,
|
||||
} from '@isa/oms/data-access';
|
||||
import { ButtonComponent } from '@isa/ui/buttons';
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
@@ -61,6 +62,7 @@ export class ConfirmationListItemActionCardComponent {
|
||||
#orderRewardCollectFacade = inject(OrderRewardCollectFacade);
|
||||
#store = inject(OrderConfiramtionStore);
|
||||
#orderItemSubsetResource = inject(OrderItemSubsetResource);
|
||||
#displayOrdersResource = inject(DisplayOrdersResource);
|
||||
#handleCommandFacade = inject(HandleCommandFacade);
|
||||
|
||||
item = input.required<DisplayOrderItem>();
|
||||
@@ -165,7 +167,7 @@ export class ConfirmationListItemActionCardComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.#orderItemSubsetResource.refresh();
|
||||
this.reloadResources();
|
||||
}
|
||||
} finally {
|
||||
this.isLoading.set(false);
|
||||
@@ -175,4 +177,9 @@ export class ConfirmationListItemActionCardComponent {
|
||||
async handleCommand(params: HandleCommand) {
|
||||
await this.#handleCommandFacade.handle(params);
|
||||
}
|
||||
|
||||
reloadResources() {
|
||||
this.#orderItemSubsetResource.refresh();
|
||||
this.#displayOrdersResource.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
:host {
|
||||
@apply block w-full text-isa-neutral-900 mt-[1.42rem];
|
||||
@apply flex flex-col w-full text-isa-neutral-900 mt-[1.42rem] gap-4;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@if (!hasPendingActions()) {
|
||||
<tabs-navigate-back-button [navigateTo]="rewardCatalogRoute()" />
|
||||
}
|
||||
<div
|
||||
class="bg-isa-white p-6 rounded-2xl flex flex-col gap-6 items-start self-stretch"
|
||||
>
|
||||
|
||||
@@ -12,9 +12,21 @@ import { OrderConfirmationAddressesComponent } from './order-confirmation-addres
|
||||
import { OrderConfirmationHeaderComponent } from './order-confirmation-header/order-confirmation-header.component';
|
||||
import { OrderConfirmationItemListComponent } from './order-confirmation-item-list/order-confirmation-item-list.component';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import {
|
||||
NavigateBackButtonComponent,
|
||||
TabService,
|
||||
injectTabId,
|
||||
} from '@isa/core/tabs';
|
||||
import { OrderConfiramtionStore } from './reward-order-confirmation.store';
|
||||
import { DisplayOrdersResource } from '@isa/oms/data-access';
|
||||
import {
|
||||
DisplayOrdersResource,
|
||||
getProcessingStatusState,
|
||||
ProcessingStatusState,
|
||||
} from '@isa/oms/data-access';
|
||||
import {
|
||||
hasOrderTypeFeature,
|
||||
hasLoyaltyCollectCommand,
|
||||
} from '@isa/checkout/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'checkout-reward-order-confirmation',
|
||||
@@ -25,13 +37,14 @@ import { DisplayOrdersResource } from '@isa/oms/data-access';
|
||||
OrderConfirmationHeaderComponent,
|
||||
OrderConfirmationAddressesComponent,
|
||||
OrderConfirmationItemListComponent,
|
||||
NavigateBackButtonComponent,
|
||||
],
|
||||
providers: [OrderConfiramtionStore, DisplayOrdersResource],
|
||||
})
|
||||
export class RewardOrderConfirmationComponent {
|
||||
#store = inject(OrderConfiramtionStore);
|
||||
#displayOrdersResource = inject(DisplayOrdersResource);
|
||||
#tabId = inject(TabService).activatedTabId;
|
||||
#tabId = injectTabId();
|
||||
#activatedRoute = inject(ActivatedRoute);
|
||||
|
||||
params = toSignal(this.#activatedRoute.paramMap);
|
||||
@@ -49,6 +62,43 @@ export class RewardOrderConfirmationComponent {
|
||||
orderIds = this.displayOrderIds;
|
||||
orders = this.#store.orders;
|
||||
|
||||
/**
|
||||
* Checks if there are any items with pending actions (Rücklage items that still need to be collected).
|
||||
* Returns true if at least one item requires action.
|
||||
*/
|
||||
hasPendingActions = computed(() => {
|
||||
const orders = this.#store.orders();
|
||||
if (!orders) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const allItems = orders.flatMap((order) => order.items ?? []);
|
||||
|
||||
return allItems.some((item) => {
|
||||
const isRuecklage = hasOrderTypeFeature(item.features, ['Rücklage']);
|
||||
if (!isRuecklage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasCollectCommand = hasLoyaltyCollectCommand(item.subsetItems);
|
||||
const statuses = item.subsetItems?.map(
|
||||
(subset) => subset.processingStatus,
|
||||
);
|
||||
const processingStatus = getProcessingStatusState(statuses);
|
||||
const isComplete =
|
||||
processingStatus !== undefined &&
|
||||
processingStatus !== ProcessingStatusState.Ordered;
|
||||
|
||||
// Item has pending action if it has collect command and is not complete
|
||||
return hasCollectCommand && !isComplete;
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Route to the reward catalog for the current tab.
|
||||
*/
|
||||
rewardCatalogRoute = computed(() => `/${this.#tabId()}/reward`);
|
||||
|
||||
constructor() {
|
||||
// Update store state
|
||||
effect(() => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { canDeactivateTabCleanup } from '@isa/core/tabs';
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: ':displayOrderIds',
|
||||
title: 'Prämienshop - Bestellbestätigung',
|
||||
providers: [
|
||||
CoreCommandModule.forChild(OMS_ACTION_HANDLERS).providers ?? [],
|
||||
],
|
||||
|
||||
@@ -14,8 +14,8 @@ import { isaActionEdit } from '@isa/icons';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { AddressComponent } from '@isa/shared/address';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
import { injectTabId, TabService } from '@isa/core/tabs';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'checkout-billing-and-shipping-address-card',
|
||||
@@ -26,7 +26,8 @@ import { NavigationStateService } from '@isa/core/navigation';
|
||||
providers: [provideIcons({ isaActionEdit })],
|
||||
})
|
||||
export class BillingAndShippingAddressCardComponent {
|
||||
#navigationState = inject(NavigationStateService);
|
||||
#tabService = inject(TabService);
|
||||
#router = inject(Router);
|
||||
#shippingAddressResource = inject(SelectedCustomerShippingAddressResource);
|
||||
#payerAddressResource = inject(SelectedCustomerPayerAddressResource);
|
||||
|
||||
@@ -45,18 +46,19 @@ export class BillingAndShippingAddressCardComponent {
|
||||
return this.#customerResource.value();
|
||||
});
|
||||
|
||||
async navigateToCustomer() {
|
||||
navigateToCustomer() {
|
||||
const customerId = this.customer()?.id;
|
||||
if (!customerId) return;
|
||||
const tabId = this.tabId();
|
||||
if (!customerId || !tabId) return;
|
||||
|
||||
const returnUrl = `/${this.tabId()}/reward/cart`;
|
||||
const returnUrl = `/${tabId}/reward/cart`;
|
||||
|
||||
// Preserve context across intermediate navigations (auto-scoped to active tab)
|
||||
await this.#navigationState.navigateWithPreservedContext(
|
||||
['/', 'kunde', this.tabId(), 'customer', 'search', customerId],
|
||||
{ returnUrl },
|
||||
'select-customer',
|
||||
);
|
||||
// Preserve context across intermediate navigations (scoped to active tab)
|
||||
this.#tabService.patchTabMetadata(tabId, {
|
||||
'select-customer': { returnUrl },
|
||||
});
|
||||
|
||||
this.#router.navigate(['/', 'kunde', tabId, 'customer', 'search', customerId]);
|
||||
}
|
||||
|
||||
payer = computed(() => {
|
||||
|
||||
@@ -60,6 +60,11 @@ export class CompleteOrderButtonComponent {
|
||||
return calculateTotalLoyaltyPoints(cart?.items);
|
||||
});
|
||||
|
||||
hasItemsInCart = computed(() => {
|
||||
const cart = this.#shoppingCartResource.value();
|
||||
return cart && cart?.items?.length > 0;
|
||||
});
|
||||
|
||||
hasInsufficientPoints = computed(() => {
|
||||
return this.primaryBonusCardPoints() < this.totalPointsRequired();
|
||||
});
|
||||
@@ -104,6 +109,7 @@ export class CompleteOrderButtonComponent {
|
||||
this.isBusy() ||
|
||||
this.isCompleted() ||
|
||||
this.isLoading() ||
|
||||
!this.hasItemsInCart() ||
|
||||
this.hasInsufficientPoints()
|
||||
);
|
||||
});
|
||||
|
||||
@@ -66,7 +66,8 @@ export class RewardShoppingCartItemQuantityControlComponent {
|
||||
if (
|
||||
orderType === OrderTypeFeature.Delivery ||
|
||||
orderType === OrderTypeFeature.DigitalShipping ||
|
||||
orderType === OrderTypeFeature.B2BShipping
|
||||
orderType === OrderTypeFeature.B2BShipping ||
|
||||
orderType === OrderTypeFeature.Pickup
|
||||
) {
|
||||
return 999;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@if (isHorizontal()) {
|
||||
<checkout-destination-info
|
||||
[underline]="true"
|
||||
class="cursor-pointer max-w-[14.25rem] grow-0 shrink-0"
|
||||
class="cursor-pointer max-w-[14.25rem] min-w-[14.25rem] grow-0 shrink-0"
|
||||
(click)="updatePurchaseOption()"
|
||||
[shoppingCartItem]="itm"
|
||||
></checkout-destination-info>
|
||||
@@ -29,7 +29,7 @@
|
||||
@if (!isHorizontal()) {
|
||||
<checkout-destination-info
|
||||
[underline]="true"
|
||||
class="cursor-pointer mt-4 w-[14.25rem] grow items-end"
|
||||
class="cursor-pointer mt-4 w-[14.25rem] min-w-[14.25rem] grow items-end"
|
||||
(click)="updatePurchaseOption()"
|
||||
[shoppingCartItem]="itm"
|
||||
></checkout-destination-info>
|
||||
@@ -37,11 +37,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (quantityControl.maxQuantity() < 2 && !isDownload()) {
|
||||
<div
|
||||
class="text-isa-accent-red isa-text-body-2-bold flex flex-row items-center gap-2"
|
||||
>
|
||||
<ng-icon name="isaOtherInfo" size="1.5rem"></ng-icon>
|
||||
<div>Geringer Bestand - Artikel holen vor Abschluss</div>
|
||||
</div>
|
||||
@if (!isDownload()) {
|
||||
@if (showLowStockMessage()) {
|
||||
<div
|
||||
class="text-isa-accent-red isa-text-body-2-bold flex flex-row items-center gap-2"
|
||||
>
|
||||
<ng-icon name="isaOtherInfo" size="1.5rem"></ng-icon>
|
||||
<div>{{ inStock() }} Exemplare sofort lieferbar</div>
|
||||
</div>
|
||||
} @else if (quantityControl.maxQuantity() < 2) {
|
||||
<div
|
||||
class="text-isa-accent-red isa-text-body-2-bold flex flex-row items-center gap-2"
|
||||
>
|
||||
<ng-icon name="isaOtherInfo" size="1.5rem"></ng-icon>
|
||||
<div>Geringer Bestand - Artikel holen vor Abschluss</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +72,20 @@ export class RewardShoppingCartItemComponent {
|
||||
hasOrderTypeFeature(this.item().features, ['Download']),
|
||||
);
|
||||
|
||||
isAbholung = computed(() =>
|
||||
hasOrderTypeFeature(this.item().features, ['Abholung']),
|
||||
);
|
||||
|
||||
inStock = computed(() => this.item().availability?.inStock ?? 0);
|
||||
|
||||
showLowStockMessage = computed(() => {
|
||||
return this.isAbholung() && this.inStock() < 2;
|
||||
});
|
||||
|
||||
async updatePurchaseOption() {
|
||||
const shoppingCartItemId = this.itemId();
|
||||
const shoppingCartId = this.shoppingCartId();
|
||||
const branch = this.item().destination?.data?.targetBranch?.data;
|
||||
|
||||
if (this.isBusy() || !shoppingCartId || !shoppingCartItemId) {
|
||||
return;
|
||||
@@ -90,6 +101,8 @@ export class RewardShoppingCartItemComponent {
|
||||
useRedemptionPoints: true,
|
||||
disabledPurchaseOptions: ['b2b-delivery'],
|
||||
hideDisabledPurchaseOptions: true,
|
||||
pickupBranch: branch,
|
||||
inStoreBranch: branch,
|
||||
});
|
||||
|
||||
await firstValueFrom(ref.afterClosed$);
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
<div class="flex flex-col gap-2 mb-4 text-center">
|
||||
<h1 class="isa-text-subtitle-1-regular text-isa-black">Prämienausgabe</h1>
|
||||
<p class="isa-text-body-2-regular text-isa-secondary-900">
|
||||
Kontrolliere Sie Lieferart und Versand um die Prämienausgabe abzuschließen.
|
||||
<br />
|
||||
Sie können Prämien unter folgendem Link zurück in den Warenkorb legen:
|
||||
Kontrollieren Sie Lieferart und Versand um die Prämienausgabe abzuschließen.
|
||||
@if (hasItemsInCart()) {
|
||||
<br />
|
||||
Sie können Prämien unter folgendem Link zurück in den Warenkorb legen:
|
||||
}
|
||||
</p>
|
||||
<lib-reward-selection-trigger></lib-reward-selection-trigger>
|
||||
</div>
|
||||
@@ -26,7 +28,19 @@
|
||||
</div>
|
||||
}
|
||||
<checkout-billing-and-shipping-address-card></checkout-billing-and-shipping-address-card>
|
||||
<checkout-reward-shopping-cart-items></checkout-reward-shopping-cart-items>
|
||||
|
||||
@if (hasItemsInCart()) {
|
||||
<checkout-reward-shopping-cart-items></checkout-reward-shopping-cart-items>
|
||||
} @else {
|
||||
<ui-empty-state
|
||||
class="w-full self-center"
|
||||
[appearance]="EmptyStateAppearance.NoArticles"
|
||||
title="Leere Liste"
|
||||
description="Es befinden sich keine Artikel auf der Liste."
|
||||
>
|
||||
</ui-empty-state>
|
||||
}
|
||||
|
||||
<checkout-complete-order-button
|
||||
class="fixed bottom-6 right-6"
|
||||
></checkout-complete-order-button>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { CompleteOrderButtonComponent } from './complete-order-button/complete-o
|
||||
import { RewardSelectionTriggerComponent } from '@isa/checkout/shared/reward-selection-dialog';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaOtherInfo } from '@isa/icons';
|
||||
import { EmptyStateComponent, EmptyStateAppearance } from '@isa/ui/empty-state';
|
||||
|
||||
@Component({
|
||||
selector: 'checkout-reward-shopping-cart',
|
||||
@@ -34,10 +35,12 @@ import { isaOtherInfo } from '@isa/icons';
|
||||
CompleteOrderButtonComponent,
|
||||
RewardSelectionTriggerComponent,
|
||||
NgIconComponent,
|
||||
EmptyStateComponent,
|
||||
],
|
||||
providers: [provideIcons({ isaOtherInfo })],
|
||||
})
|
||||
export class RewardShoppingCartComponent {
|
||||
EmptyStateAppearance = EmptyStateAppearance;
|
||||
#shoppingCartResource = inject(SelectedRewardShoppingCartResource).resource;
|
||||
#primaryCustomerCardResource = inject(PrimaryCustomerCardResource);
|
||||
|
||||
@@ -55,6 +58,11 @@ export class RewardShoppingCartComponent {
|
||||
return this.primaryBonusCardPoints() < this.totalPointsRequired();
|
||||
});
|
||||
|
||||
hasItemsInCart = computed(() => {
|
||||
const cart = this.#shoppingCartResource.value();
|
||||
return cart && cart?.items?.length > 0;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.#shoppingCartResource.reload();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { RewardShoppingCartComponent } from './reward-shopping-cart.component';
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
title: 'Prämienshop - Warenkorb',
|
||||
component: RewardShoppingCartComponent,
|
||||
},
|
||||
];
|
||||
|
||||
460
libs/checkout/feature/select-branch-dropdown/README.md
Normal file
460
libs/checkout/feature/select-branch-dropdown/README.md
Normal file
@@ -0,0 +1,460 @@
|
||||
# @isa/checkout/feature/select-branch-dropdown
|
||||
|
||||
Branch selection dropdown components for the checkout domain, enabling users to select a branch for their checkout session.
|
||||
|
||||
## Overview
|
||||
|
||||
The Select Branch Dropdown feature library provides two UI components for branch selection in the checkout flow:
|
||||
|
||||
- **SelectedBranchDropdownComponent** - High-level component that integrates with `CheckoutMetadataService` for automatic persistence
|
||||
- **BranchDropdownComponent** - Low-level component with `ControlValueAccessor` support for custom form integration
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Component API Reference](#component-api-reference)
|
||||
- [Usage Examples](#usage-examples)
|
||||
- [State Management](#state-management)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Testing](#testing)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Features
|
||||
|
||||
- **Branch Selection** - Dropdown interface for selecting a branch
|
||||
- **Metadata Integration** - Persists selected branch in checkout metadata via `CheckoutMetadataService.setSelectedBranch()`
|
||||
- **Standalone Component** - Modern Angular standalone architecture
|
||||
- **Responsive Design** - Works across tablet and desktop layouts
|
||||
- **E2E Testing Support** - Includes data-what and data-which attributes
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Import the Component
|
||||
|
||||
```typescript
|
||||
import { SelectedBranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';
|
||||
|
||||
@Component({
|
||||
selector: 'app-checkout',
|
||||
imports: [SelectedBranchDropdownComponent],
|
||||
template: `
|
||||
<checkout-selected-branch-dropdown [tabId]="tabId()" />
|
||||
`
|
||||
})
|
||||
export class CheckoutComponent {
|
||||
tabId = injectTabId();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Using the Low-Level Component with Forms
|
||||
|
||||
```typescript
|
||||
import { BranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';
|
||||
|
||||
@Component({
|
||||
selector: 'app-branch-form',
|
||||
imports: [BranchDropdownComponent, ReactiveFormsModule],
|
||||
template: `
|
||||
<checkout-branch-dropdown formControlName="branch" />
|
||||
`
|
||||
})
|
||||
export class BranchFormComponent {}
|
||||
```
|
||||
|
||||
## Component API Reference
|
||||
|
||||
### SelectedBranchDropdownComponent
|
||||
|
||||
High-level branch selection component with automatic `CheckoutMetadataService` integration.
|
||||
|
||||
**Selector:** `checkout-selected-branch-dropdown`
|
||||
|
||||
**Inputs:**
|
||||
| Input | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `tabId` | `number` | Yes | - | The tab ID for metadata storage |
|
||||
| `appearance` | `DropdownAppearance` | No | `AccentOutline` | Visual style of the dropdown |
|
||||
|
||||
**Example:**
|
||||
```html
|
||||
<checkout-selected-branch-dropdown
|
||||
[tabId]="tabId()"
|
||||
[appearance]="DropdownAppearance.AccentOutline"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### BranchDropdownComponent
|
||||
|
||||
Low-level branch dropdown with `ControlValueAccessor` support for reactive forms.
|
||||
|
||||
**Selector:** `checkout-branch-dropdown`
|
||||
|
||||
**Inputs:**
|
||||
| Input | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `appearance` | `DropdownAppearance` | No | `AccentOutline` | Visual style of the dropdown |
|
||||
|
||||
**Outputs:**
|
||||
| Output | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `selected` | `Branch \| null` | Two-way bindable selected branch (model) |
|
||||
|
||||
**Example:**
|
||||
```html
|
||||
<!-- With ngModel -->
|
||||
<checkout-branch-dropdown [(selected)]="selectedBranch" />
|
||||
|
||||
<!-- With reactive forms -->
|
||||
<checkout-branch-dropdown formControlName="branch" />
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage in Checkout Flow
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { SelectedBranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-checkout-header',
|
||||
imports: [SelectedBranchDropdownComponent],
|
||||
template: `
|
||||
<div class="checkout-header">
|
||||
<h1>Checkout</h1>
|
||||
<checkout-selected-branch-dropdown [tabId]="tabId()" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class CheckoutHeaderComponent {
|
||||
tabId = injectTabId();
|
||||
}
|
||||
```
|
||||
|
||||
### Using BranchDropdownComponent with Reactive Forms
|
||||
|
||||
```typescript
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BranchDropdownComponent } from '@isa/checkout/feature/select-branch-dropdown';
|
||||
|
||||
@Component({
|
||||
selector: 'app-branch-form-example',
|
||||
imports: [BranchDropdownComponent, ReactiveFormsModule],
|
||||
template: `
|
||||
<form [formGroup]="form">
|
||||
<checkout-branch-dropdown formControlName="branch" />
|
||||
</form>
|
||||
`
|
||||
})
|
||||
export class BranchFormExampleComponent {
|
||||
#fb = inject(FormBuilder);
|
||||
form = this.#fb.group({
|
||||
branch: [null]
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Reading Selected Branch from Metadata
|
||||
|
||||
```typescript
|
||||
import { Component, inject, computed } from '@angular/core';
|
||||
import { CheckoutMetadataService } from '@isa/checkout/data-access';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-branch-reader-example',
|
||||
template: `
|
||||
@if (selectedBranch(); as branch) {
|
||||
<p>Selected Branch: {{ branch.name }}</p>
|
||||
} @else {
|
||||
<p>No branch selected</p>
|
||||
}
|
||||
`
|
||||
})
|
||||
export class BranchReaderExampleComponent {
|
||||
#checkoutMetadata = inject(CheckoutMetadataService);
|
||||
tabId = injectTabId();
|
||||
|
||||
selectedBranch = computed(() => {
|
||||
return this.#checkoutMetadata.getSelectedBranch(this.tabId()!);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### CheckoutMetadataService Integration
|
||||
|
||||
The component integrates with `CheckoutMetadataService` from `@isa/checkout/data-access`:
|
||||
|
||||
```typescript
|
||||
// Set selected branch (stores the full Branch object)
|
||||
this.#checkoutMetadata.setSelectedBranch(tabId, branch);
|
||||
|
||||
// Get selected branch (returns Branch | undefined)
|
||||
const branch = this.#checkoutMetadata.getSelectedBranch(tabId);
|
||||
|
||||
// Clear selected branch
|
||||
this.#checkoutMetadata.setSelectedBranch(tabId, undefined);
|
||||
```
|
||||
|
||||
**State Persistence:**
|
||||
- Branch selection persists in tab metadata
|
||||
- Available across all checkout components in the same tab
|
||||
- Cleared when tab is closed or session ends
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Required Libraries
|
||||
|
||||
#### Angular Core
|
||||
- `@angular/core` - Angular framework
|
||||
- `@angular/forms` - Form controls and ControlValueAccessor
|
||||
|
||||
#### ISA Feature Libraries
|
||||
- `@isa/checkout/data-access` - CheckoutMetadataService, BranchesResource, Branch type
|
||||
- `@isa/core/logging` - Logger utility
|
||||
|
||||
#### ISA UI Libraries
|
||||
- `@isa/ui/input-controls` - DropdownButtonComponent, DropdownFilterComponent, DropdownOptionComponent
|
||||
|
||||
### Path Alias
|
||||
|
||||
Import from: `@isa/checkout/feature/select-branch-dropdown`
|
||||
|
||||
```typescript
|
||||
// Import components
|
||||
import {
|
||||
SelectedBranchDropdownComponent,
|
||||
BranchDropdownComponent
|
||||
} from '@isa/checkout/feature/select-branch-dropdown';
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The library uses **Vitest** with **Angular Testing Utilities** for testing.
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run tests for this library
|
||||
npx nx test checkout-feature-select-branch-dropdown --skip-nx-cache
|
||||
|
||||
# Run tests with coverage
|
||||
npx nx test checkout-feature-select-branch-dropdown --coverage.enabled=true --skip-nx-cache
|
||||
|
||||
# Run tests in watch mode
|
||||
npx nx test checkout-feature-select-branch-dropdown --watch
|
||||
```
|
||||
|
||||
### Test Configuration
|
||||
|
||||
- **Framework:** Vitest
|
||||
- **Test Runner:** @nx/vite:test
|
||||
- **Coverage:** v8 provider with Cobertura reports
|
||||
- **JUnit XML:** `testresults/junit-checkout-feature-select-branch-dropdown.xml`
|
||||
- **Coverage XML:** `coverage/libs/checkout/feature/select-branch-dropdown/cobertura-coverage.xml`
|
||||
|
||||
### Testing Recommendations
|
||||
|
||||
#### Component Testing
|
||||
|
||||
```typescript
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { SelectedBranchDropdownComponent } from './selected-branch-dropdown.component';
|
||||
import { CheckoutMetadataService, BranchesResource } from '@isa/checkout/data-access';
|
||||
import { signal } from '@angular/core';
|
||||
|
||||
describe('SelectedBranchDropdownComponent', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [SelectedBranchDropdownComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: CheckoutMetadataService,
|
||||
useValue: {
|
||||
setSelectedBranch: vi.fn(),
|
||||
getSelectedBranch: vi.fn(() => null)
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: BranchesResource,
|
||||
useValue: {
|
||||
resource: {
|
||||
hasValue: () => true,
|
||||
value: () => []
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
const fixture = TestBed.createComponent(SelectedBranchDropdownComponent);
|
||||
fixture.componentRef.setInput('tabId', 1);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance).toBeTruthy();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Attribute Requirements
|
||||
|
||||
All interactive elements should include `data-what` and `data-which` attributes:
|
||||
|
||||
```html
|
||||
<!-- Dropdown -->
|
||||
<select
|
||||
data-what="branch-dropdown"
|
||||
data-which="branch-selection">
|
||||
</select>
|
||||
|
||||
<!-- Option -->
|
||||
<option
|
||||
data-what="branch-option"
|
||||
[attr.data-which]="branch.id">
|
||||
</option>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Use Tab Context
|
||||
|
||||
Ensure tab context is available when using the component:
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
const tabId = this.#tabService.activatedTabId();
|
||||
if (tabId) {
|
||||
this.#checkoutMetadata.setSelectedBranch(tabId, branch);
|
||||
}
|
||||
|
||||
// Bad (missing tab context validation)
|
||||
this.#checkoutMetadata.setSelectedBranch(null, branch);
|
||||
```
|
||||
|
||||
### 2. Clear Branch on Checkout Completion
|
||||
|
||||
Always clear branch selection when checkout is complete:
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
completeCheckout() {
|
||||
// ... checkout logic
|
||||
this.#checkoutMetadata.setSelectedBranch(tabId, undefined);
|
||||
}
|
||||
|
||||
// Bad (leaves stale branch)
|
||||
completeCheckout() {
|
||||
// ... checkout logic
|
||||
// Forgot to clear branch!
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use Computed Signals for Derived State
|
||||
|
||||
Leverage Angular signals for reactive values:
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
selectedBranch = computed(() => {
|
||||
return this.#checkoutMetadata.getSelectedBranch(this.tabId()!);
|
||||
});
|
||||
|
||||
// Bad (manual tracking)
|
||||
selectedBranch: Branch | undefined;
|
||||
ngOnInit() {
|
||||
effect(() => {
|
||||
this.selectedBranch = this.#checkoutMetadata.getSelectedBranch(this.tabId()!);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Follow OnPush Change Detection
|
||||
|
||||
Always use OnPush change detection for performance:
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
@Component({
|
||||
selector: 'checkout-select-branch-dropdown',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
// ...
|
||||
})
|
||||
|
||||
// Bad (default change detection)
|
||||
@Component({
|
||||
selector: 'checkout-select-branch-dropdown',
|
||||
// Missing changeDetection
|
||||
})
|
||||
```
|
||||
|
||||
### 5. Add E2E Attributes
|
||||
|
||||
Always include data-what and data-which attributes:
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
<select
|
||||
data-what="branch-dropdown"
|
||||
data-which="branch-selection"
|
||||
[(ngModel)]="selectedBranch">
|
||||
</select>
|
||||
|
||||
// Bad (no E2E attributes)
|
||||
<select [(ngModel)]="selectedBranch">
|
||||
</select>
|
||||
```
|
||||
|
||||
## Architecture Notes
|
||||
|
||||
### Component Structure
|
||||
|
||||
```
|
||||
SelectedBranchDropdownComponent (high-level, with metadata integration)
|
||||
├── Uses BranchDropdownComponent internally
|
||||
├── CheckoutMetadataService integration
|
||||
├── Tab context via tabId input
|
||||
└── Automatic branch persistence
|
||||
|
||||
BranchDropdownComponent (low-level, form-compatible)
|
||||
├── ControlValueAccessor implementation
|
||||
├── BranchesResource for loading options
|
||||
├── DropdownButtonComponent from @isa/ui/input-controls
|
||||
└── Filter support via DropdownFilterComponent
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
SelectedBranchDropdownComponent:
|
||||
1. Component receives tabId input
|
||||
↓
|
||||
2. Reads current branch from CheckoutMetadataService.getSelectedBranch(tabId)
|
||||
↓
|
||||
3. User selects branch from dropdown
|
||||
↓
|
||||
4. setSelectedBranch(tabId, branch) called
|
||||
↓
|
||||
5. Other checkout components react to metadata change
|
||||
|
||||
BranchDropdownComponent:
|
||||
1. BranchesResource loads available branches
|
||||
↓
|
||||
2. User selects branch from dropdown
|
||||
↓
|
||||
3. selected model emits new value
|
||||
↓
|
||||
4. ControlValueAccessor notifies form (if used with forms)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Internal ISA Frontend library - not for external distribution.
|
||||
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'checkout',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'checkout',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
20
libs/checkout/feature/select-branch-dropdown/project.json
Normal file
20
libs/checkout/feature/select-branch-dropdown/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "checkout-feature-select-branch-dropdown",
|
||||
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/checkout/feature/select-branch-dropdown/src",
|
||||
"prefix": "checkout",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:checkout", "type:feature"],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"reportsDirectory": "../../../../coverage/libs/checkout/feature/select-branch-dropdown"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './lib/branch-dropdown.component';
|
||||
export * from './lib/selected-branch-dropdown.component';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user